Area 1 (tv/page.tsx 964→423 lines): - hooks: use-fullscreen, use-idle, use-volume, use-quality, use-subtitles, use-channel-input, use-channel-passwords, use-tv-keyboard - components: SubtitlePicker, VolumeControl, QualityPicker, TopControlBar, LogoWatermark, AutoplayPrompt, ChannelNumberOverlay, TvBaseLayer Area 2 (edit-channel-sheet.tsx 1244→678 lines): - hooks: use-channel-form (all form state + reset logic) - lib/schemas.ts: extracted Zod schemas + extractErrors - components: AlgorithmicFilterEditor, RecyclePolicyEditor, WebhookEditor, AccessSettingsEditor, LogoEditor Area 3 (dashboard/page.tsx 406→261 lines): - hooks: use-channel-order, use-import-channel, use-regenerate-all - lib/channel-export.ts: pure export utility - components: DashboardHeader
142 lines
5.0 KiB
TypeScript
142 lines
5.0 KiB
TypeScript
import type { WebhookFormat } from "@/hooks/use-channel-form";
|
|
import { WEBHOOK_PRESETS } from "@/hooks/use-channel-form";
|
|
|
|
function Field({
|
|
label,
|
|
hint,
|
|
children,
|
|
}: {
|
|
label: string;
|
|
hint?: string;
|
|
children: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<div className="space-y-1.5">
|
|
<label className="block text-xs font-medium text-zinc-400">{label}</label>
|
|
{children}
|
|
{hint && <p className="text-[11px] text-zinc-600">{hint}</p>}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface WebhookEditorProps {
|
|
webhookUrl: string;
|
|
webhookPollInterval: number | "";
|
|
webhookFormat: WebhookFormat;
|
|
webhookBodyTemplate: string;
|
|
webhookHeaders: string;
|
|
onWebhookUrlChange: (v: string) => void;
|
|
onWebhookPollIntervalChange: (v: number | "") => void;
|
|
onWebhookFormatChange: (fmt: WebhookFormat) => void;
|
|
onWebhookBodyTemplateChange: (v: string) => void;
|
|
onWebhookHeadersChange: (v: string) => void;
|
|
}
|
|
|
|
export function WebhookEditor({
|
|
webhookUrl,
|
|
webhookPollInterval,
|
|
webhookFormat,
|
|
webhookBodyTemplate,
|
|
webhookHeaders,
|
|
onWebhookUrlChange,
|
|
onWebhookPollIntervalChange,
|
|
onWebhookFormatChange,
|
|
onWebhookBodyTemplateChange,
|
|
onWebhookHeadersChange,
|
|
}: WebhookEditorProps) {
|
|
return (
|
|
<div className="space-y-3">
|
|
<Field label="Webhook URL" hint="POST events to this URL on broadcast changes">
|
|
<input
|
|
value={webhookUrl}
|
|
onChange={(e) => onWebhookUrlChange(e.target.value)}
|
|
placeholder="https://example.com/webhook"
|
|
className="w-full rounded-md border border-zinc-700 bg-zinc-800 px-3 py-2 text-sm text-zinc-100 placeholder:text-zinc-600 focus:border-zinc-500 focus:outline-none"
|
|
/>
|
|
</Field>
|
|
|
|
{webhookUrl && (
|
|
<>
|
|
<Field
|
|
label="Poll interval (seconds)"
|
|
hint="How often to check for broadcast changes"
|
|
>
|
|
<input
|
|
type="number"
|
|
min={1}
|
|
value={webhookPollInterval}
|
|
placeholder="5"
|
|
onChange={(e) =>
|
|
onWebhookPollIntervalChange(
|
|
e.target.value === "" ? "" : Number(e.target.value),
|
|
)
|
|
}
|
|
className="w-full rounded-md border border-zinc-700 bg-zinc-800 px-3 py-2 text-sm text-zinc-100 placeholder:text-zinc-600 focus:border-zinc-500 focus:outline-none"
|
|
/>
|
|
</Field>
|
|
|
|
<div className="space-y-2">
|
|
<p className="text-xs text-zinc-400">Payload format</p>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{(["default", "discord", "slack", "custom"] as WebhookFormat[]).map(
|
|
(fmt) => (
|
|
<button
|
|
key={fmt}
|
|
type="button"
|
|
onClick={() => {
|
|
onWebhookFormatChange(fmt);
|
|
if (fmt === "discord")
|
|
onWebhookBodyTemplateChange(WEBHOOK_PRESETS.discord);
|
|
else if (fmt === "slack")
|
|
onWebhookBodyTemplateChange(WEBHOOK_PRESETS.slack);
|
|
else if (fmt === "default") onWebhookBodyTemplateChange("");
|
|
}}
|
|
className={`rounded px-2.5 py-1 text-xs font-medium capitalize transition-colors ${
|
|
webhookFormat === fmt
|
|
? "bg-zinc-600 text-zinc-100"
|
|
: "bg-zinc-800 text-zinc-400 hover:bg-zinc-700 hover:text-zinc-200"
|
|
}`}
|
|
>
|
|
{fmt === "default" ? "K-TV default" : fmt}
|
|
</button>
|
|
),
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{webhookFormat !== "default" && (
|
|
<Field
|
|
label="Body template (Handlebars)"
|
|
hint="Context: event, timestamp, channel_id, data.item.title, data.item.duration_secs, …"
|
|
>
|
|
<textarea
|
|
rows={6}
|
|
value={webhookBodyTemplate}
|
|
onChange={(e) => {
|
|
onWebhookBodyTemplateChange(e.target.value);
|
|
onWebhookFormatChange("custom");
|
|
}}
|
|
placeholder={'{\n "text": "Now playing: {{data.item.title}}"\n}'}
|
|
className="w-full resize-y rounded-md border border-zinc-700 bg-zinc-800 px-3 py-2 font-mono text-xs text-zinc-100 placeholder:text-zinc-600 focus:border-zinc-500 focus:outline-none"
|
|
/>
|
|
</Field>
|
|
)}
|
|
|
|
<Field
|
|
label="Extra headers (JSON)"
|
|
hint={'e.g. {"Authorization": "Bearer token"}'}
|
|
>
|
|
<textarea
|
|
rows={2}
|
|
value={webhookHeaders}
|
|
onChange={(e) => onWebhookHeadersChange(e.target.value)}
|
|
placeholder={'{"Authorization": "Bearer xxx"}'}
|
|
className="w-full resize-none rounded-md border border-zinc-700 bg-zinc-800 px-3 py-2 font-mono text-xs text-zinc-100 placeholder:text-zinc-600 focus:border-zinc-500 focus:outline-none"
|
|
/>
|
|
</Field>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|