feat: implement configuration management and enhance user registration flow

This commit is contained in:
2026-03-11 22:26:16 +01:00
parent 62549faffa
commit 0f1b9c11fe
12 changed files with 370 additions and 95 deletions

View File

@@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
import { Plus, Upload } from "lucide-react";
import { useState, useEffect } from "react";
import { Plus, Upload, RefreshCw } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
useChannels,
@@ -12,6 +12,7 @@ import {
} from "@/hooks/use-channels";
import { useAuthContext } from "@/context/auth-context";
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";
@@ -31,6 +32,69 @@ export default function DashboardPage() {
const deleteChannel = useDeleteChannel();
const generateSchedule = useGenerateSchedule();
// Channel ordering — persisted to localStorage
const [channelOrder, setChannelOrder] = useState<string[]>([]);
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 [createOpen, setCreateOpen] = useState(false);
const [importOpen, setImportOpen] = useState(false);
const [importPending, setImportPending] = useState(false);
@@ -124,6 +188,18 @@ export default function DashboardPage() {
</p>
</div>
<div className="flex gap-2">
{channels && channels.length > 0 && (
<Button
variant="outline"
onClick={handleRegenerateAll}
disabled={isRegeneratingAll}
title="Regenerate schedules for all channels"
className="border-zinc-700 text-zinc-400 hover:text-zinc-100"
>
<RefreshCw className={`size-4 ${isRegeneratingAll ? "animate-spin" : ""}`} />
Regenerate all
</Button>
)}
<Button variant="outline" onClick={() => setImportOpen(true)} className="border-zinc-700 text-zinc-300 hover:text-zinc-100">
<Upload className="size-4" />
Import
@@ -148,7 +224,7 @@ export default function DashboardPage() {
</div>
)}
{channels && channels.length === 0 && (
{!isLoading && channels && channels.length === 0 && (
<div className="flex flex-col items-center justify-center gap-3 rounded-xl border border-dashed border-zinc-800 py-20 text-center">
<p className="text-sm text-zinc-500">No channels yet</p>
<Button variant="outline" onClick={() => setCreateOpen(true)}>
@@ -158,9 +234,9 @@ export default function DashboardPage() {
</div>
)}
{channels && channels.length > 0 && (
{sortedChannels.length > 0 && (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{channels.map((channel) => (
{sortedChannels.map((channel, idx) => (
<ChannelCard
key={channel.id}
channel={channel}
@@ -168,11 +244,15 @@ export default function DashboardPage() {
generateSchedule.isPending &&
generateSchedule.variables === channel.id
}
isFirst={idx === 0}
isLast={idx === sortedChannels.length - 1}
onEdit={() => setEditChannel(channel)}
onDelete={() => setDeleteTarget(channel)}
onGenerateSchedule={() => generateSchedule.mutate(channel.id)}
onViewSchedule={() => setScheduleChannel(channel)}
onExport={() => handleExport(channel)}
onMoveUp={() => handleMoveUp(channel.id)}
onMoveDown={() => handleMoveDown(channel.id)}
/>
))}
</div>