Add news article comments and reactions

This commit is contained in:
2026-05-01 11:43:49 +02:00
parent 874f8feb9c
commit 28e7e46e13
22 changed files with 20083 additions and 26 deletions

View File

@@ -0,0 +1,120 @@
@php
$commentsCollection = $comments ?? collect();
$commentsCount = $commentsCount ?? $commentsCollection->count();
$viewer = auth()->user();
@endphp
@if($isPreview)
@if($article->commentsAreEnabled())
<section id="comments" class="mt-8 rounded-[32px] border border-white/[0.06] bg-[linear-gradient(180deg,rgba(11,16,26,0.94),rgba(7,11,19,0.92))] p-6 shadow-[0_18px_45px_rgba(0,0,0,0.2)] sm:p-8">
<div class="rounded-2xl border border-indigo-300/20 bg-indigo-400/10 px-5 py-4 text-sm text-indigo-100">
Comments are enabled for this article, but posting is disabled in preview mode.
</div>
</section>
@endif
@elseif($article->commentsAreEnabled())
<section id="comments" class="mt-8 rounded-[32px] border border-white/[0.06] bg-[linear-gradient(180deg,rgba(11,16,26,0.94),rgba(7,11,19,0.92))] p-6 shadow-[0_18px_45px_rgba(0,0,0,0.2)] sm:p-8">
<div class="flex flex-col gap-3 border-b border-white/[0.06] pb-6 sm:flex-row sm:items-end sm:justify-between">
<div>
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/70">Conversation</p>
<h2 class="mt-2 text-2xl font-semibold tracking-[-0.03em] text-white">{{ number_format((int) $commentsCount) }} {{ (int) $commentsCount === 1 ? 'Comment' : 'Comments' }}</h2>
<p class="mt-2 max-w-2xl text-sm leading-6 text-white/50">Keep the discussion focused on the article. Safe markdown formatting is supported for signed-in members.</p>
</div>
</div>
@if(session('status'))
<div class="mt-6 rounded-2xl border border-emerald-400/20 bg-emerald-500/10 px-4 py-3 text-sm text-emerald-100">
{{ session('status') }}
</div>
@endif
@if($errors->has('body'))
<div class="mt-6 rounded-2xl border border-rose-400/20 bg-rose-500/10 px-4 py-3 text-sm text-rose-100">
{{ $errors->first('body') }}
</div>
@endif
@auth
<form method="POST" action="{{ route('news.comments.store', ['slug' => $article->slug]) }}" class="mt-6 rounded-[28px] border border-white/[0.06] bg-black/20 p-4 sm:p-5">
@csrf
<label class="block">
<span class="text-[11px] font-semibold uppercase tracking-[0.18em] text-white/35">Add your comment</span>
<textarea name="body" rows="4" maxlength="4000" placeholder="Share your thoughts about this article" class="mt-3 w-full rounded-2xl border border-white/[0.08] bg-black/30 px-4 py-3 text-sm leading-6 text-white placeholder:text-white/25 outline-none transition focus:border-sky-400/40 focus:bg-black/40">{{ old('body') }}</textarea>
</label>
<div class="mt-4 flex flex-wrap items-center justify-between gap-3">
<p class="text-xs leading-5 text-white/35">Basic markdown is supported. Replies and reactions use the enhanced editor when JavaScript is available.</p>
<button type="submit" class="inline-flex items-center gap-2 rounded-full border border-sky-400/20 bg-sky-500/10 px-4 py-2.5 text-sm font-semibold text-sky-100 transition hover:bg-sky-500/15">
<i class="fa-solid fa-paper-plane text-xs"></i>
Post comment
</button>
</div>
</form>
@else
<div class="mt-6 rounded-[28px] border border-white/[0.06] bg-black/20 p-5 text-sm text-white/55">
<a href="{{ route('login') }}" class="font-semibold text-sky-200 transition hover:text-white">Sign in</a>
to join the discussion on this article.
</div>
@endauth
@if($commentsCollection->isEmpty())
<div class="mt-6 rounded-[28px] border border-dashed border-white/[0.08] bg-white/[0.02] px-6 py-12 text-center">
<div class="mx-auto flex h-14 w-14 items-center justify-center rounded-full border border-white/[0.08] bg-white/[0.03] text-white/25">
<i class="fa-regular fa-comments text-xl"></i>
</div>
<h3 class="mt-4 text-lg font-semibold text-white">No comments yet</h3>
<p class="mx-auto mt-2 max-w-xl text-sm leading-6 text-white/40">Start the conversation if you have feedback, context, or a question about this update.</p>
</div>
@else
<div class="mt-6 space-y-4">
@foreach($commentsCollection as $comment)
@php
$commentUser = $comment->user;
$displayName = $commentUser?->name ?: $commentUser?->username ?: $comment->author_name ?: 'Former member';
$username = $commentUser?->username;
$profileUrl = $username ? '/@' . strtolower((string) $username) : null;
$canDelete = $viewer && ((int) $viewer->id === (int) $comment->user_id || (int) $viewer->id === (int) $article->author_id || $viewer->isAdmin() || $viewer->isModerator());
@endphp
<article class="rounded-[28px] border border-white/[0.06] bg-black/20 p-5">
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div class="flex min-w-0 items-center gap-3">
@if($commentUser)
<img src="{{ \App\Support\AvatarUrl::forUser((int) $commentUser->id, $commentUser->profile?->avatar_hash, 64) }}" alt="{{ $displayName }}" class="h-11 w-11 rounded-full border border-white/[0.08] object-cover">
@else
<div class="flex h-11 w-11 items-center justify-center rounded-full border border-white/[0.08] bg-white/[0.04] text-sm font-semibold text-white/55">
{{ \Illuminate\Support\Str::upper(\Illuminate\Support\Str::substr($displayName, 0, 1)) }}
</div>
@endif
<div class="min-w-0">
<div class="flex flex-wrap items-center gap-x-2 gap-y-1">
@if($profileUrl)
<a href="{{ $profileUrl }}" class="truncate text-sm font-semibold text-white transition hover:text-sky-300">{{ $displayName }}</a>
<span class="text-xs uppercase tracking-[0.16em] text-white/25">{{ '@' . $username }}</span>
@else
<span class="truncate text-sm font-semibold text-white">{{ $displayName }}</span>
@endif
</div>
<p class="mt-1 text-xs uppercase tracking-[0.16em] text-white/25">{{ optional($comment->created_at)->diffForHumans() ?? 'Unknown time' }}</p>
</div>
</div>
@if($canDelete)
<form method="POST" action="{{ route('news.comments.destroy', ['slug' => $article->slug, 'comment' => $comment->id]) }}">
@csrf
@method('DELETE')
<button type="submit" class="inline-flex items-center gap-2 rounded-full border border-white/[0.08] bg-white/[0.03] px-3 py-1.5 text-xs font-semibold uppercase tracking-[0.16em] text-white/55 transition hover:border-rose-300/20 hover:bg-rose-500/10 hover:text-rose-100">
<i class="fa-regular fa-trash-can text-[11px]"></i>
Remove
</button>
</form>
@endif
</div>
<div class="story-prose prose prose-invert mt-4 max-w-none text-[0.98rem] leading-7 prose-p:text-white/74">
{!! $comment->getDisplayHtml() !!}
</div>
</article>
@endforeach
</div>
@endif
</section>
@endif

View File

@@ -64,6 +64,7 @@
:breadcrumbs="$headerBreadcrumbs"
:description="$article->excerpt ? Str::limit(strip_tags((string) $article->excerpt), 180) : 'Latest Skinbase announcement and community update.'"
headerClass="pb-6"
innerClass="mx-auto max-w-7xl"
>
<x-slot name="actions">
<div class="flex flex-wrap items-center gap-2 text-sm text-white/60">
@@ -125,7 +126,7 @@
<p class="mt-5 text-lg leading-8 text-white/65">{{ $article->excerpt }}</p>
@endif
<div class="prose prose-invert prose-sky mt-8 max-w-none prose-p:text-white/72 prose-li:text-white/70 prose-strong:text-white prose-a:text-sky-300 hover:prose-a:text-sky-200 prose-headings:text-white">
<div class="story-prose prose prose-invert mt-8 max-w-none text-[1.02rem] leading-8 prose-p:text-white/72 prose-strong:text-white prose-headings:text-white [&_img]:rounded-[24px] [&_img]:border [&_img]:border-white/[0.08] [&_img]:shadow-[0_20px_45px_rgba(0,0,0,0.24)]">
{!! $article->rendered_content !!}
</div>
@@ -161,6 +162,34 @@
</a>
</div>
@endif
@if($article->commentsAreEnabled() && ! $isPreview)
<script id="news-comments-props" type="application/json">
{!! json_encode([
'articleId' => (int) $article->id,
'isLoggedIn' => auth()->check(),
'loginUrl' => route('login'),
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP) !!}
</script>
<div id="news-comments-root">
@include('news._comments', [
'article' => $article,
'comments' => $comments ?? collect(),
'commentsCount' => $commentsCount ?? 0,
'isPreview' => $isPreview,
])
</div>
@vite(['resources/js/Pages/News/NewsComments.jsx'])
@else
@include('news._comments', [
'article' => $article,
'comments' => $comments ?? collect(),
'commentsCount' => $commentsCount ?? 0,
'isPreview' => $isPreview,
])
@endif
</div>
@include('news._related_entities', ['relatedEntities' => $relatedEntities ?? []])