From 72e94397271c10cdac82846351e69005f913b6b7 Mon Sep 17 00:00:00 2001 From: ghassan Date: Mon, 2 Mar 2026 02:16:28 +0300 Subject: [PATCH] made the video cards components --- app/Http/Controllers/VideoController.php | 6 +- app/Models/Video.php | 26 + ..._02_25_000000_add_type_to_videos_table.php | 22 + .../views/components/video-card.blade.php | 817 +++++ resources/views/layouts/app.blade.php | 142 +- .../partials/edit-video-modal.blade.php | 112 +- resources/views/user/channel.blade.php | 102 +- resources/views/user/history.blade.php | 30 +- resources/views/user/liked.blade.php | 30 +- resources/views/videos/create.blade.php | 30 + resources/views/videos/index.blade.php | 87 +- resources/views/videos/show.blade.php | 4 +- resources/views/videos/test.txt | 1 - .../views/videos/types/generic.blade.php | 123 + resources/views/videos/types/match.blade.php | 2691 +++++++++++++++++ resources/views/videos/types/music.blade.php | 0 16 files changed, 3973 insertions(+), 250 deletions(-) create mode 100644 database/migrations/2026_02_25_000000_add_type_to_videos_table.php create mode 100644 resources/views/components/video-card.blade.php delete mode 100644 resources/views/videos/test.txt create mode 100644 resources/views/videos/types/generic.blade.php create mode 100644 resources/views/videos/types/match.blade.php create mode 100644 resources/views/videos/types/music.blade.php diff --git a/app/Http/Controllers/VideoController.php b/app/Http/Controllers/VideoController.php index 461a2b1..0cd7913 100644 --- a/app/Http/Controllers/VideoController.php +++ b/app/Http/Controllers/VideoController.php @@ -58,6 +58,7 @@ class VideoController extends Controller 'video' => 'required|file|mimes:mp4,webm,ogg,mov,avi,wmv,flv,mkv|max:512000', 'thumbnail' => 'nullable|image|mimes:jpg,jpeg,png,webp|max:5120', 'visibility' => 'nullable|in:public,unlisted,private', + 'type' => 'nullable|in:generic,music,match', ]); $videoFile = $request->file('video'); @@ -148,6 +149,7 @@ class VideoController extends Controller 'height' => $height, 'status' => 'processing', 'visibility' => $request->visibility ?? 'public', + 'type' => $request->type ?? 'generic', ]); // Dispatch compression job in the background @@ -221,6 +223,7 @@ class VideoController extends Controller 'thumbnail' => $video->thumbnail, 'thumbnail_url' => $video->thumbnail ? asset('storage/thumbnails/' . $video->thumbnail) : null, 'visibility' => $video->visibility ?? 'public', + 'type' => $video->type ?? 'generic', ] ]); } @@ -237,9 +240,10 @@ class VideoController extends Controller 'description' => 'nullable|string', 'thumbnail' => 'nullable|image|mimes:jpg,jpeg,png,webp|max:5120', 'visibility' => 'nullable|in:public,unlisted,private', + 'type' => 'nullable|in:generic,music,match', ]); - $data = $request->only(['title', 'description', 'visibility']); + $data = $request->only(['title', 'description', 'visibility', 'type']); if ($request->hasFile('thumbnail')) { if ($video->thumbnail) { diff --git a/app/Models/Video.php b/app/Models/Video.php index 0d0ee99..294ff66 100644 --- a/app/Models/Video.php +++ b/app/Models/Video.php @@ -21,6 +21,7 @@ class Video extends Model 'height', 'status', 'visibility', + 'type', ]; protected $casts = [ @@ -137,5 +138,30 @@ class Video extends Model } return $query->where('visibility', '!=', 'private'); } + + // Video type helpers + public function getTypeIconAttribute() + { + return match($this->type) { + 'music' => 'bi-music-note', + 'match' => 'bi-trophy', + default => 'bi-film', + }; + } + + public function isGeneric() + { + return $this->type === 'generic'; + } + + public function isMusic() + { + return $this->type === 'music'; + } + + public function isMatch() + { + return $this->type === 'match'; + } } diff --git a/database/migrations/2026_02_25_000000_add_type_to_videos_table.php b/database/migrations/2026_02_25_000000_add_type_to_videos_table.php new file mode 100644 index 0000000..319fad7 --- /dev/null +++ b/database/migrations/2026_02_25_000000_add_type_to_videos_table.php @@ -0,0 +1,22 @@ +enum('type', ['generic', 'music', 'match'])->default('generic')->after('visibility'); + }); + } + + public function down() + { + Schema::table('videos', function (Blueprint $table) { + $table->dropColumn('type'); + }); + } +}; diff --git a/resources/views/components/video-card.blade.php b/resources/views/components/video-card.blade.php new file mode 100644 index 0000000..1ba8f81 --- /dev/null +++ b/resources/views/components/video-card.blade.php @@ -0,0 +1,817 @@ +@props(['video' => null, 'size' => 'medium']) + +@php +$videoUrl = $video ? asset('storage/videos/' . $video->filename) : null; +$thumbnailUrl = $video && $video->thumbnail + ? asset('storage/thumbnails/' . $video->thumbnail) + : ($video ? 'https://picsum.photos/seed/' . $video->id . '/640/360' : 'https://picsum.photos/seed/random/640/360'); + +$typeIcon = $video ? match($video->type) { + 'music' => 'bi-music-note', + 'match' => 'bi-trophy', + default => 'bi-film', +} : 'bi-film'; + +// Check if current user is the owner of the video +$isOwner = $video && auth()->check() && auth()->id() == $video->user_id; + +// Size classes +$sizeClasses = match($size) { + 'small' => 'yt-video-card-sm', + default => '', +}; +@endphp + +
+ +
+ {{ $video->title ?? 'Video' }} + @if($videoUrl) + + @endif + @if($video && $video->duration) + {{ gmdate('i:s', $video->duration) }} + @endif +
+
+
+
+ @if($video && $video->user && $video->user->avatar_url) + {{ $video->user->name }} + @endif +
+
+

+ + + {{ $video->title ?? 'Untitled Video' }} + +

+ @if($video && $video->user) +
{{ $video->user->name }}
+ @endif + @if($video) +
+ {{ number_format($video->view_count) }} views β€’ {{ $video->created_at->diffForHumans() }} +
+ @endif +
+ @if($video) +
+ + +
+ @endif +
+
+ + + + + + + diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 292c2ad..ee9fcdc 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -398,6 +398,13 @@ background: var(--border-color); color: var(--text-primary); } + + /* Modal input focus */ + #deleteVideoInput:focus { + outline: none; + border-color: #ef4444 !important; + box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2); + } @yield('extra_styles') @@ -431,6 +438,54 @@ @auth @include('layouts.partials.upload-modal') @include('layouts.partials.edit-video-modal') + + + @endauth @@ -531,9 +586,94 @@ restoreSidebarState(); } }); + + // Delete video modal functions + let currentDeleteVideoId = null; + let currentDeleteVideoTitle = ''; + + function showDeleteModal(videoId, videoTitle) { + currentDeleteVideoId = videoId; + currentDeleteVideoTitle = videoTitle; + + document.getElementById('deleteVideoName').textContent = videoTitle; + document.getElementById('deleteVideoInput').value = ''; + + const deleteBtn = document.getElementById('confirmDeleteBtn'); + deleteBtn.disabled = true; + deleteBtn.style.opacity = '0.5'; + deleteBtn.style.cursor = 'not-allowed'; + + // Close the dropdown first + const dropdown = document.querySelector('.dropdown-menu.show'); + if (dropdown) { + dropdown.classList.remove('show'); + } + + // Show the modal + const modalElement = document.getElementById('deleteVideoModal'); + const modal = new bootstrap.Modal(modalElement); + modal.show(); + } + + document.getElementById('deleteVideoInput').addEventListener('input', function(e) { + const deleteBtn = document.getElementById('confirmDeleteBtn'); + if (e.target.value === currentDeleteVideoTitle) { + deleteBtn.disabled = false; + deleteBtn.style.opacity = '1'; + deleteBtn.style.cursor = 'pointer'; + } else { + deleteBtn.disabled = true; + deleteBtn.style.opacity = '0.5'; + deleteBtn.style.cursor = 'not-allowed'; + } + }); + + function confirmDeleteVideo() { + if (!currentDeleteVideoId || !currentDeleteVideoTitle) return; + + const inputValue = document.getElementById('deleteVideoInput').value; + if (inputValue !== currentDeleteVideoTitle) { + alert('Video name does not match. Please try again.'); + return; + } + + fetch(`/videos/${currentDeleteVideoId}`, { + method: 'DELETE', + headers: { + 'X-CSRF-TOKEN': '{{ csrf_token() }}', + 'Accept': 'application/json' + } + }) + .then(response => { + // Check for successful response + if (response.status === 200 || response.status === 302 || response.redirected) { + // Close modal first + const modalElement = document.getElementById('deleteVideoModal'); + const modal = bootstrap.Modal.getInstance(modalElement); + if (modal) { + modal.hide(); + } + // Reload the page + window.location.reload(); + } else if (response.status === 403) { + alert('You do not have permission to delete this video.'); + } else if (response.status === 404) { + alert('Video not found.'); + } else { + response.json().then(data => { + alert(data.message || 'Failed to delete video. Please try again.'); + }).catch(() => { + alert('Failed to delete video. Please try again.'); + }); + } + }) + .catch(error => { + console.error('Error:', error); + alert('An error occurred while deleting the video.'); + }); + } @yield('scripts') - diff --git a/resources/views/layouts/partials/edit-video-modal.blade.php b/resources/views/layouts/partials/edit-video-modal.blade.php index a88afac..e5891bf 100644 --- a/resources/views/layouts/partials/edit-video-modal.blade.php +++ b/resources/views/layouts/partials/edit-video-modal.blade.php @@ -83,6 +83,35 @@ class="form-textarea" placeholder="Tell viewers about your video"> + +
+ +
+ + + +
+
@@ -486,6 +515,63 @@ color: #666; } +/* Video Type Options */ +.video-type-options { + display: flex; + gap: 12px; +} + +.video-type-option { + flex: 1; + cursor: pointer; +} + +.video-type-option input { + display: none; +} + +.video-type-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + padding: 16px 12px; + background: #1a1a1a; + border: 2px solid #333; + border-radius: 12px; + transition: all 0.2s; +} + +.video-type-option:hover .video-type-content { + border-color: #555; + background: #1f1f1f; +} + +.video-type-option.active .video-type-content { + border-color: #e61e1e; + background: rgba(230, 30, 30, 0.1); +} + +.video-type-content i { + font-size: 24px; + color: #666; + transition: color 0.2s; +} + +.video-type-option.active .video-type-content i { + color: #e61e1e; +} + +.video-type-content span { + font-size: 13px; + font-weight: 500; + color: #aaa; +} + +.video-type-option.active .video-type-content span { + color: #e5e5e5; +} + /* Dropzone */ .dropzone-modal { border: 2px dashed #404040; @@ -777,6 +863,10 @@ width: 30px; } + .video-type-options { + flex-direction: column; + } + .step-navigation { flex-direction: column-reverse; gap: 12px; @@ -826,6 +916,17 @@ function openEditVideoModal(videoId) { thumbnailWrapper.classList.remove('has-thumbnail'); } + // Set video type + const videoType = video.type || 'generic'; + document.querySelectorAll('.video-type-option').forEach(opt => { + opt.classList.remove('active'); + const radio = opt.querySelector('input[type="radio"]'); + if (radio.value === videoType) { + radio.checked = true; + opt.classList.add('active'); + } + }); + // Set visibility const visibility = video.visibility || 'public'; document.querySelectorAll('.visibility-option-modal').forEach(opt => { @@ -987,6 +1088,16 @@ function removeEditThumbnail(e) { document.getElementById('edit-thumbnail-info').classList.remove('active'); } +// Video type options +const videoTypeOptions = document.querySelectorAll('.video-type-option'); +videoTypeOptions.forEach(option => { + const radio = option.querySelector('input[type="radio"]'); + radio.addEventListener('change', function() { + videoTypeOptions.forEach(opt => opt.classList.remove('active')); + option.classList.add('active'); + }); +}); + // Visibility options const editVisibilityOptions = document.querySelectorAll('.visibility-option-modal'); editVisibilityOptions.forEach(option => { @@ -1048,4 +1159,3 @@ document.getElementById('edit-video-form-modal').addEventListener('submit', func }); }); - diff --git a/resources/views/user/channel.blade.php b/resources/views/user/channel.blade.php index 7554098..cf7fc8c 100644 --- a/resources/views/user/channel.blade.php +++ b/resources/views/user/channel.blade.php @@ -571,107 +571,7 @@ @else
@foreach($videos as $video) -
- -
- @if($video->thumbnail) - {{ $video->title }} - @else - {{ $video->title }} - @endif - - @if($video->duration) - {{ gmdate('i:s', $video->duration) }} - @endif -
-
-
-
- @if($video->user && $video->user->avatar_url) - {{ $video->user->name }} - @endif -
-
-

- {{ $video->title }} -

-
{{ $video->user->name ?? 'Unknown' }}
-
- {{ number_format($video->size / 1024 / 1024, 0) }} MB β€’ {{ $video->created_at->diffForHumans() }} -
-
-
- - -
-
-
+ @endforeach
diff --git a/resources/views/user/history.blade.php b/resources/views/user/history.blade.php index 3cd9317..36369e7 100644 --- a/resources/views/user/history.blade.php +++ b/resources/views/user/history.blade.php @@ -142,35 +142,7 @@ @else
@foreach($videos as $video) -
- -
- @if($video->thumbnail) - {{ $video->title }} - @else - {{ $video->title }} - @endif - @if($video->duration) - {{ gmdate('i:s', $video->duration) }} - @endif -
-
-
-
- @if($video->user && $video->user->avatar_url) - {{ $video->user->name }} - @endif -
-
-

- {{ $video->title }} -

-
- {{ $video->user->name ?? 'Unknown' }} β€’ {{ number_format($video->size / 1024 / 1024, 0) }} MB -
-
-
-
+ @endforeach
@endif diff --git a/resources/views/user/liked.blade.php b/resources/views/user/liked.blade.php index 2fd17d5..14692d9 100644 --- a/resources/views/user/liked.blade.php +++ b/resources/views/user/liked.blade.php @@ -142,35 +142,7 @@ @else
@foreach($videos as $video) -
- -
- @if($video->thumbnail) - {{ $video->title }} - @else - {{ $video->title }} - @endif - @if($video->duration) - {{ gmdate('i:s', $video->duration) }} - @endif -
-
-
-
- @if($video->user && $video->user->avatar_url) - {{ $video->user->name }} - @endif -
-
-

- {{ $video->title }} -

-
- {{ $video->user->name ?? 'Unknown' }} β€’ {{ number_format($video->size / 1024 / 1024, 0) }} MB -
-
-
-
+ @endforeach
diff --git a/resources/views/videos/create.blade.php b/resources/views/videos/create.blade.php index 3461f01..7fb61fd 100644 --- a/resources/views/videos/create.blade.php +++ b/resources/views/videos/create.blade.php @@ -302,6 +302,36 @@
+
+ +
+ + + +
+
+
diff --git a/resources/views/videos/index.blade.php b/resources/views/videos/index.blade.php index a21eff4..06a18d4 100644 --- a/resources/views/videos/index.blade.php +++ b/resources/views/videos/index.blade.php @@ -230,92 +230,7 @@ @else
@foreach($videos as $video) -
- -
- @if($video->thumbnail) - {{ $video->title }} - @else - {{ $video->title }} - @endif - - @if($video->duration) - {{ gmdate('i:s', $video->duration) }} - @endif -
-
-
-
- @if($video->user && $video->user->avatar_url) - {{ $video->user->name }} - @endif -
-
-

- {{ $video->title }} -

-
{{ $video->user->name ?? 'Unknown' }}
-
- {{ number_format($video->size / 1024 / 1024, 0) }} MB β€’ {{ $video->created_at->diffForHumans() }} -
-
-
- - -
-
-
+ @endforeach
diff --git a/resources/views/videos/show.blade.php b/resources/views/videos/show.blade.php index 8c6f46c..283ccba 100644 --- a/resources/views/videos/show.blade.php +++ b/resources/views/videos/show.blade.php @@ -246,8 +246,10 @@ @endsection @section('content') +
+
@@ -328,6 +330,7 @@
More videos coming soon...
+
@include('layouts.partials.share-modal') @@ -368,4 +371,3 @@ }); @endsection - diff --git a/resources/views/videos/test.txt b/resources/views/videos/test.txt deleted file mode 100644 index 9daeafb..0000000 --- a/resources/views/videos/test.txt +++ /dev/null @@ -1 +0,0 @@ -test diff --git a/resources/views/videos/types/generic.blade.php b/resources/views/videos/types/generic.blade.php new file mode 100644 index 0000000..133e93c --- /dev/null +++ b/resources/views/videos/types/generic.blade.php @@ -0,0 +1,123 @@ + +
+ + +
+ +
+ +
+ + +

{{ $video->title }}

+ + +
+
+ {{ number_format($video->size / 1024 / 1024, 0) }} MB + β€’ + {{ $video->created_at->format('M d, Y') }} + @if($video->width && $video->height) + β€’ + {{ $video->width }}x{{ $video->height }} + @endif +
+
+ @auth + +
+ @csrf + +
+ + + @if(Auth::id() === $video->user_id) + + @endif + @else + + Like + + @endauth + @if($video->isShareable()) + + @endif +
+
+ + + + + + @if($video->description) +
+

{{ $video->description }}

+
+ @endif +
+ + +
+

Up Next

+ +
More videos coming soon...
+
+ +
+ +@include('layouts.partials.share-modal') +@include('layouts.partials.edit-video-modal') + +@if(Session::has('openEditModal') && Session::get('openEditModal')) +@auth + +@endauth +@endif + + diff --git a/resources/views/videos/types/match.blade.php b/resources/views/videos/types/match.blade.php new file mode 100644 index 0000000..6c2340e --- /dev/null +++ b/resources/views/videos/types/match.blade.php @@ -0,0 +1,2691 @@ +@extends('layouts.app') + +@push('extra_styles') + +@endpush + +@push('scripts') + +@endpush + +@section('content') + + +
+ + +
+ +
+
+ +
+ + + +
+
+
+
+
+ +
+
+
+
+ +
+ 0:00 + / + 0:00 +
+
+
+ + +
+ +
+
+
+
+ +
+
+ +
+

+ Taekwondo Finals – + πŸ‡§πŸ‡­ Ali Mohammed + vs + πŸ‡ΈπŸ‡¦ Omar Al‑Khaled + (U18) +

+ πŸ“ Championship 2026 β€’ Match# 103 β€’ Court 2 β€’ + M. Al‑Khalifa +
+
+
+ + + + +
+
+ + +
+ + + +
+ + +
+
+
Final β€’ Male -63kg division
+
+ +
+
+
+
+ Blue fighter +
+
+ + Blue +
+
+
+
Ali Mohammed
+
Blue Dragons Club
+
+ πŸ‡§πŸ‡­ + Bahrain +
+
+
+ +
+
+ 7 + Final score + 5 +
+
+
+ Referee +
+
+ M. Al‑Khalifa +
+
Center Referee
+
+
+ +
+
+
+ + Red +
+
+ Red fighter +
+
+
+
Omar Al‑Khaled
+
Red Tigers TKD
+
+ πŸ‡ΈπŸ‡¦ + Saudi Arabia +
+
+
+
+ + +
+ +
+

Match Overview

+

+ This Male U18 -63kg final features three 2:00 rounds (with a + golden point tie-breaker) showcasing a classic clash of styles: + Blue drives the action with relentless pressure, body kicks, and + ring control, while Red utilizes angle changes to set up tactical + head-shot counters. When analyzing the replay, coaches and + athletes should focus on distance management and guard resets + immediately following exchanges, specifically noting how conceding + points affects a fighter's momentum and leads to hesitation. + Furthermore, close attention should be paid to the effectiveness + of corner instructions and tactical time-outs, evaluating how + consistently each fighter adheres to the established game plan + during the high-fatigue environment of the final 30 seconds of + every round. +

+

+ Coaches can also focus on corner instructions between rounds, the + timing of tactical time-outs, and how often each athlete follows + the game plan under fatigue in the closing 30 seconds of every + round. +

+
+ +
+ +
+ + +
+
+

Comments & replies

+ +
+ +
+
+
+ User avatar +
+
+
+ Viewer_21 + β€’ + 2 min ago + β€’ + @00:32–00:50 +
+
+ Love how Blue keeps pressure here, but you can see the guard + dropping right after scoring. +
+
+ + 12 likes +
+
+
+ Coach Ahmed +
+ Coach Ahmed: Exactly. This is the clip we use + in training when we talk about "score and cover". +
+
+
+ +
+
+ User avatar +
+
+
+ TKDfan + β€’ + 10 min ago + β€’ + @full video +
+
+ Great match overall, both fighters stayed active until the last + seconds. Respect to both teams. +
+
+ + 5 likes +
+
+
+
+
+ + + + + + + + + + + + + + + +
+ + +
+

Up Next

+ +
More videos coming soon...
+
+ +
+ +@endsection diff --git a/resources/views/videos/types/music.blade.php b/resources/views/videos/types/music.blade.php new file mode 100644 index 0000000..e69de29