- New SportsMatch model/controller and sports UI components/modal - Move share-modal to a reusable x-share-modal/x-share-button component - Add VideoSharedWithUser notification and share-to-members flow - Device/user-agent tracking on views, downloads, share accesses - ProfileVisit model + migration; subscription source tracking - Email thumbnail support; remove stale TODO files
888 lines
57 KiB
PHP
888 lines
57 KiB
PHP
{{-- ════════════════════════════════════════════════════════════════════════
|
||
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.
|
||
════════════════════════════════════════════════════════════════════════ --}}
|
||
<div class="modal fade" id="sportsMatchModal" tabindex="-1" aria-labelledby="sportsMatchModalLabel"
|
||
aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false" data-bs-theme="dark">
|
||
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
|
||
<div class="modal-content sm-content">
|
||
|
||
<div class="modal-header sm-header">
|
||
<div class="d-flex align-items-center gap-2">
|
||
<span class="sm-header-icon"><i class="bi bi-trophy-fill"></i></span>
|
||
<div>
|
||
<h5 class="modal-title mb-0" id="sportsMatchModalLabel">Create Sports Match</h5>
|
||
<small class="text-secondary" id="sm-status-chip">Draft</small>
|
||
</div>
|
||
</div>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||
</div>
|
||
|
||
<div class="modal-body">
|
||
|
||
{{-- Friendly note --}}
|
||
<div class="alert sm-note d-flex align-items-start gap-2" role="alert">
|
||
<i class="bi bi-info-circle-fill mt-1"></i>
|
||
<div>Save the basic match info now — you can add scores, stats, officials and more later by editing this match.</div>
|
||
</div>
|
||
|
||
<form id="sportsMatchForm" novalidate>
|
||
@csrf
|
||
<input type="hidden" name="match_id" id="sm-match-id" value="">
|
||
<input type="hidden" name="video_id" id="sm-video-id" value="">
|
||
<input type="hidden" name="status" id="sm-status" value="draft">
|
||
|
||
{{-- Video upload (create) --}}
|
||
<div class="sm-group" id="sm-video-create">
|
||
<div class="sm-group-title"><i class="bi bi-film"></i> Match video <span class="text-danger">*</span></div>
|
||
<div class="sm-video-drop" id="sm-video-drop">
|
||
<input type="file" id="sm-video-file" accept="video/mp4,video/webm,video/ogg,video/quicktime,.mp4,.webm,.ogg,.mov,.avi,.wmv,.flv,.mkv" hidden>
|
||
<div id="sm-video-idle" class="sm-video-idle">
|
||
<i class="bi bi-cloud-arrow-up"></i>
|
||
<div>Click to choose the match video</div>
|
||
<small class="text-secondary">MP4, MOV, MKV, WebM, AVI…</small>
|
||
</div>
|
||
<div id="sm-video-picked" class="sm-video-picked d-none">
|
||
<i class="bi bi-film"></i>
|
||
<div class="sm-video-meta">
|
||
<span class="sm-video-name" id="sm-video-name"></span>
|
||
<span class="sm-video-size text-secondary" id="sm-video-size"></span>
|
||
</div>
|
||
<button type="button" class="btn-close btn-close-white" id="sm-video-clear" aria-label="Remove"></button>
|
||
</div>
|
||
</div>
|
||
<div class="text-danger small mt-2 d-none" id="sm-video-err">Please choose a video to upload.</div>
|
||
</div>
|
||
|
||
{{-- Attached video note (edit) --}}
|
||
<div class="sm-group d-none" id="sm-video-attached">
|
||
<div class="sm-group-title"><i class="bi bi-film"></i> Match video</div>
|
||
<div class="sm-attached d-flex align-items-center gap-2">
|
||
<i class="bi bi-play-circle-fill"></i>
|
||
<span id="sm-video-attached-title" class="text-truncate">Attached video</span>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Match details --}}
|
||
<div class="sm-group">
|
||
<div class="sm-group-title"><i class="bi bi-card-text"></i> Match details</div>
|
||
<div class="mb-3">
|
||
<label for="sm-title" class="form-label">Match title <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" name="title" id="sm-title" placeholder="e.g. Final — Ali vs. Khan" required>
|
||
<div class="invalid-feedback" data-field="title"></div>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="sm-event" class="form-label">Event name</label>
|
||
<input type="text" class="form-control" name="event_name" id="sm-event" placeholder="e.g. National Championship 2026">
|
||
</div>
|
||
<div class="row g-3">
|
||
<div class="col-sm-6">
|
||
<label for="sm-date" class="form-label">Date</label>
|
||
<input type="date" class="form-control" name="match_date" id="sm-date">
|
||
</div>
|
||
<div class="col-sm-6">
|
||
<label for="sm-time" class="form-label">Time</label>
|
||
<input type="time" class="form-control" name="match_time" id="sm-time">
|
||
<div class="invalid-feedback" data-field="match_time"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Participants --}}
|
||
<div class="sm-group">
|
||
<div class="sm-group-title"><i class="bi bi-people-fill"></i> Participants</div>
|
||
<div class="row g-3">
|
||
<div class="col-md-6">
|
||
<div class="sm-pcard">
|
||
<div class="sm-pcard-head"><span class="sm-pcard-badge sm-badge-1">1</span> Participant 1</div>
|
||
<x-sports-image name="media_participant1_photo" label="Photo" class="sm-img-lg" />
|
||
<input type="text" class="form-control" name="participant1_name" placeholder="Name">
|
||
<input type="text" class="form-control" name="participants[p1_club]" placeholder="Club / team name">
|
||
<label class="sm-mini-label">Club logo</label>
|
||
<x-sports-image name="media_club1_logo" label="Logo" class="sm-img-sm" />
|
||
</div>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<div class="sm-pcard">
|
||
<div class="sm-pcard-head"><span class="sm-pcard-badge sm-badge-2">2</span> Participant 2</div>
|
||
<x-sports-image name="media_participant2_photo" label="Photo" class="sm-img-lg" />
|
||
<input type="text" class="form-control" name="participant2_name" placeholder="Name">
|
||
<input type="text" class="form-control" name="participants[p2_club]" placeholder="Club / team name">
|
||
<label class="sm-mini-label">Club logo</label>
|
||
<x-sports-image name="media_club2_logo" label="Logo" class="sm-img-sm" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Referee --}}
|
||
<div class="sm-group">
|
||
<div class="sm-group-title"><i class="bi bi-person-badge-fill"></i> Referee</div>
|
||
<div class="d-flex align-items-center gap-3">
|
||
<x-sports-image name="media_referee_photo" label="Photo" class="sm-img-avatar" />
|
||
<div class="flex-grow-1">
|
||
<label for="sm-referee" class="form-label">Referee name</label>
|
||
<input type="text" class="form-control" name="referee_name" id="sm-referee" placeholder="Referee name">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Result --}}
|
||
<div class="sm-group">
|
||
<div class="sm-group-title"><i class="bi bi-flag-fill"></i> Result</div>
|
||
<label for="sm-score" class="form-label">Final score</label>
|
||
<input type="text" class="form-control" name="result[final_result]" id="sm-score" placeholder="e.g. 3 – 1">
|
||
</div>
|
||
|
||
{{-- ── ADVANCED (revealed only when editing) ───────────────────── --}}
|
||
<div id="sm-advanced" class="d-none">
|
||
<p class="sm-optional-head">
|
||
<i class="bi bi-sliders"></i> Advanced details
|
||
<span class="text-secondary">— optional, open only what you need</span>
|
||
</p>
|
||
|
||
<div class="accordion sm-accordion" id="sportsAccordion">
|
||
|
||
{{-- Match info (sport, type, venue) --}}
|
||
<x-sports-section id="meta" title="Match info" icon="bi-info-circle">
|
||
<div class="mb-3">
|
||
<label for="sm-sport" class="form-label">Sport</label>
|
||
<input type="text" class="form-control" name="sport" id="sm-sport" list="sm-sport-list" placeholder="e.g. Boxing, Football, Tennis…">
|
||
<datalist id="sm-sport-list">
|
||
<option value="Boxing"><option value="MMA"><option value="Kickboxing">
|
||
<option value="Wrestling"><option value="Judo"><option value="Taekwondo">
|
||
<option value="Football"><option value="Basketball"><option value="Tennis">
|
||
<option value="Volleyball"><option value="Badminton"><option value="Cricket">
|
||
<option value="Rugby"><option value="Hockey"><option value="Baseball">
|
||
<option value="Athletics"><option value="Swimming"><option value="Cycling">
|
||
<option value="Esports">
|
||
</datalist>
|
||
<div class="form-text">Sets the label for the Segments section (Rounds, Sets, Periods…).</div>
|
||
</div>
|
||
<div class="mb-3"><label class="form-label">Match type</label>
|
||
<input type="text" class="form-control" name="match_type" placeholder="Final, Semi-final, Friendly…"></div>
|
||
<div class="mb-0"><label class="form-label">Venue name</label>
|
||
<input type="text" class="form-control" name="venue_name" placeholder="e.g. City Arena"></div>
|
||
</x-sports-section>
|
||
|
||
{{-- Competition --}}
|
||
<x-sports-section id="comp" title="Competition Details" icon="bi-award">
|
||
<div class="mb-3"><label class="form-label">Competition name</label>
|
||
<input type="text" class="form-control" name="competition[name]"></div>
|
||
<div class="mb-3"><label class="form-label">Competition type</label>
|
||
<input type="text" class="form-control" name="competition[type]" placeholder="League, Cup, Tournament…"></div>
|
||
<div class="row g-3 mb-3">
|
||
<div class="col-sm-6"><label class="form-label">Stage</label>
|
||
<input type="text" class="form-control" name="competition[stage]" placeholder="Group, Quarter-final…"></div>
|
||
<div class="col-sm-6"><label class="form-label">Season</label>
|
||
<input type="text" class="form-control" name="competition[season]" placeholder="2025/26"></div>
|
||
</div>
|
||
<div class="mb-3"><label class="form-label">Organizer</label>
|
||
<input type="text" class="form-control" name="competition[organizer]"></div>
|
||
<div class="mb-0"><label class="form-label">Championship / league / tournament name</label>
|
||
<input type="text" class="form-control" name="competition[championship_name]"></div>
|
||
</x-sports-section>
|
||
|
||
{{-- Participants details (clubs are in the basic cards) --}}
|
||
<x-sports-section id="part" title="Participants Details" icon="bi-people">
|
||
<div class="sm-subcard">
|
||
<div class="sm-subcard-title">Participant 1</div>
|
||
<div class="row g-3">
|
||
<div class="col-sm-6"><label class="form-label">Type</label>
|
||
<input type="text" class="form-control" name="participants[p1_type]" placeholder="Individual / Team / Pair"></div>
|
||
<div class="col-sm-6"><label class="form-label">Country</label>
|
||
<input type="text" class="form-control" name="participants[p1_country]"></div>
|
||
<div class="col-sm-6"><label class="form-label">Role</label>
|
||
<input type="text" class="form-control" name="participants[p1_role]" placeholder="home, away, red, blue…"></div>
|
||
</div>
|
||
</div>
|
||
<div class="sm-subcard">
|
||
<div class="sm-subcard-title">Participant 2</div>
|
||
<div class="row g-3">
|
||
<div class="col-sm-6"><label class="form-label">Type</label>
|
||
<input type="text" class="form-control" name="participants[p2_type]" placeholder="Individual / Team / Pair"></div>
|
||
<div class="col-sm-6"><label class="form-label">Country</label>
|
||
<input type="text" class="form-control" name="participants[p2_country]"></div>
|
||
<div class="col-sm-6"><label class="form-label">Role</label>
|
||
<input type="text" class="form-control" name="participants[p2_role]" placeholder="home, away, red, blue…"></div>
|
||
</div>
|
||
</div>
|
||
<div class="row g-3 mb-3">
|
||
<div class="col-sm-4"><label class="form-label">Weight class / division</label>
|
||
<input type="text" class="form-control" name="participants[weight_class]"></div>
|
||
<div class="col-sm-4"><label class="form-label">Gender division</label>
|
||
<input type="text" class="form-control" name="participants[gender_division]" placeholder="Men, Women, Mixed"></div>
|
||
<div class="col-sm-4"><label class="form-label">Level</label>
|
||
<select class="form-select" name="participants[level]">
|
||
<option value="">—</option><option>Amateur</option><option>Professional</option>
|
||
</select></div>
|
||
</div>
|
||
<label class="form-label">Extra participants</label>
|
||
<div class="form-text mb-2">For relays, doubles or multi-competitor formats.</div>
|
||
<div id="sm-extra-list"></div>
|
||
<button type="button" class="btn btn-sm btn-outline-light" data-sm-add="extra">
|
||
<i class="bi bi-plus-lg"></i> Add participant
|
||
</button>
|
||
</x-sports-section>
|
||
|
||
{{-- Additional officials (referee is in the basic section) --}}
|
||
<x-sports-section id="off" title="Officials" icon="bi-person-badge">
|
||
<div class="form-text mb-2">Judges, umpires, linesmen, doctor, supervisor, timekeeper…</div>
|
||
<div id="sm-officials-list"></div>
|
||
<button type="button" class="btn btn-sm btn-outline-light" data-sm-add="official">
|
||
<i class="bi bi-plus-lg"></i> Add official
|
||
</button>
|
||
</x-sports-section>
|
||
|
||
{{-- Venue details --}}
|
||
<x-sports-section id="venue" title="Venue Details" icon="bi-geo-alt">
|
||
<div class="mb-3"><label class="form-label">Full address</label>
|
||
<input type="text" class="form-control" name="venue[address]"></div>
|
||
<div class="row g-3 mb-3">
|
||
<div class="col-sm-6"><label class="form-label">City</label>
|
||
<input type="text" class="form-control" name="venue[city]"></div>
|
||
<div class="col-sm-6"><label class="form-label">Country</label>
|
||
<input type="text" class="form-control" name="venue[country]"></div>
|
||
</div>
|
||
<div class="row g-3 mb-3">
|
||
<div class="col-sm-6"><label class="form-label">GPS latitude</label>
|
||
<input type="text" class="form-control" name="venue[lat]" placeholder="e.g. 26.2235"></div>
|
||
<div class="col-sm-6"><label class="form-label">GPS longitude</label>
|
||
<input type="text" class="form-control" name="venue[lng]" placeholder="e.g. 50.5876"></div>
|
||
</div>
|
||
<div class="mb-0"><label class="form-label">Venue notes</label>
|
||
<textarea class="form-control" rows="2" name="venue[notes]"></textarea></div>
|
||
</x-sports-section>
|
||
|
||
{{-- Result details (final score is in the basic section) --}}
|
||
<x-sports-section id="result" title="Result Details" icon="bi-flag">
|
||
<div class="row g-3 mb-3">
|
||
<div class="col-sm-6"><label class="form-label">Winner</label>
|
||
<input type="text" class="form-control" name="result[winner]"></div>
|
||
<div class="col-sm-6"><label class="form-label">Outcome type</label>
|
||
<input type="text" class="form-control" name="result[outcome_type]" placeholder="KO, decision, draw, walkover…"></div>
|
||
</div>
|
||
<div class="row g-3 mb-3">
|
||
<div class="col-sm-6"><label class="form-label">Rank / placement</label>
|
||
<input type="text" class="form-control" name="result[rank]"></div>
|
||
</div>
|
||
<div class="mb-0"><label class="form-label">Result notes</label>
|
||
<textarea class="form-control" rows="2" name="result[notes]"></textarea></div>
|
||
</x-sports-section>
|
||
|
||
{{-- Segments --}}
|
||
<x-sports-section id="seg" title="Segments" icon="bi-list-ol">
|
||
<div class="form-text mb-2">Generic segments — rounds, periods, sets, halves, quarters, laps, innings or maps.</div>
|
||
<div id="sm-segments-list"></div>
|
||
<button type="button" class="btn btn-sm btn-outline-light" data-sm-add="segment">
|
||
<i class="bi bi-plus-lg"></i> <span data-sm-seg-add>Add segment</span>
|
||
</button>
|
||
</x-sports-section>
|
||
|
||
{{-- Statistics --}}
|
||
<x-sports-section id="stat" title="Statistics" icon="bi-bar-chart">
|
||
<div id="sm-stats-list"></div>
|
||
<button type="button" class="btn btn-sm btn-outline-light" data-sm-add="statistic">
|
||
<i class="bi bi-plus-lg"></i> Add statistic
|
||
</button>
|
||
</x-sports-section>
|
||
|
||
{{-- More photos (event poster + image meta) --}}
|
||
<x-sports-section id="media" title="More Photos" icon="bi-images">
|
||
<div class="form-text mb-3">Participant, club and referee images are in the sections above. Add an event poster and image metadata here.</div>
|
||
<label class="form-label">Event poster / banner</label>
|
||
<x-sports-image name="media_event_poster" label="Upload & crop" class="sm-img-wide mb-3" />
|
||
<div class="mb-3"><label class="form-label">Image caption</label>
|
||
<input type="text" class="form-control" name="media[caption]"></div>
|
||
<div class="row g-3 mb-3">
|
||
<div class="col-sm-6"><label class="form-label">Image alt text</label>
|
||
<input type="text" class="form-control" name="media[alt]"></div>
|
||
<div class="col-sm-6"><label class="form-label">Image credit</label>
|
||
<input type="text" class="form-control" name="media[credit]"></div>
|
||
</div>
|
||
<div class="form-check form-switch">
|
||
<input class="form-check-input" type="checkbox" role="switch" id="sm-media-public" name="media[public]" value="1">
|
||
<label class="form-check-label" for="sm-media-public">Display these images publicly</label>
|
||
</div>
|
||
</x-sports-section>
|
||
|
||
{{-- Reviews --}}
|
||
<x-sports-section id="rev" title="Reviews & Notes" icon="bi-clipboard-check">
|
||
<div class="row g-3 mb-3">
|
||
<div class="col-sm-6"><label class="form-label">Review type</label>
|
||
<input type="text" class="form-control" name="reviews[review_type]" placeholder="VAR, protest, appeal…"></div>
|
||
<div class="col-sm-6"><label class="form-label">Requested by</label>
|
||
<input type="text" class="form-control" name="reviews[requested_by]"></div>
|
||
</div>
|
||
<div class="mb-3"><label class="form-label">Review result</label>
|
||
<input type="text" class="form-control" name="reviews[review_result]"></div>
|
||
<div class="mb-3"><label class="form-label">Source URL</label>
|
||
<input type="url" class="form-control" name="reviews[source_url]" placeholder="https://…"></div>
|
||
<div class="mb-3"><label class="form-label">Verification notes</label>
|
||
<textarea class="form-control" rows="2" name="reviews[verification_notes]"></textarea></div>
|
||
<div class="mb-0"><label class="form-label">Admin notes</label>
|
||
<textarea class="form-control" rows="2" name="reviews[admin_notes]"></textarea></div>
|
||
</x-sports-section>
|
||
|
||
</div>
|
||
</div>{{-- /advanced --}}
|
||
</form>
|
||
</div>{{-- /modal-body --}}
|
||
|
||
<div class="modal-footer sm-footer">
|
||
<button type="button" class="btn btn-outline-light" data-bs-dismiss="modal">Cancel</button>
|
||
<button type="button" class="btn btn-light" id="sm-save-basic">
|
||
<i class="bi bi-save"></i> Save basic info
|
||
</button>
|
||
<button type="button" class="btn btn-danger" id="sm-save-continue">
|
||
<i class="bi bi-pencil-square"></i> Save & continue editing
|
||
</button>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- ── Repeatable row templates ─────────────────────────────────────────── --}}
|
||
<template id="sm-tpl-official">
|
||
<div class="sm-row" data-sm-row="official">
|
||
<div class="row g-2 align-items-start">
|
||
<div class="col-sm-4"><input type="text" class="form-control form-control-sm" data-n="role" placeholder="Role (referee, judge…)"></div>
|
||
<div class="col-sm-4"><input type="text" class="form-control form-control-sm" data-n="name" placeholder="Name"></div>
|
||
<div class="col-sm-3">
|
||
<div class="sm-img-field" data-img="photo">
|
||
<input type="file" data-n="photo" accept=".jpg,.jpeg,.png,.webp" hidden>
|
||
<input type="hidden" data-n="photo_existing">
|
||
<button type="button" class="sm-img-drop sm-img-drop-sm" data-sm-official-crop>
|
||
<img class="sm-img-preview sm-img-preview-sm d-none" alt="">
|
||
<span class="sm-img-ph"><i class="bi bi-crop"></i><span>Photo</span></span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="col-sm-1 text-end">
|
||
<button type="button" class="btn btn-sm btn-outline-danger" data-sm-remove><i class="bi bi-x-lg"></i></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template id="sm-tpl-segment">
|
||
<div class="sm-row" data-sm-row="segment">
|
||
<div class="row g-2 align-items-start">
|
||
<div class="col-sm-3"><input type="text" class="form-control form-control-sm" data-n="type" placeholder="Type (round, set…)"></div>
|
||
<div class="col-sm-2"><input type="text" class="form-control form-control-sm" data-n="number" placeholder="No."></div>
|
||
<div class="col-sm-3"><input type="text" class="form-control form-control-sm" data-n="score" placeholder="Score / result"></div>
|
||
<div class="col-sm-3"><input type="text" class="form-control form-control-sm" data-n="winner" placeholder="Winner"></div>
|
||
<div class="col-sm-1 text-end"><button type="button" class="btn btn-sm btn-outline-danger" data-sm-remove><i class="bi bi-x-lg"></i></button></div>
|
||
<div class="col-12"><input type="text" class="form-control form-control-sm" data-n="notes" placeholder="Notes (optional)"></div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template id="sm-tpl-statistic">
|
||
<div class="sm-row" data-sm-row="statistic">
|
||
<div class="row g-2 align-items-start">
|
||
<div class="col-sm-3"><input type="text" class="form-control form-control-sm" data-n="name" placeholder="Statistic name"></div>
|
||
<div class="col-sm-3"><input type="text" class="form-control form-control-sm" data-n="value" placeholder="Value"></div>
|
||
<div class="col-sm-3"><input type="text" class="form-control form-control-sm" data-n="owner" placeholder="Owner (who/which side)"></div>
|
||
<div class="col-sm-2"><input type="text" class="form-control form-control-sm" data-n="notes" placeholder="Notes"></div>
|
||
<div class="col-sm-1 text-end"><button type="button" class="btn btn-sm btn-outline-danger" data-sm-remove><i class="bi bi-x-lg"></i></button></div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
{{-- ── 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. --}}
|
||
<x-image-cropper id="smc_media_participant1_photo" :width="380" :height="380" shape="square" output-width="600" title="Crop Participant 1 photo" target-input="sm-file-media_participant1_photo" preview-img="sm-prev-media_participant1_photo" />
|
||
<x-image-cropper id="smc_media_participant2_photo" :width="380" :height="380" shape="square" output-width="600" title="Crop Participant 2 photo" target-input="sm-file-media_participant2_photo" preview-img="sm-prev-media_participant2_photo" />
|
||
<x-image-cropper id="smc_media_referee_photo" :width="380" :height="380" shape="square" output-width="600" title="Crop Referee photo" target-input="sm-file-media_referee_photo" preview-img="sm-prev-media_referee_photo" />
|
||
<x-image-cropper id="smc_media_club1_logo" :width="380" :height="380" shape="square" output-width="600" title="Crop Club / team 1 logo" target-input="sm-file-media_club1_logo" preview-img="sm-prev-media_club1_logo" />
|
||
<x-image-cropper id="smc_media_club2_logo" :width="380" :height="380" shape="square" output-width="600" title="Crop Club / team 2 logo" target-input="sm-file-media_club2_logo" preview-img="sm-prev-media_club2_logo" />
|
||
<x-image-cropper id="smc_media_event_poster" :width="448" :height="252" shape="square" output-width="1280" title="Crop Event poster / banner" target-input="sm-file-media_event_poster" preview-img="sm-prev-media_event_poster" />
|
||
<x-image-cropper id="smc_official" :width="360" :height="360" shape="square" output-width="500" title="Crop Official photo" result-callback="smOfficialCropDone" />
|
||
|
||
<template id="sm-tpl-extra">
|
||
<div class="sm-row" data-sm-row="extra">
|
||
<div class="row g-2 align-items-start">
|
||
<div class="col-sm-3"><input type="text" class="form-control form-control-sm" data-n="name" placeholder="Name"></div>
|
||
<div class="col-sm-2"><input type="text" class="form-control form-control-sm" data-n="type" placeholder="Type"></div>
|
||
<div class="col-sm-3"><input type="text" class="form-control form-control-sm" data-n="club" placeholder="Club / team"></div>
|
||
<div class="col-sm-2"><input type="text" class="form-control form-control-sm" data-n="country" placeholder="Country"></div>
|
||
<div class="col-sm-1"><input type="text" class="form-control form-control-sm" data-n="role" placeholder="Role"></div>
|
||
<div class="col-sm-1 text-end"><button type="button" class="btn btn-sm btn-outline-danger" data-sm-remove><i class="bi bi-x-lg"></i></button></div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style>
|
||
.sm-content { background: #181818; border: 1px solid #2a2a2a; }
|
||
.sm-header { border-bottom: 1px solid #262626; }
|
||
.sm-header-icon {
|
||
width: 40px; height: 40px; border-radius: 11px; flex-shrink: 0;
|
||
display: inline-flex; align-items: center; justify-content: center;
|
||
background: rgba(245,158,11,.14); color: #f59e0b; font-size: 19px;
|
||
}
|
||
.sm-note { background: rgba(230,30,30,.08); border: 1px solid rgba(230,30,30,.18); color: #d8d8d8; font-size: 13px; }
|
||
.sm-note i { color: #e61e1e; }
|
||
.sm-basic .form-label { font-weight: 600; font-size: 13px; }
|
||
.sm-optional-head { margin: 22px 0 10px; font-size: 13px; font-weight: 700; color: #9a9a9a; }
|
||
.sm-optional-head i { color: #e61e1e; }
|
||
.sm-accordion .accordion-item { background: #141414; border: 1px solid #262626; margin-bottom: 8px; border-radius: 10px !important; overflow: hidden; }
|
||
.sm-accordion .accordion-button { background: #141414; color: #e8e8e8; font-weight: 600; font-size: 14px; box-shadow: none; }
|
||
.sm-accordion .accordion-button:not(.collapsed) { background: #1b1414; color: #fff; }
|
||
.sm-accordion .accordion-button:focus { box-shadow: none; border-color: transparent; }
|
||
.sm-accordion .accordion-button::after { filter: invert(1) grayscale(1) brightness(1.4); }
|
||
.sm-accordion .accordion-button .sm-acc-ico { color: #e61e1e; margin-right: 10px; font-size: 16px; }
|
||
.sm-accordion .accordion-body { background: #181818; }
|
||
.sm-accordion .badge { font-weight: 600; }
|
||
.sm-subcard { background: #111; border: 1px solid #242424; border-radius: 10px; padding: 14px; margin-bottom: 14px; }
|
||
.sm-subcard-title { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: #e61e1e; margin-bottom: 10px; }
|
||
.sm-row { background: #111; border: 1px solid #242424; border-radius: 10px; padding: 12px; margin-bottom: 10px; }
|
||
.sm-img-field { position: relative; }
|
||
.sm-img-preview { display: block; width: 100%; max-height: 130px; object-fit: cover; border-radius: 8px; margin-bottom: 8px; border: 1px solid #2a2a2a; }
|
||
.sm-img-preview-sm { max-height: 64px; }
|
||
.sm-hr { border-color: #262626; }
|
||
.sm-footer { border-top: 1px solid #262626; }
|
||
.sm-footer .btn-danger { background: #e61e1e; border-color: #e61e1e; }
|
||
.sm-footer .btn-danger:hover { background: #c81818; border-color: #c81818; }
|
||
#sportsMatchModal .form-text { color: #6f6f6f; font-size: 12px; }
|
||
|
||
/* Video upload dropzone */
|
||
.sm-video-drop { border: 1.5px dashed #2f2f2f; border-radius: 12px; background: #111; cursor: pointer; transition: border-color .15s, background .15s; }
|
||
.sm-video-drop:hover { border-color: #e61e1e; background: #161313; }
|
||
.sm-video-drop.sm-invalid { border-color: #dc3545; }
|
||
.sm-video-idle { padding: 22px 16px; text-align: center; color: #8a8a8a; }
|
||
.sm-video-idle i { font-size: 26px; color: #e61e1e; display: block; margin-bottom: 6px; }
|
||
.sm-video-idle div { font-size: 13px; font-weight: 600; color: #cfcfcf; }
|
||
.sm-video-picked { display: flex; align-items: center; gap: 12px; padding: 14px 16px; }
|
||
.sm-video-picked > i { font-size: 22px; color: #e61e1e; }
|
||
.sm-video-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
||
.sm-video-name { font-size: 13px; font-weight: 600; color: #e8e8e8; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.sm-video-size { font-size: 12px; }
|
||
|
||
/* Cropper-backed image fields */
|
||
.sm-img-drop { width: 100%; border: 1.5px dashed #2f2f2f; border-radius: 10px; background: #111; cursor: pointer; padding: 0; overflow: hidden; display: block; position: relative; min-height: 96px; transition: border-color .15s, background .15s; }
|
||
.sm-img-drop:hover { border-color: #e61e1e; background: #161313; }
|
||
.sm-img-drop-sm { min-height: 64px; }
|
||
.sm-img-ph { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; min-height: 96px; color: #8a8a8a; font-size: 12px; }
|
||
.sm-img-drop-sm .sm-img-ph { min-height: 64px; }
|
||
.sm-img-ph i { font-size: 20px; color: #e61e1e; }
|
||
.sm-img-preview { display: block; width: 100%; max-height: 150px; object-fit: cover; }
|
||
.sm-img-preview-sm { max-height: 64px; }
|
||
|
||
/* ── Beautified layout: grouped cards ───────────────────────────────── */
|
||
.sm-group { background: #141414; border: 1px solid #242424; border-radius: 14px; padding: 16px 18px; margin-bottom: 14px; }
|
||
.sm-group-title { display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: #cfcfcf; margin-bottom: 14px; }
|
||
.sm-group-title i { color: #e61e1e; font-size: 15px; }
|
||
#sportsMatchModal .form-label { font-weight: 600; font-size: 12.5px; color: #b9b9b9; }
|
||
.sm-attached { background: #111; border: 1px solid #242424; border-radius: 10px; padding: 10px 14px; color: #d8d8d8; font-size: 13px; }
|
||
.sm-attached i { color: #e61e1e; }
|
||
|
||
/* Participant cards */
|
||
.sm-pcard { background: #111; border: 1px solid #242424; border-radius: 12px; padding: 14px; height: 100%; }
|
||
.sm-pcard-head { display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 700; color: #cfcfcf; margin-bottom: 12px; }
|
||
.sm-pcard-badge { width: 22px; height: 22px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; color: #fff; }
|
||
.sm-badge-1 { background: #2563eb; }
|
||
.sm-badge-2 { background: #e61e1e; }
|
||
.sm-pcard .form-control { margin-bottom: 9px; }
|
||
.sm-pcard .sm-img-field { margin-bottom: 9px; }
|
||
.sm-mini-label { display: block; font-size: 11px; font-weight: 600; color: #8a8a8a; margin: 2px 0 6px; }
|
||
|
||
/* Image field size variants */
|
||
.sm-img-lg .sm-img-drop, .sm-img-lg .sm-img-ph { min-height: 132px; }
|
||
.sm-img-lg .sm-img-preview { max-height: 132px; }
|
||
.sm-img-sm .sm-img-drop, .sm-img-sm .sm-img-ph { min-height: 60px; }
|
||
.sm-img-sm .sm-img-preview { max-height: 60px; }
|
||
.sm-img-wide .sm-img-drop, .sm-img-wide .sm-img-ph { min-height: 120px; }
|
||
.sm-img-wide .sm-img-preview { max-height: 200px; }
|
||
.sm-img-avatar { flex: 0 0 auto; }
|
||
.sm-img-avatar .sm-img-drop { width: 74px; height: 74px; min-height: 74px; border-radius: 50%; }
|
||
.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-preview { width: 74px; height: 74px; max-height: 74px; }
|
||
</style>
|
||
|
||
<script>
|
||
(function () {
|
||
const modalEl = document.getElementById('sportsMatchModal');
|
||
if (!modalEl) return;
|
||
|
||
const form = document.getElementById('sportsMatchForm');
|
||
const idInput = document.getElementById('sm-match-id');
|
||
const videoIdInp = document.getElementById('sm-video-id');
|
||
const statusInp = document.getElementById('sm-status');
|
||
const titleEl = document.getElementById('sportsMatchModalLabel');
|
||
const statusChip = document.getElementById('sm-status-chip');
|
||
const videoFile = document.getElementById('sm-video-file');
|
||
let bsModal, _origBtnHtml = {};
|
||
|
||
// Sport → segment label (only relabels; never rebuilds the form)
|
||
const SEG_LABELS = {
|
||
boxing: 'Rounds', mma: 'Rounds', kickboxing: 'Rounds', wrestling: 'Rounds', judo: 'Rounds', taekwondo: 'Rounds',
|
||
tennis: 'Sets', volleyball: 'Sets', badminton: 'Sets', squash: 'Sets', 'table tennis': 'Sets',
|
||
football: 'Periods', soccer: 'Periods', basketball: 'Quarters', rugby: 'Halves', hockey: 'Periods',
|
||
cricket: 'Innings', baseball: 'Innings', cycling: 'Laps', athletics: 'Heats', esports: 'Maps',
|
||
};
|
||
|
||
function getModal() {
|
||
if (!bsModal) bsModal = new bootstrap.Modal(modalEl);
|
||
return bsModal;
|
||
}
|
||
function token() { return form.querySelector('[name="_token"]').value; }
|
||
|
||
// ── Public entry point ───────────────────────────────────────────────
|
||
window.openSportsMatchModal = function (matchId) {
|
||
resetForm();
|
||
if (matchId) loadMatch(matchId);
|
||
getModal().show();
|
||
};
|
||
|
||
function resetForm() {
|
||
form.reset();
|
||
idInput.value = '';
|
||
videoIdInp.value = '';
|
||
statusInp.value = 'draft';
|
||
titleEl.textContent = 'Create Sports Match';
|
||
statusChip.textContent = 'Draft';
|
||
['sm-officials-list', 'sm-segments-list', 'sm-stats-list', 'sm-extra-list']
|
||
.forEach(id => { const el = document.getElementById(id); if (el) el.innerHTML = ''; });
|
||
form.querySelectorAll('.is-invalid').forEach(el => el.classList.remove('is-invalid'));
|
||
form.querySelectorAll('.sm-img-preview').forEach(img => { img.src = ''; img.classList.add('d-none'); });
|
||
form.querySelectorAll('.sm-img-ph').forEach(ph => ph.classList.remove('d-none'));
|
||
document.getElementById('sm-video-create').classList.remove('d-none');
|
||
document.getElementById('sm-video-attached').classList.add('d-none');
|
||
document.getElementById('sm-advanced').classList.add('d-none'); // advanced shows only when editing
|
||
clearVideoFile();
|
||
setNowDateTime();
|
||
applySegmentLabel('');
|
||
}
|
||
|
||
// Default the date & time pickers to the current date and time
|
||
function setNowDateTime() {
|
||
const now = new Date();
|
||
const pad = n => String(n).padStart(2, '0');
|
||
document.getElementById('sm-date').value = now.getFullYear() + '-' + pad(now.getMonth() + 1) + '-' + pad(now.getDate());
|
||
document.getElementById('sm-time').value = pad(now.getHours()) + ':' + pad(now.getMinutes());
|
||
}
|
||
|
||
// ── Match video upload (create mode) ─────────────────────────────────
|
||
document.getElementById('sm-video-drop').addEventListener('click', () => videoFile.click());
|
||
videoFile.addEventListener('change', function () {
|
||
const f = videoFile.files && videoFile.files[0];
|
||
if (!f) return clearVideoFile();
|
||
document.getElementById('sm-video-err').classList.add('d-none');
|
||
document.getElementById('sm-video-drop').classList.remove('sm-invalid');
|
||
document.getElementById('sm-video-name').textContent = f.name;
|
||
document.getElementById('sm-video-size').textContent = fmtSize(f.size);
|
||
document.getElementById('sm-video-idle').classList.add('d-none');
|
||
document.getElementById('sm-video-picked').classList.remove('d-none');
|
||
});
|
||
document.getElementById('sm-video-clear').addEventListener('click', function (e) {
|
||
e.stopPropagation(); clearVideoFile();
|
||
});
|
||
function clearVideoFile() {
|
||
videoFile.value = '';
|
||
document.getElementById('sm-video-idle').classList.remove('d-none');
|
||
document.getElementById('sm-video-picked').classList.add('d-none');
|
||
}
|
||
function fmtSize(b) {
|
||
if (!b) return '';
|
||
const k = 1024, u = ['B', 'KB', 'MB', 'GB'], i = Math.floor(Math.log(b) / Math.log(k));
|
||
return (b / Math.pow(k, i)).toFixed(1) + ' ' + u[i];
|
||
}
|
||
|
||
// ── Cropper wiring ───────────────────────────────────────────────────
|
||
// Named single images: trigger the field's form-mode cropper.
|
||
window.smOpenCropper = function (cropperId) {
|
||
if (typeof window['openCropperModal_' + cropperId] !== 'function') return;
|
||
window['openCropperModal_' + cropperId]();
|
||
const internal = document.getElementById('tcInput_' + cropperId);
|
||
if (internal) internal.click();
|
||
};
|
||
// Reflect a chosen/cropped file into its field (show preview, hide placeholder).
|
||
function reflectImage(field) {
|
||
if (!field) return;
|
||
const input = field.querySelector('input[type="file"]');
|
||
const img = field.querySelector('.sm-img-preview');
|
||
const ph = field.querySelector('.sm-img-ph');
|
||
if (input && input.files && input.files[0]) {
|
||
if (img) { img.src = URL.createObjectURL(input.files[0]); img.classList.remove('d-none'); }
|
||
if (ph) ph.classList.add('d-none');
|
||
}
|
||
}
|
||
document.querySelectorAll('#sportsMatchModal input[type="file"][name^="media_"]').forEach(inp => {
|
||
inp.addEventListener('change', () => reflectImage(inp.closest('.sm-img-field')));
|
||
});
|
||
|
||
// Officials photos: one shared callback-mode cropper routed to the active row.
|
||
let _smOfficialField = null;
|
||
window.smOfficialCropDone = function (file) {
|
||
const field = _smOfficialField;
|
||
if (field && file) {
|
||
const input = field.querySelector('input[data-n="photo"]');
|
||
if (input) {
|
||
const dt = new DataTransfer(); dt.items.add(file);
|
||
input.files = dt.files;
|
||
}
|
||
const img = field.querySelector('.sm-img-preview');
|
||
const ph = field.querySelector('.sm-img-ph');
|
||
if (img) { img.src = URL.createObjectURL(file); img.classList.remove('d-none'); }
|
||
if (ph) ph.classList.add('d-none');
|
||
}
|
||
if (typeof window.closeCropperModal === 'function') window.closeCropperModal('smc_official');
|
||
};
|
||
|
||
// ── Sport label sync ─────────────────────────────────────────────────
|
||
document.getElementById('sm-sport').addEventListener('input', function () {
|
||
applySegmentLabel(this.value);
|
||
});
|
||
function applySegmentLabel(sport) {
|
||
const label = SEG_LABELS[(sport || '').trim().toLowerCase()] || 'Segments';
|
||
const head = document.querySelector('#sm-heading-seg .sm-acc-title');
|
||
if (head) head.textContent = label;
|
||
const addLbl = document.querySelector('[data-sm-seg-add]');
|
||
if (addLbl) addLbl.textContent = 'Add ' + label.toLowerCase().replace(/s$/, '');
|
||
}
|
||
|
||
// ── Repeatable rows ──────────────────────────────────────────────────
|
||
const REPEAT = {
|
||
official: { tpl: 'sm-tpl-official', list: 'sm-officials-list', name: 'officials' },
|
||
segment: { tpl: 'sm-tpl-segment', list: 'sm-segments-list', name: 'segments' },
|
||
statistic: { tpl: 'sm-tpl-statistic', list: 'sm-stats-list', name: 'statistics' },
|
||
extra: { tpl: 'sm-tpl-extra', list: 'sm-extra-list', name: 'extra_participants' },
|
||
};
|
||
|
||
document.querySelectorAll('[data-sm-add]').forEach(btn => {
|
||
btn.addEventListener('click', () => addRow(btn.getAttribute('data-sm-add')));
|
||
});
|
||
|
||
function addRow(kind, values) {
|
||
const cfg = REPEAT[kind];
|
||
const tpl = document.getElementById(cfg.tpl);
|
||
const node = tpl.content.firstElementChild.cloneNode(true);
|
||
document.getElementById(cfg.list).appendChild(node);
|
||
node.querySelector('[data-sm-remove]').addEventListener('click', () => { node.remove(); renumber(kind); });
|
||
// Official rows: wire the shared photo cropper to this row
|
||
const cropBtn = node.querySelector('[data-sm-official-crop]');
|
||
if (cropBtn) cropBtn.addEventListener('click', () => {
|
||
_smOfficialField = node.querySelector('.sm-img-field');
|
||
window.smOpenCropper('smc_official');
|
||
});
|
||
if (values) {
|
||
node.querySelectorAll('[data-n]').forEach(inp => {
|
||
const key = inp.getAttribute('data-n');
|
||
if (key === 'photo') return; // file input — can't prefill
|
||
if (key === 'photo_existing' && values.photo) { inp.value = values.photo; }
|
||
else if (values[key] != null) inp.value = values[key];
|
||
});
|
||
const pv = node.querySelector('.sm-img-preview');
|
||
const ph = node.querySelector('.sm-img-ph');
|
||
if (pv && values.photo_url) { pv.src = values.photo_url; pv.classList.remove('d-none'); if (ph) ph.classList.add('d-none'); }
|
||
}
|
||
renumber(kind);
|
||
return node;
|
||
}
|
||
|
||
// Renumber a repeatable group's inputs to contiguous 0..n names
|
||
function renumber(kind) {
|
||
const cfg = REPEAT[kind];
|
||
const rows = document.querySelectorAll('#' + cfg.list + ' [data-sm-row="' + kind + '"]');
|
||
rows.forEach((row, i) => {
|
||
row.querySelectorAll('[data-n]').forEach(inp => {
|
||
inp.setAttribute('name', cfg.name + '[' + i + '][' + inp.getAttribute('data-n') + ']');
|
||
});
|
||
});
|
||
}
|
||
|
||
// ── Load an existing record for editing ──────────────────────────────
|
||
function loadMatch(id) {
|
||
fetch('{{ url('sports-matches') }}/' + id + '/edit', { headers: { 'Accept': 'application/json' } })
|
||
.then(r => { if (!r.ok) throw new Error(); return r.json(); })
|
||
.then(({ match }) => populate(match))
|
||
.catch(() => toast('Could not load this match.', 'error'));
|
||
}
|
||
|
||
function populate(m) {
|
||
idInput.value = m.id;
|
||
statusInp.value = m.status || 'draft';
|
||
titleEl.textContent = 'Edit Sports Match';
|
||
statusChip.textContent = (m.status === 'published') ? 'Published' : 'Draft';
|
||
|
||
// Video is already attached in edit mode — hide the uploader, show a note
|
||
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 = m.video_title || ('Video #' + m.video_id);
|
||
// Reveal the advanced area for editing
|
||
document.getElementById('sm-advanced').classList.remove('d-none');
|
||
|
||
const set = (name, val) => { const el = form.querySelector('[name="' + name + '"]'); if (el && val != null) el.value = val; };
|
||
['video_id','sport','title','event_name','match_type','match_date','match_time',
|
||
'participant1_name','participant2_name','referee_name','venue_name'].forEach(k => set(k, m[k]));
|
||
applySegmentLabel(m.sport || '');
|
||
|
||
const grp = (obj, prefix) => { if (!obj) return; Object.keys(obj).forEach(k => { if (k === 'extra') return; set(prefix + '[' + k + ']', obj[k]); }); };
|
||
grp(m.competition, 'competition'); grp(m.venue, 'venue'); grp(m.result, 'result'); grp(m.reviews, 'reviews');
|
||
grp(m.participants, 'participants');
|
||
|
||
// media text + image previews
|
||
if (m.media) {
|
||
set('media[caption]', m.media.caption); set('media[alt]', m.media.alt); set('media[credit]', m.media.credit);
|
||
const pub = form.querySelector('[name="media[public]"]'); if (pub) pub.checked = !!m.media.public;
|
||
}
|
||
Object.entries(m.media_urls || {}).forEach(([key, url]) => {
|
||
const field = form.querySelector('.sm-img-field[data-img="' + key + '"]');
|
||
if (!field) return;
|
||
const img = field.querySelector('.sm-img-preview');
|
||
const ph = field.querySelector('.sm-img-ph');
|
||
if (img) { img.src = url; img.classList.remove('d-none'); }
|
||
if (ph) ph.classList.add('d-none');
|
||
});
|
||
|
||
(m.participants && m.participants.extra || []).forEach(p => addRow('extra', p));
|
||
(m.officials || []).forEach(o => addRow('official', o));
|
||
(m.segments || []).forEach(s => addRow('segment', s));
|
||
(m.statistics || []).forEach(s => addRow('statistic', s));
|
||
}
|
||
|
||
// ── Submit ───────────────────────────────────────────────────────────
|
||
document.getElementById('sm-save-basic').addEventListener('click', () => submit('close'));
|
||
document.getElementById('sm-save-continue').addEventListener('click', () => submit('continue'));
|
||
|
||
const saveBtns = [document.getElementById('sm-save-basic'), document.getElementById('sm-save-continue')];
|
||
function lockButtons(on) { saveBtns.forEach(b => b.disabled = on); }
|
||
function setBtnText(t) {
|
||
saveBtns.forEach(b => {
|
||
if (_origBtnHtml[b.id] === undefined) _origBtnHtml[b.id] = b.innerHTML;
|
||
});
|
||
const cont = document.getElementById('sm-save-continue');
|
||
cont.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>' + t;
|
||
}
|
||
function restoreBtnText() {
|
||
saveBtns.forEach(b => { if (_origBtnHtml[b.id] !== undefined) b.innerHTML = _origBtnHtml[b.id]; });
|
||
}
|
||
function clearErrors() {
|
||
form.querySelectorAll('.is-invalid').forEach(el => el.classList.remove('is-invalid'));
|
||
document.getElementById('sm-video-err').classList.add('d-none');
|
||
document.getElementById('sm-video-drop').classList.remove('sm-invalid');
|
||
}
|
||
|
||
function submit(intent) {
|
||
['official', 'segment', 'statistic', 'extra'].forEach(renumber);
|
||
clearErrors();
|
||
lockButtons(true);
|
||
|
||
// Edit mode → just update the record. Create mode → upload the video first.
|
||
const chain = idInput.value ? Promise.resolve() : uploadVideoFirst();
|
||
chain
|
||
.then(() => postMatch(intent))
|
||
.catch(e => { if (e && e.message !== 'handled') toast((e && e.message) || 'Save failed', 'error'); })
|
||
.finally(() => { lockButtons(false); restoreBtnText(); });
|
||
}
|
||
|
||
// Step 1 (create): upload the match video through the existing pipeline.
|
||
function uploadVideoFirst() {
|
||
if (!validateCreate()) return Promise.reject(new Error('handled'));
|
||
setBtnText('Uploading video…');
|
||
|
||
const vfd = new FormData();
|
||
vfd.append('_token', token());
|
||
vfd.append('video', videoFile.files[0]);
|
||
vfd.append('title', document.getElementById('sm-title').value.trim());
|
||
vfd.append('type', 'match');
|
||
vfd.append('visibility', 'public');
|
||
vfd.append('download_access', 'disabled');
|
||
// Reuse the cropped event poster as the video thumbnail when present.
|
||
const poster = document.getElementById('sm-file-media_event_poster');
|
||
if (poster && poster.files && poster.files[0]) vfd.append('thumbnail', poster.files[0]);
|
||
|
||
return fetch('{{ route('videos.store') }}', {
|
||
method: 'POST',
|
||
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': token() },
|
||
body: vfd,
|
||
})
|
||
.then(async (r) => {
|
||
const d = await r.json().catch(() => ({}));
|
||
if (r.status === 422) { showErrors(d.errors || {}); throw new Error('handled'); }
|
||
if (!r.ok || !d.success) throw new Error(d.message || 'Video upload failed');
|
||
return d;
|
||
})
|
||
.then((d) => { videoIdInp.value = d.video_id; });
|
||
}
|
||
|
||
// Step 2: create/update the match record (video_id already set).
|
||
function postMatch(intent) {
|
||
setBtnText(idInput.value ? 'Saving…' : 'Saving match…');
|
||
const id = idInput.value;
|
||
const url = id ? '{{ url('sports-matches') }}/' + id : '{{ route('sports-matches.store') }}';
|
||
const fd = new FormData(form);
|
||
fd.delete('match_id');
|
||
if (id) fd.append('_method', 'PUT');
|
||
|
||
return fetch(url, {
|
||
method: 'POST',
|
||
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': token() },
|
||
body: fd,
|
||
})
|
||
.then(async (r) => {
|
||
const d = await r.json().catch(() => ({}));
|
||
if (r.status === 422) { showErrors(d.errors || {}); throw new Error('handled'); }
|
||
if (!r.ok) throw new Error(d.message || 'Save failed');
|
||
return d;
|
||
})
|
||
.then((d) => {
|
||
toast(d.message || 'Saved', 'success');
|
||
if (intent === 'close') getModal().hide();
|
||
else populate(d.match); // switch to edit mode, keep editing the same record
|
||
});
|
||
}
|
||
|
||
// Client-side guard so a failed match-save never leaves an orphan video.
|
||
function validateCreate() {
|
||
let ok = true;
|
||
const title = document.getElementById('sm-title');
|
||
if (!videoFile.files || !videoFile.files[0]) {
|
||
document.getElementById('sm-video-err').classList.remove('d-none');
|
||
document.getElementById('sm-video-drop').classList.add('sm-invalid');
|
||
ok = false;
|
||
}
|
||
if (!title.value.trim()) { title.classList.add('is-invalid'); ok = false; }
|
||
if (!ok) toast('Please complete the required fields.', 'error');
|
||
return ok;
|
||
}
|
||
|
||
function dotToBracket(key) {
|
||
const parts = key.split('.');
|
||
return parts[0] + parts.slice(1).map(p => '[' + p + ']').join('');
|
||
}
|
||
|
||
function showErrors(errors) {
|
||
let first = null;
|
||
Object.keys(errors).forEach(key => {
|
||
// key may be dot-notation like "officials.0.photo" → bracket form
|
||
const el = form.querySelector('[name="' + key + '"]')
|
||
|| form.querySelector('[name="' + dotToBracket(key) + '"]');
|
||
const fb = form.querySelector('.invalid-feedback[data-field="' + key + '"]');
|
||
if (el) { el.classList.add('is-invalid'); first = first || el; }
|
||
if (fb) fb.textContent = errors[key][0];
|
||
});
|
||
if (first) {
|
||
const pane = first.closest('.accordion-collapse');
|
||
if (pane && !pane.classList.contains('show')) new bootstrap.Collapse(pane, { show: true });
|
||
setTimeout(() => first.scrollIntoView({ behavior: 'smooth', block: 'center' }), 150);
|
||
}
|
||
toast('Please check the highlighted fields.', 'error');
|
||
}
|
||
|
||
function toast(msg, type) {
|
||
if (typeof window.showToast === 'function') window.showToast(msg, type);
|
||
else console.log('[' + type + '] ' + msg);
|
||
}
|
||
})();
|
||
</script>
|