Files
k-tv/k-tv-frontend/app/(main)/dashboard/components/webhook-editor.tsx
Gabriel Kaszewski 8ed8da2d90 refactor(frontend): extract logic to hooks, split inline components
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
2026-03-17 02:25:02 +01:00

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>
);
}