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
+
+
+
+
+
+
+
+
+
+
+
+
A Sticky-note
+
Warm amber tint, chunky emoji, feels like a coach's Post-it on a locker.
+
+
+
π
+
+
Keep Your Hand Up
+
+
+
+
+
+
π€
+
+
Missed the counter opportunity β great angle but no follow-up
+
+
+
+
+
+
+
+
+
+
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
+
+
+
+
+
π€
+
+
02:05 β 02:20
+
Missed the counter β no follow-up after the angle change
+
+
+
+
+
+
+
+
+
D Chat-bubble
+
Emoji as coach's avatar + message bubble. Feels like WhatsApp β casual + dense.
+
+
+
+
π€
+
+
Missed the counter β no follow-up after the angle change
+
+
+
+
+
π
+
+
Excellent exit and re-entry β perfect execution
+
+
+
+
+
+
+
+
+
+
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..52d863d 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;
@@ -2542,31 +2623,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
+
- @if ($isOwner)
-
- βοΈ
- ποΈ
-
- @endif
@empty
@@ -3144,6 +3236,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. --}}
+
+
+
+
+ Drag the marker β set start (and optional end) of the note
+
+
+
+
+
+
+
+
+
+
+ 1Γ
+ 5Γ
+ 20Γ
+
+
+
+
+
+
+
+
+