"use client"; import { useState, useEffect } from "react"; import { Plus, Upload, RefreshCw, Antenna } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useChannels, useCreateChannel, useUpdateChannel, useDeleteChannel, useGenerateSchedule, } from "@/hooks/use-channels"; import { useAuthContext } from "@/context/auth-context"; import { useConfig } from "@/hooks/use-config"; import { useRescanLibrary } from "@/hooks/use-library"; import { api } from "@/lib/api"; import { toast } from "sonner"; import { useQueryClient } from "@tanstack/react-query"; import { ChannelCard } from "./components/channel-card"; import { CreateChannelDialog } from "./components/create-channel-dialog"; import { DeleteChannelDialog } from "./components/delete-channel-dialog"; import { EditChannelSheet } from "./components/edit-channel-sheet"; import { ScheduleSheet } from "./components/schedule-sheet"; import { ImportChannelDialog, type ChannelImportData, } from "./components/import-channel-dialog"; import { IptvExportDialog } from "./components/iptv-export-dialog"; import type { ChannelResponse, ProgrammingBlock, RecyclePolicy, } from "@/lib/types"; export default function DashboardPage() { const { token } = useAuthContext(); const queryClient = useQueryClient(); const { data: channels, isLoading, error } = useChannels(); const { data: config } = useConfig(); const capabilities = config?.provider_capabilities; const createChannel = useCreateChannel(); const updateChannel = useUpdateChannel(); const deleteChannel = useDeleteChannel(); const generateSchedule = useGenerateSchedule(); const rescanLibrary = useRescanLibrary(); // Channel ordering — persisted to localStorage const [channelOrder, setChannelOrder] = useState([]); useEffect(() => { try { const stored = localStorage.getItem("k-tv-channel-order"); if (stored) setChannelOrder(JSON.parse(stored)); } catch {} }, []); const saveOrder = (order: string[]) => { setChannelOrder(order); try { localStorage.setItem("k-tv-channel-order", JSON.stringify(order)); } catch {} }; // Sort channels by stored order; new channels appear at the end const sortedChannels = channels ? [...channels].sort((a, b) => { const ai = channelOrder.indexOf(a.id); const bi = channelOrder.indexOf(b.id); if (ai === -1 && bi === -1) return 0; if (ai === -1) return 1; if (bi === -1) return -1; return ai - bi; }) : []; const handleMoveUp = (channelId: string) => { const ids = sortedChannels.map((c) => c.id); const idx = ids.indexOf(channelId); if (idx <= 0) return; const next = [...ids]; [next[idx - 1], next[idx]] = [next[idx], next[idx - 1]]; saveOrder(next); }; const handleMoveDown = (channelId: string) => { const ids = sortedChannels.map((c) => c.id); const idx = ids.indexOf(channelId); if (idx === -1 || idx >= ids.length - 1) return; const next = [...ids]; [next[idx], next[idx + 1]] = [next[idx + 1], next[idx]]; saveOrder(next); }; // Regenerate all channels const [isRegeneratingAll, setIsRegeneratingAll] = useState(false); const handleRegenerateAll = async () => { if (!token || !channels || channels.length === 0) return; setIsRegeneratingAll(true); let failed = 0; for (const ch of channels) { try { await api.schedule.generate(ch.id, token); queryClient.invalidateQueries({ queryKey: ["schedule", ch.id] }); } catch { failed++; } } setIsRegeneratingAll(false); if (failed === 0) toast.success(`All ${channels.length} schedules regenerated`); else toast.error(`${failed} schedule(s) failed to generate`); }; const [iptvOpen, setIptvOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false); const [importOpen, setImportOpen] = useState(false); const [importPending, setImportPending] = useState(false); const [importError, setImportError] = useState(null); const [editChannel, setEditChannel] = useState(null); const [deleteTarget, setDeleteTarget] = useState( null, ); const [scheduleChannel, setScheduleChannel] = useState(null); const handleCreate = (data: { name: string; timezone: string; description: string; access_mode?: import("@/lib/types").AccessMode; access_password?: string; }) => { createChannel.mutate( { name: data.name, timezone: data.timezone, description: data.description || undefined, access_mode: data.access_mode, access_password: data.access_password, }, { onSuccess: () => setCreateOpen(false) }, ); }; const handleEdit = ( id: string, data: { name: string; description: string; timezone: string; schedule_config: { blocks: ProgrammingBlock[] }; recycle_policy: RecyclePolicy; auto_schedule: boolean; access_mode?: import("@/lib/types").AccessMode; access_password?: string; logo?: string | null; logo_position?: import("@/lib/types").LogoPosition; logo_opacity?: number; }, ) => { updateChannel.mutate( { id, data }, { onSuccess: () => setEditChannel(null) }, ); }; const handleImport = async (data: ChannelImportData) => { if (!token) return; setImportPending(true); setImportError(null); try { const created = await api.channels.create( { name: data.name, timezone: data.timezone, description: data.description, }, token, ); await api.channels.update( created.id, { schedule_config: { blocks: data.blocks }, recycle_policy: data.recycle_policy, }, token, ); await queryClient.invalidateQueries({ queryKey: ["channels"] }); setImportOpen(false); } catch (e) { setImportError(e instanceof Error ? e.message : "Import failed"); } finally { setImportPending(false); } }; const handleExport = (channel: ChannelResponse) => { const payload = { name: channel.name, description: channel.description ?? undefined, timezone: channel.timezone, blocks: channel.schedule_config.blocks, recycle_policy: channel.recycle_policy, }; const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json", }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${channel.name.toLowerCase().replace(/\s+/g, "-")}.json`; a.click(); URL.revokeObjectURL(url); }; const handleDelete = () => { if (!deleteTarget) return; deleteChannel.mutate(deleteTarget.id, { onSuccess: () => setDeleteTarget(null), }); }; return (
{/* Header */}

My Channels

Build your broadcast lineup

{capabilities?.rescan && ( )} {channels && channels.length > 0 && ( )}
{/* Content */} {isLoading && (
)} {error && (
{error.message}
)} {!isLoading && channels && channels.length === 0 && (

No channels yet

)} {sortedChannels.length > 0 && (
{sortedChannels.map((channel, idx) => ( setEditChannel(channel)} onDelete={() => setDeleteTarget(channel)} onGenerateSchedule={() => generateSchedule.mutate(channel.id)} onViewSchedule={() => setScheduleChannel(channel)} onExport={() => handleExport(channel)} onMoveUp={() => handleMoveUp(channel.id)} onMoveDown={() => handleMoveDown(channel.id)} /> ))}
)} {/* Dialogs / sheets */} {token && ( )} { if (!open) { setImportOpen(false); setImportError(null); } }} onSubmit={handleImport} isPending={importPending} error={importError} /> { if (!open) setEditChannel(null); }} onSubmit={handleEdit} isPending={updateChannel.isPending} error={updateChannel.error?.message} capabilities={capabilities} /> { if (!open) setScheduleChannel(null); }} /> {deleteTarget && ( { if (!open) setDeleteTarget(null); }} onConfirm={handleDelete} isPending={deleteChannel.isPending} /> )}
); }