"use client"; import { useState, useTransition } from "react"; import type { ShardOp } from "yt-dlp-transcript-common/controller/shard"; import { clearShardConfigAction, saveShardConfigAction, } from "../channels/[slug]/shardActions"; export type ShardConfigSummary = { totalShards: number; shardIndex: number; itemCount: number; createdAt: string; }; type Props = { slug: string; op: ShardOp; actionLabel: string; existing: ShardConfigSummary | null; total: string; onTotalChange: (next: string) => void; index: string; onIndexChange: (next: string) => void; }; export function ShardControl({ slug, op, actionLabel, existing, total, onTotalChange, index, onIndexChange, }: Props) { const [clearing, startTransition] = useTransition(); const [saving, startSaveTransition] = useTransition(); const [error, setError] = useState(null); const parsedTotal = parseShardField(total); // The index input is 1-based for humans ("shard 1 of 2"); parseShardIndexField // converts it to the 0-based index the slicing math uses. const parsedIndex = parseShardIndexField(index); const inputsValid = parsedTotal !== undefined && parsedIndex !== undefined && parsedTotal >= 1 && parsedIndex < parsedTotal; function handleClear() { setError(null); startTransition(async () => { const r = await clearShardConfigAction(slug, op); if (!r.ok) { setError(r.error); return; } }); } // Persist the slice without running the job (the "Save shard" probe). function handleSave() { setError(null); if (!inputsValid) { setError( "Enter a shard first: a number from 1 to the total (e.g. 1 / 2).", ); return; } startSaveTransition(async () => { const r = await saveShardConfigAction(slug, op, parsedTotal, parsedIndex); if (!r.ok) { setError(r.error); return; } }); } // Aria-labels intentionally key on `op` (a stable slug) instead of // actionLabel so they don't substring-match the action button name // (e.g. "Transcribe missing") and break role-based locators in tests. return ( Shard onIndexChange(e.target.value)} className="w-14 font-mono px-2 py-1 rounded border border-border bg-card text-foreground" /> / onTotalChange(e.target.value)} className="w-14 font-mono px-2 py-1 rounded border border-border bg-card text-foreground" /> {existing ? ( saved: {existing.shardIndex + 1}/{existing.totalShards} ({existing.itemCount} items) ) : ( (no saved shard) )} {error && ( {error} )} ); } export function parseShardField(s: string): number | undefined { const trimmed = s.trim(); if (trimmed === "") return undefined; const n = Number(trimmed); if (!Number.isFinite(n) || n < 0) return undefined; return Math.floor(n); } // The shard index input is shown 1-based ("shard 1 of 2"). Convert it to the // 0-based index used by computeShardSlice / the server actions. Returns // undefined for blank or sub-1 input (0 is not a valid 1-based shard number). export function parseShardIndexField(s: string): number | undefined { const n = parseShardField(s); if (n === undefined || n < 1) return undefined; return n - 1; }