Selam, gönderinin yorum ve alt yorumlarının Laravel ve Vue(Inertia)'da nasıl yazılmasına örnek kodlar paylaşayım dedim.
Comment Model
public function post(): BelongsTo
{
return $this->belongsTo(Post::class);
}
public function comments(): HasMany
{
return $this->hasMany(self::class, 'parent_id');
}
Post Model
public function comments(): HasMany
{
return $this->hasMany(Comment::class)->latest();
}
Route
Route::post('/post/{post}/comment', [PostController::class, 'createComment'])->name('post.comment.create');
Route::delete('/comment/{comment}', [PostController::class, 'deleteComment'])->name('post.comment.delete');
Route::put('/comment/{comment}', [PostController::class, 'updateComment'])->name('post.comment.update');
PostController
public function createComment(Request $request, Post $post)
{
$data = $request->validate([
'comment' => ['required'],
'parent_id' => ['nullable', 'exists:comments,id']
]);
$comment = Comment::create([
'post_id' => $post->id,
'user_id' => auth()->id(),
'parent_id' => $data['parent_id'] ?: null,
'comment' => nl2br($data['comment'])
]);
return response(new CommentResource($comment), 201);
}
public function deleteComment(Comment $comment)
{
if($comment->user_id !== auth()->id()) {
return response("You don't have permission to delete this comment.", 403);
}
$comment->delete();
return response('', 204);
}
public function updateComment(UpdateCommentRequest $request, Comment $comment)
{
$data = $request->validated();
$comment->update([
'comment' => nl2br($data['comment'])
]);
return new CommentResource($comment);
}
CommentResource
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'comment' => $this->comment,
'created_at' => $this->created_at->format('y-m-d H:i:s'),
'updated_at' => $this->updated_at->format('y-m-d H:i:s'),
'num_of_reactions' => $this->reactions_count,
'num_of_comments' => $this->comments_count,
'current_user_has_reaction' => $this->reactions->count() > 0,
'comments' => CommentResource::collection($this->comments),
'user' => [
'id' => $this->user->id,
'name' => $this->user->name,
'username' => $this->user->username,
'avatar_url' => Storage::url($this->user->avatar_path)
]
];
}
PostItem.vue
<template>
<div class="bg-white border rounded p-4 mb-3">
<div class="flex items-center justify-between mb-3">
<PostUserHeader :post="post"/>
<EditDeleteDropdown :user="post.user" @edit="openEditModal" @delete="deletePost"/>
</div>
<div class="mb-3">
<ReadMoreReadLess :content="post.body"/>
</div>
<div class="grid gap-3 mb-3" :class="[post.attachments.length === 1 ? 'grid-cols-1' : 'grid-cols-2']">
<PostAttachments :attachments="post.attachments" @attachmentClick="openAttachment"/>
</div>
<Disclosure v-slot="{ open }">
<div class="flex gap-2">
<button
@click="sendReaction"
class="text-gray-800 flex gap-1 items-center justify-center rounded-lg py-2 px-4 flex-1"
:class="[
post.current_user_has_reaction ?
'bg-red-300 hover:bg-red-500 hover:text-white' :
'bg-green-300 hover:bg-green-500 hover:text-white'
]"
>
<HandThumbUpIcon class="w-5 h-5"/>
<span class="mr-2">{{ post.num_of_reactions }}</span>
{{ post.current_user_has_reaction ? 'Dislike' : 'Like' }}
</button>
<DisclosureButton
class="text-gray-800 flex gap-1 items-center justify-center bg-gray-100 rounded-lg hover:bg-gray-200 py-2 px-4 flex-1"
>
<ChatBubbleLeftRightIcon class="w-5 h-5"/>
<span class="mr-2">{{ post.num_of_comments }}</span>
Comment
</DisclosureButton>
</div>
<DisclosurePanel class="mt-3">
<CommentList :post="post" :data="{comments: post.comments}" />
</DisclosurePanel>
</Disclosure>
</div>
</template>
axiosClient.js
import axios from "axios";
const instance = axios.create();
instance.interceptors.request.use(function(config) {
return config;
})
export default instance;
CommentList.vue
<script setup>
import { ChatBubbleLeftEllipsisIcon, HandThumbUpIcon } from "@heroicons/vue/24/outline/index.js";
import ReadMoreReadLess from "@/Components/app/ReadMoreReadLess.vue";
import IndigoButton from "@/Components/app/IndigoButton.vue";
import InputTextarea from "@/Components/InputTextarea.vue";
import EditDeleteDropdown from "@/Components/app/EditDeleteDropdown.vue";
import { usePage } from "@inertiajs/vue3";
import { ref } from "vue";
import axiosClient from "@/axiosClient.js";
import { Disclosure, DisclosureButton, DisclosurePanel } from "@headlessui/vue";
const authUser = usePage().props.auth.user;
const newCommentText = ref('')
const editingComment = ref(null);
const props = defineProps({
post: Object,
data: Object,
parentComment: {
type: [Object, null],
default: null
}
})
function startCommentEdit(comment) {
editingComment.value = {
id: comment.id,
comment: comment.comment.replace(/<br\s*\/?>/gi, '\n') // <br />, <br > <br> <br/>, <br />
}
}
function createComment() {
axiosClient.post(route('post.comment.create', props.post), {
comment : newCommentText.value,
parent_id : props.parentComment?.id || null
})
.then(({ data }) => {
newCommentText.value = ''
props.data.comments.unshift(data)
if(props.parentComment) {
props.parentComment.num_of_comments++;
}
props.post.num_of_comments++;
})
}
function deleteComment(comment) {
if (!window.confirm('Are you sure you want to delete this comment?')) {
return false;
}
axiosClient.delete(route('post.comment.delete', comment.id))
.then(({ data }) => {
const commentIndex = props.data.comments.findIndex(c => c.id === comment.id)
props.data.comments.splice(commentIndex, 1)
if(props.parentComment) {
props.parentComment.num_of_comments--;
}
props.post.num_of_comments--;
})
}
function updateComment() {
axiosClient.put(route('post.comment.update', editingComment.value.id), editingComment.value)
.then(({ data }) => {
editingComment.value = null
props.data.comments = props.data.comments.map((c) => {
if(c.id === data.id) {
return data;
}
return c;
})
})
}
<template>
<div class="flex gap-2 mb-3">
<a href="javascript:void(0)">
<img :src="authUser.avatar_url"
class="w-[40px] rounded-full border border-2 transition-all hover:border-blue-500"/>
</a>
<div class="flex flex-1">
<InputTextarea v-model="newCommentText" placeholder="Enter your comment here" rows="1"
class="w-full max-h-[160px] resize-none rounded-r-none"></InputTextarea>
<IndigoButton @click="createComment" class="rounded-l-none w-[100px] ">Submit</IndigoButton>
</div>
</div>
<div>
<div v-for="comment of data.comments" :key="comment.id" class="mb-4">
<div class="flex justify-between gap-2">
<div class="flex gap-2">
<a href="javascript:void(0)">
<img :src="comment.user.avatar_url"
class="w-[40px] rounded-full border border-2 transition-all hover:border-blue-500"/>
</a>
<div>
<h4 class="font-bold">
<a href="javascript:void(0)" class="hover:underline">
{{ comment.user.name }}
</a>
</h4>
<small class="text-xs text-gray-400">{{ comment.updated_at }}</small>
</div>
</div>
<EditDeleteDropdown :user="comment.user" @edit="startCommentEdit(comment)"
@delete="deleteComment(comment)"/>
</div>
<div class="pl-12">
<div v-if="editingComment && editingComment.id === comment.id">
<InputTextarea v-model="editingComment.comment" placeholder="Enter your comment here"
rows="1" class="w-full max-h-[160px] resize-none"></InputTextarea>
<div class="flex gap-2 justify-end">
<button @click="editingComment = null" class="rounded-r-none text-indigo-500">cancel
</button>
<IndigoButton @click="updateComment" class="w-[100px]">update
</IndigoButton>
</div>
</div>
<ReadMoreReadLess v-else :content="comment.comment" content-class="text-sm flex flex-1"/>
<Disclosure>
<div class="mt-1 flex gap-2">
<button @click="sendCommentReaction(comment)"
class="flex items-center text-xs text-indigo-500 py-0.5 px-1 rounded-lg"
:class="[
comment.current_user_has_reaction ?
'bg-indigo-50 hover:bg-indigo-100' :
'hover:bg-indigo-50'
]">
<HandThumbUpIcon class="w-3 h-3 mr-1"/>
<span class="mr-2">{{ comment.num_of_reactions }}</span>
{{ comment.current_user_has_reaction ? 'unlike' : 'like' }}
</button>
<DisclosureButton
class="flex items-center text-xs text-indigo-500 py-0.5 px-1 hover:bg-indigo-100 rounded-lg">
<ChatBubbleLeftEllipsisIcon class="w-3 h-3 mr-1"/>
<span class="mr-2">{{ comment.num_of_comments }}</span>
comments
</DisclosureButton>
</div>
<DisclosurePanel class="mt-3">
<CommentList :post="post"
:data="{comments: comment.comments}"
:parent-comment="comment"/>
</DisclosurePanel>
</Disclosure>
</div>
</div>
</div>
</template>