feat: initialize k-tv-frontend with Next.js and Tailwind CSS

- Added package.json with dependencies and scripts for development, build, and linting.
- Created postcss.config.mjs for Tailwind CSS integration.
- Added SVG assets for UI components including file, globe, next, vercel, and window icons.
- Configured TypeScript with tsconfig.json for strict type checking and module resolution.
This commit is contained in:
2026-03-11 19:13:21 +01:00
commit 01108aa23e
130 changed files with 29949 additions and 0 deletions

184
k-tv-backend/api/src/dto.rs Normal file
View File

@@ -0,0 +1,184 @@
//! Request and Response DTOs
//!
//! Data Transfer Objects for the API.
//! Uses domain newtypes for validation instead of the validator crate.
use chrono::{DateTime, Utc};
use domain::{Email, Password};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// Login request with validated email and password newtypes
#[derive(Debug, Deserialize)]
pub struct LoginRequest {
/// Email is validated on deserialization
pub email: Email,
/// Password is validated on deserialization (min 8 chars)
pub password: Password,
}
/// Register request with validated email and password newtypes
#[derive(Debug, Deserialize)]
pub struct RegisterRequest {
/// Email is validated on deserialization
pub email: Email,
/// Password is validated on deserialization (min 8 chars)
pub password: Password,
}
/// User response DTO
#[derive(Debug, Serialize)]
pub struct UserResponse {
pub id: Uuid,
pub email: String,
pub created_at: DateTime<Utc>,
}
/// JWT token response
#[derive(Debug, Serialize)]
pub struct TokenResponse {
pub access_token: String,
pub token_type: String,
pub expires_in: u64,
}
/// System configuration response
#[derive(Debug, Serialize)]
pub struct ConfigResponse {
pub allow_registration: bool,
}
// ============================================================================
// Channel DTOs
// ============================================================================
#[derive(Debug, Deserialize)]
pub struct CreateChannelRequest {
pub name: String,
pub description: Option<String>,
/// IANA timezone, e.g. "UTC" or "America/New_York"
pub timezone: String,
}
/// All fields are optional — only provided fields are updated.
#[derive(Debug, Deserialize)]
pub struct UpdateChannelRequest {
pub name: Option<String>,
pub description: Option<String>,
pub timezone: Option<String>,
/// Replace the entire schedule config (template import/edit)
pub schedule_config: Option<domain::ScheduleConfig>,
pub recycle_policy: Option<domain::RecyclePolicy>,
}
#[derive(Debug, Serialize)]
pub struct ChannelResponse {
pub id: Uuid,
pub owner_id: Uuid,
pub name: String,
pub description: Option<String>,
pub timezone: String,
pub schedule_config: domain::ScheduleConfig,
pub recycle_policy: domain::RecyclePolicy,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl From<domain::Channel> for ChannelResponse {
fn from(c: domain::Channel) -> Self {
Self {
id: c.id,
owner_id: c.owner_id,
name: c.name,
description: c.description,
timezone: c.timezone,
schedule_config: c.schedule_config,
recycle_policy: c.recycle_policy,
created_at: c.created_at,
updated_at: c.updated_at,
}
}
}
// ============================================================================
// EPG / playback DTOs
// ============================================================================
#[derive(Debug, Serialize)]
pub struct MediaItemResponse {
pub id: String,
pub title: String,
pub content_type: domain::ContentType,
pub duration_secs: u32,
pub genres: Vec<String>,
pub year: Option<u16>,
pub tags: Vec<String>,
}
impl From<domain::MediaItem> for MediaItemResponse {
fn from(i: domain::MediaItem) -> Self {
Self {
id: i.id.into_inner(),
title: i.title,
content_type: i.content_type,
duration_secs: i.duration_secs,
genres: i.genres,
year: i.year,
tags: i.tags,
}
}
}
#[derive(Debug, Serialize)]
pub struct ScheduledSlotResponse {
pub id: Uuid,
pub start_at: DateTime<Utc>,
pub end_at: DateTime<Utc>,
pub item: MediaItemResponse,
pub source_block_id: Uuid,
}
impl From<domain::ScheduledSlot> for ScheduledSlotResponse {
fn from(s: domain::ScheduledSlot) -> Self {
Self {
id: s.id,
start_at: s.start_at,
end_at: s.end_at,
item: s.item.into(),
source_block_id: s.source_block_id,
}
}
}
/// What is currently playing on a channel.
/// A 204 No Content response is returned instead when there is no active slot (no-signal).
#[derive(Debug, Serialize)]
pub struct CurrentBroadcastResponse {
pub slot: ScheduledSlotResponse,
/// Seconds elapsed since the start of the current item — use this as the
/// initial seek position for the player.
pub offset_secs: u32,
}
#[derive(Debug, Serialize)]
pub struct ScheduleResponse {
pub id: Uuid,
pub channel_id: Uuid,
pub valid_from: DateTime<Utc>,
pub valid_until: DateTime<Utc>,
pub generation: u32,
pub slots: Vec<ScheduledSlotResponse>,
}
impl From<domain::GeneratedSchedule> for ScheduleResponse {
fn from(s: domain::GeneratedSchedule) -> Self {
Self {
id: s.id,
channel_id: s.channel_id,
valid_from: s.valid_from,
valid_until: s.valid_until,
generation: s.generation,
slots: s.slots.into_iter().map(Into::into).collect(),
}
}
}