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

@@ -0,0 +1,94 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import type { ScheduleSlot } from "@/app/(main)/tv/components";
import type { ScheduledSlotResponse } from "@/lib/types";
// ---------------------------------------------------------------------------
// Pure transformation utilities
// ---------------------------------------------------------------------------
/** Format an ISO-8601 string to "HH:MM" in the user's local timezone. */
export function fmtTime(iso: string): string {
return new Date(iso).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
hour12: false,
});
}
/** Progress percentage through a slot based on wall-clock time. */
export function calcProgress(startAt: string, durationSecs: number): number {
if (durationSecs <= 0) return 0;
const elapsedSecs = (Date.now() - new Date(startAt).getTime()) / 1000;
return Math.min(100, Math.max(0, Math.round((elapsedSecs / durationSecs) * 100)));
}
/** Minutes until a future timestamp (rounded, minimum 0). */
export function minutesUntil(iso: string): number {
return Math.max(0, Math.round((new Date(iso).getTime() - Date.now()) / 60_000));
}
/**
* Map EPG slots to the shape expected by ScheduleOverlay.
* Marks the slot matching currentSlotId as current.
*/
export function toScheduleSlots(
slots: ScheduledSlotResponse[],
currentSlotId?: string,
): ScheduleSlot[] {
return slots.map((slot) => ({
id: slot.id,
title: slot.item.title,
startTime: fmtTime(slot.start_at),
endTime: fmtTime(slot.end_at),
isCurrent: slot.id === currentSlotId,
}));
}
/**
* Find the slot immediately after the current one.
* Returns null if current slot is last or not found.
*/
export function findNextSlot(
slots: ScheduledSlotResponse[],
currentSlotId?: string,
): ScheduledSlotResponse | null {
if (!currentSlotId || slots.length === 0) return null;
const idx = slots.findIndex((s) => s.id === currentSlotId);
if (idx === -1 || idx === slots.length - 1) return null;
return slots[idx + 1];
}
// ---------------------------------------------------------------------------
// useStreamUrl — resolves the 307 stream redirect via a Next.js API route
// ---------------------------------------------------------------------------
/**
* Resolves the live stream URL for a channel.
*
* The backend's GET /channels/:id/stream endpoint returns a 307 redirect to
* the Jellyfin stream URL. Since browsers can't read redirect Location headers
* from fetch(), we proxy through /api/stream/[channelId] (a Next.js route that
* runs server-side) and return the final URL as JSON.
*
* Returns null when the channel is in a gap (no-signal / 204).
*/
export function useStreamUrl(channelId: string | undefined, token: string | null) {
return useQuery({
queryKey: ["stream-url", channelId],
queryFn: async (): Promise<string | null> => {
const res = await fetch(
`/api/stream/${channelId}?token=${encodeURIComponent(token!)}`,
{ cache: "no-store" },
);
if (res.status === 204) return null;
if (!res.ok) throw new Error(`Stream resolve failed: ${res.status}`);
const { url } = await res.json();
return url as string;
},
enabled: !!channelId && !!token,
refetchInterval: 30_000,
retry: false,
});
}