feat: implement authentication context and hooks for user management

- Add AuthContext to manage user authentication state and token storage.
- Create hooks for login, registration, and logout functionalities.
- Implement dashboard layout with authentication check and loading state.
- Enhance dashboard page with channel management features including create, edit, and delete channels.
- Integrate API calls for channel operations and current broadcast retrieval.
- Add stream URL resolution via server-side API route to handle redirects.
- Update TV page to utilize new hooks for channel and broadcast management.
- Refactor components for better organization and user experience.
- Update application metadata for improved branding.
This commit is contained in:
2026-03-11 19:32:49 +01:00
parent 01108aa23e
commit 8d8d320a02
22 changed files with 2118 additions and 173 deletions

View File

@@ -1,8 +1,151 @@
"use client";
import { useState } from "react";
import { Plus } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
useChannels,
useCreateChannel,
useUpdateChannel,
useDeleteChannel,
useGenerateSchedule,
} from "@/hooks/use-channels";
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 type { ChannelResponse, ProgrammingBlock, RecyclePolicy } from "@/lib/types";
export default function DashboardPage() {
const { data: channels, isLoading, error } = useChannels();
const createChannel = useCreateChannel();
const updateChannel = useUpdateChannel();
const deleteChannel = useDeleteChannel();
const generateSchedule = useGenerateSchedule();
const [createOpen, setCreateOpen] = useState(false);
const [editChannel, setEditChannel] = useState<ChannelResponse | null>(null);
const [deleteTarget, setDeleteTarget] = useState<ChannelResponse | null>(null);
const handleCreate = (data: {
name: string;
timezone: string;
description: string;
}) => {
createChannel.mutate(
{ name: data.name, timezone: data.timezone, description: data.description || undefined },
{ onSuccess: () => setCreateOpen(false) },
);
};
const handleEdit = (
id: string,
data: {
name: string;
description: string;
timezone: string;
schedule_config: { blocks: ProgrammingBlock[] };
recycle_policy: RecyclePolicy;
},
) => {
updateChannel.mutate(
{ id, data },
{ onSuccess: () => setEditChannel(null) },
);
};
const handleDelete = () => {
if (!deleteTarget) return;
deleteChannel.mutate(deleteTarget.id, {
onSuccess: () => setDeleteTarget(null),
});
};
return (
<div className="flex flex-1 flex-col items-center justify-center gap-4 p-8">
<h1 className="text-2xl font-semibold tracking-tight">Dashboard</h1>
<p className="text-sm text-zinc-500">Channel management and user settings go here.</p>
<div className="mx-auto w-full max-w-5xl space-y-6 px-6 py-8">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-semibold text-zinc-100">My Channels</h1>
<p className="mt-0.5 text-sm text-zinc-500">
Build your broadcast lineup
</p>
</div>
<Button onClick={() => setCreateOpen(true)}>
<Plus className="size-4" />
New channel
</Button>
</div>
{/* Content */}
{isLoading && (
<div className="flex items-center justify-center py-20">
<div className="h-5 w-5 animate-spin rounded-full border-2 border-zinc-700 border-t-zinc-300" />
</div>
)}
{error && (
<div className="rounded-lg border border-red-900/50 bg-red-950/20 px-4 py-3 text-sm text-red-400">
{error.message}
</div>
)}
{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)}>
<Plus className="size-4" />
Create your first channel
</Button>
</div>
)}
{channels && channels.length > 0 && (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{channels.map((channel) => (
<ChannelCard
key={channel.id}
channel={channel}
isGenerating={
generateSchedule.isPending &&
generateSchedule.variables === channel.id
}
onEdit={() => setEditChannel(channel)}
onDelete={() => setDeleteTarget(channel)}
onGenerateSchedule={() => generateSchedule.mutate(channel.id)}
/>
))}
</div>
)}
{/* Dialogs / sheets */}
<CreateChannelDialog
open={createOpen}
onOpenChange={setCreateOpen}
onSubmit={handleCreate}
isPending={createChannel.isPending}
error={createChannel.error?.message}
/>
<EditChannelSheet
channel={editChannel}
open={!!editChannel}
onOpenChange={(open) => { if (!open) setEditChannel(null); }}
onSubmit={handleEdit}
isPending={updateChannel.isPending}
error={updateChannel.error?.message}
/>
{deleteTarget && (
<DeleteChannelDialog
channelName={deleteTarget.name}
open={!!deleteTarget}
onOpenChange={(open) => { if (!open) setDeleteTarget(null); }}
onConfirm={handleDelete}
isPending={deleteChannel.isPending}
/>
)}
</div>
);
}