feat: implement configuration management and enhance user registration flow
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user