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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user