diff --git a/.claude/component-usage.md b/.claude/component-usage.md new file mode 100644 index 0000000..dcab428 --- /dev/null +++ b/.claude/component-usage.md @@ -0,0 +1,186 @@ +# Reusable Select Component Usage + +This file tracks every page/partial that uses ``, ``, or ``. +**Update this file whenever you add or remove a component from a view.** + +When modifying any component or its data source (`app/Data/Countries.php`), check all pages in the relevant section below and verify the change works correctly in each context. + +--- + +## Data source + +**`app/Data/Countries.php`** — `App\Data\Countries` + +| Method | Used by component | +|---|---| +| `Countries::forPhoneCode()` | `` | +| `Countries::forCountry()` | `` | +| `Countries::forTimezone()` | `` | +| `Countries::all()` | All three (via the above methods) | + +Adding or renaming a field in `Countries::all()` requires updating the corresponding `for*()` method too. + +--- + +## Shared CSS / JS + +The `.csd-*` CSS rules and the `window.CSD` class are duplicated across all three component files inside `@once` guards. If you change the look or behaviour of the dropdown, **update all three component files**: + +- `resources/views/components/phone-code-select.blade.php` +- `resources/views/components/country-select.blade.php` +- `resources/views/components/timezone-select.blade.php` + +The `@once` Blade directive ensures the browser only receives one copy of the CSS/JS even when multiple components are on the same page. + +--- + +## `` + +**File:** `resources/views/components/video-insights.blade.php` +**Props:** `:video` — `Video` model instance. +**Behaviour:** Renders the Insights tab panel (`
`), the drill-down modal, all `.ins-*` CSS, and all insights JS (`loadInsights`, `renderInsights`, modal openers, country/day/downloader drill-downs). Only renders if `Auth::id() === $video->user_id`. Must be placed **inside `.vdb-wrap`**, after the About panel, so the tab-switch CSS applies. The parent view must call `loadInsights()` (global, defined by this component) when the Insights tab is activated. +**Data source:** `GET /videos/{video}/insights` (JSON) + drill-down routes `/insights/country/{code}`, `/insights/day/{date}`, `/insights/downloader/{userId}`. + +| View file | Placement | Notes | +|---|---|---| +| `resources/views/videos/partials/description-box.blade.php` | Inside `.vdb-wrap`, after About panel | Used by all three video type views (generic, match, music) | +| `resources/views/videos/show.blade.php` | Inside `.vdb-wrap`, after About panel | Legacy view (not rendered by controller — kept in sync) | + +--- + +## `` + +**File:** `resources/views/components/social-links-editor.blade.php` +**Props:** `existing` — associative array keyed by platform name (e.g. `['twitter' => 'handle', 'whatsapp' => '97312345678']`). +**Behaviour:** Dynamic add/remove rows; each row has a custom icon dropdown to pick the platform and a text input for the value. Supported platforms: `twitter`, `instagram`, `facebook`, `youtube`, `linkedin`, `tiktok`, `whatsapp`, `website`, `google_location`, `social_phone`, `social_email`. Hidden clear inputs ensure removed entries are cleared on save. Must be placed **inside a `
`**. +**DB columns:** `twitter`, `instagram`, `facebook`, `youtube`, `linkedin`, `tiktok`, `website` (legacy), `whatsapp`, `google_location`, `social_phone`, `social_email`. + +| View file | Placement | Notes | +|---|---|---| +| `resources/views/user/profile.blade.php` | Social tab of Edit Profile modal | `$socialExisting` array passed from `@php` block above `@section('scripts')` | + +--- + +## `` + +**File:** `resources/views/components/date-picker.blade.php` +**Stored value:** `YYYY-MM-DD` string in a hidden input (same format as ``). +**Props:** `name`, `id`, `value`, `label`, `required`, `class`, `style`, `minYear` (default 1900), `maxYear` (default current year). +**Behaviour:** Day grid (5 columns, 1–31), month list (January–December), year searchable list (descending). Days auto-constrain on month/year change; invalid selected day resets automatically. + +| View file | Field name | Notes | +|---|---|---| +| `resources/views/user/profile.blade.php` | `birthday` | Replaces `` | +| `resources/views/auth/register.blade.php` | `birthday` | Registration form — mandatory | + +--- + +## `` + +**File:** `resources/views/components/image-cropper.blade.php` +**Props:** `id` (unique, required), `width` (px, default 300), `height` (px, default 300), `shape` (`circle`|`square`, default `circle`), `folder` (storage subfolder), `filename` (base name without extension), `callback` (JS function name called with URL on success), `update-url` (endpoint to POST `{path}` after crop to update DB), `title` (modal heading). +**Behaviour:** Renders a full-screen dark-themed modal with Cropme.js. Shows camera icon on avatar/banner hover (owner only). After crop: POSTs base64 to `/image-upload`, optionally POSTs the path to `update-url`, then calls `callback(url)`. Uses local assets (`public/js/cropme.min.js`, `public/css/cropme.min.css`). No jQuery required. +**Assets needed:** `public/js/cropme.min.js`, `public/css/cropme.min.css`. +**Routes needed:** `image.upload` (POST `/image-upload`). + +| View file | id / use | Notes | +|---|---|---| +| `resources/views/user/channel.blade.php` | `avatar` — circle 300×300 | Owner only; `update-url = profile.updateAvatar`; callback `onAvatarSaved` | +| `resources/views/user/channel.blade.php` | `banner` — square 500×160 | Owner only; `update-url = profile.updateBanner`; callback `onBannerSaved` | +| `resources/views/layouts/partials/upload-modal.blade.php` | `thumb_upload` — square 448×252 | Form mode; `target-input=thumbnail-modal`; output 1280px | +| `resources/views/layouts/partials/edit-video-modal.blade.php` | `thumb_edit` — square 448×252 | Form mode; `target-input=edit-thumbnail-input`; output 1280px | +| `resources/views/videos/create.blade.php` | `thumb_create_mobile` — square 448×252 | Mobile; `target-input=thumbnail`; output 1280px | +| `resources/views/videos/edit.blade.php` | `thumb_edit_mobile` — square 448×252 | Mobile; `target-input=edit-thumbnail`; output 1280px | +| `resources/views/playlists/index.blade.php` | `thumb_pl_create` — square 448×252 | Form mode; `target-input=playlist-thumbnail-input`; output 1280px | +| `resources/views/playlists/show.blade.php` | `thumb_pl_edit` — square 448×252 | Form mode; `target-input=playlistThumbnailInput`; output 1280px | + +--- + +## `` + +**File:** `resources/views/components/gender-select.blade.php` +**Props:** `name`, `id`, `value` (ISO string: `"male"` or `"female"`), `label`, `required`, `class`, `style`. +**Behaviour:** Custom dropdown with blue ♂ (male) and pink ♀ (female) symbols. No search needed. Stores value as `"male"` or `"female"`. + +| View file | Field name | Notes | +|---|---|---| +| `resources/views/auth/register.blade.php` | `gender` | Registration form — mandatory | + +--- + +## `` + +Stored value format: `"+973|BH"` (dial_code + pipe + ISO2). +To read only the dial code from a stored value: `explode('|', $value)[0]`. + +| View file | Field name | Notes | +|---|---|---| +| `resources/views/user/profile.blade.php` | `phone_code` | Paired with `phone_number` text input | + +--- + +## `` + +Stored value: ISO2 code (e.g. `"BH"`). + +| View file | Field name | Notes | +|---|---|---| +| `resources/views/user/profile.blade.php` | `nationality` | Edit Profile form | +| `resources/views/auth/register.blade.php` | `nationality` | Registration form — mandatory | + +--- + +## `` + +Stored value: IANA timezone string (e.g. `"Asia/Bahrain"`). + +| View file | Field name | Notes | +|---|---|---| +| `resources/views/user/profile.blade.php` | `timezone` | Edit Profile form | + +--- + +## Usage example + +```blade +{{-- Phone code + number side by side --}} +
+ + +
+ +{{-- Country / nationality --}} + + +{{-- Timezone --}} + +``` + +--- + +## Modification checklist + +When you modify any of these components, work through this list: + +- [ ] Update `app/Data/Countries.php` if the data structure changes +- [ ] Update all three `.blade.php` component files if shared CSS/JS changes +- [ ] Update the `for*()` method in `Countries.php` that feeds the changed component +- [ ] Re-test every page listed in the usage tables above +- [ ] Add/remove rows from the usage tables if views were added or removed diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..9030888 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,5 @@ +{ + "enabledPlugins": { + "frontend-design@claude-plugins-official": true + } +} diff --git a/.env.example b/.env.example index ea0665b..ba27d4a 100755 --- a/.env.example +++ b/.env.example @@ -57,3 +57,14 @@ VITE_PUSHER_HOST="${PUSHER_HOST}" VITE_PUSHER_PORT="${PUSHER_PORT}" VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" + +# NAS File Manager +NAS_PROTOCOL=smb +NAS_HOST= +NAS_PORT=445 +NAS_USERNAME= +NAS_PASSWORD= +NAS_PATH=/media +NAS_SMB_SHARE= +NAS_SMB_DOMAIN= +NAS_FM_ROUTE_PREFIX=nas-file-manager diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ecc9239 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,156 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +**TAKEONE** (Play) is a Laravel 10 video-sharing platform with sports match annotation, HLS adaptive streaming, GPU-accelerated video processing, playlists, threaded comments, and a super-admin panel. Live at `https://video.takeone.bh`. + +Tech stack: PHP 8.1+, Laravel 10, Blade templating, Vite 5, Axios, FFmpeg/FFProbe with NVIDIA NVENC, SQLite (dev) / MySQL (prod), Laravel Sanctum. + +--- + +## Essential Commands + +### Local Development +```bash +php artisan serve # Backend on http://localhost:8000 +npm run dev # Vite dev server with HMR +npm run build # Production frontend build → public/build/ +``` + +### Database +```bash +php artisan migrate +php artisan db:seed +php artisan tinker +``` + +### Background Workers +```bash +# Video processing (CompressVideoJob, GenerateHlsJob) +php artisan queue:work --queue=video-processing + +# Orphaned file cleanup scheduler (every CLEANUP_INTERVAL_MINUTES, default 30) +php artisan schedule:run # run this every minute via cron +php artisan cleanup:orphaned-videos --dry-run # preview only +php artisan cleanup:orphaned-videos --force # delete orphans +``` + +### Testing +```bash +./vendor/bin/phpunit +./vendor/bin/phpunit --filter "TestName" +./vendor/bin/phpunit tests/Feature/ExampleTest.php +``` +Tests use in-memory SQLite, array mail/cache/session drivers, sync queue, and BCRYPT_ROUNDS=4. Set `APP_ENV=testing`. + +--- + +## Architecture + +### Role System +Three roles stored on `users.role`: `super_admin`, `admin`, `user` (null = user). The `IsSuperAdmin` middleware guards all `/admin/*` routes. Helper methods on the User model: `isSuperAdmin()`, `isAdmin()`, `isUser()`. + +### Video Lifecycle +1. `VideoController@store` validates upload, stores file, extracts metadata via FFProbe (duration, dimensions, orientation). +2. Status column transitions: `pending → processing → ready` (or `failed`). +3. `CompressVideoJob` re-encodes with NVIDIA NVENC (`h264_nvenc`, CRF 23), replaces original if smaller. +4. `GenerateHlsJob` produces 480p / 720p / 1080p HLS variants (`h264_nvenc`, preset p4) as `.m3u8` + `.ts` files. +5. Streaming served at `GET /videos/{video}/hls/{file?}` (master playlist → segments). + +Queue connection is `sync` by default (runs inline); switch `QUEUE_CONNECTION=database` or `redis` for true async. + +### Shorts Auto-Detection +A video is a Short when duration ≤ 60 s **and** portrait orientation. The `is_shorts` DB column allows manual override. Use model scopes `Video::shorts()` / `Video::notShorts()`. + +### Trending Algorithm +`Video::scopeTrending($hours=48, $limit=50)` — weighted score: 70% recent views, 15% view velocity, 10% recency bonus, 5% likes. Only applies to videos < 10 days old with ≥ 5 recent views. + +### Playlist System +- `playlist_videos` pivot carries `position`, `watched_seconds`, `watched`, `added_at`, `last_watched_at`. +- Each user has one `is_default = true` playlist ("Watch Later"). +- `Playlist` model methods: `addVideo()`, `removeVideo()`, `reorder()`, `getNextVideo()`, `getPreviousVideo()`, `updateWatchProgress()`, `canView()`, `canEdit()`. + +### Comment Threading & Mentions +Comments have a `parent_id` for one-level threading (replies). The `parsed_body` accessor converts `@username` syntax into clickable profile links. + +### Comment Timestamp Badges +The `enhanceBody()` function in `components/video-comments.blade.php` converts timestamp syntax written in comments into clickable `._comment-time-badge` spans at render time (client-side). Supported formats: +- `@mm:ss` — single timestamp, jumps to that point (colon separator) +- `@mm.ss` — same, dot separator (legacy, still supported) +- `@mm:ss-mm:ss` or `@mm.ss-mm.ss` — time range; plays from start to end then pauses + +Clicking a badge: scrolls to `#ytpWrap` (smooth), waits 500 ms, seeks `#videoPlayer` to `start`, calls `.play()`, and if a range is specified stops at `end` via a `timeupdate` listener. `@username` mention detection requires the first char to be a letter so it never collides with numeric timestamps. + +### Sports Match Annotation +Videos of type `match` support three related models: +- `MatchRound` — round number, name, `start_time_seconds` +- `MatchPoint` — `timestamp_seconds`, action, competitor (blue/red), running score +- `CoachReview` — time-range segment with coach note and emoji + +All managed via `MatchEventController` under authenticated routes. + +### Key Model Scopes & Accessors +`Video` scopes: `public()`, `visibleTo($user)`, `shorts()`, `notShorts()`, `trending()`. +`Video` accessors: `url`, `thumbnail_url`, `like_count`, `view_count`, `formatted_duration`, `iso_duration`, `open_graph_image`. +`Playlist` accessors: `thumbnail_url`, `video_count`, `total_duration`, `formatted_duration`. + +## Rules + +**Database changes require confirmation** — if any task requires a migration, schema change, or new column, always ask before proceeding. + +**Never use `alert()`, `confirm()`, or `prompt()`** — use toast notifications or inline UI feedback instead. + +**All buttons must use the global `.action-btn` system** — never add custom button CSS. Use these classes: +- `.action-btn` — default (bordered, bg-secondary) +- `.action-btn.action-btn-primary` or `.action-btn.primary` — brand red, for primary/submit actions +- `.action-btn.action-btn-danger` or `.action-btn.danger` — red border/text, for destructive actions +- `.action-btn.icon-only` — square padding, for icon-only buttons + +Structure: ``. The global CSS lives in `layouts/app.blade.php`. + +**Mobile layout uses a native-app scroll model** — on `max-width: 768px`, `html` and `body` are locked (`overflow: hidden; position: fixed`) so the window never scrolls. `.yt-main` is `position: fixed` spanning `top: 56px` to `bottom: calc(56px + env(safe-area-inset-bottom))` with `overflow-y: auto; -webkit-overflow-scrolling: touch`. This keeps the header and bottom nav truly fixed without any JavaScript. Consequences to remember: +- **Never use `position: sticky` inside `.yt-main` on mobile** — sticky elements float over content because the scroll container changed. Override with `position: relative !important` in the mobile media query. +- **Never rely on `window.scrollY` or `window.scroll` events on mobile** — the window doesn't scroll; listen on `document.getElementById('main')` instead. +- **Bottom nav needs no JS transform** — since the window is locked, browser chrome animation never shifts fixed elements. + +**Upload modal and upload page must always stay in sync** — the desktop upload UI lives in `resources/views/layouts/partials/upload-modal.blade.php` and the mobile upload UI lives in `resources/views/videos/create.blade.php`. On mobile, `openUploadModal()` redirects to the create page instead of showing the modal. **Any change made to one must be applied to the other immediately in the same task.** + +**Edit modal and edit page must always stay in sync** — the desktop edit UI lives in `resources/views/layouts/partials/edit-video-modal.blade.php` and the mobile edit UI lives in `resources/views/videos/edit.blade.php`. On mobile (< 992px), `openEditVideoModal(videoId)` redirects to `/videos/{id}/edit` instead of opening the modal. **Any change made to one must be applied to the other immediately in the same task.** — the desktop upload UI lives in `resources/views/layouts/partials/upload-modal.blade.php` and the mobile upload UI lives in `resources/views/videos/create.blade.php`. On mobile, `openUploadModal()` redirects to the create page instead of showing the modal. **Any change made to one must be applied to the other immediately in the same task** — this includes new fields, validation logic, file-type support, JS behaviour, labels, and error handling. Never update only one side. + +**Never build custom dropdowns for country, nationality, phone code, timezone, or currency** — reusable Blade components already exist for these. Always use them; never roll a new ` - @error('name') -
{{ $message }}
- @enderror + + {{-- Name --}} +
+ + + @error('name')
{{ $message }}
@enderror
- -
- - - @error('email') -
{{ $message }}
- @enderror + + {{-- Email --}} +
+ + + @error('email')
{{ $message }}
@enderror
- -
- - + + + - @error('role') -
{{ $message }}
- @enderror + @error('role')
{{ $message }}
@enderror
- -
- -
Change Password (Optional)
- -
- - - @error('new_password') -
{{ $message }}
- @enderror + + {{-- Password section --}} +
+ Change Password + — leave blank to keep current password
- -
- - + +
+
+ + + @error('new_password')
{{ $message }}
@enderror +
+
+ + +
- -
- - Cancel + Cancel
- -
- -
-
-
User Info
-
- -
- {{ $user->name }} -
{{ $user->name }}
-

{{ $user->email }}

+ + {{-- ── Right: user info sidebar ── --}} +
+ + {{-- Profile card --}} +
+
+ {{ $user->name }} +
{{ $user->name }}
+
{{ $user->email }}
@if($user->role === 'super_admin') - Super Admin + Super Admin @elseif($user->role === 'admin') - Admin + Admin @else - User + User @endif
- -
- -
- User ID - #{{ $user->id }} +
+ + {{-- Stats card --}} +
+
+
Account Info
-
- Joined - {{ $user->created_at->format('M d, Y') }} +
+
+ User ID + #{{ $user->id }} +
+
+ Joined + {{ $user->created_at->format('M d, Y') }} +
+
+ Last active + {{ $user->updated_at->diffForHumans() }} +
+
+ Videos + {{ $user->videos->count() }} +
+
+ Email verified + @if($user->email_verified_at) + Verified + @else + Unverified + @endif +
-
- Total Videos - {{ $user->videos->count() }} +
+ + {{-- Impersonate --}} + @if(!$user->isSuperAdmin()) +
+ @csrf + +
+ @endif + + {{-- Danger zone --}} + @if(!$user->isSuperAdmin()) +
+
Danger Zone
+ +
+ @endif + +
+
+ +{{-- ── Delete confirmation dialog ── --}} +@if(!$user->isSuperAdmin()) +
+
+
+
+ Delete User
-
- Email Verified - {{ $user->email_verified_at ? 'Yes' : 'No' }} + +
+
+

You are about to permanently delete {{ $user->name }}.

+
+ + All videos uploaded by this user will also be deleted. This cannot be undone.
+
+@endif + +@endsection + +@section('scripts') + @endsection diff --git a/resources/views/admin/edit-video.blade.php b/resources/views/admin/edit-video.blade.php index 79f9936..eed5ba9 100644 --- a/resources/views/admin/edit-video.blade.php +++ b/resources/views/admin/edit-video.blade.php @@ -3,164 +3,312 @@ @section('title', 'Edit Video') @section('page_title', 'Edit Video') +@section('extra_styles') + +@endsection + @section('content') - + +{{-- ── Page header ── --}} +
+
+ + + +

+ Edit Video +

+
+
+ +{{-- ── Alerts ── --}} @if(session('success')) -