454 lines
18 KiB
TypeScript
454 lines
18 KiB
TypeScript
// src/components/onboarding/connect-step/connect-step.tsx
|
|
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { Github, Wallet, CheckCircle2, AlertTriangle, Loader2, ExternalLink, ChevronDown } from 'lucide-react'
|
|
import { useTheme } from 'next-themes'
|
|
import { SignIn } from '@clerk/nextjs'
|
|
import { useOnboarding } from '@/components/onboarding/useOnboarding'
|
|
import { useAuthStatus } from '@/hooks/useAuthStatus'
|
|
import { useRepoData } from '@/hooks/useRepoData'
|
|
import { Button } from '@workspace/ui/components/button'
|
|
import { Card, CardContent } from '@workspace/ui/components/card'
|
|
import { Input } from '@workspace/ui/components/input'
|
|
import { Label } from '@workspace/ui/components/label'
|
|
import { Alert, AlertDescription } from '@workspace/ui/components/alert'
|
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@workspace/ui/components/collapsible'
|
|
import { toast } from 'sonner'
|
|
import { GitHubBackendAuth } from '@/components/GitHubBackendAuth'
|
|
import { AVAILABLE_TEMPLATES, type TemplateDetail } from '@/constants/templates'
|
|
|
|
interface Repository {
|
|
id: string | number
|
|
full_name: string
|
|
html_url?: string
|
|
description?: string
|
|
}
|
|
|
|
export function ConnectStep() {
|
|
const { nextStep, setFormData, formData } = useOnboarding()
|
|
const { resolvedTheme } = useTheme()
|
|
const [mounted, setMounted] = useState(false)
|
|
|
|
// Repository vs Template selection
|
|
const [selectedRepo, setSelectedRepo] = useState<string>(formData.githubRepo || '')
|
|
const [selectedTemplate, setSelectedTemplate] = useState<TemplateDetail | undefined>(
|
|
formData.template || undefined
|
|
)
|
|
const [projectName, setProjectName] = useState<string>(formData.projectName || '')
|
|
const [isImportMode, setIsImportMode] = useState(true)
|
|
|
|
// Auth status and warning display
|
|
const [showAuthWarning, setShowAuthWarning] = useState(false)
|
|
|
|
// Auth status hook
|
|
const {
|
|
clerk,
|
|
wallet,
|
|
backend,
|
|
isFullyAuthenticated,
|
|
isReady,
|
|
missing,
|
|
progress,
|
|
connectWallet,
|
|
checkGithubBackendAuth
|
|
} = useAuthStatus()
|
|
|
|
// Repository data
|
|
const { repoData: repositories, isLoading: isLoadingRepos } = useRepoData('')
|
|
|
|
// Handle hydration mismatch by waiting for mount
|
|
useEffect(() => {
|
|
setMounted(true)
|
|
}, [])
|
|
|
|
// Auto-hide auth warning when fully authenticated
|
|
useEffect(() => {
|
|
if (isFullyAuthenticated) {
|
|
setShowAuthWarning(false)
|
|
}
|
|
}, [isFullyAuthenticated])
|
|
|
|
// Handle repository selection
|
|
const handleRepoSelect = (repo: string) => {
|
|
setSelectedRepo(repo)
|
|
setSelectedTemplate(undefined)
|
|
setFormData({
|
|
githubRepo: repo,
|
|
template: undefined,
|
|
deploymentMode: 'repository',
|
|
projectName
|
|
})
|
|
}
|
|
|
|
// Handle template selection
|
|
const handleTemplateSelect = (template: TemplateDetail) => {
|
|
setSelectedTemplate(template)
|
|
setSelectedRepo('')
|
|
// Auto-fill project name if empty
|
|
if (!projectName) {
|
|
const suggestedName = `my-${template.name.toLowerCase().replace(/[^a-z0-9]/g, '-')}`
|
|
setProjectName(suggestedName)
|
|
}
|
|
setFormData({
|
|
template: template,
|
|
githubRepo: '',
|
|
deploymentMode: 'template',
|
|
projectName: projectName || `my-${template.name.toLowerCase().replace(/[^a-z0-9]/g, '-')}`
|
|
})
|
|
}
|
|
|
|
// Handle mode toggle between import and template
|
|
const toggleMode = (mode: 'import' | 'template') => {
|
|
setIsImportMode(mode === 'import')
|
|
// Clear selections when switching modes
|
|
if (mode === 'import') {
|
|
setSelectedTemplate(undefined)
|
|
setFormData({
|
|
template: undefined,
|
|
deploymentMode: 'repository',
|
|
projectName
|
|
})
|
|
} else {
|
|
setSelectedRepo('')
|
|
setFormData({
|
|
githubRepo: '',
|
|
deploymentMode: 'template',
|
|
projectName
|
|
})
|
|
}
|
|
}
|
|
|
|
// Handle project name change
|
|
const handleProjectNameChange = (value: string) => {
|
|
setProjectName(value)
|
|
setFormData({ projectName: value })
|
|
}
|
|
|
|
// Handle wallet connection
|
|
const handleConnectWallet = async () => {
|
|
try {
|
|
await connectWallet()
|
|
toast.success('Wallet connected successfully')
|
|
} catch (error) {
|
|
console.error('Wallet connection failed:', error)
|
|
toast.error('Failed to connect wallet')
|
|
}
|
|
}
|
|
|
|
// Handle GitHub backend auth status change
|
|
const handleGithubAuthChange = async (isAuthenticated: boolean) => {
|
|
await checkGithubBackendAuth()
|
|
if (isAuthenticated) {
|
|
toast.success('GitHub backend authentication completed!')
|
|
}
|
|
}
|
|
|
|
// Handle next step
|
|
const handleNext = () => {
|
|
if (!isFullyAuthenticated) {
|
|
toast.error('Please complete all authentication steps first')
|
|
setShowAuthWarning(true)
|
|
return
|
|
}
|
|
|
|
if (isImportMode && !selectedRepo) {
|
|
toast.error('Please select a repository to continue')
|
|
return
|
|
}
|
|
|
|
if (!isImportMode && (!selectedTemplate || !projectName.trim())) {
|
|
toast.error('Please select a template and enter a project name')
|
|
return
|
|
}
|
|
|
|
// For repository import, project name is optional but we'll use repo name as fallback
|
|
const finalProjectName = projectName.trim() || (isImportMode ? selectedRepo.split('/')[1] : '')
|
|
|
|
// Set final form data and proceed
|
|
setFormData({
|
|
deploymentMode: isImportMode ? 'repository' : 'template',
|
|
githubRepo: isImportMode ? selectedRepo : '',
|
|
template: !isImportMode ? selectedTemplate : undefined,
|
|
projectName: finalProjectName
|
|
})
|
|
|
|
nextStep()
|
|
}
|
|
|
|
// Don't render UI until after mount to prevent hydration mismatch
|
|
if (!mounted || !isReady) {
|
|
return (
|
|
<div className="w-full h-full flex items-center justify-center">
|
|
<div className="text-center">
|
|
<Loader2 className="h-8 w-8 animate-spin mx-auto mb-4" />
|
|
<p className="text-sm text-zinc-500">Loading authentication status...</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Determine if dark mode is active
|
|
const isDarkMode = resolvedTheme === 'dark'
|
|
|
|
return (
|
|
<div className="w-full h-full flex flex-col p-8 overflow-y-auto">
|
|
<div className="max-w-2xl w-full mx-auto">
|
|
{/* Header */}
|
|
<div className="text-center mb-8">
|
|
<h2 className={`text-2xl font-medium ${isDarkMode ? "text-white" : "text-zinc-900"} mb-2`}>
|
|
Connect
|
|
</h2>
|
|
<p className="text-zinc-500 mb-6">
|
|
Connect and import a GitHub repo or start from a template
|
|
</p>
|
|
|
|
{/* GitHub Account Selector - Only show if multiple accounts */}
|
|
{clerk.user?.externalAccounts && clerk.user.externalAccounts.length > 1 && (
|
|
<div className="flex items-center justify-center mb-6">
|
|
<div className="flex items-center gap-2 px-4 py-2 bg-zinc-100 dark:bg-zinc-800 rounded-md cursor-pointer hover:bg-zinc-200 dark:hover:bg-zinc-700">
|
|
<Github className="h-4 w-4" />
|
|
<span className="text-sm font-medium">
|
|
{clerk.user?.externalAccounts?.find(acc => acc.provider === 'github')?.username || 'git-account'}
|
|
</span>
|
|
<ChevronDown className="h-4 w-4" />
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Authentication Warning - Only show if not fully authenticated */}
|
|
{!isFullyAuthenticated && (
|
|
<Collapsible open={showAuthWarning} onOpenChange={setShowAuthWarning}>
|
|
<CollapsibleTrigger asChild>
|
|
<Alert className="mb-6 cursor-pointer hover:bg-amber-50 dark:hover:bg-amber-950/20">
|
|
<AlertTriangle className="h-4 w-4" />
|
|
<AlertDescription className="flex items-center justify-between w-full">
|
|
<span>Authentication required to continue ({progress.completed}/{progress.total} complete)</span>
|
|
<ChevronDown className="h-4 w-4" />
|
|
</AlertDescription>
|
|
</Alert>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent className="space-y-4 mb-6">
|
|
{/* Authentication steps - same as before but in collapsible */}
|
|
{missing.clerkSignIn && (
|
|
<Card className="border-amber-200 bg-amber-50/50 dark:bg-amber-950/20">
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center gap-3 mb-3">
|
|
<Github className="h-4 w-4" />
|
|
<span className="text-sm font-medium">Sign in with Clerk</span>
|
|
</div>
|
|
<div className="scale-90 origin-top-left">
|
|
<SignIn routing="hash" />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{missing.clerkGithub && !missing.clerkSignIn && (
|
|
<Card className="border-amber-200 bg-amber-50/50 dark:bg-amber-950/20">
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center gap-3 mb-3">
|
|
<Github className="h-4 w-4" />
|
|
<span className="text-sm font-medium">Connect GitHub Account</span>
|
|
</div>
|
|
<Button size="sm" variant="outline" onClick={() => window.open('/user-profile', '_blank')}>
|
|
<ExternalLink className="h-3 w-3 mr-2" />
|
|
Connect GitHub
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{missing.walletConnection && (
|
|
<Card className="border-amber-200 bg-amber-50/50 dark:bg-amber-950/20">
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center gap-3 mb-3">
|
|
<Wallet className="h-4 w-4" />
|
|
<span className="text-sm font-medium">Connect Wallet</span>
|
|
</div>
|
|
<Button size="sm" onClick={handleConnectWallet}>
|
|
Connect Wallet
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{missing.githubBackendSync && !missing.walletConnection && !missing.clerkGithub && (
|
|
<Card className="border-amber-200 bg-amber-50/50 dark:bg-amber-950/20">
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center gap-3 mb-3">
|
|
<Github className="h-4 w-4" />
|
|
<span className="text-sm font-medium">Sync GitHub Access</span>
|
|
</div>
|
|
<GitHubBackendAuth onAuthStatusChange={handleGithubAuthChange} />
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
)}
|
|
|
|
{/* Mode Selection Tabs */}
|
|
<div className="grid grid-cols-2 gap-1 p-1 bg-zinc-100 dark:bg-zinc-800 rounded-lg mb-6">
|
|
<Button
|
|
variant={isImportMode ? "default" : "ghost"}
|
|
className={`${isImportMode
|
|
? 'bg-white dark:bg-zinc-700 shadow-sm'
|
|
: 'bg-transparent hover:bg-white/50 dark:hover:bg-zinc-700/50'
|
|
}`}
|
|
onClick={() => toggleMode('import')}
|
|
>
|
|
Import a repository
|
|
</Button>
|
|
<Button
|
|
variant={!isImportMode ? "default" : "ghost"}
|
|
className={`${!isImportMode
|
|
? 'bg-white dark:bg-zinc-700 shadow-sm'
|
|
: 'bg-transparent hover:bg-white/50 dark:hover:bg-zinc-700/50'
|
|
}`}
|
|
onClick={() => toggleMode('template')}
|
|
>
|
|
Start with a template
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Content Area */}
|
|
{isImportMode ? (
|
|
/* Repository Selection */
|
|
<div className="space-y-4">
|
|
{isLoadingRepos ? (
|
|
<div className="p-8 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-8 text-center text-zinc-500">
|
|
<Alert>
|
|
<AlertTriangle className="h-4 w-4" />
|
|
<AlertDescription>
|
|
No repositories found. Make sure your GitHub account has repositories.
|
|
</AlertDescription>
|
|
</Alert>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="space-y-2 max-h-60 overflow-y-auto">
|
|
{repositories.map((repo: Repository) => (
|
|
<div
|
|
key={repo.id}
|
|
className={`flex items-center p-4 rounded-lg border cursor-pointer transition-all ${
|
|
selectedRepo === repo.full_name
|
|
? 'border-blue-500 bg-blue-50 dark:bg-blue-950/20'
|
|
: 'border-zinc-200 dark:border-zinc-700 hover:border-zinc-300 dark:hover:border-zinc-600'
|
|
}`}
|
|
onClick={() => handleRepoSelect(repo.full_name)}
|
|
>
|
|
<Github className="h-5 w-5 mr-3 text-zinc-500 flex-shrink-0" />
|
|
<div className="flex-1 min-w-0">
|
|
<div className="font-medium text-sm">{repo.full_name}</div>
|
|
{repo.description && (
|
|
<div className="text-xs text-zinc-500 truncate">{repo.description}</div>
|
|
)}
|
|
</div>
|
|
{selectedRepo === repo.full_name && (
|
|
<CheckCircle2 className="h-5 w-5 text-blue-500 flex-shrink-0" />
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Project Name Input for Repository Import */}
|
|
{selectedRepo && (
|
|
<div className="mt-6 space-y-2">
|
|
<Label htmlFor="projectName" className="text-sm font-medium">
|
|
Project Name
|
|
</Label>
|
|
<Input
|
|
id="projectName"
|
|
value={projectName}
|
|
onChange={(e) => handleProjectNameChange(e.target.value)}
|
|
placeholder="my-project-name"
|
|
className="w-full"
|
|
/>
|
|
<p className="text-xs text-zinc-500">
|
|
This will be the name of your deployment project
|
|
</p>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
) : (
|
|
/* Template Selection */
|
|
<div className="space-y-4">
|
|
{AVAILABLE_TEMPLATES.filter(t => !t.isComingSoon).map((template) => (
|
|
<div
|
|
key={template.id}
|
|
className={`flex items-center p-4 rounded-lg border cursor-pointer transition-all ${
|
|
selectedTemplate?.id === template.id
|
|
? 'border-blue-500 bg-blue-50 dark:bg-blue-950/20'
|
|
: 'border-zinc-200 dark:border-zinc-700 hover:border-zinc-300 dark:hover:border-zinc-600'
|
|
}`}
|
|
onClick={() => handleTemplateSelect(template)}
|
|
>
|
|
{/* Template Icon */}
|
|
<div className="flex items-center justify-center w-10 h-10 rounded-lg bg-zinc-100 dark:bg-zinc-800 mr-4">
|
|
<div className="w-6 h-6 bg-zinc-600 dark:bg-zinc-400 rounded flex items-center justify-center text-xs font-bold text-white">
|
|
{template.icon === 'web' ? 'PWA' : template.icon === 'nextjs' ? 'N' : 'IMG'}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Template Info */}
|
|
<div className="flex-1 min-w-0">
|
|
<div className="font-medium text-sm mb-1">{template.name}</div>
|
|
<div className="flex items-center text-xs text-zinc-500">
|
|
<Github className="h-3 w-3 mr-1" />
|
|
{template.repoFullName}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Selection Indicator */}
|
|
{selectedTemplate?.id === template.id && (
|
|
<CheckCircle2 className="h-5 w-5 text-blue-500 flex-shrink-0" />
|
|
)}
|
|
</div>
|
|
))}
|
|
|
|
{/* Project Name Input for Templates */}
|
|
{selectedTemplate && (
|
|
<div className="mt-6 space-y-2">
|
|
<Label htmlFor="projectName" className="text-sm font-medium">
|
|
Project Name
|
|
</Label>
|
|
<Input
|
|
id="projectName"
|
|
value={projectName}
|
|
onChange={(e) => handleProjectNameChange(e.target.value)}
|
|
placeholder="new-repository-name"
|
|
className="w-full"
|
|
/>
|
|
<p className="text-xs text-zinc-500">
|
|
This will be the name of your new GitHub repository
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Navigation */}
|
|
<div className="flex justify-between items-center mt-8">
|
|
<Button variant="outline" disabled>
|
|
Previous
|
|
</Button>
|
|
<Button
|
|
onClick={handleNext}
|
|
disabled={!isFullyAuthenticated || (isImportMode ? !selectedRepo : (!selectedTemplate || !projectName.trim()))}
|
|
>
|
|
Next
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
} |