606 lines
24 KiB
PHP

@extends('layouts.app')
@push('head')
<!-- Open Graph / WhatsApp / Facebook / Twitter Preview -->
<meta property="og:title" content="{{ $video->title }}">
<meta property="og:description" content="{{ $video->description ? Str::limit($video->description, 200) : 'Check out this video on ' . config('app.name') }}">
<meta property="og:image" content="{{ $video->thumbnail_url }}">
<meta property="og:url" content="{{ $video->share_url }}">
<meta property="og:type" content="video.other">
<meta property="og:site_name" content="{{ config('app.name') }}">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ $video->title }}">
<meta name="twitter:description" content="{{ $video->description ? Str::limit($video->description, 200) : 'Check out this video on ' . config('app.name') }}">
<meta name="twitter:image" content="{{ $video->thumbnail_url }}">
@endpush
@section('title', $video->title . ' | ' . config('app.name'))
@section('extra_styles')
<style>
/* Video Section */
.yt-video-section { flex: 1; min-width: 0; }
/* Video Player */
.video-container {
position: relative;
aspect-ratio: 16/9;
background: #000;
border-radius: 12px;
overflow: hidden;
max-height: 70vh;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
}
.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; }
/* Video Info */
.video-title {
font-size: 20px;
font-weight: 500;
margin: 16px 0 8px;
line-height: 1.3;
}
.video-stats-row {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 12px;
border-bottom: 1px solid var(--border-color);
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;
gap: 8px;
padding: 8px 16px;
border-radius: 20px;
border: none;
background: var(--bg-secondary);
color: var(--text-primary);
cursor: pointer;
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;
align-items: center;
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;
border: none;
padding: 8px 16px;
border-radius: 18px;
font-weight: 600;
font-size: 14px;
cursor: pointer;
white-space: nowrap;
}
/* Description */
.video-description {
background: var(--bg-secondary);
border-radius: 12px;
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;
border-radius: 8px;
overflow: hidden;
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;
margin-bottom: 4px;
display: -webkit-box;
-webkit-line-clamp: 2;
-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 - Stack vertically on tablet/mobile */
.video-layout-container {
flex-direction: column !important;
}
.yt-video-section {
width: 100% !important;
flex: none !important;
}
.yt-sidebar-container {
width: 100% !important;
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; }
/* Mobile video player fixes */
.video-container {
max-height: 50vh !important;
border-radius: 0 !important;
}
.video-container video {
object-fit: contain !important;
}
.video-title {
font-size: 16px !important;
margin: 12px 0 6px !important;
}
.channel-row {
flex-direction: column;
align-items: flex-start !important;
gap: 12px;
}
.channel-info {
width: 100%;
}
.subscribe-btn {
width: 100%;
}
.video-description {
padding: 12px !important;
}
}
</style>
@endsection
@section('content')
<!-- Video Layout Container -->
<div class="video-layout-container" style="display: flex; gap: 24px; max-width: 1800px; margin: 0 auto;">
<!-- Video Section -->
<div class="yt-video-section">
<!-- Video Player -->
<div class="video-container @if($video->orientation === 'portrait') portrait @elseif($video->orientation === 'square') square @elseif($video->orientation === 'ultrawide') ultrawide @endif" id="videoContainer">
<video id="videoPlayer" controls playsinline preload="metadata" autoplay>
<source src="{{ route('videos.stream', $video->id) }}" type="video/mp4">
</video>
</div>
@php
$typeIcon = match($video->type) {
'music' => 'bi-music-note',
'match' => 'bi-trophy',
default => 'bi-film',
};
@endphp
<!-- Video Title with Type Icon -->
<h1 class="video-title" style="display: flex; align-items: center; gap: 10px;">
<i class="bi {{ $typeIcon }}" style="color: #ef4444;"></i>
<span>{{ $video->title }}</span>
</h1>
<!-- Stats Row -->
<div class="video-stats-row">
<div class="video-stats-left">
<span>{{ number_format($video->view_count) }} views</span>
<span></span>
<span>{{ $video->created_at->format('M d, Y') }}</span>
</div>
<div class="video-actions">
@auth
<!-- Like Button -->
<form method="POST" action="{{ $video->isLikedBy(Auth::user()) ? route('videos.unlike', $video->id) : route('videos.like', $video->id) }}" class="d-inline">
@csrf
<button type="submit" class="yt-action-btn {{ $video->isLikedBy(Auth::user()) ? 'liked' : '' }}">
<i class="bi {{ $video->isLikedBy(Auth::user()) ? 'bi-hand-thumbs-up-fill' : 'bi-hand-thumbs-up' }}"></i>
{{ $video->like_count > 0 ? number_format($video->like_count) : 'Like' }}
</button>
</form>
<!-- Edit Button - Only for video owner -->
@if(Auth::id() === $video->user_id)
<button class="yt-action-btn" onclick="openEditVideoModal({{ $video->id }})">
<i class="bi bi-pencil"></i> Edit
</button>
@endif
@else
<a href="{{ route('login') }}" class="yt-action-btn">
<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</button>
@endif
</div>
</div>
<!-- Channel Row -->
<div class="channel-row" style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px;">
<div style="display: flex; align-items: center; gap: 12px;">
<a href="{{ route('channel', $video->user_id) }}" class="channel-info text-decoration-none" style="color: inherit; display: flex; align-items: center; gap: 12px;">
@if($video->user)
<img src="{{ $video->user->avatar_url }}" class="channel-avatar" style="width: 36px; height: 36px;" alt="{{ $video->user->name }}">
@else
<div class="channel-avatar" style="width: 36px; height: 36px;"></div>
@endif
<div>
<div class="channel-name">{{ $video->user->name ?? 'Unknown' }}</div>
<div class="channel-subs">{{ number_format($video->user->subscriber_count ?? 0) }} subscribers</div>
</div>
</a>
{{-- Subscribe Button --}}
@auth
@if(Auth::id() !== $video->user_id)
<button class="subscribe-btn">Subscribe</button>
@endif
@else
<a href="{{ route('login') }}" class="subscribe-btn">Subscribe</a>
@endauth
</div>
</div>
<!-- Description -->
@if($video->description)
@php
$fullDescription = $video->description;
$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-top: 12px;">
<div class="description-stats" style="font-size: 14px; font-weight: 500; margin-bottom: 8px;">
<span>{{ number_format($video->view_count) }} views</span>
<span></span>
<span>{{ $video->created_at->format('M d, Y') }}</span>
</div>
<div style="border-bottom: 1px solid var(--border-color); margin: 8px 0;"></div>
<div class="description-content">
@if($needsExpand)
<div class="description-short">
<span class="description-text">{!! Str::markdown($shortDescription) !!}</span>
<span style="color: var(--text-secondary);">... </span>
</div>
<div class="description-full" style="display: none;">
<span class="description-text">{!! Str::markdown($fullDescription) !!}</span>
</div>
<button onclick="toggleDescription()" style="background: none; border: none; color: var(--text-primary); font-weight: 600; font-size: 14px; cursor: pointer; padding: 0; margin-top: 4px;">Show more</button>
@else
<span class="description-text">{!! Str::markdown($fullDescription) !!}</span>
@endif
</div>
</div>
<script>
function toggleDescription() {
const short = document.querySelector('.description-short');
const full = document.querySelector('.description-full');
const btn = document.querySelector('.video-description-box button');
if (short.style.display !== 'none') {
short.style.display = 'none';
full.style.display = 'block';
btn.textContent = 'Show less';
} else {
short.style.display = 'block';
full.style.display = 'none';
btn.textContent = 'Show more';
}
}
</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
<!-- Comments Section -->
<div class="comments-section" style="margin-top: 24px; padding-top: 16: 1px solid var(--borderpx; border-top-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" 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;"></textarea>
<div style="display: flex; gap: 8px; margin-top: 8px; justify-content: flex-end;">
<button type="button" class="yt-action-btn" onclick="document.getElementById('commentBody').value = ''">Cancel</button>
<button type="button" class="yt-action-btn" style="background: var(--brand-red); color: white;" onclick="submitComment({{ $video->id }})">Comment</button>
</div>
</div>
</div>
@else
<div style="margin-bottom: 24px; padding: 16px; background: var(--bg-secondary); border-radius: 8px; text-align: center;">
<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])
@empty
<p style="color: var(--text-secondary); text-align: center; padding: 20px;">No comments yet. Be the first to comment!</p>
@endforelse
</div>
</div>
<script>
function submitComment(videoId) {
const body = document.getElementById('commentBody').value.trim();
if (!body) return;
fetch(`/videos/${videoId}/comments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
body: JSON.stringify({ body: body })
}).then(r => r.json()).then(data => {
if (data.success) { document.getElementById('commentBody').value = ''; location.reload(); }
});
}
function deleteComment(commentId) {
if (!confirm('Delete this comment?')) return;
fetch(`/comments/${commentId}`, { method: 'DELETE', headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' }})
.then(r => r.json()).then(data => { if (data.success) location.reload(); });
}
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.comment-body').forEach(text => {
text.innerHTML = text.innerHTML.replace(/@(\w+)/g, '<span style="color: #3ea6ff; font-weight: 500;">@$1</span>');
});
});
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: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
body: JSON.stringify({ body: body, parent_id: parentId })
}).then(r => r.json()).then(data => { if (data.success) location.reload(); });
}
</script>
</div>
<!-- Sidebar -->
<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>
</div>
</div>
<!-- Mobile Bottom Action Bar -->
<div class="mobile-bottom-bar">
@auth
<form method="POST" action="{{ $video->isLikedBy(Auth::user()) ? route('videos.unlike', $video->id) : route('videos.like', $video->id) }}" class="d-inline" style="flex:1;">
@csrf
<button type="submit" class="yt-action-btn {{ $video->isLikedBy(Auth::user()) ? 'liked' : '' }}" style="width:100%;">
<i class="bi {{ $video->isLikedBy(Auth::user()) ? 'bi-hand-thumbs-up-fill' : 'bi-hand-thumbs-up' }}"></i>
<span>{{ $video->like_count > 0 ? number_format($video->like_count) : 'Like' }}</span>
</button>
</form>
@else
<a href="{{ route('login') }}" class="yt-action-btn" style="flex:1;text-align:center;">
<i class="bi bi-hand-thumbs-up"></i><span>Like</span>
</a>
@endauth
@if($video->isShareable())
<button class="yt-action-btn" onclick="openShareModal('{{ $video->share_url }}', '{{ addslashes($video->title) }}')" style="flex:1;">
<i class="bi bi-share"></i><span>Share</span>
</button>
@endif
@auth
@if(Auth::id() !== $video->user_id)
<button class="yt-action-btn" style="flex:1;background:var(--brand-red);color:white;">
<i class="bi bi-bell"></i><span>Subscribe</span>
</button>
@endif
@else
<a href="{{ route('login') }}" class="yt-action-btn" style="flex:1;background:var(--brand-red);color:white;text-align:center;">
<i class="bi bi-bell"></i><span>Subscribe</span>
</a>
@endauth
</div>
@include('layouts.partials.share-modal')
@include('layouts.partials.edit-video-modal')
@if(Session::has('openEditModal') && Session::get('openEditModal'))
@auth
<script>
document.addEventListener('DOMContentLoaded', function() {
openEditVideoModal({{ $video->id }});
});
</script>
@endauth
@endif
<script>
document.addEventListener('DOMContentLoaded', function() {
var videoPlayer = document.getElementById('videoPlayer');
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'); });
}
}
});
</script>
@endsection
/* Mobile Bottom Action Bar */
@media (max-width: 576px) {
.mobile-bottom-bar {
display: flex !important;
}
.desktop-actions {
display: none;
}
}
.mobile-bottom-bar {
display: none;
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: var(--bg-primary);
border-top: 1px solid var(--border-color);
padding: 12px 16px;
justify-content: space-around;
z-index: 1000;
gap: 8px;
box-shadow: 0 -2px 10px rgba(0,0,0,0.3);
}
.mobile-bottom-bar .yt-action-btn {
flex: 1;
justify-content: center;
padding: 12px 8px;
font-size: 12px;
min-height: 44px;
}
.mobile-bottom-bar .yt-action-btn i {
font-size: 18px;
}
@media (max-width: 576px) {
.yt-video-section {
padding-bottom: 70px;
}
}
<!-- Extra Mobile Styles -->
<style>
@media (max-width: 400px) {
.video-layout-container {
padding: 0 !important;
}
.video-title {
font-size: 15px !important;
padding: 0 4px;
}
.video-stats-left {
font-size: 12px;
}
.video-description-box {
margin: 12px 4px;
padding: 10px !important;
}
.comments-section {
margin-top: 16px;
padding: 12px 4px;
}
.comment-form {
flex-direction: column;
}
.comment-form > img {
display: none;
}
}
</style>