diff --git a/app/Http/Controllers/MatchEventController.php b/app/Http/Controllers/MatchEventController.php index c2e7c33..75acd16 100644 --- a/app/Http/Controllers/MatchEventController.php +++ b/app/Http/Controllers/MatchEventController.php @@ -207,6 +207,8 @@ class MatchEventController extends Controller 'note' => 'required|string|max:1000', 'coach_name' => 'required|string|max:100', 'emoji' => 'nullable|string|max:10', + 'position_x' => 'nullable|numeric|between:0,1', + 'position_y' => 'nullable|numeric|between:0,1', ]); // Check if user owns the video @@ -222,6 +224,8 @@ class MatchEventController extends Controller 'note' => $request->note, 'coach_name' => $request->coach_name, 'emoji' => $request->emoji ?? 'πŸ”₯', + 'position_x' => $request->position_x, + 'position_y' => $request->position_y, ]); return response()->json([ @@ -239,6 +243,8 @@ class MatchEventController extends Controller 'note' => 'required|string|max:1000', 'coach_name' => 'required|string|max:100', 'emoji' => 'nullable|string|max:10', + 'position_x' => 'nullable|numeric|between:0,1', + 'position_y' => 'nullable|numeric|between:0,1', ]); // Check if user owns the video @@ -252,6 +258,8 @@ class MatchEventController extends Controller 'note' => $request->note, 'coach_name' => $request->coach_name, 'emoji' => $request->emoji, + 'position_x' => $request->position_x, + 'position_y' => $request->position_y, ]); return response()->json([ diff --git a/app/Models/CoachReview.php b/app/Models/CoachReview.php index c906826..0e5d954 100644 --- a/app/Models/CoachReview.php +++ b/app/Models/CoachReview.php @@ -18,6 +18,13 @@ class CoachReview extends Model 'note', 'coach_name', 'emoji', + 'position_x', + 'position_y', + ]; + + protected $casts = [ + 'position_x' => 'float', + 'position_y' => 'float', ]; public function video(): BelongsTo diff --git a/database/migrations/2026_08_08_000001_add_position_to_coach_reviews.php b/database/migrations/2026_08_08_000001_add_position_to_coach_reviews.php new file mode 100644 index 0000000..c50a1b1 --- /dev/null +++ b/database/migrations/2026_08_08_000001_add_position_to_coach_reviews.php @@ -0,0 +1,28 @@ +decimal('position_x', 6, 4)->nullable()->after('emoji'); + $table->decimal('position_y', 6, 4)->nullable()->after('position_x'); + }); + } + + public function down(): void + { + Schema::table('coach_reviews', function (Blueprint $table) { + $table->dropColumn(['position_x', 'position_y']); + }); + } +}; diff --git a/public/coach-review-mockups.html b/public/coach-review-mockups.html new file mode 100644 index 0000000..a58d6e1 --- /dev/null +++ b/public/coach-review-mockups.html @@ -0,0 +1,418 @@ + + + + + +Coach Review β€” Design Mockups + + + + + +
+

Coach Review Card β€” 4 design directions

+

Same content, four different visual approaches. Pick one and I'll build it.

+
+ +
+ + +
+

A Sticky-note

+

Warm amber tint, chunky emoji, feels like a coach's Post-it on a locker.

+
+
+ πŸ‘€ +
+

Keep Your Hand Up

+
+
+
+ GhassanΒ·00:48 – 00:52 +
+
+ + + +
+
+
+
+
+ πŸ€” +
+

Missed the counter opportunity β€” great angle but no follow-up

+
+
+
+ Coach SaraΒ·02:05 – 02:20 +
+
+ + + +
+
+
+
+
+
+ + +
+

B Timeline dot

+

Vertical spine with red dots. Reads like a session log β€” nice for many notes.

+
+
+
+ +
00:48 β€” 00:52
+
+ πŸ‘€ +
+

Keep Your Hand Up

+
Ghassan
+
+
+ + + +
+
+
+
+ +
02:05 β€” 02:20
+
+ πŸ€” +
+

Missed the counter β€” no follow-up after the angle change

+
Coach Sara
+
+
+ + + +
+
+
+
+ +
03:25
+
+ πŸ˜„ +
+

Excellent exit and re-entry β€” perfect execution

+
Ghassan
+
+
+ + +
+
+
+
+
+
+ + +
+

C Media card

+

Big emoji tile on the left, blue time badge β€” feels like a video-platform "content card".

+
+
+
πŸ‘€
+
+
00:48 β†’ 00:52
+

Keep Your Hand Up

+
+ Ghassan +
+ + + +
+
+
+
+
+
πŸ€”
+
+
02:05 β†’ 02:20
+

Missed the counter β€” no follow-up after the angle change

+
+ Coach Sara +
+ + + +
+
+
+
+
+
+ + +
+

D Chat-bubble

+

Emoji as coach's avatar + message bubble. Feels like WhatsApp β€” casual + dense.

+
+
+
πŸ‘€
+
+

Keep Your Hand Up

+
+
GhassanΒ·00:48–00:52
+
+ + + +
+
+
+
+
+
πŸ€”
+
+

Missed the counter β€” no follow-up after the angle change

+
+
Coach SaraΒ·02:05–02:20
+
+ + + +
+
+
+
+
+
πŸ˜„
+
+

Excellent exit and re-entry β€” perfect execution

+
+
GhassanΒ·03:25
+
+ + +
+
+
+
+
+
+ +
+ + + diff --git a/resources/views/components/video-player.blade.php b/resources/views/components/video-player.blade.php index f87e8e2..f22b14d 100644 --- a/resources/views/components/video-player.blade.php +++ b/resources/views/components/video-player.blade.php @@ -223,6 +223,10 @@ /* default aspect ratio; overridden per orientation */ aspect-ratio: 16/9; max-height: 70vh; + /* Establish a size container so descendants can size themselves relative + to the player width (used by the coach-note overlay to scale). */ + container-type: inline-size; + container-name: ytpwrap; } .ytp-wrap.portrait { aspect-ratio: 9/16; max-height: 80vh; width: auto; max-width: 100%; margin: 0 auto; } .ytp-wrap.square { aspect-ratio: 1/1; max-height: 75vh; max-width: 75vh; margin: 0 auto; } diff --git a/resources/views/videos/types/match.blade.php b/resources/views/videos/types/match.blade.php index f44ab6b..f79803f 100644 --- a/resources/views/videos/types/match.blade.php +++ b/resources/views/videos/types/match.blade.php @@ -119,15 +119,17 @@ .coach-note-overlay { position: absolute; left: 50%; - bottom: 48px; + bottom: 6cqi; transform: translateX(-50%); max-width: min(80%, 900px); width: fit-content; - padding: 10px 14px; - border-radius: 8px; + /* Scale font-size, padding, border-radius with the player width so + the note grows/shrinks proportionally on resize / fullscreen. */ + font-size: clamp(11px, 2.2cqi, 26px); + padding: clamp(4px, 0.9cqi, 14px) clamp(8px, 1.6cqi, 20px); + border-radius: clamp(4px, 0.6cqi, 10px); background: rgba(0, 0, 0, 0.68); color: #fff; - font-size: 16px; font-weight: 600; line-height: 1.35; text-align: center; @@ -139,8 +141,36 @@ backdrop-filter: blur(3px); } - .coach-note-overlay.show { - display: block; + .coach-note-overlay.show { display: block; } + + /* Drag mode (while composing / editing a coach note) β€” reveal grab UX. + Bumped above the capture strip (z-index 60) so it can be dragged into + the area the strip currently covers. */ + .coach-note-overlay.draggable { + pointer-events: auto; + cursor: grab; + outline: 2px dashed rgba(230, 30, 30, .65); + outline-offset: 2px; + user-select: none; + z-index: 70; + } + .coach-note-overlay.draggable:active, + .coach-note-overlay.dragging { cursor: grabbing; } + .coach-note-overlay.draggable::after { + content: '↕ drag me'; + position: absolute; + top: calc(-1.4em - 4px); left: 50%; transform: translateX(-50%); + font-size: 0.6em; font-weight: 700; color: var(--brand-red, #e61e1e); + background: rgba(0,0,0,.7); padding: 1px 6px; border-radius: 3px; + white-space: nowrap; + } + /* When positioned by the user we anchor by the overlay's CENTER at + (left, top). Using percentage left/top + translate(-50%,-50%) makes + the overlay scale correctly with the video area on any resize / + fullscreen. */ + .coach-note-overlay.positioned { + bottom: auto; right: auto; + transform: translate(-50%, -50%); } /* Match Highlights Toggle Button */ @@ -1287,67 +1317,120 @@ box-shadow: none; } - .review-primary-row { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 4px; - padding: 0; - background: transparent; - border: none; - border-radius: 0; + /* ═══════════════════════════════════════════════════════════════ + Coach-review timeline β€” vertical spine with red dots. Each review + is [dot] [time label] [card with emoji + note + author + tools]. + ═══════════════════════════════════════════════════════════════ */ + #reviewEvents { position: relative; padding-left: 30px; } + /* The spine line runs behind every item */ + #reviewEvents::before { + content: ''; position: absolute; + left: 11px; top: 6px; bottom: 6px; + width: 2px; background: rgba(255,255,255,.08); + } + /* Empty state should NOT show the spine */ + #reviewEvents .event-empty { padding-left: 0; } + #reviewEvents:has(> .event-empty:only-child)::before { display: none; } + + .review-card { + position: relative; + padding: 0 !important; + background: transparent !important; + border: none !important; + margin: 0 0 22px; + cursor: default; /* the card is only a container now; interactive parts handle their own clicks */ + } + .review-card:last-child { margin-bottom: 0; } + + /* Red dot on the spine, aligned with the time label */ + .review-card::before { + content: ''; + position: absolute; + left: -25px; top: 4px; + width: 12px; height: 12px; border-radius: 50%; + background: var(--brand-red, #e61e1e); + box-shadow: 0 0 0 3px rgba(230,30,30,.22); } - .review-primary-row .emoji { - font-size: 16px; - line-height: 1; - flex-shrink: 0; + /* Time label ("chapter title" style above the card) */ + .review-side { + display: flex; align-items: center; gap: 8px; + margin-bottom: 6px; } - - .review-time { - font-size: 12px; - font-weight: 500; - color: #3ea6ff; - line-height: 1.2; + .review-emoji-badge { display: none; } /* emoji shown inside the card, not header */ + .review-range { + font-size: 11px; font-weight: 700; letter-spacing: .06em; + color: var(--brand-red, #e61e1e); text-transform: uppercase; white-space: nowrap; - flex-shrink: 0; + font-variant-numeric: tabular-nums; } + .review-range-sep { margin: 0 4px; opacity: .7; } - .review-content { - margin-left: 0; + /* The card body */ + .review-main { + display: flex; align-items: flex-start; gap: 10px; + background: rgba(255,255,255,.03); + border: 1px solid rgba(255,255,255,.06); + border-radius: 8px; + padding: 10px 12px; + cursor: pointer; + transition: background .12s, border-color .12s; } - - .review-note-title { + .review-main:hover { + background: rgba(255,255,255,.05); + border-color: rgba(255,255,255,.12); + } + /* Bring the emoji into the card as the leading indicator */ + .review-main::before { + content: attr(data-emoji); + font-size: 18px; line-height: 1.3; flex-shrink: 0; + } + .review-body-col { flex: 1; min-width: 0; } + .review-note { color: var(--text-primary); - font-size: 14px; - font-weight: 700; - line-height: 1.35; - margin: 0; + font-size: 14px; font-weight: 500; line-height: 1.4; + margin: 0 0 4px; + word-break: break-word; } - - .review-author-bar { - margin-top: 4px; - display: flex; - align-items: center; - gap: 6px; - color: var(--text-secondary); - font-size: 12px; - line-height: 1.3; + .review-foot { + display: flex; align-items: center; justify-content: space-between; + gap: 8px; min-width: 0; } - - .review-author-avatar { - width: 14px; - height: 14px; - border-radius: 50%; - object-fit: cover; - border: 1px solid rgba(255, 255, 255, 0.25); - flex-shrink: 0; + .review-author { + display: inline-flex; align-items: center; gap: 6px; min-width: 0; + color: var(--text-secondary); font-size: 11.5px; } - + .review-author-dot { display: none; } .review-author-name { + color: var(--text-secondary); font-weight: 500; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + } + + .review-tools { + display: inline-flex; align-items: center; gap: 4px; flex-shrink: 0; + align-self: center; + } + .review-tool, .review-card .event-action-btn { + width: 26px; height: 26px; padding: 0; + display: inline-flex; align-items: center; justify-content: center; + border-radius: 5px; border: none; + background: rgba(255,255,255,.06); cursor: pointer; color: var(--text-primary); - font-size: 12px; - font-weight: 500; + transition: background .12s, color .12s, transform .06s; + font-size: 12px; line-height: 1; + } + .review-tool:hover, .review-card .event-action-btn:hover { background: rgba(255,255,255,.14); } + .review-tool:active, .review-card .event-action-btn:active { transform: translateY(1px); } + .review-card .event-action-btn.delete:hover { background: rgba(230,30,30,.24); color: #ff8a8a; } + .review-slowmo-btn { + width: auto; padding: 0 10px; gap: 4px; + background: rgba(230,30,30,.14); color: var(--brand-red, #e61e1e); + } + .review-slowmo-btn > i { font-size: 12px; line-height: 1; } + .review-slowmo-btn:hover { background: rgba(230,30,30,.24); color: #fff; } + .review-slowmo-btn.is-playing { + background: var(--brand-red, #e61e1e); color: #fff; + box-shadow: 0 0 0 2px rgba(230,30,30,.35); } .review-note-text { @@ -1978,18 +2061,16 @@ .yt-bottom-nav { transition: transform 0.3s ease; } body.hl-sheet-open .yt-bottom-nav { transform: translateY(100%) !important; } - /* Coach-note overlay on the small mobile player: a slim caption pinned to - the very bottom edge (max 2 lines) so it never blocks the view. The full - note stays readable in the sheet. */ - .coach-note-overlay { + /* Coach-note overlay on the small mobile player: pinned to the very + bottom edge (max 2 lines) so it never blocks the view β€” but only + when the user hasn't dragged it to a specific spot. Font-size / + padding are driven by container queries so they scale with the + player width automatically. */ + .coach-note-overlay:not(.positioned) { bottom: 6px; - font-size: 12px; - font-weight: 600; - line-height: 1.3; - padding: 5px 10px; max-width: 94%; } - .coach-note-overlay.show { + .coach-note-overlay.show:not(.positioned) { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; @@ -2338,14 +2419,24 @@ its click listener and the Highlights pane can't be opened. --}} @@ -2492,7 +2590,7 @@ @endphp
-
@{{ $fmtTime($point->timestamp_seconds) }}
+
{{ '@' . $fmtTime($point->timestamp_seconds) }}
{{ $label }} @@ -2542,31 +2640,42 @@ @php $start = $fmtTime($review->start_time_seconds); $end = $review->end_time_seconds ? $fmtTime($review->end_time_seconds) : null; - $range = $end ? ('@'.$start.'–'.$end) : ('@'.$start); $coach = $review->coach_name ?: 'Coach'; + $canSlowmo = $end && $review->end_time_seconds > $review->start_time_seconds; @endphp -
end_time_seconds) data-time-end="{{ (int) $review->end_time_seconds }}" @endif - data-id="rev{{ $review->id }}"> -
- {{ $review->emoji ?: 'πŸ“' }} - {{ $range }} + data-id="{{ $review->id }}"> +
+ + {{ $start }} + @if ($end)β€”{{ $end }}@endif +
-
- @if ($review->note) -

{{ $review->note }}

- @endif -
- {{ $coach }} +
+
+ @if ($review->note) +
{{ $review->note }}
+ @endif +
+ + {{ $coach }} + +
+ @if ($canSlowmo) + + @endif + @if ($isOwner) + + + @endif +
+
- @if ($isOwner) -
- - -
- @endif
@empty
@@ -2718,6 +2827,14 @@ document.body.removeChild(ns); } } + // Full re-hydration for match videos when navigating + // between them from a playlist + if (document.querySelector('.events-sidebar') && + typeof window.reloadMatchVideoState === 'function') { + window.reloadMatchVideoState(url); + } else if (typeof window.initMatchHighlightsToggle === 'function') { + window.initMatchHighlightsToggle(); + } } catch(e){ console.warn('plSwapContent',e); } } @@ -2986,6 +3103,14 @@ var ns=document.createElement('script'); ns.textContent=s.textContent; document.body.appendChild(ns); document.body.removeChild(ns); }); } + // Full re-hydration for match videos: updates videoId, + // reloads match data, rebinds tabs / clicks / toggle. + if (document.querySelector('.events-sidebar') && + typeof window.reloadMatchVideoState === 'function') { + window.reloadMatchVideoState(url); + } else if (typeof window.initMatchHighlightsToggle === 'function') { + window.initMatchHighlightsToggle(); + } } catch(e){ console.warn('recSwapContent', e); } } @@ -3144,6 +3269,82 @@
+ {{-- Coach-review capture strip β€” twin of the point strip, but with two time + slots (start + optional end), a note field, coach name, and emoji chips. + The scrubber drives whichever time slot is currently "active" (blue focus + ring). Click the end slot to activate it and set the end time. --}} + +