diff --git a/.claude/component-usage.md b/.claude/component-usage.md index 360ec67..bdc30f8 100644 --- a/.claude/component-usage.md +++ b/.claude/component-usage.md @@ -254,6 +254,23 @@ Stored value: IANA timezone string (e.g. `"Asia/Bahrain"`). --- +## `` & `` + +**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. `` is rendered once in `layouts/app.blade.php`; every share entry point uses ``. Never duplicate the modal or hand-write `openShareModal(...)` triggers. `` 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` | `` | Singleton, rendered once for the whole app layout | +| `resources/views/components/video-card.blade.php` | `` | Home/listing card 3-dot menu | +| `resources/views/videos/show.blade.php` | `` + `videoShare()` passes full args | Watch page (mobile + desktop share) | +| `resources/views/videos/partials/video-details.blade.php` | `` | 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: diff --git a/CLAUDE.md b/CLAUDE.md index 34bdfb2..abc4832 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -138,6 +138,20 @@ Structure: ` +@endif diff --git a/resources/views/layouts/partials/share-modal.blade.php b/resources/views/components/share-modal.blade.php similarity index 55% rename from resources/views/layouts/partials/share-modal.blade.php rename to resources/views/components/share-modal.blade.php index b761d8d..81605a2 100644 --- a/resources/views/layouts/partials/share-modal.blade.php +++ b/resources/views/components/share-modal.blade.php @@ -46,6 +46,10 @@ style="display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; border-radius: 50%; background: #6b7280; color: white; text-decoration: none;"> + @@ -63,6 +67,25 @@ + + {{-- Share with members --}} + @@ -80,6 +103,31 @@ background: #4caf50 !important; border-color: #4caf50 !important; } +/* Member share picker */ +.share-member-results { + position: absolute; left: 0; right: 0; top: calc(100% + 4px); z-index: 20; + background: var(--bg-secondary); border: 1px solid var(--border-color); + border-radius: 8px; box-shadow: 0 12px 32px rgba(0,0,0,.5); + max-height: 240px; overflow-y: auto; padding: 4px; +} +.share-member-opt { + display: flex; align-items: center; gap: 10px; padding: 8px 10px; + border-radius: 6px; cursor: pointer; +} +.share-member-opt:hover { background: rgba(255,255,255,.06); } +.share-member-opt img { width: 30px; height: 30px; border-radius: 50%; object-fit: cover; flex-shrink: 0; } +.share-member-opt .smo-name { color: var(--text-primary); font-size: 14px; font-weight: 600; line-height: 1.2; } +.share-member-opt .smo-handle { color: var(--text-secondary); font-size: 12px; } +.share-member-empty { padding: 10px; color: var(--text-secondary); font-size: 13px; text-align: center; } +.share-member-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; } +.share-member-chip { + display: inline-flex; align-items: center; gap: 6px; padding: 4px 6px 4px 4px; + background: rgba(230,30,30,.12); border: 1px solid rgba(230,30,30,.3); + border-radius: 20px; color: var(--text-primary); font-size: 13px; +} +.share-member-chip img { width: 22px; height: 22px; border-radius: 50%; object-fit: cover; } +.share-member-chip button { background: none; border: none; color: var(--text-secondary); cursor: pointer; padding: 0 2px; line-height: 1; } +.share-member-chip button:hover { color: #e61e1e; } diff --git a/resources/views/components/sports-image.blade.php b/resources/views/components/sports-image.blade.php new file mode 100644 index 0000000..e502121 --- /dev/null +++ b/resources/views/components/sports-image.blade.php @@ -0,0 +1,12 @@ +@props(['name', 'label' => 'Upload & crop']) + +{{-- A cropper-backed image field. The matching + (defined once in the sports-match modal) writes the cropped file onto the + hidden input below and updates #sm-prev-{name}. --}} +
merge(['class' => 'sm-img-field']) }} data-img="{{ $name }}"> + + +
diff --git a/resources/views/components/sports-section.blade.php b/resources/views/components/sports-section.blade.php new file mode 100644 index 0000000..b118179 --- /dev/null +++ b/resources/views/components/sports-section.blade.php @@ -0,0 +1,20 @@ +@props(['id', 'title', 'icon' => 'bi-sliders']) + +{{-- One collapsible "Optional" block inside the sports-match accordion. + Independent toggle (no data-bs-parent) so several can be open at once. --}} +
+

+ +

+
+
+ {{ $slot }} +
+
+
diff --git a/resources/views/components/video-actions.blade.php b/resources/views/components/video-actions.blade.php index e90b5d0..e966c6f 100644 --- a/resources/views/components/video-actions.blade.php +++ b/resources/views/components/video-actions.blade.php @@ -139,6 +139,7 @@ class="action-btn desktop-action subscribe-toggle-btn {{ $isSubscribed ? 'subscribed' : '' }}" data-channel-id="{{ $video->user_id }}" data-subscribe-url="{{ route('channel.subscribe', $video->user_id) }}" + data-source-video-id="{{ $video->id }}" data-subscribed="{{ $isSubscribed ? 'true' : 'false' }}"> @@ -168,7 +169,7 @@ @if ($video->isShareable()) @endif @@ -238,6 +239,7 @@ class="dropdown-item subscribe-toggle-btn {{ $isSubscribed ? 'subscribed' : '' }}" data-channel-id="{{ $video->user_id }}" data-subscribe-url="{{ route('channel.subscribe', $video->user_id) }}" + data-source-video-id="{{ $video->id }}" data-subscribed="{{ $isSubscribed ? 'true' : 'false' }}"> @@ -271,7 +273,7 @@ @if ($video->isShareable()) @endif @@ -337,9 +339,15 @@ el.textContent = Number(el.dataset.count).toLocaleString() + ' subscribers'; }); + var srcVid = btn.dataset.sourceVideoId || ''; + var body = srcVid && nowSub ? new URLSearchParams({ source_video_id: srcVid }).toString() : null; fetch(url, { method: 'POST', - headers: { 'X-CSRF-TOKEN': csrfToken, 'Accept': 'application/json' }, + headers: Object.assign( + { 'X-CSRF-TOKEN': csrfToken, 'Accept': 'application/json' }, + body ? { 'Content-Type': 'application/x-www-form-urlencoded' } : {} + ), + body: body, credentials: 'same-origin', }) .then(function (r) { @@ -516,10 +524,10 @@ if (!window._slideshowDlInit) { // Share the version the viewer is on: append ?track={id} so the recipient's player // opens directly on that language (window._ytpTrackId; 0 = primary → plain link). - window.shareCurrent = function (baseUrl, title, recordUrl, emailUrl) { + window.shareCurrent = function (baseUrl, title, recordUrl, emailUrl, membersUrl) { var t = window._ytpTrackId || 0; var url = t ? baseUrl + (baseUrl.indexOf('?') === -1 ? '?' : '&') + 'track=' + t : baseUrl; - if (typeof window.openShareModal === 'function') window.openShareModal(url, title, recordUrl, emailUrl); + if (typeof window.openShareModal === 'function') window.openShareModal(url, title, recordUrl, emailUrl, membersUrl); }; window.startSlideshowDownload = function (routeKey) { diff --git a/resources/views/components/video-card.blade.php b/resources/views/components/video-card.blade.php index 4d05b96..89fa933 100644 --- a/resources/views/components/video-card.blade.php +++ b/resources/views/components/video-card.blade.php @@ -3,7 +3,7 @@ @php use App\Data\Languages; -$videoUrl = $video ? asset('storage/videos/' . $video->filename) : null; +$videoUrl = $video ? route('videos.stream', $video) : null; $thumbnailUrl = $video && $video->thumbnail ? route('media.thumbnail', $video->thumbnail) : ($video ? 'https://picsum.photos/seed/' . $video->id . '/640/360' : 'https://picsum.photos/seed/random/640/360'); @@ -141,9 +141,9 @@ $sizeClasses = match($size) { @endif @if($video->isShareable())
  • - + Share - +
  • @endif
  • @@ -997,7 +997,7 @@ function playVideo(element) { if (!video) return; video.currentTime = 0; const isAudio = element.dataset.audio === 'true'; - video.volume = isAudio ? 0.4 : 0.10; + video.volume = 0.5; if (isAudio) { // Keep thumbnail visible — just play audio, show equalizer video.play().catch(function() {}); diff --git a/resources/views/components/video-insights.blade.php b/resources/views/components/video-insights.blade.php index 67eb5b8..f50e58d 100644 --- a/resources/views/components/video-insights.blade.php +++ b/resources/views/components/video-insights.blade.php @@ -10,9 +10,10 @@ @if($isVideoOwner) @yield('extra_styles') + + {{-- Device fingerprint: computes a stable per-device hash on first visit, + caches it in localStorage + the `_fp` cookie so the server can dedupe guests + across IP/VPN/country changes. Loaded async — never blocks paint. --}} + @@ -1065,13 +1095,14 @@ @auth @include('layouts.partials.upload-modal') @include('layouts.partials.edit-video-modal') + @include('layouts.partials.sports-match-modal') @endauth @include('layouts.partials.add-to-playlist-modal') - @include('layouts.partials.share-modal') + @auth @@ -1118,8 +1149,7 @@ @endif @@ -1426,6 +1456,7 @@ case 'new_user': return '🎉 ' + actor + ' just joined TAKEONE!'; case 'new_subscriber': return '🔔 ' + actor + ' subscribed to your channel'; case 'video_like': return '❤️ ' + actor + ' liked your video ' + title; + case 'video_shared': return '📤 ' + actor + ' shared a video with you: ' + title + (d.message ? ' "' + escHtml(d.message) + '"' : ''); case 'new_post': return '📝 ' + actor + ' posted something new'; default: return actor + ' uploaded a new video: ' + title; } @@ -2110,5 +2141,34 @@ })(); + {{-- Profile-visit tracking: any link with data-profile-visit-url fires a beacon on click --}} + + diff --git a/resources/views/layouts/partials/sports-match-modal.blade.php b/resources/views/layouts/partials/sports-match-modal.blade.php new file mode 100644 index 0000000..2d3942f --- /dev/null +++ b/resources/views/layouts/partials/sports-match-modal.blade.php @@ -0,0 +1,887 @@ +{{-- ════════════════════════════════════════════════════════════════════════ + Create / Edit Sports Match — progressive-disclosure modal. + Opened from the front-end "Sports" chooser card via openSportsMatchModal(). + A match always belongs to one of the user's videos (video_id required). + Only the basic section is needed for a first (draft) save; everything else + lives in collapsed sections and can be completed later by editing the same + record. Bootstrap 5 modal + collapsible blocks. Field names map 1:1 to + SportsMatchController validation. + ════════════════════════════════════════════════════════════════════════ --}} + + +{{-- ── Repeatable row templates ─────────────────────────────────────────── --}} + + + + + + +{{-- ── Image croppers (outside the form so their inner file inputs aren't submitted) ── + Six form-mode croppers write the cropped file straight onto each hidden input; + one callback-mode cropper serves all dynamic official rows. --}} + + + +