diff --git a/resources/views/components/video-comments.blade.php b/resources/views/components/video-comments.blade.php index 73d60c6..a8768d2 100644 --- a/resources/views/components/video-comments.blade.php +++ b/resources/views/components/video-comments.blade.php @@ -10,8 +10,15 @@ // user's nationality. Batched so the comments list stays a single query. $commenterIds = $video->comments()->pluck('user_id')->filter()->unique()->values()->all(); $commenterCountry = \App\Models\User::topCountriesFor($commenterIds); - $topComments = $video->comments()->whereNull('parent_id')->with('user', 'replies.user')->latest()->get(); - $commentCount = $topComments->sum(fn($c) => 1 + $c->replies->count()); + $topComments = $video->comments()->whereNull('parent_id')->with('user', 'replies.user')->latest()->get(); + $topLevelCount = $topComments->count(); + $replyCount = $topComments->sum(fn($c) => $c->replies->count()); + + // Permalink: ?lc= scrolls to and highlights a specific + // comment or reply so a shared link lands on the exact message. The + // id is validated against the current video's comment ids so the + // highlight only applies when the target really belongs here. + $permalinkId = (int) request('lc', 0); $allCommentIds = $topComments->pluck('id') ->merge($topComments->flatMap(fn($c) => $c->replies->pluck('id'))) @@ -46,7 +53,17 @@ {{-- ── Header ── --}}
- {{ number_format($commentCount) }} Comments + {{-- Top-level count on the header; reply count surfaces inside each + thread's "N replies" expander. "1 Comment" singular when there's + exactly one — never "1 Comments". --}} + + {{ number_format($topLevelCount) }} {{ Str::plural('Comment', $topLevelCount) }} + @if($replyCount > 0) + + · {{ number_format($replyCount) }} {{ Str::plural('reply', $replyCount) }} + + @endif +
{{ $comment->body }}
@@ -184,7 +204,10 @@ @php $rFlag = $commenterCountry[$reply->user_id] ?? null; @endphp @if($rFlag)@endif {{ $reply->user->name ?? 'User' }} - {{ ytcTime($reply->created_at) }} + {{ ytcTime($reply->created_at) }}
{{ $reply->body }}
@if($llmEnabled) @@ -290,6 +313,23 @@ font-weight: 400; color: var(--text-primary); } +.ytc-count-replies { + font-size: 13px; + color: var(--text-secondary); + margin-left: 4px; +} + +/* Comment permalink highlight — brief glow on the targeted row when a + ?lc= link lands on this page. Fades on its own after ~2.8s. */ +.ytc-comment.ytc-highlight, +.ytc-reply.ytc-highlight { + animation: ytc-highlight-pulse 2.8s ease-out; +} +@keyframes ytc-highlight-pulse { + 0% { background-color: rgba(239, 68, 68, 0.18); } + 40% { background-color: rgba(239, 68, 68, 0.18); } + 100% { background-color: transparent; } +} .ytc-sort-wrap { position: relative; } .ytc-sort-btn { display: flex; @@ -460,7 +500,11 @@ .ytc-time { font-size: 12px; color: var(--text-secondary); + text-decoration: none; } +/* The time stamp is now an permalink. Underline on hover so it's + discoverable but doesn't scream "link" at rest. */ +.ytc-time.ytc-permalink:hover { text-decoration: underline; cursor: pointer; } /* ── Comment text ── */ .ytc-text { @@ -1506,16 +1550,39 @@ const listObserver = new MutationObserver(() => { const list = document.getElementById('ytcList'); if (list) listObserver.observe(list, { childList: true, subtree: true }); -// ── Init ────────────────────────────────────────────── -document.addEventListener('DOMContentLoaded', () => { - enhanceAll(); - initLikeStates(); -}); -if (document.readyState !== 'loading') { - enhanceAll(); - initLikeStates(); +// ── Comment permalink (?lc=) ───────────────────── +// Scroll to #comment- and briefly highlight the row so a shared link +// lands on the exact message. The reply row is nested inside a .ytc-replies +// expander by default — we auto-open the parent thread first so the target +// is visible before we scroll. +function openCommentPermalink(id) { + if (!id) return; + const target = document.getElementById('comment-' + id); + if (!target) return; + // Find and expand the parent replies section, if any. + const repliesSection = target.closest('.ytc-replies-section'); + if (repliesSection) { + const toggle = repliesSection.querySelector('.ytc-replies-toggle'); + if (toggle && toggle.dataset.open !== '1') toggle.click(); + } + requestAnimationFrame(() => { + target.scrollIntoView({ behavior: 'smooth', block: 'center' }); + target.classList.add('ytc-highlight'); + setTimeout(() => target.classList.remove('ytc-highlight'), 2800); + }); } +// ── Init ────────────────────────────────────────────── +function _ytcInit() { + enhanceAll(); + initLikeStates(); + @if($permalinkId) + openCommentPermalink({{ (int) $permalinkId }}); + @endif +} +document.addEventListener('DOMContentLoaded', _ytcInit); +if (document.readyState !== 'loading') _ytcInit(); + // ── Public API (for backward compat) ───────────────── window._comment = { deleteComment: openDeleteModal,