Files
k-tv/k-tv-frontend/app/(main)/dashboard/components/recycle-policy-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

116 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { RecyclePolicy } from "@/lib/types";
import type { FieldErrors } from "@/lib/schemas";
function NumberInput({
value,
onChange,
min,
max,
step,
placeholder,
error,
}: {
value: number | "";
onChange: (v: number | "") => void;
min?: number;
max?: number;
step?: number | "any";
placeholder?: string;
error?: boolean;
}) {
return (
<input
type="number"
min={min}
max={max}
step={step}
value={value}
placeholder={placeholder}
onChange={(e) =>
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 Field({
label,
hint,
error,
children,
}: {
label: string;
hint?: string;
error?: string;
children: React.ReactNode;
}) {
return (
<div className="space-y-1.5">
<label className="block text-xs font-medium text-zinc-400">{label}</label>
{children}
{error ? (
<p className="text-[11px] text-red-400">{error}</p>
) : hint ? (
<p className="text-[11px] text-zinc-600">{hint}</p>
) : null}
</div>
);
}
interface RecyclePolicyEditorProps {
policy: RecyclePolicy;
errors: FieldErrors;
onChange: (policy: RecyclePolicy) => void;
}
export function RecyclePolicyEditor({
policy,
errors,
onChange,
}: RecyclePolicyEditorProps) {
return (
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<Field label="Cooldown (days)" hint="Don't replay within N days">
<NumberInput
value={policy.cooldown_days ?? ""}
onChange={(v) =>
onChange({ ...policy, cooldown_days: v === "" ? null : (v as number) })
}
min={0}
placeholder="7"
/>
</Field>
<Field label="Cooldown (generations)" hint="Don't replay within N schedules">
<NumberInput
value={policy.cooldown_generations ?? ""}
onChange={(v) =>
onChange({ ...policy, cooldown_generations: v === "" ? null : (v as number) })
}
min={0}
placeholder="3"
/>
</Field>
</div>
<Field
label="Min available ratio"
hint="0.01.0 · Fraction of the pool kept selectable even if cooldown is active"
error={errors["recycle_policy.min_available_ratio"]}
>
<NumberInput
value={policy.min_available_ratio}
onChange={(v) =>
onChange({ ...policy, min_available_ratio: v === "" ? 0.1 : (v as number) })
}
min={0}
max={1}
step={0.01}
placeholder="0.1"
error={!!errors["recycle_policy.min_available_ratio"]}
/>
</Field>
</div>
);
}