import { useState } from "react"; import type { FormEvent } from "react"; import type { ImportUserMDBListCollectionRequest, ImportUserTMDBCollectionRequest, ImportUserTraktCollectionRequest, UserCollectionMediaFilter, UserCollectionSyncSchedule, UserCollectionWatchFilter, } from "@/api/types"; import { useImportUserMDBListCollection, useImportUserTMDBCollection, useImportUserTraktCollection, } from "@/hooks/queries/userCollectionImports"; import { useUserLibraries } from "@/hooks/queries/libraries"; import { COLLECTION_MEDIA_FILTER_OPTIONS, COLLECTION_WATCH_FILTER_OPTIONS, displayFiltersToQueryDefinition, } from "@/lib/collectionDisplayFilters"; import { COLLECTION_MAX_ITEMS, libraryEligibilityForMediaKind, mediaKindLabel, } from "@/lib/collectionTemplates"; import type { CollectionTemplate } from "@/lib/collectionTemplates"; import { CollectionLibraryPicker, parseOptionalPositiveInteger, } from "@/pages/adminCollectionsShared"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { MDBListBrowser } from "./MDBListBrowser"; import { TemplatePosterField, type TemplatePosterMode } from "./TemplatePosterField"; interface Props { template: CollectionTemplate; onCancel: () => void; onCreated: () => void; } // MANUAL_SCHEDULE is the in-form sentinel for "no automatic refresh". Radix // Select disallows empty-string SelectItem values, so we map this to "" only // at submit time when populating the request body. const MANUAL_SCHEDULE = "none" as const; type ScheduleChoice = typeof MANUAL_SCHEDULE | Exclude; // SCHEDULE_OPTIONS is the user-allowed cadence set. It mirrors the // usercollections.AllowedSyncSchedules map on the backend, which caps user // schedules to >= 24h to keep TMDB/Trakt API quota usage bounded. const SCHEDULE_OPTIONS: Array<{ value: ScheduleChoice; label: string }> = [ { value: MANUAL_SCHEDULE, label: "Manual only" }, { value: "daily", label: "Daily" }, { value: "weekly", label: "Weekly" }, { value: "monthly", label: "Monthly" }, ]; function scheduleChoiceToRequest(choice: ScheduleChoice): UserCollectionSyncSchedule { return choice === MANUAL_SCHEDULE ? "" : choice; } // templateDefaultSchedule maps the template's built-in cron into the closest // user-allowed cadence. Templates that prescribe a sub-daily cadence get // downgraded to "daily" — users cannot opt into shorter intervals here. function templateDefaultSchedule(cron: string | undefined): ScheduleChoice { if (!cron) return MANUAL_SCHEDULE; const fields = cron.trim().split(/\s+/); if (fields.length !== 5) return "daily"; const [, , dom, month, dow] = fields; if (dom === "1" && month === "*") return "monthly"; if (dom === "*" && month === "*" && dow !== "*") return "weekly"; return "daily"; } export function UserCollectionTemplateConfigForm({ template, onCancel, onCreated }: Props) { const tmdbMutation = useImportUserTMDBCollection(); const traktMutation = useImportUserTraktCollection(); const mdblistMutation = useImportUserMDBListCollection(); const { data: libraries = [] } = useUserLibraries(); const [title, setTitle] = useState(template.title); const [description, setDescription] = useState(template.description); const [limit, setLimit] = useState(template.default_limit ? String(template.default_limit) : ""); const [schedule, setSchedule] = useState( templateDefaultSchedule(template.default_sync_schedule), ); const [isShared, setIsShared] = useState(false); const [mdblistUrl, setMdblistUrl] = useState(template.mdblist?.url ?? ""); const [libraryIds, setLibraryIds] = useState([]); const [watchFilter, setWatchFilter] = useState("all"); const [mediaFilter, setMediaFilter] = useState("all"); const [posterMode, setPosterMode] = useState(() => template.poster_path ? "default" : "custom", ); const [customPosterUrl, setCustomPosterUrl] = useState(""); const eligibility = libraryEligibilityForMediaKind(template.media_kind); const pickerLibraries = libraries.map((lib) => ({ id: lib.id, name: lib.name, type: lib.type, })); const parsedLimit = parseOptionalPositiveInteger(limit); const limitInvalid = limit.trim().length > 0 && parsedLimit === undefined; const isPending = tmdbMutation.isPending || traktMutation.isPending || mdblistMutation.isPending; const missingMDBListURL = template.source === "mdblist" && mdblistUrl.trim().length === 0; const submitDisabled = isPending || limitInvalid || missingMDBListURL; function handleSubmit(event: FormEvent) { event.preventDefault(); if (submitDisabled) return; const sharedFields = { title: title.trim() || template.title, description: description.trim() || undefined, sync_schedule: scheduleChoiceToRequest(schedule), limit: parsedLimit, is_shared: isShared, poster_url: posterMode === "custom" ? customPosterUrl.trim() || undefined : template.poster_path || undefined, library_ids: libraryIds.length > 0 ? libraryIds : undefined, display_query_definition: displayFiltersToQueryDefinition(watchFilter, mediaFilter), }; if (template.source === "tmdb" && template.tmdb) { const body: ImportUserTMDBCollectionRequest = { ...sharedFields, preset: template.tmdb.preset, media_type: template.tmdb.media_type, time_window: template.tmdb.time_window, }; tmdbMutation.mutate(body, { onSuccess: onCreated }); return; } if (template.source === "trakt" && template.trakt) { const body: ImportUserTraktCollectionRequest = { ...sharedFields, preset: template.trakt.preset, media_type: template.trakt.media_type, }; traktMutation.mutate(body, { onSuccess: onCreated }); return; } if (template.source === "mdblist") { const body: ImportUserMDBListCollectionRequest = { ...sharedFields, url: mdblistUrl.trim(), }; mdblistMutation.mutate(body, { onSuccess: onCreated }); return; } } return (
{template.icon}
{template.title} {template.source} {mediaKindLabel(template.media_kind)}

{template.description}

setTitle(event.target.value)} required />
setDescription(event.target.value)} placeholder="Optional summary" />
{template.source === "mdblist" ? (
{ setMdblistUrl(jsonURL); if (!title.trim() || title.trim() === template.title) { setTitle(list.name); } }} />
setMdblistUrl(event.target.value)} placeholder="https://mdblist.com/lists/user/slug" required />

Pick a list above or paste any public MDBList list URL (with or without{" "} /json). Items resolve via TMDB/IMDb/TVDB IDs.

) : null}

Leave empty to span every library you can see.

Uses the active profile’s watched state. Shared profiles may see different results.

{template.requires_profile ? (

This template uses your active profile’s connected Trakt account.

) : null}
setLimit(event.target.value)} placeholder={template.default_limit ? String(template.default_limit) : "No limit"} />

Share with other profiles

When on, profiles you choose can browse this collection too.

); }