Skip to main content

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>