Panel for configuring and triggering rematch operations for manga entries by status. Allows users to select which statuses to include in rematch and clears cache if requested.
export const RematchOptions: React.FC<RematchOptionsProps> = ({ selectedStatuses, onChangeSelectedStatuses, matchResults, rematchWarning, onRematchByStatus, onCloseOptions,}) => { const toggleStatus = (status: keyof StatusFilterOptions) => { onChangeSelectedStatuses({ ...selectedStatuses, [status]: !selectedStatuses[status], }); }; const resetToDefault = () => { onChangeSelectedStatuses({ ...selectedStatuses, pending: true, skipped: true, matched: false, manual: false, }); }; // Calculate the total count of manga to be rematched const calculateTotalCount = () => { return Object.entries(selectedStatuses) .filter(([, selected]) => selected) .reduce((count, [status]) => { return count + matchResults.filter((m) => m.status === status).length; }, 0); }; // Calculate individual counts for status badges const statusCounts = { pending: matchResults.filter((m) => m.status === "pending").length, skipped: matchResults.filter((m) => m.status === "skipped").length, matched: matchResults.filter((m) => m.status === "matched").length, manual: matchResults.filter((m) => m.status === "manual").length, }; type RematchStatusKey = Extract< keyof StatusFilterOptions, "pending" | "matched" | "manual" | "skipped" >; const statusOptions: Array<{ key: RematchStatusKey; label: string; description: string; icon: React.ComponentType<React.SVGProps<SVGSVGElement>>; accentClass: string; badgeClass: string; }> = [ { key: "pending", label: "Pending", description: "Queued for a fresh AniList lookup", icon: Clock3, accentClass: "bg-gradient-to-br from-blue-500/20 via-blue-400/15 to-cyan-400/10 text-blue-600 dark:from-blue-500/25 dark:via-blue-500/15 dark:to-cyan-500/10 dark:text-blue-200", badgeClass: "bg-blue-100/80 text-blue-700 dark:bg-blue-900/30 dark:text-blue-200", }, { key: "matched", label: "Matched", description: "Already aligned titles you may want to re-evaluate", icon: Sparkles, accentClass: "bg-gradient-to-br from-emerald-500/20 via-emerald-400/15 to-teal-400/10 text-emerald-600 dark:from-emerald-500/25 dark:via-emerald-500/15 dark:to-teal-500/10 dark:text-emerald-200", badgeClass: "bg-emerald-100/80 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-200", }, { key: "manual", label: "Manual", description: "Titles waiting for your curated matches", icon: Hand, accentClass: "bg-gradient-to-br from-purple-500/20 via-indigo-400/15 to-blue-400/10 text-purple-600 dark:from-purple-500/25 dark:via-indigo-500/15 dark:to-blue-500/10 dark:text-purple-200", badgeClass: "bg-purple-100/80 text-purple-700 dark:bg-purple-900/30 dark:text-purple-200", }, { key: "skipped", label: "Skipped", description: "Entries previously skipped during matching", icon: Slash, accentClass: "bg-gradient-to-br from-rose-500/20 via-orange-400/15 to-amber-400/10 text-rose-600 dark:from-rose-500/25 dark:via-orange-500/15 dark:to-amber-500/10 dark:text-rose-200", badgeClass: "bg-rose-100/80 text-rose-700 dark:bg-rose-900/30 dark:text-rose-200", }, ]; return ( <motion.div initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -10 }} transition={{ duration: 0.2 }} > <Card className="from-white/92 bg-linear-to-br supports-backdrop-filter:backdrop-blur-md relative mb-6 flex flex-col overflow-hidden border border-blue-200/70 via-white/85 to-blue-50/75 py-0 shadow-xl shadow-blue-500/10 dark:border-blue-900/60 dark:from-slate-950/70 dark:via-slate-950/55 dark:to-blue-950/45"> <div className="from-blue-200/18 via-indigo-200/12 bg-linear-to-br pointer-events-none absolute inset-0 to-transparent dark:from-blue-900/25 dark:via-indigo-900/15 dark:to-transparent" /> <CardHeader className="bg-linear-to-r relative border-b border-blue-100/60 from-blue-50/80 via-indigo-50/70 to-purple-50/65 py-3 dark:border-blue-900/50 dark:from-blue-950/40 dark:via-indigo-950/35 dark:to-purple-950/30"> <div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex items-center gap-2"> <span className="dark:bg-blue-500/12 flex h-9 w-9 items-center justify-center rounded-full bg-blue-500/15 text-blue-600 shadow-sm shadow-blue-500/25 dark:text-blue-200"> <RefreshCw className="h-4 w-4" /> </span> <div> <CardTitle className="text-lg font-semibold text-slate-900 dark:text-slate-100"> Rematch Options </CardTitle> <p className="text-xs text-slate-500 dark:text-slate-400"> Choose which states should get a fresh search run </p> </div> </div> <Button variant="ghost" size="icon" onClick={onCloseOptions} className="h-8 w-8 rounded-full border border-transparent bg-white/70 text-slate-600 shadow-sm transition hover:border-blue-200 hover:bg-blue-50 hover:text-slate-800 dark:bg-slate-900/60 dark:text-slate-300 dark:hover:border-blue-800 dark:hover:bg-slate-900/80" > <X className="h-4 w-4" /> </Button> </div> </CardHeader> <CardContent className="relative space-y-5 p-6 pb-4"> {rematchWarning && ( <Alert variant="default" className="border-yellow-200 bg-yellow-50 dark:border-yellow-900/50 dark:bg-yellow-900/20" > <AlertTriangle className="h-4 w-4 text-yellow-600 dark:text-yellow-400" /> <AlertTitle className="text-yellow-800 dark:text-yellow-400"> Warning </AlertTitle> <AlertDescription className="text-yellow-700 dark:text-yellow-300"> {rematchWarning} </AlertDescription> </Alert> )} <div className="space-y-3"> <div className="flex items-center justify-between"> <h3 className="text-sm font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400"> Status Filters </h3> <Button variant="ghost" size="sm" onClick={resetToDefault} className="h-8 gap-1 rounded-full border border-blue-100/60 bg-white/70 px-3 text-xs font-semibold uppercase tracking-[0.18em] text-blue-600 shadow-sm hover:border-blue-200 hover:bg-blue-50 dark:border-blue-900/40 dark:bg-slate-950/60 dark:text-blue-200 dark:hover:border-blue-800" > <RotateCcw className="h-3.5 w-3.5" /> Reset </Button> </div> <div className="grid grid-cols-1 gap-3 md:grid-cols-2"> {statusOptions.map( ({ key, label, description, icon: Icon, accentClass, badgeClass, }) => { const checkboxId = `status-${key}`; const isSelected = selectedStatuses[key]; return ( <label key={key} htmlFor={checkboxId} className="group relative overflow-hidden rounded-2xl border border-slate-200/70 bg-white/80 p-4 shadow-sm shadow-blue-500/10 transition duration-300 ease-out hover:-translate-y-1 hover:shadow-xl dark:border-slate-800/60 dark:bg-slate-950/65" > <div className="bg-linear-to-br pointer-events-none absolute inset-0 from-white/70 via-transparent to-transparent opacity-70 dark:from-slate-900/50" /> <div className="relative flex items-start gap-3"> <Checkbox id={checkboxId} checked={isSelected} onCheckedChange={() => toggleStatus(key)} className="mt-1 border-blue-300 data-[state=checked]:bg-blue-500 data-[state=checked]:text-white dark:border-blue-800 dark:data-[state=checked]:text-slate-900" /> <div className="flex-1 space-y-2"> <div className="flex items-center justify-between gap-3"> <div className="flex items-center gap-3"> <span className={`flex h-10 w-10 items-center justify-center rounded-xl shadow-inner ${accentClass}`} > <Icon className="h-5 w-5" /> </span> <div> <div className="flex items-center gap-2"> <span className="text-sm font-semibold text-slate-800 dark:text-slate-100"> {label} </span> <Badge variant="secondary" className={`px-2 py-0.5 text-xs font-semibold ${badgeClass}`} > {statusCounts[key]} </Badge> </div> <p className="text-xs text-slate-500 dark:text-slate-400"> {description} </p> </div> </div> </div> <div className="flex items-center justify-between text-xs font-medium uppercase tracking-[0.18em] text-slate-400 dark:text-slate-500"> <span>{isSelected ? "Included" : "Excluded"}</span> <span className="flex items-center gap-1 text-slate-500 dark:text-slate-400"> <Layers className="h-3.5 w-3.5" /> Queue position </span> </div> </div> </div> </label> ); }, )} </div> </div> <div className="dark:from-blue-500/12 dark:via-indigo-500/12 dark:to-cyan-500/12 bg-linear-to-r relative flex flex-col gap-3 rounded-2xl border border-blue-200/60 from-blue-500/10 via-indigo-500/10 to-cyan-500/10 p-5 shadow-inner sm:flex-row sm:items-center sm:justify-between dark:border-blue-500/25"> <div className="flex items-center gap-3 text-sm font-medium text-blue-700 dark:text-blue-200"> <span className="dark:bg-blue-500/12 flex h-10 w-10 items-center justify-center rounded-xl bg-blue-500/15 text-blue-600 shadow-sm shadow-blue-500/20 dark:text-blue-200"> <RefreshCw className="h-5 w-5" /> </span> <div> <span className="block text-xs uppercase tracking-[0.24em] text-blue-500/80 dark:text-blue-300/80"> Queue Summary </span> <span className="text-base font-semibold text-blue-700 dark:text-blue-100"> {calculateTotalCount()} manga selected for rematch </span> </div> </div> <Button variant="ghost" size="sm" onClick={resetToDefault} className="hidden h-9 gap-1 rounded-full border border-blue-200/70 bg-white/70 px-3 text-xs font-semibold uppercase tracking-[0.18em] text-blue-600 shadow-sm hover:border-blue-300 hover:bg-blue-50 sm:flex dark:border-blue-800/60 dark:bg-slate-950/70 dark:text-blue-200 dark:hover:border-blue-700" > <RotateCcw className="h-3.5 w-3.5" /> Reset filters </Button> </div> </CardContent> <CardFooter className="bg-linear-to-r relative flex flex-col gap-3 border-t border-blue-100/70 from-blue-50/80 via-indigo-50/70 to-purple-50/70 p-4 sm:flex-row sm:items-center sm:justify-between dark:border-blue-900/50 dark:from-blue-950/45 dark:via-indigo-950/35 dark:to-purple-950/30"> <div className="text-xs text-slate-500 dark:text-slate-400"> {calculateTotalCount() === 0 ? "Select at least one status to enable rematching." : "We'll run a fresh search for every selected status group."} </div> <Button variant="default" onClick={onRematchByStatus} className="bg-linear-to-r w-full rounded-2xl from-blue-600 via-indigo-600 to-purple-600 text-base font-semibold shadow-lg shadow-blue-500/25 transition hover:from-blue-700 hover:via-indigo-700 hover:to-purple-700 focus-visible:ring-blue-400 sm:w-auto" disabled={calculateTotalCount() === 0} > <RefreshCw className="mr-2 h-4 w-4" /> Fresh Search Selected ({calculateTotalCount()}) </Button> </CardFooter> </Card> </motion.div> );}; Copy
export const RematchOptions: React.FC<RematchOptionsProps> = ({ selectedStatuses, onChangeSelectedStatuses, matchResults, rematchWarning, onRematchByStatus, onCloseOptions,}) => { const toggleStatus = (status: keyof StatusFilterOptions) => { onChangeSelectedStatuses({ ...selectedStatuses, [status]: !selectedStatuses[status], }); }; const resetToDefault = () => { onChangeSelectedStatuses({ ...selectedStatuses, pending: true, skipped: true, matched: false, manual: false, }); }; // Calculate the total count of manga to be rematched const calculateTotalCount = () => { return Object.entries(selectedStatuses) .filter(([, selected]) => selected) .reduce((count, [status]) => { return count + matchResults.filter((m) => m.status === status).length; }, 0); }; // Calculate individual counts for status badges const statusCounts = { pending: matchResults.filter((m) => m.status === "pending").length, skipped: matchResults.filter((m) => m.status === "skipped").length, matched: matchResults.filter((m) => m.status === "matched").length, manual: matchResults.filter((m) => m.status === "manual").length, }; type RematchStatusKey = Extract< keyof StatusFilterOptions, "pending" | "matched" | "manual" | "skipped" >; const statusOptions: Array<{ key: RematchStatusKey; label: string; description: string; icon: React.ComponentType<React.SVGProps<SVGSVGElement>>; accentClass: string; badgeClass: string; }> = [ { key: "pending", label: "Pending", description: "Queued for a fresh AniList lookup", icon: Clock3, accentClass: "bg-gradient-to-br from-blue-500/20 via-blue-400/15 to-cyan-400/10 text-blue-600 dark:from-blue-500/25 dark:via-blue-500/15 dark:to-cyan-500/10 dark:text-blue-200", badgeClass: "bg-blue-100/80 text-blue-700 dark:bg-blue-900/30 dark:text-blue-200", }, { key: "matched", label: "Matched", description: "Already aligned titles you may want to re-evaluate", icon: Sparkles, accentClass: "bg-gradient-to-br from-emerald-500/20 via-emerald-400/15 to-teal-400/10 text-emerald-600 dark:from-emerald-500/25 dark:via-emerald-500/15 dark:to-teal-500/10 dark:text-emerald-200", badgeClass: "bg-emerald-100/80 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-200", }, { key: "manual", label: "Manual", description: "Titles waiting for your curated matches", icon: Hand, accentClass: "bg-gradient-to-br from-purple-500/20 via-indigo-400/15 to-blue-400/10 text-purple-600 dark:from-purple-500/25 dark:via-indigo-500/15 dark:to-blue-500/10 dark:text-purple-200", badgeClass: "bg-purple-100/80 text-purple-700 dark:bg-purple-900/30 dark:text-purple-200", }, { key: "skipped", label: "Skipped", description: "Entries previously skipped during matching", icon: Slash, accentClass: "bg-gradient-to-br from-rose-500/20 via-orange-400/15 to-amber-400/10 text-rose-600 dark:from-rose-500/25 dark:via-orange-500/15 dark:to-amber-500/10 dark:text-rose-200", badgeClass: "bg-rose-100/80 text-rose-700 dark:bg-rose-900/30 dark:text-rose-200", }, ]; return ( <motion.div initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -10 }} transition={{ duration: 0.2 }} > <Card className="from-white/92 bg-linear-to-br supports-backdrop-filter:backdrop-blur-md relative mb-6 flex flex-col overflow-hidden border border-blue-200/70 via-white/85 to-blue-50/75 py-0 shadow-xl shadow-blue-500/10 dark:border-blue-900/60 dark:from-slate-950/70 dark:via-slate-950/55 dark:to-blue-950/45"> <div className="from-blue-200/18 via-indigo-200/12 bg-linear-to-br pointer-events-none absolute inset-0 to-transparent dark:from-blue-900/25 dark:via-indigo-900/15 dark:to-transparent" /> <CardHeader className="bg-linear-to-r relative border-b border-blue-100/60 from-blue-50/80 via-indigo-50/70 to-purple-50/65 py-3 dark:border-blue-900/50 dark:from-blue-950/40 dark:via-indigo-950/35 dark:to-purple-950/30"> <div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex items-center gap-2"> <span className="dark:bg-blue-500/12 flex h-9 w-9 items-center justify-center rounded-full bg-blue-500/15 text-blue-600 shadow-sm shadow-blue-500/25 dark:text-blue-200"> <RefreshCw className="h-4 w-4" /> </span> <div> <CardTitle className="text-lg font-semibold text-slate-900 dark:text-slate-100"> Rematch Options </CardTitle> <p className="text-xs text-slate-500 dark:text-slate-400"> Choose which states should get a fresh search run </p> </div> </div> <Button variant="ghost" size="icon" onClick={onCloseOptions} className="h-8 w-8 rounded-full border border-transparent bg-white/70 text-slate-600 shadow-sm transition hover:border-blue-200 hover:bg-blue-50 hover:text-slate-800 dark:bg-slate-900/60 dark:text-slate-300 dark:hover:border-blue-800 dark:hover:bg-slate-900/80" > <X className="h-4 w-4" /> </Button> </div> </CardHeader> <CardContent className="relative space-y-5 p-6 pb-4"> {rematchWarning && ( <Alert variant="default" className="border-yellow-200 bg-yellow-50 dark:border-yellow-900/50 dark:bg-yellow-900/20" > <AlertTriangle className="h-4 w-4 text-yellow-600 dark:text-yellow-400" /> <AlertTitle className="text-yellow-800 dark:text-yellow-400"> Warning </AlertTitle> <AlertDescription className="text-yellow-700 dark:text-yellow-300"> {rematchWarning} </AlertDescription> </Alert> )} <div className="space-y-3"> <div className="flex items-center justify-between"> <h3 className="text-sm font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400"> Status Filters </h3> <Button variant="ghost" size="sm" onClick={resetToDefault} className="h-8 gap-1 rounded-full border border-blue-100/60 bg-white/70 px-3 text-xs font-semibold uppercase tracking-[0.18em] text-blue-600 shadow-sm hover:border-blue-200 hover:bg-blue-50 dark:border-blue-900/40 dark:bg-slate-950/60 dark:text-blue-200 dark:hover:border-blue-800" > <RotateCcw className="h-3.5 w-3.5" /> Reset </Button> </div> <div className="grid grid-cols-1 gap-3 md:grid-cols-2"> {statusOptions.map( ({ key, label, description, icon: Icon, accentClass, badgeClass, }) => { const checkboxId = `status-${key}`; const isSelected = selectedStatuses[key]; return ( <label key={key} htmlFor={checkboxId} className="group relative overflow-hidden rounded-2xl border border-slate-200/70 bg-white/80 p-4 shadow-sm shadow-blue-500/10 transition duration-300 ease-out hover:-translate-y-1 hover:shadow-xl dark:border-slate-800/60 dark:bg-slate-950/65" > <div className="bg-linear-to-br pointer-events-none absolute inset-0 from-white/70 via-transparent to-transparent opacity-70 dark:from-slate-900/50" /> <div className="relative flex items-start gap-3"> <Checkbox id={checkboxId} checked={isSelected} onCheckedChange={() => toggleStatus(key)} className="mt-1 border-blue-300 data-[state=checked]:bg-blue-500 data-[state=checked]:text-white dark:border-blue-800 dark:data-[state=checked]:text-slate-900" /> <div className="flex-1 space-y-2"> <div className="flex items-center justify-between gap-3"> <div className="flex items-center gap-3"> <span className={`flex h-10 w-10 items-center justify-center rounded-xl shadow-inner ${accentClass}`} > <Icon className="h-5 w-5" /> </span> <div> <div className="flex items-center gap-2"> <span className="text-sm font-semibold text-slate-800 dark:text-slate-100"> {label} </span> <Badge variant="secondary" className={`px-2 py-0.5 text-xs font-semibold ${badgeClass}`} > {statusCounts[key]} </Badge> </div> <p className="text-xs text-slate-500 dark:text-slate-400"> {description} </p> </div> </div> </div> <div className="flex items-center justify-between text-xs font-medium uppercase tracking-[0.18em] text-slate-400 dark:text-slate-500"> <span>{isSelected ? "Included" : "Excluded"}</span> <span className="flex items-center gap-1 text-slate-500 dark:text-slate-400"> <Layers className="h-3.5 w-3.5" /> Queue position </span> </div> </div> </div> </label> ); }, )} </div> </div> <div className="dark:from-blue-500/12 dark:via-indigo-500/12 dark:to-cyan-500/12 bg-linear-to-r relative flex flex-col gap-3 rounded-2xl border border-blue-200/60 from-blue-500/10 via-indigo-500/10 to-cyan-500/10 p-5 shadow-inner sm:flex-row sm:items-center sm:justify-between dark:border-blue-500/25"> <div className="flex items-center gap-3 text-sm font-medium text-blue-700 dark:text-blue-200"> <span className="dark:bg-blue-500/12 flex h-10 w-10 items-center justify-center rounded-xl bg-blue-500/15 text-blue-600 shadow-sm shadow-blue-500/20 dark:text-blue-200"> <RefreshCw className="h-5 w-5" /> </span> <div> <span className="block text-xs uppercase tracking-[0.24em] text-blue-500/80 dark:text-blue-300/80"> Queue Summary </span> <span className="text-base font-semibold text-blue-700 dark:text-blue-100"> {calculateTotalCount()} manga selected for rematch </span> </div> </div> <Button variant="ghost" size="sm" onClick={resetToDefault} className="hidden h-9 gap-1 rounded-full border border-blue-200/70 bg-white/70 px-3 text-xs font-semibold uppercase tracking-[0.18em] text-blue-600 shadow-sm hover:border-blue-300 hover:bg-blue-50 sm:flex dark:border-blue-800/60 dark:bg-slate-950/70 dark:text-blue-200 dark:hover:border-blue-700" > <RotateCcw className="h-3.5 w-3.5" /> Reset filters </Button> </div> </CardContent> <CardFooter className="bg-linear-to-r relative flex flex-col gap-3 border-t border-blue-100/70 from-blue-50/80 via-indigo-50/70 to-purple-50/70 p-4 sm:flex-row sm:items-center sm:justify-between dark:border-blue-900/50 dark:from-blue-950/45 dark:via-indigo-950/35 dark:to-purple-950/30"> <div className="text-xs text-slate-500 dark:text-slate-400"> {calculateTotalCount() === 0 ? "Select at least one status to enable rematching." : "We'll run a fresh search for every selected status group."} </div> <Button variant="default" onClick={onRematchByStatus} className="bg-linear-to-r w-full rounded-2xl from-blue-600 via-indigo-600 to-purple-600 text-base font-semibold shadow-lg shadow-blue-500/25 transition hover:from-blue-700 hover:via-indigo-700 hover:to-purple-700 focus-visible:ring-blue-400 sm:w-auto" disabled={calculateTotalCount() === 0} > <RefreshCw className="mr-2 h-4 w-4" /> Fresh Search Selected ({calculateTotalCount()}) </Button> </CardFooter> </Card> </motion.div> );};
Component props.
Rendered rematch options panel with status filters and action buttons.
Panel for configuring and triggering rematch operations for manga entries by status. Allows users to select which statuses to include in rematch and clears cache if requested.
Source