converted comment section and the cannel info and the action buttons to components
This commit is contained in:
1 parent
84fcbd84dc
commit
64eadfaf56
10 files changed
+1324
-11985
No files matched your search
@@ -1,19 +1,24 @@
|
||||
<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>
|
||||
Comments <span style="color: var(--text-secondary); font-weight: 400;"
|
||||
id="commentCount{{ $video->id }}">({{ isset($video->comment_count) ? $video->comment_count : 0 }})</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{{ $video->id }}" 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>
|
||||
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; 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 }})"
|
||||
style="flex-shrink: 0;">
|
||||
<span>Send</span>
|
||||
<i class="bi bi-chat-dots"></i>
|
||||
<span>Comment</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -23,128 +28,248 @@
|
||||
<a href="{{ route('login') }}" style="color: var(--brand-red);">Sign in</a> to comment
|
||||
</div>
|
||||
@endauth
|
||||
|
||||
<div id="commentsList{{ $video->id }}">
|
||||
@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
|
||||
@if (isset($video))
|
||||
@forelse($video->comments()->whereNull('parent_id')->with('user', 'replies.user')->latest()->limit(20)->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
|
||||
@endif
|
||||
</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>
|
||||
function submitComment(videoId) {
|
||||
const body = document.getElementById('commentBody' + videoId).value.trim();
|
||||
if (!body) return;
|
||||
(function() {
|
||||
const videoId = {{ $video->id ?? 0 }};
|
||||
const scopePrefix = 'comments_' + videoId;
|
||||
|
||||
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 & amp; & amp; data.success) {
|
||||
document.getElementById('commentBody' + videoId).value = '';
|
||||
addCommentToList(data.comment, videoId);
|
||||
} else {
|
||||
alert('Failed to post comment');
|
||||
}
|
||||
});
|
||||
}
|
||||
// Scoped functions
|
||||
window[scopePrefix + '_submitComment'] = function() {
|
||||
const body = document.getElementById('commentBody' + videoId).value.trim();
|
||||
if (!body) return;
|
||||
|
||||
function addCommentToList(comment, videoId) {
|
||||
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;">
|
||||
${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',
|
||||
fetch(`/videos/${videoId}/comments`, {
|
||||
method: 'POST',
|
||||
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(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('comment-' + commentId).remove();
|
||||
// Update comment count for all comment sections (simplified)
|
||||
document.querySelectorAll('.comments-section h3 span').forEach(span => {
|
||||
const count = parseInt(span.textContent.match(/\\((\\d+)\\)/)?.[2] || 0) - 1;
|
||||
span.textContent = `(${Math.max(0, count)})`;
|
||||
});
|
||||
if (data?.success) {
|
||||
document.getElementById('commentBody' + videoId).value = '';
|
||||
window[scopePrefix + '_addCommentToList'](data.comment);
|
||||
} else {
|
||||
alert('Failed to post comment');
|
||||
}
|
||||
})
|
||||
.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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
badge.dataset.bound = '1';
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
Reference in new issue
Block a user