Component props.
Rendered loading view with progress tracking and error handling.
export function LoadingView({
pageVariants,
contentVariants,
matchingProcess,
rateLimitState,
navigate,
matchResultsLength,
onRetry,
onDismissError,
}: Readonly<LoadingViewProps>) {
const rateLimitCountdown = useMemo(() => {
if (!rateLimitState.isRateLimited || !rateLimitState.retryAfter) {
return null;
}
// Format remaining time as MM:SS
const msRemaining = Math.max(rateLimitState.retryAfter - Date.now(), 0);
const totalSeconds = Math.ceil(msRemaining / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
}, [rateLimitState.isRateLimited, rateLimitState.retryAfter]);
return (
<motion.div
className="relative mx-auto max-w-5xl space-y-8 overflow-hidden px-4 py-10 md:px-6"
variants={pageVariants}
initial="hidden"
animate="visible"
>
{/* Loading State with Progress and Cancel Button */}
<motion.div variants={contentVariants} className="relative z-10">
<MatchingProgressPanel
isCancelling={matchingProcess.isCancelling}
progress={matchingProcess.progress}
statusMessage={matchingProcess.statusMessage}
timeEstimate={matchingProcess.timeEstimate}
onCancelProcess={matchingProcess.handleCancelProcess}
onPauseProcess={matchingProcess.handlePauseMatching}
onResumeProcess={matchingProcess.handleResumeMatchingRequests}
bypassCache={matchingProcess.bypassCache}
isFreshSearch={matchingProcess.isFreshSearch}
shouldDisableControls={rateLimitState.isRateLimited}
isPaused={matchingProcess.isTimeEstimatePaused}
isManuallyPaused={matchingProcess.isManuallyPaused}
isPauseTransitioning={matchingProcess.isPauseTransitioning}
isRateLimitActive={
rateLimitState.isRateLimited || matchingProcess.isRateLimitPaused
}
/>
</motion.div>
{rateLimitState.isRateLimited && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25 }}
className="relative z-10"
>
<Card className="overflow-hidden border border-amber-300/60 bg-amber-50/70 shadow-lg shadow-amber-200/40 dark:border-amber-500/30 dark:bg-amber-900/25">
<CardContent className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div className="flex items-start gap-4">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-amber-500/20 text-amber-600 dark:bg-amber-500/25 dark:text-amber-200">
<AlertCircle className="h-6 w-6" />
</div>
<div className="space-y-1">
<h3 className="text-base font-semibold text-amber-700 dark:text-amber-100">
AniList rate limit in effect
</h3>
<p className="text-sm text-amber-700/80 dark:text-amber-100/80">
{rateLimitState.message ||
"AniList API rate limit reached. Please wait before making more requests."}
</p>
</div>
</div>
{rateLimitCountdown && (
<div className="flex flex-col items-start rounded-xl border border-amber-400/40 bg-white/70 px-4 py-2 text-sm font-semibold text-amber-700 shadow-sm dark:border-amber-500/40 dark:bg-amber-950/40 dark:text-amber-100">
<span className="text-xs font-medium uppercase tracking-[0.18em] text-amber-600/80 dark:text-amber-200/80">
Retry in
</span>
<span className="text-lg font-semibold leading-tight">
{rateLimitCountdown}
</span>
</div>
)}
</CardContent>
</Card>
</motion.div>
)}
{/* Error Display */}
{matchingProcess.error && !matchResultsLength && (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.25, duration: 0.3 }}
>
{matchingProcess.error.includes("Authentication Required") ? (
<Card className="bg-linear-to-br mx-auto w-full max-w-xl overflow-hidden border border-amber-300/60 from-amber-50/70 via-white/80 to-amber-100/70 text-center shadow-lg shadow-amber-100/60 dark:border-amber-500/40 dark:from-amber-900/30 dark:via-slate-950/40 dark:to-amber-900/20">
<CardContent className="pb-5 pt-6">
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-amber-500/15 text-amber-600 dark:bg-amber-500/20 dark:text-amber-200">
<AlertCircle className="h-6 w-6" />
</div>
<h3 className="text-lg font-semibold text-amber-700 dark:text-amber-100">
Authentication Required
</h3>
<p className="mt-2 text-sm text-amber-700/80 dark:text-amber-100/80">
You need to connect your AniList account before matching your
manga library.
</p>
<Button
onClick={() => navigate({ to: "/settings" })}
className="bg-linear-to-r mt-4 from-blue-600 via-indigo-600 to-purple-600 text-white shadow-md hover:from-blue-700 hover:via-indigo-700 hover:to-purple-700"
>
Go to Settings
</Button>
</CardContent>
</Card>
) : (
<Card className="bg-linear-to-br mx-auto w-full max-w-xl overflow-hidden border border-rose-300/60 from-rose-50/70 via-white/80 to-rose-100/70 text-center shadow-lg shadow-rose-100/60 dark:border-rose-500/40 dark:from-rose-900/30 dark:via-slate-950/40 dark:to-rose-900/20">
<CardContent className="pb-5 pt-6">
<h3 className="text-lg font-semibold text-rose-700 dark:text-rose-100">
{matchingProcess.error}
</h3>
<p className="mt-2 text-sm text-rose-700/80 dark:text-rose-100/80">
{matchingProcess.detailMessage}
</p>
<div className="mt-5 flex flex-wrap items-center justify-center gap-3">
<Button
className="bg-linear-to-r from-rose-500 via-red-500 to-orange-500 text-white shadow-md hover:from-rose-600 hover:via-red-600 hover:to-orange-600"
onClick={() => onRetry()}
>
Retry
</Button>
<Button
variant="ghost"
onClick={() => onDismissError()}
className="hover:bg-rose-100/60 hover:text-rose-600 dark:hover:bg-rose-900/20 dark:hover:text-rose-200"
>
Dismiss
</Button>
</div>
</CardContent>
</Card>
)}
</motion.div>
)}
</motion.div>
);
}
Displays matching process progress with rate limit warnings and controls. Shows real-time progress, rate limit countdowns, and pause/resume/cancel buttons.