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
This commit is contained in:
141
k-tv-frontend/app/(main)/dashboard/components/webhook-editor.tsx
Normal file
141
k-tv-frontend/app/(main)/dashboard/components/webhook-editor.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user