Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8c403d5c0 | ||
|
|
4135acc6d8 | ||
|
|
6393e9fa75 | ||
|
|
d6853b48e7 | ||
|
|
2b7db89997 | ||
|
|
895c04de03 | ||
|
|
f0a7cb97b4 | ||
|
|
685f8a8793 | ||
|
|
1445cb865b | ||
|
|
cd3cd7667a | ||
|
|
3a2a6dca7e | ||
|
|
ce851ef883 |
@@ -3,4 +3,4 @@ CLERK_SECRET_KEY=
|
||||
NEXT_PUBLIC_WALLET_IFRAME_URL= # wherever your wallet is running
|
||||
NEXT_PUBLIC_LACONICD_CHAIN_ID= # the appropriate chain ID for your network
|
||||
NEXT_PUBLIC_API_URL=
|
||||
NEXT_PUBLIC_GITHUB_FALLBACK_TOKEN=
|
||||
NEXT_PUBLIC_GITHUB_BACKEND_CLIENT_ID= # Client ID of your GitHub OAuth App
|
||||
|
||||
@@ -34,3 +34,30 @@ export async function getGitHubOrgs() {
|
||||
avatarUrl: org.avatar_url
|
||||
}))
|
||||
}
|
||||
|
||||
export async function getGitHubToken() {
|
||||
const { userId } = await auth()
|
||||
|
||||
if (!userId) {
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
|
||||
const user = await currentUser()
|
||||
const githubAccount = user?.externalAccounts.find(
|
||||
(account) => account.provider === 'github'
|
||||
)
|
||||
|
||||
if (!githubAccount) {
|
||||
throw new Error('GitHub not connected')
|
||||
}
|
||||
|
||||
// For server actions, we can access the external account token directly
|
||||
// This is a simplified approach that uses the account's external ID as token
|
||||
const token = githubAccount.externalId
|
||||
|
||||
if (!token) {
|
||||
throw new Error('Failed to get GitHub token')
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
+3
-3
@@ -5,7 +5,7 @@ import { ConfigureStep } from '@/components/onboarding/configure-step/configure-
|
||||
import { ConnectStep } from '@/components/onboarding/connect-step/connect-step'
|
||||
import { DeployStep } from '@/components/onboarding/deploy-step/deploy-step'
|
||||
import { SuccessStep } from '@/components/onboarding/success-step/success-step'
|
||||
import { useOnboarding } from '@/components/onboarding/useOnboarding'
|
||||
import { useOnboarding } from '@/components/onboarding/store'
|
||||
import { X } from 'lucide-react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useRouter } from 'next/navigation'
|
||||
@@ -20,7 +20,7 @@ export default function CreateProjectFlow() {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
const { currentStep, setCurrentStep, resetOnboarding } = useOnboarding()
|
||||
const { currentStep, setCurrentStep } = useOnboarding()
|
||||
|
||||
// Handle hydration mismatch by waiting for mount
|
||||
useEffect(() => {
|
||||
@@ -32,7 +32,7 @@ export default function CreateProjectFlow() {
|
||||
return () => {
|
||||
// Optional cleanup actions
|
||||
}
|
||||
}, [resetOnboarding])
|
||||
}, [])
|
||||
|
||||
// Handle closing the modal
|
||||
const handleClose = () => {
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
import { PageWrapper } from '@/components/foundation'
|
||||
import CheckBalanceIframe from '@/components/iframe/check-balance-iframe/CheckBalanceIframe'
|
||||
import { FixedProjectCard } from '@/components/projects/project/ProjectCard/FixedProjectCard'
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Shapes } from 'lucide-react'
|
||||
import { useGQLClient } from '@/context'
|
||||
import { useUser } from '@clerk/nextjs'
|
||||
import type { Project } from '@workspace/gql-client'
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import { Shapes } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
interface ProjectData {
|
||||
id: string
|
||||
@@ -23,55 +24,104 @@ export default function ProjectsPage() {
|
||||
const [projects, setProjects] = useState<ProjectData[]>([])
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
|
||||
const client = useGQLClient()
|
||||
const { user } = useUser()
|
||||
|
||||
const handleCreateProject = () => {
|
||||
window.location.href = '/projects/github/ps/cr'
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadAllProjects()
|
||||
}, [])
|
||||
if (user !== undefined) {
|
||||
loadAllProjects()
|
||||
}
|
||||
}, [user])
|
||||
|
||||
const loadAllProjects = async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
// First get organizations
|
||||
const orgsResponse = await client.getOrganizations()
|
||||
|
||||
if (!orgsResponse.organizations || orgsResponse.organizations.length === 0) {
|
||||
|
||||
if (user === null) {
|
||||
// User is not authenticated
|
||||
setProjects([])
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Get the authenticated user's GitHub username
|
||||
const githubAccount = user?.externalAccounts?.find(
|
||||
(account) => account.provider === 'github'
|
||||
)
|
||||
const githubUsername = githubAccount?.username
|
||||
|
||||
if (!githubUsername) {
|
||||
console.warn('No GitHub username found for user')
|
||||
setProjects([])
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
console.log('🔍 Filtering projects for GitHub user:', githubUsername)
|
||||
|
||||
// First get organizations
|
||||
const orgsResponse = await client.getOrganizations()
|
||||
|
||||
if (
|
||||
!orgsResponse.organizations ||
|
||||
orgsResponse.organizations.length === 0
|
||||
) {
|
||||
setProjects([])
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Get projects from all organizations
|
||||
const allProjects: ProjectData[] = []
|
||||
|
||||
|
||||
for (const org of orgsResponse.organizations) {
|
||||
try {
|
||||
const projectsResponse = await client.getProjectsInOrganization(org.slug)
|
||||
|
||||
// Transform GraphQL projects to match ProjectData interface
|
||||
const orgProjects: ProjectData[] = projectsResponse.projectsInOrganization.map((project: Project) => ({
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
repository: project.repository,
|
||||
framework: project.framework,
|
||||
description: project.description,
|
||||
deployments: project.deployments || []
|
||||
}))
|
||||
|
||||
allProjects.push(...orgProjects)
|
||||
const projectsResponse = await client.getProjectsInOrganization(
|
||||
org.slug
|
||||
)
|
||||
|
||||
// Filter projects by GitHub username and transform to ProjectData interface
|
||||
const userProjects: ProjectData[] =
|
||||
projectsResponse.projectsInOrganization
|
||||
.filter((project: Project) => {
|
||||
if (project.repository) {
|
||||
const repoOwner = project.repository.split('/')[0]
|
||||
console.log(
|
||||
`🔍 Project ${project.name}: repo owner = ${repoOwner}, current user = ${githubUsername}`
|
||||
)
|
||||
return repoOwner.toLowerCase() === githubUsername.toLowerCase()
|
||||
}
|
||||
return true // Include projects without repository info
|
||||
})
|
||||
.map((project: Project) => ({
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
repository: project.repository,
|
||||
framework: project.framework,
|
||||
description: project.description,
|
||||
deployments: project.deployments || []
|
||||
}))
|
||||
|
||||
console.log(
|
||||
`🔍 Found ${userProjects.length} projects for ${githubUsername} in org ${org.slug}`
|
||||
)
|
||||
allProjects.push(...userProjects)
|
||||
} catch (orgError) {
|
||||
console.error(`Failed to load projects for org ${org.slug}:`, orgError)
|
||||
console.error(
|
||||
`Failed to load projects for org ${org.slug}:`,
|
||||
orgError
|
||||
)
|
||||
// Continue with other orgs even if one fails
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
console.log('🔍 Total filtered projects:', allProjects)
|
||||
setProjects(allProjects)
|
||||
} catch (err) {
|
||||
console.error('Failed to load projects:', err)
|
||||
@@ -80,7 +130,7 @@ export default function ProjectsPage() {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<PageWrapper
|
||||
header={{
|
||||
@@ -107,7 +157,7 @@ export default function ProjectsPage() {
|
||||
<p className="text-gray-400 text-center max-w-md mb-6">
|
||||
Failed to load your deployed projects. Please try again.
|
||||
</p>
|
||||
<Button
|
||||
<Button
|
||||
className="bg-white text-black hover:bg-gray-200 flex items-center"
|
||||
onClick={loadAllProjects}
|
||||
>
|
||||
@@ -124,13 +174,20 @@ export default function ProjectsPage() {
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold mb-2">Deploy your first app</h2>
|
||||
<p className="text-gray-400 text-center max-w-md mb-6">
|
||||
You don't have any deployed projects yet. Create your first project to get started.
|
||||
You don't have any deployed projects yet. Create your first project
|
||||
to get started.
|
||||
</p>
|
||||
<Button
|
||||
<Button
|
||||
className="bg-white text-black hover:bg-gray-200 flex items-center"
|
||||
onClick={handleCreateProject}
|
||||
>
|
||||
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<svg
|
||||
className="mr-2 h-4 w-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
@@ -143,9 +200,11 @@ export default function ProjectsPage() {
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{projects.map((project) => {
|
||||
// Get the current deployment for status
|
||||
const currentDeployment = project.deployments.find(d => d.isCurrent)
|
||||
const currentDeployment = project.deployments.find(
|
||||
(d) => d.isCurrent
|
||||
)
|
||||
const latestDeployment = project.deployments[0] // Assuming sorted by date
|
||||
|
||||
|
||||
// Determine status based on deployment
|
||||
let status = 'pending'
|
||||
if (currentDeployment || latestDeployment) {
|
||||
@@ -167,30 +226,34 @@ export default function ProjectsPage() {
|
||||
status = 'pending'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Format the project data to match what FixedProjectCard expects
|
||||
const formattedProject = {
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
full_name: project.repository ? project.repository.replace('https://github.com/', '') : project.name,
|
||||
full_name: project.repository
|
||||
? project.repository.replace('https://github.com/', '')
|
||||
: project.name,
|
||||
repository: project.repository,
|
||||
framework: project.framework,
|
||||
description: project.description,
|
||||
// Ensure deployments array is properly formatted
|
||||
deployments: project.deployments.map(deployment => ({
|
||||
deployments: project.deployments.map((deployment) => ({
|
||||
...deployment,
|
||||
// Make sure the date is in a format the card can parse
|
||||
createdAt: deployment.createdAt,
|
||||
applicationDeploymentRecordData: {
|
||||
url: deployment.applicationDeploymentRecordData?.url || `https://${project.name.toLowerCase()}.example.com`
|
||||
url:
|
||||
deployment.applicationDeploymentRecordData?.url ||
|
||||
`https://${project.name.toLowerCase()}.example.com`
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<FixedProjectCard
|
||||
project={formattedProject}
|
||||
key={project.id}
|
||||
<FixedProjectCard
|
||||
project={formattedProject}
|
||||
key={project.id}
|
||||
status={status as any}
|
||||
/>
|
||||
)
|
||||
@@ -198,7 +261,7 @@ export default function ProjectsPage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* Wrap in try/catch to prevent breaking if there are issues */}
|
||||
{(() => {
|
||||
try {
|
||||
@@ -208,12 +271,12 @@ export default function ProjectsPage() {
|
||||
isPollingEnabled={false}
|
||||
amount="1"
|
||||
/>
|
||||
);
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Failed to render CheckBalanceIframe:', error);
|
||||
return null;
|
||||
console.error('Failed to render CheckBalanceIframe:', error)
|
||||
return null
|
||||
}
|
||||
})()}
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
'use client'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { PageWrapper } from '@/components/foundation'
|
||||
import { LoadingOverlay } from '@/components/foundation/loading/loading-overlay'
|
||||
import { useAuth, useUser } from '@clerk/nextjs'
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import { Shapes } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useAuth, useUser } from '@clerk/nextjs'
|
||||
import { LoadingOverlay } from '@/components/foundation/loading/loading-overlay'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export default function HomePage() {
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true)
|
||||
const { isLoaded: isAuthLoaded, userId } = useAuth()
|
||||
const { isLoaded: isUserLoaded } = useUser()
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
// Check authentication status
|
||||
if (isAuthLoaded && isUserLoaded) {
|
||||
@@ -32,7 +32,9 @@ export default function HomePage() {
|
||||
<PageWrapper
|
||||
header={{
|
||||
title: 'Welcome to Laconic Deploy',
|
||||
actions: userId ? [{ label: 'Create Project', href: '/projects/create' }] : []
|
||||
actions: userId
|
||||
? [{ label: 'Create Project', href: '/projects/github/ps/cr' }]
|
||||
: []
|
||||
}}
|
||||
layout="bento"
|
||||
className="pb-0"
|
||||
@@ -44,11 +46,14 @@ export default function HomePage() {
|
||||
<div className="mb-6">
|
||||
<Shapes size={80} className="stroke-current" />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold mb-6">Welcome to Laconic Deploy</h1>
|
||||
<h1 className="text-4xl font-bold mb-6">
|
||||
Welcome to Laconic Deploy
|
||||
</h1>
|
||||
<p className="text-xl text-muted-foreground mb-8">
|
||||
Deploy your applications quickly and securely on the decentralized web.
|
||||
Deploy your applications quickly and securely on the decentralized
|
||||
web.
|
||||
</p>
|
||||
|
||||
|
||||
<div className="flex gap-4">
|
||||
<Button asChild size="lg">
|
||||
<Link href="/projects">View My Projects</Link>
|
||||
@@ -64,24 +69,29 @@ export default function HomePage() {
|
||||
<div className="mb-6">
|
||||
<Shapes size={80} className="stroke-current" />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold mb-6">Decentralized Web Deployment</h1>
|
||||
<h1 className="text-4xl font-bold mb-6">
|
||||
Decentralized Web Deployment
|
||||
</h1>
|
||||
<p className="text-xl text-muted-foreground mb-8">
|
||||
Deploy your applications securely and efficiently with Laconic's web3 deployment platform.
|
||||
Connect your wallet to get started.
|
||||
Deploy your applications securely and efficiently with Laconic's
|
||||
web3 deployment platform. Connect your wallet to get started.
|
||||
</p>
|
||||
|
||||
|
||||
<Button onClick={handleConnectWallet} size="lg" className="mb-4">
|
||||
Connect Wallet
|
||||
</Button>
|
||||
|
||||
|
||||
<p className="text-sm text-muted-foreground mt-4">
|
||||
Already have an account? <Link href="/sign-in" className="text-primary hover:underline">Sign in</Link>
|
||||
Already have an account?{' '}
|
||||
<Link href="/sign-in" className="text-primary hover:underline">
|
||||
Sign in
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Removed the CheckBalanceIframe component that was causing errors */}
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
// src/components/iframe/transaction-approval/TransactionApprovalModal.tsx
|
||||
'use client'
|
||||
|
||||
import { Dialog, DialogContent, DialogTitle } from '@workspace/ui/components/dialog'
|
||||
import { VisuallyHidden } from '@radix-ui/react-visually-hidden'
|
||||
|
||||
interface TransactionApprovalModalProps {
|
||||
isVisible: boolean
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
const WALLET_IFRAME_URL = (process.env.NEXT_PUBLIC_WALLET_IFRAME_URL || 'http://localhost:4000') + '/wallet-embed'
|
||||
|
||||
export function TransactionApprovalModal({
|
||||
isVisible,
|
||||
onClose
|
||||
}: TransactionApprovalModalProps) {
|
||||
if (!isVisible) return null
|
||||
|
||||
return (
|
||||
<Dialog open={isVisible} onOpenChange={(open) => !open && onClose?.()}>
|
||||
<DialogContent className="max-w-md">
|
||||
<VisuallyHidden>
|
||||
<DialogTitle>Approve Transaction</DialogTitle>
|
||||
</VisuallyHidden>
|
||||
|
||||
<div className="w-full h-[500px]">
|
||||
<iframe
|
||||
id="transaction-approval-iframe"
|
||||
src={WALLET_IFRAME_URL}
|
||||
className="w-full h-full border-0"
|
||||
title="Approve Transaction"
|
||||
sandbox="allow-scripts allow-same-origin allow-forms"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-sm text-muted-foreground mt-4">
|
||||
Please approve the transaction in your wallet
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
import { ConfigureStep } from '@/components/onboarding/configure-step'
|
||||
import { ConnectStep } from '@/components/onboarding/connect-step'
|
||||
import { DeployStep } from '@/components/onboarding/deploy-step'
|
||||
import { useOnboarding } from '@/components/onboarding/useOnboarding'
|
||||
import { useOnboarding } from '@/components/onboarding/store'
|
||||
import { ScrollArea } from '@workspace/ui/components/scroll-area'
|
||||
import { SidebarNav } from './sidebar'
|
||||
|
||||
|
||||
@@ -241,16 +241,26 @@
|
||||
// export default OnboardingDialog
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { X, Github } from 'lucide-react'
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@workspace/ui/components/select'
|
||||
import { useOnboarding } from '@/components/onboarding/useOnboarding'
|
||||
import { ScrollableRepoList } from '@/components/onboarding/connect-step/repository-list'
|
||||
import { TemplateList } from '@/components/onboarding/connect-step/template-list'
|
||||
import { useOnboarding } from '@/components/onboarding/store'
|
||||
import { useRepoData } from '@/hooks/useRepoData'
|
||||
import { Dialog, DialogContent, DialogTitle } from '@workspace/ui/components/dialog'
|
||||
import { VisuallyHidden } from '@radix-ui/react-visually-hidden'
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle
|
||||
} from '@workspace/ui/components/dialog'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@workspace/ui/components/select'
|
||||
import { Github, X } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
interface OnboardingDialogProps {
|
||||
trigger?: React.ReactNode
|
||||
@@ -269,18 +279,21 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
|
||||
defaultOpen = false,
|
||||
onClose
|
||||
}) => {
|
||||
const { nextStep, setFormData, formData, currentStep, previousStep } = useOnboarding()
|
||||
const [selectedRepo, setSelectedRepo] = useState<string>(formData.githubRepo || '')
|
||||
const { nextStep, setFormData, formData, currentStep, previousStep } =
|
||||
useOnboarding()
|
||||
const [selectedRepo, setSelectedRepo] = useState<string>(
|
||||
formData.githubRepo || ''
|
||||
)
|
||||
const [isImportMode, setIsImportMode] = useState(true)
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen)
|
||||
const { repoData: repositories, isLoading } = useRepoData('')
|
||||
|
||||
|
||||
// Handle repository selection
|
||||
const handleRepoSelect = (repo: string) => {
|
||||
setSelectedRepo(repo)
|
||||
setFormData({ githubRepo: repo })
|
||||
}
|
||||
|
||||
|
||||
// Handle mode toggle between import and template
|
||||
const toggleMode = (mode: 'import' | 'template') => {
|
||||
setIsImportMode(mode === 'import')
|
||||
@@ -293,7 +306,7 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
|
||||
{trigger && trigger}
|
||||
@@ -301,25 +314,48 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
|
||||
<VisuallyHidden>
|
||||
<DialogTitle>Onboarding</DialogTitle>
|
||||
</VisuallyHidden>
|
||||
|
||||
|
||||
<div className="flex h-full w-full">
|
||||
{/* Left sidebar with steps */}
|
||||
<div className="w-[280px] bg-zinc-950 p-8 relative overflow-hidden">
|
||||
{/* Laconic logo */}
|
||||
<div className="flex items-center mb-12">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className="mr-2">
|
||||
<path d="M12 16L4 8L12 0L20 8L12 16Z" fill="white"/>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="mr-2"
|
||||
>
|
||||
<path d="M12 16L4 8L12 0L20 8L12 16Z" fill="white" />
|
||||
</svg>
|
||||
<span className="text-white text-xl font-bold">LACONIC</span>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Steps */}
|
||||
<div className="space-y-10">
|
||||
{/* Connect step */}
|
||||
<div className="flex">
|
||||
<div className="mr-4">
|
||||
<div className={`w-10 h-10 rounded-lg ${currentStep === 'connect' ? 'bg-white' : 'bg-zinc-800'} flex items-center justify-center`}>
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" className={currentStep === 'connect' ? 'text-black' : 'text-zinc-400'}>
|
||||
<div
|
||||
className={`w-10 h-10 rounded-lg ${currentStep === 'connect' ? 'bg-white' : 'bg-zinc-800'} flex items-center justify-center`}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
height="20"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={
|
||||
currentStep === 'connect'
|
||||
? 'text-black'
|
||||
: 'text-zinc-400'
|
||||
}
|
||||
>
|
||||
<path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
|
||||
<line x1="8" y1="12" x2="16" y2="12"></line>
|
||||
</svg>
|
||||
@@ -327,16 +363,40 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
|
||||
<div className="w-px h-16 bg-zinc-800 mx-auto mt-2"></div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className={`font-medium text-base ${currentStep === 'connect' ? 'text-white' : 'text-zinc-400'}`}>Connect</h3>
|
||||
<p className={`text-sm ${currentStep === 'connect' ? 'text-zinc-400' : 'text-zinc-500'}`}>Connect and import a GitHub repo</p>
|
||||
<h3
|
||||
className={`font-medium text-base ${currentStep === 'connect' ? 'text-white' : 'text-zinc-400'}`}
|
||||
>
|
||||
Connect
|
||||
</h3>
|
||||
<p
|
||||
className={`text-sm ${currentStep === 'connect' ? 'text-zinc-400' : 'text-zinc-500'}`}
|
||||
>
|
||||
Connect and import a GitHub repo
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Configure step */}
|
||||
<div className="flex">
|
||||
<div className="mr-4">
|
||||
<div className={`w-10 h-10 rounded-lg ${currentStep === 'configure' ? 'bg-white' : 'bg-zinc-800'} flex items-center justify-center`}>
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" className={currentStep === 'configure' ? 'text-black' : 'text-zinc-400'}>
|
||||
<div
|
||||
className={`w-10 h-10 rounded-lg ${currentStep === 'configure' ? 'bg-white' : 'bg-zinc-800'} flex items-center justify-center`}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
height="20"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={
|
||||
currentStep === 'configure'
|
||||
? 'text-black'
|
||||
: 'text-zinc-400'
|
||||
}
|
||||
>
|
||||
<path d="M12 20h9"></path>
|
||||
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path>
|
||||
</svg>
|
||||
@@ -344,59 +404,110 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
|
||||
<div className="w-px h-16 bg-zinc-800 mx-auto mt-2"></div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className={`font-medium text-base ${currentStep === 'configure' ? 'text-white' : 'text-zinc-400'}`}>Configure</h3>
|
||||
<p className={`text-sm ${currentStep === 'configure' ? 'text-zinc-400' : 'text-zinc-500'}`}>Define the deployment type</p>
|
||||
<h3
|
||||
className={`font-medium text-base ${currentStep === 'configure' ? 'text-white' : 'text-zinc-400'}`}
|
||||
>
|
||||
Configure
|
||||
</h3>
|
||||
<p
|
||||
className={`text-sm ${currentStep === 'configure' ? 'text-zinc-400' : 'text-zinc-500'}`}
|
||||
>
|
||||
Define the deployment type
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Deploy step */}
|
||||
<div className="flex">
|
||||
<div className="mr-4">
|
||||
<div className={`w-10 h-10 rounded-lg ${currentStep === 'deploy' ? 'bg-white' : 'bg-zinc-800'} flex items-center justify-center`}>
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" className={currentStep === 'deploy' ? 'text-black' : 'text-zinc-400'}>
|
||||
<rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect>
|
||||
<div
|
||||
className={`w-10 h-10 rounded-lg ${currentStep === 'deploy' ? 'bg-white' : 'bg-zinc-800'} flex items-center justify-center`}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
height="20"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={
|
||||
currentStep === 'deploy'
|
||||
? 'text-black'
|
||||
: 'text-zinc-400'
|
||||
}
|
||||
>
|
||||
<rect
|
||||
x="2"
|
||||
y="2"
|
||||
width="20"
|
||||
height="20"
|
||||
rx="5"
|
||||
ry="5"
|
||||
></rect>
|
||||
<path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path>
|
||||
<line x1="17.5" y1="6.5" x2="17.51" y2="6.5"></line>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className={`font-medium text-base ${currentStep === 'deploy' ? 'text-white' : 'text-zinc-400'}`}>Deploy</h3>
|
||||
<p className={`text-sm ${currentStep === 'deploy' ? 'text-zinc-400' : 'text-zinc-500'}`}>Review and confirm deployment</p>
|
||||
<h3
|
||||
className={`font-medium text-base ${currentStep === 'deploy' ? 'text-white' : 'text-zinc-400'}`}
|
||||
>
|
||||
Deploy
|
||||
</h3>
|
||||
<p
|
||||
className={`text-sm ${currentStep === 'deploy' ? 'text-zinc-400' : 'text-zinc-500'}`}
|
||||
>
|
||||
Review and confirm deployment
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Background shape (decorative) */}
|
||||
<div className="absolute -bottom-32 -left-32 w-64 h-64 rounded-full bg-zinc-900 opacity-50"></div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Main content */}
|
||||
<div className="flex-1 bg-zinc-950 p-8 relative">
|
||||
{/* Close button */}
|
||||
<button
|
||||
<button
|
||||
className="absolute top-4 right-4 text-zinc-400 hover:text-white"
|
||||
onClick={() => handleOpenChange(false)}
|
||||
>
|
||||
<X size={24} />
|
||||
</button>
|
||||
|
||||
|
||||
{currentStep === 'connect' && (
|
||||
<div className="max-w-md mx-auto mt-8">
|
||||
{/* Connect icon */}
|
||||
<div className="flex justify-center mb-6">
|
||||
<svg viewBox="0 0 24 24" width="48" height="48" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" className="text-white">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="48"
|
||||
height="48"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="text-white"
|
||||
>
|
||||
<path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
|
||||
<line x1="8" y1="12" x2="16" y2="12"></line>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Connect header */}
|
||||
<h2 className="text-2xl font-medium text-white text-center mb-2">Connect</h2>
|
||||
<h2 className="text-2xl font-medium text-white text-center mb-2">
|
||||
Connect
|
||||
</h2>
|
||||
<p className="text-center text-zinc-400 mb-8">
|
||||
Connect and import a GitHub repo or start from a template
|
||||
</p>
|
||||
|
||||
|
||||
{/* Git account selector */}
|
||||
<div className="mb-4">
|
||||
<Select defaultValue="git-account">
|
||||
@@ -411,25 +522,25 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Mode buttons */}
|
||||
<div className="grid grid-cols-2 gap-2 mb-4">
|
||||
<Button
|
||||
variant={isImportMode ? "default" : "outline"}
|
||||
variant={isImportMode ? 'default' : 'outline'}
|
||||
className={`py-6 ${isImportMode ? 'bg-blue-600 hover:bg-blue-700 text-white' : 'bg-zinc-900 border-zinc-800 hover:bg-zinc-800 text-zinc-400'}`}
|
||||
onClick={() => toggleMode('import')}
|
||||
>
|
||||
Import a repository
|
||||
</Button>
|
||||
<Button
|
||||
variant={!isImportMode ? "default" : "outline"}
|
||||
variant={!isImportMode ? 'default' : 'outline'}
|
||||
className={`py-6 ${!isImportMode ? 'bg-blue-600 hover:bg-blue-700 text-white' : 'bg-zinc-900 border-zinc-800 hover:bg-zinc-800 text-zinc-400'}`}
|
||||
onClick={() => toggleMode('template')}
|
||||
>
|
||||
Start with a template
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Repository or template list */}
|
||||
{isImportMode ? (
|
||||
<div className="mb-8">
|
||||
@@ -445,11 +556,11 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
|
||||
<TemplateList onSelect={() => {}} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* Navigation buttons */}
|
||||
<div className="flex justify-between items-center mt-8">
|
||||
<Button
|
||||
variant="outline"
|
||||
<Button
|
||||
variant="outline"
|
||||
className="text-zinc-400 bg-zinc-900 border-zinc-800 hover:bg-zinc-800"
|
||||
disabled={true}
|
||||
>
|
||||
@@ -460,8 +571,8 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
|
||||
<div className="w-12 h-1 rounded-full bg-zinc-800"></div>
|
||||
<div className="w-12 h-1 rounded-full bg-zinc-800"></div>
|
||||
</div>
|
||||
<Button
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||
<Button
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||
onClick={nextStep}
|
||||
disabled={!selectedRepo && isImportMode}
|
||||
>
|
||||
@@ -470,24 +581,26 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{currentStep === 'configure' && (
|
||||
<div className="max-w-md mx-auto mt-8">
|
||||
{/* Configure content will go here */}
|
||||
<div className="text-center">
|
||||
<h2 className="text-2xl font-medium text-white mb-2">Configure</h2>
|
||||
<h2 className="text-2xl font-medium text-white mb-2">
|
||||
Configure
|
||||
</h2>
|
||||
<p className="text-zinc-400 mb-8">
|
||||
Define your deployment configuration
|
||||
</p>
|
||||
|
||||
|
||||
<div className="text-center mt-16 text-zinc-400">
|
||||
Configure step content will be implemented here
|
||||
</div>
|
||||
|
||||
|
||||
{/* Navigation buttons */}
|
||||
<div className="flex justify-between items-center mt-16">
|
||||
<Button
|
||||
variant="outline"
|
||||
<Button
|
||||
variant="outline"
|
||||
className="text-zinc-400 bg-zinc-900 border-zinc-800 hover:bg-zinc-800"
|
||||
onClick={() => previousStep()}
|
||||
>
|
||||
@@ -498,8 +611,8 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
|
||||
<div className="w-12 h-1 rounded-full bg-blue-600"></div>
|
||||
<div className="w-12 h-1 rounded-full bg-zinc-800"></div>
|
||||
</div>
|
||||
<Button
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||
<Button
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||
onClick={nextStep}
|
||||
>
|
||||
Next
|
||||
@@ -508,24 +621,26 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{currentStep === 'deploy' && (
|
||||
<div className="max-w-md mx-auto mt-8">
|
||||
{/* Deploy content will go here */}
|
||||
<div className="text-center">
|
||||
<h2 className="text-2xl font-medium text-white mb-2">Deploy</h2>
|
||||
<h2 className="text-2xl font-medium text-white mb-2">
|
||||
Deploy
|
||||
</h2>
|
||||
<p className="text-zinc-400 mb-8">
|
||||
Review and confirm your deployment
|
||||
</p>
|
||||
|
||||
|
||||
<div className="text-center mt-16 text-zinc-400">
|
||||
Deploy step content will be implemented here
|
||||
</div>
|
||||
|
||||
|
||||
{/* Navigation buttons */}
|
||||
<div className="flex justify-between items-center mt-16">
|
||||
<Button
|
||||
variant="outline"
|
||||
<Button
|
||||
variant="outline"
|
||||
className="text-zinc-400 bg-zinc-900 border-zinc-800 hover:bg-zinc-800"
|
||||
onClick={() => previousStep()}
|
||||
>
|
||||
@@ -536,9 +651,7 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
|
||||
<div className="w-12 h-1 rounded-full bg-zinc-800"></div>
|
||||
<div className="w-12 h-1 rounded-full bg-blue-600"></div>
|
||||
</div>
|
||||
<Button
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||
>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white">
|
||||
Deploy
|
||||
</Button>
|
||||
</div>
|
||||
@@ -552,4 +665,4 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
export default OnboardingDialog
|
||||
export default OnboardingDialog
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { GitBranch, Settings, Box } from 'lucide-react'
|
||||
import type { Step } from '@/components/onboarding/useOnboarding'
|
||||
import type { Step } from '@/components/onboarding/types'
|
||||
import { Box, GitBranch, Settings } from 'lucide-react'
|
||||
|
||||
interface OnboardingSidebarProps {
|
||||
currentStep: Step
|
||||
@@ -12,58 +12,101 @@ export function OnboardingSidebar({ currentStep }: OnboardingSidebarProps) {
|
||||
<div className="w-[280px] bg-zinc-950 p-8 relative overflow-hidden">
|
||||
{/* Laconic logo */}
|
||||
<div className="flex items-center mb-12">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className="mr-2">
|
||||
<path d="M12 16L4 8L12 0L20 8L12 16Z" fill="white"/>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="mr-2"
|
||||
>
|
||||
<path d="M12 16L4 8L12 0L20 8L12 16Z" fill="white" />
|
||||
</svg>
|
||||
<span className="text-white text-xl font-bold">LACONIC</span>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Steps */}
|
||||
<div className="space-y-10">
|
||||
{/* Connect step */}
|
||||
<div className="flex">
|
||||
<div className="mr-4">
|
||||
<div className={`w-10 h-10 rounded-lg ${currentStep === 'connect' ? 'bg-white' : 'bg-zinc-800'} flex items-center justify-center`}>
|
||||
<GitBranch className={`h-5 w-5 ${currentStep === 'connect' ? 'text-black' : 'text-zinc-400'}`} />
|
||||
<div
|
||||
className={`w-10 h-10 rounded-lg ${currentStep === 'connect' ? 'bg-white' : 'bg-zinc-800'} flex items-center justify-center`}
|
||||
>
|
||||
<GitBranch
|
||||
className={`h-5 w-5 ${currentStep === 'connect' ? 'text-black' : 'text-zinc-400'}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-px h-16 bg-zinc-800 mx-auto mt-2"></div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className={`font-medium text-base ${currentStep === 'connect' ? 'text-white' : 'text-zinc-400'}`}>Connect</h3>
|
||||
<p className={`text-sm ${currentStep === 'connect' ? 'text-zinc-400' : 'text-zinc-500'}`}>Connect and import a GitHub repo</p>
|
||||
<h3
|
||||
className={`font-medium text-base ${currentStep === 'connect' ? 'text-white' : 'text-zinc-400'}`}
|
||||
>
|
||||
Connect
|
||||
</h3>
|
||||
<p
|
||||
className={`text-sm ${currentStep === 'connect' ? 'text-zinc-400' : 'text-zinc-500'}`}
|
||||
>
|
||||
Connect and import a GitHub repo
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Configure step */}
|
||||
<div className="flex">
|
||||
<div className="mr-4">
|
||||
<div className={`w-10 h-10 rounded-lg ${currentStep === 'configure' ? 'bg-white' : 'bg-zinc-800'} flex items-center justify-center`}>
|
||||
<Settings className={`h-5 w-5 ${currentStep === 'configure' ? 'text-black' : 'text-zinc-400'}`} />
|
||||
<div
|
||||
className={`w-10 h-10 rounded-lg ${currentStep === 'configure' ? 'bg-white' : 'bg-zinc-800'} flex items-center justify-center`}
|
||||
>
|
||||
<Settings
|
||||
className={`h-5 w-5 ${currentStep === 'configure' ? 'text-black' : 'text-zinc-400'}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-px h-16 bg-zinc-800 mx-auto mt-2"></div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className={`font-medium text-base ${currentStep === 'configure' ? 'text-white' : 'text-zinc-400'}`}>Configure</h3>
|
||||
<p className={`text-sm ${currentStep === 'configure' ? 'text-zinc-400' : 'text-zinc-500'}`}>Define the deployment type</p>
|
||||
<h3
|
||||
className={`font-medium text-base ${currentStep === 'configure' ? 'text-white' : 'text-zinc-400'}`}
|
||||
>
|
||||
Configure
|
||||
</h3>
|
||||
<p
|
||||
className={`text-sm ${currentStep === 'configure' ? 'text-zinc-400' : 'text-zinc-500'}`}
|
||||
>
|
||||
Define the deployment type
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Deploy step */}
|
||||
<div className="flex">
|
||||
<div className="mr-4">
|
||||
<div className={`w-10 h-10 rounded-lg ${currentStep === 'deploy' || currentStep === 'success' ? 'bg-white' : 'bg-zinc-800'} flex items-center justify-center`}>
|
||||
<Box className={`h-5 w-5 ${currentStep === 'deploy' || currentStep === 'success' ? 'text-black' : 'text-zinc-400'}`} />
|
||||
<div
|
||||
className={`w-10 h-10 rounded-lg ${currentStep === 'deploy' || currentStep === 'success' ? 'bg-white' : 'bg-zinc-800'} flex items-center justify-center`}
|
||||
>
|
||||
<Box
|
||||
className={`h-5 w-5 ${currentStep === 'deploy' || currentStep === 'success' ? 'text-black' : 'text-zinc-400'}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className={`font-medium text-base ${currentStep === 'deploy' || currentStep === 'success' ? 'text-white' : 'text-zinc-400'}`}>Deploy</h3>
|
||||
<p className={`text-sm ${currentStep === 'deploy' || currentStep === 'success' ? 'text-zinc-400' : 'text-zinc-500'}`}>Review and confirm deployment</p>
|
||||
<h3
|
||||
className={`font-medium text-base ${currentStep === 'deploy' || currentStep === 'success' ? 'text-white' : 'text-zinc-400'}`}
|
||||
>
|
||||
Deploy
|
||||
</h3>
|
||||
<p
|
||||
className={`text-sm ${currentStep === 'deploy' || currentStep === 'success' ? 'text-zinc-400' : 'text-zinc-500'}`}
|
||||
>
|
||||
Review and confirm deployment
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Background shape (decorative) */}
|
||||
<div className="absolute -bottom-32 -left-32 w-64 h-64 rounded-full bg-zinc-900 opacity-50"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// src/components/onboarding/configure-step/configure-step.tsx
|
||||
'use client'
|
||||
|
||||
import { useOnboarding } from '@/components/onboarding/useOnboarding'
|
||||
import { useOnboarding } from '@/components/onboarding/store'
|
||||
import { useGQLClient } from '@/context'
|
||||
import { useWallet } from '@/context/WalletContext'
|
||||
import { Alert, AlertDescription } from '@workspace/ui/components/alert'
|
||||
@@ -28,6 +28,8 @@ import { useTheme } from 'next-themes'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { adaptDeployers } from '../../../utils/typeAdapters'
|
||||
import { usePayment } from '@/hooks/usePayment'
|
||||
import { TransactionApprovalModal } from '@/components/iframe/transaction-approval/TransactionApprovalModal'
|
||||
|
||||
interface Deployer {
|
||||
deployerLrn: string
|
||||
@@ -44,6 +46,7 @@ interface Organization {
|
||||
|
||||
export function ConfigureStep() {
|
||||
const { nextStep, previousStep, setFormData, formData } = useOnboarding()
|
||||
const { processPayment, calculateAmount, isLoading: isPaymentLoading, isModalVisible, setIsModalVisible } = usePayment()
|
||||
const { resolvedTheme } = useTheme()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
@@ -53,19 +56,13 @@ export function ConfigureStep() {
|
||||
const [isLoadingDeployers, setIsLoadingDeployers] = useState(true)
|
||||
const [isLoadingOrgs, setIsLoadingOrgs] = useState(true)
|
||||
|
||||
// Form state
|
||||
const [deployOption, setDeployOption] = useState<'auction' | 'lrn'>(
|
||||
(formData.deploymentType as 'auction' | 'lrn') || 'lrn' // Default to LRN for simplicity
|
||||
)
|
||||
const [numberOfDeployers, setNumberOfDeployers] = useState<string>(
|
||||
formData.deployerCount || '1'
|
||||
)
|
||||
const [maxPrice, setMaxPrice] = useState<string>(formData.maxPrice || '1000')
|
||||
const [selectedLrn, setSelectedLrn] = useState<string>(
|
||||
formData.selectedLrn || ''
|
||||
)
|
||||
// Form state - using local state since these aren't in the simplified store
|
||||
const [deployOption, setDeployOption] = useState<'auction' | 'lrn'>('lrn') // Default to LRN for simplicity
|
||||
const [numberOfDeployers, setNumberOfDeployers] = useState<string>('1')
|
||||
const [maxPrice, setMaxPrice] = useState<string>('1000')
|
||||
const [selectedLrn, setSelectedLrn] = useState<string>('')
|
||||
const [selectedOrg, setSelectedOrg] = useState<string>(
|
||||
formData.selectedOrg || ''
|
||||
formData.organizationSlug || ''
|
||||
)
|
||||
const [envVars, setEnvVars] = useState<
|
||||
{ key: string; value: string; environments: string[] }[]
|
||||
@@ -88,19 +85,8 @@ export function ConfigureStep() {
|
||||
}
|
||||
}, [mounted])
|
||||
|
||||
// Initialize environment variables from formData if available
|
||||
useEffect(() => {
|
||||
if (
|
||||
formData.environmentVariables &&
|
||||
Array.isArray(formData.environmentVariables)
|
||||
) {
|
||||
setEnvVars(
|
||||
formData.environmentVariables.length > 0
|
||||
? formData.environmentVariables
|
||||
: [{ key: '', value: '', environments: ['Production'] }]
|
||||
)
|
||||
}
|
||||
}, [formData.environmentVariables])
|
||||
// Environment variables are managed locally
|
||||
// (Removed environment variables initialization since not in simple store)
|
||||
|
||||
// Fetch deployers from backend
|
||||
const fetchDeployers = async () => {
|
||||
@@ -150,6 +136,17 @@ export function ConfigureStep() {
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-save selectedOrg to form data whenever it changes (including auto-selection)
|
||||
useEffect(() => {
|
||||
if (selectedOrg) {
|
||||
console.log('Auto-saving selectedOrg to form data:', selectedOrg)
|
||||
setFormData({
|
||||
organizationSlug: selectedOrg,
|
||||
selectedOrg: selectedOrg
|
||||
})
|
||||
}
|
||||
}, [selectedOrg])
|
||||
|
||||
// Add an empty environment variable row
|
||||
const addEnvVar = () => {
|
||||
setEnvVars([
|
||||
@@ -217,8 +214,27 @@ export function ConfigureStep() {
|
||||
return true
|
||||
}
|
||||
|
||||
// Calculate payment amount for display
|
||||
const paymentAmount = calculateAmount({
|
||||
deploymentType: deployOption,
|
||||
selectedDeployer,
|
||||
maxPrice,
|
||||
deployerCount: numberOfDeployers
|
||||
})
|
||||
|
||||
// Show payment amount in UI
|
||||
const showPaymentRequired = () => {
|
||||
if (deployOption === 'lrn' && selectedDeployer?.minimumPayment) {
|
||||
return true
|
||||
}
|
||||
if (deployOption === 'auction' && paymentAmount !== '0') {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Handle next step
|
||||
const handleNext = () => {
|
||||
const handleNext = async () => {
|
||||
if (!canProceed()) {
|
||||
toast.error('Please complete all required fields')
|
||||
return
|
||||
@@ -229,20 +245,78 @@ export function ConfigureStep() {
|
||||
(env) => env.key.trim() && env.value.trim()
|
||||
)
|
||||
|
||||
let paymentData = {
|
||||
txHash: '',
|
||||
paymentAddress: wallet?.address || ''
|
||||
}
|
||||
|
||||
// Process payment if required
|
||||
if (showPaymentRequired()) {
|
||||
const paymentResult = await processPayment({
|
||||
deploymentType: deployOption,
|
||||
selectedDeployer,
|
||||
maxPrice,
|
||||
deployerCount: numberOfDeployers
|
||||
})
|
||||
|
||||
if (!paymentResult) {
|
||||
// Payment failed - error already shown by usePayment hook
|
||||
return
|
||||
}
|
||||
|
||||
paymentData = {
|
||||
txHash: paymentResult.txHash,
|
||||
paymentAddress: wallet?.address || ''
|
||||
}
|
||||
}
|
||||
|
||||
// Save configuration to form data
|
||||
setFormData({
|
||||
deploymentType: deployOption,
|
||||
deployerCount: numberOfDeployers,
|
||||
maxPrice: maxPrice,
|
||||
selectedLrn: selectedLrn,
|
||||
organizationSlug: selectedOrg,
|
||||
selectedOrg: selectedOrg,
|
||||
paymentAddress: wallet?.address,
|
||||
environmentVariables: validEnvVars
|
||||
selectedLrn: selectedLrn,
|
||||
deploymentType: deployOption,
|
||||
maxPrice: maxPrice,
|
||||
deployerCount: numberOfDeployers,
|
||||
environmentVariables: validEnvVars,
|
||||
...paymentData //Add payments
|
||||
})
|
||||
|
||||
nextStep()
|
||||
}
|
||||
|
||||
// Update your UI to show payment info
|
||||
const renderPaymentInfo = () => {
|
||||
if (!showPaymentRequired()) return null
|
||||
|
||||
return (
|
||||
<div className="mb-6">
|
||||
<Label className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}>
|
||||
Payment Required
|
||||
</Label>
|
||||
<div className={`p-3 border rounded-md bg-muted ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}>
|
||||
<div className="text-sm space-y-1">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Amount:</span>
|
||||
<span className="font-mono">{paymentAmount} alnt</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">From:</span>
|
||||
<span className="font-mono text-xs">{wallet?.address}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Update your Next button
|
||||
const getNextButtonText = () => {
|
||||
if (isPaymentLoading) return 'Processing Payment...'
|
||||
if (showPaymentRequired()) return 'Pay and Continue'
|
||||
return 'Continue'
|
||||
}
|
||||
|
||||
// Don't render UI until after mount to prevent hydration mismatch
|
||||
if (!mounted) {
|
||||
return null
|
||||
@@ -251,11 +325,9 @@ export function ConfigureStep() {
|
||||
// Determine if dark mode is active
|
||||
const isDarkMode = resolvedTheme === 'dark'
|
||||
|
||||
// Get deployment mode info
|
||||
const isTemplateMode = formData.deploymentMode === 'template'
|
||||
const selectedItem = isTemplateMode
|
||||
? formData.template?.name
|
||||
: formData.githubRepo
|
||||
// Get deployment mode info - determine from available data
|
||||
const isTemplateMode = !!formData.framework && !formData.repoName
|
||||
const selectedItem = isTemplateMode ? formData.framework : formData.repoName
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col p-8 overflow-y-auto">
|
||||
@@ -540,20 +612,8 @@ export function ConfigureStep() {
|
||||
)}
|
||||
|
||||
{/* Payment Address */}
|
||||
<div className="mb-6">
|
||||
<Label
|
||||
className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}
|
||||
>
|
||||
Payment Address
|
||||
</Label>
|
||||
<div
|
||||
className={`p-3 border rounded-md bg-muted ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}
|
||||
>
|
||||
<div className="text-sm font-mono break-all">
|
||||
{wallet?.address || 'No wallet connected'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Add payment info section */}
|
||||
{renderPaymentInfo()}
|
||||
|
||||
{/* Environment Variables */}
|
||||
<div className="mb-6">
|
||||
@@ -628,23 +688,30 @@ export function ConfigureStep() {
|
||||
|
||||
{/* Navigation buttons */}
|
||||
<div className="flex justify-between items-center mt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
className={`bg-transparent ${isDarkMode ? 'text-zinc-400 border-zinc-700' : 'text-zinc-600 border-zinc-300'}`}
|
||||
onClick={previousStep}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
className={`${isDarkMode ? 'bg-zinc-800' : 'bg-zinc-800'} text-white hover:bg-zinc-700`}
|
||||
onClick={handleNext}
|
||||
disabled={!canProceed()}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={`bg-transparent ${isDarkMode ? 'text-zinc-400 border-zinc-700' : 'text-zinc-600 border-zinc-300'}`}
|
||||
onClick={previousStep}
|
||||
disabled={isPaymentLoading}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
className={`${isDarkMode ? 'bg-zinc-800' : 'bg-zinc-800'} text-white hover:bg-zinc-700`}
|
||||
onClick={handleNext}
|
||||
disabled={!canProceed() || isPaymentLoading}
|
||||
>
|
||||
{getNextButtonText()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Transaction Approval Modal */}
|
||||
<TransactionApprovalModal
|
||||
isVisible={isModalVisible}
|
||||
onClose={() => setIsModalVisible(false)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import { FileCog } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useOnboarding } from '../store'
|
||||
|
||||
/**
|
||||
* Second step in the onboarding flow
|
||||
* Handles deployment configuration and environment setup
|
||||
*
|
||||
* Features:
|
||||
* - Deployment type selection (auction/LRN)
|
||||
* - Environment variable configuration
|
||||
* - Account selection
|
||||
*
|
||||
* @component
|
||||
*/
|
||||
export function ConfigureStep() {
|
||||
const { formData, setFormData } = useOnboarding()
|
||||
const [activeTab, setActiveTab] = useState<'create-auction' | 'deployer-lrn'>(
|
||||
'create-auction'
|
||||
)
|
||||
const [environments, setEnvironments] = useState({
|
||||
production: false,
|
||||
preview: false,
|
||||
development: false
|
||||
})
|
||||
|
||||
// const handleEnvironmentChange = (env: keyof typeof environments) => {
|
||||
// setEnvironments((prev) => ({
|
||||
// ...prev,
|
||||
// [env]: !prev[env],
|
||||
// }))
|
||||
// setFormData({
|
||||
// environmentVars: {
|
||||
// ...formData.environmentVars,
|
||||
// [env]: !environments[env],
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="max-w-2xl mx-auto space-y-8">
|
||||
<div className="flex flex-col items-center justify-center w-full max-w-[445px] mx-auto">
|
||||
<div className="w-full flex flex-col items-center gap-6">
|
||||
{/* Header section with icon and description */}
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<FileCog className="w-16 h-16 text-foreground" />
|
||||
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<h2 className="text-2xl font-bold text-foreground">
|
||||
Configure
|
||||
</h2>
|
||||
<p className="text-base text-muted-foreground text-center">
|
||||
Set the deployer LRN for a single deployment or by creating a
|
||||
deployer auction for multiple deployments
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
Content sections will be placed here: 1. Deployment type tabs
|
||||
(auction/LRN) 2. Configuration forms 3. Environment variables 4.
|
||||
Account selection ...content here/
|
||||
{/* <Configure /> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
'use client'
|
||||
|
||||
import { GitHubBackendAuth } from '@/components/GitHubBackendAuth'
|
||||
import { useOnboarding } from '@/components/onboarding/useOnboarding'
|
||||
import { useOnboarding } from '@/components/onboarding/store'
|
||||
import { AVAILABLE_TEMPLATES, type TemplateDetail } from '@/constants/templates'
|
||||
import { useAuthStatus } from '@/hooks/useAuthStatus'
|
||||
import { useRepoData } from '@/hooks/useRepoData'
|
||||
@@ -46,10 +46,12 @@ export function ConnectStep() {
|
||||
|
||||
// Repository vs Template selection
|
||||
const [selectedRepo, setSelectedRepo] = useState<string>(
|
||||
formData.githubRepo || ''
|
||||
formData.repoName || ''
|
||||
)
|
||||
const [selectedTemplate, setSelectedTemplate] = useState(
|
||||
adaptOptionalTemplate(formData.template)
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<
|
||||
TemplateDetail | undefined
|
||||
>(
|
||||
undefined // We'll simplify template handling
|
||||
)
|
||||
const [projectName, setProjectName] = useState<string>(
|
||||
formData.projectName || ''
|
||||
@@ -71,7 +73,23 @@ export function ConnectStep() {
|
||||
} = useAuthStatus()
|
||||
|
||||
// Repository data
|
||||
const { repoData: repositories, isLoading: isLoadingRepos } = useRepoData('')
|
||||
const {
|
||||
repoData: repositories,
|
||||
isLoading: isLoadingRepos,
|
||||
error: repoError
|
||||
} = useRepoData('')
|
||||
|
||||
// Debug repository data
|
||||
useEffect(() => {
|
||||
console.log('🔍 ConnectStep: Repository data changed:', {
|
||||
repositories: repositories
|
||||
? `Array with ${Array.isArray(repositories) ? repositories.length : 'not array'} items`
|
||||
: 'null',
|
||||
isLoadingRepos,
|
||||
repoError,
|
||||
isFullyAuthenticated
|
||||
})
|
||||
}, [repositories, isLoadingRepos, repoError, isFullyAuthenticated])
|
||||
|
||||
// Handle hydration mismatch by waiting for mount
|
||||
useEffect(() => {
|
||||
@@ -90,9 +108,10 @@ export function ConnectStep() {
|
||||
setSelectedRepo(repo)
|
||||
setSelectedTemplate(undefined)
|
||||
setFormData({
|
||||
githubRepo: repo,
|
||||
template: undefined,
|
||||
deploymentMode: 'repository',
|
||||
repoName: repo,
|
||||
githubRepo: repo, // Store repo path for deploy step
|
||||
template: undefined, // Clear template selection
|
||||
framework: '', // Clear framework
|
||||
projectName
|
||||
})
|
||||
}
|
||||
@@ -107,9 +126,10 @@ export function ConnectStep() {
|
||||
setProjectName(suggestedName)
|
||||
}
|
||||
setFormData({
|
||||
template: template,
|
||||
githubRepo: '',
|
||||
deploymentMode: 'template',
|
||||
framework: template.name, // Keep for backwards compatibility
|
||||
template: template, // Store the full template object
|
||||
githubRepo: '', // Clear repo selection
|
||||
repoName: '',
|
||||
projectName:
|
||||
projectName ||
|
||||
`my-${template.name.toLowerCase().replace(/[^a-z0-9]/g, '-')}`
|
||||
@@ -123,15 +143,17 @@ export function ConnectStep() {
|
||||
if (mode === 'import') {
|
||||
setSelectedTemplate(undefined)
|
||||
setFormData({
|
||||
framework: '',
|
||||
template: undefined,
|
||||
deploymentMode: 'repository',
|
||||
githubRepo: '',
|
||||
projectName
|
||||
})
|
||||
} else {
|
||||
setSelectedRepo('')
|
||||
setFormData({
|
||||
repoName: '',
|
||||
githubRepo: '',
|
||||
deploymentMode: 'template',
|
||||
template: undefined,
|
||||
projectName
|
||||
})
|
||||
}
|
||||
@@ -186,9 +208,8 @@ export function ConnectStep() {
|
||||
|
||||
// Set final form data and proceed
|
||||
setFormData({
|
||||
deploymentMode: isImportMode ? 'repository' : 'template',
|
||||
githubRepo: isImportMode ? selectedRepo : '',
|
||||
template: !isImportMode ? (selectedTemplate as Template) : undefined,
|
||||
repoName: isImportMode ? selectedRepo : '',
|
||||
framework: !isImportMode ? selectedTemplate?.name || '' : '',
|
||||
projectName: finalProjectName
|
||||
})
|
||||
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
// 'use client'
|
||||
|
||||
// import { Button } from '@workspace/ui/components/button'
|
||||
// import { useState } from 'react'
|
||||
// import { useOnboarding } from '../store'
|
||||
|
||||
// type ConnectState = 'initial' | 'repository-select' | 'template-select'
|
||||
|
||||
// /**
|
||||
// * First step in the onboarding flow
|
||||
// * Handles GitHub connection and repository selection
|
||||
// *
|
||||
// * States:
|
||||
// * - initial: Shows GitHub connect button
|
||||
// * - repository-select: Shows list of repositories
|
||||
// * - template-select: Shows available templates
|
||||
// *
|
||||
// * @component
|
||||
// */
|
||||
// export function ConnectStep() {
|
||||
// const [connectState, setConnectState] = useState<ConnectState>('initial')
|
||||
// const [projectName, setProjectName] = useState('')
|
||||
// const { setFormData, nextStep } = useOnboarding()
|
||||
|
||||
// const handleConnect = () => {
|
||||
// setConnectState('repository-select')
|
||||
// }
|
||||
|
||||
// return (
|
||||
// <div className="max-w-2xl w-full">
|
||||
// {/* <ConnectAccountTabPanel />\ */}
|
||||
// {connectState === 'initial' ? (
|
||||
// <div className="flex flex-col items-center justify-center gap-6 p-8">
|
||||
// <h2 className="text-2xl font-semibold text-center">
|
||||
// Connect to GitHub
|
||||
// </h2>
|
||||
// <p className="text-center text-muted-foreground">
|
||||
// Connect your GitHub account to get started
|
||||
// </p>
|
||||
// <Button onClick={handleConnect} />
|
||||
// </div>
|
||||
// ) : (
|
||||
// // <ConnectAccount onAuth={() => {}} />
|
||||
// <>...connect goes here</>
|
||||
// )}
|
||||
// </div>
|
||||
// )
|
||||
// }
|
||||
|
||||
'use client'
|
||||
// src/components/onboarding/connect-step/connect-step.tsx
|
||||
import { useState } from 'react'
|
||||
import { useRepoData } from '@/hooks/useRepoData'
|
||||
import { useOnboarding } from '@/components/onboarding/useOnboarding'
|
||||
import { ScrollableRepoList } from '@/components/onboarding/connect-step/repository-list'
|
||||
import { TemplateList } from '@/components/onboarding/connect-step/template-list'
|
||||
import { StepHeader } from '@/components/onboarding/common/step-header'
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@workspace/ui/components/select'
|
||||
import { GitBranch } from 'lucide-react'
|
||||
|
||||
export const ConnectStep = () => {
|
||||
const { setFormData, formData, nextStep } = useOnboarding()
|
||||
const [selectedRepo, setSelectedRepo] = useState<string>(formData.githubRepo || '')
|
||||
const [isImportMode, setIsImportMode] = useState(true)
|
||||
const { repoData: repositories, isLoading } = useRepoData('')
|
||||
|
||||
// Handle repository selection
|
||||
const handleRepoSelect = (repo: string) => {
|
||||
setSelectedRepo(repo)
|
||||
setFormData({ githubRepo: repo })
|
||||
}
|
||||
|
||||
// Handle mode toggle between import and template
|
||||
const toggleMode = (mode: 'import' | 'template') => {
|
||||
setIsImportMode(mode === 'import')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center w-full px-8">
|
||||
<div className="w-full max-w-md">
|
||||
{/* Connect icon */}
|
||||
<div className="flex justify-center mb-6">
|
||||
<div className="w-16 h-16 bg-blue-500 bg-opacity-10 rounded-full flex items-center justify-center">
|
||||
<GitBranch className="h-8 w-8 text-blue-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Connect header */}
|
||||
<StepHeader
|
||||
title="Connect"
|
||||
description="Connect and import a GitHub repo or start from a template"
|
||||
icon={<GitBranch />}
|
||||
/>
|
||||
|
||||
{/* Git account selector */}
|
||||
<Select defaultValue="git-account">
|
||||
<SelectTrigger className="mb-4 bg-zinc-900 border-zinc-800">
|
||||
<SelectValue placeholder="Select Git account" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="git-account">git-account</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* Mode buttons */}
|
||||
<div className="flex mb-4 space-x-2">
|
||||
<Button
|
||||
variant={isImportMode ? "default" : "outline"}
|
||||
className={`flex-1 ${isImportMode ? 'bg-blue-600 hover:bg-blue-700' : 'bg-zinc-900 border-zinc-800 hover:bg-zinc-800'}`}
|
||||
onClick={() => toggleMode('import')}
|
||||
>
|
||||
Import a repository
|
||||
</Button>
|
||||
<Button
|
||||
variant={!isImportMode ? "default" : "outline"}
|
||||
className={`flex-1 ${!isImportMode ? 'bg-blue-600 hover:bg-blue-700' : 'bg-zinc-900 border-zinc-800 hover:bg-zinc-800'}`}
|
||||
onClick={() => toggleMode('template')}
|
||||
>
|
||||
Start with a template
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Repository list or template list */}
|
||||
{isImportMode ? (
|
||||
<ScrollableRepoList
|
||||
repositories={repositories || []}
|
||||
isLoading={isLoading}
|
||||
onSelect={handleRepoSelect}
|
||||
selectedRepo={selectedRepo}
|
||||
/>
|
||||
) : (
|
||||
<TemplateList onSelect={() => {}} />
|
||||
)}
|
||||
|
||||
{/* Navigation buttons */}
|
||||
<div className="flex justify-end space-x-2 mt-8">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="text-muted-foreground bg-zinc-900 border-zinc-800 hover:bg-zinc-800"
|
||||
disabled={true} // Disable previous on first step
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
onClick={nextStep}
|
||||
disabled={!selectedRepo && isImportMode}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -49,16 +49,16 @@
|
||||
import { Github } from 'lucide-react'
|
||||
|
||||
interface Repository {
|
||||
id: string | number;
|
||||
full_name: string;
|
||||
html_url?: string;
|
||||
id: string | number
|
||||
full_name: string
|
||||
html_url?: string
|
||||
}
|
||||
|
||||
interface ScrollableRepoListProps {
|
||||
repositories: Repository[];
|
||||
isLoading: boolean;
|
||||
onSelect: (repoFullName: string) => void;
|
||||
selectedRepo: string;
|
||||
repositories: Repository[]
|
||||
isLoading: boolean
|
||||
onSelect: (repoFullName: string) => void
|
||||
selectedRepo: string
|
||||
}
|
||||
|
||||
export function ScrollableRepoList({
|
||||
@@ -72,14 +72,30 @@ export function ScrollableRepoList({
|
||||
return (
|
||||
<div className="border border-zinc-800 rounded-md bg-zinc-900 overflow-hidden">
|
||||
<div className="p-6 text-center text-zinc-400">
|
||||
<svg className="animate-spin h-5 w-5 text-zinc-400 mx-auto mb-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
<svg
|
||||
className="animate-spin h-5 w-5 text-zinc-400 mx-auto mb-2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
Loading repositories...
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
if (!repositories || repositories.length === 0) {
|
||||
@@ -89,29 +105,31 @@ export function ScrollableRepoList({
|
||||
No repositories found. Make sure your GitHub account is connected.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
// Generate mock repositories if needed for testing
|
||||
const repoList = repositories.length > 0 ? repositories :
|
||||
Array.from({ length: 15 }, (_, i) => ({
|
||||
id: `mock-${i}`,
|
||||
full_name: `git-account/repo-name-${i + 1}`
|
||||
}));
|
||||
const repoList =
|
||||
repositories.length > 0
|
||||
? repositories
|
||||
: Array.from({ length: 15 }, (_, i) => ({
|
||||
id: `mock-${i}`,
|
||||
full_name: `git-account/repo-name-${i + 1}`
|
||||
}))
|
||||
|
||||
return (
|
||||
<div className="border border-zinc-800 rounded-md bg-zinc-900 overflow-hidden">
|
||||
{/* Ensure we have a fixed height container with scrolling */}
|
||||
<div
|
||||
<div
|
||||
className="max-h-60 overflow-y-auto"
|
||||
style={{
|
||||
style={{
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: '#4b5563 #1f2937'
|
||||
}}
|
||||
>
|
||||
{repoList.map((repo, index) => (
|
||||
<div
|
||||
key={repo.id || index}
|
||||
<div
|
||||
key={repo.id || index}
|
||||
className={`flex items-center p-4 border-b border-zinc-800 last:border-b-0 cursor-pointer hover:bg-zinc-800 transition-colors duration-150 ${
|
||||
selectedRepo === repo.full_name ? 'bg-zinc-800' : ''
|
||||
}`}
|
||||
@@ -121,12 +139,10 @@ export function ScrollableRepoList({
|
||||
<Github className="inline-block h-4 w-4 mr-2 text-zinc-400" />
|
||||
<span>{repo.full_name}</span>
|
||||
</div>
|
||||
<div className="text-sm text-zinc-400">
|
||||
5 minutes ago
|
||||
</div>
|
||||
<div className="text-sm text-zinc-400">5 minutes ago</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -66,18 +66,21 @@
|
||||
import { Box, Layout } from 'lucide-react'
|
||||
|
||||
interface Template {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: React.ReactNode;
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
icon: React.ReactNode
|
||||
}
|
||||
|
||||
interface TemplateListProps {
|
||||
onSelect: (templateId: string) => void;
|
||||
selectedTemplate?: string;
|
||||
onSelect: (templateId: string) => void
|
||||
selectedTemplate?: string
|
||||
}
|
||||
|
||||
export const TemplateList = ({ onSelect, selectedTemplate }: TemplateListProps) => {
|
||||
export const TemplateList = ({
|
||||
onSelect,
|
||||
selectedTemplate
|
||||
}: TemplateListProps) => {
|
||||
// Mock templates data - in a real app, this would come from an API
|
||||
const templates: Template[] = [
|
||||
{
|
||||
@@ -98,7 +101,7 @@ export const TemplateList = ({ onSelect, selectedTemplate }: TemplateListProps)
|
||||
description: 'Simple static site with HTML, CSS, and JavaScript',
|
||||
icon: <Layout className="h-6 w-6" />
|
||||
}
|
||||
];
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="border border-zinc-800 rounded-md bg-zinc-900 overflow-hidden">
|
||||
@@ -116,11 +119,13 @@ export const TemplateList = ({ onSelect, selectedTemplate }: TemplateListProps)
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">{template.name}</div>
|
||||
<div className="text-xs text-muted-foreground">{template.description}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{template.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// src/components/onboarding/deploy-step/deploy-step.tsx
|
||||
'use client'
|
||||
|
||||
import { useOnboarding } from '@/components/onboarding/useOnboarding'
|
||||
import { useOnboarding } from '@/components/onboarding/store'
|
||||
import { useWallet } from '@/context/WalletContext'
|
||||
import { useDeployment } from '@/hooks/useDeployment'
|
||||
import { useDirectTemplateDeployment } from '@/hooks/useDirectTemplateDeployment'
|
||||
import { useTemplateDeployment } from '@/hooks/useTemplate'
|
||||
import { Alert, AlertDescription } from '@workspace/ui/components/alert'
|
||||
import { Badge } from '@workspace/ui/components/badge'
|
||||
@@ -44,7 +45,7 @@ export function DeployStep() {
|
||||
useTemplateDeployment()
|
||||
|
||||
// Determine deployment type and get the right deploying state
|
||||
const isTemplateMode = formData.deploymentMode === 'template'
|
||||
const isTemplateMode = formData.template ? true : false
|
||||
const isDeploying = isTemplateMode ? isTemplateDeploying : isRepoDeploying
|
||||
|
||||
// Handle hydration mismatch by waiting for mount
|
||||
@@ -132,11 +133,17 @@ export function DeployStep() {
|
||||
throw new Error('Missing required template deployment data')
|
||||
}
|
||||
|
||||
if (!formData.template.repoFullName) {
|
||||
throw new Error('Template repository information is missing')
|
||||
}
|
||||
|
||||
const config = {
|
||||
template: {
|
||||
...formData.template,
|
||||
id: formData.template?.id || '',
|
||||
icon: formData.template?.icon || ''
|
||||
id: formData.template.id,
|
||||
name: formData.template.name,
|
||||
icon: formData.template.icon || '',
|
||||
repoFullName: formData.template.repoFullName,
|
||||
description: formData.template.description || ''
|
||||
},
|
||||
projectName: formData.projectName,
|
||||
organizationSlug: formData.selectedOrg,
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useOnboarding } from '../store'
|
||||
|
||||
/**
|
||||
* Final step in the onboarding flow
|
||||
* Displays deployment summary and triggers deployment
|
||||
*
|
||||
* Features:
|
||||
* - Configuration summary
|
||||
* - Repository display
|
||||
* - Deploy action
|
||||
*
|
||||
* @component
|
||||
*/
|
||||
export function DeployStep() {
|
||||
useOnboarding()
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="max-w-2xl mx-auto space-y-8">
|
||||
<div className="flex flex-col items-center justify-center w-full max-w-[445px] mx-auto">
|
||||
<div className="w-full flex flex-col items-center gap-6">
|
||||
{/* Header section */}
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<h2 className="text-2xl font-bold text-foreground">Deploy</h2>
|
||||
<p className="text-base text-muted-foreground text-center">
|
||||
Your deployment is configured and ready to go!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
Content sections will be placed here: 1. Repository info card 2.
|
||||
Configuration summary 3. Deploy button
|
||||
{/* ...content here */}
|
||||
{/* <Deploy /> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
'use client'
|
||||
|
||||
import { create } from 'zustand'
|
||||
import { persist, createJSONStorage } from 'zustand/middleware'
|
||||
import type { OnboardingFormData, Step } from './types'
|
||||
|
||||
/**
|
||||
@@ -18,6 +19,7 @@ import type { OnboardingFormData, Step } from './types'
|
||||
* @property {(data: Partial<OnboardingFormData>) => void} setFormData - Updates form data
|
||||
* @property {() => void} nextStep - Moves to the next step
|
||||
* @property {() => void} previousStep - Moves to the previous step
|
||||
* @property {() => void} resetOnboarding - Resets the onboarding state to initial values
|
||||
*/
|
||||
export interface OnboardingState {
|
||||
currentStep: Step
|
||||
@@ -26,10 +28,33 @@ export interface OnboardingState {
|
||||
setFormData: (data: Partial<OnboardingFormData>) => void
|
||||
nextStep: () => void
|
||||
previousStep: () => void
|
||||
resetOnboarding: () => void
|
||||
}
|
||||
|
||||
/** Order of steps in the onboarding flow */
|
||||
const STEP_ORDER: Step[] = ['connect', 'configure', 'deploy']
|
||||
const STEP_ORDER: Step[] = ['connect', 'configure', 'deploy', 'success']
|
||||
|
||||
/** Initial form data values */
|
||||
const initialFormData: OnboardingFormData = {
|
||||
projectName: '',
|
||||
repoName: '',
|
||||
repoDescription: '',
|
||||
framework: '',
|
||||
access: 'public',
|
||||
organizationSlug: '',
|
||||
template: undefined,
|
||||
githubRepo: '',
|
||||
selectedOrg: '',
|
||||
environmentVariables: [],
|
||||
selectedLrn: '',
|
||||
deploymentType: 'lrn',
|
||||
maxPrice: '1000',
|
||||
deployerCount: '1',
|
||||
deploymentId: undefined,
|
||||
deploymentUrl: undefined,
|
||||
projectId: undefined,
|
||||
repositoryUrl: undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Zustand store for managing onboarding state
|
||||
@@ -40,31 +65,58 @@ const STEP_ORDER: Step[] = ['connect', 'configure', 'deploy']
|
||||
* const { currentStep, formData, nextStep } = useOnboarding()
|
||||
* ```
|
||||
*/
|
||||
export const useOnboarding = create<OnboardingState>((set) => ({
|
||||
currentStep: 'connect',
|
||||
formData: {
|
||||
projectName: '',
|
||||
repoName: '',
|
||||
repoDescription: '',
|
||||
framework: '',
|
||||
access: 'public',
|
||||
organizationSlug: ''
|
||||
},
|
||||
setCurrentStep: (step) => set({ currentStep: step }),
|
||||
setFormData: (data) =>
|
||||
set((state) => ({
|
||||
formData: { ...state.formData, ...data }
|
||||
})),
|
||||
nextStep: () =>
|
||||
set((state) => {
|
||||
const currentIndex = STEP_ORDER.indexOf(state.currentStep)
|
||||
const nextStep = STEP_ORDER[currentIndex + 1]
|
||||
return nextStep ? { currentStep: nextStep } : state
|
||||
export const useOnboarding = create<OnboardingState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
currentStep: 'connect',
|
||||
formData: initialFormData,
|
||||
|
||||
setCurrentStep: (step) => set({ currentStep: step }),
|
||||
|
||||
setFormData: (data) => {
|
||||
set((state) => {
|
||||
const newFormData = { ...state.formData, ...data }
|
||||
|
||||
// Keep organizationSlug and selectedOrg in sync
|
||||
if (data.organizationSlug && !data.selectedOrg) {
|
||||
newFormData.selectedOrg = data.organizationSlug
|
||||
}
|
||||
if (data.selectedOrg && !data.organizationSlug) {
|
||||
newFormData.organizationSlug = data.selectedOrg
|
||||
}
|
||||
|
||||
console.log('Store: Setting form data:', data)
|
||||
console.log('Store: New state:', newFormData)
|
||||
|
||||
return { formData: newFormData }
|
||||
})
|
||||
},
|
||||
|
||||
nextStep: () =>
|
||||
set((state) => {
|
||||
const currentIndex = STEP_ORDER.indexOf(state.currentStep)
|
||||
const nextStep = STEP_ORDER[currentIndex + 1]
|
||||
return nextStep ? { currentStep: nextStep } : state
|
||||
}),
|
||||
|
||||
previousStep: () =>
|
||||
set((state) => {
|
||||
const currentIndex = STEP_ORDER.indexOf(state.currentStep)
|
||||
const previousStep = STEP_ORDER[currentIndex - 1]
|
||||
return previousStep ? { currentStep: previousStep } : state
|
||||
}),
|
||||
|
||||
resetOnboarding: () => {
|
||||
set({ currentStep: 'connect', formData: initialFormData })
|
||||
}
|
||||
}),
|
||||
previousStep: () =>
|
||||
set((state) => {
|
||||
const currentIndex = STEP_ORDER.indexOf(state.currentStep)
|
||||
const previousStep = STEP_ORDER[currentIndex - 1]
|
||||
return previousStep ? { currentStep: previousStep } : state
|
||||
})
|
||||
}))
|
||||
{
|
||||
name: 'laconic-onboarding',
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
// Only persist formData, not currentStep
|
||||
partialize: (state) => ({
|
||||
formData: state.formData
|
||||
})
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -1,11 +1,11 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useRouter, useParams } from 'next/navigation'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { CheckCircle } from 'lucide-react'
|
||||
import { useOnboarding } from '@/components/onboarding/store'
|
||||
import { Button } from '@workspace/ui/components/button'
|
||||
import { useOnboarding } from '@/components/onboarding/useOnboarding'
|
||||
import { CheckCircle } from 'lucide-react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export function SuccessStep() {
|
||||
const router = useRouter()
|
||||
@@ -14,45 +14,49 @@ export function SuccessStep() {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const providerParam = params?.provider ? String(params.provider) : 'github'
|
||||
const { formData, resetOnboarding } = useOnboarding()
|
||||
|
||||
|
||||
// Handle hydration mismatch by waiting for mount
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
// Get deployment info from form data
|
||||
const repoName = formData.githubRepo ? formData.githubRepo.split('/').pop() : (formData.projectName || 'project')
|
||||
const deploymentUrl = formData.deploymentUrl || `https://${repoName}.laconic.deploy`
|
||||
const projectId = formData.projectId || 'unknown-id'
|
||||
|
||||
|
||||
// Get deployment info from form data - using available properties
|
||||
const repoName = formData.githubRepo
|
||||
? formData.githubRepo.split('/').pop()
|
||||
: formData.repoName
|
||||
? formData.repoName.split('/').pop()
|
||||
: formData.projectName || 'project'
|
||||
const deploymentUrl = `https://${repoName}.laconic.deploy` // Default deployment URL
|
||||
const projectId = formData.projectId || formData.deploymentId || 'unknown-id' // Use projectId first, fallback to deploymentId
|
||||
|
||||
// Handle "View Project" button - navigates to project page
|
||||
const handleViewProject = () => {
|
||||
console.log('Navigating to project with ID:', projectId)
|
||||
resetOnboarding() // Reset state for next time
|
||||
|
||||
|
||||
// Navigate to the project detail page using the GraphQL project ID
|
||||
router.push(`/projects/${providerParam}/ps/${projectId}`)
|
||||
}
|
||||
|
||||
|
||||
// Auto-redirect after a delay (optional)
|
||||
useEffect(() => {
|
||||
if (mounted && projectId && projectId !== 'unknown-id') {
|
||||
const timer = setTimeout(() => {
|
||||
handleViewProject()
|
||||
}, 3000) // Auto-redirect after 3 seconds
|
||||
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [mounted, projectId])
|
||||
|
||||
|
||||
// Don't render UI until after mount to prevent hydration mismatch
|
||||
if (!mounted) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
// Determine if dark mode is active
|
||||
const isDarkMode = resolvedTheme === 'dark'
|
||||
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center p-8">
|
||||
<div className="max-w-md w-full mx-auto">
|
||||
@@ -60,17 +64,21 @@ export function SuccessStep() {
|
||||
<div className="mx-auto mb-6 flex justify-center">
|
||||
<CheckCircle className="h-16 w-16 text-green-500" />
|
||||
</div>
|
||||
|
||||
|
||||
{/* Success header */}
|
||||
<h2 className={`text-2xl font-medium ${isDarkMode ? "text-white" : "text-zinc-900"} text-center mb-2`}>
|
||||
<h2
|
||||
className={`text-2xl font-medium ${isDarkMode ? 'text-white' : 'text-zinc-900'} text-center mb-2`}
|
||||
>
|
||||
Successfully Deployed!
|
||||
</h2>
|
||||
<p className="text-center text-zinc-500 mb-8">
|
||||
Your project has been deployed successfully
|
||||
</p>
|
||||
|
||||
|
||||
{/* Deployment summary */}
|
||||
<div className={`border rounded-md p-4 mb-6 ${isDarkMode ? "border-zinc-700" : "border-zinc-300"}`}>
|
||||
<div
|
||||
className={`border rounded-md p-4 mb-6 ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Project:</span>
|
||||
@@ -86,39 +94,82 @@ export function SuccessStep() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Next steps section */}
|
||||
<div className="mb-8">
|
||||
<h3 className={`text-lg font-medium ${isDarkMode ? "text-white" : "text-zinc-900"} mb-4`}>Next steps</h3>
|
||||
|
||||
<div className={`border rounded-md overflow-hidden mb-4 ${isDarkMode ? "border-zinc-700" : "border-zinc-300"}`}>
|
||||
<h3
|
||||
className={`text-lg font-medium ${isDarkMode ? 'text-white' : 'text-zinc-900'} mb-4`}
|
||||
>
|
||||
Next steps
|
||||
</h3>
|
||||
|
||||
<div
|
||||
className={`border rounded-md overflow-hidden mb-4 ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}
|
||||
>
|
||||
<div className="flex items-center p-4 justify-between">
|
||||
<div>
|
||||
<div className={isDarkMode ? "text-white font-medium" : "text-zinc-900 font-medium"}>Setup Domain</div>
|
||||
<div className="text-zinc-500 text-sm">Add a custom domain to your project.</div>
|
||||
<div
|
||||
className={
|
||||
isDarkMode
|
||||
? 'text-white font-medium'
|
||||
: 'text-zinc-900 font-medium'
|
||||
}
|
||||
>
|
||||
Setup Domain
|
||||
</div>
|
||||
<div className="text-zinc-500 text-sm">
|
||||
Add a custom domain to your project.
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" className={`rounded-full p-1 w-8 h-8 flex items-center justify-center ${isDarkMode ? "border-zinc-700" : "border-zinc-300"}`}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={isDarkMode ? "text-white" : "text-zinc-900"}>
|
||||
<path d="M9 18L15 12L9 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={`rounded-full p-1 w-8 h-8 flex items-center justify-center ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={isDarkMode ? 'text-white' : 'text-zinc-900'}
|
||||
>
|
||||
<path
|
||||
d="M9 18L15 12L9 6"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex flex-col space-y-3">
|
||||
<Button
|
||||
<Button
|
||||
className="w-full bg-white hover:bg-white/90 text-black flex items-center justify-center"
|
||||
onClick={handleViewProject}
|
||||
disabled={!projectId || projectId === 'unknown-id'}
|
||||
>
|
||||
View Project
|
||||
<svg className="ml-2 h-4 w-4" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 12H19M19 12L13 6M19 12L13 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
View Project
|
||||
<svg
|
||||
className="ml-2 h-4 w-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5 12H19M19 12L13 6M19 12L13 18"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
|
||||
|
||||
{/* Manual navigation button if auto-redirect fails */}
|
||||
{projectId === 'unknown-id' && (
|
||||
<p className="text-center text-xs text-muted-foreground">
|
||||
@@ -129,4 +180,4 @@ export function SuccessStep() {
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,18 @@ export type Step = 'connect' | 'configure' | 'deploy' | 'success'
|
||||
* @property {string} framework - Framework used for the project
|
||||
* @property {string} access - Access level of the repository
|
||||
* @property {string} organizationSlug - Organization slug
|
||||
* @property {Template | undefined} template - Selected template for deployment
|
||||
* @property {string} githubRepo - GitHub repository path (owner/repo)
|
||||
* @property {string} selectedOrg - Selected organization for deployment
|
||||
* @property {EnvironmentVariable[]} environmentVariables - Environment variables for deployment
|
||||
* @property {string} selectedLrn - Selected LRN for deployment
|
||||
* @property {string} deploymentType - Type of deployment (lrn, wallet, etc.)
|
||||
* @property {string} maxPrice - Maximum price for deployment
|
||||
* @property {string} deployerCount - Number of deployers
|
||||
* @property {string} deploymentId - ID of the deployment after creation
|
||||
* @property {string} deploymentUrl - URL of the deployed project
|
||||
* @property {string} projectId - ID of the created project
|
||||
* @property {string} repositoryUrl - URL of the repository
|
||||
*/
|
||||
export interface OnboardingFormData {
|
||||
projectName: string
|
||||
@@ -26,6 +38,18 @@ export interface OnboardingFormData {
|
||||
framework: string
|
||||
access: 'public' | 'private'
|
||||
organizationSlug: string
|
||||
template?: Template
|
||||
githubRepo: string
|
||||
selectedOrg: string
|
||||
environmentVariables: EnvironmentVariable[]
|
||||
selectedLrn: string
|
||||
deploymentType: string
|
||||
maxPrice: string
|
||||
deployerCount: string
|
||||
deploymentId?: string
|
||||
deploymentUrl?: string
|
||||
projectId?: string
|
||||
repositoryUrl?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,12 +78,16 @@ export interface Repository {
|
||||
* @property {string} name - Template name
|
||||
* @property {string} [description] - Template description
|
||||
* @property {string} [thumbnail] - Template thumbnail URL
|
||||
* @property {string} [repoFullName] - Full repository name for the template
|
||||
* @property {any} [icon] - Template icon
|
||||
*/
|
||||
export interface Template {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
thumbnail?: string
|
||||
repoFullName?: string
|
||||
icon?: any
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,9 +109,11 @@ export interface DeploymentType {
|
||||
* @property {string} key - Environment variable key
|
||||
* @property {string} value - Environment variable value
|
||||
* @property {boolean} [isSecret] - Whether the variable is a secret
|
||||
* @property {string[]} environments - Environment names where this variable applies
|
||||
*/
|
||||
export interface EnvironmentVariable {
|
||||
key: string
|
||||
value: string
|
||||
isSecret?: boolean
|
||||
environments: string[]
|
||||
}
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
// src/components/onboarding/useOnboarding.ts
|
||||
import { useState } from 'react'
|
||||
import type {
|
||||
OnboardingFormData} from '@/types'
|
||||
|
||||
// Step type for navigation
|
||||
export type Step = 'connect' | 'configure' | 'deploy' | 'success'
|
||||
|
||||
// Initial form data
|
||||
const initialFormData: OnboardingFormData = {
|
||||
// Step 1: Connect
|
||||
githubRepo: undefined,
|
||||
template: undefined,
|
||||
projectName: undefined,
|
||||
deploymentMode: undefined,
|
||||
|
||||
// Step 2: Configure
|
||||
selectedLrn: undefined,
|
||||
selectedOrg: undefined, // Add this property
|
||||
environmentVariables: [], // Use array type
|
||||
deploymentType: undefined, // Add missing properties
|
||||
deployerCount: undefined,
|
||||
maxPrice: undefined,
|
||||
|
||||
// Step 3: Deploy
|
||||
deploymentId: undefined,
|
||||
repositoryUrl: undefined,
|
||||
deploymentUrl: undefined, // Add missing property
|
||||
projectId: undefined // Add missing property
|
||||
}
|
||||
|
||||
export function useOnboarding() {
|
||||
const [formData, setFormData] = useState<OnboardingFormData>(initialFormData)
|
||||
const [currentStep, setCurrentStep] = useState<Step>('connect')
|
||||
|
||||
// Update form data (partial update)
|
||||
const updateFormData = (updates: Partial<OnboardingFormData>) => {
|
||||
setFormData(prev => ({ ...prev, ...updates }))
|
||||
}
|
||||
|
||||
// Reset form data
|
||||
const resetFormData = () => {
|
||||
setFormData(initialFormData)
|
||||
setCurrentStep('connect')
|
||||
}
|
||||
|
||||
// Step navigation
|
||||
const nextStep = () => {
|
||||
switch (currentStep) {
|
||||
case 'connect':
|
||||
setCurrentStep('configure')
|
||||
break
|
||||
case 'configure':
|
||||
setCurrentStep('deploy')
|
||||
break
|
||||
case 'deploy':
|
||||
setCurrentStep('success')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const previousStep = () => {
|
||||
switch (currentStep) {
|
||||
case 'configure':
|
||||
setCurrentStep('connect')
|
||||
break
|
||||
case 'deploy':
|
||||
setCurrentStep('configure')
|
||||
break
|
||||
case 'success':
|
||||
setCurrentStep('deploy')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Validation helpers
|
||||
const validateStep1 = () => {
|
||||
if (formData.deploymentMode === 'template') {
|
||||
return !!(formData.template && formData.projectName)
|
||||
}
|
||||
return !!formData.githubRepo
|
||||
}
|
||||
|
||||
const validateStep2 = () => {
|
||||
return !!(formData.selectedLrn && formData.selectedOrg)
|
||||
}
|
||||
|
||||
const validateStep3 = () => {
|
||||
return !!formData.deploymentId
|
||||
}
|
||||
|
||||
return {
|
||||
// Data
|
||||
formData,
|
||||
currentStep,
|
||||
|
||||
// Actions
|
||||
setFormData: updateFormData,
|
||||
setCurrentStep,
|
||||
resetFormData,
|
||||
resetOnboarding: resetFormData, // Alias for compatibility
|
||||
nextStep,
|
||||
previousStep,
|
||||
|
||||
// Validation
|
||||
validateStep1,
|
||||
validateStep2,
|
||||
validateStep3,
|
||||
|
||||
// Store-like interface for compatibility
|
||||
getState: () => ({
|
||||
formData,
|
||||
currentStep,
|
||||
nextStep,
|
||||
previousStep,
|
||||
setFormData: updateFormData,
|
||||
resetFormData
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
// src/hooks/useAuthStatus.tsx
|
||||
'use client'
|
||||
|
||||
import { useAuth, useUser } from '@clerk/nextjs'
|
||||
import { useWallet } from '@/context/WalletContext' // Use the full provider!
|
||||
import { useBackend } from '@/context/BackendContext'
|
||||
import { useGQLClient } from '@/context'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useBackend } from '@/context/BackendContext'
|
||||
import { useWallet } from '@/context/WalletContext' // Use the full provider!
|
||||
import { useAuth, useUser } from '@clerk/nextjs'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
/**
|
||||
* @interface AuthStatus
|
||||
@@ -20,8 +20,8 @@ export interface AuthStatus {
|
||||
user: any
|
||||
}
|
||||
wallet: {
|
||||
isConnected: boolean // SIWE authenticated + backend session
|
||||
hasAddress: boolean // Just has wallet address
|
||||
isConnected: boolean // SIWE authenticated + backend session
|
||||
hasAddress: boolean // Just has wallet address
|
||||
wallet: any
|
||||
}
|
||||
backend: {
|
||||
@@ -29,11 +29,11 @@ export interface AuthStatus {
|
||||
hasGithubAuth: boolean
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
|
||||
// Computed status
|
||||
isFullyAuthenticated: boolean
|
||||
isReady: boolean
|
||||
|
||||
|
||||
// What's missing (for UI feedback)
|
||||
missing: {
|
||||
clerkSignIn: boolean
|
||||
@@ -42,7 +42,7 @@ export interface AuthStatus {
|
||||
backendConnection: boolean
|
||||
githubBackendSync: boolean
|
||||
}
|
||||
|
||||
|
||||
// Progress (for UI indicators)
|
||||
progress: {
|
||||
completed: number
|
||||
@@ -58,7 +58,7 @@ export interface AuthStatus {
|
||||
export interface AuthActions {
|
||||
// Wallet actions
|
||||
connectWallet: () => Promise<void>
|
||||
|
||||
|
||||
// Combined actions
|
||||
refreshAllStatus: () => Promise<void>
|
||||
checkGithubBackendAuth: () => Promise<boolean>
|
||||
@@ -73,35 +73,52 @@ export function useAuthStatus(): AuthStatus & AuthActions {
|
||||
// Clerk authentication
|
||||
const { isSignedIn, isLoaded: isClerkLoaded } = useAuth()
|
||||
const { user, isLoaded: isUserLoaded } = useUser()
|
||||
|
||||
|
||||
// Wallet authentication
|
||||
const {
|
||||
isConnected: isWalletSessionActive, // SIWE authenticated
|
||||
hasWalletAddress,
|
||||
wallet,
|
||||
connect: connectWallet
|
||||
const {
|
||||
isConnected: isWalletSessionActive, // SIWE authenticated
|
||||
hasWalletAddress,
|
||||
wallet,
|
||||
connect: connectWallet
|
||||
} = useWallet()
|
||||
|
||||
|
||||
// Backend authentication
|
||||
const {
|
||||
isBackendConnected,
|
||||
isLoading: isBackendLoading,
|
||||
refreshStatus: refreshBackendStatus
|
||||
} = useBackend()
|
||||
|
||||
|
||||
// GraphQL client for checking GitHub backend auth
|
||||
const gqlClient = useGQLClient()
|
||||
|
||||
|
||||
// GitHub backend auth state
|
||||
const [isGithubBackendAuth, setIsGithubBackendAuth] = useState(false)
|
||||
const [isCheckingGithubAuth, setIsCheckingGithubAuth] = useState(false)
|
||||
const lastGithubCheckRef = useRef(0)
|
||||
const isCheckingRef = useRef(false)
|
||||
|
||||
// Stable status to prevent rapid UI changes
|
||||
const [stableAuthStatus, setStableAuthStatus] = useState({
|
||||
isFullyAuthenticated: false,
|
||||
lastUpdate: 0
|
||||
})
|
||||
|
||||
// Check GitHub backend auth via GraphQL
|
||||
const checkGithubBackendAuth = useCallback(async (): Promise<boolean> => {
|
||||
if (!isBackendConnected) return false
|
||||
|
||||
// Prevent multiple rapid calls - only allow once every 3 seconds
|
||||
const now = Date.now()
|
||||
if (isCheckingRef.current || (now - lastGithubCheckRef.current < 3000)) {
|
||||
return isGithubBackendAuth
|
||||
}
|
||||
|
||||
try {
|
||||
isCheckingRef.current = true
|
||||
setIsCheckingGithubAuth(true)
|
||||
lastGithubCheckRef.current = now
|
||||
|
||||
const userData = await gqlClient.getUser()
|
||||
const hasGitHubToken = !!userData.user.gitHubToken
|
||||
setIsGithubBackendAuth(hasGitHubToken)
|
||||
@@ -111,10 +128,11 @@ export function useAuthStatus(): AuthStatus & AuthActions {
|
||||
setIsGithubBackendAuth(false)
|
||||
return false
|
||||
} finally {
|
||||
isCheckingRef.current = false
|
||||
setIsCheckingGithubAuth(false)
|
||||
}
|
||||
}, [isBackendConnected, gqlClient])
|
||||
|
||||
}, [isBackendConnected, isGithubBackendAuth]) // Minimal dependencies
|
||||
|
||||
// Check GitHub auth when backend connection changes
|
||||
useEffect(() => {
|
||||
if (isBackendConnected) {
|
||||
@@ -122,9 +140,9 @@ export function useAuthStatus(): AuthStatus & AuthActions {
|
||||
} else {
|
||||
setIsGithubBackendAuth(false)
|
||||
}
|
||||
}, [isBackendConnected, checkGithubBackendAuth])
|
||||
|
||||
// Check backend connection when wallet session is active (SIWE completed)
|
||||
}, [isBackendConnected]) // Remove checkGithubBackendAuth from dependencies to prevent infinite loop
|
||||
|
||||
// Check backend connection when wallet session is active (SIWE completed)
|
||||
useEffect(() => {
|
||||
if (isWalletSessionActive) {
|
||||
// Wait a moment for wallet session to be established, then check backend
|
||||
@@ -133,13 +151,16 @@ export function useAuthStatus(): AuthStatus & AuthActions {
|
||||
}, 1000)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [isWalletSessionActive, refreshBackendStatus])
|
||||
|
||||
}, [isWalletSessionActive]) // Remove refreshBackendStatus from dependencies to prevent rapid retriggers
|
||||
|
||||
// Check if GitHub is connected in Clerk
|
||||
const hasGithubInClerk = user?.externalAccounts?.find(
|
||||
account => account.provider === 'github' || account.verification?.strategy === 'oauth_github'
|
||||
) !== undefined
|
||||
|
||||
const hasGithubInClerk =
|
||||
user?.externalAccounts?.find(
|
||||
(account) =>
|
||||
account.provider === 'github' ||
|
||||
account.verification?.strategy === 'oauth_github'
|
||||
) !== undefined
|
||||
|
||||
// Calculate what's missing
|
||||
const missing = {
|
||||
clerkSignIn: !isSignedIn,
|
||||
@@ -148,32 +169,50 @@ export function useAuthStatus(): AuthStatus & AuthActions {
|
||||
backendConnection: hasWalletAddress && !isWalletSessionActive, // Need SIWE auth for backend
|
||||
githubBackendSync: isBackendConnected && !isGithubBackendAuth
|
||||
}
|
||||
|
||||
|
||||
// Calculate progress
|
||||
const authSteps = [
|
||||
isSignedIn, // Clerk sign in
|
||||
hasGithubInClerk, // GitHub connected to Clerk
|
||||
hasWalletAddress, // Wallet address obtained
|
||||
isWalletSessionActive, // SIWE authentication completed
|
||||
isGithubBackendAuth // GitHub synced to backend
|
||||
isSignedIn, // Clerk sign in
|
||||
hasGithubInClerk, // GitHub connected to Clerk
|
||||
hasWalletAddress, // Wallet address obtained
|
||||
isWalletSessionActive, // SIWE authentication completed
|
||||
isGithubBackendAuth // GitHub synced to backend
|
||||
]
|
||||
|
||||
|
||||
const completedSteps = authSteps.filter(Boolean).length
|
||||
const totalSteps = authSteps.length
|
||||
const progressPercentage = Math.round((completedSteps / totalSteps) * 100)
|
||||
|
||||
|
||||
// Determine if fully authenticated
|
||||
const isFullyAuthenticated = authSteps.every(Boolean)
|
||||
const currentIsFullyAuthenticated = authSteps.every(Boolean)
|
||||
|
||||
// Debounce authentication status changes to prevent flickering
|
||||
useEffect(() => {
|
||||
const now = Date.now()
|
||||
const timeSinceLastUpdate = now - stableAuthStatus.lastUpdate
|
||||
|
||||
// Only update if status actually changed and enough time has passed (300ms debounce)
|
||||
if (currentIsFullyAuthenticated !== stableAuthStatus.isFullyAuthenticated && timeSinceLastUpdate > 300) {
|
||||
setStableAuthStatus({
|
||||
isFullyAuthenticated: currentIsFullyAuthenticated,
|
||||
lastUpdate: now
|
||||
})
|
||||
}
|
||||
}, [currentIsFullyAuthenticated, stableAuthStatus])
|
||||
|
||||
// Use stable status for UI
|
||||
const isFullyAuthenticated = stableAuthStatus.isFullyAuthenticated
|
||||
|
||||
// Determine if ready (all auth systems loaded)
|
||||
const isReady = isClerkLoaded && isUserLoaded && !isBackendLoading && !isCheckingGithubAuth
|
||||
|
||||
const isReady =
|
||||
isClerkLoaded && isUserLoaded && !isBackendLoading && !isCheckingGithubAuth
|
||||
|
||||
// Combined refresh action
|
||||
const refreshAllStatus = async () => {
|
||||
await refreshBackendStatus()
|
||||
await checkGithubBackendAuth()
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
// Individual systems
|
||||
clerk: {
|
||||
@@ -192,24 +231,24 @@ export function useAuthStatus(): AuthStatus & AuthActions {
|
||||
hasGithubAuth: isGithubBackendAuth,
|
||||
isLoading: isBackendLoading || isCheckingGithubAuth
|
||||
},
|
||||
|
||||
|
||||
// Computed status
|
||||
isFullyAuthenticated,
|
||||
isReady,
|
||||
|
||||
|
||||
// Missing items
|
||||
missing,
|
||||
|
||||
|
||||
// Progress
|
||||
progress: {
|
||||
completed: completedSteps,
|
||||
total: totalSteps,
|
||||
percentage: progressPercentage
|
||||
},
|
||||
|
||||
|
||||
// Actions
|
||||
connectWallet,
|
||||
refreshAllStatus,
|
||||
checkGithubBackendAuth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
'use client'
|
||||
|
||||
import { getGitHubToken } from '@/actions/github'
|
||||
import { Octokit } from '@octokit/rest'
|
||||
import { useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
interface CreateRepoFromTemplateParams {
|
||||
templateOwner: string
|
||||
templateRepo: string
|
||||
name: string
|
||||
description?: string
|
||||
isPrivate?: boolean
|
||||
}
|
||||
|
||||
interface CreateRepoResult {
|
||||
success: boolean
|
||||
repositoryUrl?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to directly interact with GitHub API using user's own token
|
||||
* Bypasses backend GitHub integration issues
|
||||
*/
|
||||
export function useDirectGitHub() {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const createRepoFromTemplate = async (
|
||||
params: CreateRepoFromTemplateParams
|
||||
): Promise<CreateRepoResult> => {
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
console.log(
|
||||
'🔄 Creating repository from template directly via GitHub API...'
|
||||
)
|
||||
console.log('📋 Parameters:', params)
|
||||
|
||||
// Get user's GitHub token
|
||||
const token = await getGitHubToken()
|
||||
if (!token) {
|
||||
throw new Error(
|
||||
'GitHub token not available. Please reconnect your GitHub account.'
|
||||
)
|
||||
}
|
||||
|
||||
// Create Octokit instance with user's token
|
||||
const octokit = new Octokit({ auth: token })
|
||||
|
||||
// Get the authenticated user's info to use as owner
|
||||
const { data: authUser } = await octokit.rest.users.getAuthenticated()
|
||||
console.log('👤 Authenticated GitHub user:', authUser.login)
|
||||
|
||||
// Create repository from template
|
||||
const { data: newRepo } = await octokit.rest.repos.createUsingTemplate({
|
||||
template_owner: params.templateOwner,
|
||||
template_repo: params.templateRepo,
|
||||
owner: authUser.login, // Use authenticated user as owner
|
||||
name: params.name,
|
||||
description:
|
||||
params.description ||
|
||||
`Created from ${params.templateOwner}/${params.templateRepo}`,
|
||||
private: params.isPrivate || false,
|
||||
include_all_branches: false
|
||||
})
|
||||
|
||||
console.log('✅ Repository created successfully:', newRepo.html_url)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
repositoryUrl: newRepo.html_url
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to create repository from template:', error)
|
||||
|
||||
let errorMessage = 'Failed to create repository from template'
|
||||
if (error instanceof Error) {
|
||||
errorMessage = error.message
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getUserRepos = async () => {
|
||||
try {
|
||||
const token = await getGitHubToken()
|
||||
if (!token) {
|
||||
throw new Error('GitHub token not available')
|
||||
}
|
||||
|
||||
const octokit = new Octokit({ auth: token })
|
||||
const { data: repos } = await octokit.rest.repos.listForAuthenticatedUser(
|
||||
{
|
||||
sort: 'updated',
|
||||
per_page: 100
|
||||
}
|
||||
)
|
||||
|
||||
return repos
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to fetch user repositories:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
createRepoFromTemplate,
|
||||
getUserRepos,
|
||||
isLoading
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
// src/hooks/useDirectTemplateDeployment.tsx
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useGQLClient } from '@/context'
|
||||
import { useWallet } from '@/context/WalletContext'
|
||||
import { useUser } from '@clerk/nextjs'
|
||||
import { toast } from 'sonner'
|
||||
import type { TemplateDetail } from '@/constants/templates'
|
||||
import { useDirectGitHub } from './useDirectGitHub'
|
||||
import { getGitHubToken } from '@/actions/github'
|
||||
|
||||
export interface TemplateDeploymentConfig {
|
||||
template: TemplateDetail
|
||||
projectName: string
|
||||
organizationSlug: string
|
||||
environmentVariables?: Array<{
|
||||
key: string
|
||||
value: string
|
||||
environments: string[]
|
||||
}>
|
||||
deployerLrn?: string
|
||||
}
|
||||
|
||||
export interface TemplateDeploymentResult {
|
||||
projectId: string
|
||||
repositoryUrl: string
|
||||
deploymentUrl?: string
|
||||
deploymentId?: string
|
||||
}
|
||||
|
||||
export function useDirectTemplateDeployment() {
|
||||
const [isDeploying, setIsDeploying] = useState(false)
|
||||
const [deploymentResult, setDeploymentResult] =
|
||||
useState<TemplateDeploymentResult | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const gqlClient = useGQLClient()
|
||||
const { wallet } = useWallet()
|
||||
const { user } = useUser()
|
||||
const directGitHub = useDirectGitHub()
|
||||
|
||||
const deployTemplate = async (
|
||||
config: TemplateDeploymentConfig
|
||||
): Promise<TemplateDeploymentResult> => {
|
||||
setIsDeploying(true)
|
||||
setError(null)
|
||||
setDeploymentResult(null)
|
||||
|
||||
try {
|
||||
console.log('🚀 Starting direct template deployment:', config)
|
||||
|
||||
// Validate required data
|
||||
if (!wallet?.address) {
|
||||
throw new Error('Wallet not connected')
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw new Error('User not authenticated')
|
||||
}
|
||||
|
||||
// Get GitHub username from Clerk external accounts
|
||||
const githubAccount = user.externalAccounts?.find(
|
||||
(account) => account.provider === 'github'
|
||||
)
|
||||
const githubUsername = githubAccount?.username
|
||||
|
||||
if (!githubUsername) {
|
||||
throw new Error('GitHub account not connected')
|
||||
}
|
||||
|
||||
console.log('🔍 GitHub user info:', {
|
||||
githubUsername,
|
||||
githubAccount: githubAccount?.username,
|
||||
userExternalAccounts: user.externalAccounts?.length
|
||||
})
|
||||
|
||||
// Parse template repository (format: "owner/repo")
|
||||
const [templateOwner, templateRepo] =
|
||||
config.template.repoFullName.split('/')
|
||||
if (!templateOwner || !templateRepo) {
|
||||
throw new Error('Invalid template repository format')
|
||||
}
|
||||
|
||||
console.log('🔍 Template parsing details:', {
|
||||
originalTemplate: config.template.repoFullName,
|
||||
parsedOwner: templateOwner,
|
||||
parsedRepo: templateRepo,
|
||||
templateId: config.template.id,
|
||||
templateName: config.template.name
|
||||
})
|
||||
|
||||
toast.info('Creating repository from template...')
|
||||
|
||||
// STEP 1: Create repository directly via GitHub API with user's token
|
||||
const repoResult = await directGitHub.createRepoFromTemplate({
|
||||
templateOwner,
|
||||
templateRepo,
|
||||
name: config.projectName,
|
||||
description: `Created from ${config.template.name} template`,
|
||||
isPrivate: false
|
||||
})
|
||||
|
||||
if (!repoResult.success || !repoResult.repositoryUrl) {
|
||||
throw new Error(
|
||||
repoResult.error || 'Failed to create repository from template'
|
||||
)
|
||||
}
|
||||
|
||||
console.log(
|
||||
'✅ Repository created successfully:',
|
||||
repoResult.repositoryUrl
|
||||
)
|
||||
toast.success('Repository created from template!')
|
||||
|
||||
// STEP 2: Create project in backend using the newly created repository
|
||||
console.log('🔍 Preparing backend project creation...')
|
||||
console.log('🔍 Organization slug:', config.organizationSlug)
|
||||
console.log('🔍 Deployer LRN:', config.deployerLrn || 'undefined')
|
||||
console.log(
|
||||
'🔍 Environment variables:',
|
||||
config.environmentVariables || []
|
||||
)
|
||||
|
||||
toast.info('Setting up project deployment...')
|
||||
|
||||
// Add a delay to ensure the repository is fully created and accessible
|
||||
console.log('⏳ Waiting for repository to be fully accessible...')
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000))
|
||||
|
||||
// STEP 2a: Verify GitHub token availability
|
||||
console.log('🔄 Verifying GitHub token for user:', githubUsername)
|
||||
const currentClerkToken = await getGitHubToken()
|
||||
if (!currentClerkToken) {
|
||||
throw new Error(
|
||||
`GitHub token not available for user ${githubUsername}. Please reconnect your GitHub account.`
|
||||
)
|
||||
}
|
||||
console.log('✅ GitHub token verified for user:', githubUsername)
|
||||
|
||||
try {
|
||||
// Get available deployers
|
||||
console.log('🔍 Fetching available deployers...')
|
||||
const deployersResult = await gqlClient.getDeployers()
|
||||
const availableDeployers = deployersResult.deployers || []
|
||||
|
||||
if (availableDeployers.length === 0) {
|
||||
throw new Error(
|
||||
'No deployers available. Please configure at least one deployer in the backend.'
|
||||
)
|
||||
}
|
||||
|
||||
// Use the first available deployer if none specified
|
||||
const deployerToUse =
|
||||
config.deployerLrn || availableDeployers[0]?.deployerLrn
|
||||
|
||||
if (!deployerToUse) {
|
||||
throw new Error('No valid deployer found')
|
||||
}
|
||||
|
||||
console.log('🔍 Using deployer:', deployerToUse)
|
||||
|
||||
// Get the backend's wallet address for blockchain transactions
|
||||
const backendAddress = await gqlClient.getAddress()
|
||||
console.log('🔍 Backend wallet address:', backendAddress)
|
||||
console.log('🔍 Frontend wallet address:', wallet.address)
|
||||
|
||||
// Use backend's address for blockchain transactions
|
||||
const projectData = {
|
||||
name: config.projectName,
|
||||
repository: `${githubUsername}/${config.projectName}`,
|
||||
prodBranch: 'main',
|
||||
template: config.template.id,
|
||||
paymentAddress: backendAddress,
|
||||
txHash:
|
||||
'0x0000000000000000000000000000000000000000000000000000000000000000'
|
||||
}
|
||||
|
||||
console.log(
|
||||
'📤 Final project data being sent:',
|
||||
JSON.stringify(projectData, null, 2)
|
||||
)
|
||||
console.log('📤 With deployer:', deployerToUse)
|
||||
console.log('📤 Organization slug:', config.organizationSlug)
|
||||
console.log(
|
||||
'📤 Environment variables:',
|
||||
config.environmentVariables || []
|
||||
)
|
||||
|
||||
// Log the exact GraphQL variables being sent
|
||||
const mutationVariables = {
|
||||
organizationSlug: config.organizationSlug,
|
||||
data: projectData,
|
||||
lrn: deployerToUse,
|
||||
auctionParams: undefined,
|
||||
environmentVariables: config.environmentVariables || []
|
||||
}
|
||||
|
||||
console.log(
|
||||
'🔍 EXACT GraphQL variables being sent:',
|
||||
JSON.stringify(mutationVariables, null, 2)
|
||||
)
|
||||
|
||||
const projectResult = await gqlClient.addProject(
|
||||
config.organizationSlug,
|
||||
projectData,
|
||||
deployerToUse,
|
||||
undefined, // auctionParams
|
||||
config.environmentVariables || []
|
||||
)
|
||||
|
||||
console.log('✅ Backend response received:', projectResult)
|
||||
console.log('🔍 Project ID:', projectResult.addProject?.id)
|
||||
console.log('🔍 Full project object:', projectResult.addProject)
|
||||
|
||||
if (!projectResult.addProject?.id) {
|
||||
console.error(
|
||||
'❌ No project ID in response. Full response:',
|
||||
projectResult
|
||||
)
|
||||
throw new Error(
|
||||
'Failed to set up project deployment in backend - no project ID returned'
|
||||
)
|
||||
}
|
||||
|
||||
console.log(
|
||||
'✅ Project created successfully with ID:',
|
||||
projectResult.addProject.id
|
||||
)
|
||||
|
||||
// Create and return the result
|
||||
const result: TemplateDeploymentResult = {
|
||||
projectId: projectResult.addProject.id,
|
||||
repositoryUrl: repoResult.repositoryUrl,
|
||||
deploymentUrl: undefined, // Will be populated once deployment completes
|
||||
deploymentId: projectResult.addProject.id
|
||||
}
|
||||
|
||||
setDeploymentResult(result)
|
||||
toast.success('Template deployed successfully!')
|
||||
|
||||
return result
|
||||
} catch (backendError) {
|
||||
console.error('❌ Backend project creation failed:', backendError)
|
||||
|
||||
// Enhanced error handling
|
||||
let errorMessage = 'Unknown deployment error'
|
||||
let errorDetails = {
|
||||
message: 'Unknown error',
|
||||
stack: undefined as string | undefined,
|
||||
name: 'UnknownError'
|
||||
}
|
||||
|
||||
if (backendError instanceof Error) {
|
||||
errorMessage = backendError.message
|
||||
errorDetails = {
|
||||
message: backendError.message,
|
||||
stack: backendError.stack,
|
||||
name: backendError.name
|
||||
}
|
||||
} else if (typeof backendError === 'object' && backendError !== null) {
|
||||
const errorObj = backendError as any
|
||||
errorMessage = errorObj.message || JSON.stringify(backendError)
|
||||
errorDetails = {
|
||||
message: errorObj.message || errorMessage,
|
||||
stack: errorObj.stack,
|
||||
name: errorObj.name || 'BackendError'
|
||||
}
|
||||
}
|
||||
|
||||
console.error('❌ Error details:', errorDetails)
|
||||
|
||||
// Provide more specific error messages
|
||||
if (
|
||||
errorMessage.includes('Cannot return null for non-nullable field')
|
||||
) {
|
||||
errorMessage = `Backend validation error: A required field is missing or invalid.
|
||||
|
||||
Possible issues:
|
||||
1. Invalid template ID: "${config.template.id}"
|
||||
2. Repository format issue: "${repoResult.repositoryUrl}"
|
||||
3. Missing deployer configuration
|
||||
4. Organization "${config.organizationSlug}" not found
|
||||
|
||||
Repository was created successfully: ${repoResult.repositoryUrl}
|
||||
You may need to check the backend configuration or contact support.`
|
||||
}
|
||||
|
||||
throw new Error(`Backend deployment setup failed: ${errorMessage}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Template deployment failed:', error)
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Unknown error'
|
||||
setError(errorMessage)
|
||||
toast.error(`Template deployment failed: ${errorMessage}`)
|
||||
throw error
|
||||
} finally {
|
||||
setIsDeploying(false)
|
||||
}
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
setDeploymentResult(null)
|
||||
setError(null)
|
||||
setIsDeploying(false)
|
||||
}
|
||||
|
||||
return {
|
||||
deployTemplate,
|
||||
isDeploying: isDeploying || directGitHub.isLoading,
|
||||
deploymentResult,
|
||||
error,
|
||||
reset
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
'use client'
|
||||
|
||||
import { getGitHubToken } from '@/actions/github'
|
||||
import { useAuth } from '@clerk/nextjs'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
interface UseGitHubTokenReturn {
|
||||
token: string | null
|
||||
isLoading: boolean
|
||||
error: string | null
|
||||
refreshToken: () => Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* A hook to get the current user's GitHub OAuth token from Clerk
|
||||
* This ensures each user gets their own token for GitHub API calls
|
||||
*/
|
||||
export function useGitHubToken(): UseGitHubTokenReturn {
|
||||
const [token, setToken] = useState<string | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const { isLoaded: isAuthLoaded, userId } = useAuth()
|
||||
|
||||
const fetchToken = async () => {
|
||||
if (!userId) {
|
||||
setError('User not authenticated')
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
const userToken = await getGitHubToken()
|
||||
|
||||
if (!userToken) {
|
||||
setError(
|
||||
'GitHub account not connected. Please connect your GitHub account.'
|
||||
)
|
||||
setToken(null)
|
||||
} else {
|
||||
console.log('✅ Successfully retrieved user-specific GitHub token')
|
||||
setToken(userToken)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('❌ Error getting GitHub token:', err)
|
||||
setError(
|
||||
err instanceof Error ? err.message : 'Failed to get GitHub token'
|
||||
)
|
||||
setToken(null)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthLoaded) {
|
||||
fetchToken()
|
||||
}
|
||||
}, [isAuthLoaded, userId])
|
||||
|
||||
const refreshToken = async () => {
|
||||
await fetchToken()
|
||||
}
|
||||
|
||||
return { token, isLoading, error, refreshToken }
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// src/hooks/usePayment.ts
|
||||
|
||||
import { useState, useCallback } from 'react'
|
||||
import { useWallet } from '@/context/WalletContext'
|
||||
import { useGQLClient } from '@/context'
|
||||
import { paymentService } from '@/services/paymentService'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
interface PaymentConfig {
|
||||
deploymentType: 'lrn' | 'auction'
|
||||
selectedDeployer?: { minimumPayment?: string }
|
||||
maxPrice?: string
|
||||
deployerCount?: string
|
||||
}
|
||||
|
||||
interface PaymentResult {
|
||||
txHash: string
|
||||
success: boolean
|
||||
}
|
||||
|
||||
export function usePayment() {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [isModalVisible, setIsModalVisible] = useState(false)
|
||||
const { wallet } = useWallet()
|
||||
const gqlClient = useGQLClient()
|
||||
|
||||
const processPayment = useCallback(async (
|
||||
config: PaymentConfig
|
||||
): Promise<PaymentResult | null> => {
|
||||
if (!wallet?.address) {
|
||||
throw new Error('Wallet not connected')
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
// Calculate payment amount
|
||||
const amount = paymentService.calculatePaymentAmount(config)
|
||||
|
||||
// Check if payment is actually required
|
||||
if (config.deploymentType === 'lrn' && (!config.selectedDeployer?.minimumPayment || amount === '0')) {
|
||||
// No payment required for this LRN deployer
|
||||
console.log('No payment required for this deployer')
|
||||
return {
|
||||
txHash: '', // Empty string for no payment
|
||||
success: true
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Processing payment:', {
|
||||
amount,
|
||||
from: wallet.address,
|
||||
deploymentType: config.deploymentType
|
||||
})
|
||||
|
||||
// Get backend wallet address
|
||||
const recipientAddress = await gqlClient.getAddress()
|
||||
console.log('Backend recipient address:', recipientAddress)
|
||||
|
||||
toast.info('Requesting payment approval from wallet...')
|
||||
|
||||
// Get chainId from environment
|
||||
const chainId = process.env.NEXT_PUBLIC_LACONICD_CHAIN_ID || 'laconic-mainnet'
|
||||
|
||||
// Send payment via wallet iframe
|
||||
const paymentResult = await paymentService.sendPayment({
|
||||
amount,
|
||||
senderAddress: wallet.address,
|
||||
recipientAddress,
|
||||
chainId
|
||||
}, setIsModalVisible)
|
||||
|
||||
if (!paymentResult.success || !paymentResult.txHash) {
|
||||
throw new Error(paymentResult.error || 'Payment failed')
|
||||
}
|
||||
|
||||
console.log('Payment successful, txHash:', paymentResult.txHash)
|
||||
toast.info('Verifying transaction...')
|
||||
|
||||
// Verify transaction via backend
|
||||
const isVerified = await gqlClient.verifyTx(
|
||||
paymentResult.txHash,
|
||||
`${amount}alnt`,
|
||||
wallet.address
|
||||
)
|
||||
|
||||
if (!isVerified) {
|
||||
throw new Error('Transaction verification failed')
|
||||
}
|
||||
|
||||
toast.success('Payment verified successfully!')
|
||||
|
||||
return {
|
||||
txHash: paymentResult.txHash,
|
||||
success: true
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Payment failed'
|
||||
console.error('Payment error:', err)
|
||||
setError(errorMessage)
|
||||
toast.error(errorMessage)
|
||||
return null
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
setIsModalVisible(false)
|
||||
}
|
||||
}, [wallet, gqlClient])
|
||||
|
||||
const calculateAmount = useCallback((config: PaymentConfig): string => {
|
||||
return paymentService.calculatePaymentAmount(config)
|
||||
}, [])
|
||||
|
||||
return {
|
||||
processPayment,
|
||||
calculateAmount,
|
||||
isLoading,
|
||||
isModalVisible,
|
||||
setIsModalVisible,
|
||||
error
|
||||
}
|
||||
}
|
||||
@@ -1,138 +1,127 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useAuth, useUser } from "@clerk/nextjs";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import { useOctokit } from '@/context/OctokitContext'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
// Define the return type of the hook
|
||||
interface UseRepoDataReturn {
|
||||
repoData: any;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
repoData: any
|
||||
isLoading: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* A hook to fetch repository data from GitHub
|
||||
*
|
||||
*
|
||||
* @param repoId - The GitHub repository ID to fetch, or empty string to fetch all repos
|
||||
* @returns Object containing repository data, loading state, and any errors
|
||||
*/
|
||||
export function useRepoData(repoId: string): UseRepoDataReturn {
|
||||
const [repoData, setRepoData] = useState<any>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [octokit, setOctokit] = useState<Octokit | null>(null);
|
||||
|
||||
// Get auth data from Clerk
|
||||
const { isLoaded: isAuthLoaded } = useAuth();
|
||||
const { isLoaded: isUserLoaded, user } = useUser();
|
||||
const [repoData, setRepoData] = useState<any>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Initialize Octokit with the appropriate token
|
||||
useEffect(() => {
|
||||
async function initializeOctokit() {
|
||||
let token = null;
|
||||
|
||||
// Try to get GitHub token from Clerk
|
||||
if (user) {
|
||||
try {
|
||||
// Check if user has connected GitHub account
|
||||
const githubAccount = user.externalAccounts.find(
|
||||
account => account.provider === 'github'
|
||||
);
|
||||
|
||||
if (githubAccount) {
|
||||
// Try to get GitHub OAuth token from Clerk
|
||||
try {
|
||||
// token = await user.getToken({ template: 'github' });
|
||||
console.log('Using GitHub token from Clerk');
|
||||
} catch (err) {
|
||||
console.error('Error getting GitHub token from Clerk:', err);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error accessing Clerk user data:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to token from environment variable
|
||||
if (!token && typeof process !== 'undefined') {
|
||||
token = process.env.NEXT_PUBLIC_GITHUB_FALLBACK_TOKEN || '';
|
||||
if (token) {
|
||||
console.warn('Using fallback GitHub token. This should only be used for development.');
|
||||
}
|
||||
}
|
||||
|
||||
// Create Octokit instance with whatever token we found
|
||||
if (token) {
|
||||
setOctokit(new Octokit({ auth: token }));
|
||||
} else {
|
||||
setError("No GitHub token available");
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (isAuthLoaded && isUserLoaded) {
|
||||
initializeOctokit();
|
||||
}
|
||||
}, [isAuthLoaded, isUserLoaded, user]);
|
||||
// Use the centralized Octokit context instead of creating our own
|
||||
const { octokit, isAuth } = useOctokit()
|
||||
|
||||
// Fetch repo data when Octokit is available
|
||||
// Debug the context state
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
console.log('🔍 useRepoData: Context state changed:', {
|
||||
hasOctokit: !!octokit,
|
||||
isAuth,
|
||||
repoId
|
||||
})
|
||||
}, [octokit, isAuth, repoId])
|
||||
|
||||
// Fetch repo data when Octokit is available and authenticated
|
||||
useEffect(() => {
|
||||
let isMounted = true
|
||||
|
||||
async function fetchRepoData() {
|
||||
if (!octokit) {
|
||||
return;
|
||||
// Don't attempt to fetch if not authenticated
|
||||
if (!isAuth) {
|
||||
console.log('🔍 fetchRepoData: Not authenticated, skipping fetch')
|
||||
if (isMounted) {
|
||||
setError('GitHub authentication required')
|
||||
setRepoData(null)
|
||||
setIsLoading(false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (!octokit) {
|
||||
console.log('🔍 fetchRepoData: No octokit instance available')
|
||||
if (isMounted) {
|
||||
setError('GitHub client not available')
|
||||
setRepoData(null)
|
||||
setIsLoading(false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
console.log('🔍 fetchRepoData: Starting to fetch repos...')
|
||||
console.log('🔍 fetchRepoData: isAuth =', isAuth)
|
||||
|
||||
try {
|
||||
// Fetch repos from GitHub
|
||||
const { data: repos } = await octokit.repos.listForAuthenticatedUser();
|
||||
|
||||
console.log('🔍 Making GitHub API call: octokit.rest.repos.listForAuthenticatedUser()')
|
||||
const { data: repos } = await octokit.rest.repos.listForAuthenticatedUser()
|
||||
console.log('🔍 GitHub API success! Received', repos.length, 'repositories')
|
||||
|
||||
// If no repoId is provided, return all repos
|
||||
if (!repoId) {
|
||||
if (isMounted) {
|
||||
setRepoData(repos);
|
||||
setError(null);
|
||||
setIsLoading(false);
|
||||
setRepoData(repos)
|
||||
setError(null)
|
||||
setIsLoading(false)
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Find the specific repo by ID if repoId is provided
|
||||
const repo = repos.find(repo => repo.id.toString() === repoId);
|
||||
|
||||
const repo = repos.find((repo: any) => repo.id.toString() === repoId)
|
||||
|
||||
if (!repo) {
|
||||
if (isMounted) {
|
||||
setError("Repository not found");
|
||||
setRepoData(null);
|
||||
setIsLoading(false);
|
||||
setError('Repository not found')
|
||||
setRepoData(null)
|
||||
setIsLoading(false)
|
||||
}
|
||||
} else {
|
||||
if (isMounted) {
|
||||
setRepoData(repo);
|
||||
setError(null);
|
||||
setIsLoading(false);
|
||||
setRepoData(repo)
|
||||
setError(null)
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching GitHub repo:', err);
|
||||
console.error('❌ Error fetching GitHub repo:', err)
|
||||
console.error('❌ Error details:', {
|
||||
message: err instanceof Error ? err.message : 'Unknown error',
|
||||
status: (err as any)?.status,
|
||||
response: (err as any)?.response?.data,
|
||||
})
|
||||
if (isMounted) {
|
||||
setError('Failed to fetch repository data');
|
||||
setRepoData(null);
|
||||
setIsLoading(false);
|
||||
setError('Failed to fetch repository data')
|
||||
setRepoData(null)
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (octokit) {
|
||||
fetchRepoData();
|
||||
}
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [repoId, octokit]);
|
||||
|
||||
return { repoData, isLoading, error };
|
||||
}
|
||||
if (octokit && isAuth) {
|
||||
fetchRepoData()
|
||||
} else if (!isAuth) {
|
||||
// Handle case where we're not authenticated yet
|
||||
console.log('🔍 useRepoData: Waiting for authentication...')
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
}
|
||||
|
||||
return () => {
|
||||
isMounted = false
|
||||
}
|
||||
}, [repoId, octokit, isAuth])
|
||||
|
||||
return { repoData, isLoading, error }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
// src/services/paymentService.ts
|
||||
|
||||
interface PaymentConfig {
|
||||
amount: string
|
||||
senderAddress: string
|
||||
recipientAddress: string
|
||||
chainId: string
|
||||
}
|
||||
|
||||
interface PaymentResult {
|
||||
txHash: string | null
|
||||
success: boolean
|
||||
error?: string
|
||||
}
|
||||
|
||||
class PaymentService {
|
||||
private readonly WALLET_IFRAME_URL = (process.env.NEXT_PUBLIC_WALLET_IFRAME_URL || 'http://localhost:4000') + '/wallet-embed'
|
||||
private readonly TX_TIMEOUT_MS = 300000
|
||||
|
||||
/**
|
||||
* Send payment via wallet iframe
|
||||
* This should trigger a modal with the transaction approval iframe
|
||||
*/
|
||||
async sendPayment(
|
||||
config: PaymentConfig,
|
||||
setModalVisible: (visible: boolean) => void
|
||||
): Promise<PaymentResult> {
|
||||
const { amount, senderAddress, recipientAddress, chainId } = config
|
||||
|
||||
try {
|
||||
// Show the transaction approval modal first
|
||||
setModalVisible(true)
|
||||
|
||||
// Wait for the modal iframe to be ready
|
||||
await new Promise(resolve => setTimeout(resolve, 10000))
|
||||
|
||||
// Find the transaction approval iframe (from the modal)
|
||||
let iframe = document.getElementById('transaction-approval-iframe') as HTMLIFrameElement
|
||||
if (!iframe) {
|
||||
// Fallback to other iframe IDs
|
||||
iframe = document.getElementById('wallet-communication-iframe') as HTMLIFrameElement
|
||||
}
|
||||
if (!iframe) {
|
||||
iframe = document.getElementById('walletIframe') as HTMLIFrameElement
|
||||
}
|
||||
|
||||
console.log('🔵 Looking for wallet iframe:', {
|
||||
iframeFound: !!iframe,
|
||||
iframeId: iframe?.id,
|
||||
hasContentWindow: !!iframe?.contentWindow,
|
||||
iframeSrc: iframe?.src,
|
||||
expectedUrl: this.WALLET_IFRAME_URL,
|
||||
iframeReady: iframe?.contentDocument?.readyState
|
||||
})
|
||||
|
||||
if (!iframe?.contentWindow) {
|
||||
throw new Error('Wallet iframe not found. Please ensure wallet is connected.')
|
||||
}
|
||||
|
||||
// First, request wallet accounts for this chain to get the correct address
|
||||
const cosmosAddress = await this.getCosmosAddress(iframe, chainId)
|
||||
console.log('🔵 Got Cosmos address for chain:', cosmosAddress)
|
||||
|
||||
const txHash = await this.requestTransaction({
|
||||
iframe,
|
||||
sender: cosmosAddress || senderAddress, // Use cosmos address if available
|
||||
recipient: recipientAddress,
|
||||
amount,
|
||||
chainId,
|
||||
setModalVisible
|
||||
})
|
||||
|
||||
return {
|
||||
txHash,
|
||||
success: true
|
||||
}
|
||||
} catch (error) {
|
||||
// Hide modal on error
|
||||
setModalVisible(false)
|
||||
|
||||
return {
|
||||
txHash: null,
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Payment failed'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Cosmos address for the specific chain
|
||||
*/
|
||||
private async getCosmosAddress(iframe: HTMLIFrameElement, chainId: string): Promise<string | null> {
|
||||
return new Promise((resolve) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
window.removeEventListener('message', handleMessage)
|
||||
resolve(null) // Return null if timeout, don't fail the transaction
|
||||
}, 5000) // Short timeout for address lookup
|
||||
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.data.type === 'WALLET_ACCOUNTS_DATA') {
|
||||
window.removeEventListener('message', handleMessage)
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
const accounts = event.data.data
|
||||
console.log('🔵 Received wallet accounts:', accounts)
|
||||
|
||||
// Return the first account address for this chain
|
||||
if (accounts && accounts.length > 0) {
|
||||
resolve(accounts[0])
|
||||
} else {
|
||||
resolve(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', handleMessage)
|
||||
|
||||
// Request wallet accounts for this chain
|
||||
iframe.contentWindow!.postMessage(
|
||||
{
|
||||
type: 'REQUEST_WALLET_ACCOUNTS',
|
||||
chainId: chainId
|
||||
},
|
||||
this.WALLET_IFRAME_URL.replace('/wallet-embed', '') // Use base URL for this request
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Request transaction via iframe communication
|
||||
*/
|
||||
private async requestTransaction(params: {
|
||||
iframe: HTMLIFrameElement
|
||||
sender: string
|
||||
recipient: string
|
||||
amount: string
|
||||
chainId: string
|
||||
setModalVisible: (visible: boolean) => void
|
||||
}): Promise<string | null> {
|
||||
const { iframe, sender, recipient, amount, chainId, setModalVisible } = params
|
||||
|
||||
console.log('🔵 Sending payment request to wallet iframe:', {
|
||||
type: 'REQUEST_TX',
|
||||
chainId,
|
||||
fromAddress: sender,
|
||||
toAddress: recipient,
|
||||
amount,
|
||||
iframeUrl: this.WALLET_IFRAME_URL
|
||||
})
|
||||
|
||||
// Send transaction request to iframe
|
||||
console.log('🔵 About to postMessage with target origin:', this.WALLET_IFRAME_URL)
|
||||
iframe.contentWindow!.postMessage(
|
||||
{
|
||||
type: 'REQUEST_TX',
|
||||
chainId: chainId,
|
||||
fromAddress: sender,
|
||||
toAddress: recipient,
|
||||
amount: amount
|
||||
},
|
||||
this.WALLET_IFRAME_URL
|
||||
)
|
||||
|
||||
// Also try with '*' to see if it's an origin issue
|
||||
setTimeout(() => {
|
||||
console.log('🔵 Sending with wildcard origin as fallback...')
|
||||
iframe.contentWindow!.postMessage(
|
||||
{
|
||||
type: 'REQUEST_TX',
|
||||
chainId: chainId,
|
||||
fromAddress: sender,
|
||||
toAddress: recipient,
|
||||
amount: amount
|
||||
},
|
||||
'*'
|
||||
)
|
||||
}, 2000)
|
||||
|
||||
console.log('🔵 Payment request sent, waiting for response...')
|
||||
|
||||
// Test if iframe can receive messages at all
|
||||
setTimeout(() => {
|
||||
console.log('🔵 Testing iframe communication...')
|
||||
iframe.contentWindow!.postMessage({ type: 'PING' }, this.WALLET_IFRAME_URL)
|
||||
}, 1000)
|
||||
|
||||
// Wait for response
|
||||
return new Promise((resolve, reject) => {
|
||||
let timeoutId: NodeJS.Timeout
|
||||
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
// Log ALL messages for debugging
|
||||
console.log('🔵 Received message from wallet:', {
|
||||
origin: event.origin,
|
||||
type: event.data?.type,
|
||||
data: event.data
|
||||
})
|
||||
|
||||
// Accept messages from the wallet iframe origin
|
||||
const allowedOrigins = [
|
||||
'http://localhost:4000',
|
||||
'http://localhost:4000/',
|
||||
process.env.NEXT_PUBLIC_WALLET_IFRAME_URL || 'http://localhost:4000'
|
||||
].filter(Boolean)
|
||||
|
||||
if (!allowedOrigins.includes(event.origin)) {
|
||||
console.warn('❌ Message from unexpected origin:', event.origin, 'Allowed:', allowedOrigins)
|
||||
return
|
||||
}
|
||||
|
||||
console.log('✅ Message from wallet accepted:', event.origin)
|
||||
|
||||
console.log('✅ Payment response received:', event.data.type)
|
||||
|
||||
if (event.data.type === 'TRANSACTION_RESPONSE') {
|
||||
cleanup()
|
||||
setModalVisible(false)
|
||||
const txHash = event.data.data
|
||||
console.log('Transaction response:', txHash)
|
||||
|
||||
if (!txHash) {
|
||||
console.log('Transaction was cancelled or failed')
|
||||
reject(new Error('Transaction was cancelled or failed'))
|
||||
} else {
|
||||
console.log('Transaction successful:', txHash)
|
||||
resolve(txHash)
|
||||
}
|
||||
} else if (event.data.type === 'ERROR') {
|
||||
cleanup()
|
||||
setModalVisible(false)
|
||||
console.error('Transaction error:', event.data.message)
|
||||
reject(new Error(event.data.message || 'Transaction failed'))
|
||||
}
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
window.removeEventListener('message', handleMessage)
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
|
||||
// Set up listeners
|
||||
window.addEventListener('message', handleMessage)
|
||||
|
||||
// Set timeout
|
||||
timeoutId = setTimeout(() => {
|
||||
cleanup()
|
||||
setModalVisible(false)
|
||||
reject(new Error('Transaction timeout - no response from wallet'))
|
||||
}, this.TX_TIMEOUT_MS)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate payment amount based on deployment configuration
|
||||
*/
|
||||
calculatePaymentAmount(config: {
|
||||
deploymentType: 'lrn' | 'auction'
|
||||
selectedDeployer?: { minimumPayment?: string }
|
||||
maxPrice?: string
|
||||
deployerCount?: string
|
||||
}): string {
|
||||
const { deploymentType, selectedDeployer, maxPrice, deployerCount } = config
|
||||
|
||||
if (deploymentType === 'lrn') {
|
||||
return selectedDeployer?.minimumPayment?.replace(/\D/g, '') ?? '0'
|
||||
} else {
|
||||
if (!maxPrice || !deployerCount) return '0'
|
||||
return (parseInt(maxPrice) * parseInt(deployerCount)).toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const paymentService = new PaymentService()
|
||||
@@ -44,10 +44,10 @@ Run these steps in the `apps/deployer/` directory:
|
||||
NEXT_PUBLIC_WALLET_IFRAME_URL: https://wallet.laconic.com
|
||||
NEXT_PUBLIC_LACONICD_CHAIN_ID: laconic-mainnet
|
||||
NEXT_PUBLIC_API_URL: https://deploy-backend.apps.vaasl.io
|
||||
NEXT_PUBLIC_GITHUB_FALLBACK_TOKEN: your_github_token
|
||||
NEXT_PUBLIC_GITHUB_BACKEND_CLIENT_ID: Ov23li1hxnCcEggrbwED
|
||||
```
|
||||
|
||||
NOTE: Replace `your_clerk_key`, `your_clerk_secret` and `your_github_token` with actual values
|
||||
NOTE: Replace `your_clerk_key` and `your_clerk_secret` with actual values (<https://clerk.com/docs/quickstarts/nextjs-pages-router#set-your-clerk-api-keys>)
|
||||
|
||||
- Run script to deploy app to `deploy-staging.laconic.co`
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@ echo "Using DEPLOYER_LRN: $DEPLOYER_LRN"
|
||||
echo "Using AUTHORITY: $AUTHORITY"
|
||||
|
||||
# Repository URL
|
||||
REPO_URL="https://git.vdb.to/deep-stack/laconic-deployer-frontend"
|
||||
REPO_URL="https://git.vdb.to/NasSharaf/laconic-deployer-frontend"
|
||||
|
||||
# Get the latest commit hash for a branch
|
||||
BRANCH_NAME="ng-deploy-with-fixes"
|
||||
BRANCH_NAME="main"
|
||||
LATEST_HASH=$(git ls-remote $REPO_URL refs/heads/$BRANCH_NAME | awk '{print $1}')
|
||||
|
||||
echo "Latest commit hash for branch $BRANCH_NAME: $LATEST_HASH"
|
||||
@@ -117,6 +117,7 @@ else
|
||||
echo "Payment amount is null; skipping payment."
|
||||
fi
|
||||
|
||||
# TODO: Update dns when transitioning from staging to production deployment (deploy.laconic.com)
|
||||
# Generate application-deployment-request.yml
|
||||
cat >./records/application-deployment-request.yml <<EOF
|
||||
record:
|
||||
@@ -125,15 +126,15 @@ record:
|
||||
name: laconic-deployer-frontend@$PACKAGE_VERSION
|
||||
application: lrn://$AUTHORITY/applications/laconic-deployer-frontend@$PACKAGE_VERSION
|
||||
deployer: $DEPLOYER_LRN
|
||||
dns: deploy-staging.apps.vaasl.io
|
||||
dns: deploy-staging.laconic.com
|
||||
config:
|
||||
env:
|
||||
NEXT_PUBLIC_WALLET_IFRAME_URL: https://wallet.laconic.com
|
||||
NEXT_PUBLIC_LACONICD_CHAIN_ID: laconic-mainnet
|
||||
NEXT_PUBLIC_API_URL: https://deploy-backend.apps.vaasl.io
|
||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: pk_test_d2VsbC1tYW5hdGVlLTQxLmNsZXJrLmFjY291bnRzLmRldiQ
|
||||
CLERK_SECRET_KEY: sk_test_KjVFQ8zzVN7GMtxIgVWTne2YN0FD63gvB5hsfpLiRZ
|
||||
NEXT_PUBLIC_GITHUB_BACKEND_CLIENT_ID: Ov23liaet4yc0KX0iM1c
|
||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY:
|
||||
CLERK_SECRET_KEY:
|
||||
NEXT_PUBLIC_GITHUB_BACKEND_CLIENT_ID: Ov23li1hxnCcEggrbwED
|
||||
meta:
|
||||
note: Added @ $CURRENT_DATE_TIME
|
||||
repository: "$REPO_URL"
|
||||
|
||||
@@ -6,13 +6,13 @@ Ensure you have the following installed:
|
||||
- [Node.js](https://nodejs.org/) (v18+)
|
||||
- [pnpm](https://pnpm.io/) (v8+)
|
||||
- [Git](https://git-scm.com/)
|
||||
- [ngrok](https://ngrok.com/docs/getting-started/#2-install-the-ngrok-agent-cli)
|
||||
|
||||
## Project Structure
|
||||
|
||||
This monorepo contains several packages:
|
||||
|
||||
- `apps/deploy-fe`: Frontend Next.js application
|
||||
- `apps/backend`: Express.js backend API
|
||||
- `apps/deployer`: Deployment service
|
||||
- `services/gql-client`: GraphQL client library
|
||||
- `services/ui`: Shared UI components
|
||||
@@ -20,48 +20,232 @@ This monorepo contains several packages:
|
||||
|
||||
## Getting Started
|
||||
|
||||
### 1. Clone the repository
|
||||
The following steps are for running the deploy app locally (using [laconicd mainnet chain](https://laconicd-mainnet-1.laconic.com/status)):
|
||||
|
||||
```bash
|
||||
git clone https://github.com/yourusername/qwrk-laconic-core-develop.git
|
||||
cd qwrk-laconic-core-develop
|
||||
```
|
||||
- Clone laconic-wallet-web repo
|
||||
```bash
|
||||
git clone https://git.vdb.to/LaconicNetwork/laconic-wallet-web.git
|
||||
cd laconic-wallet-web
|
||||
```
|
||||
|
||||
### 2. Install dependencies
|
||||
- Create .env
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
- Update values in `.env`
|
||||
```
|
||||
# Not required since WalletConnect is not used in iframe integration
|
||||
REACT_APP_WALLET_CONNECT_PROJECT_ID=
|
||||
|
||||
### 3. Configure environment variables
|
||||
REACT_APP_DEFAULT_GAS_PRICE=0.025
|
||||
|
||||
# Reference: https://github.com/cosmos/cosmos-sdk/issues/16020
|
||||
REACT_APP_GAS_ADJUSTMENT=2
|
||||
REACT_APP_LACONICD_RPC_URL=https://laconicd-mainnet-1.laconic.com
|
||||
|
||||
Create a `.env.local` file in the `apps/deploy-fe` directory:
|
||||
# URL of Deploy app frontend app that will run locally
|
||||
REACT_APP_ALLOWED_URLS=http://localhost:3000
|
||||
```
|
||||
|
||||
```
|
||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_key
|
||||
CLERK_SECRET_KEY=your_clerk_secret
|
||||
NEXT_PUBLIC_WALLET_IFRAME_URL=http://localhost:4000
|
||||
NEXT_PUBLIC_LACONICD_CHAIN_ID=laconic-mainnet
|
||||
NEXT_PUBLIC_API_URL=http://localhost:8000
|
||||
NEXT_PUBLIC_GITHUB_FALLBACK_TOKEN=your_github_token
|
||||
```
|
||||
- Install dependencies
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
|
||||
Create a `local.toml` file in `apps/backend/environments/` (based on the example file).
|
||||
- Build app
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
|
||||
### 4. Start the development servers
|
||||
- Set env values in build
|
||||
```bash
|
||||
yarn set-env
|
||||
```
|
||||
|
||||
#### Frontend:
|
||||
```bash
|
||||
cd apps/deploy-fe
|
||||
pnpm dev
|
||||
```
|
||||
- Serve the build
|
||||
```
|
||||
# Install package serve globally
|
||||
npm install -g serve
|
||||
|
||||
### 5. Connect Your Wallet
|
||||
# Serve the wallet build
|
||||
serve -s -l 4000 ./build
|
||||
```
|
||||
|
||||
The application requires a Laconic wallet for certain operations. You need to set up:
|
||||
Wallet will run at http://localhost:4000
|
||||
|
||||
- In a new terminal, clone repo for backend
|
||||
```bash
|
||||
git clone https://git.vdb.to/cerc-io/snowballtools-base.git
|
||||
cd snowballtools-base
|
||||
```
|
||||
|
||||
- Install deps and build
|
||||
```bash
|
||||
yarn && yarn build --ignore frontend
|
||||
```
|
||||
|
||||
- Create `packages/backend/environments/local.toml`
|
||||
```bash
|
||||
cp packages/backend/environments/local.toml.example packages/backend/environments/local.toml
|
||||
```
|
||||
|
||||
- Update values in `packages/backend/environments/local.toml`
|
||||
```toml
|
||||
[server]
|
||||
host = "127.0.0.1"
|
||||
port = 8000
|
||||
gqlPath = "/graphql"
|
||||
[server.session]
|
||||
# Can be set to any random string
|
||||
secret = "RpwqcvFkLZ"
|
||||
# Frontend webapp URL origin
|
||||
appOriginUrl = "http://localhost:3000"
|
||||
# Set to true if server running behind proxy
|
||||
trustProxy = false
|
||||
# Backend URL hostname
|
||||
domain = "localhost"
|
||||
|
||||
[database]
|
||||
dbPath = "db/snowball"
|
||||
|
||||
[gitHub]
|
||||
webhookUrl = ""
|
||||
[gitHub.oAuth]
|
||||
clientId = "Ov23lihCWQvOORNxtebD"
|
||||
clientSecret = "139de35bdd610628289adcd99bb2c7c962a11c99"
|
||||
|
||||
[registryConfig]
|
||||
fetchDeploymentRecordDelay = 5000
|
||||
checkAuctionStatusDelay = 5000
|
||||
restEndpoint = "https://laconicd-mainnet-1.laconic.com"
|
||||
gqlEndpoint = "https://laconicd-mainnet-1.laconic.com/api"
|
||||
chainId = "laconic-mainnet"
|
||||
# Set private key of account laconic13maulvmjxnyx3g855vk0lsv5aptf3rpxskynef
|
||||
# This account owns the bond and authority configured below
|
||||
privateKey = ""
|
||||
bondId = "230cfedda15e78edc8986dfcb870e1b618f65c56e38d2735476d2a8cb3f25e38"
|
||||
authority = "laconic"
|
||||
[registryConfig.fee]
|
||||
gasPrice = "0.001alnt"
|
||||
|
||||
[auction]
|
||||
commitFee = "1000"
|
||||
commitsDuration = "60s"
|
||||
revealFee = "1000"
|
||||
revealsDuration = "60s"
|
||||
denom = "alnt"
|
||||
```
|
||||
|
||||
- Run ngrok
|
||||
```bash
|
||||
ngrok http 8000
|
||||
```
|
||||
|
||||
- Set ngrok URL to `gitHub.webhookUrl` in config `packages/backend/environments/local.toml`
|
||||
- Example
|
||||
```toml
|
||||
...
|
||||
[gitHub]
|
||||
webhookUrl = "https://<ngrok-url>.ngrok-free.app"
|
||||
...
|
||||
```
|
||||
|
||||
- Run backend server
|
||||
```bash
|
||||
cd packages/backend
|
||||
yarn start
|
||||
```
|
||||
|
||||
- In a new terminal, clone repo for frontend (laconic-deployer-frontend)
|
||||
```bash
|
||||
git clone https://git.vdb.to/NasSharaf/laconic-deployer-frontend.git
|
||||
cd laconic-deployer-frontend
|
||||
```
|
||||
|
||||
- Install dependencies
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
- Build dependecies for frontend app
|
||||
```bash
|
||||
pnpm build:fe-compile
|
||||
```
|
||||
|
||||
- Create a `.env.local` file in the `apps/deploy-fe` directory to configure environment variables:
|
||||
```
|
||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_key
|
||||
CLERK_SECRET_KEY=your_clerk_secret
|
||||
NEXT_PUBLIC_WALLET_IFRAME_URL=http://localhost:4000
|
||||
NEXT_PUBLIC_LACONICD_CHAIN_ID=laconic-mainnet
|
||||
NEXT_PUBLIC_API_URL=http://localhost:8000
|
||||
NEXT_PUBLIC_GITHUB_BACKEND_CLIENT_ID=Ov23lihCWQvOORNxtebD
|
||||
```
|
||||
Replace `your_clerk_key` and `your_clerk_secret` with actual values (<https://clerk.com/docs/quickstarts/nextjs-pages-router#set-your-clerk-api-keys>)
|
||||
|
||||
- Start the development server
|
||||
```bash
|
||||
cd apps/deploy-fe
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
- The deploy frontend app will be now available at <http://localhost:3000>
|
||||
|
||||
### Fund wallet account
|
||||
|
||||
- Change directory to deployer package
|
||||
```bash
|
||||
cd apps/deployer/
|
||||
```
|
||||
|
||||
- Setup config for laconic-registry-cli in `packages/deployer/config.yml`
|
||||
```yaml
|
||||
services:
|
||||
registry:
|
||||
rpcEndpoint: https://laconicd-mainnet-1.laconic.com
|
||||
gqlEndpoint: https://laconicd-mainnet-1.laconic.com/api
|
||||
# Set to private key of an account with funds
|
||||
# Private key of account laconic13maulvmjxnyx3g855vk0lsv5aptf3rpxskynef set in deployer backend can be used
|
||||
userKey:
|
||||
chainId: laconic-testnet-2
|
||||
gasPrice: 0.001alnt
|
||||
```
|
||||
|
||||
- Get wallet account address at <http://localhost:4000> for `laconicd mainnet` network
|
||||
|
||||
- Run command to send tokens
|
||||
```bash
|
||||
pnpm laconic registry tokens send --address <ACCOUNT_ADDRESS_FROM_WALLET> --type alnt --quantity 12960
|
||||
```
|
||||
|
||||
- To check balance visit <https://explorer.laconic.com/laconic-mainnet/account/ACCOUNT_ADDRESS_FROM_WALLET>
|
||||
|
||||
### Misc
|
||||
|
||||
- To check for deployments in vaasl : <https://webapp-deployer-ui.apps.vaasl.io/>
|
||||
|
||||
- If deployment fails due to low bond balance
|
||||
- Change directory to deployer package
|
||||
```bash
|
||||
cd apps/deployer/
|
||||
```
|
||||
|
||||
- Check balances
|
||||
```bash
|
||||
# Account balance
|
||||
yarn laconic registry account get --address laconic13maulvmjxnyx3g855vk0lsv5aptf3rpxskynef
|
||||
|
||||
# Bond balance
|
||||
yarn laconic registry bond get --id 230cfedda15e78edc8986dfcb870e1b618f65c56e38d2735476d2a8cb3f25e38
|
||||
```
|
||||
|
||||
- Command to refill bond
|
||||
|
||||
```bash
|
||||
yarn laconic registry bond refill --id 230cfedda15e78edc8986dfcb870e1b618f65c56e38d2735476d2a8cb3f25e38 --type alnt --quantity 10000000
|
||||
```
|
||||
|
||||
1. A running wallet instance (follow the setup in the laconic-wallet-web repository)
|
||||
2. Configure the `NEXT_PUBLIC_WALLET_IFRAME_URL` to point to your wallet instance
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -96,7 +280,7 @@ Make sure you have:
|
||||
|
||||
## Test deployment
|
||||
|
||||
- Follow the dev install steps for stack-orchestrator from <https://git.vdb.to/cerc-io/stack-orchestrator/src/branch/main/docs/CONTRIBUTING.md#install>
|
||||
- Install stack-orchestrator from <https://git.vdb.to/cerc-io/stack-orchestrator#install>
|
||||
|
||||
- Build the container for app
|
||||
|
||||
|
||||
Reference in New Issue
Block a user