diff --git a/k-tv-frontend/app/(main)/guide/page.tsx b/k-tv-frontend/app/(main)/guide/page.tsx new file mode 100644 index 0000000..5639ecb --- /dev/null +++ b/k-tv-frontend/app/(main)/guide/page.tsx @@ -0,0 +1,195 @@ +"use client"; + +import Link from "next/link"; +import { useQuery } from "@tanstack/react-query"; +import { Tv } from "lucide-react"; +import { api } from "@/lib/api"; +import { useChannels } from "@/hooks/use-channels"; +import { useAuthContext } from "@/context/auth-context"; +import type { ChannelResponse, ScheduledSlotResponse } from "@/lib/types"; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function fmtTime(iso: string) { + return new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); +} + +function fmtDuration(secs: number) { + const h = Math.floor(secs / 3600); + const m = Math.floor((secs % 3600) / 60); + return h > 0 ? `${h}h ${m}m` : `${m}m`; +} + +function slotLabel(slot: ScheduledSlotResponse) { + const { item } = slot; + if (item.content_type === "episode" && item.series_name) { + const ep = [ + item.season_number != null ? `S${item.season_number}` : "", + item.episode_number != null ? `E${String(item.episode_number).padStart(2, "0")}` : "", + ] + .filter(Boolean) + .join(""); + return ep + ? `${item.series_name} ${ep} – ${item.title}` + : `${item.series_name} – ${item.title}`; + } + return item.year ? `${item.title} (${item.year})` : item.title; +} + +// --------------------------------------------------------------------------- +// ChannelRow — fetches its own EPG slice and renders current + upcoming +// --------------------------------------------------------------------------- + +function ChannelRow({ channel }: { channel: ChannelResponse }) { + const { token } = useAuthContext(); + + const { data: slots, isError, isPending } = useQuery({ + queryKey: ["guide-epg", channel.id], + queryFn: () => { + const now = new Date(); + const from = now.toISOString(); + const until = new Date(now.getTime() + 4 * 60 * 60 * 1000).toISOString(); + return api.schedule.getEpg(channel.id, token ?? "", from, until); + }, + refetchInterval: 30_000, + retry: false, + }); + + const now = Date.now(); + + const current = slots?.find( + (s) => + new Date(s.start_at).getTime() <= now && now < new Date(s.end_at).getTime(), + ); + const upcoming = slots?.filter((s) => new Date(s.start_at).getTime() > now).slice(0, 3) ?? []; + + const progress = current + ? (now - new Date(current.start_at).getTime()) / + (new Date(current.end_at).getTime() - new Date(current.start_at).getTime()) + : 0; + + const remaining = current + ? Math.ceil((new Date(current.end_at).getTime() - now) / 60_000) + : 0; + + return ( +
Loading…
+ ) : ( ++ {isError || !slots?.length + ? "No signal — schedule may not be generated yet" + : "Nothing airing right now"} +
+ )} + + {/* Upcoming */} + {upcoming.length > 0 && ( +Loading channels…
} + + {!isLoading && channels?.length === 0 && ( ++ No channels yet.{" "} + + Create one in the dashboard. + +
+ )} + +