"use client";
import { useState } from "react";
import { Trash2, Plus } from "lucide-react";
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { Button } from "@/components/ui/button";
import { BlockTimeline, BLOCK_COLORS, minsToTime } from "./block-timeline";
import { AlgorithmicFilterEditor } from "./algorithmic-filter-editor";
import { RecyclePolicyEditor } from "./recycle-policy-editor";
import { WebhookEditor } from "./webhook-editor";
import { AccessSettingsEditor } from "./access-settings-editor";
import { LogoEditor } from "./logo-editor";
import { useChannelForm } from "@/hooks/use-channel-form";
import { channelFormSchema, extractErrors } from "@/lib/schemas";
import type { FieldErrors } from "@/lib/schemas";
import type {
AccessMode,
ChannelResponse,
LogoPosition,
ProgrammingBlock,
BlockContent,
FillStrategy,
MediaFilter,
ProviderInfo,
RecyclePolicy,
} from "@/lib/types";
// ---------------------------------------------------------------------------
// Local shared primitives (only used inside this file)
// ---------------------------------------------------------------------------
function Field({
label,
hint,
error,
children,
}: {
label: string;
hint?: string;
error?: string;
children: React.ReactNode;
}) {
return (
{children}
{error ? (
{error}
) : hint ? (
{hint}
) : null}
);
}
function TextInput({
value,
onChange,
placeholder,
required,
error,
}: {
value: string;
onChange: (v: string) => void;
placeholder?: string;
required?: boolean;
error?: boolean;
}) {
return (
onChange(e.target.value)}
placeholder={placeholder}
className={`w-full rounded-md border bg-zinc-800 px-3 py-2 text-sm text-zinc-100 placeholder:text-zinc-600 focus:outline-none ${error ? "border-red-500 focus:border-red-400" : "border-zinc-700 focus:border-zinc-500"}`}
/>
);
}
function NumberInput({
value,
onChange,
min,
error,
}: {
value: number | "";
onChange: (v: number | "") => void;
min?: number;
error?: boolean;
}) {
return (
onChange(e.target.value === "" ? "" : Number(e.target.value))
}
className={`w-full rounded-md border bg-zinc-800 px-3 py-2 text-sm text-zinc-100 placeholder:text-zinc-600 focus:outline-none ${error ? "border-red-500 focus:border-red-400" : "border-zinc-700 focus:border-zinc-500"}`}
/>
);
}
function NativeSelect({
value,
onChange,
children,
}: {
value: string;
onChange: (v: string) => void;
children: React.ReactNode;
}) {
return (
);
}
function defaultFilter(): MediaFilter {
return {
content_type: null,
genres: [],
decade: null,
tags: [],
min_duration_secs: null,
max_duration_secs: null,
collections: [],
series_names: [],
search_term: null,
};
}
// ---------------------------------------------------------------------------
// BlockEditor — inline because it's only used here and depends on local types
// ---------------------------------------------------------------------------
interface BlockEditorProps {
block: ProgrammingBlock;
index: number;
errors: FieldErrors;
providers: ProviderInfo[];
onChange: (block: ProgrammingBlock) => void;
}
function BlockEditor({ block, index, errors, providers, onChange }: BlockEditorProps) {
const setField = (key: K, value: ProgrammingBlock[K]) =>
onChange({ ...block, [key]: value });
const content = block.content;
const pfx = `blocks.${index}`;
const setContentType = (type: "algorithmic" | "manual") => {
const pid = content.provider_id ?? "";
onChange({
...block,
content:
type === "algorithmic"
? { type: "algorithmic", filter: defaultFilter(), strategy: "random", provider_id: pid }
: { type: "manual", items: [], provider_id: pid },
});
};
const setFilter = (patch: Partial) => {
if (content.type !== "algorithmic") return;
onChange({ ...block, content: { ...content, filter: { ...content.filter, ...patch } } });
};
const setStrategy = (strategy: FillStrategy) => {
if (content.type !== "algorithmic") return;
onChange({ ...block, content: { ...content, strategy } });
};
const setProviderId = (id: string) =>
onChange({ ...block, content: { ...content, provider_id: id } });
return (
setField("name", v)}
placeholder="Evening Sitcoms"
error={!!errors[`${pfx}.name`]}
/>
setContentType(v as "algorithmic" | "manual")}
>
setField("start_time", e.target.value + ":00")}
className="w-full rounded-md border border-zinc-700 bg-zinc-800 px-3 py-2 text-sm text-zinc-100 focus:border-zinc-500 focus:outline-none"
/>
setField("duration_mins", v === "" ? 60 : v)}
min={1}
error={!!errors[`${pfx}.duration_mins`]}
/>
{content.type === "algorithmic" && (
<>
{content.strategy === "sequential" && (
)}
>
)}
{content.type === "manual" && (
)}
Block access
onChange({ ...block, access_mode: m })}
onAccessPasswordChange={(pw) => onChange({ ...block, access_password: pw })}
label="Access mode"
passwordLabel="Block password"
passwordHint="Leave blank to keep existing"
/>
);
}
// ---------------------------------------------------------------------------
// EditChannelSheet
// ---------------------------------------------------------------------------
interface EditChannelSheetProps {
channel: ChannelResponse | null;
open: boolean;
onOpenChange: (open: boolean) => void;
onSubmit: (
id: string,
data: {
name: string;
description: string;
timezone: string;
schedule_config: { blocks: ProgrammingBlock[] };
recycle_policy: RecyclePolicy;
auto_schedule: boolean;
access_mode?: AccessMode;
access_password?: string;
logo?: string | null;
logo_position?: LogoPosition;
logo_opacity?: number;
webhook_url?: string | null;
webhook_poll_interval_secs?: number;
webhook_body_template?: string | null;
webhook_headers?: string | null;
},
) => void;
isPending: boolean;
error?: string | null;
providers?: ProviderInfo[];
}
export function EditChannelSheet({
channel,
open,
onOpenChange,
onSubmit,
isPending,
error,
providers = [],
}: EditChannelSheetProps) {
const form = useChannelForm(channel);
const [fieldErrors, setFieldErrors] = useState({});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!channel) return;
const result = channelFormSchema.safeParse({
name: form.name,
description: form.description,
timezone: form.timezone,
blocks: form.blocks,
recycle_policy: form.recyclePolicy,
auto_schedule: form.autoSchedule,
access_mode: form.accessMode,
access_password: form.accessPassword,
});
if (!result.success) {
setFieldErrors(extractErrors(result.error));
return;
}
setFieldErrors({});
onSubmit(channel.id, {
name: form.name,
description: form.description,
timezone: form.timezone,
schedule_config: { blocks: form.blocks },
recycle_policy: form.recyclePolicy,
auto_schedule: form.autoSchedule,
access_mode: form.accessMode !== "public" ? form.accessMode : "public",
access_password: form.accessPassword || "",
logo: form.logo,
logo_position: form.logoPosition,
logo_opacity: form.logoOpacity / 100,
webhook_url: form.webhookUrl || null,
...(form.webhookUrl
? {
webhook_poll_interval_secs:
form.webhookPollInterval === "" ? 5 : form.webhookPollInterval,
}
: {}),
webhook_body_template: form.webhookBodyTemplate || null,
webhook_headers: form.webhookHeaders || null,
});
};
return (
Edit channel
);
}