New design v0.2
New design and framework
This commit is contained in:
232
client/src/pages/DeployPage.tsx
Normal file
232
client/src/pages/DeployPage.tsx
Normal file
@@ -0,0 +1,232 @@
|
||||
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-gradient-to-b from-black to-transparent" />
|
||||
</div>
|
||||
|
||||
<LanguageSelector currentLang={lang} onLangChange={setLang} />
|
||||
|
||||
<div className="relative z-10 w-full max-w-[640px] p-4 md:p-8">
|
||||
<header className="text-center mb-12">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user