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
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
## ✅ Completed Improvements
## 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 ✅
- [x] Add new endpoint for temp video upload (returns temp filename)
- [x] Modify store method to handle two-step process
## Phase 2: Video Grid Mobile Improvements ✅
- [x] Change grid to 2 columns at 768px (was 480px)
- [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 ✅
- [x] Add route for temp video upload
## Phase 3: Video Card Mobile Improvements ✅
- [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 ✅
- [x] Restructure steps: Video → Details → Thumbnail → Privacy
- [x] Update step indicators
- [x] Add video upload with progress bar in Stage 1
- [x] Update JavaScript for new flow
### Step 4: Test
- [ ] Test the complete upload flow
## Phase 4: Additional Mobile Enhancements ✅
- [x] Pagination styling improvements for mobile
- [x] Optimize touch interactions (tap to play/pause)
- [x] Add skeleton loading animation support
- [x] Landscape mobile support
- [x] Very small screen (360px) support
- [x] Improved header upload button responsive behavior

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')
@section('title', 'Login | TAKEONE')
@section('title', 'Login | ' . config('app.name'))
@section('extra_styles')
<style>
@ -109,7 +109,9 @@
<div class="auth-container">
<div class="auth-card">
<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>
<h1 class="auth-title">Sign in</h1>

View File

@ -1,6 +1,6 @@
@extends('layouts.plain')
@section('title', 'Register | TAKEONE')
@section('title', 'Register | ' . config('app.name'))
@section('extra_styles')
<style>
@ -109,7 +109,9 @@
<div class="auth-container">
<div class="auth-card">
<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>
<h1 class="auth-title">Create Account</h1>

View File

@ -3,9 +3,11 @@
<head>
<meta charset="UTF-8">
<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 rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
@ -256,7 +258,21 @@
border-radius: 50%;
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 */
@ -321,40 +337,18 @@
background: var(--bg-dark);
z-index: 1001;
padding: 60px 16px 16px;
justify-content: center;
align-items: flex-start;
}
.mobile-search-overlay.active {
display: block;
}
.mobile-search-header {
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 {
display: flex;
flex: 1;
width: 100%;
max-width: 600px;
height: 44px;
}
@ -414,17 +408,12 @@
<!-- Mobile Search Overlay -->
<div class="mobile-search-overlay" id="mobileSearchOverlay">
<div class="mobile-search-header">
<button type="button" class="mobile-search-close" onclick="toggleMobileSearch()">
<i class="bi bi-arrow-left"></i>
<form action="{{ route('videos.search') }}" method="GET" class="mobile-search-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 action="{{ route('videos.search') }}" method="GET" class="mobile-search-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>
</form>
</div>
<!-- Sidebar Overlay (Mobile) -->

View File

@ -5,20 +5,20 @@
<i class="bi bi-list fs-5"></i>
</button>
<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>
</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">
<input type="text" name="q" class="yt-search-input" placeholder="Search" value="{{ request('q') }}">
<button type="submit" class="yt-search-btn">
<i class="bi bi-search"></i>
</button>
</form>
<button class="yt-search-voice">
<i class="bi bi-mic-fill"></i>
</button>
</div>
<div class="yt-header-right">

View File

@ -3,7 +3,10 @@
<head>
<meta charset="UTF-8">
<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 rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style>

View File

@ -16,6 +16,7 @@
height: 120px;
border-radius: 50%;
object-fit: cover;
border: 3px solid var(--border-color);
}
.channel-name {
@ -26,6 +27,7 @@
.channel-meta {
color: var(--text-secondary);
font-size: 14px;
}
.channel-stats {
@ -47,6 +49,11 @@
.yt-video-card {
cursor: pointer;
transition: transform 0.2s ease;
}
.yt-video-card:active {
transform: scale(0.98);
}
.yt-video-thumb {
@ -110,6 +117,7 @@
.yt-video-details {
flex: 1;
min-width: 0;
}
.yt-video-title {
@ -132,6 +140,9 @@
.yt-channel-name, .yt-video-meta {
color: var(--text-secondary);
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* More button */
@ -142,6 +153,20 @@
cursor: pointer;
padding: 4px;
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 {
@ -202,22 +227,290 @@
gap: 8px;
cursor: pointer;
text-decoration: none;
transition: transform 0.2s, background 0.2s;
}
.yt-upload-btn:hover {
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) {
.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 {
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) {
.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 {
gap: 12px;
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>
@endsection
@ -231,7 +524,7 @@
<img src="https://i.pravatar.cc/150?u={{ $user->id }}" alt="{{ $user->name }}" class="channel-avatar">
@endif
<div>
<div class="channel-info">
<h1 class="channel-name">{{ $user->name }}</h1>
<p class="channel-meta">Joined {{ $user->created_at->format('F d, Y') }}</p>
@ -248,12 +541,12 @@
@auth
@if(Auth::user()->id === $user->id)
<div style="margin-top: 16px;">
<div class="channel-actions">
<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 href="{{ route('profile') }}" class="yt-upload-btn" style="background: var(--bg-dark); margin-left: 8px;">
<i class="bi bi-pencil"></i> Edit Channel
<a href="{{ route('profile') }}" class="yt-upload-btn" style="background: var(--bg-dark);">
<i class="bi bi-pencil"></i> <span>Edit Channel</span>
</a>
</div>
@endif
@ -390,14 +683,21 @@
@section('scripts')
<script>
let currentPlayingVideo = null;
function playVideo(card) {
// Skip on touch devices - handled by touch events
if ('ontouchstart' in window) return;
const video = card.querySelector('video');
if (video) {
video.currentTime = 0;
video.volume = 0.10;
video.play().catch(function(e) {
// Auto-play may be blocked, ignore error
});
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) {
if (confirm('Are you sure you want to delete "' + videoTitle + '"? This action cannot be undone.')) {
fetch(`/videos/${videoId}`, {
@ -423,7 +772,6 @@ function deleteVideo(videoId, videoTitle) {
.then(response => response.json())
.then(data => {
if (data.success || data.redirect) {
// Reload the page to show updated video list
window.location.reload();
} else {
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>
@endsection

View File

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

View File

@ -1,6 +1,6 @@
@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')
<style>

View File

@ -3,20 +3,20 @@
@push('head')
<!-- Open Graph / WhatsApp / Facebook / Twitter Preview -->
<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:url" content="{{ $video->share_url }}">
<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 -->
<meta name="twitter:card" content="summary_large_image">
<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 }}">
@endpush
@section('title', $video->title . ' | TAKEONE')
@section('title', $video->title . ' | ' . config('app.name'))
@section('extra_styles')
<style>