all is working great
This commit is contained in:
parent
f850f40f78
commit
3b09f4baed
@ -201,7 +201,7 @@ class VideoController extends Controller
|
||||
}
|
||||
|
||||
// Load comments with user relationship
|
||||
$video->load(['comments.user', 'comments.replies.user']);
|
||||
$video->load(['comments.user', 'comments.replies.user', 'matchRounds.points', 'coachReviews']);
|
||||
|
||||
// Handle playlist navigation if playlist parameter is provided
|
||||
$playlist = null;
|
||||
@ -236,6 +236,19 @@ class VideoController extends Controller
|
||||
return view($view, compact('video', 'playlist', 'nextVideo', 'previousVideo', 'recommendedVideos', 'playlistVideos'));
|
||||
}
|
||||
|
||||
public function matchData(Video $video)
|
||||
{
|
||||
if (! $video->canView(Auth::user())) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'rounds' => $video->matchRounds()->with('points')->orderBy('round_number')->get(),
|
||||
'reviews' => $video->coachReviews()->orderBy('start_time_seconds')->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function edit(Video $video, Request $request)
|
||||
{
|
||||
// Check if user owns the video
|
||||
|
||||
@ -507,6 +507,7 @@
|
||||
}
|
||||
|
||||
.action-btn,
|
||||
.action-btn a,
|
||||
.comment-section .action-btn {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
@ -521,12 +522,15 @@
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.action-btn:hover,
|
||||
.action-btn a:hover,
|
||||
.comment-section .action-btn:hover {
|
||||
background: var(--border-color);
|
||||
transform: translateY(-1px);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.action-btn:active,
|
||||
@ -1186,6 +1190,100 @@
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* ===== Coach Review Tab Specific Styles ===== */
|
||||
#tab-review .event-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#tab-review .event-time-container {
|
||||
flex: 0 0 33.333%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
gap: 2px;
|
||||
line-height: 1.2;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
#tab-review .event-time {
|
||||
font-weight: 600;
|
||||
color: #3ea6ff;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#tab-review .event-label {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
#tab-review .event-meta {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.comment-avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.comment-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#tab-review .event-meta-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#tab-review .event-meta-text .coach-name {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
#tab-review .event-meta-text .review-note {
|
||||
color: var(--text-secondary);
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
/* Review item hover improvements */
|
||||
#tab-review .event-item:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
margin: -2px 0 -2px -8px;
|
||||
border-left: 3px solid var(--brand-red);
|
||||
}
|
||||
|
||||
/* Ensure consistency with points tab on mobile */
|
||||
@media (max-width: 768px) {
|
||||
#tab-review .event-item {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
#tab-review .event-time {
|
||||
align-self: flex-start;
|
||||
order: -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Match Modal Styles ===== */
|
||||
.match-modal-overlay {
|
||||
display: none;
|
||||
@ -1919,15 +2017,32 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Match Highlights Toggle
|
||||
// Match Highlights Toggle with localStorage persistence
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const toggleBtn = document.getElementById('matchHighlightsToggle');
|
||||
const sidebar = document.querySelector('.events-sidebar');
|
||||
const videoId = {{ isset($video) ? $video->id : 0 }};
|
||||
|
||||
if (toggleBtn && sidebar) {
|
||||
// Load saved state on page load
|
||||
const savedState = localStorage.getItem(`highlights_${videoId}`);
|
||||
if (savedState === 'open') {
|
||||
sidebar.classList.add('show');
|
||||
toggleBtn.classList.add('expanded');
|
||||
toggleBtn.textContent = 'Highlights';
|
||||
}
|
||||
|
||||
toggleBtn.addEventListener('click', function() {
|
||||
sidebar.classList.toggle('show');
|
||||
const isOpen = sidebar.classList.toggle('show');
|
||||
toggleBtn.classList.toggle('expanded');
|
||||
|
||||
// Save state to localStorage
|
||||
if (isOpen) {
|
||||
localStorage.setItem(`highlights_${videoId}`, 'open');
|
||||
toggleBtn.textContent = 'Highlights';
|
||||
} else {
|
||||
localStorage.removeItem(`highlights_${videoId}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -2294,20 +2409,21 @@
|
||||
</div>
|
||||
<div class="event-list" id="reviewEvents">
|
||||
<div class="event-item" data-time-start="32" data-time-end="50" data-id="rev1">
|
||||
<div
|
||||
style="display: flex; flex-direction: column; align-items: center; justify-content: center;">
|
||||
<div style="font-size: 1.2rem; margin-bottom: 2px">🔥</div>
|
||||
<div class="event-time-container">
|
||||
<div class="emoji">🔥</div>
|
||||
<div class="event-time">@00:32–00:50</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="event-label">
|
||||
Good pressure, but guard too low after first kick
|
||||
</div>
|
||||
<div class="event-label">🔥 Good pressure, but guard too low after first kick</div>
|
||||
<div class="event-meta">
|
||||
<div class="comment-avatar">
|
||||
<img src="https://picsum.photos/seed/coach-ahmed/80/80" alt="Coach Ahmed">
|
||||
</div>
|
||||
Coach Ahmed • Hands drop after scoring, drill guard recovery immediately.
|
||||
<div class="event-meta-text">
|
||||
<div class="coach-name">Coach Ahmed</div>
|
||||
<div class="review-note">Hands drop after scoring, drill guard recovery
|
||||
immediately.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@auth
|
||||
@ -2322,15 +2438,21 @@
|
||||
@endauth
|
||||
</div>
|
||||
<div class="event-item" data-time-start="125" data-time-end="140" data-id="rev2">
|
||||
<div
|
||||
style="display: flex; flex-direction: column; align-items: center; justify-content: center;">
|
||||
<div style="font-size: 1.2rem; margin-bottom: 2px">🤔</div>
|
||||
<div class="event-time-container">
|
||||
<div class="emoji">🤔</div>
|
||||
<div class="event-time">@02:05–02:20</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="event-label">Missed counter opportunity</div>
|
||||
<div class="event-label">🤔 Missed counter opportunity</div>
|
||||
<div class="event-meta">
|
||||
Coach Sara • Great angle, but no follow up. Use this clip to discuss risk vs reward.
|
||||
<div class="comment-avatar">
|
||||
<img src="https://picsum.photos/seed/coach-sara/80/80" alt="Coach Sara">
|
||||
</div>
|
||||
<div class="event-meta-text">
|
||||
<div class="coach-name">Coach Sara</div>
|
||||
<div class="review-note">Great angle, but no follow up. Use this clip to
|
||||
discuss risk vs reward.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@auth
|
||||
@ -2345,21 +2467,21 @@
|
||||
@endauth
|
||||
</div>
|
||||
<div class="event-item" data-time-start="205" data-id="rev3">
|
||||
<div
|
||||
style="display: flex; flex-direction: column; align-items: center; justify-content: center;">
|
||||
<div style="font-size: 1.2rem; margin-bottom: 2px">😄</div>
|
||||
<div class="event-time-container">
|
||||
<div class="emoji">😄</div>
|
||||
<div class="event-time">@03:25</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="event-label">
|
||||
Excellent angle change and follow-up
|
||||
</div>
|
||||
<div class="event-label">😄 Excellent angle change and follow-up</div>
|
||||
<div class="event-meta">
|
||||
<div class="comment-avatar">
|
||||
<img src="https://picsum.photos/seed/coach-ahmed/80/80" alt="Coach Ahmed">
|
||||
</div>
|
||||
Coach Ahmed • Save as positive highlight, ideal example of exit and re-entry after
|
||||
scoring.
|
||||
<div class="event-meta-text">
|
||||
<div class="coach-name">Coach Ahmed</div>
|
||||
<div class="review-note">Save as positive highlight, ideal example of exit and
|
||||
re-entry after scoring.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@auth
|
||||
@ -2693,111 +2815,38 @@
|
||||
try {
|
||||
const response = await fetch(`/videos/${videoId}/match-data`);
|
||||
const data = await response.json();
|
||||
if (data.success && data.rounds && data.rounds.length > 0) {
|
||||
renderMatchData(data.rounds, data.reviews);
|
||||
if (data.success) {
|
||||
renderMatchData(data.rounds || [], data.reviews || []);
|
||||
} else {
|
||||
console.warn('No match data:', data.message);
|
||||
renderStaticData();
|
||||
}
|
||||
} catch (error) {
|
||||
renderStaticData();
|
||||
console.error('Failed to load match data:', error);
|
||||
// Try to use server-side data first
|
||||
if (window.matchRounds && window.matchReviews) {
|
||||
renderMatchData(window.matchRounds, window.matchReviews);
|
||||
} else {
|
||||
renderStaticData();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pass server-side data to JS if available
|
||||
window.matchRounds = @json($video->matchRounds ?? []);
|
||||
window.matchReviews = @json($video->coachReviews ?? []);
|
||||
if (window.matchRounds.length > 0) {
|
||||
renderMatchData(window.matchRounds, window.matchReviews);
|
||||
}
|
||||
|
||||
function renderStaticData() {
|
||||
const staticRounds = [{
|
||||
id: 1,
|
||||
round_number: 1,
|
||||
name: 'ROUND 1',
|
||||
points: [{
|
||||
id: 1,
|
||||
timestamp_seconds: 20,
|
||||
action: 'Blue body kick',
|
||||
points: 1,
|
||||
competitor: 'blue',
|
||||
score_blue: 1,
|
||||
score_red: 0,
|
||||
notes: 'Clean contact.'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
timestamp_seconds: 45,
|
||||
action: 'Blue scores body kick',
|
||||
points: 1,
|
||||
competitor: 'blue',
|
||||
score_blue: 2,
|
||||
score_red: 0,
|
||||
notes: 'Judge: Central.'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
round_number: 2,
|
||||
name: 'ROUND 2',
|
||||
points: [{
|
||||
id: 3,
|
||||
timestamp_seconds: 90,
|
||||
action: 'Red head kick',
|
||||
points: 3,
|
||||
competitor: 'red',
|
||||
score_blue: 2,
|
||||
score_red: 3,
|
||||
notes: 'Video replay requested.'
|
||||
}]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
round_number: 3,
|
||||
name: 'ROUND 3',
|
||||
points: [{
|
||||
id: 4,
|
||||
timestamp_seconds: 150,
|
||||
action: 'Blue spinning backfist',
|
||||
points: 2,
|
||||
competitor: 'blue',
|
||||
score_blue: 4,
|
||||
score_red: 3,
|
||||
notes: 'Close round.'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
timestamp_seconds: 180,
|
||||
action: 'Red leg kick',
|
||||
points: 1,
|
||||
competitor: 'red',
|
||||
score_blue: 4,
|
||||
score_red: 4,
|
||||
notes: 'Tied!'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
const staticReviews = [{
|
||||
id: 1,
|
||||
start_time_seconds: 32,
|
||||
end_time_seconds: 50,
|
||||
note: 'Good pressure, but guard too low after first kick',
|
||||
coach_name: 'Coach Ahmed',
|
||||
emoji: '🔥'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
start_time_seconds: 125,
|
||||
end_time_seconds: 140,
|
||||
note: 'Missed counter opportunity',
|
||||
coach_name: 'Coach Sara',
|
||||
emoji: '🤔'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
start_time_seconds: 205,
|
||||
end_time_seconds: null,
|
||||
note: 'Excellent angle change and follow-up',
|
||||
coach_name: 'Coach Ahmed',
|
||||
emoji: '😄'
|
||||
}
|
||||
];
|
||||
renderMatchData(staticRounds, staticReviews);
|
||||
// No static demo data - empty state
|
||||
const pointsContainer = document.getElementById('officialEvents');
|
||||
const reviewsContainer = document.getElementById('reviewEvents');
|
||||
if (pointsContainer) pointsContainer.innerHTML =
|
||||
'<div style="text-align: center; padding: 40px; color: var(--text-secondary);">No match data yet. <button class="btn-add" onclick="openAddRoundModal()">+ Add Round</button></div>';
|
||||
if (reviewsContainer) reviewsContainer.innerHTML =
|
||||
'<div style="text-align: center; padding: 40px; color: var(--text-secondary);">No coach notes yet. <button class="btn-add" onclick="openAddReviewModal()">+ Add Note</button></div>';
|
||||
}
|
||||
|
||||
function renderMatchData(rounds, reviews) {
|
||||
@ -2812,12 +2861,12 @@
|
||||
<div class="round-marker" data-round="${round.round_number}">
|
||||
<span class="round-badge">${round.name}</span>
|
||||
${isOwner ? `
|
||||
<div class="round-actions">
|
||||
<button class="round-action-btn" onclick="openAddPointModal(${round.round_number}, ${round.id})" title="Add point">+</button>
|
||||
<button class="round-action-btn" onclick="editRound(${round.round_number}, ${round.id}, '${round.name}')" title="Edit round">✏️</button>
|
||||
<button class="round-action-btn" onclick="deleteRound(${round.id})" title="Delete round">🗑️</button>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="round-actions">
|
||||
<button class="round-action-btn" onclick="openAddPointModal(${round.round_number}, ${round.id})" title="Add point">+</button>
|
||||
<button class="round-action-btn" onclick="editRound(${round.round_number}, ${round.id}, '${round.name}')" title="Edit round">✏️</button>
|
||||
<button class="round-action-btn" onclick="deleteRound(${round.id})" title="Delete round">🗑️</button>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>`;
|
||||
|
||||
if (round.points) {
|
||||
@ -2838,10 +2887,10 @@
|
||||
</div>
|
||||
</div>
|
||||
${isOwner ? `
|
||||
<div class="event-actions">
|
||||
<button class="event-action-btn" onclick="editPoint(${point.id})" title="Edit">✏️</button>
|
||||
<button class="event-action-btn delete" onclick="deletePoint(${point.id})" title="Delete">🗑️</button>
|
||||
</div>` : ''}
|
||||
<div class="event-actions">
|
||||
<button class="event-action-btn" onclick="editPoint(${point.id})" title="Edit">✏️</button>
|
||||
<button class="event-action-btn delete" onclick="deletePoint(${point.id})" title="Delete">🗑️</button>
|
||||
</div>` : ''}
|
||||
</div>`;
|
||||
});
|
||||
}
|
||||
@ -2868,10 +2917,10 @@
|
||||
</div>
|
||||
</div>
|
||||
${isOwner ? `
|
||||
<div class="event-actions">
|
||||
<button class="event-action-btn" onclick="editReview(${review.id})" title="Edit">✏️</button>
|
||||
<button class="event-action-btn delete" onclick="deleteReview(${review.id})" title="Delete">🗑️</button>
|
||||
</div>` : ''}
|
||||
<div class="event-actions">
|
||||
<button class="event-action-btn" onclick="editReview(${review.id})" title="Edit">✏️</button>
|
||||
<button class="event-action-btn delete" onclick="deleteReview(${review.id})" title="Delete">🗑️</button>
|
||||
</div>` : ''}
|
||||
</div>`;
|
||||
});
|
||||
}
|
||||
@ -2923,11 +2972,14 @@
|
||||
document.querySelectorAll('.event-item').forEach(item => {
|
||||
item.addEventListener('click', function(e) {
|
||||
if (e.target.closest('.event-actions')) return;
|
||||
const timeStart = this.getAttribute('data-time-start');
|
||||
if (timeStart) {
|
||||
const timeStart = parseFloat(this.getAttribute('data-time-start'));
|
||||
const timeEnd = parseFloat(this.getAttribute('data-time-end') || 0);
|
||||
|
||||
if (timeStart || timeStart === 0) {
|
||||
const videoPlayer = document.getElementById('videoPlayer');
|
||||
if (videoPlayer) {
|
||||
videoPlayer.currentTime = parseInt(timeStart);
|
||||
// Start 1 second early for context
|
||||
videoPlayer.currentTime = Math.max(0, timeStart - 1.0);
|
||||
videoPlayer.play();
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user