General
Comments code
Copy the implementation or inspect every file that belongs to this component unit.
resources/views/components/ui/comments.blade.php
@props([
'post',
])
@php
/*
| Full comment section for a post: async add form, live thread, and replies.
| Driven by Alpine.js — mount once per post show page.
|
| Usage:
| <x-ui.comments :post="$post" />
*/
$reactionStates = $post->topLevelComments
->flatMap(fn ($comment) => collect([$comment])->merge($comment->replies))
->mapWithKeys(fn ($comment) => [
$comment->uuid => $comment->summarizeReactions(),
])
->all();
$reactionsEndpointTemplate = route('comments.reactions.store', ['comment' => '__COMMENT__']);
@endphp
<section
{{ $attributes->merge(['class' => 'border-t border-gray-200 dark:border-gray-800']) }}
x-data="commentSection({
endpoint: '{{ route('comments.store', $post) }}',
isGuest: {{ auth()->check() ? 'false' : 'true' }},
initialCount: {{ $post->comments_count }},
reactionStates: @js($reactionStates),
reactionsEndpointTemplate: @js($reactionsEndpointTemplate),
})"
>
<div class="mx-auto max-w-5xl px-6 py-16 lg:px-8">
<h2 class="flex items-baseline gap-2 text-lg font-semibold text-gray-900 dark:text-white">
Comments
<span class="text-sm font-normal text-gray-400" x-text="`(${count})`"></span>
</h2>
<form @submit.prevent="submit()" class="mt-6 space-y-4">
@guest
<x-ui.field label="Your name" for="author_name" required>
<x-ui.input id="author_name" x-model="form.author_name" ::invalid="!! errors.author_name" placeholder="Jane Doe" />
<template x-if="errors.author_name">
<p class="mt-1 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="errors.author_name[0]"></span>
</p>
</template>
</x-ui.field>
@endguest
<x-ui.field label="Add a comment" for="body" required>
<x-ui.textarea id="body" x-model="form.body" rows="3" ::invalid="!! errors.body" placeholder="Share your thoughts..." />
<template x-if="errors.body">
<p class="mt-1 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="errors.body[0]"></span>
</p>
</template>
</x-ui.field>
<div class="flex items-center justify-end gap-3">
<span x-show="justAdded" x-transition class="text-sm text-green-600 dark:text-green-400">Comment posted</span>
<x-ui.button type="submit" icon="paper-plane-tilt" ::loading="submitting" x-bind:disabled="submitting">
Post comment
</x-ui.button>
</div>
</form>
<div class="mt-10 space-y-2 divide-y divide-gray-200 dark:divide-gray-800">
<template x-for="comment in comments" :key="comment.uuid">
<x-ui.comment alpine />
</template>
@forelse ($post->topLevelComments as $comment)
<x-ui.comment :comment="$comment" />
@empty
<div x-show="count === 0" x-cloak class="py-10 text-center text-sm text-gray-500 dark:text-gray-400">
No comments yet. Be the first to share your thoughts.
</div>
@endforelse
</div>
</div>
</section>
@push('head')
<script>
function commentSection({ endpoint, isGuest, initialCount, reactionStates, reactionsEndpointTemplate }) {
return {
endpoint,
isGuest,
count: initialCount,
comments: [],
newReplies: {},
reactionStates,
reactionsEndpointTemplate,
pickerOpenFor: null,
submitting: false,
justAdded: false,
replyingTo: null,
errors: {},
replyErrors: {},
form: { author_name: '', body: '' },
replyForm: { author_name: '', body: '' },
initials(name) {
return (name || 'A').split(' ').slice(0, 2).map(p => p.charAt(0).toUpperCase()).join('');
},
reactionEndpoint(uuid) {
return this.reactionsEndpointTemplate.replace('__COMMENT__', uuid);
},
togglePicker(uuid) {
this.pickerOpenFor = this.pickerOpenFor === uuid ? null : uuid;
},
async toggleReaction(uuid, emoji) {
try {
const response = await fetch(this.reactionEndpoint(uuid), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
},
body: JSON.stringify({ emoji }),
});
if (! response.ok) {
throw new Error('Request failed with status ' + response.status);
}
const data = await response.json();
this.reactionStates[uuid] = data.reactions;
} catch (error) {
// Keep the UI stable if the request fails.
}
},
toggleReply(uuid) {
this.replyingTo = this.replyingTo === uuid ? null : uuid;
this.replyErrors = {};
this.replyForm = { author_name: '', body: '' };
},
cancelReply() {
this.replyingTo = null;
this.replyErrors = {};
this.replyForm = { author_name: '', body: '' };
},
async submit(parentUuid = null) {
const isReply = parentUuid !== null;
this.submitting = true;
if (isReply) {
this.replyErrors = {};
} else {
this.errors = {};
this.justAdded = false;
}
const payload = isReply
? { ...this.replyForm, parent_uuid: parentUuid }
: { ...this.form };
try {
const response = await fetch(this.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
},
body: JSON.stringify(payload),
});
if (response.status === 422) {
const data = await response.json();
if (isReply) {
this.replyErrors = data.errors ?? {};
} else {
this.errors = data.errors ?? {};
}
return;
}
if (! response.ok) {
throw new Error('Request failed with status ' + response.status);
}
const data = await response.json();
this.count = data.comments_count;
if (isReply) {
this.addReply(parentUuid, data.comment);
this.cancelReply();
} else {
this.comments.unshift({ ...data.comment, replies: [] });
this.reactionStates[data.comment.uuid] = [];
this.form.body = '';
this.justAdded = true;
setTimeout(() => { this.justAdded = false; }, 2500);
}
} catch (error) {
const message = { body: ['Something went wrong. Please try again.'] };
if (isReply) {
this.replyErrors = message;
} else {
this.errors = message;
}
} finally {
this.submitting = false;
}
},
addReply(parentUuid, reply) {
const liveParent = this.comments.find(c => c.uuid === parentUuid);
if (liveParent) {
liveParent.replies = liveParent.replies || [];
liveParent.replies.push(reply);
this.reactionStates[reply.uuid] = [];
return;
}
if (! this.newReplies[parentUuid]) {
this.newReplies[parentUuid] = [];
}
this.newReplies[parentUuid].push(reply);
this.reactionStates[reply.uuid] = [];
},
};
}
</script>
@endpush