latest update

This commit is contained in:
ghassan committed 2026-03-11 11:21:33 +03:00
1 parent 9ad842dcd5
commit 062c0e896f
42 files changed
+7052 -2921

No files matched your search

+162 -10
View File
@@ -2,8 +2,8 @@
@php
$videoUrl = $video ? asset('storage/videos/' . $video->filename) : null;
$thumbnailUrl = $video && $video->thumbnail
? asset('storage/thumbnails/' . $video->thumbnail)
$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) {
@@ -12,6 +12,9 @@ $typeIcon = $video ? match($video->type) {
default => 'bi-film',
} : 'bi-film';
// Check if video is shorts
$isShorts = $video && $video->isShorts();
// Check if current user is the owner of the video
$isOwner = $video && auth()->check() && auth()->id() == $video->user_id;
@@ -34,6 +37,11 @@ $sizeClasses = match($size) {
@if($video && $video->duration)
<span class="yt-video-duration">{{ gmdate('i:s', $video->duration) }}</span>
@endif
@if($isShorts)
<span class="yt-shorts-badge">
<i class="bi bi-collection-play-fill"></i> SHORTS
</span>
@endif
</div>
</a>
<div class="yt-video-info">
@@ -139,7 +147,7 @@ $sizeClasses = match($size) {
<form id="edit-video-form-{{ $video->id ?? '' }}" enctype="multipart/form-data">
@csrf
@method('PUT')
<!-- Title -->
<div class="cute-form-group">
<label><i class="bi bi-card-heading"></i> Title</label>
@@ -171,6 +179,16 @@ $sizeClasses = match($size) {
</div>
</div>
<!-- Shorts Toggle -->
<div class="cute-form-group">
<label><i class="bi bi-lightning-charge-fill"></i> Shorts</label>
<label class="cute-shorts-toggle">
<input type="checkbox" name="is_shorts" id="edit-is-shorts-{{ $video->id ?? '' }}" value="1">
<span class="cute-shorts-slider"></span>
<span class="cute-shorts-label">Mark as Short</span>
</label>
</div>
<!-- Thumbnail -->
<div class="cute-form-group">
<label><i class="bi bi-image"></i> Thumbnail</label>
@@ -267,6 +285,27 @@ $sizeClasses = match($size) {
font-weight: 500;
}
.yt-video-card .yt-shorts-badge {
position: absolute;
top: 8px;
left: 8px;
background: rgba(230, 30, 30, 0.9);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 700;
display: flex;
align-items: center;
gap: 4px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.yt-video-card .yt-shorts-badge i {
font-size: 12px;
}
.yt-video-card .yt-video-info {
display: flex;
margin-top: 12px;
@@ -544,6 +583,56 @@ $sizeClasses = match($size) {
color: #ff6b8a;
}
/* Shorts Toggle in Edit Modal */
.cute-shorts-toggle {
position: relative;
display: inline-flex;
align-items: center;
cursor: pointer;
margin-top: 8px;
}
.cute-shorts-toggle input {
opacity: 0;
width: 0;
height: 0;
}
.cute-shorts-slider {
position: relative;
width: 44px;
height: 24px;
background-color: #333;
border-radius: 12px;
transition: 0.3s;
margin-right: 10px;
}
.cute-shorts-slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
border-radius: 50%;
transition: 0.3s;
}
.cute-shorts-toggle input:checked + .cute-shorts-slider {
background-color: #e63030;
}
.cute-shorts-toggle input:checked + .cute-shorts-slider:before {
transform: translateX(20px);
}
.cute-shorts-label {
color: #aaa;
font-size: 12px;
}
/* Thumbnail Upload */
.cute-thumbnail-upload {
border: 2px dashed #444;
@@ -650,7 +739,7 @@ $sizeClasses = match($size) {
max-width: 320px;
margin: 10px;
}
.cute-type-options, .cute-privacy-options {
flex-direction: column;
}
@@ -658,6 +747,63 @@ $sizeClasses = match($size) {
</style>
<script>
// Global function to save to Watch Later
function saveToWatchLater(videoId) {
fetch(`/videos/${videoId}/watch-later`, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(data.message || 'Added to Watch Later');
}
})
.catch(error => console.error('Error:', error));
}
// Global function to open playlist modal
function openPlaylistModal(videoId) {
// Set the current video ID for the modal as global variable
window.currentVideoIdForModal = videoId;
// Close any open dropdown menus first
const activeDropdowns = document.querySelectorAll('.dropdown-menu.show');
activeDropdowns.forEach(function(dropdown) {
dropdown.classList.remove('show');
});
// Also close Bootstrap dropdowns by clicking the toggle
const dropdownToggles = document.querySelectorAll('.dropdown-toggle[aria-expanded="true"]');
dropdownToggles.forEach(function(toggle) {
toggle.click();
});
// Try to open the add to playlist modal
if (typeof openAddToPlaylistModal === 'function') {
openAddToPlaylistModal(videoId);
} else {
// Modal might not be loaded, try to find and show it directly
const modal = document.getElementById('addToPlaylistModal');
if (modal) {
modal.style.display = 'flex';
modal.style.opacity = '1';
} else {
// Fallback - redirect to login
window.location.href = '{{ route("login") }}?redirect=' + encodeURIComponent(window.location.href);
}
}
}
// Global function to add to queue
function addToQueue(videoId) {
alert('Queue feature coming soon!');
}
function playVideo(element) {
const video = element.querySelector('video');
if (video) {
@@ -685,7 +831,7 @@ function openEditVideoModal(videoId) {
const modalId = 'editVideoModal' + (videoId || '');
const modal = new bootstrap.Modal(document.getElementById(modalId));
modal.show();
// Fetch video data
fetch(`/videos/${videoId}/edit`, {
headers: {
@@ -700,7 +846,7 @@ function openEditVideoModal(videoId) {
const video = data.video;
document.getElementById('edit-title-' + videoId).value = video.title || '';
document.getElementById('edit-description-' + videoId).value = video.description || '';
// Set type
const typeOptions = document.querySelectorAll('#' + modalId + ' .cute-type-option');
typeOptions.forEach(opt => {
@@ -710,7 +856,7 @@ function openEditVideoModal(videoId) {
opt.querySelector('input').checked = true;
}
});
// Set privacy
const privacyOptions = document.querySelectorAll('#' + modalId + ' .cute-privacy-option');
privacyOptions.forEach(opt => {
@@ -720,7 +866,13 @@ function openEditVideoModal(videoId) {
opt.querySelector('input').checked = true;
}
});
// Set shorts toggle
const shortsCheckbox = document.getElementById('edit-is-shorts-' + videoId);
if (shortsCheckbox) {
shortsCheckbox.checked = video.is_shorts === true || video.is_shorts === 1 || video.is_shorts === '1';
}
// Clear status
const statusEl = document.getElementById('edit-status-' + videoId);
statusEl.className = 'cute-status';
@@ -780,10 +932,10 @@ document.addEventListener('submit', function(e) {
const formData = new FormData(form);
const statusEl = document.getElementById('edit-status-' + videoId);
const submitBtn = form.querySelector('.cute-btn-save');
submitBtn.disabled = true;
submitBtn.textContent = 'Saving...';
fetch(`/videos/${videoId}`, {
method: 'POST',
body: formData,
+122 -112
View File
@@ -4,11 +4,14 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', config('app.name'))</title>
<!-- Open Graph meta tags default - will be overridden by page-specific tags -->
@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/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css">
<style>
@@ -20,18 +23,18 @@
--text-primary: #f1f1f1;
--text-secondary: #aaaaaa;
}
* { box-sizing: border-box; }
body {
background-color: var(--bg-dark);
color: var(--text-primary);
font-family: "Roboto", "Arial", sans-serif;
body {
background-color: var(--bg-dark);
color: var(--text-primary);
font-family: "Roboto", "Arial", sans-serif;
margin: 0;
padding: 0;
overflow-x: hidden;
}
/* Header */
.yt-header {
position: fixed;
@@ -47,13 +50,13 @@
z-index: 1000;
border-bottom: 1px solid var(--border-color);
}
.yt-header-left {
display: flex;
align-items: center;
gap: 16px;
}
.yt-menu-btn {
width: 40px;
height: 40px;
@@ -67,37 +70,38 @@
border: none;
color: var(--text-primary);
}
.yt-menu-btn:hover { background: var(--border-color); }
.yt-logo {
display: flex;
align-items: center;
text-decoration: none;
gap: 4px;
}
.yt-logo-text {
font-size: 1.4rem;
font-weight: 700;
color: var(--text-primary);
letter-spacing: -1px;
}
/* Search */
.yt-header-center {
flex: 1;
max-width: 640px;
margin: 0 40px;
display: flex;
align-items: center;
}
.yt-search {
flex: 1;
display: flex;
height: 40px;
}
.yt-search-input {
flex: 1;
background: #121212;
@@ -108,12 +112,12 @@
color: var(--text-primary);
font-size: 16px;
}
.yt-search-input:focus {
outline: none;
border-color: #1c62b9;
}
.yt-search-btn {
width: 64px;
background: #222;
@@ -122,9 +126,9 @@
color: var(--text-primary);
cursor: pointer;
}
.yt-search-btn:hover { background: #303030; }
.yt-search-voice {
width: 40px;
height: 40px;
@@ -135,14 +139,14 @@
margin-left: 8px;
cursor: pointer;
}
/* Header Right */
.yt-header-right {
display: flex;
align-items: center;
gap: 8px;
}
.yt-icon-btn {
width: 40px;
height: 40px;
@@ -156,9 +160,9 @@
color: var(--text-primary);
font-size: 1.2rem;
}
.yt-icon-btn:hover { background: var(--border-color); }
.yt-user-avatar {
width: 32px;
height: 32px;
@@ -166,7 +170,7 @@
background: #555;
cursor: pointer;
}
/* Sidebar */
.yt-sidebar {
position: fixed;
@@ -180,13 +184,13 @@
transition: transform 0.3s;
z-index: 999;
}
.yt-sidebar-section {
padding-bottom: 12px;
border-bottom: 1px solid var(--border-color);
margin-bottom: 12px;
}
.yt-sidebar-link {
display: flex;
align-items: center;
@@ -199,16 +203,16 @@
cursor: pointer;
transition: background 0.2s;
}
.yt-sidebar-link:hover { background: var(--border-color); }
.yt-sidebar-link.active {
background: var(--border-color);
font-weight: 500;
}
.yt-sidebar-link i { font-size: 1.2rem; }
/* Mobile sidebar overlay */
.yt-sidebar-overlay {
position: fixed;
@@ -220,9 +224,9 @@
z-index: 998;
display: none;
}
.yt-sidebar-overlay.show { display: block; }
/* Main Content */
.yt-main {
margin-top: 56px;
@@ -231,7 +235,7 @@
min-height: calc(100vh - 56px);
transition: margin-left 0.3s;
}
/* Upload Button */
.yt-upload-btn {
background: var(--brand-red);
@@ -246,9 +250,9 @@
cursor: pointer;
text-decoration: none;
}
.yt-upload-btn:hover { background: #cc1a1a; }
/* Mobile circle button */
@media (max-width: 576px) {
.yt-upload-btn {
@@ -258,10 +262,10 @@
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 {
@@ -274,39 +278,39 @@
}
}
}
/* Responsive */
@media (min-width: 992px) {
.yt-sidebar.collapsed {
transform: translateX(-100%);
}
.yt-main.collapsed {
margin-left: 0;
}
}
@media (max-width: 991px) {
.yt-sidebar {
transform: translateX(-100%);
}
.yt-sidebar.open {
transform: translateX(0);
}
.yt-main {
margin-left: 0;
}
.yt-search-voice { display: none; }
}
@media (max-width: 768px) {
.yt-header-center { display: none; }
.yt-main { padding: 16px; }
}
/* Mobile Search Toggle Button */
.yt-mobile-search-toggle {
width: 40px;
@@ -321,11 +325,11 @@
color: var(--text-primary);
font-size: 1.2rem;
}
.yt-mobile-search-toggle:hover {
background: var(--border-color);
}
/* Mobile Search Overlay */
.mobile-search-overlay {
display: none;
@@ -340,18 +344,18 @@
justify-content: center;
align-items: flex-start;
}
.mobile-search-overlay.active {
display: flex;
}
.mobile-search-form {
display: flex;
width: 100%;
max-width: 600px;
height: 44px;
}
.mobile-search-input {
flex: 1;
background: #121212;
@@ -362,12 +366,12 @@
color: var(--text-primary);
font-size: 16px;
}
.mobile-search-input:focus {
outline: none;
border-color: #1c62b9;
}
.mobile-search-submit {
width: 50px;
background: #222;
@@ -379,26 +383,26 @@
align-items: center;
justify-content: center;
}
.mobile-search-submit:hover {
background: #303030;
}
/* Dropdown */
.dropdown-menu-dark {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
}
.dropdown-item {
color: var(--text-primary);
}
.dropdown-item:hover {
background: var(--border-color);
color: var(--text-primary);
}
/* Modal input focus */
#deleteVideoInput:focus {
outline: none;
@@ -406,13 +410,13 @@
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2);
}
</style>
@yield('extra_styles')
</head>
<body class="{{ $bodyClass ?? '' }}">
<!-- Header -->
@include('layouts.partials.header')
<!-- Mobile Search Overlay -->
<div class="mobile-search-overlay" id="mobileSearchOverlay">
<form action="{{ route('videos.trending') }}" method="GET" class="mobile-search-form">
@@ -422,13 +426,13 @@
</button>
</form>
</div>
<!-- Sidebar Overlay (Mobile) -->
<div class="yt-sidebar-overlay" onclick="toggleSidebar()"></div>
<!-- Sidebar -->
@include('layouts.partials.sidebar')
<!-- Main Content -->
<main class="yt-main @yield('main_class')" id="main">
@yield('content')
@@ -438,8 +442,13 @@
@auth
@include('layouts.partials.upload-modal')
@include('layouts.partials.edit-video-modal')
@endauth
<!-- Add to Playlist Modal - Available for all users (shows login prompt if not authenticated) -->
@include('layouts.partials.add-to-playlist-modal')
<!-- Delete Video Modal -->
@auth
<div class="modal fade" id="deleteVideoModal" tabindex="-1" aria-labelledby="deleteVideoModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content" style="background: #1a1a1a; border: 1px solid #3f3f3f; border-radius: 12px;">
@@ -468,9 +477,9 @@
<label for="deleteVideoInput" style="color: #aaa; font-size: 14px; margin-bottom: 8px; display: block;">
To confirm deletion, type <strong style="color: #fff;">"<span id="deleteVideoName"></span>"</strong> below:
</label>
<input type="text"
id="deleteVideoInput"
class="form-control"
<input type="text"
id="deleteVideoInput"
class="form-control"
style="background: #282828; border: 1px solid #3f3f3f; color: #fff; padding: 12px 16px; border-radius: 8px; font-size: 14px;"
placeholder="Enter video name">
</div>
@@ -518,7 +527,7 @@
function toggleMobileSearch() {
const overlay = document.getElementById('mobileSearchOverlay');
overlay.classList.toggle('active');
// Focus input when overlay opens
if (overlay.classList.contains('active')) {
setTimeout(function() {
@@ -526,7 +535,7 @@
}, 100);
}
}
// Close mobile search on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
@@ -536,16 +545,16 @@
}
}
});
// Sidebar toggle function
function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
const overlay = document.querySelector('.yt-sidebar-overlay');
const main = document.getElementById('main');
// Check if we're on mobile or desktop
const isMobile = window.innerWidth < 992;
if (isMobile) {
// Mobile behavior - use 'open' class
sidebar.classList.toggle('open');
@@ -554,19 +563,19 @@
// Desktop behavior - use 'collapsed' class
sidebar.classList.toggle('collapsed');
main.classList.toggle('collapsed');
// Save state to localStorage
const isCollapsed = sidebar.classList.contains('collapsed');
localStorage.setItem('sidebarCollapsed', isCollapsed);
}
}
// Restore sidebar state from localStorage on page load
function restoreSidebarState() {
const sidebar = document.getElementById('sidebar');
const main = document.getElementById('main');
const isMobile = window.innerWidth < 992;
// Only restore on desktop
if (!isMobile) {
const savedState = localStorage.getItem('sidebarCollapsed');
@@ -576,15 +585,15 @@
}
}
}
// Set active sidebar link based on current route
document.addEventListener('DOMContentLoaded', function() {
// Restore sidebar state
restoreSidebarState();
const currentPath = window.location.pathname;
const sidebarLinks = document.querySelectorAll('.yt-sidebar-link');
sidebarLinks.forEach(link => {
const href = link.getAttribute('href');
if (href === currentPath || (currentPath.startsWith(href) && href !== '/')) {
@@ -594,13 +603,13 @@
}
});
});
// Handle window resize to reset state for mobile
window.addEventListener('resize', function() {
const sidebar = document.getElementById('sidebar');
const main = document.getElementById('main');
const isMobile = window.innerWidth < 992;
if (isMobile) {
// On mobile, remove collapsed state
sidebar.classList.remove('collapsed');
@@ -610,35 +619,35 @@
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) {
@@ -651,16 +660,16 @@
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: {
@@ -669,7 +678,7 @@
}
})
.then(response => {
// Check for successful response
// Check for successful response
if (response.status === 200 || response.status === 302 || response.redirected) {
// Close modal first
const modalElement = document.getElementById('deleteVideoModal');
@@ -697,7 +706,7 @@
});
}
</script>
@yield('scripts')
</body>
</html>
@@ -711,17 +720,17 @@
min-width: 44px;
min-height: 44px;
}
/* Reduce padding on main content */
.yt-main {
padding: 12px 8px !important;
}
/* Smaller video title */
.yt-video-title, .video-title {
font-size: 14px !important;
}
/* Channel info compact */
.channel-info {
gap: 8px !important;
@@ -736,12 +745,12 @@
.channel-subs {
font-size: 12px !important;
}
/* Video meta smaller */
.yt-channel-name, .yt-video-meta {
font-size: 12px !important;
}
/* Action buttons horizontal scroll on mobile */
.video-actions {
overflow-x: auto;
@@ -749,13 +758,13 @@
padding-bottom: 8px;
width: 100%;
}
.yt-action-btn {
flex-shrink: 0;
padding: 8px 12px;
font-size: 13px;
}
/* Comment improvements */
.comment-item {
flex-direction: column;
@@ -764,36 +773,36 @@
width: 32px !important;
height: 32px !important;
}
/* Search input mobile */
.yt-search-input {
font-size: 14px;
padding: 0 12px;
}
/* Header spacing */
.yt-header {
padding: 0 8px;
}
/* User dropdown full width on mobile */
.dropdown-menu {
width: 100%;
min-width: 200px;
}
}
/* Very small screens */
@media (max-width: 360px) {
.yt-main {
padding: 8px 4px !important;
}
.yt-header-right .yt-icon-btn:not(:first-child) {
display: none;
}
}
/* Landscape mobile */
@media (max-height: 500px) and (orientation: landscape) {
.yt-sidebar {
@@ -803,22 +812,23 @@
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
}
/* Prevent horizontal scroll */
body {
overflow-x: hidden;
max-width: 100vw;
}
/* Better video player on mobile */
@media (max-width: 768px) {
.video-container {
border-radius: 0 !important;
margin: 0 -16px !important;
max-width: calc(100% + 32px) !important;
margin: 0 !important;
max-width: 100% !important;
width: 100% !important;
}
}
/* Sidebar link padding for touch */
@media (hover: none) {
.yt-sidebar-link {
@@ -0,0 +1,401 @@
<!-- Add to Playlist Modal -->
<div id="addToPlaylistModal" class="playlist-modal-overlay" style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.7); z-index: 9999; align-items: center; justify-content: center;">
<div class="playlist-modal-content" style="background: #282828; border-radius: 12px; width: 90%; max-width: 380px; max-height: 85vh; display: flex; flex-direction: column; box-shadow: 0 8px 32px rgba(0,0,0,0.5); overflow: hidden;">
<!-- Header -->
<div class="playlist-modal-header" style="display: flex; justify-content: space-between; align-items: center; padding: 20px 24px; border-bottom: 1px solid #3f3f3f;">
<h2 style="font-size: 18px; font-weight: 600; margin: 0; color: #fff;">Save to playlist</h2>
<button type="button" id="closePlaylistModalBtn" style="background: transparent; border: none; color: #aaa; cursor: pointer; font-size: 24px; padding: 4px; line-height: 1; border-radius: 50%; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; transition: all 0.2s;">
<i class="bi bi-x-lg"></i>
</button>
</div>
<!-- Body -->
<div style="padding: 16px 24px; flex: 1; overflow-y: auto;">
<!-- Create New Playlist Option -->
@auth
<div style="margin-bottom: 16px;">
<button onclick="showCreatePlaylistInModal()" class="create-playlist-btn" style="width: 100%; display: flex; align-items: center; gap: 12px; padding: 12px 16px; background: transparent; border: none; color: #fff; cursor: pointer; border-radius: 8px; transition: background 0.2s; font-size: 14px;">
<span style="width: 36px; height: 36px; background: #3f3f3f; border-radius: 50%; display: flex; align-items: center; justify-content: center;">
<i class="bi bi-plus-lg" style="font-size: 18px;"></i>
</span>
<span style="font-weight: 500;">Create new playlist</span>
</button>
</div>
<!-- Create Playlist Form (Hidden by default) -->
<div id="createPlaylistInModal" style="display: none; margin-bottom: 16px; padding: 16px; background: #1f1f1f; border-radius: 10px; border: 1px solid #3f3f3f;">
<input type="text" id="newPlaylistName" placeholder="Playlist name"
style="width: 100%; padding: 12px 14px; margin-bottom: 12px; border: 1px solid #3f3f3f; border-radius: 8px; background: #121212; color: #fff; font-size: 14px; outline: none; transition: border-color 0.2s;">
<div style="display: flex; gap: 10px; justify-content: flex-end;">
<button onclick="hideCreatePlaylistInModal()" style="padding: 8px 16px; background: #3f3f3f; color: #fff; border: none; border-radius: 18px; cursor: pointer; font-size: 14px; font-weight: 500; transition: background 0.2s;">Cancel</button>
<button onclick="createPlaylistFromModal()" style="padding: 8px 16px; background: #e61e1e; color: #fff; border: none; border-radius: 18px; cursor: pointer; font-size: 14px; font-weight: 500; transition: background 0.2s;">Create</button>
</div>
</div>
@endauth
<!-- Playlist List Container -->
<div id="playlistListContainer" style="flex: 1; overflow-y: auto; min-height: 80px;">
<!-- Playlist items will be loaded here via JavaScript -->
</div>
</div>
<!-- Footer -->
<div style="padding: 16px 24px; border-top: 1px solid #3f3f3f;">
<a href="{{ route('playlists.index') }}" style="display: flex; align-items: center; gap: 10px; color: #fff; text-decoration: none; font-size: 14px; padding: 10px 12px; border-radius: 8px; transition: background 0.2s;">
<i class="bi bi-collection-play" style="font-size: 18px;"></i>
<span style="font-weight: 500;">View all playlists</span>
</a>
</div>
</div>
</div>
<style>
/* Modal Overlay with proper centering */
.playlist-modal-overlay {
backdrop-filter: blur(2px);
}
/* Modal Content */
.playlist-modal-content {
font-family: "Roboto", "Arial", sans-serif;
}
/* Header button hover */
.playlist-modal-header button:hover {
background: #3f3f3f !important;
color: #fff !important;
}
/* Create playlist button hover */
.create-playlist-btn:hover {
background: #3f3f3f !important;
}
/* Input focus */
#newPlaylistName:focus {
border-color: #e61e1e !important;
}
/* Button hover effects */
#createPlaylistInModal button:hover {
opacity: 0.9;
}
#createPlaylistInModal button:last-child:hover {
background: #cc1a1a !important;
}
/* Footer link hover */
.playlist-modal-footer a:hover {
background: #3f3f3f;
}
/* Playlist items */
.playlist-item {
border-radius: 8px;
}
.playlist-item:hover {
background: #3f3f3f !important;
}
/* Scrollbar styling */
#playlistListContainer::-webkit-scrollbar {
width: 8px;
}
#playlistListContainer::-webkit-scrollbar-track {
background: transparent;
}
#playlistListContainer::-webkit-scrollbar-thumb {
background: #555;
border-radius: 4px;
}
#playlistListContainer::-webkit-scrollbar-thumb:hover {
background: #666;
}
</style>
<script>
let currentModalVideoId = null;
// Check if user is authenticated
function isAuthenticated() {
return {{ auth()->check() ? 'true' : 'false' }};
}
// Close button event listener
document.addEventListener('DOMContentLoaded', function() {
const closeBtn = document.getElementById('closePlaylistModalBtn');
if (closeBtn) {
closeBtn.addEventListener('click', function(e) {
e.stopPropagation();
closeAddToPlaylistModal();
});
}
});
function openAddToPlaylistModal(videoId) {
currentModalVideoId = videoId;
// Close any open dropdown menus first
const activeDropdowns = document.querySelectorAll('.dropdown-menu.show');
activeDropdowns.forEach(function(dropdown) {
dropdown.classList.remove('show');
});
// Also close Bootstrap dropdowns
const dropdownToggles = document.querySelectorAll('.dropdown-toggle[aria-expanded="true"]');
dropdownToggles.forEach(function(toggle) {
toggle.click();
});
const modal = document.getElementById('addToPlaylistModal');
modal.style.display = 'flex';
document.getElementById('createPlaylistInModal').style.display = 'none';
// Load playlists when modal opens
loadPlaylistsForModal(videoId);
}
function closeAddToPlaylistModal() {
const modal = document.getElementById('addToPlaylistModal');
modal.style.display = 'none';
currentModalVideoId = null;
}
// Close modal when clicking outside
document.getElementById('addToPlaylistModal').addEventListener('click', function(e) {
if (e.target === this) {
closeAddToPlaylistModal();
}
});
// Close on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
const modal = document.getElementById('addToPlaylistModal');
if (modal && modal.style.display === 'flex') {
closeAddToPlaylistModal();
}
}
});
// Load playlists for modal
function loadPlaylistsForModal(videoId) {
const container = document.getElementById('playlistListContainer');
@auth
// Fetch playlists data
fetch('{{ route("playlists.userPlaylists") }}', {
headers: {
'Accept': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
}
})
.then(response => response.json())
.then(data => {
if (data.success && data.playlists && data.playlists.length > 0) {
let html = '';
data.playlists.forEach(function(playlist) {
const isInPlaylist = playlist.video_ids && playlist.video_ids.includes(parseInt(videoId));
const visibilityText = playlist.visibility === 'public' ? 'Public' : 'Private';
const durationText = playlist.formatted_duration || '0m';
html += `
<div class="playlist-item" style="display: flex; align-items: flex-start; justify-content: space-between; padding: 12px; border-radius: 8px; cursor: pointer; transition: background 0.2s; margin-bottom: 8px;"
onmouseover="this.style.background='#3f3f3f'"
onmouseout="this.style.background='transparent'"
onclick="toggleVideoInPlaylist(${playlist.id}, ${videoId})">
<div style="display: flex; align-items: flex-start; gap: 12px; flex: 1; min-width: 0;">
<div style="width: 100px; height: 56px; background: #1a1a1a; border-radius: 8px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; overflow: hidden; position: relative;">
${playlist.thumbnail_url
? `<img src="${playlist.thumbnail_url}" style="width: 100%; height: 100%; object-fit: cover;">`
: `<i class="bi bi-collection-play" style="font-size: 24px; color: #666;"></i>`
}
${playlist.is_default
? `<span style="position: absolute; top: 2px; left: 2px; background: rgba(230,30,30,0.9); color: white; padding: 1px 4px; border-radius: 2px; font-size: 9px; font-weight: 600;">WL</span>`
: ''
}
</div>
<div style="min-width: 0; flex: 1;">
<div style="font-weight: 500; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #fff;">${playlist.name}</div>
<div style="font-size: 12px; color: #aaa; margin-top: 2px;">
${playlist.video_count || 0} videos • ${durationText}
</div>
<div style="font-size: 11px; color: #777; margin-top: 2px;">
${visibilityText}
</div>
${playlist.description ? `<div style="font-size: 11px; color: #777; margin-top: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${playlist.description}</div>` : ''}
</div>
</div>
<div style="flex-shrink: 0; margin-left: 8px;">
${isInPlaylist
? '<i class="bi bi-check-circle-fill" style="color: #e61e1e; font-size: 20px;"></i>'
: '<i class="bi bi-plus-circle" style="color: #aaa; font-size: 20px;"></i>'}
</div>
</div>
`;
});
container.innerHTML = html;
} else {
container.innerHTML = `
<div style="text-align: center; color: #aaa; padding: 30px 20px;">
<i class="bi bi-collection-play" style="font-size: 36px; margin-bottom: 12px; display: block; color: #555;"></i>
<p style="margin: 0 0 8px; font-size: 14px; color: #fff;">No playlists yet</p>
<p style="margin: 0; font-size: 12px;">Create a playlist to save videos</p>
</div>
`;
}
})
.catch(error => {
console.error('Error loading playlists:', error);
container.innerHTML = `
<div style="text-align: center; color: #aaa; padding: 30px 20px;">
<i class="bi bi-exclamation-circle" style="font-size: 28px; margin-bottom: 12px; display: block; color: #ef4444;"></i>
<p style="margin: 0; font-size: 14px; color: #fff;">Failed to load playlists</p>
</div>
`;
});
@else
container.innerHTML = `
<div style="text-align: center; color: #aaa; padding: 30px 20px;">
<i class="bi bi-person-circle" style="font-size: 40px; margin-bottom: 12px; display: block; color: #555;"></i>
<p style="margin: 0 0 8px; font-size: 14px; color: #fff;">Sign in to save videos to playlists</p>
<p style="margin: 0 0 16px; font-size: 12px;">Create a playlist to organize your videos</p>
<a href="{{ route('login') }}?redirect={{ urlencode(request()->fullUrl()) }}"
style="display: inline-block; padding: 10px 24px; background: #e61e1e; color: white; border-radius: 20px; text-decoration: none; font-weight: 500; font-size: 14px; transition: background 0.2s;">
Sign In
</a>
</div>
`;
@endauth
}
// Toggle video in playlist
function toggleVideoInPlaylist(playlistId, videoId) {
if (!videoId) return;
// Check authentication before adding
if (!isAuthenticated()) {
window.location.href = '{{ route("login") }}?redirect=' + encodeURIComponent(window.location.href);
return;
}
fetch(`/playlists/${playlistId}/videos`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
},
body: JSON.stringify({ video_id: videoId })
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast(data.message || 'Video added to playlist');
// Reload playlists to update checkmarks
loadPlaylistsForModal(videoId);
}
})
.catch(error => console.error('Error:', error));
}
// Show create playlist form in modal
function showCreatePlaylistInModal() {
// Check authentication before creating playlist
if (!isAuthenticated()) {
window.location.href = '{{ route("login") }}?redirect=' + encodeURIComponent(window.location.href);
return;
}
document.getElementById('createPlaylistInModal').style.display = 'block';
document.getElementById('newPlaylistName').focus();
}
// Hide create playlist form
function hideCreatePlaylistInModal() {
document.getElementById('createPlaylistInModal').style.display = 'none';
document.getElementById('newPlaylistName').value = '';
}
// Create playlist from modal
function createPlaylistFromModal() {
const name = document.getElementById('newPlaylistName').value.trim();
if (!name) {
alert('Please enter a playlist name');
return;
}
fetch('{{ route("playlists.store") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
},
body: JSON.stringify({
name: name,
visibility: 'private'
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
hideCreatePlaylistInModal();
showToast('Playlist created!');
// Reload playlists to show new playlist
loadPlaylistsForModal(currentModalVideoId);
}
})
.catch(error => console.error('Error:', error));
}
// Simple toast notification
function showToast(message) {
// Remove existing toast if any
const existing = document.querySelector('.playlist-toast');
if (existing) existing.remove();
const toast = document.createElement('div');
toast.className = 'playlist-toast';
toast.style.cssText = `
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
background: #fff;
color: #0f0f0f;
padding: 14px 28px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
z-index: 10000;
animation: fadeInUp 0.3s ease;
box-shadow: 0 4px 20px rgba(0,0,0,0.4);
max-width: 90%;
text-align: center;
`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'fadeOutDown 0.3s ease';
setTimeout(() => toast.remove(), 300);
}, 2500);
}
</script>
<style>
@keyframes fadeInUp {
from { opacity: 0; transform: translateX(-50%) translateY(20px); }
to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
@keyframes fadeOutDown {
from { opacity: 1; transform: translateX(-50%) translateY(0); }
to { opacity: 0; transform: translateX(-50%) translateY(20px); }
}
</style>
@@ -4,7 +4,7 @@
<button class="yt-menu-btn" onclick="toggleSidebar()">
<i class="bi bi-list fs-5"></i>
</button>
<a href="/videos" class="yt-logo">
<a href="{{ route('home') }}" class="yt-logo">
<!-- 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) -->
@@ -1,14 +1,18 @@
<!-- Sidebar -->
<nav class="yt-sidebar" id="sidebar">
<div class="yt-sidebar-section">
<a href="/videos" class="yt-sidebar-link {{ request()->is('/') || request()->is('videos') ? 'active' : '' }}">
<a href="{{ route('home') }}" class="yt-sidebar-link {{ request()->is('/') || request()->is('videos') ? 'active' : '' }}">
<i class="bi bi-house-door-fill"></i>
<span>Home</span>
</a>
<a href="#" class="yt-sidebar-link">
<a href="{{ route('videos.shorts') }}" class="yt-sidebar-link {{ request()->is('shorts') ? 'active' : '' }}">
<i class="bi bi-play-btn"></i>
<span>Shorts</span>
</a>
<a href="{{ route('videos.trending') }}" class="yt-sidebar-link {{ request()->is('trending') ? 'active' : '' }}">
<i class="bi bi-fire"></i>
<span>Trending</span>
</a>
@auth
<a href="#" class="yt-sidebar-link">
<i class="bi bi-collection-play"></i>
@@ -16,13 +20,17 @@
</a>
@endauth
</div>
@auth
<div class="yt-sidebar-section">
<a href="{{ route('channel', Auth::user()->id) }}" class="yt-sidebar-link">
<i class="bi bi-person-video"></i>
<span>Your Channel</span>
</a>
<a href="{{ route('playlists.index') }}" class="yt-sidebar-link {{ request()->is('playlists*') ? 'active' : '' }}">
<i class="bi bi-collection-play"></i>
<span>Playlists</span>
</a>
<a href="{{ route('history') }}" class="yt-sidebar-link">
<i class="bi bi-clock-history"></i>
<span>History</span>
File diff suppressed because it is too large. Load diff
+126 -14
View File
@@ -4,6 +4,10 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', config('app.name'))</title>
<!-- Open Graph meta tags default - will be overridden by page-specific tags -->
@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') }}">
@@ -18,53 +22,161 @@
--text-primary: #f1f1f1;
--text-secondary: #aaaaaa;
}
* { box-sizing: border-box; }
body {
background-color: var(--bg-dark);
color: var(--text-primary);
font-family: "Roboto", "Arial", sans-serif;
body {
background-color: var(--bg-dark);
color: var(--text-primary);
font-family: "Roboto", "Arial", sans-serif;
margin: 0;
padding: 0;
min-height: 100vh;
}
/* Header */
.yt-header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 56px;
background: var(--bg-dark);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
z-index: 1000;
border-bottom: 1px solid var(--border-color);
}
.yt-header-left {
display: flex;
align-items: center;
gap: 16px;
}
.yt-menu-btn {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.2s;
background: transparent;
border: none;
color: var(--text-primary);
}
.yt-menu-btn:hover { background: var(--border-color); }
.yt-logo {
display: flex;
align-items: center;
text-decoration: none;
gap: 4px;
}
.yt-logo-text {
font-size: 1.4rem;
font-weight: 700;
color: var(--text-primary);
letter-spacing: -1px;
}
/* Search - Hidden on plain layout */
.yt-header-center { display: none; }
/* Header Right */
.yt-header-right {
display: flex;
align-items: center;
gap: 8px;
}
.yt-icon-btn {
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;
}
.yt-icon-btn:hover { background: var(--border-color); }
.yt-user-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: #555;
cursor: pointer;
}
/* Main Content */
.plain-main {
width: 100%;
padding: 20px;
padding-top: 76px; /* 56px header + 20px */
display: flex;
align-items: center;
justify-content: center;
min-height: calc(100vh);
}
/* Upload Button */
.yt-upload-btn {
background: var(--brand-red);
color: white;
border: none;
padding: 8px 16px;
border-radius: 20px;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
text-decoration: none;
}
.yt-upload-btn:hover { background: #cc1a1a; }
/* Dropdown */
.dropdown-menu-dark {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
}
.dropdown-item {
color: var(--text-primary);
}
.dropdown-item:hover {
background: var(--border-color);
color: var(--text-primary);
}
</style>
@yield('extra_styles')
</head>
<body>
<!-- Main Content - No header or sidebar -->
<!-- Header -->
@include('layouts.partials.header')
<!-- Main Content -->
<main class="plain-main">
@yield('content')
</main>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
@yield('scripts')
</body>
</html>
+522
View File
@@ -0,0 +1,522 @@
@extends('layouts.app')
@section('title', 'My Playlists | ' . config('app.name'))
@section('extra_styles')
<style>
.playlist-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.playlist-card {
display: flex;
gap: 16px;
padding: 12px;
border-radius: 12px;
background: var(--bg-secondary);
transition: background 0.2s;
text-decoration: none;
color: inherit;
}
.playlist-card:hover {
background: var(--border-color);
}
.playlist-thumbnail {
width: 160px;
height: 90px;
border-radius: 8px;
overflow: hidden;
background: #333;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.playlist-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
}
.playlist-thumbnail .placeholder {
font-size: 32px;
color: var(--text-secondary);
}
.playlist-info {
flex: 1;
min-width: 0;
}
.playlist-name {
font-size: 16px;
font-weight: 500;
margin-bottom: 4px;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
.playlist-meta {
font-size: 14px;
color: var(--text-secondary);
}
.playlist-description {
font-size: 13px;
color: var(--text-secondary);
margin-top: 4px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.playlist-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 16px;
}
.create-playlist-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 24px;
background: var(--brand-red);
color: white;
border: none;
border-radius: 20px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.create-playlist-btn:hover {
background: #cc1a1a;
}
.empty-state {
text-align: center;
padding: 60px 20px;
}
.empty-icon {
font-size: 64px;
color: var(--text-secondary);
margin-bottom: 16px;
}
.empty-title {
font-size: 20px;
margin-bottom: 8px;
}
.empty-text {
color: var(--text-secondary);
margin-bottom: 20px;
}
@media (max-width: 576px) {
.playlist-header {
flex-direction: column;
gap: 16px;
align-items: flex-start;
}
.playlist-grid {
grid-template-columns: 1fr;
}
.playlist-card {
flex-direction: column;
}
.playlist-thumbnail {
width: 100%;
height: 120px;
}
}
</style>
@endsection
@section('content')
<div class="playlist-header">
<h1 style="font-size: 24px; font-weight: 500;">My Playlists</h1>
<button class="create-playlist-btn" onclick="openCreatePlaylistModal()">
<i class="bi bi-plus-lg"></i> New Playlist
</button>
</div>
@if($playlists->isEmpty())
<div class="empty-state">
<i class="bi bi-collection-play empty-icon"></i>
<h2 class="empty-title">No playlists yet</h2>
<p class="empty-text">Create your first playlist to organize your favorite videos.</p>
<button class="create-playlist-btn" onclick="openCreatePlaylistModal()">
<i class="bi bi-plus-lg"></i> Create Playlist
</button>
</div>
@else
<div class="playlist-grid">
@foreach($playlists as $playlist)
<a href="{{ route('playlists.show', $playlist->id) }}" class="playlist-card">
<div class="playlist-thumbnail">
@if($playlist->thumbnail_url)
<img src="{{ $playlist->thumbnail_url }}" alt="{{ $playlist->name }}">
@else
<span class="placeholder">
<i class="bi bi-collection-play"></i>
</span>
@endif
</div>
<div class="playlist-info">
<div class="playlist-name">{{ $playlist->name }}</div>
<div class="playlist-meta">
{{ $playlist->video_count }} videos • {{ $playlist->formatted_duration }}
</div>
@if($playlist->description)
<div class="playlist-description">{{ $playlist->description }}</div>
@endif
</div>
</a>
@endforeach
</div>
@endif
<!-- Create Playlist Modal - Modern YouTube Style -->
<div id="createPlaylistModal" class="playlist-create-modal-overlay" style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.7); z-index: 9999; align-items: center; justify-content: center; backdrop-filter: blur(2px);">
<div class="playlist-create-modal-content" style="background: #282828; border-radius: 12px; width: 90%; max-width: 480px; box-shadow: 0 8px 32px rgba(0,0,0,0.5); overflow: hidden; animation: modalSlideIn 0.2s ease;">
<!-- Header -->
<div class="playlist-create-modal-header" style="display: flex; justify-content: space-between; align-items: center; padding: 20px 24px; border-bottom: 1px solid #3f3f3f;">
<h2 style="font-size: 18px; font-weight: 600; margin: 0; color: #fff;">Create new playlist</h2>
<button type="button" id="closeCreatePlaylistModalBtn" style="background: transparent; border: none; color: #aaa; cursor: pointer; font-size: 24px; padding: 4px; line-height: 1; border-radius: 50%; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; transition: all 0.2s;">
<i class="bi bi-x-lg"></i>
</button>
</div>
<!-- Body -->
<div style="padding: 24px;">
<form id="createPlaylistForm" enctype="multipart/form-data">
@csrf
<!-- Thumbnail Upload -->
<div class="form-group" style="margin-bottom: 20px;">
<label style="display: block; margin-bottom: 8px; font-weight: 500; font-size: 14px; color: #fff;">Thumbnail</label>
<div id="playlist-thumbnail-dropzone" class="playlist-thumbnail-dropzone" style="border: 2px dashed #3f3f3f; border-radius: 12px; padding: 20px; text-align: center; cursor: pointer; transition: all 0.2s; background: #1a1a1a;"
onmouseover="this.style.borderColor='#e61e1e'; this.style.background='rgba(230,30,30,0.05)'"
onmouseout="this.style.borderColor='#3f3f3f'; this.style.background='#1a1a1a'">
<input type="file" name="thumbnail" id="playlist-thumbnail-input" accept="image/*" style="display: none;">
<div id="playlist-thumbnail-default">
<div style="font-size: 36px; color: #666; margin-bottom: 8px;">
<i class="bi bi-card-image"></i>
</div>
<p style="color: #aaa; font-size: 13px; margin: 0 0 4px;">Click to upload thumbnail</p>
<p style="color: #555; font-size: 11px; margin: 0;">JPG, PNG, GIF, WebP (max 5MB)</p>
</div>
<div id="playlist-thumbnail-preview" class="playlist-thumbnail-preview" style="display: none;">
<img id="playlist-thumbnail-img" src="" alt="Thumbnail preview" style="max-width: 100%; max-height: 160px; border-radius: 8px; object-fit: cover;">
<button type="button" onclick="removePlaylistThumbnail(event)" style="position: absolute; top: -8px; right: -8px; width: 24px; height: 24px; background: #e61e1e; color: white; border: none; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 14px;">
<i class="bi bi-x"></i>
</button>
</div>
</div>
</div>
<!-- Playlist Name -->
<div class="form-group" style="margin-bottom: 20px;">
<label style="display: block; margin-bottom: 8px; font-weight: 500; font-size: 14px; color: #fff;">Name *</label>
<input type="text" name="name" required
placeholder="Enter playlist name"
style="width: 100%; padding: 12px 14px; border: 1px solid #3f3f3f; border-radius: 8px; background: #121212; color: #fff; font-size: 14px; outline: none; transition: border-color 0.2s; box-sizing: border-box;"
onfocus="this.style.borderColor = '#e61e1e';"
onblur="this.style.borderColor = '#3f3f3f';">
</div>
<!-- Description -->
<div class="form-group" style="margin-bottom: 20px;">
<label style="display: block; margin-bottom: 8px; font-weight: 500; font-size: 14px; color: #fff;">Description</label>
<textarea name="description" rows="3"
placeholder="Add a description (optional)"
style="width: 100%; padding: 12px 14px; border: 1px solid #3f3f3f; border-radius: 8px; background: #121212; color: #fff; font-size: 14px; outline: none; transition: border-color 0.2s; resize: none; box-sizing: border-box;"
onfocus="this.style.borderColor = '#e61e1e';"
onblur="this.style.borderColor = '#3f3f3f';"></textarea>
</div>
<!-- Privacy -->
<div class="form-group" style="margin-bottom: 24px;">
<label style="display: flex; align-items: center; gap: 12px; cursor: pointer; padding: 10px 12px; border-radius: 8px; transition: background 0.2s;"
onmouseover="this.style.background='#3f3f3f'"
onmouseout="this.style.background='transparent'">
<input type="checkbox" name="visibility" value="public"
style="width: 20px; height: 20px; accent-color: #e61e1e; cursor: pointer;">
<div style="flex: 1;">
<span style="color: #fff; font-size: 14px; font-weight: 500;">Make playlist public</span>
<div style="color: #aaa; font-size: 12px; margin-top: 2px;">Anyone can search for and view this playlist</div>
</div>
</label>
</div>
<!-- Actions -->
<div style="display: flex; gap: 12px; justify-content: flex-end;">
<button type="button" onclick="closeCreatePlaylistModal()"
style="padding: 10px 20px; background: #3f3f3f; color: #fff; border: none; border-radius: 20px; cursor: pointer; font-size: 14px; font-weight: 500; transition: background 0.2s;"
onmouseover="this.style.background='#555'"
onmouseout="this.style.background='#3f3f3f'">
Cancel
</button>
<button type="submit"
style="padding: 10px 24px; background: #e61e1e; color: #fff; border: none; border-radius: 20px; cursor: pointer; font-size: 14px; font-weight: 500; transition: background 0.2s;"
onmouseover="this.style.background='#cc1a1a'"
onmouseout="this.style.background='#e61e1e'">
Create
</button>
</div>
</form>
</div>
</div>
</div>
@endsection
<style>
@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(-20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* Header button hover */
#closeCreatePlaylistModalBtn:hover {
background: #3f3f3f !important;
color: #fff !important;
}
</style>
@section('scripts')
<script>
// Thumbnail upload handling
const thumbnailDropzone = document.getElementById('playlist-thumbnail-dropzone');
const thumbnailInput = document.getElementById('playlist-thumbnail-input');
const thumbnailDefault = document.getElementById('playlist-thumbnail-default');
const thumbnailPreview = document.getElementById('playlist-thumbnail-preview');
const thumbnailImg = document.getElementById('playlist-thumbnail-img');
// Click to upload
thumbnailDropzone.addEventListener('click', function(e) {
if (e.target.closest('button')) return;
thumbnailInput.click();
});
// File input change
thumbnailInput.addEventListener('change', function() {
handleThumbnailSelect(this);
});
// Drag and drop
thumbnailDropzone.addEventListener('dragover', function(e) {
e.preventDefault();
this.style.borderColor = '#e61e1e';
this.style.background = 'rgba(230,30,30,0.1)';
});
thumbnailDropzone.addEventListener('dragleave', function(e) {
e.preventDefault();
this.style.borderColor = '#3f3f3f';
this.style.background = '#1a1a1a';
});
thumbnailDropzone.addEventListener('drop', function(e) {
e.preventDefault();
this.style.borderColor = '#3f3f3f';
this.style.background = '#1a1a1a';
if (e.dataTransfer.files.length) {
thumbnailInput.files = e.dataTransfer.files;
handleThumbnailSelect(thumbnailInput);
}
});
function handleThumbnailSelect(input) {
if (input.files && input.files[0]) {
const file = input.files[0];
const maxSize = 5 * 1024 * 1024; // 5MB
// Validate file type
const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
const validExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
let isValidType = validTypes.includes(file.type);
let fileExtension = '.' + file.name.split('.').pop().toLowerCase();
if (!isValidType) {
isValidType = validExtensions.includes(fileExtension);
}
if (!isValidType) {
showPlaylistToast('Invalid image format. Please select JPG, PNG, GIF, or WebP.', 'error');
return;
}
if (file.size > maxSize) {
showPlaylistToast('File size exceeds 5MB limit. Please select a smaller image.', 'error');
return;
}
// Show preview
const reader = new FileReader();
reader.onload = function(e) {
thumbnailImg.src = e.target.result;
thumbnailDefault.style.display = 'none';
thumbnailPreview.style.display = 'block';
thumbnailPreview.style.position = 'relative';
};
reader.readAsDataURL(file);
}
}
function removePlaylistThumbnail(e) {
e.preventDefault();
e.stopPropagation();
thumbnailInput.value = '';
thumbnailDefault.style.display = 'block';
thumbnailPreview.style.display = 'none';
thumbnailImg.src = '';
}
function openCreatePlaylistModal() {
document.getElementById('createPlaylistModal').style.display = 'flex';
}
function closeCreatePlaylistModal() {
document.getElementById('createPlaylistModal').style.display = 'none';
document.getElementById('createPlaylistForm').reset();
// Reset thumbnail
removePlaylistThumbnail({ preventDefault: function(){}, stopPropagation: function(){} });
}
// Close button event listener
document.addEventListener('DOMContentLoaded', function() {
const closeBtn = document.getElementById('closeCreatePlaylistModalBtn');
if (closeBtn) {
closeBtn.addEventListener('click', function(e) {
e.stopPropagation();
closeCreatePlaylistModal();
});
}
});
// Close modal when clicking outside
document.getElementById('createPlaylistModal').addEventListener('click', function(e) {
if (e.target === this) {
closeCreatePlaylistModal();
}
});
// Close on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
const modal = document.getElementById('createPlaylistModal');
if (modal && modal.style.display === 'flex') {
closeCreatePlaylistModal();
}
}
});
// Create playlist form submission
document.getElementById('createPlaylistForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
fetch('{{ route("playlists.store") }}', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
},
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
closeCreatePlaylistModal();
window.location.href = '{{ route("playlists.show", "") }}/' + data.playlist.id;
}
})
.catch(error => {
console.error('Error:', error);
showPlaylistToast('Failed to create playlist. Please try again.', 'error');
});
});
// Toast notification function
function showPlaylistToast(message, type = 'success') {
// Remove existing toast if any
const existing = document.querySelector('.playlist-create-toast');
if (existing) existing.remove();
const toast = document.createElement('div');
toast.className = 'playlist-create-toast';
const bgColor = type === 'error' ? 'rgba(239, 68, 68, 0.9)' : '#282828';
const borderColor = type === 'error' ? '#ef4444' : '#4ade80';
const icon = type === 'error' ? 'bi-exclamation-circle-fill' : 'bi-check-circle-fill';
const iconColor = type === 'error' ? '#ef4444' : '#4ade80';
toast.style.cssText = `
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
background: ${bgColor};
color: #fff;
padding: 14px 24px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
z-index: 10000;
animation: toastSlideUp 0.3s ease;
box-shadow: 0 4px 20px rgba(0,0,0,0.4);
max-width: 90%;
text-align: center;
border-left: 4px solid ${borderColor};
display: flex;
align-items: center;
gap: 10px;
`;
toast.innerHTML = `
<i class="bi ${icon}" style="color: ${iconColor}; font-size: 18px;"></i>
<span>${message}</span>
`;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'toastSlideDown 0.3s ease';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
</script>
@endsection
<style>
@keyframes toastSlideUp {
from { opacity: 0; transform: translateX(-50%) translateY(20px); }
to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
@keyframes toastSlideDown {
from { opacity: 1; transform: translateX(-50%) translateY(0); }
to { opacity: 0; transform: translateX(-50%) translateY(20px); }
}
</style>
+838
View File
@@ -0,0 +1,838 @@
@extends('layouts.app')
@section('title', $playlist->name . ' | ' . config('app.name'))
@section('extra_styles')
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/Sortable.min.css">
<style>
.playlist-header {
display: flex;
gap: 24px;
margin-bottom: 24px;
padding: 24px;
background: var(--bg-secondary);
border-radius: 12px;
}
.playlist-thumbnail {
width: 240px;
height: 135px;
border-radius: 12px;
overflow: hidden;
background: #333;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.playlist-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
}
.playlist-thumbnail .placeholder {
font-size: 48px;
color: var(--text-secondary);
}
.playlist-info {
flex: 1;
}
.playlist-title {
font-size: 24px;
font-weight: 500;
margin-bottom: 8px;
}
.playlist-meta {
color: var(--text-secondary);
font-size: 14px;
margin-bottom: 12px;
}
.playlist-description {
color: var(--text-secondary);
font-size: 14px;
margin-bottom: 16px;
line-height: 1.5;
}
.playlist-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.yt-play-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 24px;
background: var(--brand-red);
color: white;
border: none;
border-radius: 20px;
font-weight: 600;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
text-decoration: none;
}
.yt-play-btn:hover {
background: #cc1a1a;
}
.yt-shuffle-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 24px;
background: var(--bg-primary);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 20px;
font-weight: 600;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
text-decoration: none;
}
.yt-shuffle-btn:hover {
background: var(--border-color);
}
.playlist-video-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.playlist-video-item {
display: flex;
align-items: center;
gap: 16px;
padding: 12px;
border-radius: 8px;
background: var(--bg-secondary);
transition: background 0.2s;
}
.playlist-video-item:hover {
background: var(--border-color);
}
.video-position {
width: 32px;
text-align: center;
color: var(--text-secondary);
font-size: 16px;
font-weight: 500;
}
.video-position.with-drag-handle {
margin-left: 8px;
}
.video-thumb {
width: 160px;
height: 90px;
border-radius: 8px;
overflow: hidden;
background: #333;
flex-shrink: 0;
position: relative;
cursor: pointer;
}
.video-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
}
.video-duration {
position: absolute;
bottom: 4px;
right: 4px;
background: rgba(0,0,0,0.8);
color: white;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.video-details {
flex: 1;
min-width: 0;
}
.video-title {
font-size: 15px;
font-weight: 500;
margin-bottom: 4px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.video-title a {
color: inherit;
text-decoration: none;
}
.video-meta {
font-size: 13px;
color: var(--text-secondary);
}
.video-channel {
color: var(--text-secondary);
text-decoration: none;
}
.video-channel:hover {
color: var(--text-primary);
}
.video-actions {
display: flex;
gap: 8px;
}
.remove-btn {
background: transparent;
border: none;
color: var(--text-secondary);
cursor: pointer;
padding: 8px;
border-radius: 50%;
transition: background 0.2s, color 0.2s;
}
.remove-btn:hover {
background: var(--brand-red);
color: white;
}
.drag-handle {
cursor: grab;
color: var(--text-secondary);
padding: 8px;
display: flex;
align-items: center;
}
.drag-handle:active {
cursor: grabbing;
}
.drag-handle i {
font-size: 16px;
}
/* Drag and drop styles */
.playlist-video-list.sortable-ghost {
opacity: 0.4;
background: var(--border-color);
}
.playlist-video-list.sortable-drag {
background: var(--bg-secondary);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.playlist-video-item.sortable-chosen {
background: var(--border-color);
}
.playlist-video-item.dragging {
cursor: grabbing !important;
}
.drag-overlay {
display: none;
}
.empty-state {
text-align: center;
padding: 60px 20px;
}
.empty-icon {
font-size: 64px;
color: var(--text-secondary);
margin-bottom: 16px;
}
.empty-title {
font-size: 20px;
margin-bottom: 8px;
}
.empty-text {
color: var(--text-secondary);
}
@media (max-width: 768px) {
.playlist-header {
flex-direction: column;
}
.playlist-thumbnail {
width: 100%;
height: 180px;
}
.video-thumb {
width: 120px;
height: 68px;
}
.playlist-video-item {
flex-wrap: wrap;
}
.video-position {
display: none;
}
}
</style>
@endsection
@section('content')
<div class="playlist-header">
<div class="playlist-thumbnail">
@if($playlist->thumbnail_url)
<img src="{{ $playlist->thumbnail_url }}" alt="{{ $playlist->name }}">
@else
<span class="placeholder">
<i class="bi bi-collection-play"></i>
</span>
@endif
</div>
<div class="playlist-info">
<h1 class="playlist-title">{{ $playlist->name }}</h1>
<div class="playlist-meta">
{{ $playlist->video_count }} videos • {{ $playlist->formatted_duration }} •
{{ $playlist->visibility === 'public' ? 'Public' : 'Private' }}
@if($playlist->is_default)
<span style="background: var(--brand-red); color: white; padding: 2px 8px; border-radius: 4px; font-size: 12px; margin-left: 8px;">Watch Later</span>
@endif
</div>
@if($playlist->description)
<div class="playlist-description">{{ $playlist->description }}</div>
@endif
<div class="playlist-actions">
@if($playlist->video_count > 0)
<a href="{{ route('playlists.playAll', $playlist->id) }}" class="yt-play-btn">
<i class="bi bi-play-fill"></i> Play All
</a>
<a href="{{ route('playlists.shuffle', $playlist->id) }}" class="yt-shuffle-btn">
<i class="bi bi-shuffle"></i> Shuffle
</a>
@endif
@if($playlist->canEdit(Auth::user()))
<button class="yt-shuffle-btn" onclick="openEditPlaylistModal()">
<i class="bi bi-pencil"></i> Edit
</button>
@endif
</div>
</div>
</div>
@if($videos->isEmpty())
<div class="empty-state">
<i class="bi bi-collection-play empty-icon"></i>
<h2 class="empty-title">No videos in this playlist</h2>
<p class="empty-text">Add videos to this playlist to watch them here.</p>
</div>
@else
<div class="playlist-video-list" id="playlistVideoList" data-current-page="{{ $videos->currentPage() }}" data-per-page="{{ $videos->perPage() }}">
@foreach($videos as $index => $video)
<div class="playlist-video-item{{ $playlist->canEdit(Auth::user()) ? ' has-drag-handle' : '' }}" data-video-id="{{ $video->id }}" data-global-position="{{ $index + ($videos->currentPage() - 1) * $videos->perPage() }}">
@if($playlist->canEdit(Auth::user()))
<div class="drag-handle" title="Drag to reorder">
<i class="bi bi-grip-vertical"></i>
</div>
@endif
<div class="video-position{{ !$playlist->canEdit(Auth::user()) ? '' : ' with-drag-handle' }}">{{ $index + 1 + ($videos->currentPage() - 1) * $videos->perPage() }}</div>
<a href="{{ route('videos.show', $video->id) }}?playlist={{ $playlist->id }}" class="video-thumb">
<img src="{{ $video->thumbnail_url }}" alt="{{ $video->title }}">
<span class="video-duration">{{ $video->formatted_duration }}</span>
</a>
<div class="video-details">
<div class="video-title">
<a href="{{ route('videos.show', $video->id) }}?playlist={{ $playlist->id }}">{{ $video->title }}</a>
</div>
<div class="video-meta">
<a href="{{ route('channel', $video->user_id) }}" class="video-channel">{{ $video->user->name }}</a>
• {{ $video->view_count }} views
• {{ $video->created_at->diffForHumans() }}
</div>
</div>
@if($playlist->canEdit(Auth::user()))
<div class="video-actions">
<form action="{{ route('playlists.removeVideo', [$playlist->id, $video->id]) }}" method="POST">
@csrf
@method('DELETE')
<button type="submit" class="remove-btn" title="Remove from playlist">
<i class="bi bi-x-lg"></i>
</button>
</form>
</div>
@endif
</div>
@endforeach
</div>
<div style="margin-top: 24px;">
{{ $videos->links() }}
</div>
@endif
<!-- SortableJS Library -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/Sortable.min.js"></script>
<!-- Edit Playlist Modal -->
@if($playlist->canEdit(Auth::user()))
<div id="editPlaylistModal" class="edit-playlist-modal-overlay" style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.7); z-index: 2000; align-items: center; justify-content: center;">
<div class="edit-playlist-modal-content" style="background: #282828; border-radius: 16px; width: 90%; max-width: 480px; display: flex; flex-direction: column; box-shadow: 0 12px 48px rgba(0,0,0,0.6); overflow: hidden; animation: modalSlideIn 0.3s ease;">
<!-- Header -->
<div class="edit-playlist-modal-header" style="display: flex; justify-content: space-between; align-items: center; padding: 20px 24px; border-bottom: 1px solid #3f3f3f;">
<h2 style="font-size: 20px; font-weight: 600; margin: 0; color: #fff;">Edit Details</h2>
<button type="button" onclick="closeEditPlaylistModal()" style="background: transparent; border: none; color: #aaa; cursor: pointer; font-size: 24px; padding: 4px; line-height: 1; border-radius: 50%; width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; transition: all 0.2s;">
<i class="bi bi-x-lg"></i>
</button>
</div>
<!-- Body -->
<div style="padding: 24px; flex: 1; overflow-y: auto;">
<!-- Thumbnail Upload -->
<div style="margin-bottom: 24px;">
<label style="display: block; margin-bottom: 12px; font-weight: 500; color: #fff; font-size: 14px;">Thumbnail</label>
<div id="playlistThumbnailContainer" style="position: relative; width: 100%; aspect-ratio: 16/9; max-width: 240px; border-radius: 12px; overflow: hidden; background: #1a1a1a; cursor: pointer; transition: all 0.2s; margin: 0 auto;" onclick="document.getElementById('playlistThumbnailInput').click()">
@if($playlist->thumbnail_url && strpos($playlist->thumbnail_url, 'ui-avatars.com') === false)
<img id="playlistThumbnailPreview" src="{{ $playlist->thumbnail_url }}" style="width: 100%; height: 100%; object-fit: cover;">
@else
<img id="playlistThumbnailPreview" src="" style="width: 100%; height: 100%; object-fit: cover; display: none;">
@endif
<div id="playlistThumbnailPlaceholder" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; background: #1a1a1a; @if($playlist->thumbnail_url && strpos($playlist->thumbnail_url, 'ui-avatars.com') === false)display: none;@endif">
<i class="bi bi-image" style="font-size: 32px; color: #555; margin-bottom: 8px;"></i>
<span style="font-size: 12px; color: #777;">Upload thumbnail</span>
</div>
<div style="position: absolute; bottom: 8px; right: 8px; background: rgba(0,0,0,0.7); padding: 6px 10px; border-radius: 6px; display: flex; align-items: center; gap: 4px;">
<i class="bi bi-camera" style="color: white; font-size: 14px;"></i>
<span style="color: white; font-size: 12px;">Change</span>
</div>
</div>
<input type="file" id="playlistThumbnailInput" name="thumbnail" accept="image/*" style="display: none;" onchange="handlePlaylistThumbnailUpload(this)">
@if($playlist->thumbnail_url && strpos($playlist->thumbnail_url, 'ui-avatars.com') === false)
<button type="button" onclick="removePlaylistThumbnail(event)" style="display: block; margin: 12px auto 0; background: transparent; border: 1px solid #555; color: #aaa; padding: 6px 16px; border-radius: 18px; cursor: pointer; font-size: 12px; transition: all 0.2s;">Remove thumbnail</button>
@endif
</div>
<!-- Name Input -->
<div class="form-group" style="margin-bottom: 20px;">
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: #fff; font-size: 14px;">Name</label>
<input type="text" name="name" value="{{ $playlist->name }}" required
style="width: 100%; padding: 14px 16px; border: 1px solid #3f3f3f; border-radius: 10px; background: #1a1a1a; color: #fff; font-size: 14px; outline: none; transition: all 0.2s;">
</div>
<!-- Description Input -->
<div class="form-group" style="margin-bottom: 20px;">
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: #fff; font-size: 14px;">Description</label>
<textarea name="description" class="form-control" rows="3" placeholder="Tell viewers about your playlist"
style="width: 100%; padding: 14px 16px; border: 1px solid #3f3f3f; border-radius: 10px; background: #1a1a1a; color: #fff; font-size: 14px; outline: none; transition: all 0.2s; resize: none;">{{ $playlist->description }}</textarea>
</div>
<!-- Visibility Toggle -->
<div class="form-group" style="margin-bottom: 20px;">
<label style="display: flex; align-items: center; gap: 12px; cursor: pointer; padding: 12px 16px; background: #1a1a1a; border-radius: 10px; border: 1px solid #3f3f3f; transition: all 0.2s;">
<input type="checkbox" name="visibility" value="public" {{ $playlist->visibility === 'public' ? 'checked' : '' }}
style="width: 20px; height: 20px; accent-color: #e61e1e; cursor: pointer;">
<div style="flex: 1;">
<span style="color: #fff; font-size: 14px; font-weight: 500;">Make playlist public</span>
<p style="margin: 2px 0 0; font-size: 12px; color: #777;">Anyone can search for and view this playlist</p>
</div>
</label>
</div>
</div>
<!-- Footer -->
<div style="padding: 16px 24px; border-top: 1px solid #3f3f3f; display: flex; gap: 12px; justify-content: space-between;">
@if(!$playlist->is_default)
<button type="button" onclick="deletePlaylist()" style="background: transparent; border: 1px solid #e61e1e; color: #e61e1e; padding: 12px 20px; border-radius: 20px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s;">
<i class="bi bi-trash" style="margin-right: 6px;"></i>Delete
</button>
@else
<div></div>
@endif
<div style="display: flex; gap: 12px;">
<button type="button" onclick="closeEditPlaylistModal()" style="background: #3f3f3f; border: none; color: #fff; padding: 12px 20px; border-radius: 20px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s;">Cancel</button>
<button type="submit" form="editPlaylistForm" style="background: #e61e1e; border: none; color: #fff; padding: 12px 24px; border-radius: 20px; cursor: pointer; font-size: 14px; font-weight: 600; transition: all 0.2s;">
<i class="bi bi-check-lg" style="margin-right: 4px;"></i>Save
</button>
</div>
</div>
</div>
</div>
<form id="editPlaylistForm" method="POST" action="{{ route('playlists.update', $playlist->id) }}" enctype="multipart/form-data" style="display: none;">
@csrf
@method('PUT')
<input type="hidden" name="name" value="{{ $playlist->name }}">
<input type="hidden" name="description" value="{{ $playlist->description }}">
<input type="hidden" name="visibility" value="{{ $playlist->visibility }}">
<input type="file" name="thumbnail" id="editPlaylistThumbnailFile" accept="image/*">
</form>
<script>
// Open Edit Playlist Modal
function openEditPlaylistModal() {
const modal = document.getElementById('editPlaylistModal');
modal.style.display = 'flex';
// Sync form values
const nameInput = modal.querySelector('input[name="name"]');
const descInput = modal.querySelector('textarea[name="description"]');
const visInput = modal.querySelector('input[name="visibility"]');
if (nameInput) nameInput.value = '{{ $playlist->name }}';
if (descInput) descInput.value = '{{ $playlist->description }}';
}
// Close Edit Playlist Modal
function closeEditPlaylistModal() {
const modal = document.getElementById('editPlaylistModal');
modal.style.display = 'none';
}
// Handle Thumbnail Upload
function handlePlaylistThumbnailUpload(input) {
if (input.files && input.files[0]) {
const file = input.files[0];
// Validate file type
if (!file.type.startsWith('image/')) {
alert('Please select an image file');
return;
}
// Validate file size (max 5MB)
if (file.size > 5 * 1024 * 1024) {
alert('Image size must be less than 5MB');
return;
}
// Preview image
const reader = new FileReader();
reader.onload = function(e) {
const preview = document.getElementById('playlistThumbnailPreview');
const placeholder = document.getElementById('playlistThumbnailPlaceholder');
preview.src = e.target.result;
preview.style.display = 'block';
if (placeholder) placeholder.style.display = 'none';
};
reader.readAsDataURL(file);
// Copy file to hidden form input
const formFileInput = document.getElementById('editPlaylistThumbnailFile');
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
formFileInput.files = dataTransfer.files;
}
}
// Remove Thumbnail
function removePlaylistThumbnail(event) {
event.stopPropagation();
if (confirm('Are you sure you want to remove the thumbnail?')) {
// Show placeholder
const preview = document.getElementById('playlistThumbnailPreview');
const placeholder = document.getElementById('playlistThumbnailPlaceholder');
preview.src = '';
preview.style.display = 'none';
if (placeholder) placeholder.style.display = 'flex';
// Clear file inputs
document.getElementById('playlistThumbnailInput').value = '';
document.getElementById('editPlaylistThumbnailFile').value = '';
// Add hidden input to indicate thumbnail removal
const form = document.getElementById('editPlaylistForm');
let removeInput = form.querySelector('input[name="remove_thumbnail"]');
if (!removeInput) {
removeInput = document.createElement('input');
removeInput.type = 'hidden';
removeInput.name = 'remove_thumbnail';
removeInput.value = '1';
form.appendChild(removeInput);
}
}
}
// Submit Edit Playlist Form
document.addEventListener('DOMContentLoaded', function() {
const editForm = document.getElementById('editPlaylistForm');
if (editForm) {
editForm.addEventListener('submit', function(e) {
e.preventDefault();
// Sync visible form values to hidden form
const modal = document.getElementById('editPlaylistModal');
const visibleName = modal.querySelector('input[name="name"]');
const visibleDesc = modal.querySelector('textarea[name="description"]');
const visibleVis = modal.querySelector('input[name="visibility"]');
const hiddenName = editForm.querySelector('input[name="name"]');
const hiddenDesc = editForm.querySelector('input[name="description"]');
const hiddenVis = editForm.querySelector('input[name="visibility"]');
if (hiddenName) hiddenName.value = visibleName ? visibleName.value : '';
if (hiddenDesc) hiddenDesc.value = visibleDesc ? visibleDesc.value : '';
if (hiddenVis) hiddenVis.value = visibleVis.checked ? 'public' : 'private';
// Submit the form via AJAX
const formData = new FormData(editForm);
fetch(editForm.action, {
method: 'POST',
body: formData,
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast('Playlist updated!');
closeEditPlaylistModal();
// Reload page to show updated info
window.location.reload();
} else {
alert(data.message || 'Failed to update playlist');
}
})
.catch(error => {
console.error('Error:', error);
alert('Failed to update playlist');
});
});
}
});
// Simple toast notification
function showToast(message) {
const existing = document.querySelector('.edit-playlist-toast');
if (existing) existing.remove();
const toast = document.createElement('div');
toast.className = 'edit-playlist-toast';
toast.style.cssText = `
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
background: #fff;
color: #0f0f0f;
padding: 14px 28px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
z-index: 10001;
animation: fadeInUp 0.3s ease;
box-shadow: 0 4px 20px rgba(0,0,0,0.4);
max-width: 90%;
text-align: center;
`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'fadeOutDown 0.3s ease';
setTimeout(() => toast.remove(), 300);
}, 2500);
}
// Initialize Sortable for drag and drop reordering
document.addEventListener('DOMContentLoaded', function() {
const videoList = document.getElementById('playlistVideoList');
if (videoList && typeof Sortable !== 'undefined') {
new Sortable(videoList, {
handle: '.drag-handle',
animation: 150,
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
dragClass: 'sortable-drag',
easing: 'cubic-bezier(1, 0, 0, 1)',
delay: 0,
forceFallback: false,
onStart: function(evt) {
evt.item.classList.add('dragging');
},
onEnd: function(evt) {
evt.item.classList.remove('dragging');
// Get the new order of video IDs with their global positions
const items = videoList.querySelectorAll('.playlist-video-item');
// Collect video IDs in new order
const videoIds = Array.from(items).map(item => parseInt(item.dataset.videoId));
// Get global starting position from the first item
const firstItem = items[0];
const startPosition = firstItem ? parseInt(firstItem.dataset.globalPosition) : 0;
// Update position numbers visually (relative to current page)
items.forEach((item, index) => {
const positionEl = item.querySelector('.video-position');
if (positionEl) {
positionEl.textContent = index + 1;
}
// Update global position data attribute for future reorders
item.dataset.globalPosition = startPosition + index;
});
// Send AJAX request to save the new order
fetch('{{ route("playlists.reorder", $playlist->id) }}', {
method: 'PUT',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ video_ids: videoIds })
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log('Playlist reordered successfully');
} else {
console.error('Failed to reorder playlist:', data.message);
// Optionally reload to restore original order on error
}
})
.catch(error => {
console.error('Error reordering playlist:', error);
});
}
});
}
});
function deletePlaylist() {
if (confirm('Are you sure you want to delete this playlist?')) {
fetch('{{ route("playlists.destroy", $playlist->id) }}', {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
window.location.href = '{{ route("playlists.index") }}';
}
});
}
}
// Close modal when clicking outside
document.getElementById('editPlaylistModal').addEventListener('click', function(e) {
if (e.target === this) {
closeEditPlaylistModal();
}
});
// Close on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
const modal = document.getElementById('editPlaylistModal');
if (modal && modal.style.display === 'flex') {
closeEditPlaylistModal();
}
}
});
</script>
<style>
/* Toast animations */
@keyframes fadeInUp {
from { opacity: 0; transform: translateX(-50%) translateY(20px); }
to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
@keyframes fadeOutDown {
from { opacity: 1; transform: translateX(-50%) translateY(0); }
to { opacity: 0; transform: translateX(-50%) translateY(20px); }
}
/* Modal animations */
@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Modal overlay */
.edit-playlist-modal-overlay {
backdrop-filter: blur(4px);
}
/* Header button hover */
.edit-playlist-modal-header button:hover {
background: #3f3f3f !important;
color: #fff !important;
}
/* Input focus states */
.edit-playlist-modal-content input:focus,
.edit-playlist-modal-content textarea:focus {
border-color: #e61e1e !important;
box-shadow: 0 0 0 2px rgba(230, 30, 30, 0.2);
}
/* Thumbnail container hover */
#playlistThumbnailContainer:hover {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
}
/* Button hover effects */
.edit-playlist-modal-footer button:hover {
opacity: 0.9;
}
/* Delete button hover */
button[onclick="deletePlaylist()"]:hover {
background: #cc1a1a !important;
color: white !important;
}
/* Save button hover */
button[type="submit"][form="editPlaylistForm"]:hover {
background: #cc1a1a !important;
}
</style>
@endif
@endsection
+129 -94
View File
@@ -10,7 +10,7 @@
padding: 32px;
margin-bottom: 24px;
}
.channel-avatar {
width: 120px;
height: 120px;
@@ -18,44 +18,44 @@
object-fit: cover;
border: 3px solid var(--border-color);
}
.channel-name {
font-size: 24px;
font-weight: 500;
margin: 16px 0 4px;
}
.channel-meta {
color: var(--text-secondary);
font-size: 14px;
}
.channel-stats {
display: flex;
gap: 24px;
margin-top: 16px;
}
.channel-stat-value {
font-weight: 600;
}
/* Video Grid */
.yt-video-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}
.yt-video-card {
cursor: pointer;
transition: transform 0.2s ease;
}
.yt-video-card:active {
transform: scale(0.98);
}
.yt-video-thumb {
position: relative;
aspect-ratio: 16/9;
@@ -63,7 +63,7 @@
overflow: hidden;
background: #1a1a1a;
}
.yt-video-thumb img {
width: 100%;
height: 100%;
@@ -72,7 +72,7 @@
top: 0;
left: 0;
}
.yt-video-thumb video {
width: 100%;
height: 100%;
@@ -84,11 +84,11 @@
transition: opacity 0.3s ease;
background: #000;
}
.yt-video-thumb video.active {
opacity: 1;
}
.yt-video-duration {
position: absolute;
bottom: 8px;
@@ -100,13 +100,13 @@
font-size: 12px;
font-weight: 500;
}
.yt-video-info {
display: flex;
margin-top: 12px;
gap: 12px;
}
.yt-channel-icon {
width: 36px;
height: 36px;
@@ -114,12 +114,12 @@
background: #555;
flex-shrink: 0;
}
.yt-video-details {
flex: 1;
min-width: 0;
}
.yt-video-title {
font-size: 16px;
font-weight: 500;
@@ -131,12 +131,12 @@
overflow: hidden;
line-height: 1.3;
}
.yt-video-title a {
color: inherit;
text-decoration: none;
}
.yt-channel-name, .yt-video-meta {
color: var(--text-secondary);
font-size: 14px;
@@ -144,7 +144,7 @@
overflow: hidden;
text-overflow: ellipsis;
}
/* More button */
.yt-more-btn {
background: transparent;
@@ -160,15 +160,15 @@
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 {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
@@ -176,7 +176,7 @@
padding: 8px 0;
min-width: 200px;
}
.yt-more-dropdown-item {
display: flex;
align-items: center;
@@ -191,30 +191,30 @@
text-align: left;
font-size: 14px;
}
.yt-more-dropdown-item:hover { background: var(--border-color); }
/* Empty State */
.yt-empty {
text-align: center;
padding: 80px 20px;
}
.yt-empty-icon {
font-size: 80px;
color: var(--text-secondary);
}
.yt-empty-title {
font-size: 24px;
margin: 20px 0 8px;
}
.yt-empty-text {
color: var(--text-secondary);
margin-bottom: 20px;
}
.yt-upload-btn {
background: var(--brand-red);
color: white;
@@ -229,15 +229,15 @@
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;
@@ -245,250 +245,250 @@
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: 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%);
@@ -496,18 +496,18 @@
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;
}
@@ -523,11 +523,11 @@
@else
<img src="https://i.pravatar.cc/150?u={{ $user->id }}" alt="{{ $user->name }}" class="channel-avatar">
@endif
<div class="channel-info">
<h1 class="channel-name">{{ $user->name }}</h1>
<p class="channel-meta">Joined {{ $user->created_at->format('F d, Y') }}</p>
<div class="channel-stats">
<div>
<span class="channel-stat-value">{{ $videos->total() }}</span>
@@ -538,7 +538,7 @@
<span class="channel-meta"> views</span>
</div>
</div>
@auth
@if(Auth::user()->id === $user->id)
<div class="channel-actions">
@@ -554,7 +554,42 @@
</div>
</div>
</div>
<!-- Playlists Section - Only show for own channel -->
@auth
@if(Auth::user()->id === $user->id && $playlists && $playlists->count() > 0)
<div class="playlists-section" style="margin-bottom: 24px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<h2 style="font-size: 20px; font-weight: 500;">My Playlists</h2>
<a href="{{ route('playlists.index') }}" style="color: var(--text-secondary); text-decoration: none; font-size: 14px;">
View all <i class="bi bi-arrow-right"></i>
</a>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px;">
@foreach($playlists->take(6) as $playlist)
<a href="{{ route('playlists.show', $playlist->id) }}" class="playlist-card-mini" style="text-decoration: none; color: inherit;">
<div class="playlist-thumb-mini" style="position: relative; aspect-ratio: 16/9; background: var(--bg-secondary); border-radius: 8px; overflow: hidden; margin-bottom: 8px;">
@if($playlist->thumbnail_url)
<img src="{{ $playlist->thumbnail_url }}" alt="{{ $playlist->name }}" style="width: 100%; height: 100%; object-fit: cover;">
@else
<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: var(--text-secondary);">
<i class="bi bi-collection-play" style="font-size: 32px;"></i>
</div>
@endif
<span class="playlist-count" style="position: absolute; bottom: 4px; right: 4px; background: rgba(0,0,0,0.8); color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px; font-weight: 500;">
{{ $playlist->video_count }}
</span>
</div>
<div class="playlist-name-mini" style="font-size: 14px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
{{ $playlist->name }}
</div>
</a>
@endforeach
</div>
</div>
@endif
@endauth
@if($videos->isEmpty())
<div class="yt-empty">
<i class="bi bi-camera-video yt-empty-icon"></i>
@@ -574,10 +609,10 @@
<x-video-card :video="$video" size="small" />
@endforeach
</div>
<div class="mt-4">{{ $videos->links() }}</div>
@endif
@include('layouts.partials.share-modal')
@endsection
@@ -588,7 +623,7 @@ 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;
@@ -628,7 +663,7 @@ document.addEventListener('touchstart', function(e) {
if (currentPlayingVideo && currentPlayingVideo !== card) {
stopVideo(currentPlayingVideo);
}
const video = card.querySelector('video');
if (video) {
if (card.classList.contains('playing')) {
+163 -169
View File
@@ -4,193 +4,187 @@
@section('extra_styles')
<style>
.profile-header {
background: var(--bg-secondary);
border-radius: 12px;
padding: 32px;
margin-bottom: 24px;
}
.profile-avatar {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
margin-bottom: 16px;
}
.profile-name {
font-size: 24px;
font-weight: 500;
margin-bottom: 4px;
}
.profile-email {
color: var(--text-secondary);
margin-bottom: 16px;
}
.profile-stats {
display: flex;
gap: 24px;
margin-top: 16px;
}
.profile-stat {
text-align: center;
}
.profile-stat-value {
font-size: 20px;
font-weight: 600;
}
.profile-stat-label {
font-size: 14px;
color: var(--text-secondary);
}
.form-card {
background: var(--bg-secondary);
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
}
.form-title {
font-size: 18px;
font-weight: 500;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border-color);
}
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
.form-input {
width: 100%;
background: #121212;
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 12px 16px;
color: var(--text-primary);
font-size: 14px;
}
.form-input:focus {
outline: none;
border-color: var(--brand-red);
}
.btn-primary {
background: var(--brand-red);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.btn-primary:hover {
background: #cc1a1a;
}
.alert {
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 16px;
}
.alert-success {
background: rgba(34, 197, 94, 0.2);
border: 1px solid #22c55e;
color: #22c55e;
}
.profile-header { background: var(--bg-secondary); border-radius: 12px; padding: 32px; margin-bottom: 24px; position: relative; overflow: hidden; }
.profile-banner { position: absolute; top: 0; left: 0; right: 0; height: 120px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); z-index: 0; }
.profile-content { position: relative; z-index: 1; margin-top: 60px; }
.profile-avatar { width: 120px; height: 120px; border-radius: 50%; object-fit: cover; border: 4px solid var(--bg-secondary); margin-bottom: 16px; }
.profile-name { font-size: 28px; font-weight: 600; margin-bottom: 4px; }
.profile-username { color: var(--text-secondary); margin-bottom: 12px; font-size: 14px; }
.profile-bio { color: var(--text-primary); margin-bottom: 16px; max-width: 600px; line-height: 1.6; }
.profile-meta { display: flex; flex-wrap: wrap; gap: 16px; margin-bottom: 20px; color: var(--text-secondary); font-size: 14px; }
.profile-meta-item { display: flex; align-items: center; gap: 6px; }
.profile-stats { display: flex; gap: 32px; margin-top: 16px; padding-top: 20px; border-top: 1px solid var(--border-color); }
.profile-stat { text-align: center; }
.profile-stat-value { font-size: 22px; font-weight: 700; }
.profile-stat-label { font-size: 13px; color: var(--text-secondary); }
.social-links { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 16px; }
.social-link { display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; border-radius: 50%; background: var(--bg-primary); color: var(--text-primary); text-decoration: none; transition: all 0.2s; font-size: 18px; }
.social-link:hover { transform: translateY(-2px); background: var(--brand-red); color: white; }
.social-link.twitter:hover { background: #1da1f2; }
.social-link.instagram:hover { background: #e4405f; }
.social-link.facebook:hover { background: #1877f2; }
.social-link.youtube:hover { background: #ff0000; }
.social-link.linkedin:hover { background: #0077b5; }
.social-link.tiktok:hover { background: #000000; }
.form-card { background: var(--bg-secondary); border-radius: 12px; padding: 24px; margin-bottom: 24px; }
.form-title { font-size: 18px; font-weight: 600; margin-bottom: 20px; padding-bottom: 12px; border-bottom: 1px solid var(--border-color); display: flex; align-items: center; gap: 8px; }
.form-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; }
.form-group { margin-bottom: 16px; }
.form-label { display: block; margin-bottom: 8px; font-weight: 500; font-size: 14px; }
.form-input, .form-textarea { width: 100%; background: #121212; border: 1px solid var(--border-color); border-radius: 8px; padding: 12px 16px; color: var(--text-primary); font-size: 14px; transition: border-color 0.2s; }
.form-input:focus, .form-textarea:focus { outline: none; border-color: var(--brand-red); }
.form-textarea { min-height: 100px; resize: vertical; }
.form-hint { font-size: 12px; color: var(--text-secondary); margin-top: 4px; }
.btn-primary { background: var(--brand-red); color: white; border: none; padding: 12px 32px; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; transition: background 0.2s; display: inline-flex; align-items: center; gap: 8px; }
.btn-primary:hover { background: #cc1a1a; }
.btn-secondary { background: transparent; color: var(--text-primary); border: 1px solid var(--border-color); padding: 12px 24px; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; display: inline-flex; align-items: center; gap: 8px; text-decoration: none; }
.btn-secondary:hover { background: var(--bg-secondary); border-color: var(--text-secondary); }
.alert { padding: 12px 16px; border-radius: 8px; margin-bottom: 20px; }
.alert-success { background: rgba(34, 197, 94, 0.15); border: 1px solid #22c55e; color: #22c55e; }
.section-title { font-size: 20px; font-weight: 600; margin-bottom: 20px; display: flex; align-items: center; gap: 8px; }
.video-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; }
.empty-state { text-align: center; padding: 40px 20px; color: var(--text-secondary); }
.empty-state i { font-size: 48px; margin-bottom: 16px; opacity: 0.5; }
.w-100 { width: 100%; }
.mt-4 { margin-top: 24px; }
.text-center { text-align: center; }
.gap-3 { gap: 12px; }
.d-flex { display: flex; }
.flex-column { flex-direction: column; }
.video-card { background: var(--bg-primary); border-radius: 8px; overflow: hidden; transition: transform 0.2s; }
.video-card:hover { transform: translateY(-4px); }
.video-thumbnail { position: relative; aspect-ratio: 16/9; background: #000; }
.video-thumbnail img { width: 100%; height: 100%; object-fit: cover; }
.video-duration { position: absolute; bottom: 8px; right: 8px; background: rgba(0,0,0,0.8); color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px; }
.video-info { padding: 12px; }
.video-title { font-size: 14px; font-weight: 500; margin-bottom: 6px; line-height: 1.4; }
.video-title a { color: var(--text-primary); text-decoration: none; }
.video-title a:hover { color: var(--brand-red); }
.video-meta { font-size: 12px; color: var(--text-secondary); }
@media (max-width: 768px) { .profile-stats { justify-content: center; } .profile-meta { justify-content: center; } .social-links { justify-content: center; } }
</style>
@endsection
@section('content')
<div class="profile-header text-center">
@if($user->avatar)
<img src="{{ asset('storage/avatars/' . $user->avatar) }}" alt="{{ $user->name }}" class="profile-avatar">
@else
<img src="https://i.pravatar.cc/150?u={{ $user->id }}" alt="{{ $user->name }}" class="profile-avatar">
@endif
<h1 class="profile-name">{{ $user->name }}</h1>
<p class="profile-email">{{ $user->email }}</p>
<div class="profile-stats justify-content-center">
<div class="profile-stat">
<div class="profile-stat-value">{{ $user->videos->count() }}</div>
<div class="profile-stat-label">Videos</div>
<div class="profile-header">
<div class="profile-banner"></div>
<div class="profile-content text-center">
@if($user->avatar)
<img src="{{ asset('storage/avatars/' . $user->avatar) }}" alt="{{ $user->name }}" class="profile-avatar">
@else
<img src="https://i.pravatar.cc/150?u={{ $user->id }}" alt="{{ $user->name }}" class="profile-avatar">
@endif
<h1 class="profile-name">{{ $user->name }}</h1>
@if($user->username)
<p class="profile-username">@ {{ $user->username }}</p>
@endif
@if($user->bio)
<p class="profile-bio">{{ $user->bio }}</p>
@endif
<div class="profile-meta justify-content-center">
@if($user->location)
<div class="profile-meta-item"><i class="bi bi-geo-alt"></i><span>{{ $user->location }}</span></div>
@endif
@if($user->website)
<div class="profile-meta-item"><i class="bi bi-link-45deg"></i><a href="{{ $user->website_url }}" target="_blank" style="color: inherit;">{{ Str::limit($user->website, 30) }}</a></div>
@endif
<div class="profile-meta-item"><i class="bi bi-calendar3"></i><span>Joined {{ $user->created_at->format('M Y') }}</span></div>
</div>
<div class="profile-stat">
<div class="profile-stat-value">{{ $user->likes->count() }}</div>
<div class="profile-stat-label">Likes</div>
</div>
<div class="profile-stat">
<div class="profile-stat-value">{{ \DB::table('video_views')->whereIn('video_id', $user->videos->pluck('id'))->count() }}</div>
<div class="profile-stat-label">Total Views</div>
@if($user->hasSocialLinks())
<div class="social-links justify-content-center">
@if($user->twitter)<a href="https://twitter.com/{{ $user->twitter }}" target="_blank" class="social-link twitter" title="Twitter"><i class="bi bi-twitter-x"></i></a>@endif
@if($user->instagram)<a href="https://instagram.com/{{ $user->instagram }}" target="_blank" class="social-link instagram" title="Instagram"><i class="bi bi-instagram"></i></a>@endif
@if($user->facebook)<a href="https://facebook.com/{{ $user->facebook }}" target="_blank" class="social-link facebook" title="Facebook"><i class="bi bi-facebook"></i></a>@endif
@if($user->youtube)<a href="https://youtube.com/@{{ $user->youtube }}" target="_blank" class="social-link youtube" title="YouTube"><i class="bi bi-youtube"></i></a>@endif
@if($user->linkedin)<a href="https://linkedin.com/in/{{ $user->linkedin }}" target="_blank" class="social-link linkedin" title="LinkedIn"><i class="bi bi-linkedin"></i></a>@endif
@if($user->tiktok)<a href="https://tiktok.com/@{{ $user->tiktok }}" target="_blank" class="social-link tiktok" title="TikTok"><i class="bi bi-tiktok"></i></a>@endif
@if($user->website)<a href="{{ $user->website_url }}" target="_blank" class="social-link" title="Website"><i class="bi bi-globe"></i></a>@endif
</div>
@endif
<div class="profile-stats justify-content-center">
<a href="{{ route('channel', $user->id) }}" class="profile-stat"><div class="profile-stat-value">{{ $user->videos->count() }}</div><div class="profile-stat-label">Videos</div></a>
<div class="profile-stat"><div class="profile-stat-value">{{ number_format($user->subscriber_count) }}</div><div class="profile-stat-label">Subscribers</div></div>
<div class="profile-stat"><div class="profile-stat-value">{{ \DB::table('video_views')->whereIn('video_id', $user->videos->pluck('id'))->count() }}</div><div class="profile-stat-label">Views</div></div>
<div class="profile-stat"><div class="profile-stat-value">{{ $user->likes->count() }}</div><div class="profile-stat-label">Likes</div></div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<div class="col-lg-8">
<div class="form-card">
<h2 class="form-title">Edit Profile</h2>
@if(session('success'))
<div class="alert alert-success">
{{ session('success') }}
<h2 class="section-title"><i class="bi bi-play-circle"></i> Recent Videos</h2>
@if($user->videos->count() > 0)
<div class="video-grid">
@foreach($user->videos->take(4) as $video)
<div class="video-card">
<a href="{{ route('videos.show', $video) }}">
<div class="video-thumbnail">
<img src="{{ $video->thumbnail ? asset('storage/thumbnails/' . $video->thumbnail) : 'https://i.ytimg.com/vi/default.jpg' }}" alt="{{ $video->title }}">
<span class="video-duration">{{ $video->duration ?? '0:00' }}</span>
</div>
</a>
<div class="video-info">
<h3 class="video-title"><a href="{{ route('videos.show', $video) }}">{{ Str::limit($video->title, 50) }}</a></h3>
<div class="video-meta"><span>{{ number_format($video->view_count ?? 0) }} views</span><span> • </span><span>{{ $video->created_at->diffForHumans() }}</span></div>
</div>
</div>
@endforeach
</div>
@if($user->videos->count() > 4)
<div class="text-center mt-4"><a href="{{ route('channel', $user->id) }}" class="btn-secondary">View All Videos <i class="bi bi-arrow-right"></i></a></div>
@endif
@else
<div class="empty-state"><i class="bi bi-camera-video"></i><p>No videos yet</p></div>
@endif
<form method="POST" action="{{ route('profile.update') }}" enctype="multipart/form-data">
@csrf
@method('PUT')
<div class="form-group">
<label class="form-label">Name</label>
<input type="text" name="name" class="form-input" value="{{ old('name', $user->name) }}" required>
</div>
<div class="form-group">
<label class="form-label">Avatar</label>
<input type="file" name="avatar" class="form-input" accept="image/*">
<small class="text-muted">Max size: 5MB. Supported: JPG, PNG, WebP</small>
</div>
<button type="submit" class="btn-primary">Save Changes</button>
</form>
</div>
</div>
<div class="col-lg-6">
<div class="col-lg-4">
<div class="form-card">
<h2 class="form-title">Quick Links</h2>
<a href="{{ route('channel', $user->id) }}" class="btn-primary d-inline-block text-decoration-none">
<i class="bi bi-play-btn"></i> View My Channel
</a>
<a href="{{ route('settings') }}" class="btn-primary d-inline-block text-decoration-none ms-2">
<i class="bi bi-gear"></i> Settings
</a>
<h2 class="form-title"><i class="bi bi-pencil-square"></i> Edit Profile</h2>
@if(session('success'))<div class="alert alert-success">{{ session('success') }}</div>@endif
<form method="POST" action="{{ route('profile.update') }}" enctype="multipart/form-data">
@csrf @method('PUT')
<div class="form-group"><label class="form-label">Name</label><input type="text" name="name" class="form-input" value="{{ old('name', $user->name) }}" required></div>
<div class="form-group"><label class="form-label">Avatar</label><input type="file" name="avatar" class="form-input" accept="image/*"><small class="form-hint">Max: 5MB</small></div>
<div class="form-group"><label class="form-label">Bio</label><textarea name="bio" class="form-textarea" placeholder="Tell us about yourself...">{{ old('bio', $user->bio) }}</textarea></div>
<div class="form-group"><label class="form-label">Location</label><input type="text" name="location" class="form-input" value="{{ old('location', $user->location) }}" placeholder="City, Country"></div>
<div class="form-group"><label class="form-label">Birthday</label><input type="date" name="birthday" class="form-input" value="{{ old('birthday', $user->birthday) }}"></div>
<button type="submit" class="btn-primary w-100"><i class="bi bi-check-lg"></i> Save Changes</button>
</form>
</div>
<div class="form-card">
<h2 class="form-title"><i class="bi bi-share"></i> Social Links</h2>
<form method="POST" action="{{ route('profile.update') }}" enctype="multipart/form-data">
@csrf @method('PUT')
<div class="form-row">
<div class="form-group"><label class="form-label"><i class="bi bi-twitter-x"></i> Twitter</label><input type="text" name="twitter" class="form-input" value="{{ old('twitter', $user->twitter) }}" placeholder="username"></div>
<div class="form-group"><label class="form-label"><i class="bi bi-instagram"></i> Instagram</label><input type="text" name="instagram" class="form-input" value="{{ old('instagram', $user->instagram) }}" placeholder="username"></div>
</div>
<div class="form-row">
<div class="form-group"><label class="form-label"><i class="bi bi-facebook"></i> Facebook</label><input type="text" name="facebook" class="form-input" value="{{ old('facebook', $user->facebook) }}" placeholder="username"></div>
<div class="form-group"><label class="form-label"><i class="bi bi-youtube"></i> YouTube</label><input type="text" name="youtube" class="form-input" value="{{ old('youtube', $user->youtube) }}" placeholder="channel"></div>
</div>
<div class="form-row">
<div class="form-group"><label class="form-label"><i class="bi bi-linkedin"></i> LinkedIn</label><input type="text" name="linkedin" class="form-input" value="{{ old('linkedin', $user->linkedin) }}" placeholder="username"></div>
<div class="form-group"><label class="form-label"><i class="bi bi-tiktok"></i> TikTok</label><input type="text" name="tiktok" class="form-input" value="{{ old('tiktok', $user->tiktok) }}" placeholder="username"></div>
</div>
<div class="form-group"><label class="form-label"><i class="bi bi-globe"></i> Website</label><input type="url" name="website" class="form-input" value="{{ old('website', $user->website) }}" placeholder="https://example.com"></div>
<button type="submit" class="btn-primary w-100"><i class="bi bi-check-lg"></i> Save Social Links</button>
</form>
</div>
<div class="form-card">
<h2 class="form-title"><i class="bi bi-link-45deg"></i> Quick Links</h2>
<div class="d-flex flex-column gap-3">
<a href="{{ route('channel', $user->id) }}" class="btn-secondary text-center"><i class="bi bi-play-btn"></i> View My Channel</a>
<a href="{{ route('settings') }}" class="btn-secondary text-center"><i class="bi bi-gear"></i> Account Settings</a>
<a href="{{ route('history') }}" class="btn-secondary text-center"><i class="bi bi-clock-history"></i> Watch History</a>
<a href="{{ route('liked') }}" class="btn-secondary text-center"><i class="bi bi-heart"></i> Liked Videos</a>
</div>
</div>
</div>
</div>
@endsection
File diff suppressed because it is too large. Load diff
+353
View File
@@ -0,0 +1,353 @@
@extends('layouts.app')
@section('title', 'Shorts - ' . config('app.name'))
@section('content')
<div class="yt-main">
<div class="yt-content">
<!-- Shorts Header -->
<div class="shorts-header">
<div class="shorts-brand">
<div class="shorts-logo-icon">
<i class="bi bi-play-fill"></i>
</div>
<h1>Shorts</h1>
</div>
</div>
<!-- Shorts Grid - YouTube Style -->
<div class="shorts-container">
@forelse($videos as $video)
<div class="shorts-card">
<a href="{{ route('videos.show', $video->id) }}" class="shorts-link">
<div class="shorts-thumbnail-wrapper">
<img src="{{ $video->thumbnail_url }}" alt="{{ $video->title }}" class="shorts-thumb">
<div class="shorts-overlay">
<span class="shorts-views">
<i class="bi bi-play-fill"></i> {{ number_format($video->view_count) }}
</span>
</div>
@if($video->duration)
<span class="shorts-time">{{ gmdate('i:s', $video->duration) }}</span>
@endif
</div>
<div class="shorts-details">
<h3 class="shorts-title">{{ $video->title }}</h3>
<div class="shorts-channel-info">
@if($video->user)
<div class="shorts-channel-row">
@if($video->user->avatar_url)
<img src="{{ $video->user->avatar_url }}" class="shorts-avatar" alt="{{ $video->user->name }}">
@else
<div class="shorts-avatar-placeholder">{{ substr($video->user->name, 0, 1) }}</div>
@endif
<span class="shorts-channel-name">{{ $video->user->name }}</span>
</div>
@endif
<span class="shorts-ago">{{ $video->created_at->diffForHumans() }}</span>
</div>
</div>
</a>
</div>
@empty
<div class="shorts-empty-state">
<div class="shorts-empty-icon">
<i class="bi bi-collection-play"></i>
</div>
<h2>No Shorts yet</h2>
<p>Be the first to upload a Short!</p>
@auth
<a href="{{ route('videos.create') }}" class="shorts-upload-btn">
<i class="bi bi-plus-lg"></i> Upload Short
</a>
@else
<a href="{{ route('login') }}" class="shorts-upload-btn">
<i class="bi bi-box-arrow-in-right"></i> Login to Upload
</a>
@endauth
</div>
@endforelse
</div>
<!-- Pagination -->
@if($videos->hasPages())
<div class="shorts-pagination">
{{ $videos->links() }}
</div>
@endif
</div>
</div>
<style>
/* Shorts Page - YouTube Shorts Style */
.shorts-header {
margin-bottom: 20px;
padding: 12px 0;
}
.shorts-brand {
display: flex;
align-items: center;
gap: 12px;
}
.shorts-logo-icon {
width: 40px;
height: 40px;
background: linear-gradient(135deg, #ff0050, #ff6b6b);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 20px;
}
.shorts-brand h1 {
font-size: 22px;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
/* Shorts Grid - 4 columns on desktop */
.shorts-container {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
/* Shorts Card */
.shorts-card {
background: transparent;
border-radius: 12px;
overflow: hidden;
transition: transform 0.2s;
}
.shorts-card:hover {
transform: scale(1.02);
}
.shorts-link {
text-decoration: none;
color: inherit;
display: block;
}
.shorts-thumbnail-wrapper {
position: relative;
aspect-ratio: 9/16;
border-radius: 12px;
overflow: hidden;
background: #1a1a1a;
}
.shorts-thumb {
width: 100%;
height: 100%;
object-fit: cover;
}
.shorts-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 8px;
background: linear-gradient(transparent, rgba(0,0,0,0.7));
opacity: 0;
transition: opacity 0.2s;
}
.shorts-card:hover .shorts-overlay {
opacity: 1;
}
.shorts-views {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
font-weight: 500;
color: white;
}
.shorts-time {
position: absolute;
bottom: 8px;
right: 8px;
background: rgba(0,0,0,0.8);
color: white;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.shorts-details {
padding: 10px 4px;
}
.shorts-title {
font-size: 14px;
font-weight: 500;
color: var(--text-primary);
margin: 0 0 8px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.3;
}
.shorts-channel-info {
display: flex;
flex-direction: column;
gap: 6px;
}
.shorts-channel-row {
display: flex;
align-items: center;
gap: 8px;
}
.shorts-avatar, .shorts-avatar-placeholder {
width: 24px;
height: 24px;
border-radius: 50%;
object-fit: cover;
}
.shorts-avatar-placeholder {
background: #666;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 600;
}
.shorts-channel-name {
font-size: 12px;
color: var(--text-secondary);
}
.shorts-ago {
font-size: 11px;
color: var(--text-secondary);
opacity: 0.7;
}
/* Empty State */
.shorts-empty-state {
grid-column: 1 / -1;
text-align: center;
padding: 60px 20px;
background: var(--bg-secondary);
border-radius: 16px;
}
.shorts-empty-icon {
width: 80px;
height: 80px;
margin: 0 auto 20px;
background: linear-gradient(135deg, #ff0050, #ff6b6b);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.shorts-empty-icon i {
font-size: 36px;
color: white;
}
.shorts-empty-state h2 {
font-size: 20px;
color: var(--text-primary);
margin: 0 0 8px;
}
.shorts-empty-state p {
color: var(--text-secondary);
margin: 0 0 20px;
}
.shorts-upload-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: #ff0050;
color: white;
border-radius: 20px;
text-decoration: none;
font-weight: 500;
font-size: 14px;
transition: background 0.2s;
}
.shorts-upload-btn:hover {
background: #e60048;
}
/* Pagination */
.shorts-pagination {
margin-top: 30px;
display: flex;
justify-content: center;
}
.shorts-pagination .pagination {
gap: 4px;
}
.shorts-pagination .page-link {
background: var(--bg-secondary);
border-color: var(--border-color);
color: var(--text-primary);
padding: 6px 12px;
font-size: 13px;
}
.shorts-pagination .page-item.active .page-link {
background: #ff0050;
border-color: #ff0050;
}
/* Responsive */
@media (max-width: 1200px) {
.shorts-container {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 900px) {
.shorts-container {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
}
@media (max-width: 600px) {
.shorts-container {
grid-template-columns: 1fr;
max-width: 320px;
margin: 0 auto;
}
.shorts-header {
text-align: center;
}
.shorts-brand {
justify-content: center;
}
}
</style>
@endsection
File diff suppressed because it is too large. Load diff
+6 -23
View File
@@ -177,13 +177,13 @@
<div class="trending-header">
<h1><i class="bi bi-fire trending-icon"></i> Trending</h1>
<div class="trending-filters">
<a href="{{ route('videos.trending', ['hours' => 24]) }}" class="{{ == 24 ? 'active' : '' }}">Today</a>
<a href="{{ route('videos.trending', ['hours' => 48]) }}" class="{{ == 48 ? 'active' : '' }}">This Week</a>
<a href="{{ route('videos.trending', ['hours' => 168]) }}" class="{{ == 168 ? 'active' : '' }}">This Month</a>
<a href="{{ route('videos.trending', ['hours' => 24]) }}" class="{{ $hours == 24 ? 'active' : '' }}">Today</a>
<a href="{{ route('videos.trending', ['hours' => 48]) }}" class="{{ $hours == 48 ? 'active' : '' }}">This Week</a>
<a href="{{ route('videos.trending', ['hours' => 168]) }}" class="{{ $hours == 168 ? 'active' : '' }}">This Month</a>
</div>
</div>
@if(())
@if($videos->isEmpty())
<div class="empty-trending">
<i class="bi bi-play-circle"></i>
<h3>No trending videos yet</h3>
@@ -191,25 +191,8 @@
</div>
@else
<div class="yt-video-grid">
@foreach( as )
<a href="{{ route('videos.show', ) }}" class="yt-video-card">
<div class="yt-video-thumb">
<img src="{{ }}" alt="{{ }}" loading="lazy">
<span class="trending-badge"><i class="bi bi-fire"></i> Trending</span>
@if()<span class="yt-video-duration">{{ gmdate('i:s', ) }}</span>@endif
</div>
<div class="yt-video-meta">
@if()<img src="{{ ->avatar_url }}" alt="{{ ->name }}" class="yt-video-avatar">@endif
<div class="yt-video-info">
<h3 class="yt-video-title">{{ }}</h3>
<div class="yt-video-channel"><a href="#">{{ ->name ?? 'Unknown' }}</a></div>
<div class="yt-video-stats">
{{ number_format() }} views
@if( > 0) • {{ number_format() }} likes @endif
</div>
</div>
</div>
</a>
@foreach($videos as $video)
<x-video-card :video="$video" />
@endforeach
</div>
@endif
File diff suppressed because it is too large. Load diff
+354 -66
View File
@@ -6,7 +6,7 @@
<style>
/* Video Section */
.yt-video-section { flex: 1; min-width: 0; }
/* Video Player */
.video-container {
position: relative;
@@ -20,20 +20,122 @@
align-items: center;
width: 100%;
}
.video-container.portrait,
.video-container.square,
.video-container.portrait,
.video-container.square,
.video-container.ultrawide {
margin: 0 auto;
width: auto;
}
.video-container.portrait { aspect-ratio: 9/16; max-width: 50vh; }
.video-container.square { aspect-ratio: 1/1; max-width: 70vh; }
.video-container.ultrawide { aspect-ratio: 21/9; max-width: 100%; }
.video-container video { width: 100%; height: 100%; object-fit: contain; }
/* Playlist Navigation Controls */
.playlist-controls {
position: absolute;
bottom: 60px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 12px;
background: rgba(0, 0, 0, 0.8);
padding: 8px 16px;
border-radius: 24px;
z-index: 10;
opacity: 0;
transition: opacity 0.3s ease;
}
.video-container:hover .playlist-controls,
.playlist-controls.visible { opacity: 1; }
.playlist-nav-btn {
background: transparent;
border: none;
color: white;
cursor: pointer;
padding: 8px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s ease;
}
.playlist-nav-btn:hover { background: rgba(255, 255, 255, 0.2); }
.playlist-nav-btn:disabled { opacity: 0.3; cursor: not-allowed; }
.playlist-nav-btn:disabled:hover { background: transparent; }
.playlist-nav-btn i { font-size: 20px; }
.playlist-nav-label {
font-size: 12px;
color: #aaa;
max-width: 120px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Autoplay Toggle */
.autoplay-toggle {
display: flex;
align-items: center;
gap: 8px;
padding: 0 8px;
border-left: 1px solid rgba(255, 255, 255, 0.2);
}
.autoplay-toggle label {
font-size: 12px;
color: #aaa;
cursor: pointer;
white-space: nowrap;
}
.autoplay-switch { position: relative; width: 36px; height: 20px; }
.autoplay-switch input { opacity: 0; width: 0; height: 0; }
.autoplay-slider {
position: absolute;
cursor: pointer;
top: 0; left: 0; right: 0; bottom: 0;
background-color: #666;
transition: 0.3s;
border-radius: 20px;
}
.autoplay-slider:before {
position: absolute;
content: "";
height: 14px;
width: 14px;
left: 3px;
bottom: 3px;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
.autoplay-switch input:checked + .autoplay-slider { background-color: var(--brand-red); }
.autoplay-switch input:checked + .autoplay-slider:before { transform: translateX(16px); }
/* Keyboard hint */
.keyboard-hint {
position: absolute;
bottom: 10px;
right: 10px;
font-size: 10px;
color: rgba(255, 255, 255, 0.5);
background: rgba(0, 0, 0, 0.6);
padding: 4px 8px;
border-radius: 4px;
}
/* Video Info */
.video-title {
font-size: 20px;
@@ -41,7 +143,7 @@
margin: 16px 0 8px;
line-height: 1.3;
}
.video-stats-row {
display: flex;
align-items: center;
@@ -51,15 +153,15 @@
flex-wrap: wrap;
gap: 12px;
}
.video-stats-left { display: flex; align-items: center; gap: 16px; color: var(--text-secondary); }
.video-actions {
display: flex;
align-items: center;
gap: 8px;
}
.yt-action-btn {
display: flex;
align-items: center;
@@ -73,11 +175,11 @@
font-size: 14px;
font-weight: 500;
}
.yt-action-btn:hover { background: var(--border-color); }
.yt-action-btn.liked { color: var(--brand-red); }
/* Channel Row */
.channel-row {
display: flex;
@@ -85,27 +187,27 @@
justify-content: space-between;
padding: 16px 0;
}
.channel-info {
display: flex;
align-items: center;
gap: 12px;
}
.channel-avatar {
width: 48px; height: 48px; border-radius: 50%; background: #555;
}
.channel-name {
font-size: 16px;
font-weight: 500;
}
.channel-subs {
font-size: 14px;
color: var(--text-secondary);
}
.subscribe-btn {
background: white;
color: black;
@@ -117,7 +219,7 @@
cursor: pointer;
white-space: nowrap;
}
/* Description */
.video-description {
background: var(--bg-secondary);
@@ -125,26 +227,26 @@
padding: 16px;
margin-top: 16px;
}
.description-text {
white-space: pre-wrap;
font-size: 14px;
line-height: 1.5;
}
/* Sidebar */
.yt-sidebar-container {
width: 400px;
flex-shrink: 0;
}
.sidebar-video-card {
display: flex;
gap: 8px;
margin-bottom: 8px;
cursor: pointer;
}
.sidebar-thumb {
width: 168px;
aspect-ratio: 16/9;
@@ -153,11 +255,11 @@
background: #1a1a1a;
flex-shrink: 0;
}
.sidebar-thumb img { width: 100%; height: 100%; object-fit: cover; }
.sidebar-info { flex: 1; min-width: 0; }
.sidebar-title {
font-size: 14px;
font-weight: 500;
@@ -167,21 +269,20 @@
-webkit-box-orient: vertical;
overflow: hidden;
}
.sidebar-meta { font-size: 12px; color: var(--text-secondary); }
/* Responsive */
@media (max-width: 1300px) {
.yt-sidebar-container { width: 300px; }
}
@media (max-width: 991px) {
.yt-main { margin-left: 0; flex-direction: column; }
.yt-sidebar-container { width: 100%; }
.yt-header-center { display: none; }
.sidebar-video-card { flex-direction: column; }
.sidebar-thumb { width: 100%; }
.video-layout-container {
flex-direction: column !important;
}
@@ -194,12 +295,12 @@
margin-top: 16px;
}
}
@media (max-width: 576px) {
.video-stats-row { flex-direction: column; align-items: flex-start; }
.video-actions { width: 100%; overflow-x: auto; justify-content: flex-start; }
.yt-main { padding: 12px !important; }
.video-container {
max-height: 50vh !important;
border-radius: 0 !important;
@@ -241,14 +342,55 @@
<video id="videoPlayer" controls playsinline preload="metadata" autoplay>
<source src="{{ route('videos.stream', $video->id) }}" type="video/mp4">
</video>
<!-- Playlist Navigation Controls (only show when in playlist) -->
@if($nextVideo || $previousVideo)
<div class="playlist-controls" id="playlistControls">
<!-- Previous Video -->
@if($previousVideo)
<button class="playlist-nav-btn" onclick="goToPreviousVideo()" title="Previous video">
<i class="bi bi-skip-backward-fill"></i>
</button>
<span class="playlist-nav-label">{{ Str::limit($previousVideo->title, 20) }}</span>
@else
<button class="playlist-nav-btn" disabled title="No previous video">
<i class="bi bi-skip-backward-fill"></i>
</button>
@endif
<!-- Autoplay Toggle -->
<div class="autoplay-toggle">
<label for="autoplayToggle">Autoplay</label>
<label class="autoplay-switch">
<input type="checkbox" id="autoplayToggle" checked>
<span class="autoplay-slider"></span>
</label>
</div>
<!-- Next Video -->
@if($nextVideo)
<span class="playlist-nav-label">{{ Str::limit($nextVideo->title, 20) }}</span>
<button class="playlist-nav-btn" onclick="goToNextVideo()" title="Next video">
<i class="bi bi-skip-forward-fill"></i>
</button>
@else
<button class="playlist-nav-btn" disabled title="No next video">
<i class="bi bi-skip-forward-fill"></i>
</button>
@endif
</div>
<div class="keyboard-hint">
<i class="bi bi-arrow-left"></i> <i class="bi bi-arrow-right"></i>
</div>
@endif
</div>
<!-- Video Title with Trophy Icon (Match Type) -->
<h1 class="video-title" style="display: flex; align-items: center; gap: 10px; margin: 8px 0 6px;">
<i class="bi bi-trophy" style="color: #ef4444;"></i>
<span>{{ $video->title }}</span>
</h1>
<!-- Stats Row - Hidden, shown in description box -->
<div class="video-stats-row" style="display: none;">
<div class="video-stats-left">
@@ -257,7 +399,7 @@
<span>{{ $video->created_at->format('M d, Y') }}</span>
</div>
</div>
<!-- Channel Row - All in one line -->
<div class="channel-row" style="display: flex; align-items: center; justify-content: space-between; padding: 12px 0; flex-wrap: wrap; gap: 16px;">
<div style="display: flex; align-items: center; gap: 12px;">
@@ -273,7 +415,7 @@
</div>
</a>
</div>
<div class="video-actions" style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
@auth
@if(Auth::id() !== $video->user_id)
@@ -286,7 +428,7 @@
@else
<a href="{{ route('login') }}" class="subscribe-btn">Subscribe</a>
@endauth
@auth
<form method="POST" action="{{ $video->isLikedBy(Auth::user()) ? route('videos.unlike', $video->id) : route('videos.like', $video->id) }}" class="d-inline">
@csrf
@@ -300,7 +442,7 @@
<i class="bi bi-hand-thumbs-up"></i> Like
</a>
@endauth
@if($video->isShareable())
<button class="yt-action-btn" onclick="openShareModal('{{ $video->share_url }}', '{{ addslashes($video->title) }}')">
<i class="bi bi-share"></i> Share
@@ -308,7 +450,7 @@
@endif
</div>
</div>
<!-- Description Box -->
@if($video->description)
@php
@@ -316,7 +458,7 @@
$shortDescription = Str::limit($fullDescription, 200);
$needsExpand = strlen($fullDescription) > 200;
@endphp
<div class="video-description-box" style="background: var(--bg-secondary); border-radius: 12px; padding: 12px; margin-bottom: 16px;">
<div class="description-stats" style="font-size: 14px; font-weight: 500; margin-bottom: 8px;">
<span>{{ number_format($video->view_count) }} views</span>
@@ -341,13 +483,13 @@
@endif
</div>
</div>
<script>
function toggleDescription() {
const descShort = document.getElementById('descShort');
const descFull = document.getElementById('descFull');
const toggleBtn = document.getElementById('descToggleBtn');
if (descShort.style.display !== 'none') {
descShort.style.display = 'none';
descFull.style.display = 'block';
@@ -359,27 +501,27 @@
}
}
</script>
<style>
.video-description-box .description-text { font-size: 14px; line-height: 1.5; color: var(--text-primary); }
.video-description-box .description-text p { margin-bottom: 8px; }
.video-description-box .description-text a { color: #3ea6ff; }
</style>
@endif
<!-- Comment Section -->
<div class="comments-section" style="margin-top: 24px; padding-top: 16px; border-top: 1px solid var(--border-color);">
<h3 style="font-size: 18px; font-weight: 600; margin-bottom: 16px;">
Comments <span style="color: var(--text-secondary); font-weight: 400;">({{ $video->comment_count }})</span>
</h3>
@auth
<div class="comment-form" style="display: flex; gap: 12px; margin-bottom: 24px;">
<img src="{{ Auth::user()->avatar_url }}" class="channel-avatar" style="width: 40px; height: 40px;" alt="{{ Auth::user()->name }}">
<div style="flex: 1;">
<textarea
id="commentBody"
class="form-control"
<textarea
id="commentBody"
class="form-control"
placeholder="Add a comment... Use @ to mention someone"
rows="3"
style="background: var(--bg-secondary); border: 1px solid var(--border-color); color: var(--text-primary); border-radius: 8px; padding: 12px; width: 100%; resize: none;"
@@ -395,7 +537,7 @@
<a href="{{ route('login') }}" style="color: var(--brand-red);">Sign in</a> to comment
</div>
@endauth
<div id="commentsList">
@forelse($video->comments()->whereNull('parent_id')->with('user', 'replies.user')->latest()->get() as $comment)
@include('videos.partials.comment', ['comment' => $comment])
@@ -404,12 +546,12 @@
@endforelse
</div>
</div>
<script>
function submitComment(videoId) {
const body = document.getElementById('commentBody').value.trim();
if (!body) return;
fetch(`/videos/${videoId}/comments`, {
method: 'POST',
headers: {
@@ -426,10 +568,10 @@
}
});
}
function deleteComment(commentId) {
if (!confirm('Are you sure you want to delete this comment?')) return;
fetch(`/comments/${commentId}`, {
method: 'DELETE',
headers: {
@@ -443,7 +585,7 @@
}
});
}
document.addEventListener('DOMContentLoaded', function() {
const commentTexts = document.querySelectorAll('.comment-body');
commentTexts.forEach(text => {
@@ -451,12 +593,12 @@
text.innerHTML = html;
});
});
function submitReply(videoId, parentId) {
const textarea = document.querySelector(`#replyForm${parentId} textarea`);
const body = textarea.value.trim();
if (!body) return;
fetch(`/videos/${videoId}/comments`, {
method: 'POST',
headers: {
@@ -474,18 +616,99 @@
}
</script>
</div>
<!-- Sidebar -->
<!-- Sidebar - Up Next / Recommendations -->
<div class="yt-sidebar-container">
<h3 style="font-size: 16px; font-weight: 500; margin-bottom: 12px;">Up Next</h3>
<div class="text-secondary">More videos coming soon...</div>
@if($playlist && $playlistVideos && $playlistVideos->count() > 0)
<h3 style="font-size: 16px; font-weight: 500; margin-bottom: 12px;">
<i class="bi bi-collection-play" style="margin-right: 8px;"></i>
{{ $playlist->name }}
<span style="font-weight: 400; color: var(--text-secondary); font-size: 14px;">({{ $playlistVideos->count() }} videos)</span>
</h3>
<div class="recommended-videos-list">
@foreach($playlistVideos as $index => $playlistVideo)
@if($playlistVideo->id !== $video->id)
<div class="sidebar-video-card{{ $playlistVideo->id === $video->id ? ' current-video' : '' }}"
onclick="window.location.href='{{ route('videos.show', $playlistVideo->id) }}?playlist={{ $playlist->id }}'">
<div class="sidebar-thumb" style="position: relative;">
@if ($playlistVideo->thumbnail)
<img src="{{ asset('storage/thumbnails/' . $playlistVideo->thumbnail) }}" alt="{{ $playlistVideo->title }}">
@else
<img src="https://picsum.photos/seed/{{ $playlistVideo->id }}/320/180" alt="{{ $playlistVideo->title }}">
@endif
@if ($playlistVideo->duration)
<span class="yt-video-duration">{{ gmdate('i:s', $playlistVideo->duration) }}</span>
@endif
@if ($playlistVideo->is_shorts)
<span class="yt-shorts-badge" style="position: absolute; top: 8px; left: 8px; font-size: 10px; padding: 2px 6px;">
<i class="bi bi-collection-play-fill"></i> SHORTS
</span>
@endif
<!-- Playlist position indicator -->
<span style="position: absolute; bottom: 4px; left: 4px; background: rgba(0,0,0,0.8); color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px; font-weight: 500;">
{{ $index + 1 }}
</span>
</div>
<div class="sidebar-info">
<div class="sidebar-title">
<i class="bi {{ match ($playlistVideo->type) {'music' => 'bi-music-note','match' => 'bi-trophy',default => 'bi-film'} }}"
style="color: #ef4444; margin-right: 4px; font-size: 12px;"></i>
{{ Str::limit($playlistVideo->title, 60) }}
</div>
<div class="sidebar-meta">
<div>{{ $playlistVideo->user->name ?? 'Unknown' }}</div>
<div>{{ number_format($playlistVideo->view_count) }} views • {{ $playlistVideo->created_at->diffForHumans() }}</div>
</div>
</div>
</div>
@endif
@endforeach
</div>
@if($playlist->canEdit(Auth::user()))
<a href="{{ route('playlists.show', $playlist->id) }}" class="yt-action-btn" style="margin-top: 12px; display: inline-block;">
<i class="bi bi-pencil"></i> Edit Playlist
</a>
@endif
@else
<h3 style="font-size: 16px; font-weight: 500; margin-bottom: 12px;">Up Next</h3>
@if($recommendedVideos && $recommendedVideos->count() > 0)
<div class="recommended-videos-list">
@foreach($recommendedVideos as $recVideo)
<div class="sidebar-video-card" onclick="window.location.href='{{ route('videos.show', $recVideo->id) }}'">
<div class="sidebar-thumb">
@if($recVideo->thumbnail)
<img src="{{ asset('storage/thumbnails/' . $recVideo->thumbnail) }}" alt="{{ $recVideo->title }}">
@else
<img src="https://picsum.photos/seed/{{ $recVideo->id }}/320/180" alt="{{ $recVideo->title }}">
@endif
@if($recVideo->duration)
<span class="yt-video-duration">{{ gmdate('i:s', $recVideo->duration) }}</span>
@endif
</div>
<div class="sidebar-info">
<div class="sidebar-title">
<i class="bi {{ match($recVideo->type) { 'music' => 'bi-music-note', 'match' => 'bi-trophy', default => 'bi-film' } }}" style="color: #ef4444; margin-right: 4px; font-size: 12px;"></i>
{{ Str::limit($recVideo->title, 60) }}
</div>
<div class="sidebar-meta">
<div>{{ $recVideo->user->name ?? 'Unknown' }}</div>
<div>{{ number_format($recVideo->view_count) }} views • {{ $recVideo->created_at->diffForHumans() }}</div>
</div>
</div>
</div>
@endforeach
</div>
@else
<div class="text-secondary">More videos coming soon...</div>
@endif
@endif
</div>
</div>
@include('layouts.partials.share-modal')
@include('layouts.partials.edit-video-modal')
@if(Session::has('openEditModal') && Session::get('openEditModal'))
@auth
<script>
@@ -495,17 +718,82 @@
</script>
@endauth
@endif
<script>
// Playlist navigation URLs
const previousVideoUrl = @json($previousVideo ? route('videos.show', $previousVideo->id) . '?playlist=' . $playlist->id : null);
const nextVideoUrl = @json($nextVideo ? route('videos.show', $nextVideo->id) . '?playlist=' . $playlist->id : null);
// Autoplay management
let autoplayEnabled = true;
document.addEventListener('DOMContentLoaded', function() {
var videoPlayer = document.getElementById('videoPlayer');
var playlistControls = document.getElementById('playlistControls');
var autoplayToggle = document.getElementById('autoplayToggle');
if (videoPlayer) {
videoPlayer.volume = 0.5;
var playPromise = videoPlayer.play();
if (playPromise !== undefined) {
playPromise.then(function() { console.log('Video autoplayed'); }).catch(function(error) { console.log('Autoplay blocked'); });
}
// Handle video ended - autoplay next
if (nextVideoUrl) {
videoPlayer.addEventListener('ended', function() {
if (autoplayEnabled) {
console.log('Video ended, going to next...');
window.location.href = nextVideoUrl;
}
});
}
}
// Show playlist controls on hover
if (playlistControls) {
const videoContainer = document.getElementById('videoContainer');
videoContainer.addEventListener('mouseenter', function() {
playlistControls.classList.add('visible');
});
videoContainer.addEventListener('mouseleave', function() {
playlistControls.classList.remove('visible');
});
// Initially show for a few seconds
setTimeout(function() { playlistControls.classList.add('visible'); }, 2000);
setTimeout(function() { playlistControls.classList.remove('visible'); }, 6000);
}
// Autoplay toggle handler
if (autoplayToggle) {
autoplayToggle.addEventListener('change', function() {
autoplayEnabled = this.checked;
localStorage.setItem('playlistAutoplay', autoplayEnabled ? '1' : '0');
});
// Load saved preference
const savedAutoplay = localStorage.getItem('playlistAutoplay');
if (savedAutoplay !== null) {
autoplayEnabled = savedAutoplay === '1';
autoplayToggle.checked = autoplayEnabled;
}
}
// Keyboard navigation
document.addEventListener('keydown', function(e) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { return; }
if (e.key === 'ArrowLeft') { e.preventDefault(); goToPreviousVideo(); }
else if (e.key === 'ArrowRight') { e.preventDefault(); goToNextVideo(); }
});
});
function goToPreviousVideo() {
if (previousVideoUrl) { window.location.href = previousVideoUrl; }
}
function goToNextVideo() {
if (nextVideoUrl) { window.location.href = nextVideoUrl; }
}
</script>
@endsection
+359 -66
View File
@@ -6,7 +6,7 @@
<style>
/* Video Section */
.yt-video-section { flex: 1; min-width: 0; }
/* Video Player */
.video-container {
position: relative;
@@ -20,20 +20,122 @@
align-items: center;
width: 100%;
}
.video-container.portrait,
.video-container.square,
.video-container.portrait,
.video-container.square,
.video-container.ultrawide {
margin: 0 auto;
width: auto;
}
.video-container.portrait { aspect-ratio: 9/16; max-width: 50vh; }
.video-container.square { aspect-ratio: 1/1; max-width: 70vh; }
.video-container.ultrawide { aspect-ratio: 21/9; max-width: 100%; }
.video-container video { width: 100%; height: 100%; object-fit: contain; }
/* Playlist Navigation Controls */
.playlist-controls {
position: absolute;
bottom: 60px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 12px;
background: rgba(0, 0, 0, 0.8);
padding: 8px 16px;
border-radius: 24px;
z-index: 10;
opacity: 0;
transition: opacity 0.3s ease;
}
.video-container:hover .playlist-controls,
.playlist-controls.visible { opacity: 1; }
.playlist-nav-btn {
background: transparent;
border: none;
color: white;
cursor: pointer;
padding: 8px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s ease;
}
.playlist-nav-btn:hover { background: rgba(255, 255, 255, 0.2); }
.playlist-nav-btn:disabled { opacity: 0.3; cursor: not-allowed; }
.playlist-nav-btn:disabled:hover { background: transparent; }
.playlist-nav-btn i { font-size: 20px; }
.playlist-nav-label {
font-size: 12px;
color: #aaa;
max-width: 120px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Autoplay Toggle */
.autoplay-toggle {
display: flex;
align-items: center;
gap: 8px;
padding: 0 8px;
border-left: 1px solid rgba(255, 255, 255, 0.2);
}
.autoplay-toggle label {
font-size: 12px;
color: #aaa;
cursor: pointer;
white-space: nowrap;
}
.autoplay-switch { position: relative; width: 36px; height: 20px; }
.autoplay-switch input { opacity: 0; width: 0; height: 0; }
.autoplay-slider {
position: absolute;
cursor: pointer;
top: 0; left: 0; right: 0; bottom: 0;
background-color: #666;
transition: 0.3s;
border-radius: 20px;
}
.autoplay-slider:before {
position: absolute;
content: "";
height: 14px;
width: 14px;
left: 3px;
bottom: 3px;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
.autoplay-switch input:checked + .autoplay-slider { background-color: var(--brand-red); }
.autoplay-switch input:checked + .autoplay-slider:before { transform: translateX(16px); }
/* Keyboard hint */
.keyboard-hint {
position: absolute;
bottom: 10px;
right: 10px;
font-size: 10px;
color: rgba(255, 255, 255, 0.5);
background: rgba(0, 0, 0, 0.6);
padding: 4px 8px;
border-radius: 4px;
}
/* Video Info */
.video-title {
font-size: 20px;
@@ -41,7 +143,7 @@
margin: 16px 0 8px;
line-height: 1.3;
}
.video-stats-row {
display: flex;
align-items: center;
@@ -51,15 +153,15 @@
flex-wrap: wrap;
gap: 12px;
}
.video-stats-left { display: flex; align-items: center; gap: 16px; color: var(--text-secondary); }
.video-actions {
display: flex;
align-items: center;
gap: 8px;
}
.yt-action-btn {
display: flex;
align-items: center;
@@ -73,11 +175,11 @@
font-size: 14px;
font-weight: 500;
}
.yt-action-btn:hover { background: var(--border-color); }
.yt-action-btn.liked { color: var(--brand-red); }
/* Channel Row */
.channel-row {
display: flex;
@@ -85,27 +187,27 @@
justify-content: space-between;
padding: 16px 0;
}
.channel-info {
display: flex;
align-items: center;
gap: 12px;
}
.channel-avatar {
width: 48px; height: 48px; border-radius: 50%; background: #555;
}
.channel-name {
font-size: 16px;
font-weight: 500;
}
.channel-subs {
font-size: 14px;
color: var(--text-secondary);
}
.subscribe-btn {
background: white;
color: black;
@@ -117,7 +219,7 @@
cursor: pointer;
white-space: nowrap;
}
/* Description */
.video-description {
background: var(--bg-secondary);
@@ -125,26 +227,26 @@
padding: 16px;
margin-top: 16px;
}
.description-text {
white-space: pre-wrap;
font-size: 14px;
line-height: 1.5;
}
/* Sidebar */
.yt-sidebar-container {
width: 400px;
flex-shrink: 0;
}
.sidebar-video-card {
display: flex;
gap: 8px;
margin-bottom: 8px;
cursor: pointer;
}
.sidebar-thumb {
width: 168px;
aspect-ratio: 16/9;
@@ -153,11 +255,11 @@
background: #1a1a1a;
flex-shrink: 0;
}
.sidebar-thumb img { width: 100%; height: 100%; object-fit: cover; }
.sidebar-info { flex: 1; min-width: 0; }
.sidebar-title {
font-size: 14px;
font-weight: 500;
@@ -167,21 +269,20 @@
-webkit-box-orient: vertical;
overflow: hidden;
}
.sidebar-meta { font-size: 12px; color: var(--text-secondary); }
/* Responsive */
@media (max-width: 1300px) {
.yt-sidebar-container { width: 300px; }
}
@media (max-width: 991px) {
.yt-main { margin-left: 0; flex-direction: column; }
.yt-sidebar-container { width: 100%; }
.yt-header-center { display: none; }
.sidebar-video-card { flex-direction: column; }
.sidebar-thumb { width: 100%; }
.video-layout-container {
flex-direction: column !important;
}
@@ -194,12 +295,12 @@
margin-top: 16px;
}
}
@media (max-width: 576px) {
.video-stats-row { flex-direction: column; align-items: flex-start; }
.video-actions { width: 100%; overflow-x: auto; justify-content: flex-start; }
.yt-main { padding: 12px !important; }
.video-container {
max-height: 50vh !important;
border-radius: 0 !important;
@@ -241,14 +342,55 @@
<video id="videoPlayer" controls playsinline preload="metadata" autoplay>
<source src="{{ route('videos.stream', $video->id) }}" type="video/mp4">
</video>
<!-- Playlist Navigation Controls (only show when in playlist) -->
@if($nextVideo || $previousVideo)
<div class="playlist-controls" id="playlistControls">
<!-- Previous Video -->
@if($previousVideo)
<button class="playlist-nav-btn" onclick="goToPreviousVideo()" title="Previous video">
<i class="bi bi-skip-backward-fill"></i>
</button>
<span class="playlist-nav-label">{{ Str::limit($previousVideo->title, 20) }}</span>
@else
<button class="playlist-nav-btn" disabled title="No previous video">
<i class="bi bi-skip-backward-fill"></i>
</button>
@endif
<!-- Autoplay Toggle -->
<div class="autoplay-toggle">
<label for="autoplayToggle">Autoplay</label>
<label class="autoplay-switch">
<input type="checkbox" id="autoplayToggle" checked>
<span class="autoplay-slider"></span>
</label>
</div>
<!-- Next Video -->
@if($nextVideo)
<span class="playlist-nav-label">{{ Str::limit($nextVideo->title, 20) }}</span>
<button class="playlist-nav-btn" onclick="goToNextVideo()" title="Next video">
<i class="bi bi-skip-forward-fill"></i>
</button>
@else
<button class="playlist-nav-btn" disabled title="No next video">
<i class="bi bi-skip-forward-fill"></i>
</button>
@endif
</div>
<div class="keyboard-hint">
<i class="bi bi-arrow-left"></i> <i class="bi bi-arrow-right"></i>
</div>
@endif
</div>
<!-- Video Title with Music Note Icon (Music Type) -->
<h1 class="video-title" style="display: flex; align-items: center; gap: 10px; margin: 8px 0 6px;">
<i class="bi bi-music-note" style="color: #ef4444;"></i>
<span>{{ $video->title }}</span>
</h1>
<!-- Stats Row - Hidden, shown in description box -->
<div class="video-stats-row" style="display: none;">
<div class="video-stats-left">
@@ -257,7 +399,7 @@
<span>{{ $video->created_at->format('M d, Y') }}</span>
</div>
</div>
<!-- Channel Row - All in one line -->
<div class="channel-row" style="display: flex; align-items: center; justify-content: space-between; padding: 12px 0; flex-wrap: wrap; gap: 16px;">
<div style="display: flex; align-items: center; gap: 12px;">
@@ -273,7 +415,7 @@
</div>
</a>
</div>
<div class="video-actions" style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
@auth
@if(Auth::id() !== $video->user_id)
@@ -286,7 +428,7 @@
@else
<a href="{{ route('login') }}" class="subscribe-btn">Subscribe</a>
@endauth
@auth
<form method="POST" action="{{ $video->isLikedBy(Auth::user()) ? route('videos.unlike', $video->id) : route('videos.like', $video->id) }}" class="d-inline">
@csrf
@@ -300,7 +442,7 @@
<i class="bi bi-hand-thumbs-up"></i> Like
</a>
@endauth
@if($video->isShareable())
<button class="yt-action-btn" onclick="openShareModal('{{ $video->share_url }}', '{{ addslashes($video->title) }}')">
<i class="bi bi-share"></i> Share
@@ -308,7 +450,7 @@
@endif
</div>
</div>
<!-- Description Box -->
@if($video->description)
@php
@@ -316,7 +458,7 @@
$shortDescription = Str::limit($fullDescription, 200);
$needsExpand = strlen($fullDescription) > 200;
@endphp
<div class="video-description-box" style="background: var(--bg-secondary); border-radius: 12px; padding: 12px; margin-bottom: 16px;">
<div class="description-stats" style="font-size: 14px; font-weight: 500; margin-bottom: 8px;">
<span>{{ number_format($video->view_count) }} views</span>
@@ -341,13 +483,13 @@
@endif
</div>
</div>
<script>
function toggleDescription() {
const descShort = document.getElementById('descShort');
const descFull = document.getElementById('descFull');
const toggleBtn = document.getElementById('descToggleBtn');
if (descShort.style.display !== 'none') {
descShort.style.display = 'none';
descFull.style.display = 'block';
@@ -359,27 +501,27 @@
}
}
</script>
<style>
.video-description-box .description-text { font-size: 14px; line-height: 1.5; color: var(--text-primary); }
.video-description-box .description-text p { margin-bottom: 8px; }
.video-description-box .description-text a { color: #3ea6ff; }
</style>
@endif
<!-- Comment Section -->
<div class="comments-section" style="margin-top: 24px; padding-top: 16px; border-top: 1px solid var(--border-color);">
<h3 style="font-size: 18px; font-weight: 600; margin-bottom: 16px;">
Comments <span style="color: var(--text-secondary); font-weight: 400;">({{ $video->comment_count }})</span>
</h3>
@auth
<div class="comment-form" style="display: flex; gap: 12px; margin-bottom: 24px;">
<img src="{{ Auth::user()->avatar_url }}" class="channel-avatar" style="width: 40px; height: 40px;" alt="{{ Auth::user()->name }}">
<div style="flex: 1;">
<textarea
id="commentBody"
class="form-control"
<textarea
id="commentBody"
class="form-control"
placeholder="Add a comment... Use @ to mention someone"
rows="3"
style="background: var(--bg-secondary); border: 1px solid var(--border-color); color: var(--text-primary); border-radius: 8px; padding: 12px; width: 100%; resize: none;"
@@ -395,7 +537,7 @@
<a href="{{ route('login') }}" style="color: var(--brand-red);">Sign in</a> to comment
</div>
@endauth
<div id="commentsList">
@forelse($video->comments()->whereNull('parent_id')->with('user', 'replies.user')->latest()->get() as $comment)
@include('videos.partials.comment', ['comment' => $comment])
@@ -404,12 +546,12 @@
@endforelse
</div>
</div>
<script>
function submitComment(videoId) {
const body = document.getElementById('commentBody').value.trim();
if (!body) return;
fetch(`/videos/${videoId}/comments`, {
method: 'POST',
headers: {
@@ -426,10 +568,10 @@
}
});
}
function deleteComment(commentId) {
if (!confirm('Are you sure you want to delete this comment?')) return;
fetch(`/comments/${commentId}`, {
method: 'DELETE',
headers: {
@@ -443,7 +585,7 @@
}
});
}
document.addEventListener('DOMContentLoaded', function() {
const commentTexts = document.querySelectorAll('.comment-body');
commentTexts.forEach(text => {
@@ -451,12 +593,12 @@
text.innerHTML = html;
});
});
function submitReply(videoId, parentId) {
const textarea = document.querySelector(`#replyForm${parentId} textarea`);
const body = textarea.value.trim();
if (!body) return;
fetch(`/videos/${videoId}/comments`, {
method: 'POST',
headers: {
@@ -474,18 +616,104 @@
}
</script>
</div>
<!-- Sidebar -->
<!-- Sidebar - Up Next / Recommendations -->
<div class="yt-sidebar-container">
<h3 style="font-size: 16px; font-weight: 500; margin-bottom: 12px;">Up Next</h3>
<div class="text-secondary">More videos coming soon...</div>
@if($playlist && $playlistVideos && $playlistVideos->count() > 0)
<h3 style="font-size: 16px; font-weight: 500; margin-bottom: 12px;">
<i class="bi bi-collection-play" style="margin-right: 8px;"></i>
{{ $playlist->name }}
<span style="font-weight: 400; color: var(--text-secondary); font-size: 14px;">({{ $playlistVideos->count() }} videos)</span>
</h3>
<div class="recommended-videos-list">
@foreach($playlistVideos as $index => $playlistVideo)
@if($playlistVideo->id !== $video->id)
<div class="sidebar-video-card{{ $playlistVideo->id === $video->id ? ' current-video' : '' }}"
onclick="window.location.href='{{ route('videos.show', $playlistVideo->id) }}?playlist={{ $playlist->id }}'">
<div class="sidebar-thumb" style="position: relative;">
@if ($playlistVideo->thumbnail)
<img src="{{ asset('storage/thumbnails/' . $playlistVideo->thumbnail) }}" alt="{{ $playlistVideo->title }}">
@else
<img src="https://picsum.photos/seed/{{ $playlistVideo->id }}/320/180" alt="{{ $playlistVideo->title }}">
@endif
@if ($playlistVideo->duration)
<span class="yt-video-duration">{{ gmdate('i:s', $playlistVideo->duration) }}</span>
@endif
@if ($playlistVideo->is_shorts)
<span class="yt-shorts-badge" style="position: absolute; top: 8px; left: 8px; font-size: 10px; padding: 2px 6px;">
<i class="bi bi-collection-play-fill"></i> SHORTS
</span>
@endif
<!-- Playlist position indicator -->
<span style="position: absolute; bottom: 4px; left: 4px; background: rgba(0,0,0,0.8); color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px; font-weight: 500;">
{{ $index + 1 }}
</span>
</div>
<div class="sidebar-info">
<div class="sidebar-title">
<i class="bi {{ match ($playlistVideo->type) {'music' => 'bi-music-note','match' => 'bi-trophy',default => 'bi-film'} }}"
style="color: #ef4444; margin-right: 4px; font-size: 12px;"></i>
{{ Str::limit($playlistVideo->title, 60) }}
</div>
<div class="sidebar-meta">
<div>{{ $playlistVideo->user->name ?? 'Unknown' }}</div>
<div>{{ number_format($playlistVideo->view_count) }} views • {{ $playlistVideo->created_at->diffForHumans() }}</div>
</div>
</div>
</div>
@endif
@endforeach
</div>
@if($playlist->canEdit(Auth::user()))
<a href="{{ route('playlists.show', $playlist->id) }}" class="yt-action-btn" style="margin-top: 12px; display: inline-block;">
<i class="bi bi-pencil"></i> Edit Playlist
</a>
@endif
@else
<h3 style="font-size: 16px; font-weight: 500; margin-bottom: 12px;">Up Next</h3>
@if($recommendedVideos && $recommendedVideos->count() > 0)
<div class="recommended-videos-list">
@foreach($recommendedVideos as $recVideo)
<div class="sidebar-video-card" onclick="window.location.href='{{ route('videos.show', $recVideo->id) }}'">
<div class="sidebar-thumb">
@if($recVideo->thumbnail)
<img src="{{ asset('storage/thumbnails/' . $recVideo->thumbnail) }}" alt="{{ $recVideo->title }}">
@else
<img src="https://picsum.photos/seed/{{ $recVideo->id }}/320/180" alt="{{ $recVideo->title }}">
@endif
@if($recVideo->duration)
<span class="yt-video-duration">{{ gmdate('i:s', $recVideo->duration) }}</span>
@endif
@if($recVideo->is_shorts)
<span class="yt-shorts-badge" style="position: absolute; top: 8px; left: 8px; font-size: 10px; padding: 2px 6px;">
<i class="bi bi-collection-play-fill"></i> SHORTS
</span>
@endif
</div>
<div class="sidebar-info">
<div class="sidebar-title">
<i class="bi {{ match($recVideo->type) { 'music' => 'bi-music-note', 'match' => 'bi-trophy', default => 'bi-film' } }}" style="color: #ef4444; margin-right: 4px; font-size: 12px;"></i>
{{ Str::limit($recVideo->title, 60) }}
</div>
<div class="sidebar-meta">
<div>{{ $recVideo->user->name ?? 'Unknown' }}</div>
<div>{{ number_format($recVideo->view_count) }} views • {{ $recVideo->created_at->diffForHumans() }}</div>
</div>
</div>
</div>
@endforeach
</div>
@else
<div class="text-secondary">No recommendations available yet. Check back later!</div>
@endif
@endif
</div>
</div>
@include('layouts.partials.share-modal')
@include('layouts.partials.edit-video-modal')
@if(Session::has('openEditModal') && Session::get('openEditModal'))
@auth
<script>
@@ -495,17 +723,82 @@
</script>
@endauth
@endif
<script>
// Playlist navigation URLs
const previousVideoUrl = @json($previousVideo ? route('videos.show', $previousVideo->id) . '?playlist=' . $playlist->id : null);
const nextVideoUrl = @json($nextVideo ? route('videos.show', $nextVideo->id) . '?playlist=' . $playlist->id : null);
// Autoplay management
let autoplayEnabled = true;
document.addEventListener('DOMContentLoaded', function() {
var videoPlayer = document.getElementById('videoPlayer');
var playlistControls = document.getElementById('playlistControls');
var autoplayToggle = document.getElementById('autoplayToggle');
if (videoPlayer) {
videoPlayer.volume = 0.5;
var playPromise = videoPlayer.play();
if (playPromise !== undefined) {
playPromise.then(function() { console.log('Video autoplayed'); }).catch(function(error) { console.log('Autoplay blocked'); });
}
// Handle video ended - autoplay next
if (nextVideoUrl) {
videoPlayer.addEventListener('ended', function() {
if (autoplayEnabled) {
console.log('Video ended, going to next...');
window.location.href = nextVideoUrl;
}
});
}
}
// Show playlist controls on hover
if (playlistControls) {
const videoContainer = document.getElementById('videoContainer');
videoContainer.addEventListener('mouseenter', function() {
playlistControls.classList.add('visible');
});
videoContainer.addEventListener('mouseleave', function() {
playlistControls.classList.remove('visible');
});
// Initially show for a few seconds
setTimeout(function() { playlistControls.classList.add('visible'); }, 2000);
setTimeout(function() { playlistControls.classList.remove('visible'); }, 6000);
}
// Autoplay toggle handler
if (autoplayToggle) {
autoplayToggle.addEventListener('change', function() {
autoplayEnabled = this.checked;
localStorage.setItem('playlistAutoplay', autoplayEnabled ? '1' : '0');
});
// Load saved preference
const savedAutoplay = localStorage.getItem('playlistAutoplay');
if (savedAutoplay !== null) {
autoplayEnabled = savedAutoplay === '1';
autoplayToggle.checked = autoplayEnabled;
}
}
// Keyboard navigation
document.addEventListener('keydown', function(e) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { return; }
if (e.key === 'ArrowLeft') { e.preventDefault(); goToPreviousVideo(); }
else if (e.key === 'ArrowRight') { e.preventDefault(); goToNextVideo(); }
});
});
function goToPreviousVideo() {
if (previousVideoUrl) { window.location.href = previousVideoUrl; }
}
function goToNextVideo() {
if (nextVideoUrl) { window.location.href = nextVideoUrl; }
}
</script>
@endsection
+53 -129
View File
@@ -1,133 +1,57 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@extends('layouts.app')
<title>Laravel</title>
@section('title', 'Welcome - ' . config('app.name'))
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,600&display=swap" rel="stylesheet" />
<!-- Styles -->
<style>
/* ! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com */*,::after,::before{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}::after,::before{--tw-content:''}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;font-family:Figtree, sans-serif;font-feature-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*, ::before, ::after{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgb(59 130 246 / 0.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::-webkit-backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgb(59 130 246 / 0.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgb(59 130 246 / 0.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.relative{position:relative}.mx-auto{margin-left:a Line truncated
</style>
</head>
<body class="antialiased">
<div class="relative sm:flex sm:justify-center sm:items-center min-h-screen bg-dots-darker bg-center bg-gray-100 dark:bg-dots-lighter dark:bg-gray-900 selection:bg-red-500 selection:text-white">
@if (Route::has('login'))
<div class="sm:fixed sm:top-0 sm:right-0 p-6 text-right z-10">
@auth
<a href="{{ url('/home') }}" class="font-semibold text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Home</a>
@else
<a href="{{ route('login') }}" class="font-semibold text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Log in</a>
@if (Route::has('register'))
<a href="{{ route('register') }}" class="ml-4 font-semibold text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Register</a>
@endif
@endauth
</div>
@endif
<div class="max-w-7xl mx-auto p-6 lg:p-8">
<div class="flex justify-center">
<svg viewBox="0 0 62 65" fill="none" xmlns="http://www.w3.org/2000/svg" class="h-16 w-auto bg-gray-100 dark:bg-gray-900">
<path d="M61.8548 14.6253C61.8778 14.7102 61.8895 14.7978 61.8897 14.8858V28.5615C61.8898 28.737 61.8434 28.9095 61.7554 29.0614C61.6675 29.2132 61.5409 29.3392 61.3887 29.4265L49.9104 36.0351V49.1337C49.9104 49.4902 49.7209 49.8192 49.4118 49.9987L25.4519 63.7916C25.3971 63.8227 25.3372 63.8427 25.2774 63.8639C25.255 63.8714 25.2338 63.8851 25.2101 63.8913C25.0426 63.9354 24.8666 63.9354 24.6991 63.8913C24.6716 63.8838 24.6467 63.8689 24.6205 63.8589C24.5657 63.8389 24.5084 63.8215 24.456 63.7916L0.501061 49.9987C0.348882 49.9113 0.222437 49.7853 0.134469 49.6334C0.0465019 49.4816 0.000120578 49.3092 0 49.1337L0 8.10652C0 8.01678 0.0124642 7.92953 0.0348998 7.84477C0.0423783 7.8161 0.0598282 7.78993 0.0697995 7.76126C0.0884958 7.70891 0.105946 7.65531 0.133367 7.6067C0.152063 7.5743 0.179485 7.54812 0.20192 7.51821C0.230588 7.47832 0.256763 7.43719 0.290416 7.40229C0.319084 7.37362 0.356476 7.35243 0.388883 7.32751C0.425029 7.29759 0.457436 7.26518 0.498568 7.2415L12.4779 0.345059C12.6296 0.257786 12.8015 0.211853 12.9765 0.211853C13.1515 0.211853 13.3234 0.257786 13.475 0.345059L25.4531 7.2415H25.4556C25.4955 7.26643 25.5292 7.29759 25.5653 7.32626C25.5977 7.35119 25.6339 7.37362 25.6625 7.40104C25.6974 7.43719 25.7224 7.47832 25.7523 7.51821C25.7735 7.54812 25.8021 7.5743 25.8196 7.6067C25.8483 7.65656 25.8645 7.70891 25.8844 7.76126C25.8944 7.78993 25.9118 7.8161 25.9193 7.84602C25.9423 7.93096 25.954 8.01853 25.9542 8.10652V33.7317L35.9355 27.9844V14.8846C35.9355 14.7973 35.948 14.7088 35.9704 14.6253C35.9792 14.5954 35.9954 14.5692 36.0053 14.5405C36.0253 14.4882 36.0427 14.4346 36.0702 14.386C36.0888 14.3536 36.1163 14.3274 36.1375 14.2975C36.1674 14.2576 36.1923 14.2165 36.2272 14.1816C36.2559 14.1529 36.292 14.1317 36.3244 14.1068C36.3618 14.0769 36.3942 14.0445 36.4341 14.0208L48.4147 7.12434C48.5663 7.03694 48.7383 6.99094 48.9133 6.99094C49.0883 6.99094 49.2602 7.03694 49.4118 7.12434L61.3899 14.0208C61.4323 14.0457 61.4647 14.0769 61.5021 14.1055C61.5333 14.1305 61.5694 14.1529 61.5981 14.1803C61.633 14.2165 61.6579 14.2576 61.6878 14.2975C61.7103 14.3274 61.7377 14.3536 61.7551 14.386C61.7838 14.4346 61.8 14.4882 61.8199 14.5405C61.8312 14.5692 61.8474 14.5954 61.8548 14.6253ZM59.893 27.9844V16.6121L55.7013 19.0252L49.9104 22.3593V33.7317L59.8942 27.9844H59.893ZM47.9149 48.5566V37.1768L42.2187 40.4299L25.953 49.7133V61.2003L47.9149 48.5566ZM1.99677 9.83281V48.5566L23.9562 61.199V49.7145L12.4841 43.2219L12.4804 43.2194L12.4754 43.2169C12.4368 43.1945 12.4044 43.1621 12.3682 43.1347C12.3371 43.1097 12.3009 43.0898 12.2735 43.0624L12.271 43.0586C12.2386 43.0275 12.2162 42.9888 12.1887 42.9539C12.1638 42.9203 12.1339 42.8916 12.114 42.8567L12.1127 42.853C12.0903 42.8156 12.0766 42.7707 12.0604 42.7283C12.0442 42.6909 12.023 42.656 12.013 42.6161C12.0005 42.5688 11.998 42.5177 11.9931 42.4691C11.9881 42.4317 11.9781 42.3943 11.9781 42.3569V15.5801L6.18848 12.2446L1.99677 9.83281ZM12.9777 2.36177L2.99764 8.10652L12.9752 13.8513L22.9541 8.10527L12.9752 2.36177H12.9777ZM18.1678 38.2138L23.9574 34.8809V9.83281L19.7657 12.2459L13.9749 15.5801V40.6281L18.1678 38.2138ZM48.9133 9.14105L38.9344 14.8858L48.9133 20.6305L58.8909 14.8846L48.9133 9.14105ZM47.9149 22.3593L42.124 19.0252L37.9323 16.6121V27.9844L43.7219 31.3174L47.9149 33.7317V22.3593ZM24.9533 47.987L39.59 39.631L46.9065 35.4555L36.9352 29.7145L25.4544 36.3242L14.9907 42.3482L24.9533 47.987Z" fill="#FF2D20"/>
</svg>
</div>
<div class="mt-16">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8">
<a href="https://laravel.com/docs" class="scale-100 p-6 bg-white dark:bg-gray-800/50 dark:bg-gradient-to-bl from-gray-700/50 via-transparent dark:ring-1 dark:ring-inset dark:ring-white/5 rounded-lg shadow-2xl shadow-gray-500/20 dark:shadow-none flex motion-safe:hover:scale-[1.01] transition-all duration-250 focus:outline focus:outline-2 focus:outline-red-500">
<div>
<div class="h-16 w-16 bg-red-50 dark:bg-red-800/20 flex items-center justify-center rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" class="w-7 h-7 stroke-red-500">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25" />
</svg>
</div>
<h2 class="mt-6 text-xl font-semibold text-gray-900 dark:text-white">Documentation</h2>
<p class="mt-4 text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
Laravel has wonderful documentation covering every aspect of the framework. Whether you are a newcomer or have prior experience with Laravel, we recommend reading our documentation from beginning to end.
</p>
</div>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" class="self-center shrink-0 stroke-red-500 w-6 h-6 mx-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75" />
</svg>
</a>
<a href="https://laracasts.com" class="scale-100 p-6 bg-white dark:bg-gray-800/50 dark:bg-gradient-to-bl from-gray-700/50 via-transparent dark:ring-1 dark:ring-inset dark:ring-white/5 rounded-lg shadow-2xl shadow-gray-500/20 dark:shadow-none flex motion-safe:hover:scale-[1.01] transition-all duration-250 focus:outline focus:outline-2 focus:outline-red-500">
<div>
<div class="h-16 w-16 bg-red-50 dark:bg-red-800/20 flex items-center justify-center rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" class="w-7 h-7 stroke-red-500">
<path stroke-linecap="round" d="M15.75 10.5l4.72-4.72a.75.75 0 011.28.53v11.38a.75.75 0 01-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 002.25-2.25v-9a2.25 2.25 0 00-2.25-2.25h-9A2.25 2.25 0 002.25 7.5v9a2.25 2.25 0 002.25 2.25z" />
</svg>
</div>
<h2 class="mt-6 text-xl font-semibold text-gray-900 dark:text-white">Laracasts</h2>
<p class="mt-4 text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
Laracasts offers thousands of video tutorials on Laravel, PHP, and JavaScript development. Check them out, see for yourself, and massively level up your development skills in the process.
</p>
</div>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" class="self-center shrink-0 stroke-red-500 w-6 h-6 mx-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75" />
</svg>
</a>
<a href="https://laravel-news.com" class="scale-100 p-6 bg-white dark:bg-gray-800/50 dark:bg-gradient-to-bl from-gray-700/50 via-transparent dark:ring-1 dark:ring-inset dark:ring-white/5 rounded-lg shadow-2xl shadow-gray-500/20 dark:shadow-none flex motion-safe:hover:scale-[1.01] transition-all duration-250 focus:outline focus:outline-2 focus:outline-red-500">
<div>
<div class="h-16 w-16 bg-red-50 dark:bg-red-800/20 flex items-center justify-center rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" class="w-7 h-7 stroke-red-500">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 7.5h1.5m-1.5 3h1.5m-7.5 3h7.5m-7.5 3h7.5m3-9h3.375c.621 0 1.125.504 1.125 1.125V18a2.25 2.25 0 01-2.25 2.25M16.5 7.5V18a2.25 2.25 0 002.25 2.25M16.5 7.5V4.875c0-.621-.504-1.125-1.125-1.125H4.125C3.504 3.75 3 4.254 3 4.875V18a2.25 2.25 0 002.25 2.25h13.5M6 7.5h3v3H6v-3z" />
</svg>
</div>
<h2 class="mt-6 text-xl font-semibold text-gray-900 dark:text-white">Laravel News</h2>
<p class="mt-4 text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
Laravel News is a community driven portal and newsletter aggregating all of the latest and most important news in the Laravel ecosystem, including new package releases and tutorials.
</p>
</div>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" class="self-center shrink-0 stroke-red-500 w-6 h-6 mx-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75" />
</svg>
</a>
<div class="scale-100 p-6 bg-white dark:bg-gray-800/50 dark:bg-gradient-to-bl from-gray-700/50 via-transparent dark:ring-1 dark:ring-inset dark:ring-white/5 rounded-lg shadow-2xl shadow-gray-500/20 dark:shadow-none flex motion-safe:hover:scale-[1.01] transition-all duration-250 focus:outline focus:outline-2 focus:outline-red-500">
<div>
<div class="h-16 w-16 bg-red-50 dark:bg-red-800/20 flex items-center justify-center rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" class="w-7 h-7 stroke-red-500">
<path stroke-linecap="round" stroke-linejoin="round" d="M6.115 5.19l.319 1.913A6 6 0 008.11 10.36L9.75 12l-.387.775c-.217.433-.132.956.21 1.298l1.348 1.348c.21.21.329.497.329.795v1.089c0 .426.24.815.622 1.006l.153.076c.433.217.956.132 1.298-.21l.723-.723a8.7 8.7 0 002.288-4.042 1.087 1.087 0 00-.358-1.099l-1.33-1.108c-.251-.21-.582-.299-.905-.245l-1.17.195a1.125 1.125 0 01-.98-.314l-.295-.295a1.125 1.125 0 010-1.591l.13-.132a1.125 1.125 0 011.3-.21l.603.302a.809.809 0 001.086-1.086L14.25 7.5l1.256-.837a4.5 4.5 0 001.528-1.732l.146-.292M6.115 5.19A9 9 0 1017.18 4.64M6.115 5.19A8.965 8.965 0 0112 3c1.929 0 3.716.607 5.18 1.64" />
</svg>
</div>
<h2 class="mt-6 text-xl font-semibold text-gray-900 dark:text-white">Vibrant Ecosystem</h2>
<p class="mt-4 text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
Laravel's robust library of first-party tools and libraries, such as <a href="https://forge.laravel.com" class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Forge</a>, <a href="https://vapor.laravel.com" class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Vapor</a>, <a href="https://nova.laravel.com" class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Nova</a>, and <a href="https://envoyer.io" class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Envoyer</a> help you take your projects to the next level. Pair them with powerful open source libraries like <a href="https://laravel.com/docs/billing" class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Cashier</a>, <a href="https://laravel.com/docs/dusk" class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Dusk</a>, <a href="https://laravel.com/docs/broadcasting" class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Echo</a>, <a href="https://laravel.com/docs/horizon" class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Horizon</a>, <a href="https://laravel.com/docs/sanctum" class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Sanctum</a>, <a href="https://laravel.com/docs/telescope" class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Telescope</a>, and more.
</p>
</div>
</div>
</div>
</div>
<div class="flex justify-center mt-16 px-0 sm:items-center sm:justify-between">
<div class="text-center text-sm sm:text-left">
&nbsp;
</div>
<div class="text-center text-sm text-gray-500 dark:text-gray-400 sm:text-right sm:ml-0">
Laravel v{{ Illuminate\Foundation\Application::VERSION }} (PHP v{{ PHP_VERSION }})
</div>
</div>
@section('content')
<div class="welcome-container">
<div class="text-center mb-5">
<img src="{{ asset('storage/images/fullLogo.png') }}" alt="{{ config('app.name') }}" style="height: 60px;" class="mb-4">
<h1 class="mb-3">Welcome to {{ config('app.name') }}</h1>
<p class="text-secondary">Discover, watch, and share videos</p>
</div>
<div class="row justify-content-center">
<div class="col-md-8">
<div class="d-grid gap-3">
@auth
<a href="{{ route('videos.index') }}" class="btn btn-lg" style="background: var(--brand-red); color: white;">
<i class="bi bi-play-circle-fill me-2"></i>
Browse Videos
</a>
@else
<a href="{{ route('login') }}" class="btn btn-lg" style="background: var(--brand-red); color: white;">
<i class="bi bi-box-arrow-in-right me-2"></i>
Sign In
</a>
<a href="{{ route('register') }}" class="btn btn-lg btn-outline-light">
<i class="bi bi-person-plus me-2"></i>
Create Account
</a>
@endauth
<a href="{{ route('videos.trending') }}" class="btn btn-lg btn-outline-light">
<i class="bi bi-fire me-2"></i>
Trending Videos
</a>
</div>
</div>
</body>
</html>
</div>
<div class="text-center mt-5 pt-4 border-top" style="border-color: var(--border-color) !important;">
<p class="text-secondary mb-2">Explore our features</p>
<div class="d-flex justify-content-center gap-4 flex-wrap">
<div class="text-center">
<i class="bi bi-play-btn-fill fs-2" style="color: var(--brand-red);"></i>
<p class="mt-2 mb-0">Watch Videos</p>
</div>
<div class="text-center">
<i class="bi bi-cloud-upload fs-2" style="color: var(--brand-red);"></i>
<p class="mt-2 mb-0">Upload Content</p>
</div>
<div class="text-center">
<i class="bi bi-people fs-2" style="color: var(--brand-red);"></i>
<p class="mt-2 mb-0">Build Community</p>
</div>
</div>
</div>
</div>
@endsection