General
Comment code
Copy the implementation or inspect every file that belongs to this component unit.
resources/views/components/ui/comment.blade.php
@props([
'comment' => null,
'alpine' => false,
])
@php
/*
| A top-level comment with optional replies. Pass a Comment model for
| server rendering, or set `alpine` when inside an x-for loop.
|
| Usage:
| <x-ui.comment :comment="$comment" />
| <x-ui.comment alpine />
*/
$authorName = $comment
? ($comment->author_name ?? $comment->user?->fullName() ?? 'Anonymous')
: null;
@endphp
<div {{ $attributes->merge(['class' => 'py-6']) }}>
<div class="flex gap-4">
@if ($alpine)
<div class="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-brand-500 to-brand-700 text-sm font-semibold text-white" x-text="initials(comment.author_name)"></div>
@else
<x-ui.avatar
:name="$authorName"
:initials="$comment->user?->avatarInitials()"
:background="$comment->user?->avatarBackground()"
:color="$comment->user?->avatar_character_color"
size="sm"
/>
@endif
<div class="min-w-0 flex-1">
<div class="flex items-baseline gap-2">
@if ($alpine)
<p class="text-sm font-semibold text-gray-900 dark:text-white" x-text="comment.author_name"></p>
<span class="text-xs text-gray-400" x-text="comment.created_at"></span>
@else
<p class="text-sm font-semibold text-gray-900 dark:text-white">{{ $authorName }}</p>
<span class="text-xs text-gray-400">{{ $comment->created_at->diffForHumans() }}</span>
@if ($comment->status !== \App\Models\Comment::STATUS_APPROVED)
<x-ui.badge variant="amber" size="sm">{{ \App\Models\Comment::STATUS_LABELS[$comment->status] ?? Str::headline($comment->status) }}</x-ui.badge>
@endif
@endif
</div>
@if ($alpine)
<p class="mt-1 whitespace-pre-line text-sm leading-relaxed text-gray-600 dark:text-gray-300" x-text="comment.body"></p>
@else
<p class="mt-1 whitespace-pre-line text-sm leading-relaxed text-gray-600 dark:text-gray-300">{{ $comment->body }}</p>
@endif
@if ($alpine)
<x-ui.reactions uuid-key="comment.uuid" />
@else
<x-ui.reactions :uuid="$comment->uuid" />
@endif
<button
type="button"
@click="toggleReply({{ $alpine ? 'comment.uuid' : "'{$comment->uuid}'" }})"
class="mt-2 inline-flex items-center gap-1 text-xs font-medium text-gray-500 transition hover:text-brand-600 dark:text-gray-400 dark:hover:text-brand-400"
>
<x-ui.icon name="arrow-bend-up-left" weight="bold" size="sm" />
Reply
</button>
@if ($alpine)
<x-ui.comment.reply-form alpine />
<div x-show="(comment.replies || []).length" class="mt-4 space-y-4 border-l-2 border-gray-100 pl-4 dark:border-gray-800">
<template x-for="reply in comment.replies" :key="reply.uuid">
<x-ui.comment.reply alpine />
</template>
</div>
@else
<x-ui.comment.reply-form :parent-uuid="$comment->uuid" />
<div x-show="{{ $comment->replies->isNotEmpty() ? 'true' : "(newReplies['{$comment->uuid}'] || []).length" }}" class="mt-4 space-y-4 border-l-2 border-gray-100 pl-4 dark:border-gray-800">
@foreach ($comment->replies as $reply)
<x-ui.comment.reply :reply="$reply" />
@endforeach
<template x-for="reply in (newReplies['{{ $comment->uuid }}'] || [])" :key="reply.uuid">
<x-ui.comment.reply alpine size="xs" />
</template>
</div>
@endif
</div>
</div>
</div>
resources/views/components/ui/comment/reply-form.blade.php
@props([
'parentUuid' => null,
'alpine' => false,
])
@php
/*
| Inline reply form shown beneath a comment. Pass `parentUuid` for
| server-rendered comments, or set `alpine` when the parent uuid lives
| in Alpine scope as `comment.uuid`.
*/
$replyTarget = $alpine ? 'comment.uuid' : "'{$parentUuid}'";
@endphp
<div x-data="{}" x-id="['reply-author', 'reply-body', 'reply-body-error']" x-show="replyingTo === {{ $replyTarget }}" x-collapse x-cloak>
<form @submit.prevent="submit({{ $replyTarget }})" class="mt-3 space-y-3">
@if ($alpine)
<template x-if="isGuest">
<div class="space-y-1.5">
<label :for="$id('reply-author')" class="sr-only">Your name</label>
<input
:id="$id('reply-author')"
type="text"
x-model="replyForm.author_name"
placeholder="Your name"
class="block w-full rounded-lg border-gray-300 bg-white px-3.5 py-2.5 text-sm text-gray-900 shadow-sm transition focus:border-brand-500 focus:ring-1 focus:ring-brand-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100"
/>
</div>
</template>
@else
@guest
<div class="space-y-1.5">
<label :for="$id('reply-author')" class="sr-only">Your name</label>
<input
:id="$id('reply-author')"
type="text"
x-model="replyForm.author_name"
placeholder="Your name"
class="block w-full rounded-lg border-gray-300 bg-white px-3.5 py-2.5 text-sm text-gray-900 shadow-sm transition focus:border-brand-500 focus:ring-1 focus:ring-brand-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100"
/>
</div>
@endguest
@endif
<div class="space-y-1.5">
<label :for="$id('reply-body')" class="sr-only">Reply</label>
<textarea
:id="$id('reply-body')"
x-model="replyForm.body"
rows="2"
placeholder="Write a reply..."
:aria-describedby="replyErrors.body ? $id('reply-body-error') : null"
:aria-invalid="replyErrors.body ? 'true' : 'false'"
class="block w-full rounded-lg border-gray-300 bg-white px-3.5 py-2.5 text-sm text-gray-900 shadow-sm transition focus:border-brand-500 focus:ring-1 focus:ring-brand-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100"
></textarea>
</div>
<template x-if="replyErrors.body">
<p :id="$id('reply-body-error')" class="flex items-center gap-1 text-sm text-red-600 dark:text-red-400">
<x-ui.icon name="warning-circle" weight="fill" size="sm" />
<span x-text="replyErrors.body[0]"></span>
</p>
</template>
<div class="flex items-center justify-end gap-2">
<x-ui.button type="button" variant="ghost" size="sm" @click="cancelReply()">Cancel</x-ui.button>
<x-ui.button type="submit" size="sm" icon="paper-plane-tilt" ::loading="submitting" x-bind:disabled="submitting">Reply</x-ui.button>
</div>
</form>
</div>
resources/views/components/ui/comment/reply.blade.php
@props([
'reply' => null,
'alpine' => false,
'size' => 'sm',
])
@php
/*
| A single reply in a comment thread. Pass a Comment model for server
| rendering, or set `alpine` when inside an x-for loop.
|
| Usage:
| <x-ui.comment.reply :reply="$reply" />
| <x-ui.comment.reply alpine size="xs" />
*/
$sizes = [
'xs' => 'h-6 w-6 text-[10px]',
'sm' => 'h-7 w-7 text-[10px]',
];
$avatarSize = $size === 'xs' ? 'xs' : 'sm';
$fallbackSize = $sizes[$avatarSize] ?? $sizes['sm'];
$authorName = $reply
? ($reply->author_name ?? $reply->user?->fullName() ?? 'Anonymous')
: null;
@endphp
<div {{ $attributes->merge(['class' => 'flex gap-3']) }}>
@if ($alpine)
<div @class([
'mt-0.5 flex shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-brand-500 to-brand-700 font-semibold text-white',
$fallbackSize,
]) x-text="initials(reply.author_name)"></div>
<div class="min-w-0 flex-1">
<div class="flex items-baseline gap-2">
<p class="text-sm font-semibold text-gray-900 dark:text-white" x-text="reply.author_name"></p>
<span class="text-xs text-gray-400" x-text="reply.created_at"></span>
</div>
<p class="mt-1 whitespace-pre-line text-sm leading-relaxed text-gray-600 dark:text-gray-300" x-text="reply.body"></p>
<x-ui.reactions uuid-key="reply.uuid" />
</div>
@else
<x-ui.avatar
:name="$authorName"
:initials="$reply->user?->avatarInitials()"
:background="$reply->user?->avatarBackground()"
:color="$reply->user?->avatar_character_color"
:size="$avatarSize"
/>
<div class="min-w-0 flex-1">
<div class="flex items-baseline gap-2">
<p class="text-sm font-semibold text-gray-900 dark:text-white">{{ $authorName }}</p>
<span class="text-xs text-gray-400">{{ $reply->created_at->diffForHumans() }}</span>
</div>
<p class="mt-1 whitespace-pre-line text-sm leading-relaxed text-gray-600 dark:text-gray-300">{{ $reply->body }}</p>
<x-ui.reactions :uuid="$reply->uuid" />
</div>
@endif
</div>