Component props with sync configuration and callbacks.
Collapsible panel with nested toggle options and auto-pause settings.
export function SyncConfigurationPanel({
syncConfig,
setSyncConfig,
isCustomThresholdEnabled,
setIsCustomThresholdEnabled,
handleToggleOption,
}: Readonly<SyncConfigurationPanelProps>) {
const toggleOptions: Array<{
key: keyof SyncConfig;
title: string;
description: string;
icon: LucideIcon;
accent: string;
}> = [
{
key: "prioritizeAniListStatus",
title: "Prioritize AniList status",
description:
"Keeps your existing AniList status when differences are detected.",
icon: ShieldCheck,
accent: "from-blue-500 to-indigo-500",
},
{
key: "preserveCompletedStatus",
title: "Preserve completed entries",
description:
"Protects anything marked completed in AniList from further changes.",
icon: CheckCircle2,
accent: "from-emerald-500 to-emerald-600",
},
{
key: "prioritizeAniListProgress",
title: "Prioritize AniList progress",
description:
"Keeps the higher chapter count between AniList and Kenmei records.",
icon: Gauge,
accent: "from-sky-500 to-blue-500",
},
{
key: "prioritizeAniListScore",
title: "Prioritize AniList scores",
description:
"Maintains your AniList scores unless Kenmei has stronger data.",
icon: Star,
accent: "from-amber-500 to-orange-500",
},
{
key: "setPrivate",
title: "Set entries as private",
description:
"Applies AniList privacy to new or updated entries when enabled.",
icon: Lock,
accent: "from-purple-500 to-fuchsia-500",
},
];
return (
<Collapsible className="space-y-3">
<CollapsibleTrigger asChild>
<Button
variant="ghost"
className="group flex w-full items-center justify-between rounded-2xl border border-slate-200/70 bg-white/80 px-4 py-3 text-sm font-semibold text-slate-800 shadow-sm transition hover:border-blue-200/70 hover:bg-blue-50/70 data-[state=open]:border-blue-200/70 data-[state=open]:bg-blue-50/70 dark:border-slate-800/70 dark:bg-slate-950/60 dark:text-slate-100 dark:hover:border-blue-800/50 dark:hover:bg-blue-950/20 dark:data-[state=open]:border-blue-900/50 dark:data-[state=open]:bg-blue-950/30"
>
<span className="flex items-center gap-2">
<Settings className="h-4 w-4 text-blue-500 dark:text-blue-400" />
Sync Configuration
</span>
<div className="flex items-center gap-2 text-xs text-slate-500 transition group-data-[state=open]:text-blue-500 dark:text-slate-400 dark:group-data-[state=open]:text-blue-300">
<span>Adjust your rules</span>
<ChevronDown className="h-4 w-4 transition-transform duration-300 group-data-[state=open]:-rotate-90" />
</div>
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="mt-4 space-y-4 rounded-3xl border border-slate-200/60 bg-white/80 p-5 shadow-sm backdrop-blur-xl dark:border-slate-800/60 dark:bg-slate-950/60">
<div className="grid gap-4 md:grid-cols-2">
{toggleOptions.map((option) => {
const Icon = option.icon;
return (
<div
key={option.key as string}
className="flex flex-col gap-3 rounded-2xl border border-slate-200/70 bg-white/80 p-4 shadow-sm transition hover:border-blue-200/60 hover:shadow-md dark:border-slate-800/60 dark:bg-slate-950/50 dark:hover:border-blue-900/50"
>
<div className="flex items-start justify-between gap-4">
<div className="flex items-start gap-3">
<div
className={`bg-linear-to-br flex h-10 w-10 shrink-0 items-center justify-center rounded-full ${option.accent} text-white shadow`}
>
<Icon className="h-5 w-5" />
</div>
<div className="space-y-1">
<Label
htmlFor={option.key as string}
className="text-sm font-semibold text-slate-900 dark:text-slate-100"
>
{option.title}
</Label>
<p className="text-xs text-slate-500 dark:text-slate-400">
{option.description}
</p>
</div>
</div>
<Switch
id={option.key as string}
checked={Boolean(syncConfig[option.key])}
onCheckedChange={() => handleToggleOption(option.key)}
/>
</div>
</div>
);
})}
</div>
<div className="rounded-2xl border border-amber-200/70 bg-amber-50/60 p-4 shadow-sm transition hover:border-amber-300/70 hover:shadow-md dark:border-amber-800/40 dark:bg-amber-900/20">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-start gap-3">
<div className="bg-linear-to-br flex h-10 w-10 shrink-0 items-center justify-center rounded-full from-amber-500 to-orange-500 text-white shadow">
<Clock3 className="h-5 w-5" />
</div>
<div className="space-y-1">
<Label
htmlFor="autoPauseInactive"
className="text-sm font-semibold text-slate-900 dark:text-slate-100"
>
Auto-pause inactive manga
</Label>
<p className="text-xs text-amber-700 dark:text-amber-300/80">
Automatically pause series that haven't updated recently.
</p>
</div>
</div>
<Switch
id="autoPauseInactive"
checked={syncConfig.autoPauseInactive}
onCheckedChange={() => handleToggleOption("autoPauseInactive")}
/>
</div>
{syncConfig.autoPauseInactive && (
<div className="mt-4 space-y-3 rounded-2xl border border-amber-200/70 bg-white/80 p-4 shadow-sm dark:border-amber-800/40 dark:bg-slate-950/60">
<Label
htmlFor="autoPauseThreshold"
className="text-xs font-semibold uppercase tracking-wide text-amber-700 dark:text-amber-300/80"
>
Pause after
</Label>
{isCustomThresholdEnabled ? (
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
<input
id="customAutoPauseThreshold"
type="number"
min="1"
placeholder="Enter days"
value={syncConfig.autoPauseThreshold.toString()}
onChange={(e) => {
const value = Number.parseInt(e.target.value, 10);
if (!Number.isNaN(value) && value > 0) {
setSyncConfig((prev) => {
const newConfig = {
...prev,
autoPauseThreshold: value,
};
saveSyncConfig(newConfig);
return newConfig;
});
}
}}
className="h-9 w-full rounded-lg border border-amber-200/70 bg-white/90 px-3 text-sm text-slate-700 shadow-sm outline-none transition focus:border-amber-300 focus:ring-2 focus:ring-amber-200 dark:border-amber-800/40 dark:bg-slate-950/70 dark:text-slate-100 dark:focus:border-amber-700 dark:focus:ring-amber-900/40"
/>
<Button
variant="outline"
className="h-9 rounded-lg border-amber-200/70 px-3 text-xs font-semibold text-amber-700 hover:border-amber-300 hover:bg-amber-100/60 dark:border-amber-800/40 dark:text-amber-300 dark:hover:border-amber-600 dark:hover:bg-amber-900/20"
onClick={() => setIsCustomThresholdEnabled(false)}
>
Use Presets
</Button>
</div>
) : (
<select
id="autoPauseThreshold"
value={syncConfig.autoPauseThreshold.toString()}
onChange={(e) => {
const value = e.target.value;
if (value === "custom") {
setIsCustomThresholdEnabled(true);
} else {
setSyncConfig((prev) => {
const newConfig = {
...prev,
autoPauseThreshold: Number(value),
};
saveSyncConfig(newConfig);
return newConfig;
});
}
}}
className="h-9 w-full rounded-lg border border-amber-200/70 bg-white/90 px-3 text-sm text-slate-700 shadow-sm outline-none transition focus:border-amber-300 focus:ring-2 focus:ring-amber-200 dark:border-amber-800/40 dark:bg-slate-950/70 dark:text-slate-100 dark:focus:border-amber-700 dark:focus:ring-amber-900/40"
>
<option value="1">1 day</option>
<option value="7">7 days</option>
<option value="14">14 days</option>
<option value="30">30 days</option>
<option value="60">2 months</option>
<option value="90">3 months</option>
<option value="180">6 months</option>
<option value="365">1 year</option>
<option value="custom">Custom...</option>
</select>
)}
<p className="text-xs text-slate-500 dark:text-slate-400">
Series beyond this window are automatically paused on AniList.
</p>
</div>
)}
</div>
</CollapsibleContent>
</Collapsible>
);
}
Collapsible configuration panel for managing sync priority and behavior settings. Provides toggles for: preserved completed entries, AniList prioritization, progress sync strategy, score handling, privacy settings, and auto-pause configuration with custom threshold support.
Features: