- New SportsMatch model/controller and sports UI components/modal - Move share-modal to a reusable x-share-modal/x-share-button component - Add VideoSharedWithUser notification and share-to-members flow - Device/user-agent tracking on views, downloads, share accesses - ProfileVisit model + migration; subscription source tracking - Email thumbnail support; remove stale TODO files
19 KiB
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.phpresources/views/components/country-select.blade.phpresources/views/components/timezone-select.blade.phpresources/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/videos/create.blade.php |
primary_language (id="video_language_create") |
Video-mode language field inside #basic-fields-create (generic/match). setAudioMode() swaps name="primary_language" between this and primary_language_create so only the active mode's picker submits |
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
{{-- 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
/>
<x-share-modal> & <x-share-button>
Files: resources/views/components/share-modal.blade.php (singleton modal + openShareModal() JS), resources/views/components/share-button.blade.php (trigger).
Rule: The only sanctioned way to share. <x-share-modal /> is rendered once in layouts/app.blade.php; every share entry point uses <x-share-button :video="$video" />. Never duplicate the modal or hand-write openShareModal(...) triggers. <x-share-button> props: video (required), tag (button|a); extra attributes forwarded; slot overrides the label. Offers copy-link, social, send-by-email, and share-to-members (notification + email).
| View file | Usage | Notes |
|---|---|---|
resources/views/layouts/app.blade.php |
<x-share-modal /> |
Singleton, rendered once for the whole app layout |
resources/views/components/video-card.blade.php |
<x-share-button :video tag="a" class="dropdown-item"> |
Home/listing card 3-dot menu |
resources/views/videos/show.blade.php |
<x-share-button :video class="yt-action-btn"> + videoShare() passes full args |
Watch page (mobile + desktop share) |
resources/views/videos/partials/video-details.blade.php |
<x-share-button :video class="action-btn"> |
Watch-page details share button |
resources/views/components/video-actions.blade.php |
shareCurrent(...) → openShareModal(...) |
Main watch-page share; passes email + members URLs |
Known not-yet-migrated: resources/views/videos/shorts.blade.php (JS feed share, partial args) and resources/views/playlists/show.blade.php (playlists have no email/members endpoints — video-only feature). Migrate shorts when touched.
Modification checklist
When you modify any of these components, work through this list:
- Update
app/Data/Countries.phpif the data structure changes - Update all three
.blade.phpcomponent files if shared CSS/JS changes - Update the
for*()method inCountries.phpthat 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