diff --git a/app/components/CommentSection.vue b/app/components/CommentSection.vue index a48790e..1b44566 100644 --- a/app/components/CommentSection.vue +++ b/app/components/CommentSection.vue @@ -23,6 +23,10 @@ const replyContent = ref>({}) const isReplyingTo = ref(null) const isReplyAnonymous = ref>({}) const expandedComments = ref>(new Set()) +const commentOffset = ref(0) +const commentTotal = ref(0) +const commentLimit = 5 +const isCommentsLoadingMore = ref(false) function addTimeAgoToReplies(replies: any[]): any[] { return replies?.map(r => ({ @@ -32,21 +36,42 @@ function addTimeAgoToReplies(replies: any[]): any[] { })) || [] } -async function loadComments() { +async function loadComments(reset = true) { if (!props.slug) return - isCommentsLoading.value = true + if (reset) { + isCommentsLoading.value = true + commentOffset.value = 0 + } else { + isCommentsLoadingMore.value = true + } try { - const data = await fetchComments(props.source, props.slug, user.value?.id) - comments.value = data.map(c => ({ + const data = await fetchComments(props.source, props.slug, user.value?.id, commentLimit, commentOffset.value) + const newComments = data.items.map(c => ({ ...c, timeAgo: timeAgo(c.createdAt), replies: addTimeAgoToReplies(c.replies || []) })) + if (reset) { + comments.value = newComments + } else { + comments.value = [...comments.value, ...newComments] + } + commentTotal.value = data.total + commentOffset.value += data.items.length } finally { isCommentsLoading.value = false + isCommentsLoadingMore.value = false } } +async function loadMoreComments() { + await loadComments(false) +} + +function hasMoreComments() { + return comments.value.length < commentTotal.value +} + function loadCommentDraft() { const draft = getDraft(props.slug, props.source) if (draft) { @@ -418,8 +443,7 @@ watch(() => props.slug, () => { class="flex items-center gap-1.5 text-xs text-slate-500 hover:text-slate-300"> Trả lời -
- +
@@ -483,7 +508,23 @@ watch(() => props.slug, () => { -
+ +
+ +
+ +
+ Đã hiển thị tất cả {{ commentTotal }} bình luận +
+ +

Chưa có bình luận nào.

@@ -495,10 +536,12 @@ watch(() => props.slug, () => { overflow: hidden; transition: max-height 0.35s ease, opacity 0.3s ease; } + .replies-container.collapsed { max-height: 0; opacity: 0; } + .replies-container.expanded { max-height: 5000px; opacity: 1; @@ -509,10 +552,12 @@ watch(() => props.slug, () => { .comment-list-leave-active { transition: all 0.4s ease; } + .comment-list-enter-from { opacity: 0; transform: translateY(-20px); } + .comment-list-leave-to { opacity: 0; transform: translateX(-50px); @@ -520,6 +565,7 @@ watch(() => props.slug, () => { margin-bottom: 0; overflow: hidden; } + .comment-list-leave-active { position: absolute; width: 100%; diff --git a/app/composables/useComments.ts b/app/composables/useComments.ts index 7c4d2ac..c3a5af4 100644 --- a/app/composables/useComments.ts +++ b/app/composables/useComments.ts @@ -34,14 +34,14 @@ export type Comment = { } export function useComments() { - async function fetchComments(source: string, slug: string, userId?: number) { + async function fetchComments(source: string, slug: string, userId?: number, limit?: number, offset?: number) { try { const data = await $fetch('/api/comments', { - query: { source, slug, userId }, + query: { source, slug, userId, limit, offset }, }) - return data.items as Comment[] + return data as { items: Comment[], total: number } } catch { - return [] as Comment[] + return { items: [] as Comment[], total: 0 } } } diff --git a/app/pages/phim/[slug].vue b/app/pages/phim/[slug].vue index 7bb2b09..ab4e56f 100644 --- a/app/pages/phim/[slug].vue +++ b/app/pages/phim/[slug].vue @@ -212,7 +212,7 @@ useHead(() => ({
-
+
({

+ + +
+ + + +
- - -
diff --git a/app/pages/xem/[slug].vue b/app/pages/xem/[slug].vue index a0242e4..0a61217 100644 --- a/app/pages/xem/[slug].vue +++ b/app/pages/xem/[slug].vue @@ -590,7 +590,7 @@ useHead(() => ({
{{ formatEpisodeName(activeEpisode?.name, selectedEpisode) - }} + }} {{ serverLabel(activeServer, selectedServer) }}
@@ -658,7 +658,7 @@ useHead(() => ({ class="inline-flex self-center sm:self-start items-center gap-1.5 px-3 py-1.5 rounded-full mt-2 border bg-cinek-500/10 border-cinek-500/20 text-cinek-500"> Đã chiếu: {{ episodeProgress.available }} / {{ episodeProgress.total - }} tập + }} tập @@ -758,9 +758,11 @@ useHead(() => ({ - - - +
+ + + +
@@ -894,5 +896,4 @@ useHead(() => ({ inset: 0; background: linear-gradient(90deg, rgb(250 204 21) v-bind('`${progressPercent}%`'), transparent 0); } - diff --git a/server/api/comments/index.get.ts b/server/api/comments/index.get.ts index d18e2bf..2d66d72 100644 --- a/server/api/comments/index.get.ts +++ b/server/api/comments/index.get.ts @@ -3,7 +3,9 @@ import { comments, users, commentVotes } from '../../database/schema' export default defineEventHandler(async (event) => { const query = getQuery(event) - const { source = '', slug, userId } = query + const { source = '', slug, userId, limit = '20', offset = '0' } = query + const pageLimit = Math.min(Math.max(Number(limit), 1), 100) + const pageOffset = Math.max(Number(offset), 0) if (!slug) { throw createError({ statusCode: 400, message: 'Thiếu slug phim' }) @@ -20,6 +22,12 @@ export default defineEventHandler(async (event) => { whereConditions.push(eq(comments.source, String(source))) } + // Count total comments + const [{ count }] = await db + .select({ count: sql`count(*)` }) + .from(comments) + .where(and(...whereConditions)) + // Fetch pinned comments first, then regular ones const results = await db .select({ @@ -43,7 +51,8 @@ export default defineEventHandler(async (event) => { .leftJoin(users, eq(comments.userId, users.id)) .where(and(...whereConditions)) .orderBy(sql`${comments.pinned} DESC, ${comments.createdAt} DESC`) - .limit(100) + .limit(pageLimit) + .offset(pageOffset) const commentIds = results.map(r => r.id) @@ -150,5 +159,6 @@ export default defineEventHandler(async (event) => { userVote: userVotes[r.id] || 0, replies: (repliesByParent[r.id] || []).map(attachChildren), })), + total: Number(count), } })