Compare commits
2
Commits
76f5ef0c84
...
402c6dc89f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
402c6dc89f | ||
|
|
ad6cbaf742 |
No files matched your search
@@ -99,31 +99,10 @@ class MatchEventController extends Controller
|
|||||||
return response()->json(['success' => false, 'message' => 'Unauthorized'], 403);
|
return response()->json(['success' => false, 'message' => 'Unauthorized'], 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get ALL previous points in this round (ordered by timestamp)
|
// Create the row first with placeholder scores, then recompute the whole
|
||||||
$previousPoints = MatchPoint::where('match_round_id', $request->round_id)
|
// round's running scores. This is the only correct approach when two
|
||||||
->where('timestamp_seconds', '<', $request->timestamp_seconds)
|
// points can share a timestamp (Blue + Red at the same moment) or when
|
||||||
->orderBy('timestamp_seconds', 'asc')
|
// a point is inserted between existing ones.
|
||||||
->pluck('points', 'competitor')
|
|
||||||
->toArray();
|
|
||||||
|
|
||||||
// Calculate cumulative scores by summing each point value
|
|
||||||
$scoreBlue = 0;
|
|
||||||
$scoreRed = 0;
|
|
||||||
|
|
||||||
if (isset($previousPoints['blue'])) {
|
|
||||||
$scoreBlue += $previousPoints['blue'];
|
|
||||||
}
|
|
||||||
if (isset($previousPoints['red'])) {
|
|
||||||
$scoreRed += $previousPoints['red'];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add current point
|
|
||||||
if ($request->competitor === 'blue') {
|
|
||||||
$scoreBlue += $request->points;
|
|
||||||
} else {
|
|
||||||
$scoreRed += $request->points;
|
|
||||||
}
|
|
||||||
|
|
||||||
$point = MatchPoint::create([
|
$point = MatchPoint::create([
|
||||||
'video_id' => $video->id,
|
'video_id' => $video->id,
|
||||||
'match_round_id' => $request->round_id,
|
'match_round_id' => $request->round_id,
|
||||||
@@ -132,17 +111,42 @@ class MatchEventController extends Controller
|
|||||||
'points' => $request->points,
|
'points' => $request->points,
|
||||||
'competitor' => $request->competitor,
|
'competitor' => $request->competitor,
|
||||||
'notes' => $request->notes,
|
'notes' => $request->notes,
|
||||||
'score_blue' => $scoreBlue,
|
'score_blue' => 0,
|
||||||
'score_red' => $scoreRed,
|
'score_red' => 0,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$this->recomputeRoundScores($request->round_id);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'point' => $point,
|
'point' => $point->fresh(),
|
||||||
'message' => 'Point added successfully!',
|
'message' => 'Point added successfully!',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recompute cumulative score_blue/score_red for every point in a round,
|
||||||
|
* ordered by (timestamp_seconds, id). Handles same-timestamp ties by
|
||||||
|
* insertion order, so Blue and Red saved at the same moment both end up
|
||||||
|
* reflecting the running score after that moment.
|
||||||
|
*/
|
||||||
|
private function recomputeRoundScores(int $roundId): void
|
||||||
|
{
|
||||||
|
$blue = 0;
|
||||||
|
$red = 0;
|
||||||
|
MatchPoint::where('match_round_id', $roundId)
|
||||||
|
->orderBy('timestamp_seconds', 'asc')
|
||||||
|
->orderBy('id', 'asc')
|
||||||
|
->get()
|
||||||
|
->each(function (MatchPoint $p) use (&$blue, &$red) {
|
||||||
|
if ($p->competitor === 'blue') $blue += (int) $p->points;
|
||||||
|
else $red += (int) $p->points;
|
||||||
|
if ((int) $p->score_blue !== $blue || (int) $p->score_red !== $red) {
|
||||||
|
$p->update(['score_blue' => $blue, 'score_red' => $red]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public function updatePoint(Request $request, MatchPoint $point)
|
public function updatePoint(Request $request, MatchPoint $point)
|
||||||
{
|
{
|
||||||
$request->validate([
|
$request->validate([
|
||||||
@@ -158,6 +162,7 @@ class MatchEventController extends Controller
|
|||||||
return response()->json(['success' => false, 'message' => 'Unauthorized'], 403);
|
return response()->json(['success' => false, 'message' => 'Unauthorized'], 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$roundId = $point->match_round_id;
|
||||||
$point->update([
|
$point->update([
|
||||||
'timestamp_seconds' => $request->timestamp_seconds,
|
'timestamp_seconds' => $request->timestamp_seconds,
|
||||||
'action' => $request->action,
|
'action' => $request->action,
|
||||||
@@ -166,9 +171,11 @@ class MatchEventController extends Controller
|
|||||||
'notes' => $request->notes,
|
'notes' => $request->notes,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$this->recomputeRoundScores($roundId);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'point' => $point,
|
'point' => $point->fresh(),
|
||||||
'message' => 'Point updated successfully!',
|
'message' => 'Point updated successfully!',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -180,7 +187,9 @@ class MatchEventController extends Controller
|
|||||||
return response()->json(['success' => false, 'message' => 'Unauthorized'], 403);
|
return response()->json(['success' => false, 'message' => 'Unauthorized'], 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$roundId = $point->match_round_id;
|
||||||
$point->delete();
|
$point->delete();
|
||||||
|
$this->recomputeRoundScores($roundId);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
|
|||||||
@@ -1133,6 +1133,9 @@ class VideoController extends Controller
|
|||||||
'slides' => $slides,
|
'slides' => $slides,
|
||||||
'language' => $video->language,
|
'language' => $video->language,
|
||||||
'audio_tracks' => $audioTracks,
|
'audio_tracks' => $audioTracks,
|
||||||
|
'sports_match_id'=> $video->type === 'match'
|
||||||
|
? \App\Models\SportsMatch::where('video_id', $video->id)->value('id')
|
||||||
|
: null,
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1362,10 +1362,18 @@
|
|||||||
<i class="bi bi-fire"></i>
|
<i class="bi bi-fire"></i>
|
||||||
<span>Trending</span>
|
<span>Trending</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ auth()->check() ? route('videos.create') : route('login') }}" class="yt-bottom-nav-item {{ request()->routeIs('videos.create') ? 'active' : '' }}">
|
@auth
|
||||||
<i class="bi bi-plus-circle-fill"></i>
|
<a href="#" class="yt-bottom-nav-item {{ request()->routeIs('videos.create') ? 'active' : '' }}"
|
||||||
<span>Upload</span>
|
onclick="event.preventDefault(); openUploadChooser();">
|
||||||
</a>
|
<i class="bi bi-plus-circle-fill"></i>
|
||||||
|
<span>Upload</span>
|
||||||
|
</a>
|
||||||
|
@else
|
||||||
|
<a href="{{ route('login') }}" class="yt-bottom-nav-item">
|
||||||
|
<i class="bi bi-plus-circle-fill"></i>
|
||||||
|
<span>Upload</span>
|
||||||
|
</a>
|
||||||
|
@endauth
|
||||||
<a href="{{ route('history') }}" class="yt-bottom-nav-item {{ request()->routeIs('history') ? 'active' : '' }}">
|
<a href="{{ route('history') }}" class="yt-bottom-nav-item {{ request()->routeIs('history') ? 'active' : '' }}">
|
||||||
<i class="bi bi-collection-play-fill"></i>
|
<i class="bi bi-collection-play-fill"></i>
|
||||||
<span>History</span>
|
<span>History</span>
|
||||||
|
|||||||
@@ -233,12 +233,8 @@ window._editCurrentVideoId = null;
|
|||||||
|
|
||||||
// ── Modal open / close ────────────────────────────────────────────────────────
|
// ── Modal open / close ────────────────────────────────────────────────────────
|
||||||
function openEditVideoModal(videoId) {
|
function openEditVideoModal(videoId) {
|
||||||
if (window.innerWidth < 992) {
|
// Always resolve type first so a match video opens the sports-match sheet
|
||||||
window.location.href = `/videos/${videoId}/edit`;
|
// instead of the generic video-edit modal — same for mobile.
|
||||||
return;
|
|
||||||
}
|
|
||||||
window._editCurrentVideoId = videoId;
|
|
||||||
|
|
||||||
fetch(`/videos/${videoId}/edit`, {
|
fetch(`/videos/${videoId}/edit`, {
|
||||||
headers: {
|
headers: {
|
||||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||||
@@ -251,6 +247,25 @@ function openEditVideoModal(videoId) {
|
|||||||
if (!data.success) { showToast('Failed to load video data', 'error'); return; }
|
if (!data.success) { showToast('Failed to load video data', 'error'); return; }
|
||||||
const v = data.video;
|
const v = data.video;
|
||||||
|
|
||||||
|
// Match videos always route to the sports-match sheet — edit the existing
|
||||||
|
// match if one is linked, otherwise open create-mode with the video already
|
||||||
|
// attached so the user can add the match details.
|
||||||
|
if (v.type === 'match' && typeof window.openSportsMatchModal === 'function') {
|
||||||
|
if (v.sports_match_id) {
|
||||||
|
window.openSportsMatchModal(v.sports_match_id);
|
||||||
|
} else {
|
||||||
|
window.openSportsMatchModal({ videoId: v.id, videoTitle: v.title });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mobile fallback for non-match videos: full-page edit view
|
||||||
|
if (window.innerWidth < 992) {
|
||||||
|
window.location.href = `/videos/${videoId}/edit`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window._editCurrentVideoId = videoId;
|
||||||
|
|
||||||
document.getElementById('edit-form').action = `/videos/${videoId}`;
|
document.getElementById('edit-form').action = `/videos/${videoId}`;
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
|
|||||||
@@ -524,6 +524,72 @@
|
|||||||
.sm-img-avatar .sm-img-ph { min-height: 74px; gap: 2px; font-size: 10px; }
|
.sm-img-avatar .sm-img-ph { min-height: 74px; gap: 2px; font-size: 10px; }
|
||||||
.sm-img-avatar .sm-img-ph i { font-size: 16px; }
|
.sm-img-avatar .sm-img-ph i { font-size: 16px; }
|
||||||
.sm-img-avatar .sm-img-preview { width: 74px; height: 74px; max-height: 74px; }
|
.sm-img-avatar .sm-img-preview { width: 74px; height: 74px; max-height: 74px; }
|
||||||
|
|
||||||
|
/* ── Mobile: render as a bottom sheet ─────────────────────────── */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#sportsMatchModal .modal-dialog {
|
||||||
|
margin: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
#sportsMatchModal .sm-content {
|
||||||
|
border-radius: 20px 20px 0 0;
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: none;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 92vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 0 -12px 40px rgba(0,0,0,.6);
|
||||||
|
transform: translateY(100%);
|
||||||
|
transition: transform .3s cubic-bezier(.22,1,.36,1);
|
||||||
|
}
|
||||||
|
#sportsMatchModal.show .sm-content { transform: translateY(0); }
|
||||||
|
|
||||||
|
/* Grabber handle at the top */
|
||||||
|
#sportsMatchModal .sm-header {
|
||||||
|
position: relative;
|
||||||
|
padding-top: 18px;
|
||||||
|
}
|
||||||
|
#sportsMatchModal .sm-header::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 6px; left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 40px; height: 4px;
|
||||||
|
background: #333; border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sportsMatchModal .modal-body {
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
padding-bottom: calc(16px + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
#sportsMatchModal .sm-footer {
|
||||||
|
padding: 10px 14px calc(10px + env(safe-area-inset-bottom));
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
#sportsMatchModal .sm-footer .btn {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Slide-up animation on the modal fade class as well */
|
||||||
|
#sportsMatchModal.fade .modal-dialog { transform: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
#sportsMatchModal .sm-header h5 { font-size: 15px; }
|
||||||
|
#sportsMatchModal .sm-header-icon { width: 34px; height: 34px; font-size: 16px; }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -555,12 +621,28 @@
|
|||||||
function token() { return form.querySelector('[name="_token"]').value; }
|
function token() { return form.querySelector('[name="_token"]').value; }
|
||||||
|
|
||||||
// ── Public entry point ───────────────────────────────────────────────
|
// ── Public entry point ───────────────────────────────────────────────
|
||||||
window.openSportsMatchModal = function (matchId) {
|
// - openSportsMatchModal(matchId) → edit existing match
|
||||||
|
// - openSportsMatchModal({videoId, videoTitle}) → create against an existing video
|
||||||
|
// - openSportsMatchModal() → fresh create (upload a new video)
|
||||||
|
window.openSportsMatchModal = function (arg) {
|
||||||
resetForm();
|
resetForm();
|
||||||
if (matchId) loadMatch(matchId);
|
if (typeof arg === 'number' || typeof arg === 'string') {
|
||||||
|
loadMatch(arg);
|
||||||
|
} else if (arg && typeof arg === 'object' && arg.videoId) {
|
||||||
|
attachExistingVideo(arg.videoId, arg.videoTitle || '');
|
||||||
|
}
|
||||||
getModal().show();
|
getModal().show();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function attachExistingVideo(videoId, videoTitle) {
|
||||||
|
videoIdInp.value = videoId;
|
||||||
|
document.getElementById('sm-video-create').classList.add('d-none');
|
||||||
|
document.getElementById('sm-video-attached').classList.remove('d-none');
|
||||||
|
document.getElementById('sm-video-attached-title').textContent = videoTitle || ('Video #' + videoId);
|
||||||
|
// Advanced fields are also useful when the video already exists.
|
||||||
|
document.getElementById('sm-advanced').classList.remove('d-none');
|
||||||
|
}
|
||||||
|
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
form.reset();
|
form.reset();
|
||||||
idInput.value = '';
|
idInput.value = '';
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user