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
80 lines
2.4 KiB
TypeScript
80 lines
2.4 KiB
TypeScript
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;
|
|
}
|