takeone-youtube-clone/resources/views/layouts/partials/sports-match-modal.blade.php
ghassan 73527f3781 Add sports-match type, device tracking, profile visits, and share refactor
- 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
2026-05-29 01:50:28 +03:00

888 lines
57 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{{-- ════════════════════════════════════════════════════════════════════════
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 &amp; 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 &amp; 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 &amp; 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>