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:
132
k-tv-frontend/lib/api.ts
Normal file
132
k-tv-frontend/lib/api.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import type {
|
||||
TokenResponse,
|
||||
UserResponse,
|
||||
ChannelResponse,
|
||||
CreateChannelRequest,
|
||||
UpdateChannelRequest,
|
||||
ScheduleResponse,
|
||||
ScheduledSlotResponse,
|
||||
CurrentBroadcastResponse,
|
||||
} from "@/lib/types";
|
||||
|
||||
const API_BASE =
|
||||
process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000/api/v1";
|
||||
|
||||
export class ApiRequestError extends Error {
|
||||
constructor(
|
||||
public readonly status: number,
|
||||
message: string,
|
||||
) {
|
||||
super(message);
|
||||
this.name = "ApiRequestError";
|
||||
}
|
||||
}
|
||||
|
||||
async function request<T>(
|
||||
path: string,
|
||||
options: RequestInit & { token?: string } = {},
|
||||
): Promise<T> {
|
||||
const { token, ...init } = options;
|
||||
const headers = new Headers(init.headers);
|
||||
|
||||
if (token) {
|
||||
headers.set("Authorization", `Bearer ${token}`);
|
||||
}
|
||||
if (init.body && !headers.has("Content-Type")) {
|
||||
headers.set("Content-Type", "application/json");
|
||||
}
|
||||
|
||||
const res = await fetch(`${API_BASE}${path}`, { ...init, headers });
|
||||
|
||||
if (!res.ok) {
|
||||
let message = res.statusText;
|
||||
try {
|
||||
const body = await res.json();
|
||||
message = body.message ?? body.error ?? message;
|
||||
} catch {
|
||||
// ignore parse error, use statusText
|
||||
}
|
||||
throw new ApiRequestError(res.status, message);
|
||||
}
|
||||
|
||||
if (res.status === 204) return null as T;
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
auth: {
|
||||
register: (email: string, password: string) =>
|
||||
request<TokenResponse>("/auth/register", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email, password }),
|
||||
}),
|
||||
|
||||
login: (email: string, password: string) =>
|
||||
request<TokenResponse>("/auth/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email, password }),
|
||||
}),
|
||||
|
||||
logout: (token: string) =>
|
||||
request<void>("/auth/logout", { method: "POST", token }),
|
||||
|
||||
me: (token: string) => request<UserResponse>("/auth/me", { token }),
|
||||
},
|
||||
|
||||
channels: {
|
||||
list: (token: string) =>
|
||||
request<ChannelResponse[]>("/channels", { token }),
|
||||
|
||||
get: (id: string, token: string) =>
|
||||
request<ChannelResponse>(`/channels/${id}`, { token }),
|
||||
|
||||
create: (data: CreateChannelRequest, token: string) =>
|
||||
request<ChannelResponse>("/channels", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
token,
|
||||
}),
|
||||
|
||||
update: (id: string, data: UpdateChannelRequest, token: string) =>
|
||||
request<ChannelResponse>(`/channels/${id}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(data),
|
||||
token,
|
||||
}),
|
||||
|
||||
delete: (id: string, token: string) =>
|
||||
request<void>(`/channels/${id}`, { method: "DELETE", token }),
|
||||
},
|
||||
|
||||
schedule: {
|
||||
generate: (channelId: string, token: string) =>
|
||||
request<ScheduleResponse>(`/channels/${channelId}/schedule`, {
|
||||
method: "POST",
|
||||
token,
|
||||
}),
|
||||
|
||||
getActive: (channelId: string, token: string) =>
|
||||
request<ScheduleResponse>(`/channels/${channelId}/schedule`, { token }),
|
||||
|
||||
getCurrentBroadcast: (channelId: string, token: string) =>
|
||||
request<CurrentBroadcastResponse | null>(`/channels/${channelId}/now`, {
|
||||
token,
|
||||
}),
|
||||
|
||||
getEpg: (
|
||||
channelId: string,
|
||||
token: string,
|
||||
from?: string,
|
||||
until?: string,
|
||||
) => {
|
||||
const params = new URLSearchParams();
|
||||
if (from) params.set("from", from);
|
||||
if (until) params.set("until", until);
|
||||
const qs = params.toString();
|
||||
return request<ScheduledSlotResponse[]>(
|
||||
`/channels/${channelId}/epg${qs ? `?${qs}` : ""}`,
|
||||
{ token },
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
118
k-tv-frontend/lib/types.ts
Normal file
118
k-tv-frontend/lib/types.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
// API response and request types matching the backend DTOs
|
||||
|
||||
export type ContentType = "movie" | "episode" | "short";
|
||||
|
||||
export type FillStrategy = "best_fit" | "sequential" | "random";
|
||||
|
||||
export interface MediaFilter {
|
||||
content_type?: ContentType | null;
|
||||
genres: string[];
|
||||
decade?: number | null;
|
||||
tags: string[];
|
||||
min_duration_secs?: number | null;
|
||||
max_duration_secs?: number | null;
|
||||
collections: string[];
|
||||
}
|
||||
|
||||
export interface RecyclePolicy {
|
||||
cooldown_days?: number | null;
|
||||
cooldown_generations?: number | null;
|
||||
min_available_ratio: number;
|
||||
}
|
||||
|
||||
export type BlockContent =
|
||||
| { type: "algorithmic"; filter: MediaFilter; strategy: FillStrategy }
|
||||
| { type: "manual"; items: string[] };
|
||||
|
||||
export interface ProgrammingBlock {
|
||||
id: string;
|
||||
name: string;
|
||||
/** "HH:MM:SS" */
|
||||
start_time: string;
|
||||
duration_mins: number;
|
||||
content: BlockContent;
|
||||
}
|
||||
|
||||
export interface ScheduleConfig {
|
||||
blocks: ProgrammingBlock[];
|
||||
}
|
||||
|
||||
// Auth
|
||||
|
||||
export interface TokenResponse {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
}
|
||||
|
||||
export interface UserResponse {
|
||||
id: string;
|
||||
email: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// Channels
|
||||
|
||||
export interface ChannelResponse {
|
||||
id: string;
|
||||
owner_id: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
timezone: string;
|
||||
schedule_config: ScheduleConfig;
|
||||
recycle_policy: RecyclePolicy;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreateChannelRequest {
|
||||
name: string;
|
||||
timezone: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface UpdateChannelRequest {
|
||||
name?: string;
|
||||
description?: string;
|
||||
timezone?: string;
|
||||
schedule_config?: ScheduleConfig;
|
||||
recycle_policy?: RecyclePolicy;
|
||||
}
|
||||
|
||||
// Media & Schedule
|
||||
|
||||
export interface MediaItemResponse {
|
||||
id: string;
|
||||
title: string;
|
||||
content_type: ContentType;
|
||||
duration_secs: number;
|
||||
description?: string | null;
|
||||
genres: string[];
|
||||
tags: string[];
|
||||
year?: number | null;
|
||||
}
|
||||
|
||||
export interface ScheduledSlotResponse {
|
||||
id: string;
|
||||
block_id: string;
|
||||
item: MediaItemResponse;
|
||||
/** RFC3339 */
|
||||
start_at: string;
|
||||
/** RFC3339 */
|
||||
end_at: string;
|
||||
}
|
||||
|
||||
export interface ScheduleResponse {
|
||||
id: string;
|
||||
channel_id: string;
|
||||
generation: number;
|
||||
generated_at: string;
|
||||
valid_from: string;
|
||||
valid_until: string;
|
||||
slots: ScheduledSlotResponse[];
|
||||
}
|
||||
|
||||
export interface CurrentBroadcastResponse {
|
||||
slot: ScheduledSlotResponse;
|
||||
offset_secs: number;
|
||||
}
|
||||
Reference in New Issue
Block a user