Storage structure
- All audio tracks (primary + per-language) now live in one folder per song with
unique lowercase names ({slug}-{lang}-{id}); no more tracks/ subfolder.
- Generated renders (download video + HLS) moved into the song's local-only cache/
subfolder, separated from source files (never synced to NAS, safe to wipe).
- tracks:reorganize artisan command (dry-run default) consolidates legacy files,
updates DB paths, and deletes orphans + empty folders.
- CLAUDE.md documents the canonical layout as a global rule (identical local + NAS).
Version-aware download & share
- Download MP3/Video and Share now act on the version being played; ?track={id}
is carried through share links and auto-selects audio + title + flag + about +
OG/meta on open.
GPU + visualizer
- Setting::gpuUsable() runs a cached health probe (nvidia-smi + nvenc smoke test,
256x144) before sending any encode to the GPU; falls back to CPU otherwise.
- Visualizer "Download Video" bakes in equal-width, cover-coloured, translucent
frequency bars; loop-filter rebuild makes generation ~25x faster.
Image cropper
- result-callback mode + per-song cover-slide cropper in upload/edit (modal + mobile).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
265 lines
17 KiB
Markdown
265 lines
17 KiB
Markdown
# Reusable Select Component Usage
|
||
|
||
This file tracks every page/partial that uses `<x-phone-code-select>`, `<x-country-select>`, `<x-timezone-select>`, or `<x-language-select>`.
|
||
**Update this file whenever you add or remove a component from a view.**
|
||
|
||
When modifying any component or its data source, check all pages in the relevant section below and verify the change works correctly in each context.
|
||
|
||
---
|
||
|
||
## Data sources
|
||
|
||
**`app/Data/Countries.php`** — `App\Data\Countries`
|
||
|
||
| Method | Used by component |
|
||
|---|---|
|
||
| `Countries::forPhoneCode()` | `<x-phone-code-select>` |
|
||
| `Countries::forCountry()` | `<x-country-select>` |
|
||
| `Countries::forTimezone()` | `<x-timezone-select>` |
|
||
| `Countries::all()` | All three (via the above methods) |
|
||
|
||
Adding or renaming a field in `Countries::all()` requires updating the corresponding `for*()` method too.
|
||
|
||
**`app/Data/Languages.php`** — `App\Data\Languages`
|
||
|
||
| Method | Used by component |
|
||
|---|---|
|
||
| `Languages::forLanguage()` | `<x-language-select>` |
|
||
| `Languages::all()` | Via `forLanguage()` |
|
||
|
||
Arabic and English are pinned to the top of the list; all others are sorted alphabetically by English name. Stored value is the ISO 639-1 code (e.g. `"ar"`, `"en"`).
|
||
|
||
---
|
||
|
||
## Shared CSS / JS
|
||
|
||
The `.csd-*` CSS rules and the `window.CSD` class are duplicated across all four component files inside `@once` guards. If you change the look or behaviour of the dropdown, **update all four component files**:
|
||
|
||
- `resources/views/components/phone-code-select.blade.php`
|
||
- `resources/views/components/country-select.blade.php`
|
||
- `resources/views/components/timezone-select.blade.php`
|
||
- `resources/views/components/language-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.
|
||
|
||
`language-select` also emits an extra `@once('lsd-badge-styles')` block for the `.lsd-code` ISO badge that appears in place of a flag emoji.
|
||
|
||
---
|
||
|
||
## `<x-video-insights>`
|
||
|
||
**File:** `resources/views/components/video-insights.blade.php`
|
||
**Props:** `:video` — `Video` model instance.
|
||
**Behaviour:** Renders the Insights tab panel (`<div class="vdb-panel" id="vdb-insights">`), 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) |
|
||
|
||
---
|
||
|
||
## `<x-social-links-editor>`
|
||
|
||
**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 `<form>`**.
|
||
**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')` |
|
||
|
||
---
|
||
|
||
## `<x-date-picker>`
|
||
|
||
**File:** `resources/views/components/date-picker.blade.php`
|
||
**Stored value:** `YYYY-MM-DD` string in a hidden input (same format as `<input type="date">`).
|
||
**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 `<input type="date">` |
|
||
| `resources/views/auth/register.blade.php` | `birthday` | Registration form — mandatory |
|
||
|
||
---
|
||
|
||
## `<x-image-cropper>`
|
||
|
||
**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), `target-input` (form mode: ID of file input the cropped File is set on), `preview-img` (ID of `<img>` updated with the cropped preview), `output-width` (final output px width), `result-callback` (callback mode: name of a global JS fn given the cropped `File`).
|
||
**Three operating modes (mutually exclusive, checked in this order):** (1) **callback mode** — when `result-callback` is set, both "Crop & Save" and "Upload as-is" hand the resulting `File` to `window[resultCallback](file)` and do **not** auto-close; the host fn decides when to close (`closeCropperModal(id)`) or load the next image. Used for multi-image queues (cover slides). (2) **form mode** — when `target-input` is set, the cropped File is placed on that file input (DataTransfer) and a `change` event is dispatched. (3) **server mode** — otherwise POSTs base64 to `/image-upload`, optionally POSTs path to `update-url`, then calls `callback(url)`.
|
||
**Behaviour:** Renders a full-screen dark-themed modal with Cropme.js. Shows camera icon on avatar/banner hover (owner only). Exposes per-id globals: `openCropperModal_{id}()`, `tcPreload_{id}(file)`, `closeCropperModal(id)`. 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-t1-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 |
|
||
| `resources/views/layouts/partials/edit-video-modal.blade.php` | `slides_edit` — square 448×252 | Callback mode; `result-callback=editSlidesCropDone`; crops each cover slide before it enters the strip (queues multiple) |
|
||
| `resources/views/layouts/partials/upload-modal.blade.php` | `slides_upload` — square 448×252 | Callback mode; `result-callback=uploadSlidesCropDone`; cover-slide crop queue |
|
||
| `resources/views/videos/create.blade.php` | `slides_create_mobile` — square 448×252 | Mobile; callback mode; `result-callback=cSlidesCropDone`; cover-slide crop queue |
|
||
| `resources/views/videos/edit.blade.php` | `slides_edit_mobile` — square 448×252 | Mobile; callback mode; `result-callback=epSlidesCropDone`; cover-slide crop queue |
|
||
|
||
---
|
||
|
||
## `<x-gender-select>`
|
||
|
||
**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 |
|
||
|
||
---
|
||
|
||
## `<x-phone-code-select>`
|
||
|
||
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 |
|
||
|
||
---
|
||
|
||
## `<x-country-select>`
|
||
|
||
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 |
|
||
|
||
---
|
||
|
||
## `<x-timezone-select>`
|
||
|
||
Stored value: IANA timezone string (e.g. `"Asia/Bahrain"`).
|
||
|
||
| View file | Field name | Notes |
|
||
|---|---|---|
|
||
| `resources/views/user/profile.blade.php` | `timezone` | Edit Profile form |
|
||
|
||
---
|
||
|
||
## `<x-track-editor-form>`
|
||
|
||
**File:** `resources/views/components/track-editor-form.blade.php`
|
||
**Props:** `prefix` (default `'t1'`), `isPrimary` (bool, default `false`), `languageName`, `languageId`, `titleName`, `titleId`, `descName`, `descId`, `videoFileInputId`.
|
||
**Behaviour:** Renders the full track editor form panel shown inside the Track Editor popup. Contains: optional Primary Track banner (when `:is-primary="true"`), language dropdown (`<x-language-select>`), title input, description rich-text editor (`<x-rich-text-editor>`), video+thumbnail zone (hidden, shown for video/match type via `_editApplyMode`), and audio+slides zone (hidden, shown for music type). All element IDs are prefixed with `edit-{prefix}-*`. JS functions `editHandleThumbnail(input, prefix)`, `editRemoveThumbnail(event, prefix)`, `editSlidesZoneClick(event, tid)`, `editHandleSlides(files, tid)`, `editClearSlides(event, tid)` all accept the prefix/tid param. Adding cover slides routes through the `slides_edit` image-cropper (callback mode `editSlidesCropDone`) — each picked/dropped image is cropped to 16:9 before entering `_editSlidesData`; the live `<x-image-cropper>` instances are defined in edit-video-modal.blade.php.
|
||
|
||
| View file | Prefix used | Notes |
|
||
|---|---|---|
|
||
| `resources/views/layouts/partials/edit-video-modal.blade.php` | `t1` | Primary track only; secondary tracks are built via JS (`_editAddExistingTrack`) |
|
||
|
||
---
|
||
|
||
## `<x-language-select>`
|
||
|
||
**File:** `resources/views/components/language-select.blade.php`
|
||
**Data source:** `app/Data/Languages.php` — `Languages::forLanguage()`
|
||
**Stored value:** ISO 639-1 code (e.g. `"ar"`, `"en"`, `"fr"`).
|
||
**Props:** `name`, `id`, `value`, `label`, `placeholder`, `required`, `class`, `style`.
|
||
**Icon:** 2-letter uppercase ISO code rendered as a monospace badge (`.lsd-code`) — no flag emoji.
|
||
**Arabic and English are always pinned to the top** of the list; all other languages are alphabetical by English name.
|
||
|
||
**Rule:** This component must be used for every language picker in the application. Never build a custom `<select>` or inline list for language selection.
|
||
|
||
| View file | Field name | Notes |
|
||
|---|---|---|
|
||
| `resources/views/layouts/partials/upload-modal.blade.php` | `primary_language` | Inside accordion track 1 body (`id="lang-tracks-section-modal"`); extra track language rows use `LANG_OPTIONS_MODAL` JS constant for inline dynamic CSD |
|
||
| `resources/views/videos/create.blade.php` | `primary_language` | Inside accordion track 1 body (`id="lang-tracks-section-create"`); extra track language rows use `LANG_OPTIONS_CREATE` JS constant for inline dynamic CSD |
|
||
| `resources/views/components/track-editor-form.blade.php` | `$languageName` (prop) | Rendered inside the track editor form; used for primary track in edit-video-modal (prefix `t1`) |
|
||
| `resources/views/videos/edit.blade.php` | `primary_language` | Inside `@else` (audio only) block; pre-populated with `value="{{ $video->language ?? '' }}"` |
|
||
|
||
---
|
||
|
||
## `<x-rich-text-editor>`
|
||
|
||
**File:** `resources/views/components/rich-text-editor.blade.php`
|
||
**Props:** `name`, `id`, `value` (initial HTML), `placeholder`, `class`, `style`, `minHeight` (default `110px`).
|
||
**Server sanitizer:** `app/Support/HtmlSanitizer.php` — `HtmlSanitizer::clean()` (allowlist, on save) and `HtmlSanitizer::render()` (display; upgrades legacy plain text).
|
||
**Stored value:** sanitized HTML. Allowed tags: `p, br, div, span, b/strong, i/em, u, s, h2, h3, ul, ol, li, blockquote, a`. `<a>` may carry `href` (http/https/mailto only), `target="_blank"` (auto `rel=noopener`), and `class` limited to `.action-btn` variants (button links). `style` limited to `text-align`.
|
||
**Behaviour:** Renders a hidden `<textarea class="rte-source" name id>` as the form field (source of truth) wrapped in `.rte-wrap`. `window.RTE` builds the toolbar + `contenteditable` editor in JS (so Blade-rendered and JS-generated rows share one implementation) and a `MutationObserver` auto-inits any `.rte-wrap` added later (modals, cloned track rows). Toolbar: bold, italic, underline, strikethrough, heading (H2), bullet/numbered list, quote, align left/center/right, link, button-link (`.action-btn`), emoji, clear formatting. Editor↔textarea stay synced via `input`; external code that sets `textarea.value` must dispatch `new Event('rte:refresh')` to update the editor.
|
||
**Rendering:** display HTML via `{!! \App\Support\HtmlSanitizer::render($value) !!}`; truncation is CSS-clamp (`.vdb-clamp`) + JS overflow check, never character-truncation (would break tags).
|
||
|
||
| View file | Field name / id | Notes |
|
||
|---|---|---|
|
||
| `resources/views/components/track-editor-form.blade.php` | `$descName` / `$descId` | Description in the Track Editor popup; primary + JS-cloned template tracks (edit-video-modal) |
|
||
| `resources/views/layouts/partials/upload-modal.blade.php` | (no name) `lt-track1-desc-modal` + `extra_track_descriptions[]` | Primary desc collected manually into FormData; extra-track rows generated via JS template string (`.rte-wrap` markup) |
|
||
| `resources/views/videos/create.blade.php` | `description` `video-description`, (no name) `lt-track1-desc-create`, `extra_track_descriptions[]` | Mobile upload; extra rows are JS template literal markup |
|
||
| `resources/views/videos/edit.blade.php` | `description` `edit-description`, `track_description_updates[{id}]` | Mobile edit; per-track rows rendered via Blade `@foreach` |
|
||
|
||
**Render sites (display):** `resources/views/videos/partials/description-box.blade.php` (generic/match, also music), `resources/views/videos/partials/audio-player.blade.php` (`_updateDescriptionBox` per-track switch). SPA swaps re-run `_vdbCheckOverflow()` in `generic.blade.php` / `match.blade.php`.
|
||
|
||
---
|
||
|
||
## Usage example
|
||
|
||
```blade
|
||
{{-- Phone code + number side by side --}}
|
||
<div style="display:flex; gap:8px;">
|
||
<x-phone-code-select
|
||
name="phone_code"
|
||
value="+973|BH"
|
||
label="Phone"
|
||
required
|
||
style="width:140px; flex-shrink:0;"
|
||
/>
|
||
<input type="tel" name="phone_number" class="form-control">
|
||
</div>
|
||
|
||
{{-- Country / nationality --}}
|
||
<x-country-select
|
||
name="nationality"
|
||
label="Nationality"
|
||
placeholder="Select nationality"
|
||
value="{{ old('nationality', $user->nationality) }}"
|
||
required
|
||
/>
|
||
|
||
{{-- Timezone --}}
|
||
<x-timezone-select
|
||
name="timezone"
|
||
label="Timezone"
|
||
value="{{ old('timezone', $user->timezone ?? 'Asia/Bahrain') }}"
|
||
required
|
||
/>
|
||
|
||
{{-- Language --}}
|
||
<x-language-select
|
||
name="language"
|
||
label="Language"
|
||
placeholder="Select language"
|
||
value="{{ old('language', $video->language ?? '') }}"
|
||
required
|
||
/>
|
||
```
|
||
|
||
---
|
||
|
||
## 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
|