converted comment section and the cannel info and the action buttons to components

This commit is contained in:
ghassan 2026-03-21 03:22:30 +03:00
parent 84fcbd84dc
commit 64eadfaf56
10 changed files with 1324 additions and 11985 deletions

View File

@ -17,6 +17,7 @@ class CommentController extends Controller
public function index(Video $video) public function index(Video $video)
{ {
$comments = $video->comments()->whereNull('parent_id')->with(['user', 'replies.user'])->get(); $comments = $video->comments()->whereNull('parent_id')->with(['user', 'replies.user'])->get();
return response()->json($comments); return response()->json($comments);
} }
@ -35,12 +36,14 @@ class CommentController extends Controller
// Handle mentions // Handle mentions
preg_match_all('/@(\w+)/', $request->body, $matches); preg_match_all('/@(\w+)/', $request->body, $matches);
if (!empty($matches[1])) { if (! empty($matches[1])) {
// Mentions found - in production, you would send notifications here // Mentions found - in production, you would send notifications here
// For now, we just parse them // For now, we just parse them
} }
return response()->json($comment->load('user')); $video->increment('comment_count');
return response()->json(['success' => true, 'comment' => $comment->load('user')]);
} }
public function update(Request $request, Comment $comment) public function update(Request $request, Comment $comment)
@ -67,6 +70,7 @@ class CommentController extends Controller
} }
$comment->delete(); $comment->delete();
return response()->json(['success' => true]); return response()->json(['success' => true]);
} }
} }

View File

@ -1,19 +1,24 @@
<div class="comments-section" style="margin-top: 24px; padding-top: 16px; border-top: 1px solid var(--border-color);"> <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;"> <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> Comments <span style="color: var(--text-secondary); font-weight: 400;"
id="commentCount{{ $video->id }}">({{ isset($video->comment_count) ? $video->comment_count : 0 }})</span>
</h3> </h3>
@auth @auth
<div class="comment-form" style="display: flex; gap: 12px; margin-bottom: 24px;"> <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;" <img src="{{ Auth::user()->avatar_url }}" class="channel-avatar" style="width: 40px; height: 40px;"
alt="{{ Auth::user()->name }}"> alt="{{ Auth::user()->name }}">
<div style="flex: 1; display: flex; align-items: center; gap: 8px;"> <div style="flex: 1; display: flex; align-items: center; gap: 8px;">
<textarea id="commentBody{{ $video->id }}" class="form-control" <textarea id="commentBody{{ $video->id }}" class="form-control"
placeholder="Add a comment... Use @ to mention someone" rows="1" placeholder="Add a comment... Use @mm.ss for timestamps (e.g. @1.30)" 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> 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; resize: none;"></textarea>
<button type="button" class="action-btn" onclick="clearCommentForm('{{ $video->id }}')"
style="flex-shrink: 0;">
<i class="bi bi-x-lg"></i>
</button>
<button type="button" class="action-btn comment-btn" onclick="submitComment({{ $video->id }})" <button type="button" class="action-btn comment-btn" onclick="submitComment({{ $video->id }})"
style="flex-shrink: 0;"> style="flex-shrink: 0;">
<span>Send</span> <i class="bi bi-chat-dots"></i>
<span>Comment</span>
</button> </button>
</div> </div>
</div> </div>
@ -23,128 +28,248 @@
<a href="{{ route('login') }}" style="color: var(--brand-red);">Sign in</a> to comment <a href="{{ route('login') }}" style="color: var(--brand-red);">Sign in</a> to comment
</div> </div>
@endauth @endauth
<div id="commentsList{{ $video->id }}"> <div id="commentsList{{ $video->id }}">
@forelse($video->comments()->whereNull('parent_id')->with('user', 'replies.user')->latest()->get() as $comment) @if (isset($video))
@include('videos.partials.comment', ['comment' => $comment]) @forelse($video->comments()->whereNull('parent_id')->with('user', 'replies.user')->latest()->limit(20)->get() as $comment)
@empty @include('videos.partials.comment', ['comment' => $comment])
<p style="color: var(--text-secondary); text-align: center; padding: 20px;">No comments yet. Be the first to @empty
comment!</p> <p style="color: var(--text-secondary); text-align: center; padding: 20px;">No comments yet. Be the
@endforelse first to comment!</p>
@endforelse
@endif
</div> </div>
</div> </div>
<style>
/* Comment Section Specific Styles */
.comment-time-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 7px;
border-radius: 999px;
background: rgba(62, 166, 255, 0.15);
border: 1px solid rgba(62, 166, 255, 0.4);
color: #7dd3fc;
font-weight: 600;
font-size: 12px;
line-height: 1.2;
cursor: pointer;
text-decoration: none;
user-select: none;
margin: 0 2px;
transition: all 0.2s ease;
}
.comment-time-badge:hover {
background: rgba(62, 166, 255, 0.26);
border-color: rgba(125, 211, 252, 0.8);
color: #e0f2fe;
transform: translateY(-1px);
}
.comment-form textarea:focus {
border-bottom-color: var(--brand-red);
}
.action-btn.comment-btn:hover {
background: #dc2626 !important;
}
</style>
<script> <script>
function submitComment(videoId) { (function() {
const body = document.getElementById('commentBody' + videoId).value.trim(); const videoId = {{ $video->id ?? 0 }};
if (!body) return; const scopePrefix = 'comments_' + videoId;
fetch(`/videos/${videoId}/comments`, { // Scoped functions
method: 'POST', window[scopePrefix + '_submitComment'] = function() {
headers: { const body = document.getElementById('commentBody' + videoId).value.trim();
'Content-Type': 'application/json', if (!body) return;
'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 & amp; & amp; data.success) {
document.getElementById('commentBody' + videoId).value = '';
addCommentToList(data.comment, videoId);
} else {
alert('Failed to post comment');
}
});
}
function addCommentToList(comment, videoId) { fetch(`/videos/${videoId}/comments`, {
const commentsList = document.getElementById('commentsList' + videoId); method: 'POST',
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 = commentsList.closest('.comments-section').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: { headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}' 'Content-Type': 'application/json',
} 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute(
'content')
},
body: JSON.stringify({
body
})
}) })
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success) { if (data?.success) {
document.getElementById('comment-' + commentId).remove(); document.getElementById('commentBody' + videoId).value = '';
// Update comment count for all comment sections (simplified) window[scopePrefix + '_addCommentToList'](data.comment);
document.querySelectorAll('.comments-section h3 span').forEach(span => { } else {
const count = parseInt(span.textContent.match(/\\((\\d+)\\)/)?.[2] || 0) - 1; alert('Failed to post comment');
span.textContent = `(${Math.max(0, count)})`; }
}); })
.catch(error => {
console.error('Error:', error);
alert('Failed to post comment');
});
};
window[scopePrefix + '_clearCommentForm'] = function(vid) {
document.getElementById('commentBody' + vid).value = '';
};
window[scopePrefix + '_addCommentToList'] = function(comment) {
const commentsList = document.getElementById('commentsList' + videoId);
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;" data-time-enhanced="0">
${escapeHtml(comment.body)}
</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);
window[scopePrefix + '_enhanceCommentBodyWithTimeBadges'](commentsList);
window[scopePrefix + '_updateCommentCount'](1);
};
window[scopePrefix + '_updateCommentCount'] = function(delta = 0) {
const countEl = document.getElementById('commentCount' + videoId);
if (countEl) {
let count = parseInt(countEl.textContent.match(/\((\d+)\)/)?.[1] || 0) + delta;
countEl.textContent = `(${Math.max(0, count)})`;
}
};
window[scopePrefix + '_parseDotTimeToSeconds'] = function(dotTime) {
const parts = String(dotTime).trim().split('.');
if (parts.length !== 2) return null;
const mins = parseInt(parts[0], 10);
const secs = parseInt(parts[1], 10);
if (isNaN(mins) || isNaN(secs) || secs < 0 || secs > 59) return null;
return mins * 60 + secs;
};
let commentPlaybackStopHandler = null;
let commentPlaybackEndTime = null;
window[scopePrefix + '_clearCommentPlaybackHandler'] = function(videoPlayer) {
if (videoPlayer && commentPlaybackStopHandler) {
videoPlayer.removeEventListener('timeupdate', commentPlaybackStopHandler);
}
commentPlaybackStopHandler = null;
commentPlaybackEndTime = null;
};
window[scopePrefix + '_playCommentTimeRange'] = function(startSec, endSec = null) {
const videoPlayer = document.querySelector('#videoPlayer, video');
if (!videoPlayer) return;
window[scopePrefix + '_clearCommentPlaybackHandler'](videoPlayer);
const startPlayback = () => {
const playbackStart = Math.max(0, startSec - 1);
videoPlayer.currentTime = playbackStart;
videoPlayer.play().catch(e => console.warn('Autoplay prevented:', e));
if (endSec !== null && endSec > startSec) {
commentPlaybackEndTime = endSec;
commentPlaybackStopHandler = () => {
if (videoPlayer.currentTime >= commentPlaybackEndTime) {
videoPlayer.pause();
window[scopePrefix + '_clearCommentPlaybackHandler'](videoPlayer);
}
};
videoPlayer.addEventListener('timeupdate', commentPlaybackStopHandler);
}
};
startPlayback();
};
window[scopePrefix + '_enhanceCommentBodyWithTimeBadges'] = function(root = document) {
const commentBodies = root.querySelectorAll('.comment-body[data-time-enhanced="0"]');
const timeRangeRegex = /@(\\d{1,2}\\.\\d{2})(?:-(\\d{1,2}\\.\\d{2}))?/g;
const mentionRegex = /@(\\w+)/g;
commentBodies.forEach(bodyEl => {
const originalText = bodyEl.textContent || '';
if (!originalText.trim()) return;
let html = originalText.replace(timeRangeRegex, (match, start, end) => {
const startSec = window[scopePrefix + '_parseDotTimeToSeconds'](start);
const endSec = end ? window[scopePrefix + '_parseDotTimeToSeconds'](end) :
null;
if (startSec === null || (end && endSec === null)) return match;
if (endSec !== null && endSec <= startSec) return match;
const label = end ? `@${start}-${end}` : `@${start}`;
return `<span class="comment-time-badge" data-start="${startSec}" data-end="${endSec ?? ''}">${label}</span>`;
});
html = html.replace(mentionRegex, (m, u) => `@${u}`);
bodyEl.innerHTML = html.replace(/(^|[\\s>])@(\\w+)/g,
'$1<span style="color: #3ea6ff; font-weight: 500;">@$2</span>');
bodyEl.dataset.timeEnhanced = '1';
});
root.querySelectorAll('.comment-time-badge').forEach(badge => {
if (badge.dataset.bound === '1') return;
badge.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const start = parseFloat(this.dataset.start || '');
const end = this.dataset.end === '' ? null : parseFloat(this.dataset.end);
if (!isNaN(start)) {
window[scopePrefix + '_playCommentTimeRange'](start, end);
} }
}); });
} badge.dataset.bound = '1';
}
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();
}
}); });
} };
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Global functions for partials (edit/delete/reply)
window.submitComment = window[scopePrefix + '_submitComment'];
window.clearCommentForm = window[scopePrefix + '_clearCommentForm'];
window.deleteComment = function(commentId) {
if (confirm('Are you sure?')) {
fetch(`/comments/${commentId}`, {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute(
'content')
}
})
.then(res => res.json())
.then(data => {
if (data.success) {
document.getElementById('comment-' + commentId)?.remove();
window[scopePrefix + '_updateCommentCount'](-1);
}
});
}
};
// Initialize
document.addEventListener('DOMContentLoaded', () => {
window[scopePrefix + '_enhanceCommentBodyWithTimeBadges']();
});
})();
</script> </script>

View File

@ -601,215 +601,115 @@
</style> </style>
@endif @endif
<!-- Comments Section --> @include('components.video-comments', ['video' => $video])
<div class="comments-section" </div>
style="margin-top: 24px; padding-top: 16: 1px solid var(--borderpx; border-top-color);"> </div>
<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 <!-- Sidebar - Up Next / Recommendations -->
<div class="comment-form" style="display: flex; gap: 12px; margin-bottom: 24px;"> <div class="yt-sidebar-container">
<img src="{{ Auth::user()->avatar_url }}" class="channel-avatar" style="width: 40px; height: 40px;" @if ($playlist && $playlistVideos && $playlistVideos->count() > 0)
alt="{{ Auth::user()->name }}"> <h3 style="font-size: 16px; font-weight: 500; margin-bottom: 12px;">
<div style="flex: 1;"> <i class="bi bi-collection-play" style="margin-right: 8px;"></i>
<textarea id="commentBody" class="form-control" placeholder="Add a comment... Use @ to mention someone" {{ $playlist->name }}
rows="3" <span
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> style="font-weight: 400; color: var(--text-secondary); font-size: 14px;">({{ $playlistVideos->count() }}
<div style="display: flex; gap: 8px; margin-top: 8px; justify-content: flex-end;"> videos)</span>
<button type="button" class="yt-action-btn" </h3>
onclick="document.getElementById('commentBody').value = ''">Cancel</button> <div class="recommended-videos-list">
<button type="button" class="yt-action-btn" @foreach ($playlistVideos as $index => $playlistVideo)
style="background: var(--brand-red); color: white;" @if ($playlistVideo->id !== $video->id)
onclick="submitComment({{ $video->id }})">Comment</button> <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>
</div> </div>
</div> @endif
@else @endforeach
<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> </div>
@if ($playlist->canEdit(Auth::user()))
<script> <a href="{{ route('playlists.show', $playlist->id) }}" class="yt-action-btn"
function submitComment(videoId) { style="margin-top: 12px; display: inline-block;">
const body = document.getElementById('commentBody').value.trim(); <i class="bi bi-pencil"></i> Edit Playlist
if (!body) return; </a>
fetch(`/videos/${videoId}/comments`, { @endif
method: 'POST', @else
headers: { <h3 style="font-size: 16px; font-weight: 500; margin-bottom: 12px;">Up Next</h3>
'Content-Type': 'application/json', @if ($recommendedVideos && $recommendedVideos->count() > 0)
'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 - Up Next / Recommendations -->
<div class="yt-sidebar-container">
@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"> <div class="recommended-videos-list">
@foreach ($playlistVideos as $index => $playlistVideo) @foreach ($recommendedVideos as $recVideo)
@if ($playlistVideo->id !== $video->id) <div class="sidebar-video-card"
<div class="sidebar-video-card{{ $playlistVideo->id === $video->id ? ' current-video' : '' }}" onclick="window.location.href='{{ route('videos.show', $recVideo->id) }}'">
onclick="window.location.href='{{ route('videos.show', $playlistVideo->id) }}?playlist={{ $playlist->id }}'"> <div class="sidebar-thumb">
<div class="sidebar-thumb" style="position: relative;"> @if ($recVideo->thumbnail)
@if ($playlistVideo->thumbnail) <img src="{{ asset('storage/thumbnails/' . $recVideo->thumbnail) }}"
<img src="{{ asset('storage/thumbnails/' . $playlistVideo->thumbnail) }}" alt="{{ $recVideo->title }}">
alt="{{ $playlistVideo->title }}"> @else
@else <img src="https://picsum.photos/seed/{{ $recVideo->id }}/320/180"
<img src="https://picsum.photos/seed/{{ $playlistVideo->id }}/320/180" alt="{{ $recVideo->title }}">
alt="{{ $playlistVideo->title }}"> @endif
@endif @if ($recVideo->duration)
@if ($playlistVideo->duration) <span class="yt-video-duration">{{ gmdate('i:s', $recVideo->duration) }}</span>
<span @endif
class="yt-video-duration">{{ gmdate('i:s', $playlistVideo->duration) }}</span> @if ($recVideo->is_shorts)
@endif <span class="yt-shorts-badge"
@if ($playlistVideo->is_shorts) style="position: absolute; top: 8px; left: 8px; font-size: 10px; padding: 2px 6px;">
<span class="yt-shorts-badge" <i class="bi bi-collection-play-fill"></i> SHORTS
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> </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>
<div class="sidebar-info"> <div class="sidebar-meta">
<div class="sidebar-title"> <div>{{ $recVideo->user->name ?? 'Unknown' }}</div>
<i class="bi {{ match ($playlistVideo->type) {'music' => 'bi-music-note','match' => 'bi-trophy',default => 'bi-film'} }}" <div>{{ number_format($recVideo->view_count) }} views
style="color: #ef4444; margin-right: 4px; font-size: 12px;"></i> {{ $recVideo->created_at->diffForHumans() }}</div>
{{ 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>
</div> </div>
@endif </div>
@endforeach @endforeach
</div> </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 @else
<h3 style="font-size: 16px; font-weight: 500; margin-bottom: 12px;">Up Next</h3> <div class="text-secondary">No recommendations available yet. Check back later!</div>
@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 @endif
</div> @endif
</div>
</div> </div>

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +0,0 @@
@extends('layouts.app')
@section('title', ($video->title ?? 'Match Video') . ' | ' . config('app.name'))
@section('extra_styles')
<style>
/* ===== Video Section ===== */
.yt-video-section {
flex: 1;
min

File diff suppressed because it is too large Load Diff

View File

@ -210,13 +210,81 @@
color: var(--text-secondary); 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);
}
.action-btn,
.comments-section .action-btn {
border: none;
border-radius: 8px;
padding: 8px 14px;
font-size: 0.82rem;
font-weight: 500;
cursor: pointer;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
color: var(--text-primary);
display: flex;
align-items: center;
gap: 6px;
transition: all 0.2s ease;
}
.action-btn:hover,
.comments-section .action-btn:hover {
background: var(--border-color);
transform: translateY(-1px);
}
.action-btn:active,
.comments-section .action-btn:active {
transform: translateY(0);
}
.action-btn svg,
.action-btn i,
.comments-section .action-btn svg,
.comments-section .action-btn i {
flex-shrink: 0;
}
.action-btn.comment-btn {
background: var(--brand-red);
color: white;
border-color: var(--brand-red);
}
.yt-action-btn.liked {
color: var(--brand-red);
}
/* Channel Row */ /* Channel Row */
.channel-row { .channel-row {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 16px 0; padding: 16px 0;
overflow: visible;
} }
.channel-info { .channel-info {
@ -242,6 +310,18 @@
color: var(--text-secondary); 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 */ /* Description */
.video-description { .video-description {
background: var(--bg-secondary); background: var(--bg-secondary);
@ -352,16 +432,8 @@
.video-actions { .video-actions {
width: 100%; width: 100%;
justify-content: flex-end; overflow-x: auto;
} justify-content: flex-start;
.video-actions>.desktop-action {
display: none !important;
}
.mobile-action-dropdown {
display: block;
margin-left: auto;
} }
.yt-main { .yt-main {
@ -474,7 +546,76 @@
</div> </div>
</div> </div>
<x-channel-row :video="$video" /> <!-- 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>
<!-- Description Box --> <!-- Description Box -->
@if ($video->description) @if ($video->description)
@ -546,45 +687,84 @@
</style> </style>
@endif @endif
<x-video-comments :video="$video" /> <!-- 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>
</div> <script>
function submitComment(videoId) {
const body = document.getElementById('commentBody').value.trim();
if (!body) return;
<script> fetch(`/videos/${videoId}/comments`, {
function submitComment(videoId) { method: 'POST',
const body = document.getElementById('commentBody').value.trim(); headers: {
if (!body) return; 'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
fetch(`/videos/${videoId}/comments`, { },
method: 'POST', body: JSON.stringify({
headers: { body: body
'Content-Type': 'application/json', })
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify({
body: body
}) })
}) .then(response => response.json())
.then(response => response.json()) .catch(error => {
.catch(error => { console.error('Error:', error);
console.error('Error:', error); alert('Failed to post comment: ' + error);
alert('Failed to post comment: ' + error); })
}) .then(data => {
.then(data => { if (data && data.success) {
if (data && data.success) { document.getElementById('commentBody').value = '';
document.getElementById('commentBody').value = ''; addCommentToList(data.comment);
addCommentToList(data.comment); } else {
} else { alert('Failed to post comment');
alert('Failed to post comment'); }
} });
}); }
}
function addCommentToList(comment) { function addCommentToList(comment) {
const commentsList = document.getElementById('commentsList'); const commentsList = document.getElementById('commentsList');
const commentHtml = ` const commentHtml = `
<div class="comment-item" style="display: flex; gap: 12px; margin-bottom: 16px;" id="comment-${comment.id}"> <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}"> <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="flex: 1;">
@ -603,176 +783,177 @@
</div> </div>
</div> </div>
`; `;
commentsList.insertAdjacentHTML('afterbegin', commentHtml); commentsList.insertAdjacentHTML('afterbegin', commentHtml);
const commentCount = document.querySelector('h3 span'); const commentCount = document.querySelector('h3 span');
if (commentCount) { if (commentCount) {
const count = parseInt(commentCount.textContent.match(/\\((\\d+)\\)/)?.[2] || 0) + 1; const count = parseInt(commentCount.textContent.match(/\\((\\d+)\\)/)?.[2] || 0) + 1;
commentCount.textContent = `(${count})`; commentCount.textContent = `(${count})`;
}
} }
}
function deleteComment(commentId) { function deleteComment(commentId) {
if (confirm('Are you sure you want to delete this comment?')) { if (confirm('Are you sure you want to delete this comment?')) {
fetch(`/comments/${commentId}`, { fetch(`/comments/${commentId}`, {
method: 'DELETE', 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: { headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}' 'X-CSRF-TOKEN': '{{ csrf_token() }}'
} },
body: JSON.stringify({
body: body,
parent_id: parentId
})
}) })
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success) { if (data.success) {
document.getElementById('comment-' + commentId).remove(); location.reload();
const commentCount = document.querySelector('h3 span');
if (commentCount) {
const count = parseInt(commentCount.textContent.match(/\\((\\d+)\\)/)?.[2] || 0) - 1;
commentCount.textContent = `(${count})`;
}
} }
}); });
} }
} </script>
</div>
document.addEventListener('DOMContentLoaded', function() { <!-- Sidebar - Up Next / Recommendations -->
const commentTexts = document.querySelectorAll('.comment-body'); <div class="yt-sidebar-container">
commentTexts.forEach(text => { @if ($playlist && $playlistVideos && $playlistVideos->count() > 0)
const html = text.innerHTML.replace(/@(\w+)/g, <h3 style="font-size: 16px; font-weight: 500; margin-bottom: 12px;">
'<span style="color: #3ea6ff; font-weight: 500;">@$1</span>'); <i class="bi bi-collection-play" style="margin-right: 8px;"></i>
text.innerHTML = html; {{ $playlist->name }}
}); <span
}); style="font-weight: 400; color: var(--text-secondary); font-size: 14px;">({{ $playlistVideos->count() }}
videos)</span>
function submitReply(videoId, parentId) { </h3>
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 -->
<div class="yt-sidebar-container">
@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"> <div class="recommended-videos-list">
@foreach ($recommendedVideos as $recVideo) @foreach ($playlistVideos as $index => $playlistVideo)
<div class="sidebar-video-card" @if ($playlistVideo->id !== $video->id)
onclick="window.location.href='{{ route('videos.show', $recVideo->id) }}'"> <div class="sidebar-video-card{{ $playlistVideo->id === $video->id ? ' current-video' : '' }}"
<div class="sidebar-thumb"> onclick="window.location.href='{{ route('videos.show', $playlistVideo->id) }}?playlist={{ $playlist->id }}'">
@if ($recVideo->thumbnail) <div class="sidebar-thumb" style="position: relative;">
<img src="{{ asset('storage/thumbnails/' . $recVideo->thumbnail) }}" @if ($playlistVideo->thumbnail)
alt="{{ $recVideo->title }}"> <img src="{{ asset('storage/thumbnails/' . $playlistVideo->thumbnail) }}"
@else alt="{{ $playlistVideo->title }}">
<img src="https://picsum.photos/seed/{{ $recVideo->id }}/320/180" @else
alt="{{ $recVideo->title }}"> <img src="https://picsum.photos/seed/{{ $playlistVideo->id }}/320/180"
@endif alt="{{ $playlistVideo->title }}">
@if ($recVideo->duration) @endif
<span class="yt-video-duration">{{ gmdate('i:s', $recVideo->duration) }}</span> @if ($playlistVideo->duration)
@endif <span
@if ($recVideo->is_shorts) class="yt-video-duration">{{ gmdate('i:s', $playlistVideo->duration) }}</span>
<span class="yt-shorts-badge" @endif
style="position: absolute; top: 8px; left: 8px; font-size: 10px; padding: 2px 6px;"> @if ($playlistVideo->is_shorts)
<i class="bi bi-collection-play-fill"></i> 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> </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>
<div class="sidebar-meta"> <div class="sidebar-info">
<div>{{ $recVideo->user->name ?? 'Unknown' }}</div> <div class="sidebar-title">
<div>{{ number_format($recVideo->view_count) }} views <i class="bi {{ match ($playlistVideo->type) {'music' => 'bi-music-note','match' => 'bi-trophy',default => 'bi-film'} }}"
{{ $recVideo->created_at->diffForHumans() }}</div> 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>
</div> </div>
</div> @endif
@endforeach @endforeach
</div> </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 @else
<div class="text-secondary">No recommendations available yet. Check back later!</div> <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 @endif
@endif </div>
</div>
</div> </div>

View File

@ -1,45 +0,0 @@
<script>
function showDeleteCommentModal(commentId, commentBody) {
const modalId = 'confirmDeleteModal' + commentId;
const modalElement = document.getElementById(modalId);
if (modalElement) {
const modal = new bootstrap.Modal(modalElement);
modal.show();
window.currentDeleteCommentId = commentId;
}
}
function deleteCommentWithModal(commentId) {
fetch(`/comments/${commentId}`, {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
'Accept': 'application/json'
}
})
.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 = `(${Math.max(0, count)})`;
}
// Close modal
const modalId = 'confirmDeleteModal' + commentId;
const modalElement = document.getElementById(modalId);
if (modalElement) {
const modal = bootstrap.Modal.getInstance(modalElement);
modal.hide();
}
} else {
alert('Failed to delete comment');
}
})
.catch(error => {
console.error('Error:', error);
alert('Failed to delete comment');
});
}
</script>