takeone-youtube-clone/resources/views/components/video-comments.blade.php
2026-04-05 03:30:22 +03:00

885 lines
36 KiB
PHP

{{-- DEFINE HELPER FUNCTION FIRST (before any HTML) --}}
@php
if (!function_exists('_renderComment')) {
function _renderComment($comment, $video, $depth = 0)
{
$avatar =
$comment->user->avatar_url ??
'https://ui-avatars.com/api/?name=' .
urlencode($comment->user->name ?? 'User') .
'&background=ef4444&color=fff';
$isOwn = $comment->user_id === (auth()->id() ?? 0);
$videoId = $video->id ?? 0;
$html = '<div class="_comment-item" id="comment-' . $comment->id . '">';
$html .=
'<img src="' .
e($avatar) .
'" class="_comment-avatar" style="width:36px;height:36px;" alt="' .
e($comment->user->name ?? 'User') .
'">';
$html .= '<div style="flex:1">';
$html .= '<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px">';
$html .= '<span style="font-weight:600;font-size:14px">' . e($comment->user->name ?? 'User') . '</span>';
$html .=
'<span style="color:var(--text-secondary);font-size:12px">' .
($comment->created_at ? $comment->created_at->diffForHumans() : '') .
'</span>';
$html .= '</div>';
$html .=
'<div class="_comment-body" data-_comment-enhanced="0" style="font-size:14px;line-height:1.5">' .
e($comment->body) .
'</div>';
// Edit form (only for owner)
if ($isOwn) {
$html .=
'<div id="commentEditWrap' . $comment->id . '" class="_comment-edit-wrap" style="display:none;">';
$html .=
'<textarea id="commentEditInput' .
$comment->id .
'" class="_comment-edit-textarea" rows="3">' .
e($comment->body) .
'</textarea>';
$html .= '<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:8px;">';
$html .=
'<button type="button" class="_comment-btn _comment-btn-primary _comment-save-edit-trigger" data-comment-id="' .
$comment->id .
'"><i class="bi bi-chat-dots"></i><span>Send</span></button>';
$html .=
'<button type="button" class="_comment-btn _comment-btn-secondary _comment-cancel-edit-trigger" data-comment-id="' .
$comment->id .
'">Cancel</button>';
$html .= '</div></div>';
}
// Action buttons
$html .= '<div style="display:flex;gap:12px;margin-top:8px;">';
$html .=
'<button type="button" class="_comment-btn _comment-btn-link _comment-reply-trigger" data-comment-id="' .
$comment->id .
'">Reply</button>';
if ($isOwn) {
$html .=
'<button type="button" class="_comment-btn _comment-btn-link _comment-edit-trigger" data-comment-id="' .
$comment->id .
'">Edit</button>';
$html .=
'<button type="button" class="_comment-btn _comment-btn-link _comment-delete-trigger" data-comment-id="' .
$comment->id .
'">Delete</button>';
}
$html .= '</div>';
// Reply form
$html .=
'<div id="replyForm' .
$comment->id .
'" class="_comment-reply-form" style="display:none;margin-top:12px;">';
$html .=
'<textarea id="replyBody' .
$comment->id .
'" class="_comment-reply-textarea" placeholder="Write a reply..." rows="2" style="margin-bottom:8px;"></textarea>';
$html .= '<div style="display:flex;gap:8px;justify-content:flex-end;">';
$html .=
'<button type="button" class="_comment-btn _comment-btn-primary _comment-submit-reply-trigger" data-video-id="' .
$videoId .
'" data-parent-id="' .
$comment->id .
'"><i class="bi bi-chat-dots"></i><span>Send</span></button>';
$html .=
'<button type="button" class="_comment-btn _comment-btn-secondary _comment-cancel-reply-trigger" data-comment-id="' .
$comment->id .
'">Cancel</button>';
$html .= '</div></div>';
// Nested replies (max depth 3)
if ($comment->replies && $comment->replies->count() > 0 && $depth < 3) {
$html .= '<div class="_comment-reply-wrapper" style="margin-left:24px;margin-top:12px;">';
foreach ($comment->replies as $reply) {
$html .= _renderComment($reply, $video, $depth + 1);
}
$html .= '</div>';
}
$html .= '</div></div>';
return $html;
}
}
@endphp
{{-- MAIN COMMENTS SECTION --}}
<div class="_comment-section"
style="margin-top: 24px; padding-top: 16px; border-top: 1px solid var(--border-color, #e5e7eb);">
<h3 style="font-size: 18px; font-weight: 600; margin-bottom: 16px; color: var(--text-primary, #1f2937);">
Comments <span style="color: var(--text-secondary, #6b7280); font-weight: 400;"
id="_commentCount">({{ $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="_comment-avatar"
style="width: 40px; height: 40px; border-radius: 50%; object-fit: cover;" alt="{{ Auth::user()->name }}">
<div style="flex: 1; display: flex; align-items: center; gap: 8px;">
<textarea id="_commentBody" class="_comment-textarea"
placeholder="Add a comment... Use @ to mention someone or @mm.ss for timestamps" rows="1"
style="background: transparent; border: none; border-bottom: 2px solid var(--border-color, #e5e7eb); color: var(--text-primary, #1f2937); padding: 12px 0 8px 0; flex: 1; margin: 0; height: 40px; font-size: 14px; outline: none; overflow: hidden; resize: none; font-family: inherit;"></textarea>
<button type="button" id="_commentSubmitBtn" class="_comment-btn _comment-btn-primary"
style="flex-shrink: 0;">
<i class="bi bi-chat-dots"></i>
<span>Send</span>
</button>
</div>
</div>
@else
<div
style="margin-bottom: 24px; padding: 16px; background: var(--bg-secondary, #f9fafb); border-radius: 8px; text-align: center;">
<a href="{{ route('login') }}" style="color: var(--brand-red, #ef4444);">Sign in</a> to comment
</div>
@endauth
<div id="_commentsList">
@forelse($video->comments()->whereNull('parent_id')->with('user', 'replies.user')->latest()->get() as $comment)
{!! _renderComment($comment, $video, 0) !!}
@empty
<p style="color: var(--text-secondary, #6b7280); text-align: center; padding: 20px;">No comments yet. Be the
first to comment!</p>
@endforelse
</div>
</div>
{{-- DELETE MODAL --}}
<div id="_commentDeleteModal" class="_comment-modal"
style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 9999; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.2s ease;">
<div class="_comment-modal-content"
style="background: var(--bg-secondary, #f9fafb); border-radius: 12px; padding: 24px; width: 90%; max-width: 340px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); transform: translateY(-10px); transition: transform 0.2s ease;">
<div style="display: flex; align-items: flex-start; gap: 12px; margin-bottom: 16px;">
<div class="_comment-modal-icon"
style="width: 40px; height: 40px; border-radius: 50%; background: #fef2f2; display: flex; align-items: center; justify-content: center; flex-shrink: 0;">
<i class="bi bi-exclamation-triangle" style="color: #dc2626; font-size: 20px;"></i>
</div>
<div style="flex: 1;">
<h4 class="_comment-modal-title"
style="font-weight: 600; font-size: 16px; margin: 0 0 6px 0; color: var(--text-primary, #1f2937);">
Delete Comment?</h4>
<p class="_comment-modal-message"
style="font-size: 14px; color: var(--text-secondary, #6b7280); margin: 0; line-height: 1.5;">This
action cannot be undone. The comment and its replies will be permanently removed.</p>
</div>
</div>
<div class="_comment-modal-actions" style="display: flex; gap: 10px; justify-content: flex-end;">
<button id="_commentModalCancel" class="_comment-btn _comment-btn-secondary"
style="background: transparent; color: var(--text-secondary, #6b7280); border: 1px solid var(--border-color, #e5e7eb); padding: 8px 16px; border-radius: 8px; font-size: 13px; font-weight: 500; cursor: pointer;">Cancel</button>
<button id="_commentModalConfirm" class="_comment-btn _comment-btn-danger"
style="background: #dc2626; color: white; border: none; padding: 8px 16px; border-radius: 8px; font-size: 13px; font-weight: 500; cursor: pointer; display: flex; align-items: center; gap: 6px;">
<i class="bi bi-trash" style="font-size: 12px;"></i> Delete
</button>
</div>
</div>
</div>
{{-- TOAST NOTIFICATION --}}
<div id="_commentToast" class="_comment-toast"
style="display: none; position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%) translateY(20px); background: #1f2937; color: white; padding: 12px 20px; border-radius: 8px; font-size: 14px; font-weight: 500; z-index: 10000; box-shadow: 0 4px 12px rgba(0,0,0,0.3); opacity: 0; transition: all 0.3s ease; max-width: 90%; text-align: center; pointer-events: none;">
<span id="_commentToastMessage"></span>
</div>
{{-- PREFIXED CSS --}}
<style>
._comment-section {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
}
._comment-textarea,
._comment-reply-textarea,
._comment-edit-textarea {
background: var(--bg-secondary, #f9fafb);
border: 1px solid var(--border-color, #e5e7eb);
color: var(--text-primary, #1f2937);
border-radius: 8px;
padding: 10px 12px;
font-size: 14px;
font-family: inherit;
resize: vertical;
outline: none;
transition: border-color 0.15s ease;
width: 100%;
box-sizing: border-box;
}
._comment-textarea:focus,
._comment-reply-textarea:focus,
._comment-edit-textarea:focus {
border-color: var(--brand-red, #ef4444);
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
}
._comment-form ._comment-textarea {
background: transparent;
border: none;
border-bottom: 2px solid var(--border-color, #e5e7eb);
border-radius: 0;
padding: 12px 0 8px 0;
}
._comment-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 8px 16px;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
border: none;
text-decoration: none;
white-space: nowrap;
}
._comment-btn-primary {
background: var(--brand-red, #ef4444);
color: white;
}
._comment-btn-primary:hover {
background: #dc2626;
}
._comment-btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
._comment-btn-secondary {
background: transparent;
color: var(--text-secondary, #6b7280);
border: 1px solid var(--border-color, #e5e7eb);
}
._comment-btn-secondary:hover {
background: var(--bg-secondary, #f9fafb);
color: var(--text-primary, #1f2937);
}
._comment-btn-danger {
background: #dc2626;
color: white;
}
._comment-btn-danger:hover {
background: #b91c1c;
}
._comment-btn-link {
background: none;
border: none;
color: var(--text-secondary, #6b7280);
font-size: 12px;
font-weight: 600;
padding: 0;
cursor: pointer;
}
._comment-btn-link:hover {
color: #dc2626;
}
._comment-item {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
._comment-avatar {
flex-shrink: 0;
border-radius: 50%;
object-fit: cover;
}
._comment-body {
font-size: 14px;
line-height: 1.5;
word-wrap: break-word;
color: var(--text-primary, #1f2937);
}
._comment-reply-wrapper {
padding-left: 48px;
margin-bottom: 12px;
position: relative;
}
._comment-reply-wrapper::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 2px;
background: var(--border-color, #e5e7eb);
}
._comment-time-badge {
display: inline-flex !important;
align-items: center;
gap: 4px;
padding: 4px 8px;
border-radius: 6px;
background: var(--brand-red, #ef4444);
border: 1px solid #dc2626;
color: white;
font-weight: 600;
font-size: 12px;
line-height: 1;
cursor: pointer;
text-decoration: none;
user-select: none;
margin: 0 2px;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(220, 38, 38, 0.3);
white-space: nowrap;
}
._comment-time-badge:hover {
background: #dc2626 !important;
border-color: #b91c1c !important;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(220, 38, 38, 0.4) !important;
}
._comment-time-badge:active {
transform: translateY(0);
}
._comment-time-badge i {
font-size: 11px;
line-height: 1;
display: inline-block;
}
._comment-modal._comment-modal-show {
opacity: 1;
}
._comment-modal._comment-modal-show ._comment-modal-content {
transform: translateY(0);
}
._comment-toast._comment-toast-show {
opacity: 1;
transform: translateX(-50%) translateY(0);
pointer-events: auto;
}
._comment-toast._comment-toast-success {
background: #059669;
}
._comment-toast._comment-toast-error {
background: #dc2626;
}
._comment-edit-wrap,
._comment-reply-form {
margin-top: 12px;
}
@media (max-width: 768px) {
._comment-form {
gap: 8px;
padding: 0 8px;
}
._comment-form ._comment-avatar {
width: 32px;
height: 32px;
}
._comment-form ._comment-textarea {
font-size: 16px;
height: 44px;
}
._comment-btn-primary {
min-width: 52px;
padding: 8px 12px;
font-size: 14px;
}
._comment-section {
padding-left: 8px;
padding-right: 8px;
}
._comment-modal-content {
max-width: 280px;
padding: 20px;
}
}
@media (max-width: 480px) {
._comment-form ._comment-avatar {
display: none;
}
._comment-form>div {
gap: 4px;
}
}
</style>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
{{-- JAVASCRIPT --}}
<script>
(function() {
'use strict';
const _comment = {
videoId: {{ $video->id }},
csrfToken: '{{ csrf_token() }}',
userId: {{ Auth::id() ?? 0 }},
elements: {},
state: {
deleteCommentId: null,
toastTimeout: null,
playbackHandler: null
}
};
function initElements() {
_comment.elements.list = document.getElementById('_commentsList');
_comment.elements.count = document.getElementById('_commentCount');
_comment.elements.modal = document.getElementById('_commentDeleteModal');
_comment.elements.modalConfirm = document.getElementById('_commentModalConfirm');
_comment.elements.modalCancel = document.getElementById('_commentModalCancel');
_comment.elements.toast = document.getElementById('_commentToast');
_comment.elements.toastMessage = document.getElementById('_commentToastMessage');
_comment.elements.submitBtn = document.getElementById('_commentSubmitBtn');
}
function showToast(message, type = 'info') {
if (!_comment.elements.toast || !_comment.elements.toastMessage) return;
if (_comment.state.toastTimeout) clearTimeout(_comment.state.toastTimeout);
_comment.elements.toastMessage.textContent = message;
_comment.elements.toast.className = '_comment-toast _comment-toast-' + type;
_comment.elements.toast.style.display = 'block';
requestAnimationFrame(() => _comment.elements.toast.classList.add('_comment-toast-show'));
_comment.state.toastTimeout = setTimeout(() => {
_comment.elements.toast.classList.remove('_comment-toast-show');
setTimeout(() => _comment.elements.toast.style.display = 'none', 300);
}, 3000);
}
function openDeleteModal(commentId) {
if (!_comment.elements.modal) return;
_comment.state.deleteCommentId = commentId;
_comment.elements.modal.style.display = 'flex';
requestAnimationFrame(() => _comment.elements.modal.classList.add('_comment-modal-show'));
document.body.style.overflow = 'hidden';
}
function closeDeleteModal() {
if (!_comment.elements.modal) return;
_comment.elements.modal.classList.remove('_comment-modal-show');
setTimeout(() => {
_comment.elements.modal.style.display = 'none';
_comment.state.deleteCommentId = null;
document.body.style.overflow = '';
}, 200);
}
function initDeleteModal() {
if (!_comment.elements.modal) return;
_comment.elements.modalCancel?.addEventListener('click', (e) => {
e.stopPropagation();
closeDeleteModal();
});
_comment.elements.modal.addEventListener('click', (e) => {
if (e.target === _comment.elements.modal) closeDeleteModal();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && _comment.elements.modal.style.display === 'flex')
closeDeleteModal();
});
_comment.elements.modalConfirm?.addEventListener('click', async () => {
if (!_comment.state.deleteCommentId) return;
const btn = _comment.elements.modalConfirm;
btn.disabled = true;
btn.innerHTML = '<i class="bi bi-hourglass-split"></i> Deleting...';
try {
const res = await fetch(`/comments/${_comment.state.deleteCommentId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': _comment.csrfToken
}
});
const data = await res.json();
if (!res.ok || !(data.success || data.deleted)) throw new Error(data.error ||
'Failed to delete');
const commentEl = document.getElementById('comment-' + _comment.state
.deleteCommentId);
if (commentEl) {
commentEl.style.transition = 'opacity 0.2s, transform 0.2s';
commentEl.style.opacity = '0';
commentEl.style.transform = 'translateX(-20px)';
setTimeout(() => commentEl.remove(), 200);
}
if (_comment.elements.count) {
const current = parseInt(_comment.elements.count.textContent.match(/\d+/)?.[
0] || 0);
_comment.elements.count.textContent = `(${Math.max(0, current - 1)})`;
}
showToast('Comment deleted', 'success');
closeDeleteModal();
} catch (e) {
showToast(e.message || 'Failed to delete', 'error');
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-trash"></i> Delete';
}
});
}
function initEventDelegation() {
if (!_comment.elements.list) return;
_comment.elements.list.addEventListener('click', (e) => {
const db = e.target.closest('._comment-delete-trigger');
if (db) {
e.preventDefault();
e.stopPropagation();
const id = db.dataset.commentId;
if (id) openDeleteModal(parseInt(id));
return;
}
const rb = e.target.closest('._comment-reply-trigger');
if (rb) {
e.preventDefault();
e.stopPropagation();
const id = rb.dataset.commentId;
if (id) toggleReplyForm(id);
return;
}
const eb = e.target.closest('._comment-edit-trigger');
if (eb) {
e.preventDefault();
e.stopPropagation();
const id = eb.dataset.commentId;
if (id) startEditComment(id);
return;
}
const ceb = e.target.closest('._comment-cancel-edit-trigger');
if (ceb) {
e.preventDefault();
e.stopPropagation();
const id = ceb.dataset.commentId;
if (id) cancelEditComment(id);
return;
}
const seb = e.target.closest('._comment-save-edit-trigger');
if (seb) {
e.preventDefault();
e.stopPropagation();
const id = seb.dataset.commentId;
if (id) saveEditComment(id);
return;
}
const crb = e.target.closest('._comment-cancel-reply-trigger');
if (crb) {
e.preventDefault();
e.stopPropagation();
const id = crb.dataset.commentId;
if (id) toggleReplyForm(id);
return;
}
const srb = e.target.closest('._comment-submit-reply-trigger');
if (srb) {
e.preventDefault();
e.stopPropagation();
const vid = srb.dataset.videoId,
pid = srb.dataset.parentId;
if (vid && pid) submitReply(vid, pid);
return;
}
});
}
function enhanceCommentBody(bodyEl) {
if (!bodyEl || bodyEl.dataset._commentEnhanced === '1') return;
let text = bodyEl.textContent || '';
const timeRegex = /@(\d{1,2})\.(\d{2})(?:-(\d{1,2})\.(\d{2}))?/g;
text = text.replace(timeRegex, (match, sM, sS, eM, eS) => {
const startMin = parseInt(sM, 10),
startSec = parseInt(sS, 10);
if (isNaN(startMin) || isNaN(startSec) || startSec > 59) return match;
const startDisplay =
`${String(startMin).padStart(2,'0')}:${String(startSec).padStart(2,'0')}`,
startSeconds = startMin * 60 + startSec;
if (eM && eS) {
const endMin = parseInt(eM, 10),
endSec = parseInt(eS, 10);
if (isNaN(endMin) || isNaN(endSec) || endSec > 59) return match;
const endDisplay =
`${String(endMin).padStart(2,'0')}:${String(endSec).padStart(2,'0')}`,
endSeconds = endMin * 60 + endSec;
return `<span class="_comment-time-badge" data-start="${startSeconds}" data-end="${endSeconds}" title="Play ${startDisplay} to ${endDisplay}"><i class="bi bi-clock"></i>${startDisplay}-${endDisplay}</span>`;
}
return `<span class="_comment-time-badge" data-start="${startSeconds}" title="Jump to ${startDisplay}"><i class="bi bi-play-fill"></i>${startDisplay}</span>`;
});
text = text.replace(/@([a-zA-Z0-9_]+)/g, '<span style="color:#3ea6ff;font-weight:500;">@$1</span>');
bodyEl.innerHTML = text;
bodyEl.dataset._commentEnhanced = '1';
bodyEl.querySelectorAll('._comment-time-badge').forEach(badge => {
badge.onclick = (e) => {
e.stopPropagation();
playTimeRange(parseFloat(badge.dataset.start), badge.dataset.end ? parseFloat(badge
.dataset.end) : null);
};
});
}
function enhanceAllComments() {
document.querySelectorAll(
'._comment-body[data-_comment-enhanced="0"], ._comment-body:not([data-_comment-enhanced])')
.forEach(enhanceCommentBody);
}
function clearPlaybackHandler(video) {
if (video && _comment.state.playbackHandler) video.removeEventListener('timeupdate', _comment.state
.playbackHandler);
_comment.state.playbackHandler = null;
}
function playTimeRange(startSec, endSec = null) {
const video = document.getElementById('videoPlayer');
if (!video) {
showToast('Video player not found', 'error');
return;
}
clearPlaybackHandler(video);
video.currentTime = Math.max(0, startSec - 1);
video.play().catch(() => {});
if (endSec) {
_comment.state.playbackHandler = () => {
if (video.currentTime >= endSec) {
video.pause();
clearPlaybackHandler(video);
}
};
video.addEventListener('timeupdate', _comment.state.playbackHandler);
}
}
function toggleReplyForm(commentId) {
const form = document.getElementById('replyForm' + commentId);
if (!form) return;
form.style.display = form.style.display === 'none' ? 'block' : 'none';
if (form.style.display === 'block') {
const input = document.getElementById('replyBody' + commentId);
if (input) input.focus();
}
}
function startEditComment(commentId) {
const body = document.querySelector(`#comment-${commentId} ._comment-body`),
wrap = document.getElementById('commentEditWrap' + commentId),
input = document.getElementById('commentEditInput' + commentId);
if (!body || !wrap || !input) return;
input.value = body.textContent.trim();
body.style.display = 'none';
wrap.style.display = 'block';
input.focus();
}
function cancelEditComment(commentId) {
const body = document.querySelector(`#comment-${commentId} ._comment-body`),
wrap = document.getElementById('commentEditWrap' + commentId);
if (body) body.style.display = 'block';
if (wrap) wrap.style.display = 'none';
}
async function saveEditComment(commentId) {
const input = document.getElementById('commentEditInput' + commentId),
bodyEl = document.querySelector(`#comment-${commentId} ._comment-body`),
wrap = document.getElementById('commentEditWrap' + commentId);
if (!input || !bodyEl || !wrap) return;
const body = input.value.trim();
if (!body) {
showToast('Comment cannot be empty', 'error');
return;
}
const btn = wrap?.querySelector('._comment-btn-primary');
if (btn) {
btn.disabled = true;
btn.innerHTML = '<i class="bi bi-hourglass-split"></i> Sending...';
}
try {
const res = await fetch(`/comments/${commentId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': _comment.csrfToken
},
body: JSON.stringify({
body
})
});
const data = await res.json();
if (res.ok && (data.success || data.body)) {
bodyEl.textContent = data.body || body;
bodyEl.dataset._commentEnhanced = '0';
enhanceCommentBody(bodyEl);
wrap.style.display = 'none';
bodyEl.style.display = 'block';
showToast('Comment updated', 'success');
} else {
throw new Error(data.error || 'Failed to update');
}
} catch (e) {
showToast(e.message, 'error');
} finally {
if (btn) {
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-chat-dots"></i> <span>Send</span>';
}
}
}
async function submitReply(vid, parentId) {
const input = document.getElementById('replyBody' + parentId);
if (!input) return;
const body = input.value.trim();
if (!body) {
showToast('Reply cannot be empty', 'error');
return;
}
const btn = input.parentElement?.querySelector('._comment-btn-primary');
if (btn) {
btn.disabled = true;
btn.innerHTML = '<i class="bi bi-hourglass-split"></i> Sending...';
}
try {
const res = await fetch(`/videos/${vid}/comments`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': _comment.csrfToken
},
body: JSON.stringify({
body,
parent_id: parentId
})
});
const data = await res.json();
if (res.ok && data.success) {
input.value = '';
toggleReplyForm(parentId);
refreshComments();
showToast('Reply posted', 'success');
} else {
throw new Error(data.error || 'Failed to post reply');
}
} catch (e) {
showToast(e.message, 'error');
} finally {
if (btn) {
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-chat-dots"></i> <span>Send</span>';
}
}
}
async function submitComment(vid) {
const input = document.getElementById('_commentBody');
if (!input) return;
const body = input.value.trim();
if (!body) {
showToast('Comment cannot be empty', 'error');
return;
}
if (_comment.elements.submitBtn) {
_comment.elements.submitBtn.disabled = true;
_comment.elements.submitBtn.innerHTML = '<i class="bi bi-hourglass-split"></i> Sending...';
}
try {
const res = await fetch(`/videos/${vid}/comments`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': _comment.csrfToken
},
body: JSON.stringify({
body
})
});
const data = await res.json();
if (res.ok && data.success) {
input.value = '';
refreshComments();
showToast('Comment posted', 'success');
} else {
throw new Error(data.error || 'Failed to post comment');
}
} catch (e) {
showToast(e.message, 'error');
} finally {
if (_comment.elements.submitBtn) {
_comment.elements.submitBtn.disabled = false;
_comment.elements.submitBtn.innerHTML = '<i class="bi bi-chat-dots"></i> <span>Send</span>';
}
}
}
async function refreshComments() {
try {
const res = await fetch(`/videos/${_comment.videoId}/comments`),
comments = await res.json(),
list = _comment.elements.list;
if (!list) return;
if (Array.isArray(comments) && comments.length) {
list.innerHTML = comments.map(c => {
const avatar = c.user?.avatar_url ||
`https://ui-avatars.com/api/?name=${encodeURIComponent(c.user?.name||'User')}&background=ef4444&color=fff`;
return `<div class="_comment-item" id="comment-${c.id}"><img src="${avatar}" class="_comment-avatar" style="width:36px;height:36px;"><div style="flex:1"><div style="display:flex;align-items:center;gap:8px;margin-bottom:4px"><span style="font-weight:600;font-size:14px">${c.user?.name||'User'}</span><span style="color:var(--text-secondary);font-size:12px">${c.created_at||''}</span></div><div class="_comment-body" data-_comment-enhanced="0" style="font-size:14px;line-height:1.5">${c.body||''}</div></div></div>`;
}).join('');
} else {
list.innerHTML =
'<p style="color:var(--text-secondary);text-align:center;padding:20px">No comments yet. Be the first to comment!</p>';
}
if (_comment.elements.count) _comment.elements.count.textContent =
`(${Array.isArray(comments) ? comments.length : 0})`;
setTimeout(enhanceAllComments, 50);
} catch (e) {
showToast('Failed to load comments', 'error');
}
}
function init() {
initElements();
initDeleteModal();
initEventDelegation();
enhanceAllComments();
_comment.elements.submitBtn?.addEventListener('click', () => submitComment(_comment.videoId));
if (_comment.elements.list && !_comment.elements.list._observerAttached) {
const observer = new MutationObserver(() => enhanceAllComments());
observer.observe(_comment.elements.list, {
childList: true,
subtree: true
});
_comment.elements.list._observerAttached = true;
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
window._comment = {
deleteComment: openDeleteModal,
playTimeRange: playTimeRange
};
})();
</script>