latest update

This commit is contained in:
ghassan committed 2026-04-05 03:30:22 +03:00
1 parent 64eadfaf56
commit 2a562b99f1
25 files changed
+2617 -2337

No files matched your search

+21 -297
View File
@@ -1,7 +1,6 @@
@extends('layouts.app')
@section('title', isset($query) ? 'Search: ' . $query . ' | ' . config('app.name') : 'Video Gallery | ' .
config('app.name'))
@section('title', isset($query) ? 'Search: ' . $query . ' | ' . config('app.name') : 'Video Gallery | ' . config('app.name'))
@section('extra_styles')
<style>
@@ -30,177 +29,11 @@
gap: 24px;
}
.yt-video-card {
cursor: pointer;
}
@media (max-width: 992px) { .yt-video-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 576px) { .yt-video-grid { grid-template-columns: 1fr; } }
.yt-video-thumb {
position: relative;
aspect-ratio: 16/9;
border-radius: 12px;
overflow: hidden;
background: #1a1a1a;
}
.yt-video-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
position: absolute;
top: 0;
left: 0;
}
.yt-video-thumb video {
width: 100%;
height: 100%;
object-fit: contain;
position: absolute;
top: 0;
left: 0;
opacity: 0;
transition: opacity 0.3s ease;
background: #000;
}
.yt-video-thumb video.active {
opacity: 1;
}
.yt-video-duration {
position: absolute;
bottom: 8px;
right: 8px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 3px 6px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.yt-video-info {
display: flex;
margin-top: 12px;
gap: 12px;
}
.yt-channel-icon {
width: 36px;
height: 36px;
border-radius: 50%;
background: #555;
flex-shrink: 0;
}
.yt-video-details {
flex: 1;
}
.yt-video-title {
font-size: 16px;
font-weight: 500;
color: var(--text-primary);
margin: 0 0 4px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
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;
}
/* More button */
.yt-more-btn {
background: transparent;
border: none;
color: var(--text-primary);
cursor: pointer;
padding: 4px;
border-radius: 50%;
}
.yt-more-dropdown {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 8px 0;
min-width: 200px;
}
.yt-more-dropdown-item {
display: flex;
align-items: center;
gap: 16px;
padding: 10px 16px;
color: var(--text-primary);
text-decoration: none;
cursor: pointer;
background: transparent;
border: none;
width: 100%;
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;
}
/* Responsive */
@media (max-width: 1200px) {
.yt-video-grid {
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
}
@media (max-width: 992px) {
.yt-video-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 576px) {
.yt-video-grid {
grid-template-columns: 1fr;
}
.yt-header-right .yt-icon-btn:not(:last-child) {
display: none;
}
}
/* Other styles unchanged */
.yt-empty { text-align: center; padding: 80px 20px; }
</style>
@endsection
@@ -208,131 +41,22 @@
@isset($query)
<div class="search-info">
<h2>Search results for "{{ $query }}"</h2>
<p>{{ $videos->total() }} videos found</p>
<p>{{ $videos->count() }} videos found</p>
</div>
@endif
@endisset
@if ($videos->isEmpty())
<div class="yt-empty">
<i class="bi bi-camera-video yt-empty-icon"></i>
@isset($query)
<h2 class="yt-empty-title">No results found</h2>
<p class="yt-empty-text">Try different keywords or browse all videos.</p>
@else
<h2 class="yt-empty-title">No videos yet</h2>
<p class="yt-empty-text">Be the first to upload a video!</p>
@endisset
@auth
<a href="/videos/create" class="yt-upload-btn" style="display: inline-flex;">
<i class="bi bi-plus-lg"></i>
Upload Video
</a>
@else
<a href="{{ route('login') }}" class="yt-upload-btn" style="display: inline-flex;">
<i class="bi bi-box-arrow-in-right"></i>
Login to Upload
</a>
@endauth
</div>
@else
<div class="yt-video-grid">
@foreach ($videos as $video)
<x-video-card :video="$video" />
@endforeach
</div>
@endif
@include('layouts.partials.share-modal')
@endsection
@section('scripts')
<script>
function playVideo(card) {
const video = card.querySelector('video');
if (video) {
video.currentTime = 0;
video.volume = 0.10; // Set volume to 10%
video.play().catch(function(e) {
// Auto-play may be blocked, ignore error
});
video.classList.add('active');
}
}
function stopVideo(card) {
const video = card.querySelector('video');
if (video) {
video.pause();
video.currentTime = 0;
video.classList.remove('active');
}
}
// Mobile touch support for hover-like behavior
document.addEventListener('touchstart', function(e) {
const card = e.target.closest('.yt-video-card');
if (card) {
// Stop any other playing videos first
document.querySelectorAll('.yt-video-card').forEach(function(otherCard) {
if (otherCard !== card) {
stopVideo(otherCard);
}
});
playVideo(card);
}
}, {
passive: true
});
document.addEventListener('touchend', function(e) {
const card = e.target.closest('.yt-video-card');
if (card) {
stopVideo(card);
}
}, {
passive: true
});
</script>
@if ($videos->isEmpty())
<div class="yt-empty">
<h2>No videos found</h2>
@auth
<a href="{{ route('videos.create') }}" class="btn btn-primary">Upload First Video</a>
@endauth
</div>
@else
<div class="yt-video-grid">
@foreach ($videos as $video)
@include('components.video-card', ['video' => $video])
@endforeach
</div>
@endif
@endsection
<!-- Extra Mobile Styles -->
<style>
@media (max-width: 400px) {
.yt-video-grid {
gap: 12px;
}
.yt-video-thumb {
border-radius: 8px;
}
.yt-video-info {
gap: 8px;
margin-top: 8px;
}
.yt-channel-icon {
width: 28px;
height: 28px;
}
.yt-video-title {
font-size: 13px;
}
.yt-channel-name,
.yt-video-meta {
font-size: 11px;
}
.search-info {
padding: 12px;
}
.search-info h2 {
font-size: 16px;
}
}
</style>
+339
View File
@@ -0,0 +1,339 @@
@extends('layouts.app')
@section('title', isset($query) ? 'Search: ' . $query . ' | ' . config('app.name') : 'Video Gallery | ' .
config('app.name'))
@section('extra_styles')
<style>
/* Search info */
.search-info {
margin-bottom: 20px;
padding: 16px;
background: var(--bg-secondary);
border-radius: 12px;
}
.search-info h2 {
font-size: 20px;
margin: 0;
}
.search-info p {
color: var(--text-secondary);
margin: 8px 0 0;
}
/* Video Grid */
.yt-video-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}
.yt-video-card {
cursor: pointer;
}
.yt-video-thumb {
position: relative;
aspect-ratio: 16/9;
border-radius: 12px;
overflow: hidden;
background: #1a1a1a;
}
.yt-video-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
position: absolute;
top: 0;
left: 0;
}
.yt-video-thumb video {
width: 100%;
height: 100%;
object-fit: contain;
position: absolute;
top: 0;
left: 0;
opacity: 0;
transition: opacity 0.3s ease;
background: #000;
}
.yt-video-thumb video.active {
opacity: 1;
}
.yt-video-duration {
position: absolute;
bottom: 8px;
right: 8px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 3px 6px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.yt-video-info {
display: flex;
margin-top: 12px;
gap: 12px;
}
.yt-channel-icon {
width: 36px;
height: 36px;
border-radius: 50%;
background: #555;
flex-shrink: 0;
}
.yt-video-details {
flex: 1;
}
.yt-video-title {
font-size: 16px;
font-weight: 500;
color: var(--text-primary);
margin: 0 0 4px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
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;
}
/* More button */
.yt-more-btn {
background: transparent;
border: none;
color: var(--text-primary);
cursor: pointer;
padding: 4px;
border-radius: 50%;
}
.yt-more-dropdown {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 8px 0;
min-width: 200px;
}
.yt-more-dropdown-item {
display: flex;
align-items: center;
gap: 16px;
padding: 10px 16px;
color: var(--text-primary);
text-decoration: none;
cursor: pointer;
background: transparent;
border: none;
width: 100%;
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;
}
/* Responsive */
@media (max-width: 1200px) {
.yt-video-grid {
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
}
@media (max-width: 992px) {
.yt-video-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 576px) {
.yt-video-grid {
grid-template-columns: 1fr;
}
.yt-header-right .yt-icon-btn:not(:last-child) {
display: none;
}
}
</style>
@endsection
@section('content')
@isset($query)
<div class="search-info">
<h2>Search results for "{{ $query }}"</h2>
<p>{{ $videos->total() }} videos found</p>
</div>
@endif
@if ($videos->isEmpty())
<div class="yt-empty">
<i class="bi bi-camera-video yt-empty-icon"></i>
@isset($query)
<h2 class="yt-empty-title">No results found</h2>
<p class="yt-empty-text">Try different keywords or browse all videos.</p>
@else
<h2 class="yt-empty-title">No videos yet</h2>
<p class="yt-empty-text">Be the first to upload a video!</p>
@endisset
@auth
<a href="/videos/create" class="yt-upload-btn" style="display: inline-flex;">
<i class="bi bi-plus-lg"></i>
Upload Video
</a>
@else
<a href="{{ route('login') }}" class="yt-upload-btn" style="display: inline-flex;">
<i class="bi bi-box-arrow-in-right"></i>
Login to Upload
</a>
@endauth
</div>
@else
<div class="yt-video-grid">
@foreach ($videos as $video)
<x-video-card :video="$video" />
@endforeach
</div>
@endif
@include('layouts.partials.share-modal')
@endsection
@section('scripts')
<script>
function playVideo(card) {
const video = card.querySelector('video');
if (video) {
video.currentTime = 0;
video.volume = 0.10; // Set volume to 10%
video.play().catch(function(e) {
// Auto-play may be blocked, ignore error
});
video.classList.add('active');
}
}
function stopVideo(card) {
const video = card.querySelector('video');
if (video) {
video.pause();
video.currentTime = 0;
video.classList.remove('active');
}
}
// Mobile touch support for hover-like behavior
document.addEventListener('touchstart', function(e) {
const card = e.target.closest('.yt-video-card');
if (card) {
// Stop any other playing videos first
document.querySelectorAll('.yt-video-card').forEach(function(otherCard) {
if (otherCard !== card) {
stopVideo(otherCard);
}
});
playVideo(card);
}
}, {
passive: true
});
document.addEventListener('touchend', function(e) {
const card = e.target.closest('.yt-video-card');
if (card) {
stopVideo(card);
}
}, {
passive: true
});
</script>
@endsection
<!-- Extra Mobile Styles -->
<style>
@media (max-width: 400px) {
.yt-video-grid {
gap: 12px;
}
.yt-video-thumb {
border-radius: 8px;
}
.yt-video-info {
gap: 8px;
margin-top: 8px;
}
.yt-channel-icon {
width: 28px;
height: 28px;
}
.yt-video-title {
font-size: 13px;
}
.yt-channel-name,
.yt-video-meta {
font-size: 11px;
}
.search-info {
padding: 12px;
}
.search-info h2 {
font-size: 16px;
}
}
</style>
@@ -1,68 +1,78 @@
<div class="comment-item" style="display: flex; gap: 12px; margin-bottom: 16px;" id="comment-{{ $comment->id }}">
<img src="{{ $comment->user->avatar_url }}" class="channel-avatar" style="width: 36px; height: 36px; flex-shrink: 0;"
<div class="_comment-item" style="display: flex; gap: 12px; margin-bottom: 16px;" id="comment-{{ $comment->id }}">
<img src="{{ $comment->user->avatar_url }}" class="_comment-avatar" style="width: 36px; height: 36px; flex-shrink: 0;"
alt="{{ $comment->user->name }}">
<div style="flex: 1;">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 4px;">
<span style="font-weight: 600; font-size: 14px;">{{ $comment->user->name }}</span>
<span
style="color: var(--text-secondary); font-size: 12px;">{{ $comment->created_at->diffForHumans() }}</span>
style="color: var(--text-secondary, #6b7280); font-size: 12px;">{{ $comment->created_at->diffForHumans() }}</span>
</div>
<div class="comment-body" style="font-size: 14px; line-height: 1.5; word-wrap: break-word;">
{{-- ✅ Comment Body - Prefixed class --}}
<div class="_comment-body" style="font-size: 14px; line-height: 1.5; word-wrap: break-word;"
data-_comment-enhanced="0">
{{ $comment->body }}
</div>
{{-- ✅ Edit Form (only for comment owner) --}}
@auth
@if (Auth::id() === $comment->user_id)
<div id="commentEditWrap{{ $comment->id }}" style="display:none; margin-top:8px;">
<textarea id="commentEditInput{{ $comment->id }}" class="form-control" rows="3"
style="background: var(--bg-secondary); border: 1px solid var(--border-color); color: var(--text-primary); border-radius: 8px; padding: 8px; width: 100%; resize: vertical; font-size: 14px;">{{ $comment->body }}</textarea>
<div id="commentEditWrap{{ $comment->id }}" class="_comment-edit-wrap" style="display:none;">
<textarea id="commentEditInput{{ $comment->id }}" class="_comment-edit-textarea" rows="3">{{ $comment->body }}</textarea>
<div style="display:flex; gap:8px; justify-content:flex-end; margin-top:8px;">
<button type="button" class="yt-action-btn" style="font-size: 12px; padding: 6px 12px;"
onclick="cancelEditComment({{ $comment->id }})">Cancel</button>
<button type="button" class="yt-action-btn"
style="background: var(--brand-red); color: white; font-size: 12px; padding: 6px 12px;"
onclick="saveEditComment({{ $comment->id }})">Save</button>
<button type="button" class="_comment-btn _comment-btn-primary"
onclick="_comment.saveEditComment({{ $comment->id }})">
<i class="bi bi-chat-dots"></i>
<span>Send</span>
</button>
<button type="button" class="_comment-btn _comment-btn-secondary"
onclick="_comment.cancelEditComment({{ $comment->id }})">
Cancel
</button>
</div>
</div>
@endif
@endauth
{{-- ✅ Action Buttons --}}
<div style="display: flex; gap: 12px; margin-top: 8px;">
@auth
<button onclick="toggleReplyForm({{ $comment->id }})"
style="background: none; border: none; color: var(--text-secondary); font-size: 12px; font-weight: 600; cursor: pointer; padding: 0;">
<button onclick="_comment.toggleReplyForm({{ $comment->id }})" class="_comment-btn _comment-btn-link">
Reply
</button>
@if (Auth::id() === $comment->user_id)
<button onclick="startEditComment({{ $comment->id }})"
style="background: none; border: none; color: var(--text-secondary); font-size: 12px; font-weight: 600; cursor: pointer; padding: 0;">
<button onclick="_comment.startEditComment({{ $comment->id }})"
class="_comment-btn _comment-btn-link">
Edit
</button>
<button onclick="deleteComment({{ $comment->id }})"
style="background: none; border: none; color: var(--text-secondary); font-size: 12px; font-weight: 600; cursor: pointer; padding: 0;">
<button onclick="window._commentDelete({{ $comment->id }})" class="_comment-btn _comment-btn-link">
Delete
</button>
@endif
@endauth
</div>
<!-- Reply Form -->
<div id="replyForm{{ $comment->id }}" style="display: none; margin-top: 12px;">
<div style="display: flex; gap: 8px;">
<textarea class="form-control" placeholder="Write a reply..." rows="2"
style="background: var(--bg-secondary); border: 1px solid var(--border-color); color: var(--text-primary); border-radius: 8px; padding: 8px; width: 100%; resize: none; font-size: 14px;"></textarea>
</div>
<div style="display: flex; gap: 8px; margin-top: 8px; justify-content: flex-end;">
<button type="button" class="yt-action-btn" style="font-size: 12px; padding: 6px 12px;"
onclick="toggleReplyForm({{ $comment->id }})">Cancel</button>
<button type="button" class="yt-action-btn"
style="background: var(--brand-red); color: white; font-size: 12px; padding: 6px 12px;"
onclick="submitReply({{ $video->id ?? $comment->video_id }}, {{ $comment->id }})">Reply</button>
{{-- ✅ Reply Form --}}
<div id="replyForm{{ $comment->id }}" class="_comment-reply-form" style="display: none; margin-top: 12px;">
<textarea id="replyBody{{ $comment->id }}" class="_comment-reply-textarea" placeholder="Write a reply..."
rows="2" style="margin-bottom: 8px;"></textarea>
<div style="display: flex; gap: 8px; justify-content: flex-end;">
<button type="button" class="_comment-btn _comment-btn-primary"
onclick="_comment.submitReply({{ $video->id ?? $comment->video_id }}, {{ $comment->id }})">
<i class="bi bi-chat-dots"></i>
<span>Send</span>
</button>
<button type="button" class="_comment-btn _comment-btn-secondary"
onclick="_comment.toggleReplyForm({{ $comment->id }})">
Cancel
</button>
</div>
</div>
<!-- Replies -->
{{-- ✅ Nested Replies --}}
@if ($comment->replies && $comment->replies->count() > 0)
<div
style="margin-left: 24px; margin-top: 12px; border-left: 2px solid var(--border-color); padding-left: 12px;">
<div class="_comment-reply-wrapper"
style="margin-left: 24px; margin-top: 12px; border-left: 2px solid var(--border-color, #e5e7eb); padding-left: 12px;">
@foreach ($comment->replies as $reply)
@include('videos.partials.comment', ['comment' => $reply, 'video' => $video ?? null])
@endforeach
@@ -71,65 +81,4 @@
</div>
</div>
<script>
function toggleReplyForm(commentId) {
const form = document.getElementById('replyForm' + commentId);
form.style.display = form.style.display === 'none' ? 'block' : 'none';
}
function startEditComment(commentId) {
const wrap = document.getElementById('commentEditWrap' + commentId);
const body = document.querySelector('#comment-' + commentId + ' .comment-body');
const input = document.getElementById('commentEditInput' + commentId);
if (!wrap || !body || !input) return;
input.value = body.textContent.trim();
wrap.style.display = 'block';
body.style.display = 'none';
}
function cancelEditComment(commentId) {
const wrap = document.getElementById('commentEditWrap' + commentId);
const body = document.querySelector('#comment-' + commentId + ' .comment-body');
if (wrap) wrap.style.display = 'none';
if (body) body.style.display = 'block';
}
async function saveEditComment(commentId) {
const input = document.getElementById('commentEditInput' + commentId);
const bodyEl = document.querySelector('#comment-' + commentId + ' .comment-body');
const wrap = document.getElementById('commentEditWrap' + commentId);
if (!input || !bodyEl || !wrap) return;
const body = input.value.trim();
if (!body) return;
try {
const res = await fetch(`/comments/${commentId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify({
body
})
});
const data = await res.json();
if (res.ok) {
bodyEl.textContent = data.body || body;
bodyEl.dataset.timeEnhanced = '';
if (typeof enhanceCommentBodyWithTimeBadges === 'function') {
enhanceCommentBodyWithTimeBadges(document.getElementById('comment-' + commentId));
}
wrap.style.display = 'none';
bodyEl.style.display = 'block';
} else {
alert(data.error || 'Failed to update comment');
}
} catch (e) {
alert('Failed to update comment: ' + e.message);
}
}
</script>
{{-- ✅ NO <script> TAGS - All JavaScript is in video-comments.blade.php --}}
+34 -195
View File
@@ -210,36 +210,7 @@
color: var(--text-secondary);
}
/* 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);
}
/* Description */
.video-description {
@@ -373,15 +344,7 @@
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%;
@@ -390,6 +353,30 @@
.video-description {
padding: 12px !important;
}
.comment-form {
flex-direction: column !important;
align-items: stretch !important;
gap: 12px !important;
}
.comment-form>div {
flex-direction: column !important;
align-items: stretch !important;
gap: 12px !important;
width: 100% !important;
}
.comment-form textarea {
height: 80px !important;
width: 100% !important;
}
.comment-form button {
align-self: flex-end !important;
width: auto !important;
padding: 8px 20px !important;
}
}
</style>
@endsection
@@ -537,165 +524,17 @@
</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; display: flex; align-items: center; gap: 8px;">
<textarea id="commentBody" class="form-control" placeholder="Add a comment... Use @ to mention someone" rows="1"
style="background: transparent; border: none; border-bottom: 2px solid var(--border-color); color: var(--text-primary); border-radius: 0; padding: 12px 0 8px 0; flex: 1; margin: 0; height: 40px; font-size: 14px; outline: none; overflow: hidden;"></textarea>
<button type="button" class="action-btn"
onclick="document.getElementById('commentBody').value = ''" style="flex-shrink: 0;">
<i class="bi bi-x-lg"></i>
<span>Cancel</span>
</button>
<button type="button" class="action-btn comment-btn" onclick="submitComment({{ $video->id }})"
style="flex-shrink: 0;">
<i class="bi bi-chat-dots"></i>
<span>Comment</span>
</button>
</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(response => response.json())
.catch(error => {
console.error('Error:', error);
alert('Failed to post comment: ' + error);
})
.then(data => {
if (data && data.success) {
document.getElementById('commentBody').value = '';
addCommentToList(data.comment);
} else {
alert('Failed to post comment');
}
});
}
function addCommentToList(comment) {
const commentsList = document.getElementById('commentsList');
const commentHtml = `
<div class="comment-item" style="display: flex; gap: 12px; margin-bottom: 16px;" id="comment-${comment.id}">
<img src="${comment.user.avatar_url}" class="channel-avatar" style="width: 36px; height: 36px; flex-shrink: 0;" alt="${comment.user.name}">
<div style="flex: 1;">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 4px;">
<span style="font-weight: 600; font-size: 14px;">${comment.user.name}</span>
<span style="color: var(--text-secondary); font-size: 12px;">just now</span>
</div>
<div class="comment-body" style="font-size: 14px; line-height: 1.5; word-wrap: break-word;">
${comment.body.replace(/@(\\w+)/g, '<span style="color: #3ea6ff; font-weight: 500;">@$1</span>')}
</div>
<div style="display: flex; gap: 12px; margin-top: 8px;">
<button onclick="toggleReplyForm(${comment.id})" style="background: none; border: none; color: var(--text-secondary); font-size: 12px; font-weight: 600; cursor: pointer; padding: 0;">
Reply
</button>
</div>
</div>
</div>
`;
commentsList.insertAdjacentHTML('afterbegin', commentHtml);
const commentCount = document.querySelector('h3 span');
if (commentCount) {
const count = parseInt(commentCount.textContent.match(/\\((\\d+)\\)/)?.[2] || 0) + 1;
commentCount.textContent = `(${count})`;
}
}
function deleteComment(commentId) {
if (confirm('Are you sure you want to delete this comment?')) {
fetch(`/comments/${commentId}`, {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
document.getElementById('comment-' + commentId).remove();
const commentCount = document.querySelector('h3 span');
if (commentCount) {
const count = parseInt(commentCount.textContent.match(/\\((\\d+)\\)/)?.[2] || 0) - 1;
commentCount.textContent = `(${count})`;
}
}
});
}
}
document.addEventListener('DOMContentLoaded', function() {
const commentTexts = document.querySelectorAll('.comment-body');
commentTexts.forEach(text => {
const html = text.innerHTML.replace(/@(\w+)/g,
'<span style="color: #3ea6ff; font-weight: 500;">@$1</span>');
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: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify({
body: body,
parent_id: parentId
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
}
});
}
</script>
<x-video-comments :video="$video" />
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
setTimeout(function() {
if (typeof enhanceComments === 'function') {
enhanceComments();
}
}, 100);
});
</script>
<!-- Sidebar - Up Next / Recommendations -->
<div class="yt-sidebar-container">
File diff suppressed because it is too large. Load diff
+2 -268
View File
@@ -279,48 +279,7 @@
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 {
@@ -547,75 +506,7 @@
</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;">
<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: 40px; height: 40px; border-radius: 50%;" alt="{{ $video->user->name }}">
@else
<div class="channel-avatar" style="width: 40px; height: 40px; border-radius: 50%;"></div>
@endif
<div>
<div class="channel-name" style="font-weight: 600;">{{ $video->user->name ?? 'Unknown' }}</div>
<div class="channel-subs" style="font-size: 12px; color: var(--text-secondary);">
{{ number_format($video->user->subscriber_count ?? 0) }} subscribers</div>
</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)
<button class="subscribe-btn">Subscribe</button>
@else
<button class="action-btn" onclick="openEditVideoModal({{ $video->id }})">
<i class="bi bi-pencil"></i>
<span>Edit</span>
</button>
@endif
@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
<button type="submit" class="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>
@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="action-btn"
onclick="openShareModal('{{ $video->share_url }}', '{{ addslashes($video->title) }}')">
<i class="bi bi-share"></i> Share
</button>
@endif
<!-- Save to Playlist Button -->
<button class="action-btn" onclick="openAddToPlaylistModal({{ $video->id }})">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path>
</svg>
<span>Save</span>
</button>
</div>
</div>
<x-channel-row :video="$video" />
<!-- Description Box -->
@if ($video->description)
@@ -687,165 +578,8 @@
</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>
<x-video-comments :video="$video" />
@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; display: flex; align-items: center; gap: 8px;">
<textarea id="commentBody" class="form-control" placeholder="Add a comment... Use @ to mention someone"
rows="1"
style="background: transparent; border: none; border-bottom: 2px solid var(--border-color); color: var(--text-primary); border-radius: 0; padding: 12px 0 8px 0; flex: 1; margin: 0; height: 40px; font-size: 14px; outline: none; overflow: hidden;"></textarea>
<button type="button" class="action-btn"
onclick="document.getElementById('commentBody').value = ''" style="flex-shrink: 0;">
<i class="bi bi-x-lg"></i>
<span>Cancel</span>
</button>
<button type="button" class="action-btn comment-btn"
onclick="submitComment({{ $video->id }})" style="flex-shrink: 0;">
<i class="bi bi-chat-dots"></i>
<span>Comment</span>
</button>
</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(response => response.json())
.catch(error => {
console.error('Error:', error);
alert('Failed to post comment: ' + error);
})
.then(data => {
if (data && data.success) {
document.getElementById('commentBody').value = '';
addCommentToList(data.comment);
} else {
alert('Failed to post comment');
}
});
}
function addCommentToList(comment) {
const commentsList = document.getElementById('commentsList');
const commentHtml = `
<div class="comment-item" style="display: flex; gap: 12px; margin-bottom: 16px;" id="comment-${comment.id}">
<img src="${comment.user.avatar_url}" class="channel-avatar" style="width: 36px; height: 36px; flex-shrink: 0;" alt="${comment.user.name}">
<div style="flex: 1;">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 4px;">
<span style="font-weight: 600; font-size: 14px;">${comment.user.name}</span>
<span style="color: var(--text-secondary); font-size: 12px;">just now</span>
</div>
<div class="comment-body" style="font-size: 14px; line-height: 1.5; word-wrap: break-word;">
${comment.body.replace(/@(\\w+)/g, '<span style="color: #3ea6ff; font-weight: 500;">@$1</span>')}
</div>
<div style="display: flex; gap: 12px; margin-top: 8px;">
<button onclick="toggleReplyForm(${comment.id})" style="background: none; border: none; color: var(--text-secondary); font-size: 12px; font-weight: 600; cursor: pointer; padding: 0;">
Reply
</button>
</div>
</div>
</div>
`;
commentsList.insertAdjacentHTML('afterbegin', commentHtml);
const commentCount = document.querySelector('h3 span');
if (commentCount) {
const count = parseInt(commentCount.textContent.match(/\\((\\d+)\\)/)?.[2] || 0) + 1;
commentCount.textContent = `(${count})`;
}
}
function deleteComment(commentId) {
if (confirm('Are you sure you want to delete this comment?')) {
fetch(`/comments/${commentId}`, {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
document.getElementById('comment-' + commentId).remove();
const commentCount = document.querySelector('h3 span');
if (commentCount) {
const count = parseInt(commentCount.textContent.match(/\\((\\d+)\\)/)?.[2] || 0) - 1;
commentCount.textContent = `(${count})`;
}
}
});
}
}
document.addEventListener('DOMContentLoaded', function() {
const commentTexts = document.querySelectorAll('.comment-body');
commentTexts.forEach(text => {
const html = text.innerHTML.replace(/@(\w+)/g,
'<span style="color: #3ea6ff; font-weight: 500;">@$1</span>');
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: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify({
body: body,
parent_id: parentId
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
}
});
}
</script>
</div>
<!-- Sidebar - Up Next / Recommendations -->