Added onboarding modal
This commit is contained in:
parent
69b8cf1395
commit
29c5f6f931
@ -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
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
@ -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>
|
||||
)
|
||||
}
|
@ -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"
|
||||
|
@ -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
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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'
|
||||
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
@ -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'
|
||||
}
|
||||
)
|
||||
)
|
Loading…
Reference in New Issue
Block a user