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:
79
k-tv-frontend/lib/schemas.ts
Normal file
79
k-tv-frontend/lib/schemas.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const mediaFilterSchema = z.object({
|
||||
content_type: z.enum(["movie", "episode", "short"]).nullable().optional(),
|
||||
genres: z.array(z.string()),
|
||||
decade: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1900, "Decade must be ≥ 1900")
|
||||
.max(2099, "Decade must be ≤ 2090")
|
||||
.nullable()
|
||||
.optional(),
|
||||
tags: z.array(z.string()),
|
||||
min_duration_secs: z.number().min(0, "Must be ≥ 0").nullable().optional(),
|
||||
max_duration_secs: z.number().min(0, "Must be ≥ 0").nullable().optional(),
|
||||
collections: z.array(z.string()),
|
||||
series_names: z.array(z.string()),
|
||||
search_term: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export const accessModeSchema = z.enum([
|
||||
"public",
|
||||
"password_protected",
|
||||
"account_required",
|
||||
"owner_only",
|
||||
]);
|
||||
|
||||
export const blockSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().min(1, "Block name is required"),
|
||||
start_time: z.string(),
|
||||
duration_mins: z.number().int().min(1, "Must be at least 1 minute"),
|
||||
content: z.discriminatedUnion("type", [
|
||||
z.object({
|
||||
type: z.literal("algorithmic"),
|
||||
filter: mediaFilterSchema,
|
||||
strategy: z.enum(["best_fit", "sequential", "random"]),
|
||||
provider_id: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("manual"),
|
||||
items: z.array(z.string()),
|
||||
provider_id: z.string().optional(),
|
||||
}),
|
||||
]),
|
||||
loop_on_finish: z.boolean().optional(),
|
||||
ignore_recycle_policy: z.boolean().optional(),
|
||||
access_mode: accessModeSchema.optional(),
|
||||
access_password: z.string().optional(),
|
||||
});
|
||||
|
||||
export const channelFormSchema = z.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
timezone: z.string().min(1, "Timezone is required"),
|
||||
description: z.string().optional(),
|
||||
blocks: z.array(blockSchema),
|
||||
recycle_policy: z.object({
|
||||
cooldown_days: z.number().int().min(0).nullable().optional(),
|
||||
cooldown_generations: z.number().int().min(0).nullable().optional(),
|
||||
min_available_ratio: z
|
||||
.number()
|
||||
.min(0, "Must be ≥ 0")
|
||||
.max(1, "Must be ≤ 1"),
|
||||
}),
|
||||
auto_schedule: z.boolean(),
|
||||
access_mode: accessModeSchema.optional(),
|
||||
access_password: z.string().optional(),
|
||||
});
|
||||
|
||||
export type FieldErrors = Record<string, string | undefined>;
|
||||
|
||||
export function extractErrors(err: z.ZodError): FieldErrors {
|
||||
const map: FieldErrors = {};
|
||||
for (const issue of err.issues) {
|
||||
const key = issue.path.join(".");
|
||||
if (!map[key]) map[key] = issue.message;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
Reference in New Issue
Block a user