Add upload type chooser and redesign upload modal

Clicking Create now opens a card-based chooser (Generic / Music / Sports)
before the upload modal; the chosen type is applied and its Content Type
dropdown is hidden as redundant.

Per type:
- Generic/Match show their fields inline in the modal (no card/popup);
  Music keeps the track-card + Track Editor popup for multi-language tracks.
- "Language Track" wording stays music-only; a single Language field is now
  available for generic/match too (mirrored on the mobile create page with
  name-swapping so only the active picker submits).

Also unifies all modal controls (dropdowns, selects, inputs) to one larger,
red-accented dark style scoped to #uploadModal.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
ghassan 2026-05-24 14:12:08 +03:00
parent d9959c4452
commit 6aae6f86b6
5 changed files with 343 additions and 13 deletions

View File

@ -184,6 +184,7 @@ Stored value: IANA timezone string (e.g. `"Asia/Bahrain"`).
|---|---|---|
| `resources/views/layouts/partials/upload-modal.blade.php` | `primary_language` | Inside accordion track 1 body (`id="lang-tracks-section-modal"`); extra track language rows use `LANG_OPTIONS_MODAL` JS constant for inline dynamic CSD |
| `resources/views/videos/create.blade.php` | `primary_language` | Inside accordion track 1 body (`id="lang-tracks-section-create"`); extra track language rows use `LANG_OPTIONS_CREATE` JS constant for inline dynamic CSD |
| `resources/views/videos/create.blade.php` | `primary_language` (`id="video_language_create"`) | Video-mode language field inside `#basic-fields-create` (generic/match). `setAudioMode()` swaps `name="primary_language"` between this and `primary_language_create` so only the active mode's picker submits |
| `resources/views/components/track-editor-form.blade.php` | `$languageName` (prop) | Rendered inside the track editor form; used for primary track in edit-video-modal (prefix `t1`) |
| `resources/views/videos/edit.blade.php` | `primary_language` | Inside `@else` (audio only) block; pre-populated with `value="{{ $video->language ?? '' }}"` |

View File

@ -28,7 +28,7 @@
@auth
<!-- Create / Upload -->
<button type="button" class="yt-upload-btn d-none d-md-flex"
data-bs-toggle="modal" data-bs-target="#uploadModal">
onclick="openUploadChooser()">
<i class="bi bi-camera-video-fill"></i>
<span>Create</span>
</button>

View File

@ -1,4 +1,41 @@
@php use App\Data\Languages; @endphp
{{-- ── Upload type chooser (desktop) shown before the upload modal ── --}}
<div id="upload-type-chooser" class="utc-overlay" onclick="if(event.target===this)closeUploadChooser()">
<div class="utc-box" role="dialog" aria-modal="true" aria-labelledby="utc-title">
<button type="button" class="btn-close btn-close-white utc-close" onclick="closeUploadChooser()" aria-label="Close"></button>
<div class="utc-head">
<div class="utc-head-icon"><i class="bi bi-stars"></i></div>
<h3 class="utc-title" id="utc-title">What are you creating?</h3>
<p class="utc-sub">Pick a type to get started you can change it later.</p>
</div>
<div class="utc-grid">
<button type="button" class="utc-card" data-accent="generic" onclick="chooseUploadType('generic')">
<span class="utc-card-ico"><i class="bi bi-film"></i></span>
<span class="utc-card-title">Generic</span>
<span class="utc-card-desc">Videos, vlogs &amp; anything else</span>
<i class="bi bi-arrow-right-short utc-card-arr"></i>
</button>
<button type="button" class="utc-card" data-accent="music" onclick="chooseUploadType('music')">
<span class="utc-card-ico"><i class="bi bi-music-note-beamed"></i></span>
<span class="utc-card-title">Music</span>
<span class="utc-card-desc">Songs with cover art &amp; languages</span>
<i class="bi bi-arrow-right-short utc-card-arr"></i>
</button>
<button type="button" class="utc-card" data-accent="match" onclick="chooseUploadType('match')">
<span class="utc-card-ico"><i class="bi bi-trophy"></i></span>
<span class="utc-card-title">Sports</span>
<span class="utc-card-desc">Matches with rounds &amp; annotations</span>
<i class="bi bi-arrow-right-short utc-card-arr"></i>
</button>
</div>
</div>
</div>
<div class="modal fade" id="uploadModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable um-dialog">
@ -47,7 +84,7 @@
{{-- ── Global Settings ── --}}
<div class="um-gs-row">
<div class="um-gs-wrap">
<div class="um-gs-wrap" id="gs-type-wrap">
<span class="um-gs-lbl">Content Type</span>
<button type="button" class="um-gs-btn" id="gs-type-btn">
<i class="bi bi-film um-gs-ico" id="gs-type-ico"></i>
@ -92,7 +129,7 @@
<div class="um-rule"></div>
{{-- ── Track Cards Section ── --}}
<div class="um-tracks-header">
<div class="um-tracks-header" id="um-tracks-header">
<div>
<span class="um-tracks-title" id="um-tracks-section-label">Language Tracks</span>
<span class="um-tracks-sub" id="um-tracks-section-sub">Add audio tracks in different languages</span>
@ -107,7 +144,7 @@
<div id="um-tc-t1">
{{-- Empty state: just the Add button --}}
<button type="button" class="action-btn action-btn-primary" id="um-tc-t1-add-btn" onclick="openTrackPopup('t1')" style="width:100%;justify-content:center;padding:16px;font-size:14px;border-radius:12px;">
<i class="bi bi-plus-circle"></i> <span>Add Language Track</span>
<i class="bi bi-plus-circle"></i> <span>Add Video Details</span>
</button>
{{-- Filled state: track card (hidden until lang or title is set) --}}
<div class="um-track-card" id="um-tc-t1-card" style="display:none;">
@ -131,6 +168,9 @@
<div id="um-tc-extra"></div>
</div>
{{-- Inline host for the track-1 form (generic/match show fields here, no popup) --}}
<div id="um-t1-inline-host"></div>
{{-- Progress --}}
<div id="progress-container-modal" class="um-prog">
<div class="um-prog-track">
@ -159,7 +199,7 @@
</div>
<button type="button" class="btn-close btn-close-white" onclick="closeTrackPopup()" aria-label="Close"></button>
</div>
<div class="um-tp-body">
<div class="um-tp-body" id="um-tp-body">
{{-- Track 1 form (Blade-rendered) --}}
<div class="um-track-form" id="um-tf-t1" style="display:none;">
@ -181,7 +221,6 @@
<div>
<label class="um-lbl" style="font-size:10px;margin-bottom:6px;">Title <span class="um-req">*</span></label>
<input type="text" id="lt-track1-title-modal" class="um-input"
style="font-size:13px;padding:9px 12px;"
placeholder="Track title…" autocomplete="off">
</div>
</div>
@ -605,6 +644,174 @@
/* ── Submit ──────────────────────────────────────────────── */
.um-submit { width: 100%; justify-content: center; font-size: 14px; font-weight: 700; letter-spacing: .02em; padding: 13px 20px; margin-top: 16px; }
.um-submit:disabled { opacity: .45; cursor: not-allowed; }
/* ── Upload type chooser ─────────────────────────────────────── */
.utc-overlay {
position: fixed; inset: 0; z-index: 1065;
background: rgba(8,8,8,.78); backdrop-filter: blur(8px);
display: none; align-items: center; justify-content: center; padding: 20px;
opacity: 0; transition: opacity .22s ease;
}
.utc-overlay.show { display: flex; opacity: 1; }
.utc-box {
position: relative; width: 100%; max-width: 640px;
background: #181818; border: 1px solid #262626; border-radius: 22px;
padding: 30px 30px 34px;
box-shadow: 0 32px 80px rgba(0,0,0,.8), 0 0 0 1px rgba(255,255,255,.04);
transform: translateY(14px) scale(.97); transition: transform .26s cubic-bezier(.22,1,.36,1);
}
.utc-overlay.show .utc-box { transform: translateY(0) scale(1); }
.utc-close { position: absolute; top: 18px; right: 18px; }
.utc-head { text-align: center; margin-bottom: 24px; }
.utc-head-icon {
width: 52px; height: 52px; margin: 0 auto 14px;
background: rgba(230,30,30,.13); border: 1px solid rgba(230,30,30,.28);
border-radius: 15px; display: flex; align-items: center; justify-content: center;
font-size: 24px; color: #e61e1e;
}
.utc-title { font-size: 21px; font-weight: 800; color: #f1f1f1; margin: 0 0 6px; letter-spacing: -.01em; }
.utc-sub { font-size: 13px; color: #666; margin: 0; }
.utc-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
@media (max-width: 560px) { .utc-grid { grid-template-columns: 1fr; } }
.utc-card {
position: relative; display: flex; flex-direction: column; align-items: center; text-align: center;
gap: 4px; padding: 24px 16px 20px;
background: #111; border: 1.5px solid #242424; border-radius: 16px;
color: inherit; font-family: inherit; cursor: pointer;
transition: transform .18s ease, border-color .18s ease, background .18s ease, box-shadow .18s ease;
--accent: #e61e1e;
}
.utc-card[data-accent="generic"] { --accent: #3b82f6; }
.utc-card[data-accent="music"] { --accent: #a855f7; }
.utc-card[data-accent="match"] { --accent: #f59e0b; }
.utc-card:hover {
transform: translateY(-4px); background: #161616;
border-color: color-mix(in srgb, var(--accent) 60%, transparent);
box-shadow: 0 14px 32px rgba(0,0,0,.5);
}
.utc-card-ico {
width: 56px; height: 56px; margin-bottom: 10px;
display: flex; align-items: center; justify-content: center;
border-radius: 16px; font-size: 26px;
color: var(--accent);
background: color-mix(in srgb, var(--accent) 14%, transparent);
border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
transition: transform .2s ease;
}
.utc-card:hover .utc-card-ico { transform: scale(1.08) rotate(-4deg); }
.utc-card-title { font-size: 15px; font-weight: 700; color: #f1f1f1; }
.utc-card-desc { font-size: 11.5px; color: #666; line-height: 1.4; max-width: 150px; }
.utc-card-arr {
position: absolute; top: 12px; right: 12px; font-size: 20px;
color: var(--accent); opacity: 0; transform: translateX(-4px);
transition: opacity .18s ease, transform .18s ease;
}
.utc-card:hover .utc-card-arr { opacity: 1; transform: translateX(0); }
/* ════════════════════════════════════════════════════════════════
Unified control system one consistent look for every field,
dropdown and picker in the upload modal. Scoped to #uploadModal
(which contains the track popup too) so the styling follows the
track-1 form whether it is shown inline or inside the popup, and
the shared .csd-* component is untouched elsewhere in the app.
════════════════════════════════════════════════════════════════ */
/* ── Labels: identical size / weight / colour everywhere ── */
#uploadModal .um-gs-lbl,
#uploadModal .um-lbl,
#uploadModal .csd-lbl {
font-size: 10px; font-weight: 700; letter-spacing: .06em;
text-transform: uppercase; color: #8a8a8a; margin-bottom: 7px;
}
#uploadModal .um-lbl i { color: #e61e1e; font-size: 12px; }
/* ── Buttons, selects & inputs: same height / radius / border / bg ── */
#uploadModal .um-gs-btn,
#uploadModal .csd-btn,
#uploadModal .um-input {
min-height: 50px;
background: #161616;
border: 1px solid #2c2c2c;
border-radius: 12px;
font-size: 14px;
color: #f1f1f1;
box-sizing: border-box;
transition: border-color .15s ease, background .15s ease, box-shadow .15s ease;
}
#uploadModal .um-gs-btn,
#uploadModal .csd-btn {
display: flex; align-items: center; gap: 10px;
padding: 0 15px; line-height: 1.2;
}
#uploadModal .um-input { padding: 13px 15px; }
/* Hover / focus / open — red accent matching the modal theme */
#uploadModal .um-gs-btn:hover, #uploadModal .um-gs-btn.open,
#uploadModal .csd-btn:hover, #uploadModal .csd-btn[aria-expanded="true"],
#uploadModal .um-input:focus {
border-color: #e61e1e;
background: #1b1414;
outline: none;
}
#uploadModal .um-gs-btn.open,
#uploadModal .csd-btn[aria-expanded="true"],
#uploadModal .um-input:focus { box-shadow: 0 0 0 3px rgba(230,30,30,.13); }
/* Icons & chevrons — sized up; chevron picks up the red accent on open */
#uploadModal .um-gs-ico { font-size: 16px; color: #b85656; }
#uploadModal .csd-ico { font-size: 18px; }
#uploadModal .um-gs-arr,
#uploadModal .csd-arr { font-size: 11px; color: #777; margin-left: auto; }
#uploadModal .um-gs-btn.open .um-gs-arr,
#uploadModal .csd-btn[aria-expanded="true"] .csd-arr { color: #e61e1e; }
#uploadModal .um-gs-txt,
#uploadModal .csd-val { flex: 1; color: #f1f1f1; }
#uploadModal .csd-val.ph { color: #5f5f5f; }
/* ── Dropdown panels: dark surface tuned to the modal, red-accented options ── */
#uploadModal .um-gs-menu,
#uploadModal .csd-panel {
background: #1a1a1a;
border: 1px solid #2c2c2c;
border-radius: 13px;
box-shadow: 0 18px 44px rgba(0,0,0,.65), 0 0 0 1px rgba(230,30,30,.04);
padding: 7px;
}
#uploadModal .um-gs-opt,
#uploadModal .csd-opt {
border-radius: 9px;
padding: 11px 12px;
font-size: 14px;
color: #cfcfcf;
gap: 10px;
}
#uploadModal .um-gs-opt:hover,
#uploadModal .csd-opt:hover {
background: rgba(230,30,30,.10);
color: #ffffff;
}
#uploadModal .um-gs-opt.active,
#uploadModal .csd-opt[aria-selected="true"] {
background: rgba(230,30,30,.16);
color: #ff6b6b;
}
#uploadModal .um-gs-opt i { font-size: 16px; width: 18px; color: inherit; }
/* Language search box inside the picker — match the surface */
#uploadModal .csd-srch { border-bottom: 1px solid #2c2c2c; padding: 11px 13px; }
#uploadModal .csd-list { padding: 5px; }
#uploadModal .csd-sinput { font-size: 14px; }
/* ── Rich-text editor (description) — align border & radius only ── */
#uploadModal .rte-wrap {
border-color: #2b2b2b !important;
border-radius: 11px !important;
overflow: hidden;
}
#uploadModal .rte-wrap:focus-within { border-color: #e61e1e !important; }
</style>
<script>
@ -614,6 +821,47 @@ let _fileSelected = false;
let _umExtraCount = 0;
let _currentMode = 'generic'; // 'generic', 'music', 'match'
// ── Type chooser (desktop) ────────────────────────────────────
const _UTC_META = {
generic: { icon: 'bi-film', label: 'Generic' },
music: { icon: 'bi-music-note-beamed', label: 'Music' },
match: { icon: 'bi-trophy', label: 'Match' },
};
function openUploadChooser() {
// Mobile keeps the existing full-page create flow (with its own type picker)
if (window.innerWidth < 992) {
window.location.href = '{{ route("videos.create") }}';
return;
}
document.getElementById('upload-type-chooser').classList.add('show');
}
function closeUploadChooser() {
document.getElementById('upload-type-chooser').classList.remove('show');
}
function chooseUploadType(type) {
const meta = _UTC_META[type] || _UTC_META.generic;
closeUploadChooser();
openUploadModal();
// Apply the chosen content type once the modal is on screen
setTimeout(() => {
_gsSetDefault('type', type, meta.icon, meta.label);
_applyMode(type);
// Type was already picked from the chooser cards — hide the redundant dropdown
const typeWrap = document.getElementById('gs-type-wrap');
if (typeWrap) typeWrap.style.display = 'none';
}, 60);
}
// Close chooser on Escape
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && document.getElementById('upload-type-chooser')?.classList.contains('show')) {
closeUploadChooser();
}
});
// ── Modal open / close ────────────────────────────────────────
function openUploadModal() {
if (window.innerWidth < 992) {
@ -747,18 +995,38 @@ function _applyMode(type) {
// Show/hide "Add Language Track" button
document.getElementById('um-add-track-btn').style.display = isMusic ? '' : 'none';
// Update tracks section label
// The tracks-section header (label + subtitle) is only meaningful for music's
// language-track list — hide it for generic/match where fields are shown inline.
const tracksHeader = document.getElementById('um-tracks-header');
if (tracksHeader) tracksHeader.style.display = isMusic ? '' : 'none';
// Update tracks section label (music only)
const lbl = document.getElementById('um-tracks-section-label');
const sub = document.getElementById('um-tracks-section-sub');
if (isMusic) {
if (lbl) lbl.textContent = 'Language Tracks';
if (sub) sub.textContent = 'Add audio tracks in different languages';
} else {
if (lbl) lbl.textContent = type === 'match' ? 'Match Video' : 'Video Details';
if (sub) sub.textContent = 'Click Edit on the track below to add your file and details';
}
// Show/hide fields in popup form (language always visible)
// Empty-state primary button label — "Language Track" wording is exclusive to music
const t1AddLbl = document.querySelector('#um-tc-t1-add-btn span');
if (t1AddLbl) {
t1AddLbl.textContent = isMusic ? 'Add Language Track'
: (type === 'match' ? 'Add Match Details' : 'Add Video Details');
}
// The single "Language" field is universal metadata (what language the content is in) —
// shown for every type. Only the multi-track "Add Language Track" feature above is music-only.
const langWrap = document.getElementById('um-tf-t1-lang-wrap');
if (langWrap) langWrap.style.display = '';
// "Primary track" wording is a music concept — hide it for generic/match
const primaryNote = document.querySelector('#um-tf-t1 .um-tf-primary-note');
const primaryBadge = document.querySelector('#um-tc-t1-card .um-tc-primary');
if (primaryNote) primaryNote.style.display = isMusic ? '' : 'none';
if (primaryBadge) primaryBadge.style.display = isMusic ? '' : 'none';
// Show/hide fields in popup form
const videoZone = document.getElementById('um-tf-t1-video-zone');
const thumbWrap = document.getElementById('um-tf-t1-thumb-wrap');
const musicPair = document.getElementById('um-tf-t1-music-pair');
@ -790,6 +1058,35 @@ function _applyMode(type) {
}
document.getElementById('type_modal').value = type;
// Generic/match: show the track-1 fields inline in the modal (no button/card/popup).
// Music: keep the track-card + Track Editor popup workflow (multiple language tracks).
_positionT1Form(isMusic);
}
// Relocate the track-1 form between the inline host (generic/match) and the popup (music)
function _positionT1Form(isMusic) {
const form = document.getElementById('um-tf-t1');
const host = document.getElementById('um-t1-inline-host');
const tcList = document.getElementById('um-tc-list');
const tpBody = document.getElementById('um-tp-body');
const extra = document.getElementById('um-tf-extra');
if (!form) return;
if (isMusic) {
// Form lives in the popup, hidden until the user opens a track for editing
if (tpBody && form.parentElement !== tpBody) tpBody.insertBefore(form, extra);
form.style.display = 'none';
if (host) host.style.display = 'none';
if (tcList) tcList.style.display = '';
updateTrackCard('t1');
} else {
// Fields are shown directly in the modal body
if (host && form.parentElement !== host) host.appendChild(form);
form.style.display = '';
if (host) host.style.display = '';
if (tcList) tcList.style.display = 'none';
}
}
// ── Helpers ───────────────────────────────────────────────────
@ -1108,6 +1405,12 @@ function clearSlidesForTrack(e, tid) {
// ── Track popup ───────────────────────────────────────────────
function openTrackPopup(trackId) {
// Generic/match: track-1 fields are inline in the modal — bring them into view instead of a popup
if (trackId === 't1' && _currentMode !== 'music') {
const host = document.getElementById('um-t1-inline-host');
if (host) host.scrollIntoView({ behavior: 'smooth', block: 'center' });
return;
}
document.querySelectorAll('.um-track-form').forEach(f => f.style.display = 'none');
const form = document.getElementById('um-tf-' + trackId);
if (form) form.style.display = '';

View File

@ -1386,7 +1386,7 @@ $headerSocialMap = [
<i class="bi bi-pencil"></i>
<span>Edit channel</span>
</button>
<button class="ch-btn-ghost" data-bs-toggle="modal" data-bs-target="#uploadModal">
<button class="ch-btn-ghost" onclick="openUploadChooser()">
<i class="bi bi-camera-video"></i>
<span>Upload</span>
</button>
@ -1793,7 +1793,7 @@ $headerSocialMap = [
<p>This channel hasn't uploaded any videos.</p>
@auth
@if(Auth::id() === $user->id)
<button class="ch-btn-primary" data-bs-toggle="modal" data-bs-target="#uploadModal">
<button class="ch-btn-primary" onclick="openUploadChooser()">
<i class="bi bi-cloud-upload"></i> Upload your first video
</button>
@endif

View File

@ -190,6 +190,14 @@
<!-- Title + Description (video mode) -->
<div id="basic-fields-create">
<div class="form-group">
<x-language-select
name="primary_language"
id="video_language_create"
label="Language"
placeholder="Select language"
/>
</div>
<div class="form-group">
<label class="form-label">Title *</label>
<input type="text" name="title" id="video-title" class="form-input" placeholder="Enter video title">
@ -496,6 +504,18 @@
innerTitle.removeAttribute('name'); innerDesc.removeAttribute('name');
}
// Language: video mode submits the basic-fields picker; music mode submits the track-1 picker.
// Only one carries name="primary_language" at a time so the value never collides.
const audioLang = document.getElementById('primary_language_create');
const videoLang = document.getElementById('video_language_create');
if (audio) {
if (videoLang) videoLang.removeAttribute('name');
if (audioLang) audioLang.setAttribute('name', 'primary_language');
} else {
if (audioLang) audioLang.removeAttribute('name');
if (videoLang) videoLang.setAttribute('name', 'primary_language');
}
if (audio) {
document.querySelectorAll('#type-options .option-item').forEach(o => o.classList.remove('active'));
const musicOpt = document.querySelector('#type-options .option-item[data-type="music"]');
@ -1038,6 +1058,12 @@
}
}
// Default mode is generic (video): only the video-mode language picker submits primary_language
;(function() {
const audioLang = document.getElementById('primary_language_create');
if (audioLang) audioLang.removeAttribute('name');
})();
// Wire primary language select → update track 1 header
;(function() {
const plInput = document.getElementById('primary_language_create');