mirror of
https://github.com/ngthanhvu/kr-phim.git
synced 2026-08-10 14:07:47 +00:00
feat: change libraly icons
This commit is contained in:
@@ -1,15 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { CirclePlay, Globe2, MessageCircle, Music2, Send } from 'lucide-vue-next'
|
||||
|
||||
const currentYear = new Date().getFullYear()
|
||||
|
||||
const socialLinks = [
|
||||
{ label: 'Telegram', icon: Send },
|
||||
{ label: 'Chat', icon: MessageCircle },
|
||||
{ label: 'Website', icon: Globe2 },
|
||||
{ label: 'Cộng đồng', icon: MessageCircle },
|
||||
{ label: 'TikTok', icon: Music2 },
|
||||
{ label: 'Video', icon: CirclePlay },
|
||||
{ label: 'Telegram', icon: 'send' as const },
|
||||
{ label: 'Chat', icon: 'message-circle' as const },
|
||||
{ label: 'Website', icon: 'globe' as const },
|
||||
{ label: 'Cộng đồng', icon: 'message-circle' as const },
|
||||
{ label: 'TikTok', icon: 'music' as const },
|
||||
{ label: 'Video', icon: 'circle-play' as const },
|
||||
]
|
||||
|
||||
const linkGroups = [
|
||||
@@ -38,7 +36,7 @@ const linkGroups = [
|
||||
class="grid size-12 place-items-center rounded-full bg-yellow-400 text-slate-950 shadow-lg shadow-yellow-950/30 transition hover:-translate-y-0.5 hover:bg-white"
|
||||
:aria-label="item.label"
|
||||
>
|
||||
<component :is="item.icon" class="size-5" />
|
||||
<AppIcon :name="item.icon" class="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Heart,
|
||||
History,
|
||||
LogOut,
|
||||
Menu,
|
||||
Search,
|
||||
Settings,
|
||||
User,
|
||||
X,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const keyword = ref(typeof route.query.q === 'string' ? route.query.q : '')
|
||||
@@ -42,12 +31,12 @@ const navItems = [
|
||||
|
||||
const memberMenuItems = computed(() => {
|
||||
const items = [
|
||||
{ label: 'Trang cá nhân', icon: User, to: '/thanh-vien' },
|
||||
{ label: 'Yêu thích', icon: Heart, to: '/yeu-thich' },
|
||||
{ label: 'Lịch sử', icon: History, to: '/lich-su' },
|
||||
{ label: 'Trang cá nhân', icon: 'user' as const, to: '/thanh-vien' },
|
||||
{ label: 'Yêu thích', icon: 'heart' as const, to: '/yeu-thich' },
|
||||
{ label: 'Lịch sử', icon: 'history' as const, to: '/lich-su' },
|
||||
]
|
||||
if (user.value?.role === 'admin') {
|
||||
items.push({ label: 'Trang quản trị', icon: Settings, to: '/admin' })
|
||||
items.push({ label: 'Trang quản trị', icon: 'settings' as const, to: '/admin' })
|
||||
}
|
||||
return items
|
||||
})
|
||||
@@ -109,7 +98,7 @@ watch(() => route.path, () => {
|
||||
<form
|
||||
class="ml-auto flex w-full max-w-xs items-center rounded-full border border-white/10 bg-white/8 px-4 py-2 shadow-2xl shadow-yellow-950/20 sm:max-w-sm"
|
||||
@submit.prevent="submitSearch">
|
||||
<Search class="mr-3 size-4 shrink-0 text-yellow-200" />
|
||||
<AppIcon name="search" class="mr-3 size-4 shrink-0 text-yellow-200" />
|
||||
<input v-model="keyword" type="search" placeholder="Tìm phim Hàn Quốc..."
|
||||
class="w-full bg-transparent text-sm text-white outline-none placeholder:text-slate-400">
|
||||
</form>
|
||||
@@ -151,14 +140,14 @@ watch(() => route.path, () => {
|
||||
</div>
|
||||
<NuxtLink v-for="item in memberMenuItems" :key="item.label" :to="item.to"
|
||||
class="flex h-10 w-full cursor-pointer items-center gap-2.5 px-4 transition hover:bg-white/[0.04] hover:text-white">
|
||||
<component :is="item.icon" class="size-4 shrink-0 text-white/50" />
|
||||
<AppIcon :name="item.icon" class="size-4 shrink-0 text-white/50" />
|
||||
<span class="text-[13px] font-medium text-white/70">{{ item.label }}</span>
|
||||
</NuxtLink>
|
||||
<div class="my-1.5 border-t border-white/[0.06]" />
|
||||
<button type="button"
|
||||
class="flex h-10 w-full cursor-pointer items-center gap-2.5 px-4 text-[13px] font-medium text-red-400/70 transition hover:bg-red-400/8 hover:text-red-400"
|
||||
@click="handleLogout">
|
||||
<LogOut class="size-4 shrink-0" />
|
||||
<AppIcon name="log-out" class="size-4 shrink-0" />
|
||||
Đăng xuất
|
||||
</button>
|
||||
</div>
|
||||
@@ -168,7 +157,7 @@ watch(() => route.path, () => {
|
||||
<button type="button"
|
||||
class="grid size-10 shrink-0 cursor-pointer place-items-center rounded-lg border border-white/10 bg-white/8 text-white transition hover:bg-white/16 lg:hidden"
|
||||
aria-label="Mở menu" @click="mobileMenuOpen = true">
|
||||
<Menu class="size-5" />
|
||||
<AppIcon name="menu" class="size-5" />
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
@@ -182,7 +171,7 @@ watch(() => route.path, () => {
|
||||
<button type="button"
|
||||
class="grid size-10 cursor-pointer place-items-center rounded-lg border border-white/10 bg-white/8 text-white transition hover:bg-white/16"
|
||||
aria-label="Đóng menu" @click="closeMobileMenu">
|
||||
<X class="size-5" />
|
||||
<AppIcon name="x" class="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -210,14 +199,14 @@ watch(() => route.path, () => {
|
||||
<NuxtLink v-for="item in memberMenuItems" :key="item.label" :to="item.to"
|
||||
class="flex h-11 w-full cursor-pointer items-center gap-3 px-4 text-left text-sm font-semibold text-slate-200 transition hover:bg-white/8 hover:text-white"
|
||||
@click="closeMobileMenu">
|
||||
<component :is="item.icon" class="size-4 shrink-0" />
|
||||
<AppIcon :name="item.icon" class="size-4 shrink-0" />
|
||||
{{ item.label }}
|
||||
</NuxtLink>
|
||||
<div class="my-2 border-t border-white/10" />
|
||||
<button type="button"
|
||||
class="flex h-11 w-full cursor-pointer items-center gap-3 px-4 text-left text-sm font-black text-red-400 transition hover:bg-white/8 hover:text-red-300"
|
||||
@click="handleLogout">
|
||||
<LogOut class="size-4 shrink-0" />
|
||||
<AppIcon name="log-out" class="size-4 shrink-0" />
|
||||
Đăng xuất
|
||||
</button>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import type { IconName } from '~/lib/icons'
|
||||
import { getIcon } from '~/lib/icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
|
||||
const props = defineProps<{
|
||||
name: IconName
|
||||
class?: string
|
||||
}>()
|
||||
|
||||
const icon = computed(() => getIcon(props.name))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FontAwesomeIcon :icon="icon" :class="props.class" />
|
||||
</template>
|
||||
@@ -1,14 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { Play } from 'lucide-vue-next'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template>
|
||||
<NuxtLink to="/"
|
||||
class="group inline-flex h-8 min-w-24 shrink-0 flex-row flex-nowrap items-center gap-2 whitespace-nowrap sm:h-10 sm:min-w-29.5 sm:gap-2.5"
|
||||
aria-label="CineK">
|
||||
<span
|
||||
class="grid size-7 shrink-0 place-items-center rounded-lg border border-yellow-200/80 bg-black/30 text-white shadow-lg shadow-black/20 transition group-hover:border-yellow-300 group-hover:text-yellow-200 sm:size-9">
|
||||
<Play class="ml-0.5 size-4 fill-current sm:size-5" />
|
||||
<AppIcon name="play" class="ml-0.5 size-4 fill-current sm:size-5" />
|
||||
</span>
|
||||
<span class="inline-flex items-baseline text-xl font-black leading-none tracking-normal sm:text-2xl">
|
||||
<span class="text-white">Cine</span><span class="text-yellow-300">K</span>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowLeft, Check, Eye, EyeOff, KeyRound, LockKeyhole, Mail, UserRound, X } from 'lucide-vue-next'
|
||||
|
||||
const open = defineModel<boolean>({ default: false })
|
||||
const { fetchUser } = useAuth()
|
||||
|
||||
@@ -84,7 +82,7 @@ watch(open, (val) => {
|
||||
<button type="button"
|
||||
class="absolute right-4 top-4 z-20 grid h-8 w-8 place-items-center rounded-full bg-black/20 text-white/70 backdrop-blur-sm transition hover:bg-black/40 hover:text-white"
|
||||
@click="closeModal">
|
||||
<X class="size-4" />
|
||||
<AppIcon name="x" class="size-4" />
|
||||
</button>
|
||||
|
||||
<div class="flex flex-col md:flex-row">
|
||||
@@ -132,12 +130,12 @@ watch(open, (val) => {
|
||||
<button type="button"
|
||||
class="mb-3 flex items-center gap-1.5 text-sm text-white/50 transition hover:text-white"
|
||||
@click="mode = 'login'">
|
||||
<ArrowLeft class="size-3.5" />
|
||||
<AppIcon name="arrow-left" class="size-3.5" />
|
||||
Quay lại đăng nhập
|
||||
</button>
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-[#FFD166]/10">
|
||||
<KeyRound class="size-5 text-[#FFD166]" />
|
||||
<AppIcon name="key" class="size-5 text-[#FFD166]" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-white">Quên mật khẩu</h2>
|
||||
@@ -151,7 +149,7 @@ watch(open, (val) => {
|
||||
<Transition name="form-slide" mode="out-in">
|
||||
<div v-if="mode === 'forgot'" :key="'forgot'" class="space-y-3">
|
||||
<div class="relative">
|
||||
<Mail class="absolute left-3.5 top-1/2 size-4 -translate-y-1/2 text-white/30" />
|
||||
<AppIcon name="mail" class="absolute left-3.5 top-1/2 size-4 -translate-y-1/2 text-white/30" />
|
||||
<input v-model="email" type="email" required placeholder="Email của bạn"
|
||||
class="h-12 w-full rounded-xl border border-white/[0.08] bg-white/[0.04] pl-11 pr-4 text-sm text-white placeholder:text-white/30 outline-none transition-all focus:border-[#FFD166]/40 focus:bg-white/[0.06] focus:ring-2 focus:ring-[#FFD166]/10">
|
||||
</div>
|
||||
@@ -159,28 +157,28 @@ watch(open, (val) => {
|
||||
<div v-else :key="mode" class="space-y-3">
|
||||
<Transition name="form-field">
|
||||
<div v-if="mode === 'register'" class="relative">
|
||||
<UserRound class="absolute left-3.5 top-1/2 size-4 -translate-y-1/2 text-white/30" />
|
||||
<AppIcon name="user-round" class="absolute left-3.5 top-1/2 size-4 -translate-y-1/2 text-white/30" />
|
||||
<input v-model="name" type="text" placeholder="Tên hiển thị"
|
||||
class="h-12 w-full rounded-xl border border-white/[0.08] bg-white/[0.04] pl-11 pr-4 text-sm text-white placeholder:text-white/30 outline-none transition-all focus:border-[#FFD166]/40 focus:bg-white/[0.06] focus:ring-2 focus:ring-[#FFD166]/10">
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<div class="relative">
|
||||
<Mail class="absolute left-3.5 top-1/2 size-4 -translate-y-1/2 text-white/30" />
|
||||
<AppIcon name="mail" class="absolute left-3.5 top-1/2 size-4 -translate-y-1/2 text-white/30" />
|
||||
<input v-model="email" type="email" required placeholder="Email hoặc số điện thoại"
|
||||
class="h-12 w-full rounded-xl border border-white/[0.08] bg-white/[0.04] pl-11 pr-4 text-sm text-white placeholder:text-white/30 outline-none transition-all focus:border-[#FFD166]/40 focus:bg-white/[0.06] focus:ring-2 focus:ring-[#FFD166]/10">
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<LockKeyhole class="absolute left-3.5 top-1/2 size-4 -translate-y-1/2 text-white/30" />
|
||||
<AppIcon name="lock" class="absolute left-3.5 top-1/2 size-4 -translate-y-1/2 text-white/30" />
|
||||
<input v-model="password" :type="showPassword ? 'text' : 'password'" required minlength="6"
|
||||
:placeholder="mode === 'login' ? 'Mật khẩu' : 'Mật khẩu (tối thiểu 6 ký tự)'"
|
||||
class="h-12 w-full rounded-xl border border-white/[0.08] bg-white/[0.04] pl-11 pr-11 text-sm text-white placeholder:text-white/30 outline-none transition-all focus:border-[#FFD166]/40 focus:bg-white/[0.06] focus:ring-2 focus:ring-[#FFD166]/10">
|
||||
<button type="button"
|
||||
class="absolute right-3 top-1/2 -translate-y-1/2 text-white/30 transition hover:text-white/60"
|
||||
@click="showPassword = !showPassword">
|
||||
<EyeOff v-if="showPassword" class="size-4" />
|
||||
<Eye v-else class="size-4" />
|
||||
<AppIcon v-if="showPassword" name="eye-off" class="size-4" />
|
||||
<AppIcon v-else name="eye" class="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -241,7 +239,7 @@ watch(open, (val) => {
|
||||
|
||||
<Transition name="form-message">
|
||||
<p v-if="success" class="mt-4 flex items-center justify-center gap-2 rounded-xl bg-green-500/10 p-3 text-sm font-medium text-green-400">
|
||||
<Check class="size-4 shrink-0" />
|
||||
<AppIcon name="check" class="size-4 shrink-0" />
|
||||
{{ success }}
|
||||
</p>
|
||||
</Transition>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import { CornerDownLeft, Crown, Image, Loader2, ThumbsDown, ThumbsUp, Trash2, User, Venus, Mars, Minus } from 'lucide-vue-next'
|
||||
|
||||
const props = defineProps<{
|
||||
replies: any[]
|
||||
depth?: number
|
||||
@@ -41,11 +39,11 @@ function formatMentions(content: string) {
|
||||
:class="reply.userRole === 'admin' ? 'ring-2 ring-yellow-400 ring-offset-1 ring-offset-[#0d0f17]' : ''">
|
||||
<img v-if="reply.userAvatar" :src="reply.userAvatar"
|
||||
class="size-8 rounded-full object-cover" alt="" />
|
||||
<User v-else class="size-3.5 text-slate-500" />
|
||||
<AppIcon name="user" v-else class="size-3.5 text-slate-500" />
|
||||
</div>
|
||||
<div v-if="reply.userRole === 'admin'"
|
||||
class="absolute -top-0.5 -left-0.5 size-3.5 bg-cinek-500 rounded-full flex items-center justify-center">
|
||||
<Crown class="size-2 text-slate-950" />
|
||||
class="absolute -top-0.5 -left-0.5 size-3 bg-yellow-400 rounded-full flex items-center justify-center ring-1 ring-[#0d0f17]">
|
||||
<svg class="size-1.5 text-slate-950" viewBox="0 0 24 24" fill="currentColor"><path d="M5 16L3 5l5.5 5L12 4l3.5 6L21 5l-2 11H5z"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
@@ -53,9 +51,9 @@ function formatMentions(content: string) {
|
||||
<span v-if="reply.userRole === 'admin'"
|
||||
class="px-1.5 py-0.5 rounded text-[9px] font-black bg-yellow-400 text-slate-900">ADMIN</span>
|
||||
<span class="text-xs font-semibold text-white">{{ reply.userName }}</span>
|
||||
<Venus v-if="reply.userGender === 'female'" class="size-3 text-pink-400" />
|
||||
<Mars v-if="reply.userGender === 'male'" class="size-3 text-blue-400" />
|
||||
<Minus v-if="reply.userGender === 'other'" class="size-3 text-purple-400" />
|
||||
<AppIcon name="venus" v-if="reply.userGender === 'female'" class="size-3 text-pink-400" />
|
||||
<AppIcon name="mars" v-if="reply.userGender === 'male'" class="size-3 text-blue-400" />
|
||||
<AppIcon name="minus" v-if="reply.userGender === 'other'" class="size-3 text-purple-400" />
|
||||
<span class="text-xs text-slate-600">{{ reply.timeAgo || '' }}</span>
|
||||
</div>
|
||||
<p class="text-xs text-slate-400 whitespace-pre-wrap wrap-break-word leading-relaxed cursor-pointer select-none py-1 transition-all duration-200"
|
||||
@@ -67,22 +65,22 @@ function formatMentions(content: string) {
|
||||
<button type="button" @click="handleVote(reply.id, reply.userVote === 1 ? 0 : 1)"
|
||||
class="flex items-center gap-1 text-xs"
|
||||
:class="reply.userVote === 1 ? 'text-cinek-400' : 'text-slate-600 hover:text-slate-400'">
|
||||
<ThumbsUp class="size-3" /><span v-if="reply.likeCount">{{ reply.likeCount }}</span>
|
||||
<AppIcon name="thumbs-up" class="size-3" /><span v-if="reply.likeCount">{{ reply.likeCount }}</span>
|
||||
</button>
|
||||
<button type="button" @click="handleVote(reply.id, reply.userVote === -1 ? 0 : -1)"
|
||||
class="flex items-center gap-1 text-xs"
|
||||
:class="reply.userVote === -1 ? 'text-red-400' : 'text-slate-600 hover:text-slate-400'">
|
||||
<ThumbsDown class="size-3" /><span v-if="reply.dislikeCount">{{ reply.dislikeCount }}</span>
|
||||
<AppIcon name="thumbs-down" class="size-3" /><span v-if="reply.dislikeCount">{{ reply.dislikeCount }}</span>
|
||||
</button>
|
||||
<button v-if="depth < maxDepth" type="button" @click="emit('start-reply', reply.id)"
|
||||
class="flex items-center gap-1 text-xs text-slate-600 hover:text-slate-400">
|
||||
<CornerDownLeft class="size-3" /><span>Trả lời</span>
|
||||
<AppIcon name="corner-down-left" class="size-3" /><span>Trả lời</span>
|
||||
</button>
|
||||
<button v-if="user && (user.id === reply.userId || user.role === 'admin')" type="button"
|
||||
@click="emit('delete', reply.id)" class="text-xs text-slate-600 hover:text-red-400 disabled:opacity-50"
|
||||
:disabled="props.isDeleting?.has(reply.id)">
|
||||
<Loader2 v-if="props.isDeleting?.has(reply.id)" class="size-3 animate-spin" />
|
||||
<Trash2 v-else class="size-3" />
|
||||
<AppIcon name="loader" v-if="props.isDeleting?.has(reply.id)" class="size-3 animate-spin" />
|
||||
<AppIcon name="trash" v-else class="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
<!-- Reply form -->
|
||||
@@ -103,18 +101,20 @@ function formatMentions(content: string) {
|
||||
<span class="text-xs text-slate-500">Tiết lộ</span>
|
||||
</div>
|
||||
<button type="button" class="flex items-center gap-1.5 text-xs text-slate-500 hover:text-slate-300 transition">
|
||||
<Image class="size-4" /><span>GIF</span>
|
||||
<AppIcon name="image" class="size-4" /><span>GIF</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-xs text-slate-600">{{ (replyContent[reply.id] || '').length }} / 1000</span>
|
||||
<span class="text-xs text-white/30">{{ (replyContent[reply.id] || '').length }} / 1000</span>
|
||||
<button type="button" @click="emit('cancel-reply', reply.id)"
|
||||
class="px-3 py-1.5 rounded-lg text-xs text-slate-500 hover:text-white transition">Hủy</button>
|
||||
class="px-3 py-1.5 rounded-lg text-xs text-white/50 hover:text-white transition">Hủy</button>
|
||||
<button type="button" @click="emit('submit-reply', reply.id)"
|
||||
class="px-4 py-1.5 rounded-lg bg-cinek-500 text-xs font-bold text-slate-950 hover:bg-cinek-400 transition"
|
||||
class="flex items-center gap-2 text-sm font-semibold transition disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
:class="(replyContent[reply.id] || '').trim() ? 'text-[#FFD166] hover:text-[#FFC845]' : 'text-white/30'"
|
||||
:disabled="props.isSubmittingReply?.[reply.id]">
|
||||
<Loader2 v-if="props.isSubmittingReply?.[reply.id]" class="size-3 animate-spin inline" />
|
||||
<span v-else>Gửi</span>
|
||||
<span>Gửi</span>
|
||||
<AppIcon name="loader" v-if="props.isSubmittingReply?.[reply.id]" class="size-4 animate-spin" />
|
||||
<AppIcon name="send" v-else class="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+183
-174
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import { ChevronDown, ChevronUp, CornerDownLeft, Crown, Image, Loader2, MessageSquare, Pin, Send, ThumbsDown, ThumbsUp, Trash2, User, Venus, Mars, Minus } from 'lucide-vue-next'
|
||||
|
||||
const props = defineProps<{
|
||||
source: string
|
||||
@@ -348,7 +347,7 @@ watch(() => props.slug, () => {
|
||||
<template>
|
||||
<div class="mt-10 border-t border-white/10 pt-8">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<MessageSquare class="size-5 text-yellow-300" />
|
||||
<AppIcon name="message-square" class="size-5 text-yellow-300" />
|
||||
<h2 class="text-xl font-bold text-white">Bình luận</h2>
|
||||
<span class="text-sm text-slate-400">({{ comments.length }})</span>
|
||||
</div>
|
||||
@@ -357,203 +356,213 @@ watch(() => props.slug, () => {
|
||||
<p class="text-sm text-slate-400">Đăng nhập để bình luận về phim này.</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="mb-8 p-4 rounded-xl bg-[#13151f] border border-white/5">
|
||||
<div class="flex gap-3">
|
||||
<div class="size-9 rounded-full bg-white/10 flex items-center justify-center shrink-0 mt-0.5">
|
||||
<img v-if="user?.avatar" :src="user.avatar" class="size-9 rounded-full object-cover" alt="" />
|
||||
<User v-else class="size-5 text-slate-500" />
|
||||
<div v-else class="mb-8 rounded-2xl bg-[#181a24] border border-white/5 overflow-hidden">
|
||||
<div class="flex items-center gap-3 px-5 pt-4 pb-2">
|
||||
<div class="size-10 rounded-full bg-white/10 flex items-center justify-center shrink-0">
|
||||
<img v-if="user?.avatar" :src="user.avatar" class="size-10 rounded-full object-cover" alt="" />
|
||||
<AppIcon name="user" v-else class="size-5 text-slate-500" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<textarea v-model="commentContent" @input="triggerAutoSaveDraft" placeholder="Viết bình luận..." rows="3"
|
||||
maxlength="1000"
|
||||
class="w-full rounded-lg bg-[#0d0f17] border border-white/10 px-3 py-2.5 text-sm text-white placeholder:text-slate-600 focus:border-cinek-500/50 focus:outline-none resize-none transition" />
|
||||
<div class="flex items-center justify-between mt-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-slate-500">Tiết lộ</span>
|
||||
<button type="button" @click="isContentHidden = !isContentHidden"
|
||||
class="relative w-9 h-5 rounded-full transition-colors"
|
||||
:class="isContentHidden ? 'bg-cinek-500' : 'bg-white/20'">
|
||||
<span class="absolute top-0.5 left-0.5 size-4 rounded-full bg-white shadow transition-transform"
|
||||
:class="isContentHidden ? 'translate-x-4' : 'translate-x-0'" />
|
||||
</button>
|
||||
<span class="text-xs" :class="isContentHidden ? 'text-cinek-400' : 'text-slate-500'">Không tiết lộ</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-xs text-slate-600">{{ commentContent.length }} / 1000</span>
|
||||
<button type="button" :disabled="!commentContent.trim() || isCommentSubmitting"
|
||||
@click="handleSubmitComment"
|
||||
class="flex items-center gap-1.5 rounded-lg bg-cinek-500 px-4 py-1.5 text-xs font-bold text-slate-950 hover:bg-cinek-400 disabled:cursor-not-allowed disabled:opacity-40 transition">
|
||||
<Loader2 v-if="isCommentSubmitting" class="size-3.5 animate-spin" />
|
||||
<Send class="size-3.5" v-else />
|
||||
<span>Gửi</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hasDraft" class="mt-2 flex items-center gap-2">
|
||||
<span class="text-xs text-yellow-400/70 flex items-center gap-1">
|
||||
<span class="size-1.5 rounded-full bg-yellow-400/50 inline-block" />
|
||||
Có nháp đã lưu
|
||||
</span>
|
||||
<button type="button" @click="handleDeleteDraft"
|
||||
class="text-xs text-slate-500 hover:text-red-400 transition">Xóa nháp</button>
|
||||
<span class="text-sm font-semibold text-white">{{ user.name || user.email?.split('@')[0] }}</span>
|
||||
</div>
|
||||
<div class="px-5">
|
||||
<textarea v-model="commentContent" @input="triggerAutoSaveDraft" placeholder="Viết bình luận" rows="3"
|
||||
maxlength="1000"
|
||||
class="w-full rounded-xl bg-[#0f1118] border border-white/5 px-4 py-3 text-sm text-white placeholder:text-slate-600 focus:border-white/10 focus:outline-none resize-none transition" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between px-5 py-4">
|
||||
<div class="flex items-center gap-5">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<button type="button" @click="isContentHidden = !isContentHidden"
|
||||
class="relative w-10 h-6 rounded-full transition-colors"
|
||||
:class="isContentHidden ? 'bg-[#FFD166]' : 'bg-white/15'">
|
||||
<span class="absolute top-1 size-4 rounded-full bg-white shadow transition-transform"
|
||||
:class="isContentHidden ? 'left-5' : 'left-1'" />
|
||||
</button>
|
||||
<span class="text-xs font-medium" :class="isContentHidden ? 'text-[#FFD166]' : 'text-white/60'">Không tiết
|
||||
lộ</span>
|
||||
</div>
|
||||
<button type="button" class="flex items-center gap-1.5 text-xs text-white/50 hover:text-white/80 transition">
|
||||
<AppIcon name="image" class="size-4" /><span class="text-white/50">GIF</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="text-xs text-white/30">{{ commentContent.length }} / 1000</span>
|
||||
<button type="button" :disabled="!commentContent.trim() || isCommentSubmitting" @click="handleSubmitComment"
|
||||
class="flex items-center gap-2 text-sm font-semibold transition disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
:class="commentContent.trim() ? 'text-[#FFD166] hover:text-[#FFC845]' : 'text-white/30'">
|
||||
<span>Gửi</span>
|
||||
<AppIcon name="loader" v-if="isCommentSubmitting" class="size-4 animate-spin" />
|
||||
<AppIcon name="send" class="size-4" v-else />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hasDraft" class="px-5 pb-3 flex items-center gap-2">
|
||||
<span class="text-xs text-yellow-400/70 flex items-center gap-1">
|
||||
<span class="size-1.5 rounded-full bg-yellow-400/50 inline-block" />
|
||||
Có nháp đã lưu
|
||||
</span>
|
||||
<button type="button" @click="handleDeleteDraft"
|
||||
class="text-xs text-slate-500 hover:text-red-400 transition">Xóa nháp</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isCommentsLoading" class="space-y-4">
|
||||
<div v-for="i in 2" :key="i" class="flex items-start gap-3 p-4 bg-[#13151f] rounded-xl animate-pulse">
|
||||
<div class="size-10 rounded-full bg-white/5 shrink-0" />
|
||||
<div class="flex-1 space-y-2">
|
||||
<div class="h-3.5 bg-white/5 rounded w-20" />
|
||||
<div class="h-3 bg-white/5 rounded w-full" />
|
||||
<div class="h-3 bg-white/5 rounded w-3/4" />
|
||||
</div>
|
||||
<div class="size-10 rounded-full bg-white/5 shrink-0" />
|
||||
<div class="flex-1 space-y-2">
|
||||
<div class="h-3.5 bg-white/5 rounded w-20" />
|
||||
<div class="h-3 bg-white/5 rounded w-full" />
|
||||
<div class="h-3 bg-white/5 rounded w-3/4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TransitionGroup v-else-if="comments.length" name="comment-list" tag="div" class="space-y-1">
|
||||
<div v-for="comment in comments" :key="comment.id" class="mb-2">
|
||||
<div class="transition"
|
||||
:class="comment.pinned ? 'rounded-xl p-4 bg-yellow-500/5 border border-yellow-500/20' : 'py-4 border-b border-white/5'">
|
||||
<div v-if="comment.pinned" class="flex items-center gap-1.5 mb-3 text-xs text-yellow-400/80 font-medium">
|
||||
<Pin class="size-3" /> Ghim bởi Admin
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<div class="shrink-0 relative">
|
||||
<div class="size-10 rounded-full bg-white/10 flex items-center justify-center"
|
||||
:class="comment.userRole === 'admin' ? 'ring-2 ring-yellow-400 ring-offset-2 ring-offset-[#0d0f17]' : ''">
|
||||
<img v-if="comment.userAvatar" :src="comment.userAvatar"
|
||||
class="size-10 rounded-full object-cover" alt="" />
|
||||
<User v-else class="size-5 text-slate-500" />
|
||||
</div>
|
||||
<div v-if="comment.userRole === 'admin'"
|
||||
class="absolute -top-1 -left-1 size-4 bg-cinek-500 rounded-full flex items-center justify-center">
|
||||
<Crown class="size-2.5 text-slate-950" />
|
||||
</div>
|
||||
<TransitionGroup v-else-if="comments.length" name="comment-list" tag="div" class="space-y-1">
|
||||
<div v-for="comment in comments" :key="comment.id" class="mb-2">
|
||||
<div class="transition"
|
||||
:class="comment.pinned ? 'rounded-xl p-4 bg-yellow-500/5 border border-yellow-500/20' : 'py-4 border-b border-white/5'">
|
||||
<div v-if="comment.pinned" class="flex items-center gap-1.5 mb-3 text-xs text-yellow-400/80 font-medium">
|
||||
<AppIcon name="pin" class="size-3" /> Ghim bởi Admin
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<div class="shrink-0 relative">
|
||||
<div class="size-10 rounded-full bg-white/10 flex items-center justify-center"
|
||||
:class="comment.userRole === 'admin' ? 'ring-2 ring-yellow-400 ring-offset-2 ring-offset-[#0d0f17]' : ''">
|
||||
<img v-if="comment.userAvatar" :src="comment.userAvatar" class="size-10 rounded-full object-cover"
|
||||
alt="" />
|
||||
<AppIcon name="user" v-else class="size-5 text-slate-500" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex flex-wrap items-center gap-2 mb-1">
|
||||
<span v-if="comment.pinned"
|
||||
class="flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-medium bg-cinek-500/20 text-cinek-400">
|
||||
<Pin class="size-3" />Đã ghim
|
||||
</span>
|
||||
<span v-if="comment.userRole === 'admin'"
|
||||
class="px-2 py-0.5 rounded text-[10px] font-black bg-yellow-400 text-slate-900 shadow-sm">ADMIN</span>
|
||||
<span class="text-sm font-semibold text-white">{{ comment.userName }}</span>
|
||||
<Venus v-if="comment.userGender === 'female'" class="size-3.5 text-pink-400" />
|
||||
<Mars v-if="comment.userGender === 'male'" class="size-3.5 text-blue-400" />
|
||||
<Minus v-if="comment.userGender === 'other'" class="size-3.5 text-purple-400" />
|
||||
<span class="text-xs text-slate-600">{{ timeAgo(comment.createdAt) }}</span>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<p class="text-sm text-slate-300 whitespace-pre-wrap wrap-break-word cursor-pointer select-none py-1.5 transition-all duration-200"
|
||||
:class="comment.anonymous && !revealedComments.has(comment.id) ? 'blur-sm opacity-60' : ''"
|
||||
v-html="formatMentions(comment.content)"
|
||||
@click="comment.anonymous && (revealedComments.has(comment.id) ? revealedComments.delete(comment.id) : revealedComments.add(comment.id))">
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 mt-2.5">
|
||||
<button type="button" @click="handleVote(comment.id, comment.userVote === 1 ? 0 : 1)"
|
||||
class="flex items-center gap-1 text-xs"
|
||||
:class="comment.userVote === 1 ? 'text-cinek-400' : 'text-slate-500 hover:text-slate-300'">
|
||||
<ThumbsUp class="size-3.5" /><span v-if="comment.likeCount">{{ comment.likeCount }}</span>
|
||||
</button>
|
||||
<button type="button" @click="handleVote(comment.id, comment.userVote === -1 ? 0 : -1)"
|
||||
class="flex items-center gap-1 text-xs"
|
||||
:class="comment.userVote === -1 ? 'text-red-400' : 'text-slate-500 hover:text-slate-300'">
|
||||
<ThumbsDown class="size-3.5" /><span v-if="comment.dislikeCount">{{ comment.dislikeCount }}</span>
|
||||
</button>
|
||||
<button type="button" @click="startReply(comment.id)"
|
||||
class="flex items-center gap-1.5 text-xs text-slate-500 hover:text-slate-300">
|
||||
<CornerDownLeft class="size-3.5" /><span>Trả lời</span>
|
||||
</button>
|
||||
<button v-if="user && user.role === 'admin'" type="button" @click="handleTogglePin(comment.id)"
|
||||
class="flex items-center gap-1.5 text-xs"
|
||||
:class="comment.pinned ? 'text-cinek-400 hover:text-cinek-300' : 'text-slate-600 hover:text-slate-400'">
|
||||
<Pin class="size-3.5" /><span>{{ comment.pinned ? 'Bỏ ghim' : 'Ghim' }}</span>
|
||||
</button>
|
||||
<button v-if="user && (user.id === comment.userId || user.role === 'admin')" type="button"
|
||||
@click="handleDeleteComment(comment.id)"
|
||||
class="flex items-center gap-1.5 text-xs text-slate-600 hover:text-red-400">
|
||||
<Trash2 class="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="isReplyingTo === comment.id" class="mt-4">
|
||||
<div class="rounded-xl border border-white/10 bg-[#13151f] p-4">
|
||||
<textarea v-model="replyContent[comment.id]" rows="2" maxlength="1000"
|
||||
class="w-full rounded-lg bg-[#0d0f17] border border-white/10 px-3 py-2 text-sm text-white placeholder:text-slate-600 focus:border-cinek-500/50 focus:outline-none resize-none transition"
|
||||
placeholder="Viết trả lời..." />
|
||||
<div class="flex items-center justify-between mt-3">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="button" @click="isReplyAnonymous[comment.id] = !isReplyAnonymous[comment.id]"
|
||||
class="relative w-9 h-5 rounded-full transition-colors"
|
||||
:class="(isReplyAnonymous[comment.id] ?? false) ? 'bg-cinek-500' : 'bg-white/20'">
|
||||
<span class="absolute top-0.5 left-0.5 size-4 rounded-full bg-white shadow transition-transform"
|
||||
:class="(isReplyAnonymous[comment.id] ?? false) ? 'translate-x-4' : 'translate-x-0'" />
|
||||
</button>
|
||||
<span class="text-xs text-slate-500">Tiết lộ</span>
|
||||
</div>
|
||||
<button type="button" class="flex items-center gap-1.5 text-xs text-slate-500 hover:text-slate-300 transition">
|
||||
<Image class="size-4" /><span>GIF</span>
|
||||
<div v-if="comment.userRole === 'admin'"
|
||||
class="absolute -top-0.5 -left-0.5 size-3.5 bg-yellow-400 rounded-full flex items-center justify-center ring-2 ring-[#0d0f17]">
|
||||
<svg class="size-2 text-slate-950" viewBox="0 0 24 24" fill="currentColor"><path d="M5 16L3 5l5.5 5L12 4l3.5 6L21 5l-2 11H5z"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex flex-wrap items-center gap-2 mb-1">
|
||||
<span v-if="comment.pinned"
|
||||
class="flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-medium bg-cinek-500/20 text-cinek-400">
|
||||
<AppIcon name="pin" class="size-3" />Đã ghim
|
||||
</span>
|
||||
<span v-if="comment.userRole === 'admin'"
|
||||
class="px-2 py-0.5 rounded text-[10px] font-black bg-yellow-400 text-slate-900 shadow-sm">ADMIN</span>
|
||||
<span class="text-sm font-semibold text-white">{{ comment.userName }}</span>
|
||||
<AppIcon name="venus" v-if="comment.userGender === 'female'" class="size-3.5 text-pink-400" />
|
||||
<AppIcon name="mars" v-if="comment.userGender === 'male'" class="size-3.5 text-blue-400" />
|
||||
<AppIcon name="minus" v-if="comment.userGender === 'other'" class="size-3.5 text-purple-400" />
|
||||
<span class="text-xs text-slate-600">{{ timeAgo(comment.createdAt) }}</span>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<p class="text-sm text-slate-300 whitespace-pre-wrap wrap-break-word cursor-pointer select-none py-1.5 transition-all duration-200"
|
||||
:class="comment.anonymous && !revealedComments.has(comment.id) ? 'blur-sm opacity-60' : ''"
|
||||
v-html="formatMentions(comment.content)"
|
||||
@click="comment.anonymous && (revealedComments.has(comment.id) ? revealedComments.delete(comment.id) : revealedComments.add(comment.id))">
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 mt-2.5">
|
||||
<button type="button" @click="handleVote(comment.id, comment.userVote === 1 ? 0 : 1)"
|
||||
class="flex items-center gap-1 text-xs"
|
||||
:class="comment.userVote === 1 ? 'text-cinek-400' : 'text-slate-500 hover:text-slate-300'">
|
||||
<AppIcon name="thumbs-up" class="size-3.5" /><span v-if="comment.likeCount">{{ comment.likeCount }}</span>
|
||||
</button>
|
||||
<button type="button" @click="handleVote(comment.id, comment.userVote === -1 ? 0 : -1)"
|
||||
class="flex items-center gap-1 text-xs"
|
||||
:class="comment.userVote === -1 ? 'text-red-400' : 'text-slate-500 hover:text-slate-300'">
|
||||
<AppIcon name="thumbs-down" class="size-3.5" /><span v-if="comment.dislikeCount">{{ comment.dislikeCount }}</span>
|
||||
</button>
|
||||
<button type="button" @click="startReply(comment.id)"
|
||||
class="flex items-center gap-1.5 text-xs text-slate-500 hover:text-slate-300">
|
||||
<AppIcon name="corner-down-left" class="size-3.5" /><span>Trả lời</span>
|
||||
</button>
|
||||
<button v-if="user && user.role === 'admin'" type="button" @click="handleTogglePin(comment.id)"
|
||||
class="flex items-center gap-1.5 text-xs"
|
||||
:class="comment.pinned ? 'text-cinek-400 hover:text-cinek-300' : 'text-slate-600 hover:text-slate-400'">
|
||||
<AppIcon name="pin" class="size-3.5" /><span>{{ comment.pinned ? 'Bỏ ghim' : 'Ghim' }}</span>
|
||||
</button>
|
||||
<button v-if="user && (user.id === comment.userId || user.role === 'admin')" type="button"
|
||||
@click="handleDeleteComment(comment.id)"
|
||||
class="flex items-center gap-1.5 text-xs text-slate-600 hover:text-red-400">
|
||||
<AppIcon name="trash" class="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="isReplyingTo === comment.id" class="mt-4">
|
||||
<div class="rounded-xl border border-white/10 bg-[#13151f] p-4">
|
||||
<textarea v-model="replyContent[comment.id]" rows="2" maxlength="1000"
|
||||
class="w-full rounded-lg bg-[#0d0f17] border border-white/10 px-3 py-2 text-sm text-white placeholder:text-slate-600 focus:border-cinek-500/50 focus:outline-none resize-none transition"
|
||||
placeholder="Viết trả lời..." />
|
||||
<div class="flex items-center justify-between mt-3">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="button" @click="isReplyAnonymous[comment.id] = !isReplyAnonymous[comment.id]"
|
||||
class="relative w-9 h-5 rounded-full transition-colors"
|
||||
:class="(isReplyAnonymous[comment.id] ?? false) ? 'bg-cinek-500' : 'bg-white/20'">
|
||||
<span class="absolute top-0.5 left-0.5 size-4 rounded-full bg-white shadow transition-transform"
|
||||
:class="(isReplyAnonymous[comment.id] ?? false) ? 'translate-x-4' : 'translate-x-0'" />
|
||||
</button>
|
||||
<span class="text-xs text-slate-500">Tiết lộ</span>
|
||||
</div>
|
||||
<button type="button"
|
||||
class="flex items-center gap-1.5 text-xs text-slate-500 hover:text-slate-300 transition">
|
||||
<AppIcon name="image" class="size-4" /><span>GIF</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-xs text-white/30">{{ (replyContent[comment.id] || '').length }} / 1000</span>
|
||||
<button type="button" @click="cancelReply(comment.id)"
|
||||
class="px-3 py-1.5 rounded-lg text-xs text-white/50 hover:text-white transition">Hủy</button>
|
||||
<button type="button" @click="submitReply(comment.id)"
|
||||
class="flex items-center gap-2 text-sm font-semibold transition disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
:class="(replyContent[comment.id] || '').trim() ? 'text-[#FFD166] hover:text-[#FFC845]' : 'text-white/30'">
|
||||
<span>Gửi</span>
|
||||
<AppIcon name="send" class="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-xs text-slate-600">{{ (replyContent[comment.id] || '').length }} / 1000</span>
|
||||
<button type="button" @click="cancelReply(comment.id)"
|
||||
class="px-3 py-1.5 rounded-lg text-xs text-slate-500 hover:text-white transition">Hủy</button>
|
||||
<button type="button" @click="submitReply(comment.id)"
|
||||
class="px-4 py-1.5 rounded-lg bg-cinek-500 text-xs font-bold text-slate-950 hover:bg-cinek-400 transition">Gửi</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="comment.replies?.length" class="mt-4">
|
||||
<button @click="toggleExpand(comment.id)"
|
||||
class="flex items-center gap-1.5 text-xs text-cinek-400 hover:text-cinek-300 transition mb-2">
|
||||
<ChevronUp v-if="expandedComments.has(comment.id)" class="size-3" />
|
||||
<ChevronDown v-else class="size-3" />
|
||||
<span>{{ expandedComments.has(comment.id) ? 'Ẩn phản hồi' : `Hiển thị
|
||||
${countAllReplies(comment.replies)} phản hồi` }}</span>
|
||||
</button>
|
||||
<div class="replies-container pl-3 border-l-2 border-white/10"
|
||||
:class="expandedComments.has(comment.id) ? 'expanded' : 'collapsed'">
|
||||
<div class="space-y-3">
|
||||
<CommentReplies :replies="comment.replies" :depth="0" :user="user" :parent-id="comment.id"
|
||||
:is-replying-to="isReplyingTo" :reply-content="replyContent"
|
||||
:is-reply-anonymous="isReplyAnonymous" :is-submitting-reply="isSubmittingReply"
|
||||
@start-reply="startReply" @cancel-reply="cancelReply" @submit-reply="submitReply"
|
||||
@vote="handleVote" @delete="handleDeleteComment" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="comment.replies?.length" class="mt-4">
|
||||
<button @click="toggleExpand(comment.id)"
|
||||
class="flex items-center gap-1.5 text-xs text-cinek-400 hover:text-cinek-300 transition mb-2">
|
||||
<AppIcon name="chevron-up" v-if="expandedComments.has(comment.id)" class="size-3" />
|
||||
<AppIcon name="chevron-down" v-else class="size-3" />
|
||||
<span>{{ expandedComments.has(comment.id) ? 'Ẩn phản hồi' : `Hiển thị
|
||||
${countAllReplies(comment.replies)} phản hồi` }}</span>
|
||||
</button>
|
||||
<div class="replies-container pl-3 border-l-2 border-white/10"
|
||||
:class="expandedComments.has(comment.id) ? 'expanded' : 'collapsed'">
|
||||
<div class="space-y-3">
|
||||
<CommentReplies :replies="comment.replies" :depth="0" :user="user" :parent-id="comment.id"
|
||||
:is-replying-to="isReplyingTo" :reply-content="replyContent" :is-reply-anonymous="isReplyAnonymous"
|
||||
:is-submitting-reply="isSubmittingReply" @start-reply="startReply" @cancel-reply="cancelReply"
|
||||
@submit-reply="submitReply" @vote="handleVote" @delete="handleDeleteComment" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
|
||||
<!-- Load More Button -->
|
||||
<div v-if="hasMoreComments()" class="flex justify-center mt-6">
|
||||
<button type="button" @click="loadMoreComments"
|
||||
class="flex items-center gap-2 rounded-lg bg-white/5 border border-white/10 px-5 py-2.5 text-sm font-medium text-slate-300 hover:bg-white/10 hover:text-white transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:disabled="isCommentsLoadingMore">
|
||||
<Loader2 v-if="isCommentsLoadingMore" class="size-4 animate-spin" />
|
||||
<span>{{ isCommentsLoadingMore ? 'Đang tải...' : `Xem thêm bình luận (${comments.length}/${commentTotal})`
|
||||
}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
|
||||
<div v-if="!hasMoreComments() && comments.length && commentTotal > commentLimit" class="text-center mt-4">
|
||||
<span class="text-xs text-slate-600">Đã hiển thị tất cả {{ commentTotal }} bình luận</span>
|
||||
</div>
|
||||
<!-- Load More Button -->
|
||||
<div v-if="hasMoreComments()" class="flex justify-center mt-6">
|
||||
<button type="button" @click="loadMoreComments"
|
||||
class="flex items-center gap-2 rounded-lg bg-white/5 border border-white/10 px-5 py-2.5 text-sm font-medium text-slate-300 hover:bg-white/10 hover:text-white transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:disabled="isCommentsLoadingMore">
|
||||
<AppIcon name="loader" v-if="isCommentsLoadingMore" class="size-4 animate-spin" />
|
||||
<span>{{ isCommentsLoadingMore ? 'Đang tải...' : `Xem thêm bình luận (${comments.length}/${commentTotal})`
|
||||
}}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="!comments.length && !isCommentsLoading"
|
||||
class="flex flex-col items-center justify-center py-12 text-center">
|
||||
<MessageSquare class="size-10 text-white/10 mb-3" />
|
||||
<p class="text-slate-400 text-sm">Chưa có bình luận nào.</p>
|
||||
</div>
|
||||
<div v-if="!hasMoreComments() && comments.length && commentTotal > commentLimit" class="text-center mt-4">
|
||||
<span class="text-xs text-slate-600">Đã hiển thị tất cả {{ commentTotal }} bình luận</span>
|
||||
</div>
|
||||
|
||||
<div v-if="!comments.length && !isCommentsLoading"
|
||||
class="flex flex-col items-center justify-center py-12 text-center">
|
||||
<AppIcon name="message-square" class="size-10 text-white/10 mb-3" />
|
||||
<p class="text-slate-400 text-sm">Chưa có bình luận nào.</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import { ChevronDown, ChevronUp, Circle, Heart, Play } from 'lucide-vue-next'
|
||||
|
||||
const props = defineProps<{
|
||||
movie: {
|
||||
source: string
|
||||
@@ -139,14 +137,14 @@ onBeforeUnmount(() => {
|
||||
<div class="grid grid-cols-[1fr_6.25rem_6.25rem] gap-2">
|
||||
<span
|
||||
class="inline-flex h-10 items-center justify-center gap-2 rounded-md bg-yellow-400 px-4 text-sm font-black text-slate-950 transition hover:bg-white">
|
||||
<Play class="size-4 fill-current" />
|
||||
<AppIcon name="play" class="size-4 fill-current" />
|
||||
Xem ngay
|
||||
</span>
|
||||
<span class="grid h-10 place-items-center rounded-md border border-white/30 bg-white/8 text-white">
|
||||
<Heart class="size-5 fill-current" />
|
||||
<AppIcon name="heart" class="size-5 fill-current" />
|
||||
</span>
|
||||
<span class="grid h-10 place-items-center rounded-md border border-white/30 bg-white/8 text-white">
|
||||
<Circle class="size-5 fill-current" />
|
||||
<AppIcon name="circle" class="size-5 fill-current" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -171,10 +169,10 @@ onBeforeUnmount(() => {
|
||||
class="mt-1 inline-flex items-center gap-0.5 text-[10px] font-semibold text-yellow-300 transition hover:text-yellow-200"
|
||||
@click.stop="isDescriptionExpanded = !isDescriptionExpanded">
|
||||
<template v-if="isDescriptionExpanded">
|
||||
Thu gọn <ChevronUp class="size-3" />
|
||||
Thu gọn <AppIcon name="chevron-up" class="size-3" />
|
||||
</template>
|
||||
<template v-else>
|
||||
Xem thêm <ChevronDown class="size-3" />
|
||||
Xem thêm <AppIcon name="chevron-down" class="size-3" />
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowUp } from 'lucide-vue-next'
|
||||
|
||||
const isVisible = ref(false)
|
||||
|
||||
function updateVisibility() {
|
||||
@@ -37,7 +35,7 @@ onBeforeUnmount(() => {
|
||||
aria-label="Lên đầu trang"
|
||||
@click="scrollToTop"
|
||||
>
|
||||
<ArrowUp class="size-6" />
|
||||
<AppIcon name="arrow-up" class="size-6" />
|
||||
</button>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user