Files
ai-stack-deployer/client/src/pages/DeployPage.tsx
Oussama Douhou dd41bb5a6a Margin top mobile
2026-01-13 16:33:04 +01:00

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>
);
}