latest update my Video Platform is working Great

This commit is contained in:
ghassan 2026-02-26 22:35:52 +03:00
parent dcdcafe0ba
commit 3aa49d638d
12 changed files with 440 additions and 83 deletions

47
TODO.md
View File

@ -1,27 +1,32 @@
# TODO: Two-Stage Upload Modal Implementation # TODO: Mobile View Improvements for Channel Page
## Task ## ✅ Completed Improvements
Restructure the video upload modal to have:
- Stage 1: Upload video file (with progress bar)
- Stage 2: Title and Description
- Stage 3: Thumbnail
- Stage 4: Privacy settings
## Implementation Steps ## Phase 1: Channel Header Mobile Improvements ✅
- [x] Reduce header padding from 32px to 16px on mobile
- [x] Scale down avatar from 120px to 80px on mobile, 60px on very small screens
- [x] Adjust channel name font size for mobile
- [x] Stack channel stats vertically on very small screens
- [x] Handle long channel names with ellipsis
- [x] Improve button layout (stack buttons on mobile)
### Step 1: Update VideoController.php ✅ ## Phase 2: Video Grid Mobile Improvements ✅
- [x] Add new endpoint for temp video upload (returns temp filename) - [x] Change grid to 2 columns at 768px (was 480px)
- [x] Modify store method to handle two-step process - [x] Change grid to 1 column at 480px
- [x] Improve video thumbnail aspect ratio handling
- [x] Reduce gaps from 24px to 12px on mobile
### Step 2: Update routes/web.php ✅ ## Phase 3: Video Card Mobile Improvements ✅
- [x] Add route for temp video upload - [x] Reduce video info spacing on mobile
- [x] Scale down channel icon on mobile (36px → 28px)
- [x] Adjust title font size for mobile
- [x] Improve tap targets for more button (32px → 28px)
### Step 3: Update upload-modal.blade.php ✅ ## Phase 4: Additional Mobile Enhancements ✅
- [x] Restructure steps: Video → Details → Thumbnail → Privacy - [x] Pagination styling improvements for mobile
- [x] Update step indicators - [x] Optimize touch interactions (tap to play/pause)
- [x] Add video upload with progress bar in Stage 1 - [x] Add skeleton loading animation support
- [x] Update JavaScript for new flow - [x] Landscape mobile support
- [x] Very small screen (360px) support
### Step 4: Test - [x] Improved header upload button responsive behavior
- [ ] Test the complete upload flow

BIN
public/images/fullLogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
public/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -1,6 +1,6 @@
@extends('layouts.plain') @extends('layouts.plain')
@section('title', 'Login | TAKEONE') @section('title', 'Login | ' . config('app.name'))
@section('extra_styles') @section('extra_styles')
<style> <style>
@ -109,7 +109,9 @@
<div class="auth-container"> <div class="auth-container">
<div class="auth-card"> <div class="auth-card">
<div class="auth-logo"> <div class="auth-logo">
<a href="/videos">TAKEONE</a> <a href="/videos">
<img src="{{ asset('storage/images/fullLogo.png') }}" alt="{{ config('app.name') }}" style="height: 40px;">
</a>
</div> </div>
<h1 class="auth-title">Sign in</h1> <h1 class="auth-title">Sign in</h1>

View File

@ -1,6 +1,6 @@
@extends('layouts.plain') @extends('layouts.plain')
@section('title', 'Register | TAKEONE') @section('title', 'Register | ' . config('app.name'))
@section('extra_styles') @section('extra_styles')
<style> <style>
@ -109,7 +109,9 @@
<div class="auth-container"> <div class="auth-container">
<div class="auth-card"> <div class="auth-card">
<div class="auth-logo"> <div class="auth-logo">
<a href="/videos">TAKEONE</a> <a href="/videos">
<img src="{{ asset('storage/images/fullLogo.png') }}" alt="{{ config('app.name') }}" style="height: 40px;">
</a>
</div> </div>
<h1 class="auth-title">Create Account</h1> <h1 class="auth-title">Create Account</h1>

View File

@ -3,9 +3,11 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', 'TAKEONE')</title> <title>@yield('title', config('app.name'))</title>
@stack('head') <!-- Favicon -->
<link rel="icon" type="image/png" href="{{ asset('storage/images/logo.png') }}">
<link rel="apple-touch-icon" href="{{ asset('storage/images/logo.png') }}">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
@ -256,7 +258,21 @@
border-radius: 50%; border-radius: 50%;
justify-content: center; justify-content: center;
} }
.yt-upload-btn span { display: none; } .yt-upload-btn span {
display: none;
}
/* Show text on larger mobile */
@media (min-width: 400px) {
.yt-upload-btn {
width: auto;
border-radius: 20px;
padding: 8px 16px;
}
.yt-upload-btn span {
display: inline;
}
}
} }
/* Responsive */ /* Responsive */
@ -321,40 +337,18 @@
background: var(--bg-dark); background: var(--bg-dark);
z-index: 1001; z-index: 1001;
padding: 60px 16px 16px; padding: 60px 16px 16px;
justify-content: center;
align-items: flex-start;
} }
.mobile-search-overlay.active { .mobile-search-overlay.active {
display: block;
}
.mobile-search-header {
display: flex; display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.mobile-search-close {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
background: transparent;
border: none;
color: var(--text-primary);
font-size: 1.2rem;
}
.mobile-search-close:hover {
background: var(--border-color);
} }
.mobile-search-form { .mobile-search-form {
display: flex; display: flex;
flex: 1; width: 100%;
max-width: 600px;
height: 44px; height: 44px;
} }
@ -414,17 +408,12 @@
<!-- Mobile Search Overlay --> <!-- Mobile Search Overlay -->
<div class="mobile-search-overlay" id="mobileSearchOverlay"> <div class="mobile-search-overlay" id="mobileSearchOverlay">
<div class="mobile-search-header"> <form action="{{ route('videos.search') }}" method="GET" class="mobile-search-form">
<button type="button" class="mobile-search-close" onclick="toggleMobileSearch()"> <input type="text" name="q" class="mobile-search-input" placeholder="Search" value="{{ request('q') }}">
<i class="bi bi-arrow-left"></i> <button type="submit" class="mobile-search-submit">
<i class="bi bi-search"></i>
</button> </button>
<form action="{{ route('videos.search') }}" method="GET" class="mobile-search-form"> </form>
<input type="text" name="q" class="mobile-search-input" placeholder="Search" value="{{ request('q') }}">
<button type="submit" class="mobile-search-submit">
<i class="bi bi-search"></i>
</button>
</form>
</div>
</div> </div>
<!-- Sidebar Overlay (Mobile) --> <!-- Sidebar Overlay (Mobile) -->

View File

@ -5,20 +5,20 @@
<i class="bi bi-list fs-5"></i> <i class="bi bi-list fs-5"></i>
</button> </button>
<a href="/videos" class="yt-logo"> <a href="/videos" class="yt-logo">
<span class="yt-logo-text">TAKEONE</span> <!-- Mobile logo (visible only on mobile) -->
<img src="{{ asset('storage/images/logo.png') }}" alt="{{ config('app.name') }}" class="d-md-none" style="height: 30px;">
<!-- Desktop logo (visible only on desktop) -->
<img src="{{ asset('storage/images/fullLogo.png') }}" alt="{{ config('app.name') }}" class="d-none d-md-block" style="height: 30px;">
</a> </a>
</div> </div>
<div class="yt-header-center d-none d-md-flex"> <div class="yt-header-center d-none d-md-flex">
<form action="{{ route('videos.search') }}" method="GET" class="yt-search"> <form action="{{ route('videos.search') }}" method="GET" class="yt-search">
<input type="text" name="q" class="yt-search-input" placeholder="Search" value="{{ request('q') }}"> <input type="text" name="q" class="yt-search-input" placeholder="Search" value="{{ request('q') }}">
<button type="submit" class="yt-search-btn"> <button type="submit" class="yt-search-btn">
<i class="bi bi-search"></i> <i class="bi bi-search"></i>
</button> </button>
</form> </form>
<button class="yt-search-voice">
<i class="bi bi-mic-fill"></i>
</button>
</div> </div>
<div class="yt-header-right"> <div class="yt-header-right">

View File

@ -3,7 +3,10 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', 'TAKEONE')</title> <title>@yield('title', config('app.name'))</title>
<!-- Favicon -->
<link rel="icon" type="image/png" href="{{ asset('storage/images/logo.png') }}">
<link rel="apple-touch-icon" href="{{ asset('storage/images/logo.png') }}">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style> <style>

View File

@ -16,6 +16,7 @@
height: 120px; height: 120px;
border-radius: 50%; border-radius: 50%;
object-fit: cover; object-fit: cover;
border: 3px solid var(--border-color);
} }
.channel-name { .channel-name {
@ -26,6 +27,7 @@
.channel-meta { .channel-meta {
color: var(--text-secondary); color: var(--text-secondary);
font-size: 14px;
} }
.channel-stats { .channel-stats {
@ -47,6 +49,11 @@
.yt-video-card { .yt-video-card {
cursor: pointer; cursor: pointer;
transition: transform 0.2s ease;
}
.yt-video-card:active {
transform: scale(0.98);
} }
.yt-video-thumb { .yt-video-thumb {
@ -110,6 +117,7 @@
.yt-video-details { .yt-video-details {
flex: 1; flex: 1;
min-width: 0;
} }
.yt-video-title { .yt-video-title {
@ -132,6 +140,9 @@
.yt-channel-name, .yt-video-meta { .yt-channel-name, .yt-video-meta {
color: var(--text-secondary); color: var(--text-secondary);
font-size: 14px; font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
/* More button */ /* More button */
@ -142,6 +153,20 @@
cursor: pointer; cursor: pointer;
padding: 4px; padding: 4px;
border-radius: 50%; border-radius: 50%;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
.yt-more-btn:hover {
background: var(--border-color);
}
.yt-more-btn:active {
background: var(--border-color);
} }
.yt-more-dropdown { .yt-more-dropdown {
@ -202,22 +227,290 @@
gap: 8px; gap: 8px;
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;
transition: transform 0.2s, background 0.2s;
} }
.yt-upload-btn:hover { .yt-upload-btn:hover {
background: #cc1a1a; background: #cc1a1a;
} }
.yt-upload-btn:active {
transform: scale(0.95);
}
/* Channel action buttons container */
.channel-actions {
display: flex;
gap: 8px;
margin-top: 16px;
flex-wrap: wrap;
}
/* ============================================
MOBILE RESPONSIVE STYLES
============================================ */
@media (max-width: 768px) { @media (max-width: 768px) {
.channel-header {
padding: 20px;
margin-bottom: 16px;
}
.channel-avatar {
width: 90px;
height: 90px;
}
.channel-name {
font-size: 20px;
margin: 12px 0 4px;
}
.channel-meta {
font-size: 13px;
}
.channel-stats {
gap: 16px;
margin-top: 12px;
}
.yt-video-grid { .yt-video-grid {
grid-template-columns: 1fr 1fr; grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.yt-video-title {
font-size: 14px;
}
.yt-channel-icon {
width: 32px;
height: 32px;
}
.yt-channel-name, .yt-video-meta {
font-size: 12px;
}
.yt-more-btn {
width: 28px;
height: 28px;
}
.channel-actions {
flex-direction: column;
width: 100%;
}
.channel-actions .yt-upload-btn {
width: 100%;
justify-content: center;
} }
} }
@media (max-width: 480px) { @media (max-width: 480px) {
.channel-header {
padding: 16px;
border-radius: 8px;
}
.channel-header .d-flex {
flex-direction: column;
align-items: flex-start !important;
gap: 16px !important;
}
.channel-avatar {
width: 80px;
height: 80px;
}
.channel-name {
font-size: 18px;
margin: 8px 0 4px;
}
.channel-meta {
font-size: 12px;
}
.channel-stats {
flex-wrap: wrap;
gap: 12px;
}
.channel-stat-value {
font-size: 14px;
}
.yt-video-grid { .yt-video-grid {
gap: 12px;
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.yt-video-thumb {
border-radius: 8px;
}
.yt-video-info {
gap: 8px;
margin-top: 8px;
}
.yt-channel-icon {
width: 28px;
height: 28px;
}
.yt-video-details {
padding-right: 4px;
}
.yt-video-title {
font-size: 13px;
-webkit-line-clamp: 2;
}
.yt-empty {
padding: 40px 16px;
}
.yt-empty-icon {
font-size: 60px;
}
.yt-empty-title {
font-size: 20px;
}
}
/* Very small screens */
@media (max-width: 360px) {
.channel-avatar {
width: 64px;
height: 64px;
}
.channel-name {
font-size: 16px;
}
.channel-stats {
flex-direction: column;
gap: 4px;
}
.yt-video-info {
padding-right: 0;
}
.yt-video-details {
max-width: calc(100% - 36px);
}
}
/* Landscape mobile */
@media (max-height: 500px) and (orientation: landscape) {
.channel-header {
padding: 12px;
}
.channel-avatar {
width: 60px;
height: 60px;
}
.channel-name {
font-size: 16px;
margin: 8px 0 2px;
}
.channel-meta {
font-size: 11px;
}
.channel-stats {
margin-top: 8px;
}
.yt-video-grid {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
}
/* Pagination improvements */
.pagination {
margin-top: 24px;
}
.pagination .page-link {
background: var(--bg-secondary);
border-color: var(--border-color);
color: var(--text-primary);
padding: 8px 12px;
}
.pagination .page-link:hover {
background: var(--border-color);
border-color: var(--border-color);
color: var(--text-primary);
}
.pagination .page-item.active .page-link {
background: var(--brand-red);
border-color: var(--brand-red);
}
.pagination .page-item.disabled .page-link {
background: var(--bg-secondary);
border-color: var(--border-color);
color: var(--text-secondary);
}
@media (max-width: 480px) {
.pagination {
margin-top: 16px;
}
.pagination .page-link {
padding: 6px 10px;
font-size: 14px;
}
.pagination .page-item:not(.active):not(.disabled) .page-link {
min-width: 36px;
}
}
/* Smooth scroll behavior */
html {
scroll-behavior: smooth;
}
/* Video card skeleton loading */
.skeleton {
background: linear-gradient(90deg, var(--bg-secondary) 25%, var(--border-color) 50%, var(--bg-secondary) 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 8px;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* Mobile touch optimizations */
@media (hover: none) {
.yt-video-card:hover .yt-video-thumb video {
opacity: 0;
}
.yt-video-card.playing .yt-video-thumb video {
opacity: 1;
}
} }
</style> </style>
@endsection @endsection
@ -231,7 +524,7 @@
<img src="https://i.pravatar.cc/150?u={{ $user->id }}" alt="{{ $user->name }}" class="channel-avatar"> <img src="https://i.pravatar.cc/150?u={{ $user->id }}" alt="{{ $user->name }}" class="channel-avatar">
@endif @endif
<div> <div class="channel-info">
<h1 class="channel-name">{{ $user->name }}</h1> <h1 class="channel-name">{{ $user->name }}</h1>
<p class="channel-meta">Joined {{ $user->created_at->format('F d, Y') }}</p> <p class="channel-meta">Joined {{ $user->created_at->format('F d, Y') }}</p>
@ -248,12 +541,12 @@
@auth @auth
@if(Auth::user()->id === $user->id) @if(Auth::user()->id === $user->id)
<div style="margin-top: 16px;"> <div class="channel-actions">
<a href="{{ route('videos.create') }}" class="yt-upload-btn"> <a href="{{ route('videos.create') }}" class="yt-upload-btn">
<i class="bi bi-plus-lg"></i> Upload Video <i class="bi bi-plus-lg"></i> <span>Upload Video</span>
</a> </a>
<a href="{{ route('profile') }}" class="yt-upload-btn" style="background: var(--bg-dark); margin-left: 8px;"> <a href="{{ route('profile') }}" class="yt-upload-btn" style="background: var(--bg-dark);">
<i class="bi bi-pencil"></i> Edit Channel <i class="bi bi-pencil"></i> <span>Edit Channel</span>
</a> </a>
</div> </div>
@endif @endif
@ -390,14 +683,21 @@
@section('scripts') @section('scripts')
<script> <script>
let currentPlayingVideo = null;
function playVideo(card) { function playVideo(card) {
// Skip on touch devices - handled by touch events
if ('ontouchstart' in window) return;
const video = card.querySelector('video'); const video = card.querySelector('video');
if (video) { if (video) {
video.currentTime = 0; video.currentTime = 0;
video.volume = 0.10;
video.play().catch(function(e) { video.play().catch(function(e) {
// Auto-play may be blocked, ignore error // Auto-play may be blocked, ignore error
}); });
video.classList.add('active'); video.classList.add('active');
currentPlayingVideo = card;
} }
} }
@ -410,6 +710,55 @@ function stopVideo(card) {
} }
} }
// Desktop hover behavior
document.querySelectorAll('.yt-video-card').forEach(function(card) {
card.addEventListener('mouseenter', function() {
playVideo(this);
});
card.addEventListener('mouseleave', function() {
stopVideo(this);
});
});
// Mobile touch support with tap-to-play/pause
document.addEventListener('touchstart', function(e) {
const card = e.target.closest('.yt-video-card');
if (card) {
// Stop currently playing video if different
if (currentPlayingVideo && currentPlayingVideo !== card) {
stopVideo(currentPlayingVideo);
}
const video = card.querySelector('video');
if (video) {
if (card.classList.contains('playing')) {
// Pause this video
video.pause();
card.classList.remove('playing');
} else {
// Play this video
video.currentTime = 0;
video.volume = 0.10;
video.play().catch(function(e) {});
video.classList.add('active');
card.classList.add('playing');
currentPlayingVideo = card;
}
}
}
}, { passive: true });
// Stop video when tapping outside
document.addEventListener('touchstart', function(e) {
if (!e.target.closest('.yt-video-card')) {
if (currentPlayingVideo) {
stopVideo(currentPlayingVideo);
currentPlayingVideo.classList.remove('playing');
currentPlayingVideo = null;
}
}
});
function deleteVideo(videoId, videoTitle) { function deleteVideo(videoId, videoTitle) {
if (confirm('Are you sure you want to delete "' + videoTitle + '"? This action cannot be undone.')) { if (confirm('Are you sure you want to delete "' + videoTitle + '"? This action cannot be undone.')) {
fetch(`/videos/${videoId}`, { fetch(`/videos/${videoId}`, {
@ -423,7 +772,6 @@ function deleteVideo(videoId, videoTitle) {
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success || data.redirect) { if (data.success || data.redirect) {
// Reload the page to show updated video list
window.location.reload(); window.location.reload();
} else { } else {
alert('Failed to delete video. Please try again.'); alert('Failed to delete video. Please try again.');
@ -435,6 +783,14 @@ function deleteVideo(videoId, videoTitle) {
}); });
} }
} }
// Smooth scroll for pagination on mobile
document.querySelectorAll('.pagination a').forEach(function(link) {
link.addEventListener('click', function(e) {
// Add loading indicator
document.querySelector('.yt-video-grid').style.opacity = '0.5';
});
});
</script> </script>
@endsection @endsection

View File

@ -1,6 +1,6 @@
@extends('layouts.app') @extends('layouts.app')
@section('title', 'Upload Video | TAKEONE') @section('title', 'Upload Video | ' . config('app.name'))
@section('extra_styles') @section('extra_styles')
<style> <style>

View File

@ -1,6 +1,6 @@
@extends('layouts.app') @extends('layouts.app')
@section('title', isset($query) ? 'Search: ' . $query . ' | TAKEONE' : 'Video Gallery | TAKEONE') @section('title', isset($query) ? 'Search: ' . $query . ' | ' . config('app.name') : 'Video Gallery | ' . config('app.name'))
@section('extra_styles') @section('extra_styles')
<style> <style>

View File

@ -3,20 +3,20 @@
@push('head') @push('head')
<!-- Open Graph / WhatsApp / Facebook / Twitter Preview --> <!-- Open Graph / WhatsApp / Facebook / Twitter Preview -->
<meta property="og:title" content="{{ $video->title }}"> <meta property="og:title" content="{{ $video->title }}">
<meta property="og:description" content="{{ $video->description ? Str::limit($video->description, 200) : 'Check out this video on TAKEONE' }}"> <meta property="og:description" content="{{ $video->description ? Str::limit($video->description, 200) : 'Check out this video on ' . config('app.name') }}">
<meta property="og:image" content="{{ $video->thumbnail_url }}"> <meta property="og:image" content="{{ $video->thumbnail_url }}">
<meta property="og:url" content="{{ $video->share_url }}"> <meta property="og:url" content="{{ $video->share_url }}">
<meta property="og:type" content="video.other"> <meta property="og:type" content="video.other">
<meta property="og:site_name" content="TAKEONE"> <meta property="og:site_name" content="{{ config('app.name') }}">
<!-- Twitter Card --> <!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ $video->title }}"> <meta name="twitter:title" content="{{ $video->title }}">
<meta name="twitter:description" content="{{ $video->description ? Str::limit($video->description, 200) : 'Check out this video on TAKEONE' }}"> <meta name="twitter:description" content="{{ $video->description ? Str::limit($video->description, 200) : 'Check out this video on ' . config('app.name') }}">
<meta name="twitter:image" content="{{ $video->thumbnail_url }}"> <meta name="twitter:image" content="{{ $video->thumbnail_url }}">
@endpush @endpush
@section('title', $video->title . ' | TAKEONE') @section('title', $video->title . ' | ' . config('app.name'))
@section('extra_styles') @section('extra_styles')
<style> <style>