233 lines
7.5 KiB
TypeScript
233 lines
7.5 KiB
TypeScript
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
import { motion, AnimatePresence } from "framer-motion";
|
|
import { useI18n } from '@/hooks/useI18n';
|
|
import { LanguageSelector } from '@/components/deploy/LanguageSelector';
|
|
import { DeployForm } from '@/components/deploy/DeployForm';
|
|
import { DeployProgress } from '@/components/deploy/DeployProgress';
|
|
import { DeploySuccess } from '@/components/deploy/DeploySuccess';
|
|
import { DeployError } from '@/components/deploy/DeployError';
|
|
import { CanvasRevealEffect } from '@/components/ui/sign-in-flow-1';
|
|
|
|
type DeployState = 'form' | 'progress' | 'success' | 'error';
|
|
|
|
interface ProgressData {
|
|
progress: number;
|
|
message: string;
|
|
}
|
|
|
|
export default function DeployPage() {
|
|
const { lang, setLang, t } = useI18n();
|
|
const [state, setState] = useState<DeployState>('form');
|
|
const [stackName, setStackName] = useState('');
|
|
const [deploymentUrl, setDeploymentUrl] = useState('');
|
|
const [progressData, setProgressData] = useState<ProgressData>({ progress: 0, message: '' });
|
|
const [logs, setLogs] = useState<string[]>([]);
|
|
const [errorMessage, setErrorMessage] = useState('');
|
|
const eventSourceRef = useRef<EventSource | null>(null);
|
|
|
|
const handleDeploy = useCallback(async (name: string) => {
|
|
setStackName(name);
|
|
setProgressData({ progress: 0, message: t('initializing') });
|
|
setLogs([]);
|
|
|
|
try {
|
|
const response = await fetch('/api/deploy', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name }),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok || !data.success) {
|
|
throw new Error(data.error || 'Deployment failed');
|
|
}
|
|
|
|
setDeploymentUrl(data.url);
|
|
setState('progress');
|
|
|
|
const es = new EventSource(`/api/status/${data.deploymentId}`);
|
|
eventSourceRef.current = es;
|
|
|
|
es.addEventListener('progress', (event) => {
|
|
const eventData = JSON.parse(event.data);
|
|
setProgressData({
|
|
progress: eventData.progress,
|
|
message: eventData.currentStep || eventData.message,
|
|
});
|
|
setLogs((prev) => [
|
|
...prev,
|
|
`[${new Date().toLocaleTimeString()}] ${eventData.currentStep || eventData.message}`,
|
|
]);
|
|
});
|
|
|
|
es.addEventListener('complete', () => {
|
|
es.close();
|
|
setState('success');
|
|
});
|
|
|
|
es.addEventListener('error', (event) => {
|
|
const eventData = (event as MessageEvent).data
|
|
? JSON.parse((event as MessageEvent).data)
|
|
: { message: 'Unknown error' };
|
|
es.close();
|
|
setErrorMessage(eventData.message);
|
|
setState('error');
|
|
});
|
|
|
|
es.onerror = () => {
|
|
es.close();
|
|
setErrorMessage(t('connectionLost'));
|
|
setState('error');
|
|
};
|
|
} catch (err) {
|
|
setErrorMessage(err instanceof Error ? err.message : 'Deployment failed');
|
|
setState('error');
|
|
}
|
|
}, [t]);
|
|
|
|
const handleReset = useCallback(() => {
|
|
if (eventSourceRef.current) {
|
|
eventSourceRef.current.close();
|
|
eventSourceRef.current = null;
|
|
}
|
|
setStackName('');
|
|
setDeploymentUrl('');
|
|
setProgressData({ progress: 0, message: '' });
|
|
setLogs([]);
|
|
setErrorMessage('');
|
|
setState('form');
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
if (eventSourceRef.current) {
|
|
eventSourceRef.current.close();
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<div className="min-h-screen bg-black relative flex flex-col items-center justify-center font-mono overflow-hidden">
|
|
<div className="absolute inset-0 z-0">
|
|
<div className="absolute inset-0">
|
|
<CanvasRevealEffect
|
|
animationSpeed={3}
|
|
containerClassName="bg-black"
|
|
colors={[[255, 255, 255], [255, 255, 255]]}
|
|
dotSize={2}
|
|
/>
|
|
</div>
|
|
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(0,0,0,1)_0%,transparent_100%)]" />
|
|
<div className="absolute top-0 left-0 right-0 h-1/3 bg-linear-to-b from-black to-transparent" />
|
|
</div>
|
|
|
|
<LanguageSelector currentLang={lang} onLangChange={setLang} />
|
|
|
|
<div className="relative z-10 w-full max-w-w160 p-4 md:p-8">
|
|
<header className="text-center mb-12 mt-25 md:mt-0">
|
|
<motion.h1
|
|
initial={{ opacity: 0, y: -20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5, delay: 0.1 }}
|
|
className="text-3xl md:text-4xl font-bold mb-4 text-white tracking-tight"
|
|
>
|
|
{t('title')}
|
|
</motion.h1>
|
|
<motion.p
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ duration: 0.5, delay: 0.2 }}
|
|
className="text-gray-400"
|
|
>
|
|
{t('subtitle')}
|
|
</motion.p>
|
|
</header>
|
|
|
|
<main className="w-full">
|
|
<AnimatePresence mode="wait">
|
|
{state === 'form' && (
|
|
<motion.div
|
|
key="form"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -20 }}
|
|
transition={{ duration: 0.3 }}
|
|
>
|
|
<DeployForm t={t} onDeploy={handleDeploy} isDeploying={false} />
|
|
</motion.div>
|
|
)}
|
|
|
|
{state === 'progress' && (
|
|
<motion.div
|
|
key="progress"
|
|
initial={{ opacity: 0, scale: 0.95 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
exit={{ opacity: 0, scale: 0.95 }}
|
|
transition={{ duration: 0.3 }}
|
|
>
|
|
<DeployProgress
|
|
t={t}
|
|
stackName={stackName}
|
|
deploymentUrl={deploymentUrl}
|
|
progressData={progressData}
|
|
logs={logs}
|
|
/>
|
|
</motion.div>
|
|
)}
|
|
|
|
{state === 'success' && (
|
|
<motion.div
|
|
key="success"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -20 }}
|
|
transition={{ duration: 0.3 }}
|
|
>
|
|
<DeploySuccess
|
|
t={t}
|
|
stackName={stackName}
|
|
deploymentUrl={deploymentUrl}
|
|
onDeployAnother={handleReset}
|
|
/>
|
|
</motion.div>
|
|
)}
|
|
|
|
{state === 'error' && (
|
|
<motion.div
|
|
key="error"
|
|
initial={{ opacity: 0, x: 20 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
exit={{ opacity: 0, x: -20 }}
|
|
transition={{ duration: 0.3 }}
|
|
>
|
|
<DeployError t={t} errorMessage={errorMessage} onTryAgain={handleReset} />
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</main>
|
|
|
|
<motion.footer
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ duration: 0.5, delay: 0.5 }}
|
|
className="text-center mt-12 pt-8 text-sm text-gray-600 border-t border-white/5"
|
|
>
|
|
<p>
|
|
{t('poweredBy')}{' '}
|
|
<a
|
|
href="https://flexinit.nl"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-white/60 font-semibold hover:text-white hover:underline transition-colors"
|
|
>
|
|
FLEXINIT
|
|
</a>{' '}
|
|
• FlexAI Assistant
|
|
</p>
|
|
</motion.footer>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|