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,28 +1,64 @@
"use client";
import Link from "next/link";
import { Pencil, Trash2, RefreshCw, Tv2, CalendarDays, Download } from "lucide-react";
import { Pencil, Trash2, RefreshCw, Tv2, CalendarDays, Download, ChevronUp, ChevronDown } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useActiveSchedule } from "@/hooks/use-channels";
import type { ChannelResponse } from "@/lib/types";
interface ChannelCardProps {
channel: ChannelResponse;
isGenerating: boolean;
isFirst: boolean;
isLast: boolean;
onEdit: () => void;
onDelete: () => void;
onGenerateSchedule: () => void;
onViewSchedule: () => void;
onExport: () => void;
onMoveUp: () => void;
onMoveDown: () => void;
}
function useScheduleStatus(channelId: string) {
const { data: schedule } = useActiveSchedule(channelId);
if (!schedule) return { status: "none" as const, label: null };
const expiresAt = new Date(schedule.valid_until);
const hoursLeft = (expiresAt.getTime() - Date.now()) / (1000 * 60 * 60);
if (hoursLeft < 0) {
return { status: "expired" as const, label: "Schedule expired" };
}
if (hoursLeft < 6) {
const h = Math.ceil(hoursLeft);
return { status: "expiring" as const, label: `Expires in ${h}h` };
}
const fmt = expiresAt.toLocaleDateString(undefined, { weekday: "short", hour: "2-digit", minute: "2-digit", hour12: false });
return { status: "ok" as const, label: `Until ${fmt}` };
}
export function ChannelCard({
channel,
isGenerating,
isFirst,
isLast,
onEdit,
onDelete,
onGenerateSchedule,
onViewSchedule,
onExport,
onMoveUp,
onMoveDown,
}: ChannelCardProps) {
const blockCount = channel.schedule_config.blocks.length;
const { status, label } = useScheduleStatus(channel.id);
const scheduleColor =
status === "expired" ? "text-red-400" :
status === "expiring" ? "text-amber-400" :
status === "ok" ? "text-zinc-500" :
"text-zinc-600";
return (
<div className="flex flex-col gap-4 rounded-xl border border-zinc-800 bg-zinc-900 p-5 transition-colors hover:border-zinc-700">
@@ -40,6 +76,26 @@ export function ChannelCard({
</div>
<div className="flex shrink-0 items-center gap-1">
{/* Order controls */}
<div className="flex flex-col">
<button
onClick={onMoveUp}
disabled={isFirst}
title="Move up"
className="rounded p-0.5 text-zinc-600 transition-colors hover:text-zinc-300 disabled:opacity-20 disabled:cursor-not-allowed"
>
<ChevronUp className="size-3.5" />
</button>
<button
onClick={onMoveDown}
disabled={isLast}
title="Move down"
className="rounded p-0.5 text-zinc-600 transition-colors hover:text-zinc-300 disabled:opacity-20 disabled:cursor-not-allowed"
>
<ChevronDown className="size-3.5" />
</button>
</div>
<Button
variant="ghost"
size="icon-sm"
@@ -71,12 +127,13 @@ export function ChannelCard({
{/* Meta */}
<div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-zinc-500">
<span>
<span className="text-zinc-400">{channel.timezone}</span>
</span>
<span className="text-zinc-400">{channel.timezone}</span>
<span>
{blockCount} {blockCount === 1 ? "block" : "blocks"}
</span>
{label && (
<span className={scheduleColor}>{label}</span>
)}
</div>
{/* Actions */}
@@ -85,7 +142,7 @@ export function ChannelCard({
size="sm"
onClick={onGenerateSchedule}
disabled={isGenerating}
className="flex-1"
className={`flex-1 ${status === "expired" ? "border border-red-800/50 bg-red-950/30 text-red-300 hover:bg-red-900/40" : ""}`}
>
<RefreshCw className={`size-3.5 ${isGenerating ? "animate-spin" : ""}`} />
{isGenerating ? "Generating…" : "Generate schedule"}