diff --git a/AGENTS.md b/AGENTS.md index 852db63..eb1b347 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,48 +1,3 @@ -# Codex Agent Handoff - -This repo is a Laravel/Inertia React live streaming platform called Nyone. - -For updating UI/UX, start by reading: - -- `docs/ui-ux-handoff.md` -- `resources/js/pages/welcome.tsx` -- `resources/js/pages/dashboard.tsx` -- `resources/js/pages/channels/show.tsx` -- `resources/js/pages/admin/dashboard.tsx` -- `resources/js/components/app-header.tsx` -- `resources/js/components/app-sidebar.tsx` - -## Project Shape - -- Backend: Laravel 13, Fortify auth, Inertia, PostgreSQL intended for production, SQLite currently used in local `.env`. -- Frontend: React 19, TypeScript, Tailwind CSS 4, Radix UI primitives, lucide-react icons, hls.js. -- Streaming: MediaMTX receives OBS RTMP, validates publish requests via Laravel, serves HLS, and calls lifecycle hooks. -- Key user flows: public live directory, creator studio, channel watch page with chat/follow, basic admin moderation. - -## Design Work Rules - -- Focus on UI/UX in `resources/js` and `resources/css/app.css`. -- Keep the existing Laravel routes/controllers/models unless a UI change requires a small prop shape addition. -- Use existing UI primitives from `resources/js/components/ui`. -- Use lucide icons for controls. -- Avoid decorative landing-page marketing. The first screen should remain the live product experience. -- Keep operational/tooling files intact unless explicitly asked: `deploy/`, `routes/`, migrations, MediaMTX integration. -- Generated Wayfinder files live in `resources/js/actions`, `resources/js/routes`, and `resources/js/wayfinder`; do not hand-edit them. - -## Verify Before Handing Back - -Use the available Herd/Node binaries on this machine: - -```powershell -& 'C:\Users\meghdad\.config\herd\bin\php84\php.exe' artisan test -& 'C:\Users\meghdad\.config\herd\bin\nvm\v25.2.1\npm.cmd' run format:check -& 'C:\Users\meghdad\.config\herd\bin\nvm\v25.2.1\npm.cmd' run lint:check -& 'C:\Users\meghdad\.config\herd\bin\nvm\v25.2.1\npm.cmd' run types:check -& 'C:\Users\meghdad\.config\herd\bin\nvm\v25.2.1\npm.cmd' run build -``` - -=== - === foundation rules === diff --git a/app/Http/Controllers/AdminDashboardController.php b/app/Http/Controllers/AdminDashboardController.php index e78d430..cf28eae 100644 --- a/app/Http/Controllers/AdminDashboardController.php +++ b/app/Http/Controllers/AdminDashboardController.php @@ -6,6 +6,7 @@ use App\Models\Broadcast; use App\Models\Channel; use App\Models\User; use App\Models\Vod; +use Illuminate\Support\Facades\Storage; use Inertia\Inertia; use Inertia\Response; @@ -34,6 +35,7 @@ class AdminDashboardController extends Controller 'id' => $broadcast->channel->id, 'slug' => $broadcast->channel->slug, 'display_name' => $broadcast->channel->display_name, + 'thumbnail_url' => $this->mediaUrl($broadcast->channel->thumbnail_path), 'owner' => $broadcast->channel->user->name, ], ]), @@ -46,6 +48,8 @@ class AdminDashboardController extends Controller 'id' => $channel->id, 'slug' => $channel->slug, 'display_name' => $channel->display_name, + 'avatar_url' => $this->mediaUrl($channel->avatar_path), + 'thumbnail_url' => $this->mediaUrl($channel->thumbnail_path), 'is_live' => $channel->is_live, 'suspended_at' => $channel->suspended_at?->toIso8601String(), 'owner' => [ @@ -55,4 +59,9 @@ class AdminDashboardController extends Controller ]), ]); } + + private function mediaUrl(?string $path): ?string + { + return $path ? Storage::url($path) : null; + } } diff --git a/app/Http/Controllers/ChannelController.php b/app/Http/Controllers/ChannelController.php index 7f54d6a..4ba89cf 100644 --- a/app/Http/Controllers/ChannelController.php +++ b/app/Http/Controllers/ChannelController.php @@ -6,6 +6,7 @@ use App\Models\Channel; use App\Models\ChatMessage; use App\Models\Vod; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Storage; use Inertia\Inertia; use Inertia\Response; @@ -26,6 +27,9 @@ class ChannelController extends Controller 'slug' => $channel->slug, 'display_name' => $channel->display_name, 'description' => $channel->description, + 'avatar_url' => $this->mediaUrl($channel->avatar_path), + 'banner_url' => $this->mediaUrl($channel->banner_path), + 'thumbnail_url' => $this->mediaUrl($channel->thumbnail_path), 'is_live' => $channel->is_live, 'viewer_count' => $channel->viewer_count, 'followers_count' => $channel->follows_count, @@ -84,4 +88,9 @@ class ChannelController extends Controller : false, ]); } + + private function mediaUrl(?string $path): ?string + { + return $path ? Storage::url($path) : null; + } } diff --git a/app/Http/Controllers/CreatorDashboardController.php b/app/Http/Controllers/CreatorDashboardController.php index fe982c4..d20ad55 100644 --- a/app/Http/Controllers/CreatorDashboardController.php +++ b/app/Http/Controllers/CreatorDashboardController.php @@ -8,6 +8,7 @@ use App\Models\Channel; use App\Services\Streaming\StreamKeyManager; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; use Illuminate\Validation\Rule; use Inertia\Inertia; @@ -165,6 +166,9 @@ class CreatorDashboardController extends Controller 'slug' => $channel->slug, 'display_name' => $channel->display_name, 'description' => $channel->description, + 'avatar_url' => $this->mediaUrl($channel->avatar_path), + 'banner_url' => $this->mediaUrl($channel->banner_path), + 'thumbnail_url' => $this->mediaUrl($channel->thumbnail_path), 'is_live' => $channel->is_live, 'viewer_count' => $channel->viewer_count, 'stream_key_last_used_at' => $channel->streamKey?->last_used_at?->toIso8601String(), @@ -179,4 +183,9 @@ class CreatorDashboardController extends Controller ] : null, ]; } + + private function mediaUrl(?string $path): ?string + { + return $path ? Storage::url($path) : null; + } } diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index c0029c9..5239c69 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers; use App\Models\Category; use App\Models\Channel; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Storage; use Inertia\Inertia; use Inertia\Response; use Laravel\Fortify\Features; @@ -46,6 +47,9 @@ class HomeController extends Controller 'display_name' => $channel->display_name, 'description' => $channel->description, 'thumbnail_path' => $channel->thumbnail_path, + 'thumbnail_url' => $this->mediaUrl($channel->thumbnail_path), + 'avatar_url' => $this->mediaUrl($channel->avatar_path), + 'banner_url' => $this->mediaUrl($channel->banner_path), 'viewer_count' => $channel->viewer_count, 'followers_count' => $channel->follows_count ?? 0, 'category' => $channel->category ? [ @@ -64,4 +68,9 @@ class HomeController extends Controller ], ]; } + + private function mediaUrl(?string $path): ?string + { + return $path ? Storage::url($path) : null; + } } diff --git a/resources/css/app.css b/resources/css/app.css index 7015d58..9d484db 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -62,74 +62,82 @@ } :root { - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --destructive-foreground: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.87 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --radius: 0.625rem; - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.87 0 0); + --background: oklch(0.982 0.012 315); + --foreground: oklch(0.18 0.045 312); + --card: oklch(0.996 0.006 315); + --card-foreground: oklch(0.18 0.045 312); + --popover: oklch(0.996 0.006 315); + --popover-foreground: oklch(0.18 0.045 312); + --primary: oklch(0.57 0.245 348); + --primary-foreground: oklch(0.99 0.006 315); + --secondary: oklch(0.935 0.03 314); + --secondary-foreground: oklch(0.26 0.075 312); + --muted: oklch(0.945 0.022 315); + --muted-foreground: oklch(0.48 0.05 312); + --accent: oklch(0.925 0.045 333); + --accent-foreground: oklch(0.25 0.08 318); + --destructive: oklch(0.56 0.22 24); + --destructive-foreground: oklch(0.99 0.006 315); + --border: oklch(0.878 0.032 314); + --input: oklch(0.878 0.032 314); + --ring: oklch(0.64 0.18 348); + --chart-1: oklch(0.58 0.22 348); + --chart-2: oklch(0.58 0.14 205); + --chart-3: oklch(0.62 0.16 145); + --chart-4: oklch(0.7 0.16 78); + --chart-5: oklch(0.56 0.18 285); + --radius: 0.5rem; + --sidebar: oklch(0.955 0.02 314); + --sidebar-foreground: oklch(0.19 0.05 312); + --sidebar-primary: oklch(0.51 0.22 348); + --sidebar-primary-foreground: oklch(0.99 0.006 315); + --sidebar-accent: oklch(0.91 0.04 318); + --sidebar-accent-foreground: oklch(0.23 0.075 312); + --sidebar-border: oklch(0.86 0.032 314); + --sidebar-ring: oklch(0.64 0.18 348); + --live: oklch(0.62 0.25 24); + --signal: oklch(0.67 0.16 155); + --warning: oklch(0.72 0.15 78); + --success: oklch(0.63 0.16 150); } .dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.145 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.145 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.985 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.396 0.141 25.723); - --destructive-foreground: oklch(0.637 0.237 25.331); - --border: oklch(0.269 0 0); - --input: oklch(0.269 0 0); - --ring: oklch(0.439 0 0); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.985 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(0.269 0 0); - --sidebar-ring: oklch(0.439 0 0); + --background: oklch(0.145 0.04 315); + --foreground: oklch(0.968 0.012 316); + --card: oklch(0.18 0.045 315); + --card-foreground: oklch(0.968 0.012 316); + --popover: oklch(0.18 0.045 315); + --popover-foreground: oklch(0.968 0.012 316); + --primary: oklch(0.68 0.245 348); + --primary-foreground: oklch(0.13 0.04 315); + --secondary: oklch(0.245 0.055 314); + --secondary-foreground: oklch(0.955 0.014 316); + --muted: oklch(0.235 0.048 314); + --muted-foreground: oklch(0.73 0.04 316); + --accent: oklch(0.27 0.075 326); + --accent-foreground: oklch(0.97 0.012 316); + --destructive: oklch(0.62 0.22 24); + --destructive-foreground: oklch(0.99 0.006 315); + --border: oklch(0.31 0.055 315); + --input: oklch(0.31 0.055 315); + --ring: oklch(0.68 0.22 348); + --chart-1: oklch(0.68 0.245 348); + --chart-2: oklch(0.7 0.16 198); + --chart-3: oklch(0.72 0.16 150); + --chart-4: oklch(0.78 0.16 80); + --chart-5: oklch(0.7 0.18 292); + --sidebar: oklch(0.17 0.045 315); + --sidebar-foreground: oklch(0.96 0.012 316); + --sidebar-primary: oklch(0.68 0.245 348); + --sidebar-primary-foreground: oklch(0.13 0.04 315); + --sidebar-accent: oklch(0.255 0.06 315); + --sidebar-accent-foreground: oklch(0.96 0.012 316); + --sidebar-border: oklch(0.31 0.055 315); + --sidebar-ring: oklch(0.68 0.22 348); + --live: oklch(0.67 0.25 24); + --signal: oklch(0.73 0.17 155); + --warning: oklch(0.78 0.16 78); + --success: oklch(0.7 0.17 150); } @layer base { @@ -138,6 +146,73 @@ } body { - @apply bg-background text-foreground; + @apply bg-background text-foreground antialiased; + } + + ::selection { + background: color-mix(in oklab, var(--primary) 28%, transparent); + } +} + +@layer utilities { + .bg-live { + background-color: var(--live); + } + + .text-live { + color: var(--live); + } + + .border-live { + border-color: var(--live); + } + + .bg-signal { + background-color: var(--signal); + } + + .text-signal { + color: var(--signal); + } + + .bg-warning { + background-color: var(--warning); + } + + .text-warning { + color: var(--warning); + } + + .bg-success { + background-color: var(--success); + } + + .text-success { + color: var(--success); + } + + .theatre-surface { + background: linear-gradient( + 180deg, + color-mix(in oklab, var(--card) 95%, black 5%), + var(--background) + ); + } + + .live-pulse { + animation: live-pulse 1.8s ease-in-out infinite; + } +} + +@keyframes live-pulse { + 0%, + 100% { + opacity: 1; + transform: scale(1); + } + + 50% { + opacity: 0.55; + transform: scale(0.82); } } diff --git a/resources/js/components/app-header.tsx b/resources/js/components/app-header.tsx index 77e2bfb..1be6230 100644 --- a/resources/js/components/app-header.tsx +++ b/resources/js/components/app-header.tsx @@ -40,8 +40,7 @@ type Props = { breadcrumbs?: BreadcrumbItem[]; }; -const activeItemStyles = - 'text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100'; +const activeItemStyles = 'bg-accent text-accent-foreground'; export function AppHeader({ breadcrumbs = [] }: Props) { const page = usePage(); @@ -71,7 +70,7 @@ export function AppHeader({ breadcrumbs = [] }: Props) { return ( <> -
+
{/* Mobile Menu */}
@@ -153,7 +152,7 @@ export function AppHeader({ breadcrumbs = [] }: Props) { {item.title} {isCurrentUrl(item.href) && ( -
+
)} ))} @@ -203,7 +202,7 @@ export function AppHeader({ breadcrumbs = [] }: Props) { src={auth.user?.avatar} alt={auth.user?.name} /> - + {getInitials(auth.user?.name ?? '')} diff --git a/resources/js/components/app-logo.tsx b/resources/js/components/app-logo.tsx index 7f29c82..eeb038f 100644 --- a/resources/js/components/app-logo.tsx +++ b/resources/js/components/app-logo.tsx @@ -3,8 +3,8 @@ import AppLogoIcon from '@/components/app-logo-icon'; export default function AppLogo() { return ( <> -
- +
+
diff --git a/resources/js/components/ui/alert.tsx b/resources/js/components/ui/alert.tsx index 6e6727a..2462676 100644 --- a/resources/js/components/ui/alert.tsx +++ b/resources/js/components/ui/alert.tsx @@ -4,7 +4,7 @@ import * as React from "react" import { cn } from "@/lib/utils" const alertVariants = cva( - "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + "relative w-full rounded-md border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", { variants: { variant: { diff --git a/resources/js/components/ui/card.tsx b/resources/js/components/ui/card.tsx index 5e960a6..a116bee 100644 --- a/resources/js/components/ui/card.tsx +++ b/resources/js/components/ui/card.tsx @@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
) { return ( -
+
-
- +
+
- + {title} {description} diff --git a/resources/js/layouts/auth/auth-simple-layout.tsx b/resources/js/layouts/auth/auth-simple-layout.tsx index b1396ae..7bc5466 100644 --- a/resources/js/layouts/auth/auth-simple-layout.tsx +++ b/resources/js/layouts/auth/auth-simple-layout.tsx @@ -10,21 +10,21 @@ export default function AuthSimpleLayout({ }: AuthLayoutProps) { return (
-
+
-
- +
+
{title}
-

{title}

+

{title}

{description}

diff --git a/resources/js/layouts/settings/layout.tsx b/resources/js/layouts/settings/layout.tsx index efd7904..789a567 100644 --- a/resources/js/layouts/settings/layout.tsx +++ b/resources/js/layouts/settings/layout.tsx @@ -1,4 +1,5 @@ import { Link } from '@inertiajs/react'; +import { Palette, ShieldCheck, UserRound } from 'lucide-react'; import type { PropsWithChildren } from 'react'; import Heading from '@/components/heading'; import { Button } from '@/components/ui/button'; @@ -14,17 +15,17 @@ const sidebarNavItems: NavItem[] = [ { title: 'Profile', href: edit(), - icon: null, + icon: UserRound, }, { title: 'Security', href: editSecurity(), - icon: null, + icon: ShieldCheck, }, { title: 'Appearance', href: editAppearance(), - icon: null, + icon: Palette, }, ]; @@ -38,7 +39,7 @@ export default function SettingsLayout({ children }: PropsWithChildren) { description="Manage your profile and account settings" /> -
+