castad-pre-v0.3/castad-data/components/LoadingOverlay.tsx

134 lines
4.9 KiB
TypeScript

import React from 'react';
import { useLanguage } from '../src/contexts/LanguageContext';
import { cn } from '../src/lib/utils';
import { Card, CardContent } from '../src/components/ui/card';
import { Progress } from '../src/components/ui/progress';
import { Loader2, Sparkles, Music, Image, Video, CheckCircle } from 'lucide-react';
interface LoadingOverlayProps {
status: string;
message: string;
progress?: number;
}
const STEP_ICONS: Record<string, React.ReactNode> = {
'crawling': <Sparkles className="w-6 h-6" />,
'generating_text': <Sparkles className="w-6 h-6" />,
'generating_audio': <Music className="w-6 h-6" />,
'generating_poster': <Image className="w-6 h-6" />,
'generating_video': <Video className="w-6 h-6" />,
'completed': <CheckCircle className="w-6 h-6 text-green-500" />,
};
const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ status, message, progress }) => {
const { t } = useLanguage();
let displayMessage = message;
switch (status) {
case 'crawling':
case 'generating_text':
displayMessage = t('loadingStep1');
break;
case 'generating_audio':
displayMessage = t('loadingStep2').replace('{style}', 'Auto');
break;
case 'generating_poster':
case 'generating_video':
displayMessage = t('loadingStep3');
break;
case 'completed':
displayMessage = t('loadingStep4');
break;
default:
break;
}
const currentProgress = progress || (
status === 'crawling' ? 10 :
status === 'generating_text' ? 25 :
status === 'generating_audio' ? 50 :
status === 'generating_poster' ? 70 :
status === 'generating_video' ? 85 :
status === 'completed' ? 100 : 10
);
const currentIcon = STEP_ICONS[status] || <Sparkles className="w-6 h-6" />;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/95 backdrop-blur-sm">
<Card className="w-full max-w-md mx-4 border-border/50 shadow-2xl">
<CardContent className="pt-8 pb-8 text-center">
{/* Animated Icon */}
<div className="relative w-24 h-24 mx-auto mb-8">
{/* Outer ring */}
<div className="absolute inset-0 rounded-full border-4 border-primary/20 animate-ping" style={{ animationDuration: '2s' }} />
{/* Spinning ring */}
<div className="absolute inset-0 rounded-full border-4 border-transparent border-t-primary border-r-accent animate-spin" style={{ animationDuration: '1.5s' }} />
{/* Center icon */}
<div className="absolute inset-3 rounded-full bg-gradient-to-br from-primary to-accent flex items-center justify-center shadow-lg">
<div className="text-primary-foreground animate-pulse">
{currentIcon}
</div>
</div>
</div>
<h2 className="text-2xl font-bold text-foreground mb-2">
{t('loadingTitle')}
</h2>
<p className="text-muted-foreground text-sm mb-6">
{t('loadingSubtitle')}
</p>
{/* Progress Bar */}
<div className="mb-6">
<Progress value={currentProgress} className="h-2" />
<div className="flex items-center justify-between mt-2 text-xs text-muted-foreground">
<span>{t('textStyle') === '자막 스타일' ? '진행률' : 'Progress'}</span>
<span className="font-medium">{currentProgress}%</span>
</div>
</div>
{/* Status Message */}
<div className="flex items-center justify-center gap-2 mb-4">
<Loader2 className="w-4 h-4 animate-spin text-primary" />
<p className="text-lg font-semibold gradient-text">
{displayMessage}
</p>
</div>
{/* Steps indicator */}
<div className="flex items-center justify-center gap-2 mb-6">
{['generating_text', 'generating_audio', 'generating_poster', 'completed'].map((step, idx) => {
const isActive = status === step;
const isPast = ['generating_text', 'generating_audio', 'generating_poster', 'generating_video', 'completed'].indexOf(status) >
['generating_text', 'generating_audio', 'generating_poster', 'completed'].indexOf(step);
return (
<div
key={step}
className={cn(
"w-2 h-2 rounded-full transition-all",
isActive && "w-6 bg-primary",
isPast && "bg-primary/60",
!isActive && !isPast && "bg-muted-foreground/30"
)}
/>
);
})}
</div>
<p className="text-xs text-muted-foreground">
{t('loadingWait')}
</p>
</CardContent>
</Card>
</div>
);
};
export default LoadingOverlay;