feat: add multi-language support and update comments tab UI

This commit is contained in:
Paulo Castellano 2026-04-15 20:36:06 -03:00
parent 54c2ef68f5
commit b27f3fbd1e
4 changed files with 96 additions and 99 deletions

1
lang/php_en.json Normal file

File diff suppressed because one or more lines are too long

1
lang/php_es.json Normal file

File diff suppressed because one or more lines are too long

1
lang/php_pt-br.json Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,11 +1,11 @@
<script setup lang="ts">
import {
IconArrowBackUp,
IconEdit,
IconLoader2,
IconMoodSmile,
IconSend,
IconTrash,
IconCornerDownRight,
IconLoader2,
IconX,
} from '@tabler/icons-vue';
import { computed, nextTick, onMounted, ref, watch } from 'vue';
@ -15,6 +15,7 @@ import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import date from '@/date';
import dayjs from '@/dayjs';
import {
index as fetchComments,
store as storeComment,
@ -81,6 +82,29 @@ const textareaRef = ref<InstanceType<typeof Textarea> | null>(null);
const hasOlderComments = computed(() => currentPage.value < lastPage.value);
interface DayGroup {
label: string;
comments: Comment[];
}
const commentsByDay = computed((): DayGroup[] => {
const groups: Map<string, Comment[]> = new Map();
for (const comment of comments.value) {
const day = dayjs.utc(comment.created_at).local().format('YYYY-MM-DD');
if (!groups.has(day)) groups.set(day, []);
groups.get(day)!.push(comment);
}
const today = dayjs().format('YYYY-MM-DD');
const yesterday = dayjs().subtract(1, 'day').format('YYYY-MM-DD');
return Array.from(groups.entries()).map(([day, items]) => ({
label: day === today ? 'Today' : day === yesterday ? 'Yesterday' : dayjs(day).format('MMM D, YYYY'),
comments: items,
}));
});
const getInitials = (name: string): string => {
return name
.split(' ')
@ -340,8 +364,12 @@ const toggleReaction = async (comment: Comment, emoji: string) => {
}
};
const toggleEmojiPicker = (commentId: string) => {
emojiPickerCommentId.value = emojiPickerCommentId.value === commentId ? null : commentId;
const showEmojiPicker = (commentId: string) => {
emojiPickerCommentId.value = commentId;
};
const hideEmojiPicker = () => {
emojiPickerCommentId.value = null;
};
const handleKeydown = (event: KeyboardEvent) => {
@ -414,14 +442,17 @@ watch(() => props.postId, () => {
<p class="text-sm text-muted-foreground">{{ $t('comments.empty') }}</p>
</div>
<!-- Comments -->
<div class="space-y-1 p-2">
<template v-for="comment in comments" :key="comment.id">
<!-- Comments grouped by day -->
<div class="px-2 py-1">
<template v-for="group in commentsByDay" :key="group.label">
<div class="mt-2 mb-4 text-center text-xs text-muted-foreground">{{ group.label }}</div>
<template v-for="comment in group.comments" :key="comment.id">
<!-- Top-level comment -->
<div
class="group relative rounded-lg p-2 hover:bg-muted/50"
class="group relative rounded-lg py-1.5 px-2 hover:bg-muted/50"
@mouseenter="hoveredCommentId = comment.id"
@mouseleave="hoveredCommentId = null"
@mouseleave="hoveredCommentId = null; emojiPickerCommentId = null"
>
<!-- Editing mode -->
<div v-if="editingComment?.id === comment.id" class="space-y-2">
@ -461,7 +492,7 @@ watch(() => props.postId, () => {
<p class="mt-0.5 whitespace-pre-wrap text-sm">{{ comment.body }}</p>
<!-- Reactions -->
<div v-if="groupedReactions(comment.reactions).length > 0" class="mt-1 flex flex-wrap gap-1">
<div v-if="groupedReactions(comment.reactions).length > 0" class="mt-1.5 flex flex-wrap gap-1">
<button
v-for="r in groupedReactions(comment.reactions)"
:key="r.emoji"
@ -473,70 +504,49 @@ watch(() => props.postId, () => {
<span class="text-[10px] text-muted-foreground">{{ r.count }}</span>
</button>
</div>
</div>
<!-- Hover actions -->
<!-- Floating toolbar -->
<div
v-if="hoveredCommentId === comment.id && editingComment?.id !== comment.id"
class="absolute -top-2 right-1 flex items-center gap-0.5 rounded-md border bg-background px-1 py-0.5 shadow-sm"
class="absolute -top-3 right-2 z-50 flex items-center gap-0.5 rounded-md border bg-background px-1 py-0.5 shadow-sm"
>
<TooltipProvider>
<TooltipProvider :delay-duration="200">
<Tooltip>
<TooltipTrigger as-child>
<button class="rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground" @click="toggleEmojiPicker(comment.id)">
<IconMoodSmile class="h-3.5 w-3.5" />
</button>
<button class="rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground" @mouseenter="showEmojiPicker(comment.id)"><IconMoodSmile class="h-3.5 w-3.5" /></button>
</TooltipTrigger>
<TooltipContent side="top"><span class="text-xs">React</span></TooltipContent>
<TooltipContent side="top" class="text-xs">React</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger as-child>
<button class="rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground" @click="startReply(comment)">
<IconCornerDownRight class="h-3.5 w-3.5" />
</button>
<button class="rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground" @click="startReply(comment)"><IconArrowBackUp class="h-3.5 w-3.5" /></button>
</TooltipTrigger>
<TooltipContent side="top"><span class="text-xs">{{ $t('comments.reply') }}</span></TooltipContent>
<TooltipContent side="top" class="text-xs">{{ $t('comments.reply') }}</TooltipContent>
</Tooltip>
<template v-if="comment.user_id === currentUserId">
<Tooltip>
<TooltipTrigger as-child>
<button class="rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground" @click="startEdit(comment)"><IconEdit class="h-3.5 w-3.5" /></button>
</TooltipTrigger>
<TooltipContent side="top" class="text-xs">{{ $t('comments.edit') }}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<button class="rounded p-1 text-muted-foreground hover:bg-muted hover:text-destructive" @click="deleteComment(comment)"><IconTrash class="h-3.5 w-3.5" /></button>
</TooltipTrigger>
<TooltipContent side="top" class="text-xs">{{ $t('comments.delete') }}</TooltipContent>
</Tooltip>
</template>
</TooltipProvider>
<template v-if="comment.user_id === currentUserId">
<TooltipProvider>
<Tooltip>
<TooltipTrigger as-child>
<button class="rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground" @click="startEdit(comment)">
<IconEdit class="h-3.5 w-3.5" />
</button>
</TooltipTrigger>
<TooltipContent side="top"><span class="text-xs">{{ $t('comments.edit') }}</span></TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger as-child>
<button class="rounded p-1 text-muted-foreground hover:bg-muted hover:text-destructive" @click="deleteComment(comment)">
<IconTrash class="h-3.5 w-3.5" />
</button>
</TooltipTrigger>
<TooltipContent side="top"><span class="text-xs">{{ $t('comments.delete') }}</span></TooltipContent>
</Tooltip>
</TooltipProvider>
</template>
</div>
<!-- Emoji picker dropdown -->
<!-- Emoji picker -->
<div
v-if="emojiPickerCommentId === comment.id"
class="absolute -top-10 right-1 z-10 flex items-center gap-0.5 rounded-lg border bg-background p-1.5 shadow-md"
>
<button
v-for="emoji in EMOJIS"
:key="emoji"
class="rounded p-0.5 text-sm transition-transform hover:scale-125 hover:bg-muted"
@click="toggleReaction(comment, emoji)"
>
{{ emoji }}
</button>
class="absolute -top-10 right-2 z-50 flex items-center gap-0.5 rounded-lg border bg-popover p-1.5 shadow-md" >
<button v-for="emoji in EMOJIS" :key="emoji" class="rounded p-0.5 text-sm transition-transform hover:scale-125 hover:bg-muted" @click="toggleReaction(comment, emoji)">{{ emoji }}</button>
</div>
</div>
</template>
@ -547,9 +557,9 @@ watch(() => props.postId, () => {
<div
v-for="reply in comment.replies"
:key="reply.id"
class="group relative ml-6 rounded-lg border-l-2 border-border p-2 pl-3 hover:bg-muted/50"
class="group relative ml-8 rounded-lg py-1.5 px-2 hover:bg-muted/50"
@mouseenter="hoveredCommentId = reply.id"
@mouseleave="hoveredCommentId = null"
@mouseleave="hoveredCommentId = null; emojiPickerCommentId = null"
>
<!-- Editing reply -->
<div v-if="editingComment?.id === reply.id" class="space-y-2">
@ -589,7 +599,7 @@ watch(() => props.postId, () => {
<p class="mt-0.5 whitespace-pre-wrap text-sm">{{ reply.body }}</p>
<!-- Reply reactions -->
<div v-if="groupedReactions(reply.reactions).length > 0" class="mt-1 flex flex-wrap gap-1">
<div v-if="groupedReactions(reply.reactions).length > 0" class="mt-1.5 flex flex-wrap gap-1">
<button
v-for="r in groupedReactions(reply.reactions)"
:key="r.emoji"
@ -601,66 +611,50 @@ watch(() => props.postId, () => {
<span class="text-[10px] text-muted-foreground">{{ r.count }}</span>
</button>
</div>
</div>
<!-- Reply hover actions -->
<!-- Reply floating toolbar -->
<div
v-if="hoveredCommentId === reply.id && editingComment?.id !== reply.id"
class="absolute -top-2 right-1 flex items-center gap-0.5 rounded-md border bg-background px-1 py-0.5 shadow-sm"
class="absolute -top-3 right-2 z-50 flex items-center gap-0.5 rounded-md border bg-background px-1 py-0.5 shadow-sm"
>
<TooltipProvider>
<TooltipProvider :delay-duration="200">
<Tooltip>
<TooltipTrigger as-child>
<button class="rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground" @click="toggleEmojiPicker(reply.id)">
<IconMoodSmile class="h-3.5 w-3.5" />
</button>
<button class="rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground" @mouseenter="showEmojiPicker(reply.id)"><IconMoodSmile class="h-3.5 w-3.5" /></button>
</TooltipTrigger>
<TooltipContent side="top"><span class="text-xs">React</span></TooltipContent>
<TooltipContent side="top" class="text-xs">React</TooltipContent>
</Tooltip>
<template v-if="reply.user_id === currentUserId">
<Tooltip>
<TooltipTrigger as-child>
<button class="rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground" @click="startEdit(reply)"><IconEdit class="h-3.5 w-3.5" /></button>
</TooltipTrigger>
<TooltipContent side="top" class="text-xs">{{ $t('comments.edit') }}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<button class="rounded p-1 text-muted-foreground hover:bg-muted hover:text-destructive" @click="deleteComment(reply)"><IconTrash class="h-3.5 w-3.5" /></button>
</TooltipTrigger>
<TooltipContent side="top" class="text-xs">{{ $t('comments.delete') }}</TooltipContent>
</Tooltip>
</template>
</TooltipProvider>
<template v-if="reply.user_id === currentUserId">
<TooltipProvider>
<Tooltip>
<TooltipTrigger as-child>
<button class="rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground" @click="startEdit(reply)">
<IconEdit class="h-3.5 w-3.5" />
</button>
</TooltipTrigger>
<TooltipContent side="top"><span class="text-xs">{{ $t('comments.edit') }}</span></TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger as-child>
<button class="rounded p-1 text-muted-foreground hover:bg-muted hover:text-destructive" @click="deleteComment(reply)">
<IconTrash class="h-3.5 w-3.5" />
</button>
</TooltipTrigger>
<TooltipContent side="top"><span class="text-xs">{{ $t('comments.delete') }}</span></TooltipContent>
</Tooltip>
</TooltipProvider>
</template>
</div>
<!-- Reply emoji picker -->
<div
v-if="emojiPickerCommentId === reply.id"
class="absolute -top-10 right-1 z-10 flex items-center gap-0.5 rounded-lg border bg-background p-1.5 shadow-md"
>
<button
v-for="emoji in EMOJIS"
:key="emoji"
class="rounded p-0.5 text-sm transition-transform hover:scale-125 hover:bg-muted"
@click="toggleReaction(reply, emoji)"
>
{{ emoji }}
</button>
class="absolute -top-10 right-2 z-50 flex items-center gap-0.5 rounded-lg border bg-popover p-1.5 shadow-md" >
<button v-for="emoji in EMOJIS" :key="emoji" class="rounded p-0.5 text-sm transition-transform hover:scale-125 hover:bg-muted" @click="toggleReaction(reply, emoji)">{{ emoji }}</button>
</div>
</div>
</template>
</div>
</template>
</template>
</template>
</div>
<!-- Loading spinner for initial load -->
@ -673,7 +667,7 @@ watch(() => props.postId, () => {
<div class="shrink-0 border-t p-2">
<!-- Replying to indicator -->
<div v-if="replyingTo" class="mb-1.5 flex items-center gap-1.5 text-xs text-muted-foreground">
<IconCornerDownRight class="h-3 w-3" />
<IconArrowBackUp class="h-3 w-3" />
<span>{{ $t('comments.replying_to', { name: replyingTo.user.name }) }}</span>
<button class="ml-auto rounded p-0.5 hover:bg-muted" @click="cancelReply">
<IconX class="h-3 w-3" />