185 lines
6.9 KiB
TypeScript
185 lines
6.9 KiB
TypeScript
import { useState, useEffect, useRef } from 'react';
|
|
import { useNavigate, useLocation } from 'react-router';
|
|
import { motion } from 'motion/react';
|
|
import { Check, AlertCircle } from 'lucide-react';
|
|
import { generateMarketingReport } from '../lib/supabase';
|
|
|
|
const steps = [
|
|
{ label: 'Scanning website...', key: 'scrape' },
|
|
{ label: 'Analyzing social media presence...', key: 'social' },
|
|
{ label: 'Researching competitors & keywords...', key: 'analyze' },
|
|
{ label: 'Generating AI marketing report...', key: 'generate' },
|
|
{ label: 'Finalizing report...', key: 'finalize' },
|
|
];
|
|
|
|
export default function AnalysisLoadingPage() {
|
|
const [currentStep, setCurrentStep] = useState(0);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const url = (location.state as { url?: string })?.url;
|
|
const hasStarted = useRef(false);
|
|
|
|
useEffect(() => {
|
|
if (hasStarted.current) return;
|
|
hasStarted.current = true;
|
|
|
|
if (!url) {
|
|
navigate('/', { replace: true });
|
|
return;
|
|
}
|
|
|
|
const runAnalysis = async () => {
|
|
try {
|
|
// Step 1: Scraping
|
|
setCurrentStep(1);
|
|
|
|
// Simulate step progression while waiting for API
|
|
const stepTimer = setInterval(() => {
|
|
setCurrentStep((prev) => (prev < steps.length - 1 ? prev + 1 : prev));
|
|
}, 5000);
|
|
|
|
const result = await generateMarketingReport(url);
|
|
|
|
clearInterval(stepTimer);
|
|
setCurrentStep(steps.length);
|
|
|
|
if (result.success && result.report) {
|
|
const reportPath = result.reportId
|
|
? `/report/${result.reportId}`
|
|
: '/report/live';
|
|
|
|
// Brief delay to show completion
|
|
setTimeout(() => {
|
|
navigate(reportPath, {
|
|
replace: true,
|
|
state: {
|
|
report: result.report,
|
|
metadata: result.metadata,
|
|
reportId: result.reportId,
|
|
},
|
|
});
|
|
}, 800);
|
|
} else {
|
|
throw new Error(result.error || 'Report generation failed');
|
|
}
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'An error occurred');
|
|
}
|
|
};
|
|
|
|
runAnalysis();
|
|
}, [url, navigate]);
|
|
|
|
return (
|
|
<div className="relative min-h-screen bg-primary-900 flex flex-col items-center justify-center px-6 overflow-hidden">
|
|
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_rgba(79,29,161,0.25)_0%,_transparent_70%)]" />
|
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] h-[500px] bg-purple-600/20 rounded-full blur-[120px] pointer-events-none" />
|
|
|
|
<div className="relative z-10 flex flex-col items-center w-full max-w-lg">
|
|
<motion.h1
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.6 }}
|
|
className="text-4xl md:text-5xl font-serif font-bold mb-4"
|
|
style={{
|
|
background: 'linear-gradient(to right, #fff3eb, #e4cfff, #f5f9ff)',
|
|
WebkitBackgroundClip: 'text',
|
|
WebkitTextFillColor: 'transparent',
|
|
}}
|
|
>
|
|
INFINITH
|
|
</motion.h1>
|
|
|
|
{url && (
|
|
<motion.p
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.6, delay: 0.1 }}
|
|
className="text-purple-300/80 text-sm font-mono mb-12 truncate max-w-full"
|
|
>
|
|
{url}
|
|
</motion.p>
|
|
)}
|
|
|
|
{error ? (
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.95 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
className="w-full p-6 rounded-2xl bg-red-500/10 border border-red-500/20 text-center"
|
|
>
|
|
<AlertCircle className="w-10 h-10 text-red-400 mx-auto mb-3" />
|
|
<p className="text-red-300 text-sm mb-4">{error}</p>
|
|
<button
|
|
onClick={() => navigate('/', { replace: true })}
|
|
className="px-6 py-2 text-sm font-medium text-white bg-white/10 rounded-lg hover:bg-white/20 transition-colors"
|
|
>
|
|
Try Again
|
|
</button>
|
|
</motion.div>
|
|
) : (
|
|
<>
|
|
<div className="w-full space-y-5 mb-14">
|
|
{steps.map((step, index) => {
|
|
const isCompleted = currentStep > index;
|
|
const isActive = currentStep === index + 1 && currentStep <= steps.length;
|
|
|
|
return (
|
|
<motion.div
|
|
key={step.key}
|
|
initial={{ opacity: 0, x: -20 }}
|
|
animate={{ opacity: isActive || isCompleted ? 1 : 0.3, x: 0 }}
|
|
transition={{ duration: 0.4, delay: index * 0.15 }}
|
|
className="flex items-center gap-4"
|
|
>
|
|
<div className="w-7 h-7 flex-shrink-0 flex items-center justify-center">
|
|
{isCompleted ? (
|
|
<motion.div
|
|
initial={{ scale: 0 }}
|
|
animate={{ scale: 1 }}
|
|
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
|
|
className="w-7 h-7 rounded-full bg-gradient-to-r from-[#4F1DA1] to-[#6C5CE7] flex items-center justify-center"
|
|
>
|
|
<Check className="w-4 h-4 text-white" strokeWidth={3} />
|
|
</motion.div>
|
|
) : isActive ? (
|
|
<div className="w-7 h-7 rounded-full border-2 border-purple-400 border-t-transparent animate-spin" />
|
|
) : (
|
|
<div className="w-7 h-7 rounded-full border-2 border-white/10" />
|
|
)}
|
|
</div>
|
|
<span
|
|
className={`text-base font-sans transition-colors duration-300 ${
|
|
isCompleted
|
|
? 'text-white'
|
|
: isActive
|
|
? 'text-purple-200'
|
|
: 'text-white/30'
|
|
}`}
|
|
>
|
|
{step.label}
|
|
</span>
|
|
</motion.div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="w-full h-2 bg-white/10 rounded-full overflow-hidden">
|
|
<motion.div
|
|
initial={{ width: '0%' }}
|
|
animate={{ width: `${(currentStep / steps.length) * 100}%` }}
|
|
transition={{ duration: 0.8, ease: 'easeInOut' }}
|
|
className="h-full bg-gradient-to-r from-[#4F1DA1] to-[#6C5CE7] rounded-full"
|
|
/>
|
|
</div>
|
|
|
|
<p className="text-white/40 text-xs mt-4">
|
|
AI가 마케팅 데이터를 분석하고 있습니다. 약 20~30초 소요됩니다.
|
|
</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|