Added onboarding modal

This commit is contained in:
NasSharaf 2025-05-12 18:38:21 -04:00
parent 69b8cf1395
commit 29c5f6f931
19 changed files with 2193 additions and 1216 deletions

View File

@ -1,6 +1,6 @@
./.env.example
./.env.local
./.gitignore
./.turbo/turbo-build.log
./.vscode/settings.json
./biome.jsonc
./components.json
@ -15,26 +15,28 @@
./src/app/(web3-authenticated)/(dashboard)/home/loading.tsx
./src/app/(web3-authenticated)/(dashboard)/home/page.tsx
./src/app/(web3-authenticated)/(dashboard)/layout.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/(create)/cr/(configure)/cf/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/(create)/cr/(deploy)/dp/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/(create)/cr/(success)/sc/[id]/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/(create)/cr/(template)/tm/(configure)/cf/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/(create)/cr/(template)/tm/(deploy)/dp/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/[id]/(deployments)/dep/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/[id]/(integrations)/int/GitPage.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/[id]/(integrations)/int/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/[id]/(settings)/set/(collaborators)/col/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/[id]/(settings)/set/(domains)/dom/(add)/cf/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/[id]/(settings)/set/(domains)/dom/(add)/config/cf/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/[id]/(settings)/set/(environment-variables)/env/EnvVarsPage.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/[id]/(settings)/set/(environment-variables)/env/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/[id]/(settings)/set/(git)/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/[id]/(settings)/set/ProjectSettingsPage.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/[id]/(settings)/set/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/[id]/deployments/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/[id]/layout.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/[id]/loading.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/(projects)/ps/[id]/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/(create)/cr/(configure)/cf/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/(create)/cr/(connect)/cn/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/(create)/cr/(deploy)/dp/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/(create)/cr/(success)/sc/[id]/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/(create)/cr/(template)/tm/(configure)/cf/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/(create)/cr/(template)/tm/(deploy)/dp/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/(create)/cr/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(deployments)/dep/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(integrations)/int/GitPage.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(integrations)/int/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(settings)/set/(collaborators)/col/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(settings)/set/(domains)/dom/(add)/cf/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(settings)/set/(domains)/dom/(add)/config/cf/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(settings)/set/(environment-variables)/env/EnvVarsPage.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(settings)/set/(environment-variables)/env/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(settings)/set/(git)/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(settings)/set/ProjectSettingsPage.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(settings)/set/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/deployments/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/layout.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/loading.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/page.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/error.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/loading.tsx
./src/app/(web3-authenticated)/(dashboard)/projects/page.tsx
@ -134,6 +136,7 @@
./src/components/iframe/auto-sign-in/index.ts
./src/components/iframe/auto-sign-in/types.ts
./src/components/iframe/check-balance-iframe/CheckBalanceIframe.tsx
./src/components/iframe/check-balance-iframe/CheckBalanceWrapper.tsx
./src/components/iframe/check-balance-iframe/useCheckBalance.tsx
./src/components/layout/index.ts
./src/components/layout/navigation/github-session-button/GitHubSessionButton.tsx
@ -203,7 +206,10 @@
./src/context/WalletContext.tsx
./src/context/WalletContextProvider.tsx
./src/context/index.ts
./src/hooks/disabled_useRepoData.tsx
./src/hooks/useDeployment.tsx
./src/hooks/useRepoData.tsx
./src/hooks/useRepoSelection.tsx
./src/lib/utils.ts
./src/middleware.ts
./src/types/common.ts

View File

@ -1,227 +0,0 @@
'use client'
import { useState, useEffect } from 'react'
import { useParams, useRouter } from 'next/navigation'
import { PageWrapper } from '@/components/foundation'
import { LoadingOverlay } from '@/components/foundation/loading/loading-overlay'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@workspace/ui/components/card'
import { Input } from '@workspace/ui/components/input'
import { Label } from '@workspace/ui/components/label'
import { Button } from '@workspace/ui/components/button'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@workspace/ui/components/select'
import { toast } from 'sonner'
import { Stepper } from '@/components/core/stepper/Stepper'
import { useRepoData } from '@/hooks/useRepoData'
export default function ConfigureDeploymentPage() {
const router = useRouter()
const params = useParams()
const providerParam = params?.provider ? String(params.provider) : 'github'
// Use the existing useRepoData hook to fetch all repos (empty string for ID means all repos)
const { repoData: repositories, isLoading } = useRepoData('')
const [selectedRepo, setSelectedRepo] = useState<string>('')
const [selectedBranch, setSelectedBranch] = useState<string>('main')
const [projectName, setProjectName] = useState<string>('')
const [branches, setBranches] = useState<string[]>(['main'])
const [envVars, setEnvVars] = useState<{ key: string; value: string }[]>([
{ key: '', value: '' }
])
// Define stepper values for the existing Stepper component
const stepperValues = [
{ step: 1, label: 'Select Repository', route: '/projects/github/ps/cr/tm/cf' },
{ step: 2, label: 'Configure', route: '/projects/github/ps/cr/cf' },
{ step: 3, label: 'Deploy', route: '/projects/github/ps/cr/dp' },
{ step: 4, label: 'Success', route: '/projects/github/ps/cr/sc' }
]
// When a repository is selected, update project name and branch
useEffect(() => {
if (!selectedRepo || !repositories) return
const repo = repositories.find(r => r.full_name === selectedRepo)
if (repo) {
setProjectName(repo.name)
setSelectedBranch(repo.default_branch)
// For simplicity, just use the default branch and some common branch names
// In a real implementation, you would fetch branches for the selected repo
setBranches([repo.default_branch, 'develop', 'feature/new-ui'])
}
}, [selectedRepo, repositories])
const handleRepoChange = (repo: string) => {
setSelectedRepo(repo)
}
const handleBranchChange = (branch: string) => {
setSelectedBranch(branch)
}
const handleProjectNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setProjectName(e.target.value)
}
const handleEnvVarChange = (index: number, field: 'key' | 'value', value: string) => {
const newEnvVars = [...envVars]
newEnvVars[index][field] = value
// Add a new empty row if the last row has both key and value filled
if (
index === newEnvVars.length - 1 &&
newEnvVars[index].key !== '' &&
newEnvVars[index].value !== ''
) {
newEnvVars.push({ key: '', value: '' })
}
setEnvVars(newEnvVars)
}
const handleSubmit = () => {
if (!selectedRepo || !selectedBranch || !projectName) {
toast.error('Please fill in all required fields')
return
}
// Filter out empty env vars
const filteredEnvVars = envVars.filter(
envVar => envVar.key.trim() !== '' && envVar.value.trim() !== ''
)
// Convert env vars array to object
const environmentVariables = filteredEnvVars.reduce(
(acc, { key, value }) => ({ ...acc, [key]: value }),
{}
)
// Find the selected repository to get its URL
const repo = repositories?.find(r => r.full_name === selectedRepo)
// Store the configuration in session storage to be used in the next step
sessionStorage.setItem(
'deploymentConfig',
JSON.stringify({
repositoryUrl: selectedRepo,
repositoryHtmlUrl: repo?.html_url || `https://github.com/${selectedRepo}`,
branch: selectedBranch,
projectName,
environmentVariables
})
)
// Navigate to the deployment page
router.push(`/projects/${providerParam}/ps/cr/dp`)
}
if (isLoading) {
return <LoadingOverlay isLoading={true} />
}
return (
<PageWrapper
header={{
title: 'Configure Deployment',
description: 'Set up your project deployment configuration'
}}
>
<div className="max-w-3xl mx-auto">
{/* Using the existing Stepper component with the correct props */}
<Stepper activeStep={2} stepperValues={stepperValues} />
<Card className="mt-6">
<CardHeader>
<CardTitle>Project Configuration</CardTitle>
<CardDescription>
Configure your project settings for deployment
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="repo">GitHub Repository</Label>
<Select
value={selectedRepo}
onValueChange={handleRepoChange}
>
<SelectTrigger id="repo">
<SelectValue placeholder="Select a repository" />
</SelectTrigger>
<SelectContent>
{repositories && repositories.map(repo => (
<SelectItem key={repo.id} value={repo.full_name}>
{repo.full_name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="branch">Branch</Label>
<Select
value={selectedBranch}
onValueChange={handleBranchChange}
disabled={!selectedRepo}
>
<SelectTrigger id="branch">
<SelectValue placeholder="Select a branch" />
</SelectTrigger>
<SelectContent>
{branches.map(branch => (
<SelectItem key={branch} value={branch}>
{branch}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="projectName">Project Name</Label>
<Input
id="projectName"
value={projectName}
onChange={handleProjectNameChange}
placeholder="Enter a name for your project"
/>
</div>
<div className="space-y-2">
<Label>Environment Variables</Label>
<div className="space-y-2">
{envVars.map((envVar, index) => (
<div key={index} className="flex gap-2">
<Input
placeholder="KEY"
value={envVar.key}
onChange={e => handleEnvVarChange(index, 'key', e.target.value)}
className="flex-1"
/>
<Input
placeholder="VALUE"
value={envVar.value}
onChange={e => handleEnvVarChange(index, 'value', e.target.value)}
className="flex-1"
/>
</div>
))}
</div>
<p className="text-xs text-muted-foreground">
Environment variables will be securely stored and available during build and runtime.
</p>
</div>
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline" onClick={() => router.back()}>
Back
</Button>
<Button onClick={handleSubmit}>
Continue to Deployment
</Button>
</CardFooter>
</Card>
</div>
</PageWrapper>
)
}

View File

@ -1,270 +0,0 @@
'use client'
import { useState, useEffect } from 'react'
import { useParams, useRouter } from 'next/navigation'
import { PageWrapper } from '@/components/foundation'
import { LoadingOverlay } from '@/components/foundation/loading/loading-overlay'
import { Stepper } from '@/components/core/stepper/Stepper'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@workspace/ui/components/card'
import { Button } from '@workspace/ui/components/button'
import { toast } from 'sonner'
import { Progress } from '@workspace/ui/components/progress'
import { Loader2, CheckCircle, AlertCircle, GitBranch } from 'lucide-react'
import { StopWatch } from '@/components/core/stop-watch'
interface DeploymentConfig {
repositoryUrl: string;
repositoryHtmlUrl: string;
branch: string;
projectName: string;
environmentVariables: Record<string, string>;
}
export default function DeployPage() {
const router = useRouter()
const params = useParams()
const providerParam = params?.provider ? String(params.provider) : 'github'
const [deploymentConfig, setDeploymentConfig] = useState<DeploymentConfig | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [isDeploying, setIsDeploying] = useState(false)
const [deploymentStatus, setDeploymentStatus] = useState<'idle' | 'pending' | 'building' | 'ready' | 'error'>('idle')
const [deploymentProgress, setDeploymentProgress] = useState<number>(0)
const [, setElapsedTime] = useState<number>(0)
const [deploymentId, setDeploymentId] = useState<string>('')
// Define stepper values for the existing Stepper component
const stepperValues = [
{ step: 1, label: 'Select Repository', route: '/projects/github/ps/cr/tm/cf' },
{ step: 2, label: 'Configure', route: '/projects/github/ps/cr/cf' },
{ step: 3, label: 'Deploy', route: '/projects/github/ps/cr/dp' },
{ step: 4, label: 'Success', route: '/projects/github/ps/cr/sc' }
]
// Load deployment config from session storage
useEffect(() => {
const storedConfig = sessionStorage.getItem('deploymentConfig')
if (storedConfig) {
setDeploymentConfig(JSON.parse(storedConfig))
} else {
toast.error('Deployment configuration not found')
router.push(`/projects/${providerParam}/ps/cr/cf`)
}
setIsLoading(false)
}, [router, providerParam])
// Handle elapsed time updates from StopWatch component
const handleTimeUpdate = (time: number) => {
setElapsedTime(time)
}
// Simulate deployment process (would connect to your backend in a real implementation)
const startDeployment = () => {
if (!deploymentConfig) {
toast.error('Deployment configuration not found')
return
}
setIsDeploying(true)
setDeploymentStatus('pending')
setDeploymentProgress(10)
// Simulate deployment steps with timeouts
setTimeout(() => {
setDeploymentStatus('building')
setDeploymentProgress(40)
setTimeout(() => {
// 80% chance of success, 20% chance of failure (for demo purposes)
const success = Math.random() < 0.8
if (success) {
setDeploymentStatus('ready')
setDeploymentProgress(100)
// Generate a random ID for the deployment
const id = Math.random().toString(36).substring(2, 10)
setDeploymentId(id)
// Store deployment details in session storage
const deploymentDetails = {
id,
url: `https://${deploymentConfig.projectName.toLowerCase().replace(/[^a-z0-9]/g, '-')}.laconic.deploy`,
projectId: 'project_' + Math.random().toString(36).substring(2, 10),
projectName: deploymentConfig.projectName,
status: 'ready',
createdAt: new Date().toISOString(),
repository: {
name: deploymentConfig.repositoryUrl.split('/')[1],
url: deploymentConfig.repositoryHtmlUrl || `https://github.com/${deploymentConfig.repositoryUrl}`,
branch: deploymentConfig.branch
}
};
sessionStorage.setItem('deploymentResult', JSON.stringify(deploymentDetails))
// Navigate to success page after a short delay
setTimeout(() => {
router.push(`/projects/${providerParam}/ps/cr/sc/${id}`)
}, 2000)
} else {
setDeploymentStatus('error')
setDeploymentProgress(100)
}
setIsDeploying(false)
}, 5000) // 5 seconds for building
}, 3000) // 3 seconds for pending
}
const getStatusIcon = () => {
switch (deploymentStatus) {
case 'pending':
return <Loader2 className="h-6 w-6 animate-spin text-blue-500" />
case 'building':
return <Loader2 className="h-6 w-6 animate-spin text-blue-500" />
case 'ready':
return <CheckCircle className="h-6 w-6 text-green-500" />
case 'error':
return <AlertCircle className="h-6 w-6 text-red-500" />
default:
return null
}
}
const getStatusText = () => {
switch (deploymentStatus) {
case 'pending':
return 'Preparing deployment...'
case 'building':
return 'Building your project...'
case 'ready':
return 'Deployment successful!'
case 'error':
return 'Deployment failed'
default:
return 'Ready to deploy'
}
}
if (isLoading) {
return <LoadingOverlay isLoading={true} />
}
return (
<PageWrapper
header={{
title: 'Deploy Project',
description: 'Deploy your project to Laconic'
}}
>
<div className="max-w-3xl mx-auto">
{/* Using the existing Stepper component with the correct props */}
<Stepper activeStep={3} stepperValues={stepperValues} />
<Card className="mt-6">
<CardHeader>
<CardTitle>Deployment</CardTitle>
<CardDescription>
Deploy your project to Laconic's decentralized hosting
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{deploymentConfig && (
<div className="space-y-4">
<div>
<p className="text-sm font-medium">Repository</p>
<div className="flex items-center">
<GitBranch className="h-4 w-4 mr-2 text-muted-foreground" />
<p className="text-sm text-muted-foreground">
{deploymentConfig.repositoryUrl} ({deploymentConfig.branch})
</p>
</div>
</div>
<div>
<p className="text-sm font-medium">Project Name</p>
<p className="text-sm text-muted-foreground">
{deploymentConfig.projectName}
</p>
</div>
{Object.keys(deploymentConfig.environmentVariables || {}).length > 0 && (
<div>
<p className="text-sm font-medium">Environment Variables</p>
<p className="text-sm text-muted-foreground">
{Object.keys(deploymentConfig.environmentVariables).length} environment variables configured
</p>
</div>
)}
</div>
)}
{deploymentStatus === 'idle' ? (
<div className="flex flex-col items-center justify-center py-6">
<p className="text-center mb-4">
Ready to deploy your project? Click the button below to start the deployment process.
</p>
<Button
onClick={startDeployment}
disabled={isDeploying}
className="w-full sm:w-auto"
>
{isDeploying ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Deploying...
</>
) : (
'Start Deployment'
)}
</Button>
</div>
) : (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
{getStatusIcon()}
<p className="text-sm font-medium">{getStatusText()}</p>
</div>
{/* Using your existing StopWatch component */}
<StopWatch
start={deploymentStatus !== 'ready' && deploymentStatus !== 'error'}
onTimeUpdate={handleTimeUpdate}
/>
</div>
<Progress value={deploymentProgress} className="h-2" />
<p className="text-xs text-muted-foreground text-center">
{deploymentStatus === 'pending' && 'Setting up the deployment environment...'}
{deploymentStatus === 'building' && 'Building your application...'}
{deploymentStatus === 'ready' && 'Deployment completed successfully!'}
{deploymentStatus === 'error' && 'There was an error deploying your application. Please try again.'}
</p>
</div>
)}
</CardContent>
<CardFooter className="flex justify-between">
<Button
variant="outline"
onClick={() => router.back()}
disabled={deploymentStatus === 'pending' || deploymentStatus === 'building'}
>
Back
</Button>
{deploymentStatus === 'ready' && (
<Button
onClick={() => router.push(`/projects/${providerParam}/ps/cr/sc/${deploymentId}`)}
>
View Deployment
</Button>
)}
{deploymentStatus === 'error' && (
<Button onClick={startDeployment}>
Retry Deployment
</Button>
)}
</CardFooter>
</Card>
</div>
</PageWrapper>
)
}

View File

@ -1,258 +0,0 @@
'use client'
import { useState, useEffect } from 'react'
import { useParams, useRouter } from 'next/navigation'
import { PageWrapper } from '@/components/foundation'
import { LoadingOverlay } from '@/components/foundation/loading/loading-overlay'
import { Stepper } from '@/components/core/stepper/Stepper'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@workspace/ui/components/card'
import { Button } from '@workspace/ui/components/button'
import { toast } from 'sonner'
import { CheckCircle, Copy, ExternalLink, Clock } from 'lucide-react'
import Link from 'next/link'
import { relativeTimeMs } from '@/utils/time'
import { getInitials } from '@/utils/getInitials'
import { Avatar, AvatarFallback } from '@workspace/ui/components/avatar'
interface DeploymentDetails {
id: string;
url: string;
projectId: string;
projectName: string;
status: string;
createdAt: string;
repository: {
name: string;
url: string;
branch: string;
};
}
export default function SuccessPage({ params }: { params: { id: string } }) {
const router = useRouter()
const paramsObj = useParams()
const providerParam = paramsObj?.provider ? String(paramsObj.provider) : 'github'
const [isLoading, setIsLoading] = useState(true)
const [deployment, setDeployment] = useState<DeploymentDetails | null>(null)
const deploymentId = params.id
// Define stepper values for the existing Stepper component
const stepperValues = [
{ step: 1, label: 'Select Repository', route: '/projects/github/ps/cr/tm/cf' },
{ step: 2, label: 'Configure', route: '/projects/github/ps/cr/cf' },
{ step: 3, label: 'Deploy', route: '/projects/github/ps/cr/dp' },
{ step: 4, label: 'Success', route: '/projects/github/ps/cr/sc' }
]
// Get deployment details from session storage
useEffect(() => {
// For now, we'll get the deployment details from session storage
// In a real app, you'd fetch this from your API
const storedDeployment = sessionStorage.getItem('deploymentResult')
if (storedDeployment) {
setDeployment(JSON.parse(storedDeployment))
} else {
// If not found in session storage, simulate it (for demo purposes)
// In a real app, you'd fetch from the API using the ID
simulateDeploymentDetails()
}
setIsLoading(false)
}, [deploymentId])
// Simulate deployment details if needed (for demo purposes)
const simulateDeploymentDetails = () => {
const mockDeployment: DeploymentDetails = {
id: deploymentId,
url: `https://project-${deploymentId}.laconic.deploy`,
projectId: 'project_' + Math.random().toString(36).substring(2, 10),
projectName: 'Demo Project',
status: 'ready',
createdAt: new Date().toISOString(),
repository: {
name: 'demo-repo',
url: 'https://github.com/yourusername/demo-repo',
branch: 'main'
}
}
setDeployment(mockDeployment)
}
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text)
toast.success('Copied to clipboard')
}
if (isLoading) {
return <LoadingOverlay isLoading={true} />
}
if (!deployment) {
return (
<PageWrapper
header={{
title: 'Deployment Not Found',
description: 'The deployment you are looking for does not exist'
}}
>
<div className="max-w-3xl mx-auto">
<Card>
<CardContent className="pt-6">
<div className="flex flex-col items-center justify-center py-12">
<p className="text-center mb-6">
We couldn't find the deployment you're looking for. It may have been deleted or expired.
</p>
<Button asChild>
<Link href="/projects">View All Projects</Link>
</Button>
</div>
</CardContent>
</Card>
</div>
</PageWrapper>
)
}
// Calculate relative time for the deployment
const deploymentTime = new Date(deployment.createdAt).getTime()
const deployedBy = 'You' // In a real app, you'd get this from the deployment data
return (
<PageWrapper
header={{
title: 'Deployment Success',
description: 'Your project has been successfully deployed',
actions: [
{
label: 'View App',
href: deployment.url,
icon: 'external-link',
external: true
}
]
}}
>
<div className="max-w-3xl mx-auto">
{/* Using the existing Stepper component with the correct props */}
<Stepper activeStep={4} stepperValues={stepperValues} />
<Card className="mt-6">
<CardHeader className="pb-4">
<div className="flex items-center space-x-2">
<CheckCircle className="h-6 w-6 text-green-500" />
<CardTitle>Deployment Successful</CardTitle>
</div>
<CardDescription>
Your project has been successfully deployed to Laconic's decentralized hosting
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="p-4 border rounded-lg bg-muted/50 relative">
<h3 className="font-medium mb-2">Deployment URL</h3>
<div className="flex items-center">
<code className="text-sm bg-background p-2 rounded flex-1 overflow-x-auto">
{deployment.url}
</code>
<Button
variant="ghost"
size="icon"
onClick={() => copyToClipboard(deployment.url)}
className="ml-2"
>
<Copy className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="ml-2"
asChild
>
<a href={deployment.url} target="_blank" rel="noopener noreferrer">
<ExternalLink className="h-4 w-4" />
</a>
</Button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h3 className="font-medium mb-2">Project Details</h3>
<ul className="space-y-2 text-sm">
<li>
<span className="text-muted-foreground">Project Name:</span>{' '}
{deployment.projectName}
</li>
<li>
<span className="text-muted-foreground">Repository:</span>{' '}
{deployment.repository.name}
</li>
<li>
<span className="text-muted-foreground">Branch:</span>{' '}
{deployment.repository.branch}
</li>
<li>
<span className="text-muted-foreground">Deployment ID:</span>{' '}
<code className="text-xs bg-muted p-1 rounded">{deployment.id}</code>
</li>
</ul>
</div>
<div>
<h3 className="font-medium mb-2">Deployment Information</h3>
<ul className="space-y-2 text-sm">
<li>
<span className="text-muted-foreground">Status:</span>{' '}
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100">
{deployment.status.toUpperCase()}
</span>
</li>
<li>
<div className="flex items-center">
<Clock className="h-4 w-4 mr-2 text-muted-foreground" />
<span className="text-muted-foreground">Deployed at:</span>{' '}
<span className="ml-1">{relativeTimeMs(deploymentTime)}</span>
</div>
</li>
<li>
<div className="flex items-center">
<span className="text-muted-foreground">Deployed by:</span>{' '}
<span className="flex items-center ml-1">
<Avatar className="h-5 w-5 mr-1">
<AvatarFallback>{getInitials(deployedBy)}</AvatarFallback>
</Avatar>
{deployedBy}
</span>
</div>
</li>
</ul>
</div>
</div>
<div className="p-4 border rounded-lg bg-background relative">
<h3 className="font-medium mb-2">What's Next?</h3>
<ul className="space-y-2 text-sm">
<li> Configure custom domains for your deployment</li>
<li> Set up automatic deployments for new commits</li>
<li> Add collaborators to your project</li>
<li> Monitor deployment performance and analytics</li>
</ul>
</div>
</CardContent>
<CardFooter className="flex justify-between">
<Button
variant="outline"
onClick={() => router.push('/projects')}
>
View All Projects
</Button>
<Button
onClick={() => router.push(`/projects/${providerParam}/ps/${deployment.projectId}`)}
>
Go to Project Dashboard
</Button>
</CardFooter>
</Card>
</div>
</PageWrapper>
)

View File

@ -0,0 +1,203 @@
'use client'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { X } from 'lucide-react'
import { useTheme } from 'next-themes'
import { useOnboarding } from '@/components/onboarding/useOnboarding'
import { ConnectStep } from '@/components/onboarding/connect-step/connect-step'
import { ConfigureStep } from '@/components/onboarding/configure-step/configure-step'
import { DeployStep } from '@/components/onboarding/deploy-step/deploy-step'
import { SuccessStep } from '@/components/onboarding/success-step/success-step'
import { LaconicMark } from '@/components/assets/laconic-mark'
/**
* Parent component for the onboarding flow
* Handles the overall layout and step transitions
*/
export default function CreateProjectFlow() {
const router = useRouter()
const { resolvedTheme } = useTheme()
const [mounted, setMounted] = useState(false)
const { currentStep, setCurrentStep, resetOnboarding } = useOnboarding()
// Handle hydration mismatch by waiting for mount
useEffect(() => {
setMounted(true)
}, [])
// Reset onboarding state when the component unmounts (optional)
useEffect(() => {
return () => {
// Optional cleanup actions
}
}, [resetOnboarding])
// Handle closing the modal
const handleClose = () => {
router.push('/projects')
}
// Navigate directly to a specific step
const navigateToStep = (step: 'connect' | 'configure' | 'deploy') => {
setCurrentStep(step)
}
// 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="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
{/* Fixed dimensions modal container */}
<div className={`${isDarkMode ? 'bg-black' : 'bg-white'} rounded-xl overflow-hidden flex shadow-2xl w-[1000px] h-[620px]`}>
{/* Left sidebar with fixed width */}
<div className={`w-[280px] min-w-[280px] ${isDarkMode ? 'bg-zinc-900' : 'bg-zinc-50'} p-8 relative overflow-hidden border-r ${isDarkMode ? 'border-zinc-800' : 'border-zinc-200'}`}>
{/* Laconic logo */}
<div className="flex items-center gap-2 mb-12">
<LaconicMark className="h-8 w-8" />
<span className={`${isDarkMode ? 'text-white' : 'text-zinc-900'} text-xl font-bold`}>LACONIC</span>
</div>
{/* Steps - clickable */}
<div className="space-y-6">
{/* Connect step */}
<button
className="flex w-full text-left"
onClick={() => navigateToStep('connect')}
>
<div className="mr-4">
<div className={`w-10 h-10 rounded-lg ${currentStep === 'connect'
? (isDarkMode ? 'bg-white' : 'bg-black')
: (isDarkMode ? 'bg-zinc-800' : 'bg-zinc-200')
} 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'
? (isDarkMode ? 'text-black' : 'text-white')
: (isDarkMode ? 'text-zinc-400' : 'text-zinc-600')
}>
<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>
<div className={`w-px h-10 ${isDarkMode ? 'bg-zinc-800' : 'bg-zinc-200'} mx-auto mt-2`}></div>
</div>
<div>
<h3 className={`font-medium text-base ${currentStep === 'connect'
? (isDarkMode ? 'text-white' : 'text-zinc-900')
: (isDarkMode ? 'text-zinc-400' : 'text-zinc-600')
}`}>Connect</h3>
<p className={`text-sm ${currentStep === 'connect'
? (isDarkMode ? 'text-zinc-300' : 'text-zinc-700')
: (isDarkMode ? 'text-zinc-500' : 'text-zinc-500')
}`}>Connect and import a GitHub repo</p>
</div>
</button>
{/* Configure step */}
<button
className="flex w-full text-left"
onClick={() => navigateToStep('configure')}
>
<div className="mr-4">
<div className={`w-10 h-10 rounded-lg ${currentStep === 'configure'
? (isDarkMode ? 'bg-white' : 'bg-black')
: (isDarkMode ? 'bg-zinc-800' : 'bg-zinc-200')
} 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'
? (isDarkMode ? 'text-black' : 'text-white')
: (isDarkMode ? 'text-zinc-400' : 'text-zinc-600')
}>
<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>
</div>
<div className={`w-px h-10 ${isDarkMode ? 'bg-zinc-800' : 'bg-zinc-200'} mx-auto mt-2`}></div>
</div>
<div>
<h3 className={`font-medium text-base ${currentStep === 'configure'
? (isDarkMode ? 'text-white' : 'text-zinc-900')
: (isDarkMode ? 'text-zinc-400' : 'text-zinc-600')
}`}>Configure</h3>
<p className={`text-sm ${currentStep === 'configure'
? (isDarkMode ? 'text-zinc-300' : 'text-zinc-700')
: (isDarkMode ? 'text-zinc-500' : 'text-zinc-500')
}`}>Define the deployment type</p>
</div>
</button>
{/* Deploy step */}
<button
className="flex w-full text-left"
onClick={() => navigateToStep('deploy')}
>
<div className="mr-4">
<div className={`w-10 h-10 rounded-lg ${currentStep === 'deploy' || currentStep === 'success'
? (isDarkMode ? 'bg-white' : 'bg-black')
: (isDarkMode ? 'bg-zinc-800' : 'bg-zinc-200')
} 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' || currentStep === 'success'
? (isDarkMode ? 'text-black' : 'text-white')
: (isDarkMode ? 'text-zinc-400' : 'text-zinc-600')
}>
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
<polyline points="7.5 4.21 12 6.81 16.5 4.21"></polyline>
<polyline points="7.5 19.79 7.5 14.6 3 12"></polyline>
<polyline points="21 12 16.5 14.6 16.5 19.79"></polyline>
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
<line x1="12" y1="22.08" x2="12" y2="12"></line>
</svg>
</div>
</div>
<div>
<h3 className={`font-medium text-base ${currentStep === 'deploy' || currentStep === 'success'
? (isDarkMode ? 'text-white' : 'text-zinc-900')
: (isDarkMode ? 'text-zinc-400' : 'text-zinc-600')
}`}>Deploy</h3>
<p className={`text-sm ${currentStep === 'deploy' || currentStep === 'success'
? (isDarkMode ? 'text-zinc-300' : 'text-zinc-700')
: (isDarkMode ? 'text-zinc-500' : 'text-zinc-500')
}`}>Review and confirm deployment</p>
</div>
</button>
</div>
{/* Laconic mark (larger, bottom left) */}
<div className="absolute -bottom-2 -left-2 opacity-10">
<LaconicMark className={`w-40 h-40 ${isDarkMode ? 'text-zinc-300' : 'text-zinc-700'}`} />
</div>
</div>
{/* Main content with fixed dimensions and scrolling */}
<div className={`flex-1 ${isDarkMode ? 'bg-black' : 'bg-white'} relative`}>
{/* Close button */}
<button
className={`absolute top-4 right-4 ${isDarkMode ? 'text-zinc-400 hover:text-white' : 'text-zinc-600 hover:text-zinc-900'} z-10`}
onClick={handleClose}
>
<X size={24} />
</button>
{/* Scrollable content container */}
<div className="w-full h-full overflow-y-auto">
{currentStep === 'connect' && <ConnectStep />}
{currentStep === 'configure' && <ConfigureStep />}
{currentStep === 'deploy' && <DeployStep />}
{currentStep === 'success' && <SuccessStep />}
</div>
{/* Progress indicator */}
<div className="absolute bottom-6 left-0 right-0 flex justify-center gap-3">
<div className={`w-12 h-1 rounded-full ${currentStep === 'connect' ? 'bg-blue-600' : (isDarkMode ? 'bg-zinc-700' : 'bg-zinc-300')}`}></div>
<div className={`w-12 h-1 rounded-full ${currentStep === 'configure' ? 'bg-blue-600' : (isDarkMode ? 'bg-zinc-700' : 'bg-zinc-300')}`}></div>
<div className={`w-12 h-1 rounded-full ${currentStep === 'deploy' || currentStep === 'success' ? 'bg-blue-600' : (isDarkMode ? 'bg-zinc-700' : 'bg-zinc-300')}`}></div>
</div>
</div>
</div>
</div>
)
}

View File

@ -72,7 +72,7 @@ export default function ProjectsPage() {
<PageWrapper
header={{
title: 'Projects',
actions: [{ label: 'Create Project', href: '/projects/create' }]
actions: [{ label: 'Create Project', href: '/projects/github/ps/cr' }]
}}
layout="bento"
className="pb-0"

View File

@ -1,21 +1,256 @@
// 'use client'
// import { VisuallyHidden } from '@radix-ui/react-visually-hidden'
// import {
// Dialog,
// DialogContent,
// DialogTitle,
// DialogTrigger
// } from '@workspace/ui/components/dialog'
// import { useEffect, useState } from 'react'
// import Onboarding from './Onboarding'
// import { useOnboarding } from './store'
// // Local storage keys
// const ONBOARDING_COMPLETED_KEY = 'onboarding_completed'
// const ONBOARDING_STATE_KEY = 'onboarding_state'
// const ONBOARDING_PROGRESS_KEY = 'onboarding_progress'
// const ONBOARDING_FORCE_CONNECT_KEY = 'onboarding_force_connect'
// interface OnboardingDialogProps {
// trigger?: React.ReactNode
// defaultOpen?: boolean
// onClose?: () => void
// }
// /**
// * OnboardingDialog component
// *
// * A dialog modal that contains the onboarding flow.
// * Can be triggered by a custom element or automatically opened.
// * Sets the initial step based on GitHub connection status.
// * Provides warnings when exiting mid-step and options to continue progress.
// */
// const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
// trigger,
// defaultOpen = false,
// onClose
// }) => {
// const onboardingStore = useOnboarding()
// const { setCurrentStep, currentStep, formData } = onboardingStore
// // const { octokit } = useOctokit()
// const [showExitWarning, setShowExitWarning] = useState(false)
// const [showContinueAlert, setShowContinueAlert] = useState(false)
// const [isOpen, setIsOpen] = useState(defaultOpen)
// const [forceConnectStep, setForceConnectStep] = useState(false)
// // Check for force connect flag when component mounts
// useEffect(() => {
// const shouldForceConnect =
// localStorage.getItem(ONBOARDING_FORCE_CONNECT_KEY) === 'true'
// if (shouldForceConnect) {
// setForceConnectStep(true)
// // Clear the flag so it's only used once
// localStorage.removeItem(ONBOARDING_FORCE_CONNECT_KEY)
// }
// }, [])
// // Local implementation of reset function that handles all necessary state
// const resetOnboardingState = () => {
// // Reset step to connect
// setCurrentStep('connect')
// // Flag to force starting from the connect step
// setForceConnectStep(true)
// // Also reset form data to ensure substeps are cleared
// const store = onboardingStore
// store.setFormData({
// projectName: '',
// repoName: '',
// repoDescription: '',
// framework: '',
// access: 'public',
// organizationSlug: ''
// })
// }
// // Close and reset onboarding dialog
// const closeOnboarding = () => {
// // Remove the "in progress" flag from localStorage
// localStorage.removeItem(ONBOARDING_PROGRESS_KEY)
// // Also remove saved state to prevent issues on next open
// localStorage.removeItem(ONBOARDING_STATE_KEY)
// // Reset component state
// resetOnboardingState()
// setShowContinueAlert(false)
// // Explicitly set isOpen to false to ensure dialog closes
// setIsOpen(false)
// }
// // Check if there's existing progress
// useEffect(() => {
// if (isOpen) {
// const savedProgress = localStorage.getItem(ONBOARDING_PROGRESS_KEY)
// const savedState = localStorage.getItem(ONBOARDING_STATE_KEY)
// if (savedProgress === 'true' && savedState && !forceConnectStep) {
// // Show continue or start fresh dialog
// setShowContinueAlert(true)
// } else {
// // Set initial step based on GitHub connection status
// initializeOnboarding()
// }
// }
// }, [isOpen, forceConnectStep])
// // Set the initial step based on GitHub connection status
// const initializeOnboarding = () => {
// // Reset previous state
// resetOnboardingState()
// // If GitHub is connected AND we're not forcing the connect step,
// // start at the configure step. Otherwise, start at the connect step
// // if (octokit && !forceConnectStep) {
// // setCurrentStep('configure')
// // } else {
// // setCurrentStep('connect')
// // }
// // Mark that we have onboarding in progress
// localStorage.setItem(ONBOARDING_PROGRESS_KEY, 'true')
// // Save the initial state
// saveCurrentState()
// }
// // Start fresh by initializing onboarding and forcing the connect step
// // Continue from saved state and don't force the connect step
// // Save current onboarding state
// const saveCurrentState = () => {
// try {
// const state = {
// currentStep,
// formData,
// forceConnectStep // Save this flag as part of the state
// }
// localStorage.setItem(ONBOARDING_STATE_KEY, JSON.stringify(state))
// } catch (error) {
// console.error('Error saving onboarding state:', error)
// }
// }
// // Load saved onboarding state
// // Save state on step changes
// // useEffect(() => {
// // if (isOpen) {
// // saveCurrentState()
// // }
// // }, [isOpen, currentStep, formData, forceConnectStep])
// // Mark onboarding as completed when user reaches the deploy step
// useEffect(() => {
// if (currentStep === 'deploy') {
// localStorage.setItem(ONBOARDING_COMPLETED_KEY, 'true')
// }
// }, [currentStep])
// // Handle dialog close attempt
// const handleOpenChange = (open: boolean) => {
// // First update the isOpen state to ensure UI responds immediately
// setIsOpen(open)
// if (!open) {
// // When dialog is closing, properly clean up
// closeOnboarding()
// if (onClose) {
// onClose()
// }
// }
// }
// // Define the missing functions to handle dialog closing
// return (
// <>
// <Dialog open={isOpen} onOpenChange={handleOpenChange}>
// {trigger && <DialogTrigger asChild>{trigger}</DialogTrigger>}
// <DialogContent className="max-w-[95vw] max-h-[95vh] w-[1200px] h-[800px] overflow-hidden p-0">
// <VisuallyHidden>
// <DialogTitle>Onboarding</DialogTitle>
// </VisuallyHidden>
// <div className="h-full min-w-full w-3xl">
// <Onboarding />
// </div>
// </DialogContent>
// </Dialog>
// {/* Exit Warning Dialog */}
// {/* <AlertDialog open={showExitWarning} onOpenChange={setShowExitWarning}>
// <AlertDialogContent>
// <AlertDialogTitle>Exit Onboarding?</AlertDialogTitle>
// <AlertDialogDescription>
// You haven't completed the onboarding process. If you exit now, your
// progress will be lost, including any organization or repository
// selections.
// </AlertDialogDescription>
// <AlertDialogFooter>
// <AlertDialogCancel onClick={cancelClose}>Cancel</AlertDialogCancel>
// <AlertDialogAction onClick={completeClose}>
// Exit Anyway
// </AlertDialogAction>
// </AlertDialogFooter>
// </AlertDialogContent>
// </AlertDialog> */}
// {/* Continue Progress Dialog */}
// {/* <AlertDialog open={showContinueAlert} onOpenChange={setShowContinueAlert}>
// <AlertDialogContent>
// <AlertDialogTitle>Continue Onboarding?</AlertDialogTitle>
// <AlertDialogDescription>
// You're in the middle of setting up your project, including
// organization and repository selection. Would you like to continue
// where you left off or start fresh?
// </AlertDialogDescription>
// <AlertDialogFooter>
// <AlertDialogCancel onClick={startFresh}>
// Start Fresh
// </AlertDialogCancel>
// <AlertDialogAction onClick={continueOnboarding}>
// Continue
// </AlertDialogAction>
// </AlertDialogFooter>
// </AlertDialogContent>
// </AlertDialog> */}
// </>
// )
// }
// /**
// * Helper function to check if the user has completed onboarding
// * @returns {boolean} Whether onboarding has been completed
// */
// export const hasCompletedOnboarding = (): boolean => {
// return localStorage.getItem(ONBOARDING_COMPLETED_KEY) === 'true'
// }
// 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 { useRepoData } from '@/hooks/useRepoData'
import { Dialog, DialogContent, DialogTitle } from '@workspace/ui/components/dialog'
import { VisuallyHidden } from '@radix-ui/react-visually-hidden'
import {
Dialog,
DialogContent,
DialogTitle,
DialogTrigger
} from '@workspace/ui/components/dialog'
import { useEffect, useState } from 'react'
import Onboarding from './Onboarding'
import { useOnboarding } from './store'
// Local storage keys
const ONBOARDING_COMPLETED_KEY = 'onboarding_completed'
const ONBOARDING_STATE_KEY = 'onboarding_state'
const ONBOARDING_PROGRESS_KEY = 'onboarding_progress'
const ONBOARDING_FORCE_CONNECT_KEY = 'onboarding_force_connect'
interface OnboardingDialogProps {
trigger?: React.ReactNode
@ -28,214 +263,293 @@ interface OnboardingDialogProps {
*
* A dialog modal that contains the onboarding flow.
* Can be triggered by a custom element or automatically opened.
* Sets the initial step based on GitHub connection status.
* Provides warnings when exiting mid-step and options to continue progress.
*/
const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
trigger,
defaultOpen = false,
onClose
}) => {
const onboardingStore = useOnboarding()
const { setCurrentStep, currentStep, formData } = onboardingStore
// const { octokit } = useOctokit()
const [showExitWarning, setShowExitWarning] = useState(false)
const [showContinueAlert, setShowContinueAlert] = useState(false)
const { nextStep, setFormData, formData, currentStep } = useOnboarding()
const [selectedRepo, setSelectedRepo] = useState<string>(formData.githubRepo || '')
const [isImportMode, setIsImportMode] = useState(true)
const [isOpen, setIsOpen] = useState(defaultOpen)
const [forceConnectStep, setForceConnectStep] = useState(false)
// Check for force connect flag when component mounts
useEffect(() => {
const shouldForceConnect =
localStorage.getItem(ONBOARDING_FORCE_CONNECT_KEY) === 'true'
if (shouldForceConnect) {
setForceConnectStep(true)
// Clear the flag so it's only used once
localStorage.removeItem(ONBOARDING_FORCE_CONNECT_KEY)
}
}, [])
// Local implementation of reset function that handles all necessary state
const resetOnboardingState = () => {
// Reset step to connect
setCurrentStep('connect')
// Flag to force starting from the connect step
setForceConnectStep(true)
// Also reset form data to ensure substeps are cleared
const store = onboardingStore
store.setFormData({
projectName: '',
repoName: '',
repoDescription: '',
framework: '',
access: 'public',
organizationSlug: ''
})
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')
}
// Close and reset onboarding dialog
const closeOnboarding = () => {
// Remove the "in progress" flag from localStorage
localStorage.removeItem(ONBOARDING_PROGRESS_KEY)
// Also remove saved state to prevent issues on next open
localStorage.removeItem(ONBOARDING_STATE_KEY)
// Reset component state
resetOnboardingState()
setShowContinueAlert(false)
// Explicitly set isOpen to false to ensure dialog closes
setIsOpen(false)
}
// Check if there's existing progress
useEffect(() => {
if (isOpen) {
const savedProgress = localStorage.getItem(ONBOARDING_PROGRESS_KEY)
const savedState = localStorage.getItem(ONBOARDING_STATE_KEY)
if (savedProgress === 'true' && savedState && !forceConnectStep) {
// Show continue or start fresh dialog
setShowContinueAlert(true)
} else {
// Set initial step based on GitHub connection status
initializeOnboarding()
}
}
}, [isOpen, forceConnectStep])
// Set the initial step based on GitHub connection status
const initializeOnboarding = () => {
// Reset previous state
resetOnboardingState()
// If GitHub is connected AND we're not forcing the connect step,
// start at the configure step. Otherwise, start at the connect step
// if (octokit && !forceConnectStep) {
// setCurrentStep('configure')
// } else {
// setCurrentStep('connect')
// }
// Mark that we have onboarding in progress
localStorage.setItem(ONBOARDING_PROGRESS_KEY, 'true')
// Save the initial state
saveCurrentState()
}
// Start fresh by initializing onboarding and forcing the connect step
// Continue from saved state and don't force the connect step
// Save current onboarding state
const saveCurrentState = () => {
try {
const state = {
currentStep,
formData,
forceConnectStep // Save this flag as part of the state
}
localStorage.setItem(ONBOARDING_STATE_KEY, JSON.stringify(state))
} catch (error) {
console.error('Error saving onboarding state:', error)
}
}
// Load saved onboarding state
// Save state on step changes
// useEffect(() => {
// if (isOpen) {
// saveCurrentState()
// }
// }, [isOpen, currentStep, formData, forceConnectStep])
// Mark onboarding as completed when user reaches the deploy step
useEffect(() => {
if (currentStep === 'deploy') {
localStorage.setItem(ONBOARDING_COMPLETED_KEY, 'true')
}
}, [currentStep])
// Handle dialog close attempt
// Handle dialog open/close
const handleOpenChange = (open: boolean) => {
// First update the isOpen state to ensure UI responds immediately
setIsOpen(open)
if (!open) {
// When dialog is closing, properly clean up
closeOnboarding()
if (onClose) {
onClose()
}
if (!open && onClose) {
onClose()
}
}
// Define the missing functions to handle dialog closing
return (
<>
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
{trigger && <DialogTrigger asChild>{trigger}</DialogTrigger>}
<DialogContent className="max-w-[95vw] max-h-[95vh] w-[1200px] h-[800px] overflow-hidden p-0">
<VisuallyHidden>
<DialogTitle>Onboarding</DialogTitle>
</VisuallyHidden>
<div className="h-full min-w-full w-3xl">
<Onboarding />
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
{trigger && trigger}
<DialogContent className="max-w-[95vw] max-h-[95vh] w-[1200px] h-[800px] overflow-hidden p-0 bg-black border-zinc-800">
<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>
<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'}>
<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>
<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>
</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'}>
<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>
</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>
</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>
<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>
</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>
</DialogContent>
</Dialog>
{/* Exit Warning Dialog */}
{/* <AlertDialog open={showExitWarning} onOpenChange={setShowExitWarning}>
<AlertDialogContent>
<AlertDialogTitle>Exit Onboarding?</AlertDialogTitle>
<AlertDialogDescription>
You haven't completed the onboarding process. If you exit now, your
progress will be lost, including any organization or repository
selections.
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel onClick={cancelClose}>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={completeClose}>
Exit Anyway
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog> */}
{/* Continue Progress Dialog */}
{/* <AlertDialog open={showContinueAlert} onOpenChange={setShowContinueAlert}>
<AlertDialogContent>
<AlertDialogTitle>Continue Onboarding?</AlertDialogTitle>
<AlertDialogDescription>
You're in the middle of setting up your project, including
organization and repository selection. Would you like to continue
where you left off or start fresh?
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel onClick={startFresh}>
Start Fresh
</AlertDialogCancel>
<AlertDialogAction onClick={continueOnboarding}>
Continue
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog> */}
</>
{/* Main content */}
<div className="flex-1 bg-zinc-950 p-8 relative">
{/* Close 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">
<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>
<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">
<SelectTrigger className="w-full bg-zinc-900 border-zinc-800 py-6 text-white">
<div className="flex items-center">
<Github className="mr-2 h-5 w-5" />
<SelectValue placeholder="Select Git account" />
</div>
</SelectTrigger>
<SelectContent className="bg-zinc-900 border-zinc-800">
<SelectItem value="git-account">git-account</SelectItem>
</SelectContent>
</Select>
</div>
{/* Mode buttons */}
<div className="grid grid-cols-2 gap-2 mb-4">
<Button
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"}
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">
<ScrollableRepoList
repositories={repositories || []}
isLoading={isLoading}
onSelect={handleRepoSelect}
selectedRepo={selectedRepo}
/>
</div>
) : (
<div className="mb-8">
<TemplateList onSelect={() => {}} />
</div>
)}
{/* Navigation buttons */}
<div className="flex justify-between items-center mt-8">
<Button
variant="outline"
className="text-zinc-400 bg-zinc-900 border-zinc-800 hover:bg-zinc-800"
disabled={true}
>
Previous
</Button>
<div className="flex space-x-2">
<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 className="w-12 h-1 rounded-full bg-zinc-800"></div>
</div>
<Button
className="bg-blue-600 hover:bg-blue-700 text-white"
onClick={nextStep}
disabled={!selectedRepo && isImportMode}
>
Next
</Button>
</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>
<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"
className="text-zinc-400 bg-zinc-900 border-zinc-800 hover:bg-zinc-800"
onClick={() => useOnboarding.getState().previousStep()}
>
Previous
</Button>
<div className="flex space-x-2">
<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 className="w-12 h-1 rounded-full bg-zinc-800"></div>
</div>
<Button
className="bg-blue-600 hover:bg-blue-700 text-white"
onClick={nextStep}
>
Next
</Button>
</div>
</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>
<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"
className="text-zinc-400 bg-zinc-900 border-zinc-800 hover:bg-zinc-800"
onClick={() => useOnboarding.getState().previousStep()}
>
Previous
</Button>
<div className="flex space-x-2">
<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 className="w-12 h-1 rounded-full bg-blue-600"></div>
</div>
<Button
className="bg-blue-600 hover:bg-blue-700 text-white"
>
Deploy
</Button>
</div>
</div>
</div>
)}
</div>
</div>
</DialogContent>
</Dialog>
)
}
/**
* Helper function to check if the user has completed onboarding
* @returns {boolean} Whether onboarding has been completed
*/
export const hasCompletedOnboarding = (): boolean => {
return localStorage.getItem(ONBOARDING_COMPLETED_KEY) === 'true'
}
export default OnboardingDialog
export default OnboardingDialog

View File

@ -0,0 +1,69 @@
'use client'
import { GitBranch, Settings, Box } from 'lucide-react'
import type { Step } from '@/components/onboarding/useOnboarding'
interface OnboardingSidebarProps {
currentStep: Step
}
export function OnboardingSidebar({ currentStep }: OnboardingSidebarProps) {
return (
<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>
<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>
<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>
</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>
<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>
</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>
</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>
</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>
)
}

View File

@ -1,68 +1,320 @@
import { FileCog } from 'lucide-react'
import { useState } from 'react'
import { useOnboarding } from '../store'
'use client'
import { useState, useEffect } from 'react'
import { PlusCircle } from 'lucide-react'
import { useTheme } from 'next-themes'
import { useOnboarding } from '@/components/onboarding/useOnboarding'
import { Button } from '@workspace/ui/components/button'
import { Input } from '@workspace/ui/components/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@workspace/ui/components/select'
import { Checkbox } from '@workspace/ui/components/checkbox'
import { Label } from '@workspace/ui/components/label'
/**
* 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 { nextStep, previousStep, setFormData, formData } = useOnboarding()
const { resolvedTheme } = useTheme()
const [mounted, setMounted] = useState(false)
// Form state
const [deployOption, setDeployOption] = useState<'auction' | 'lrn'>(
formData.deploymentType as ('auction' | 'lrn') || 'auction'
)
const [environments, setEnvironments] = useState({
const [numberOfDeployers, setNumberOfDeployers] = useState<string>(
formData.deployerCount || "1"
)
const [maxPrice, setMaxPrice] = useState<string>(
formData.maxPrice || "1000"
)
const [selectedLrn, setSelectedLrn] = useState<string>(
formData.selectedLrn || ""
)
const [selectedAccount, setSelectedAccount] = useState<string>("")
const [environments, setEnvironments] = useState<{
production: boolean,
preview: boolean,
development: boolean
}>(formData.environments || {
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],
// },
// })
// }
const [envVars, setEnvVars] = useState<{ key: string; value: string }[]>([
{ key: '', value: '' }
])
// Handle hydration mismatch by waiting for mount
useEffect(() => {
setMounted(true)
}, [])
// Initialize environment variables from formData if available
useEffect(() => {
if (formData.environmentVariables) {
const vars: { key: string; value: string }[] = Object.entries(formData.environmentVariables).map(
([key, value]) => ({ key, value })
)
setEnvVars(vars.length > 0 ? vars : [{ key: '', value: '' }])
}
}, [formData.environmentVariables])
// Add an empty environment variable row
const addEnvVar = () => {
setEnvVars([...envVars, { key: '', value: '' }])
}
// Toggle deployment option
const toggleDeployOption = (option: 'auction' | 'lrn') => {
setDeployOption(option)
}
// Toggle environment checkbox
const toggleEnvironment = (env: 'production' | 'preview' | 'development') => {
setEnvironments({
...environments,
[env]: !environments[env]
})
}
// Handle next step
const handleNext = () => {
// Save configuration to form data
setFormData({
deploymentType: deployOption,
deployerCount: numberOfDeployers,
maxPrice: maxPrice,
selectedLrn: selectedLrn,
environments: environments,
environmentVariables: envVars.reduce((acc, { key, value }) => {
if (key && value) {
acc[key] = value
}
return acc
}, {} as Record<string, string>)
})
nextStep()
}
// 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">
<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="w-full h-full flex flex-col p-8 overflow-y-auto">
{/* Configure icon and header */}
<div className="flex flex-col items-center justify-center mb-8">
<div className="mb-4">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={isDarkMode ? "text-white" : "text-black"}>
<path d="M12 20h9" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
<h2 className={`text-2xl font-medium text-center mb-2 ${isDarkMode ? "text-white" : "text-zinc-900"}`}>Configure</h2>
<p className={`text-center text-zinc-500 max-w-md`}>
Set the deployer LRN for a single deployment or by creating a deployer auction for multiple deployments
</p>
</div>
<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 className="max-w-xl mx-auto w-full">
{/* Deployment options */}
<div className="grid grid-cols-2 gap-2 mb-6">
<Button
variant={deployOption === 'auction' ? "default" : "outline"}
className={`py-3 ${deployOption === 'auction'
? (isDarkMode ? 'bg-zinc-800 text-white' : 'bg-zinc-800 text-white')
: (isDarkMode ? 'bg-transparent border-zinc-700 text-zinc-400' : 'bg-transparent border-zinc-300 text-zinc-600')}`}
onClick={() => toggleDeployOption('auction')}
>
Create Auction
</Button>
<Button
variant={deployOption === 'lrn' ? "default" : "outline"}
className={`py-3 ${deployOption === 'lrn'
? (isDarkMode ? 'bg-zinc-800 text-white' : 'bg-zinc-800 text-white')
: (isDarkMode ? 'bg-transparent border-zinc-700 text-zinc-400' : 'bg-transparent border-zinc-300 text-zinc-600')}`}
onClick={() => toggleDeployOption('lrn')}
>
Deployer LRN
</Button>
</div>
{deployOption === 'auction' ? (
<>
{/* Auction settings */}
<div className="grid grid-cols-2 gap-4 mb-6">
<div>
<Label htmlFor="deployers" className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}>
Number of Deployers
</Label>
<Select value={numberOfDeployers} onValueChange={setNumberOfDeployers}>
<SelectTrigger id="deployers" className={isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}>
<SelectValue placeholder="Select number" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">1</SelectItem>
<SelectItem value="2">2</SelectItem>
<SelectItem value="3">3</SelectItem>
<SelectItem value="5">5</SelectItem>
<SelectItem value="10">10</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="maxPrice" className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}>
Maximum Price (aint)
</Label>
<Select value={maxPrice} onValueChange={setMaxPrice}>
<SelectTrigger id="maxPrice" className={isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}>
<SelectValue placeholder="Select price" />
</SelectTrigger>
<SelectContent>
<SelectItem value="500">500</SelectItem>
<SelectItem value="1000">1000</SelectItem>
<SelectItem value="2000">2000</SelectItem>
<SelectItem value="5000">5000</SelectItem>
</SelectContent>
</Select>
</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 /> */}
</>
) : (
<>
{/* LRN settings */}
<div className="mb-6">
<Label htmlFor="lrn" className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}>
Select Deployer LRN
</Label>
<Select value={selectedLrn} onValueChange={setSelectedLrn}>
<SelectTrigger id="lrn" className={isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent>
<SelectItem value="lrn-1">Deployer LRN 1</SelectItem>
<SelectItem value="lrn-2">Deployer LRN 2</SelectItem>
<SelectItem value="lrn-3">Deployer LRN 3</SelectItem>
</SelectContent>
</Select>
</div>
</>
)}
{/* Environment Variables */}
<div className="mb-6">
<Label className={`text-sm font-medium mb-2 block ${isDarkMode ? 'text-white' : 'text-zinc-900'}`}>Environment Variables</Label>
<div className={`border rounded-md p-4 ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}>
{envVars.map((envVar, index) => (
<div key={index} className="grid grid-cols-2 gap-2 mb-2">
<Input
placeholder="KEY"
value={envVar.key}
onChange={(e) => {
const newEnvVars = [...envVars];
newEnvVars[index].key = e.target.value;
setEnvVars(newEnvVars);
}}
className={`bg-transparent ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}
/>
<Input
placeholder="VALUE"
value={envVar.value}
onChange={(e) => {
const newEnvVars = [...envVars];
newEnvVars[index].value = e.target.value;
setEnvVars(newEnvVars);
}}
className={`bg-transparent ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}
/>
</div>
))}
<Button
variant="outline"
className={`w-full mt-2 ${isDarkMode ? 'text-zinc-400 border-zinc-700' : 'text-zinc-600 border-zinc-300'}`}
onClick={addEnvVar}
>
<PlusCircle className="h-4 w-4 mr-2" />
Add Variable
</Button>
</div>
</div>
{/* Environment Tags */}
<div className="mb-6">
<Label className={`text-sm font-medium mb-2 block ${isDarkMode ? 'text-white' : 'text-zinc-900'}`}>Environment</Label>
<div className="space-y-3">
<div className="flex items-center">
<Checkbox
id="production"
checked={environments.production}
onCheckedChange={() => toggleEnvironment('production')}
className={isDarkMode ? 'border-zinc-600' : 'border-zinc-300'}
/>
<Label htmlFor="production" className={`ml-2 ${isDarkMode ? 'text-zinc-400' : 'text-zinc-600'}`}>
Production
</Label>
</div>
<div className="flex items-center">
<Checkbox
id="preview"
checked={environments.preview}
onCheckedChange={() => toggleEnvironment('preview')}
className={isDarkMode ? 'border-zinc-600' : 'border-zinc-300'}
/>
<Label htmlFor="preview" className={`ml-2 ${isDarkMode ? 'text-zinc-400' : 'text-zinc-600'}`}>
Preview
</Label>
</div>
<div className="flex items-center">
<Checkbox
id="development"
checked={environments.development}
onCheckedChange={() => toggleEnvironment('development')}
className={isDarkMode ? 'border-zinc-600' : 'border-zinc-300'}
/>
<Label htmlFor="development" className={`ml-2 ${isDarkMode ? 'text-zinc-400' : 'text-zinc-600'}`}>
Development
</Label>
</div>
</div>
</div>
{/* Account selection */}
<div className="mb-8">
<Label htmlFor="account" className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}>
Select Account
</Label>
<Select value={selectedAccount} onValueChange={setSelectedAccount}>
<SelectTrigger id="account" className={isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent>
<SelectItem value="account-1">Account 1</SelectItem>
<SelectItem value="account-2">Account 2</SelectItem>
</SelectContent>
</Select>
</div>
{/* 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}
>
Next
</Button>
</div>
</div>
</div>
)
}
}

View File

@ -0,0 +1,68 @@
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>
)
}

View File

@ -1,48 +1,174 @@
'use client'
import { useState, useEffect } from 'react'
import { Github } from 'lucide-react'
import { useTheme } from 'next-themes'
import { useOnboarding } from '@/components/onboarding/useOnboarding'
import { Button } from '@workspace/ui/components/button'
import { useState } from 'react'
import { useOnboarding } from '../store'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@workspace/ui/components/select'
import { useRepoData } from '@/hooks/useRepoData'
type ConnectState = 'initial' | 'repository-select' | 'template-select'
interface Repository {
id: string | number
full_name: string
html_url?: string
}
/**
* 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')
const { nextStep, setFormData, formData } = useOnboarding()
const { resolvedTheme } = useTheme()
const [mounted, setMounted] = useState(false)
const [selectedRepo, setSelectedRepo] = useState<string>(formData.githubRepo || '')
const [isImportMode, setIsImportMode] = useState(true)
const { repoData: repositories, isLoading } = useRepoData('')
// Handle hydration mismatch by waiting for mount
useEffect(() => {
setMounted(true)
}, [])
// 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')
}
// Handle next step
const handleNext = () => {
if (selectedRepo || !isImportMode) {
nextStep()
}
}
// 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="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 className="w-full h-full flex flex-col items-center justify-center p-8">
<div className="max-w-md w-full mx-auto">
{/* Connect icon */}
<div className="mx-auto mb-6 flex justify-center">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={isDarkMode ? "text-white" : "text-black"}>
<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" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<line x1="8" y1="12" x2="16" y2="12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
) : (
// <ConnectAccount onAuth={() => {}} />
<>...connect goes here</>
)}
{/* Connect header */}
<h2 className={`text-2xl font-medium ${isDarkMode ? "text-white" : "text-zinc-900"} text-center mb-2`}>Connect</h2>
<p className="text-center text-zinc-500 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">
<SelectTrigger className={`w-full py-3 ${isDarkMode ? "border-zinc-700" : "border-zinc-300"}`}>
<div className="flex items-center">
<Github className="mr-2 h-5 w-5" />
<SelectValue placeholder="Select Git account" />
</div>
</SelectTrigger>
<SelectContent>
<SelectItem value="git-account">git-account</SelectItem>
</SelectContent>
</Select>
</div>
{/* Mode buttons */}
<div className="grid grid-cols-2 gap-2 mb-4">
<Button
variant={isImportMode ? "default" : "outline"}
className={`py-3 ${isImportMode
? (isDarkMode ? 'bg-zinc-800 text-white' : 'bg-zinc-800 text-white')
: (isDarkMode ? 'bg-transparent border-zinc-700 text-zinc-400' : 'bg-transparent border-zinc-300 text-zinc-600')}`}
onClick={() => toggleMode('import')}
>
Import a repository
</Button>
<Button
variant={!isImportMode ? "default" : "outline"}
className={`py-3 ${!isImportMode
? (isDarkMode ? 'bg-zinc-800 text-white' : 'bg-zinc-800 text-white')
: (isDarkMode ? 'bg-transparent border-zinc-700 text-zinc-400' : 'bg-transparent border-zinc-300 text-zinc-600')}`}
onClick={() => toggleMode('template')}
>
Start with a template
</Button>
</div>
{/* Repository or template list */}
{isImportMode ? (
<div className={`border rounded-md overflow-hidden mb-4 ${isDarkMode ? "border-zinc-700" : "border-zinc-300"}`}>
{isLoading ? (
<div className="p-6 text-center text-zinc-500">
<div className="animate-spin h-5 w-5 border-2 border-zinc-500 border-t-transparent rounded-full mx-auto mb-2"></div>
Loading repositories...
</div>
) : !repositories || repositories.length === 0 ? (
<div className="p-6 text-center text-zinc-500">
No repositories found
</div>
) : (
<div className="max-h-60 overflow-y-auto">
{repositories.map((repo: Repository) => (
<div
key={repo.id}
className={`flex items-center p-4 ${isDarkMode ? "border-zinc-700" : "border-zinc-200"} border-b last:border-b-0 cursor-pointer ${
selectedRepo === repo.full_name
? (isDarkMode ? 'bg-zinc-800' : 'bg-zinc-100')
: (isDarkMode ? 'hover:bg-zinc-800' : 'hover:bg-zinc-50')
}`}
onClick={() => handleRepoSelect(repo.full_name)}
>
<div className={`flex-1 ${isDarkMode ? "text-white" : "text-zinc-900"}`}>
<Github className="inline-block h-4 w-4 mr-2 text-zinc-500" />
<span>{repo.full_name}</span>
</div>
<div className="text-sm text-zinc-500">
5 minutes ago
</div>
</div>
))}
</div>
)}
</div>
) : (
<div className={`border rounded-md overflow-hidden mb-4 ${isDarkMode ? "border-zinc-700" : "border-zinc-300"}`}>
<div className="p-6 text-center text-zinc-500">
Template selection coming soon
</div>
</div>
)}
{/* Navigation buttons */}
<div className="flex justify-between items-center mt-8">
<Button
variant="outline"
className={`${isDarkMode ? "text-zinc-400 border-zinc-700" : "text-zinc-600 border-zinc-300"} bg-transparent`}
disabled={true}
>
Previous
</Button>
<Button
className={`${isDarkMode ? 'bg-zinc-800' : 'bg-zinc-800'} text-white hover:bg-zinc-700`}
onClick={handleNext}
disabled={!selectedRepo && isImportMode}
>
Next
</Button>
</div>
</div>
</div>
)
}
}

View File

@ -0,0 +1,155 @@
// '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"
/>
{/* 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>
)
}

View File

@ -1,46 +1,132 @@
import { Button } from '@workspace/ui/components/button'
import { GithubIcon } from 'lucide-react'
// import { Button } from '@workspace/ui/components/button'
// import { GithubIcon } from 'lucide-react'
// interface Repository {
// name: string
// updatedAt: string
// }
// interface RepositoryListProps {
// repositories: Repository[]
// onSelect: (repo: Repository) => void
// }
// interface RepoCardProps {
// repo: Repository
// onClick: () => void
// }
// function RepoCard({ repo, onClick }: RepoCardProps) {
// return (
// <Button
// onClick={onClick}
// variant="ghost"
// className="w-full flex items-center gap-3 p-3 hover:bg-accent hover:text-accent-foreground text-left"
// aria-label={`Select repository ${repo.name}`}
// >
// <GithubIcon className="h-4 w-4 text-muted-foreground" />
// <span className="flex-1 text-sm">{repo.name}</span>
// <span className="text-xs text-muted-foreground">{repo.updatedAt}</span>
// </Button>
// )
// }
// export function RepositoryList({
// repositories,
// onSelect
// }: RepositoryListProps) {
// return (
// <div className="space-y-2">
// {repositories.map((repo) => (
// <RepoCard key={repo.name} repo={repo} onClick={() => onSelect(repo)} />
// ))}
// </div>
// )
// }
'use client'
import { Github } from 'lucide-react'
interface Repository {
name: string
updatedAt: string
id: string | number;
full_name: string;
html_url?: string;
}
interface RepositoryListProps {
repositories: Repository[]
onSelect: (repo: Repository) => void
interface ScrollableRepoListProps {
repositories: Repository[];
isLoading: boolean;
onSelect: (repoFullName: string) => void;
selectedRepo: string;
}
interface RepoCardProps {
repo: Repository
onClick: () => void
}
function RepoCard({ repo, onClick }: RepoCardProps) {
return (
<Button
onClick={onClick}
variant="ghost"
className="w-full flex items-center gap-3 p-3 hover:bg-accent hover:text-accent-foreground text-left"
aria-label={`Select repository ${repo.name}`}
>
<GithubIcon className="h-4 w-4 text-muted-foreground" />
<span className="flex-1 text-sm">{repo.name}</span>
<span className="text-xs text-muted-foreground">{repo.updatedAt}</span>
</Button>
)
}
export function RepositoryList({
export function ScrollableRepoList({
repositories,
onSelect
}: RepositoryListProps) {
isLoading,
onSelect,
selectedRepo
}: ScrollableRepoListProps) {
// If there are no repositories or we're loading, show appropriate message
if (isLoading) {
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>
Loading repositories...
</div>
</div>
);
}
if (!repositories || repositories.length === 0) {
return (
<div className="border border-zinc-800 rounded-md bg-zinc-900 overflow-hidden">
<div className="p-6 text-center text-zinc-400">
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}`
}));
return (
<div className="space-y-2">
{repositories.map((repo) => (
<RepoCard key={repo.name} repo={repo} onClick={() => onSelect(repo)} />
))}
<div className="border border-zinc-800 rounded-md bg-zinc-900 overflow-hidden">
{/* Ensure we have a fixed height container with scrolling */}
<div
className="max-h-60 overflow-y-auto"
style={{
scrollbarWidth: 'thin',
scrollbarColor: '#4b5563 #1f2937'
}}
>
{repoList.map((repo, 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' : ''
}`}
onClick={() => onSelect(repo.full_name)}
>
<div className="flex-1 text-white">
<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>
))}
</div>
</div>
)
}
);
}

View File

@ -1,62 +1,126 @@
import { Button } from '@workspace/ui/components/button'
import { Input } from '@workspace/ui/components/input'
import type React from 'react'
// import { Button } from '@workspace/ui/components/button'
// import { Input } from '@workspace/ui/components/input'
// import type React from 'react'
// interface Template {
// id: string
// name: string
// description: string
// icon: React.ReactNode
// }
// interface TemplateListProps {
// templates: Template[]
// onSelect: (template: Template) => void
// projectName: string
// onProjectNameChange: (name: string) => void
// }
// export function TemplateList({
// templates,
// onSelect,
// projectName,
// onProjectNameChange
// }: TemplateListProps) {
// return (
// <div className="space-y-6">
// <div className="space-y-2">
// <label
// htmlFor="project-name"
// className="text-sm font-medium text-foreground"
// >
// Project Name
// </label>
// <Input
// id="project-name"
// value={projectName}
// onChange={(e) => onProjectNameChange(e.target.value)}
// placeholder="new-repository-name"
// className="bg-background"
// />
// </div>
// <div className="space-y-2">
// {templates.map((template) => (
// <Button
// key={template.id}
// onClick={() => onSelect(template)}
// className="w-full flex items-center gap-3 p-3 rounded-lg hover:bg-accent hover:text-accent-foreground transition-colors text-left"
// >
// <div className="h-8 w-8 rounded bg-muted flex items-center justify-center">
// {template.icon}
// </div>
// <div>
// <div className="text-sm font-medium">{template.name}</div>
// <div className="text-xs text-muted-foreground">
// {template.description}
// </div>
// </div>
// </Button>
// ))}
// </div>
// </div>
// )
// }
'use client'
// src/components/onboarding/connect-step/template-list.tsx
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 {
templates: Template[]
onSelect: (template: Template) => void
projectName: string
onProjectNameChange: (name: string) => void
onSelect: (templateId: string) => void;
selectedTemplate?: string;
}
export function TemplateList({
templates,
onSelect,
projectName,
onProjectNameChange
}: TemplateListProps) {
export const TemplateList = ({ onSelect, selectedTemplate }: TemplateListProps) => {
// Mock templates data - in a real app, this would come from an API
const templates: Template[] = [
{
id: 'next-js',
name: 'Next.js',
description: 'React framework with hybrid static & server rendering',
icon: <Layout className="h-6 w-6" />
},
{
id: 'react-app',
name: 'React App',
description: 'Modern React application with Vite',
icon: <Box className="h-6 w-6" />
},
{
id: 'static-site',
name: 'Static Site',
description: 'Simple static site with HTML, CSS, and JavaScript',
icon: <Layout className="h-6 w-6" />
}
];
return (
<div className="space-y-6">
<div className="space-y-2">
<label
htmlFor="project-name"
className="text-sm font-medium text-foreground"
>
Project Name
</label>
<Input
id="project-name"
value={projectName}
onChange={(e) => onProjectNameChange(e.target.value)}
placeholder="new-repository-name"
className="bg-background"
/>
</div>
<div className="space-y-2">
<div className="border border-zinc-800 rounded-md bg-zinc-900 overflow-hidden">
<div className="max-h-60 overflow-y-auto">
{templates.map((template) => (
<Button
<div
key={template.id}
onClick={() => onSelect(template)}
className="w-full flex items-center gap-3 p-3 rounded-lg hover:bg-accent hover:text-accent-foreground transition-colors text-left"
className={`flex items-center p-4 border-b border-zinc-800 last:border-b-0 cursor-pointer hover:bg-zinc-800 ${
selectedTemplate === template.id ? 'bg-zinc-800' : ''
}`}
onClick={() => onSelect(template.id)}
>
<div className="h-8 w-8 rounded bg-muted flex items-center justify-center">
<div className="mr-3 p-2 bg-zinc-800 rounded-md">
{template.icon}
</div>
<div>
<div className="text-sm font-medium">{template.name}</div>
<div className="text-xs text-muted-foreground">
{template.description}
</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>
</Button>
</div>
))}
</div>
</div>
)
}
);
};

View File

@ -1,42 +1,247 @@
'use client'
import { useOnboarding } from '../store'
import { useState, useEffect } from 'react'
import { useTheme } from 'next-themes'
import { Github, Loader2 } from 'lucide-react'
import { useOnboarding } from '@/components/onboarding/useOnboarding'
import { Button } from '@workspace/ui/components/button'
import { Progress } from '@workspace/ui/components/progress'
import { Dialog, DialogContent, DialogTitle, DialogDescription, DialogFooter } from '@workspace/ui/components/dialog'
/**
* Final step in the onboarding flow
* Displays deployment summary and triggers deployment
*
* Features:
* - Configuration summary
* - Repository display
* - Deploy action
*
* @component
*/
export function DeployStep() {
useOnboarding()
const { previousStep, nextStep, formData, setFormData } = useOnboarding()
const { resolvedTheme } = useTheme()
const [mounted, setMounted] = useState(false)
// State
const [isDeploying, setIsDeploying] = useState(false)
const [deploymentProgress, setDeploymentProgress] = useState(0)
const [showConfirmDialog, setShowConfirmDialog] = useState(false)
// Handle hydration mismatch by waiting for mount
useEffect(() => {
setMounted(true)
}, [])
// Repository information from previous steps
const repoFullName = formData.githubRepo || 'git-account/repo-name'
const branch = 'main'
// Open the confirmation modal
const handlePayAndDeploy = () => {
setShowConfirmDialog(true)
}
// Close the confirmation modal
const handleCancelConfirm = () => {
setShowConfirmDialog(false)
}
// Handle confirmed deployment
const handleConfirmDeploy = () => {
setShowConfirmDialog(false)
startDeployment()
}
// Start the deployment process
const startDeployment = () => {
setIsDeploying(true)
// Simulate deployment process with progress updates
let progress = 0
const interval = setInterval(() => {
progress += 10
setDeploymentProgress(progress)
if (progress >= 100) {
clearInterval(interval)
// Generate deployment ID and create URL
const deploymentId = `deploy-${Math.random().toString(36).substring(2, 9)}`
const repoName = repoFullName.split('/').pop() || 'app'
const projectId = `proj-${Math.random().toString(36).substring(2, 9)}`
// Save deployment info
setFormData({
deploymentId,
deploymentUrl: `https://${repoName}.laconic.deploy`,
projectId
})
// Move to success step after short delay
setTimeout(() => {
nextStep()
}, 500)
}
}, 500)
}
// 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">
<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 className="w-full h-full flex flex-col items-center justify-center p-8">
<div className="max-w-md w-full mx-auto">
{/* Deploy icon */}
<div className="mx-auto mb-6 flex justify-center">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={isDarkMode ? "text-white" : "text-black"}>
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<polyline points="7.5 4.21 12 6.81 16.5 4.21" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<polyline points="7.5 19.79 7.5 14.6 3 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<polyline points="21 12 16.5 14.6 16.5 19.79" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<polyline points="3.27 6.96 12 12.01 20.73 6.96" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<line x1="12" y1="22.08" x2="12" y2="12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
{/* Deploy header */}
<h2 className={`text-2xl font-medium ${isDarkMode ? "text-white" : "text-zinc-900"} text-center mb-2`}>Deploy</h2>
<p className="text-center text-zinc-500 mb-8">
Your deployment is configured and ready to go!
</p>
{/* Repository info */}
<div className={`border rounded-lg overflow-hidden mb-8 ${isDarkMode ? "border-zinc-700" : "border-zinc-300"}`}>
<div className="p-5 flex items-center">
<div className="mr-3">
<Github className="h-5 w-5 text-zinc-500" />
</div>
<div className="flex-1">
<div className={isDarkMode ? "text-white" : "text-zinc-900"}>{repoFullName}</div>
<div className="text-sm text-zinc-500">
<svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" className="inline-block mr-1">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
{branch}
</div>
</div>
</div>
Content sections will be placed here: 1. Repository info card 2.
Configuration summary 3. Deploy button
{/* ...content here */}
{/* <Deploy /> */}
</div>
{/* Deployment progress */}
{isDeploying && deploymentProgress > 0 && (
<div className="mb-8">
<div className="flex justify-between items-center mb-2">
<div className={`${isDarkMode ? "text-white" : "text-zinc-900"} text-sm`}>
{deploymentProgress < 30 && "Preparing deployment..."}
{deploymentProgress >= 30 && deploymentProgress < 90 && "Deploying your project..."}
{deploymentProgress >= 90 && "Finalizing deployment..."}
</div>
<div className="text-zinc-500 text-xs">{deploymentProgress}%</div>
</div>
<Progress value={deploymentProgress} className={`h-1 ${isDarkMode ? "bg-zinc-800" : "bg-zinc-200"}`} />
</div>
)}
{/* Navigation buttons */}
<div className="flex justify-between items-center mt-4">
<Button
variant="outline"
className={`${isDarkMode ? "text-zinc-400 border-zinc-700" : "text-zinc-600 border-zinc-300"} bg-transparent`}
onClick={previousStep}
disabled={isDeploying}
>
Previous
</Button>
{isDeploying ? (
<Button
className={`${isDarkMode ? "bg-zinc-700 text-zinc-300" : "bg-zinc-300 text-zinc-600"}`}
disabled
>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Deploying...
</Button>
) : (
<Button
className="bg-blue-600 hover:bg-blue-700 text-white flex items-center"
onClick={handlePayAndDeploy}
>
Pay and Deploy
<svg className="ml-2 h-4 w-4" fill="none" viewBox="0 0 24 24" 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>
)}
</div>
</div>
</div>
</div>
{/* Transaction Confirmation Dialog */}
<Dialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
<DialogContent className="bg-black border-zinc-800 text-white max-w-md">
<DialogTitle className="text-white">Confirm Transaction</DialogTitle>
<DialogDescription className="text-zinc-400">
This is a dialog description.
</DialogDescription>
<div className="space-y-6 py-4">
{/* From */}
<div className="space-y-2">
<h3 className="text-lg font-medium text-white">From</h3>
<div className="space-y-1">
<div className="text-sm text-zinc-400">Address</div>
<div className="text-sm text-white break-all font-mono">laconic1sdfjwel4jfkasfjgjal45ioasjj5jjlajfjj355</div>
</div>
<div className="space-y-1">
<div className="text-sm text-zinc-400">Public Key</div>
<div className="text-sm text-white break-all font-mono">laconic1sdfjwel4jfkasfjgjal45ioasjj5jjlajfjj355</div>
</div>
<div className="space-y-1">
<div className="text-sm text-zinc-400">HD Path</div>
<div className="text-sm text-white font-mono">m/44/118/0/0/0</div>
</div>
</div>
{/* Balance */}
<div className="space-y-1">
<div className="text-lg font-medium text-white">Balance</div>
<div className="text-lg text-white">129600</div>
</div>
{/* To */}
<div className="space-y-1">
<div className="text-lg font-medium text-white">To</div>
<div className="text-sm text-white break-all font-mono">laconic1sdfjwel4jfkasfjgjal45ioasjj5jjlajfjj355</div>
</div>
{/* Amount */}
<div className="space-y-1">
<div className="text-lg font-medium text-white">Amount</div>
<div className="space-y-1">
<div className="text-sm text-zinc-400">Balance (aint)</div>
<div className="text-sm text-white">129600</div>
</div>
<div className="space-y-1">
<div className="text-sm text-zinc-400">Amount (aint)</div>
<div className="text-sm text-white">3000</div>
</div>
</div>
</div>
<DialogFooter className="flex justify-end space-x-2">
<Button
variant="outline"
className="text-zinc-400 bg-zinc-900 border-zinc-800 hover:bg-zinc-800"
onClick={handleCancelConfirm}
>
No, cancel
</Button>
<Button
className="bg-white text-black hover:bg-white/90"
onClick={handleConfirmDeploy}
>
Yes, confirm
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
)
}
}

View File

@ -0,0 +1,42 @@
'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>
)
}

View File

@ -19,7 +19,8 @@ export {
export { ConfigureStep } from './configure-step'
export { ConnectStep } from './connect-step'
export { DeployStep } from './deploy-step'
export { OnboardingSidebar } from './OnboardingSidebar'
export { SuccessStep } from './success-step/success-step'
// Common components
export * from './common'

View File

@ -0,0 +1,97 @@
'use client'
import { useEffect, useState } from 'react'
import { useRouter, useParams } from 'next/navigation'
import { useTheme } from 'next-themes'
import { CheckCircle } from 'lucide-react'
import { Button } from '@workspace/ui/components/button'
import { useOnboarding } from '@/components/onboarding/useOnboarding'
import { toast } from 'sonner'
export function SuccessStep() {
const router = useRouter()
const params = useParams()
const { resolvedTheme } = useTheme()
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() : 'blogapp'
const deploymentUrl = formData.deploymentUrl || `https://${repoName}.laconic.deploy`
const projectId = formData.projectId || 'unknown-id'
// Function to copy URL to clipboard
// Handle "Visit Site" button
// Handle "View Project" button - navigates to project page
const handleViewProject = () => {
resetOnboarding() // Reset state for next time
router.push(`/projects/${providerParam}/ps/${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">
{/* Success icon */}
<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`}>
Successfully
</h2>
<p className="text-center text-zinc-500 mb-8">
Your auction was successfully created
</p>
{/* 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"}`}>
<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>
<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
className="w-full bg-white hover:bg-white/90 text-black flex items-center justify-center"
onClick={handleViewProject}
>
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>
</div>
</div>
</div>
)
}

View File

@ -1,42 +1,86 @@
'use client'
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
export type Step = 'connect' | 'configure' | 'deploy'
export type Step = 'connect' | 'configure' | 'deploy' | 'success'
export interface EnvironmentVariables {
key: string
value: string
}
export interface OnboardingFormData {
// Connect step
githubRepo?: string
// Configure step
deploymentType?: 'auction' | 'lrn'
deployerCount?: string
maxPrice?: string
selectedLrn?: string
environments?: {
production: boolean
preview: boolean
development: boolean
}
environmentVariables?: Record<string, string>
// Deploy step
deploymentId?: string
deploymentUrl?: string
// Success step
projectId?: string
}
interface OnboardingState {
currentStep: Step
formData: {
githubRepo?: string
deploymentType?: string
environmentVars?: Record<string, string>
}
formData: OnboardingFormData
setCurrentStep: (step: Step) => void
setFormData: (data: Partial<OnboardingState['formData']>) => void
setFormData: (data: Partial<OnboardingFormData>) => void
nextStep: () => void
previousStep: () => void
resetOnboarding: () => void
}
const STEP_ORDER: Step[] = ['connect', 'configure', 'deploy']
const STEP_ORDER: Step[] = ['connect', 'configure', 'deploy', 'success']
export const useOnboarding = create<OnboardingState>((set) => ({
currentStep: 'connect',
formData: {},
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: {},
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
}),
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: {}
})
}),
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'
}
)
)