laconic-deployer-frontend/apps/deploy-fe/src/components/onboarding/deploy-step/deploy-step.tsx
2025-08-21 19:31:07 +00:00

518 lines
17 KiB
TypeScript

// src/components/onboarding/deploy-step/deploy-step.tsx
'use client'
import { useOnboarding } from '@/components/onboarding/useOnboarding'
import { useWallet } from '@/context/WalletContext'
import { useDeployment } from '@/hooks/useDeployment'
import { useTemplateDeployment } from '@/hooks/useTemplate'
import { Alert, AlertDescription } from '@workspace/ui/components/alert'
import { Badge } from '@workspace/ui/components/badge'
import { Button } from '@workspace/ui/components/button'
import {
Card,
CardContent,
CardHeader,
CardTitle
} from '@workspace/ui/components/card'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogTitle
} from '@workspace/ui/components/dialog'
import { Progress } from '@workspace/ui/components/progress'
import { AlertTriangle, CheckCircle2, Github, Loader2 } from 'lucide-react'
import { useTheme } from 'next-themes'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
export function DeployStep() {
const { previousStep, nextStep, formData, setFormData } = useOnboarding()
const { resolvedTheme } = useTheme()
const [mounted, setMounted] = useState(false)
// State
const [showConfirmDialog, setShowConfirmDialog] = useState(false)
const [deploymentError, setDeploymentError] = useState<string | null>(null)
const [deploymentSuccess, setDeploymentSuccess] = useState(false)
// Contexts and hooks
const { wallet } = useWallet()
const { deployRepository, isDeploying: isRepoDeploying } = useDeployment()
const { deployTemplate, isDeploying: isTemplateDeploying } =
useTemplateDeployment()
// Determine deployment type and get the right deploying state
const isTemplateMode = formData.deploymentMode === 'template'
const isDeploying = isTemplateMode ? isTemplateDeploying : isRepoDeploying
// Handle hydration mismatch by waiting for mount
useEffect(() => {
setMounted(true)
}, [])
// Get deployment info
const getDeploymentInfo = () => {
if (isTemplateMode) {
return {
name: formData.template?.name || 'Template Project',
source: formData.template?.repoFullName || 'Unknown Template',
projectName: formData.projectName || 'New Project',
type: 'Template'
}
} else {
return {
name: formData.githubRepo?.split('/').pop() || 'Repository',
source: formData.githubRepo || 'Unknown Repository',
projectName:
formData.projectName ||
formData.githubRepo?.split('/').pop() ||
'New Project',
type: 'Repository'
}
}
}
const deploymentInfo = getDeploymentInfo()
// Open the confirmation modal
const handlePayAndDeploy = () => {
if (!wallet?.address) {
toast.error('Wallet not connected')
return
}
if (!formData.selectedOrg) {
toast.error('No organization selected')
return
}
if (isTemplateMode && (!formData.template || !formData.projectName)) {
toast.error('Template or project name missing')
return
}
if (!isTemplateMode && !formData.githubRepo) {
toast.error('Repository not selected')
return
}
setShowConfirmDialog(true)
}
// Close the confirmation modal
const handleCancelConfirm = () => {
setShowConfirmDialog(false)
}
// Handle confirmed deployment
const handleConfirmDeploy = async () => {
setShowConfirmDialog(false)
setDeploymentError(null)
setDeploymentSuccess(false)
try {
if (isTemplateMode) {
await deployTemplateProject()
} else {
await deployRepositoryProject()
}
} catch (error) {
console.error('Deployment failed:', error)
setDeploymentError(
error instanceof Error ? error.message : 'Deployment failed'
)
}
}
// Deploy template project
const deployTemplateProject = async () => {
if (!formData.template || !formData.projectName || !formData.selectedOrg) {
throw new Error('Missing required template deployment data')
}
const config = {
template: {
...formData.template,
id: formData.template?.id || '',
icon: formData.template?.icon || ''
},
projectName: formData.projectName,
organizationSlug: formData.selectedOrg,
environmentVariables: formData.environmentVariables || [],
deployerLrn: formData.selectedLrn
}
const result = await deployTemplate(config)
// Save deployment results
setFormData({
deploymentId: result.deploymentId,
deploymentUrl: result.deploymentUrl,
projectId: result.projectId,
repositoryUrl: result.repositoryUrl
})
setDeploymentSuccess(true)
toast.success('Template deployed successfully!')
// Move to success step after short delay
setTimeout(() => {
nextStep()
}, 2000)
}
// Deploy repository project
const deployRepositoryProject = async () => {
if (!formData.githubRepo || !formData.selectedOrg) {
throw new Error('Missing required repository deployment data')
}
const config = {
projectId: '', // Will be generated by backend
organizationSlug: formData.selectedOrg,
repository: formData.githubRepo,
branch: 'main', // Default branch
name:
formData.projectName ||
formData.githubRepo.split('/').pop() ||
'New Project',
environmentVariables: formData.environmentVariables || []
}
console.log('Deploying repository with config:', config)
const result = await deployRepository(config)
// Save deployment results
setFormData({
deploymentId: result.id,
deploymentUrl: result.url,
projectId: result.id
})
setDeploymentSuccess(true)
toast.success('Repository deployed successfully!')
// Move to success step after short delay
setTimeout(() => {
nextStep()
}, 2000)
}
// 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">
{/* 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`}
>
{isDeploying ? 'Deploying...' : 'Deploy'}
</h2>
<p className="text-center text-zinc-500 mb-8">
{isDeploying
? 'Your project is being deployed. This may take a few minutes.'
: 'Review and confirm deployment'}
</p>
{/* Deployment Summary */}
<Card className="mb-6">
<CardHeader className="pb-3">
<CardTitle className="text-sm flex items-center justify-between">
Deployment Summary
<Badge variant="secondary">{deploymentInfo.type}</Badge>
</CardTitle>
</CardHeader>
<CardContent className="pt-0 space-y-3">
<div className="flex items-center gap-3">
<Github className="h-4 w-4 text-muted-foreground" />
<div className="flex-1 min-w-0">
<div className="font-medium text-sm">
{deploymentInfo.projectName}
</div>
<div className="text-xs text-muted-foreground font-mono">
{deploymentInfo.source}
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4 pt-2 text-xs">
<div>
<div className="text-muted-foreground">Organization</div>
<div className="font-medium">{formData.selectedOrg}</div>
</div>
<div>
<div className="text-muted-foreground">Deployer</div>
<div className="font-medium">
{formData.selectedLrn ? 'LRN' : 'Auction'}
</div>
</div>
</div>
{formData.environmentVariables &&
formData.environmentVariables.length > 0 && (
<div className="pt-2">
<div className="text-xs text-muted-foreground mb-1">
Environment Variables
</div>
<div className="text-xs">
{formData.environmentVariables.length} variable
{formData.environmentVariables.length !== 1 ? 's' : ''}{' '}
configured
</div>
</div>
)}
</CardContent>
</Card>
{/* Error Display */}
{deploymentError && (
<Alert className="mb-6" variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
<div className="font-medium">Deployment Failed</div>
<div className="text-sm mt-1">{deploymentError}</div>
</AlertDescription>
</Alert>
)}
{/* Success Display */}
{deploymentSuccess && (
<Alert className="mb-6">
<CheckCircle2 className="h-4 w-4" />
<AlertDescription>
<div className="font-medium">Deployment Successful!</div>
<div className="text-sm mt-1">
Your project has been deployed successfully. You'll be
redirected to the project dashboard.
</div>
</AlertDescription>
</Alert>
)}
{/* Deployment Progress - Only show while deploying */}
{isDeploying && (
<div className="mb-8">
<div className="flex justify-between items-center mb-2">
<div
className={`${isDarkMode ? 'text-white' : 'text-zinc-900'} text-sm`}
>
{isTemplateMode
? 'Creating repository from template...'
: 'Deploying repository...'}
</div>
</div>
<Progress
value={undefined}
className={`h-2 ${isDarkMode ? 'bg-zinc-800' : 'bg-zinc-200'}`}
/>
<div className="text-xs text-muted-foreground mt-2">
This process may take several minutes. Please do not close this
window.
</div>
</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 || deploymentSuccess}
>
Previous
</Button>
{deploymentSuccess ? (
<Button
className="bg-green-600 hover:bg-green-700 text-white"
onClick={nextStep}
>
<CheckCircle2 className="mr-2 h-4 w-4" />
Continue
</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}
disabled={deploymentError !== null}
>
{formData.deploymentType === 'auction'
? 'Pay and Deploy'
: '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>
{/* Transaction Confirmation Dialog */}
<Dialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
<DialogContent className="bg-background border max-w-md">
<DialogTitle>Confirm Deployment</DialogTitle>
<DialogDescription>
Review the deployment details before proceeding.
</DialogDescription>
<div className="space-y-4 py-4">
{/* Project Info */}
<div className="space-y-2">
<h3 className="text-sm font-medium">Project Details</h3>
<div className="space-y-1 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Name:</span>
<span>{deploymentInfo.projectName}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Type:</span>
<span>{deploymentInfo.type}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Source:</span>
<span className="font-mono text-xs">
{deploymentInfo.source}
</span>
</div>
</div>
</div>
{/* Wallet Info */}
<div className="space-y-2">
<h3 className="text-sm font-medium">Payment Address</h3>
<div className="p-2 bg-muted rounded text-xs font-mono break-all">
{wallet?.address}
</div>
</div>
{/* Deployer Info */}
{formData.selectedLrn && (
<div className="space-y-2">
<h3 className="text-sm font-medium">Deployer</h3>
<div className="text-sm">
<div className="font-mono text-xs">
{formData.selectedLrn}
</div>
</div>
</div>
)}
{/* Cost Info */}
{formData.deploymentType === 'auction' && (
<div className="space-y-2">
<h3 className="text-sm font-medium">Auction Details</h3>
<div className="space-y-1 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Max Price:</span>
<span>{formData.maxPrice} aint</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Deployers:</span>
<span>{formData.deployerCount}</span>
</div>
</div>
</div>
)}
</div>
<DialogFooter className="flex justify-end space-x-2">
<Button variant="outline" onClick={handleCancelConfirm}>
Cancel
</Button>
<Button onClick={handleConfirmDeploy}>Confirm Deployment</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
)
}