lastwebnovel-app/app/pages/titles/recently-added.vue
2026-04-11 22:55:16 +02:00

237 lines
8.1 KiB
Vue

<script setup lang="ts">
import type { WebNovel } from '~/types'
useSeoMeta({
title: 'Recently Added - LastWebNovel',
description: 'Latest novels added to our collection'
})
const router = useRouter()
const isLoading = ref(true)
const recentNovels = ref<WebNovel[]>([])
const displayLimit = ref(20)
const formatNumber = (num: number): string => {
if (num >= 1000000) {
return `${(num / 1000000).toFixed(1)}M`
} else if (num >= 1000) {
return `${(num / 1000).toFixed(1)}k`
}
return num.toString()
}
const formatDate = (dateString: string): string => {
const date = new Date(dateString)
const now = new Date()
const diffTime = Math.abs(now.getTime() - date.getTime())
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
if (diffDays === 0) {
return 'Today'
} else if (diffDays === 1) {
return 'Yesterday'
} else if (diffDays < 7) {
return `${diffDays} days ago`
} else if (diffDays < 30) {
return `${Math.floor(diffDays / 7)} weeks ago`
} else if (diffDays < 365) {
return `${Math.floor(diffDays / 30)} months ago`
} else {
return `${Math.floor(diffDays / 365)} years ago`
}
}
const goToNovel = (novelId: string) => {
router.push(`/novels/${novelId}`)
}
const loadMoreNovels = () => {
displayLimit.value += 20
}
const displayedNovels = computed(() => {
return recentNovels.value.slice(0, displayLimit.value)
})
const hasMoreNovels = computed(() => {
return displayLimit.value < recentNovels.value.length
})
// Load novels data
const loadNovels = async () => {
try {
isLoading.value = true
const allItems = await queryCollection('content').all() as any[]
const novels: WebNovel[] = allItems
.filter((item) => {
const path = item._path || item.id || ''
return path.includes('/novels/') && path.endsWith('/index.md')
})
.map((item) => {
const pathParts = (item._path || item.id || '').split('/')
const novelId = pathParts[pathParts.length - 2] || pathParts[pathParts.length - 1]
return {
id: novelId,
title: item.title || 'Unknown',
slug: item.meta?.slug || 'no-slug',
author: item.meta?.author || item.author || 'Unknown',
description: item.description || item.body?.text || '',
cover: `/images/${novelId}/cover.png`,
status: item.meta?.status || item.status || 'ongoing',
genres: item.meta?.genres || item.genres || [],
rating: item.meta?.rating || item.rating || 0,
views: item.meta?.views || item.views || 0,
followers: item.meta?.followers || item.followers || 0,
chapters: item.meta?.chapters || item.chapters || 0,
language: item.meta?.language || item.language || 'English',
tags: item.meta?.tags || item.tags || [],
createdAt: item.meta?.createdAt || item.createdAt || new Date().toISOString(),
updatedAt: item.meta?.updatedAt || item.updatedAt || new Date().toISOString()
} as WebNovel
})
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
recentNovels.value = novels
console.log(`Loaded ${novels.length} recently added novels`)
} catch (err) {
console.error('Error loading novels:', err)
} finally {
isLoading.value = false
}
}
onMounted(() => {
loadNovels()
})
</script>
<template>
<UDashboardPanel id="recently-added">
<template #header>
<UDashboardNavbar title="Recently Added" :ui="{ right: 'gap-3' }">
<template #leading>
<UDashboardSidebarCollapse />
</template>
</UDashboardNavbar>
</template>
<template #body>
<UContainer class="py-6">
<!-- Header Info -->
<div class="flex flex-row items-center justify-between gap-4 mb-6">
<div>
<h2 class="text-xl font-bold mb-1">
Latest Additions
</h2>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ recentNovels.length }} novel{{ recentNovels.length !== 1 ? 's' : '' }} added
</p>
</div>
</div>
<!-- Loading State -->
<div v-if="isLoading" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
<USkeleton v-for="i in 10" :key="i" class="h-80" />
</div>
<!-- Novels Grid -->
<div
v-else-if="displayedNovels.length > 0"
class="space-y-6"
>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
<div
v-for="novel in displayedNovels"
:key="novel.id"
class="group cursor-pointer"
@click="goToNovel(novel.id)"
>
<div class="relative overflow-hidden rounded-lg mb-2 aspect-2/3">
<img
:src="`/images/${novel.slug || novel.id}/cover.png`"
:alt="novel.title"
class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110"
>
<div class="absolute inset-0 bg-linear-to-t from-black/90 via-black/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<div class="absolute bottom-0 left-0 right-0 p-3 space-y-1">
<div class="flex items-center gap-1 text-xs text-white/90">
<UIcon name="i-lucide-star" class="size-3 text-yellow-400" />
<span>{{ (novel.rating || 0).toFixed(1) }}</span>
<span class="mx-1">•</span>
<span>{{ novel.chapters || 0 }} ch</span>
</div>
<div class="flex items-center gap-1 text-xs text-white/80">
<UIcon name="i-lucide-eye" class="size-3" />
<span>{{ formatNumber(novel.views || 0) }}</span>
</div>
<div class="flex items-center gap-1 text-xs text-white/80">
<UIcon name="i-lucide-calendar" class="size-3" />
<span>{{ formatDate(novel.createdAt) }}</span>
</div>
</div>
</div>
<UBadge
:color="novel.status === 'completed' ? 'success' : novel.status === 'hiatus' ? 'warning' : 'info'"
size="xs"
class="absolute top-2 right-2"
>
{{ novel.status }}
</UBadge>
<UBadge
color="primary"
size="xs"
class="absolute top-2 left-2"
>
New
</UBadge>
</div>
<h3 class="font-semibold text-sm line-clamp-2 mb-1">
{{ novel.title }}
</h3>
<p class="text-xs text-gray-600 dark:text-gray-400 line-clamp-1 mb-1">
{{ typeof novel.author === 'string' ? novel.author : novel.author?.name }}
</p>
<div class="flex flex-wrap gap-1">
<UBadge
v-for="genre in novel.genres?.slice(0, 2)"
:key="genre"
size="xs"
variant="soft"
class="capitalize"
>
{{ genre }}
</UBadge>
</div>
</div>
</div>
<!-- Load More Button -->
<div v-if="hasMoreNovels" class="flex justify-center pt-4">
<UButton
size="lg"
variant="soft"
@click="loadMoreNovels"
>
Load More Novels
</UButton>
</div>
</div>
<!-- Empty State -->
<div v-else class="text-center py-12">
<UIcon name="i-lucide-sparkles" class="size-12 mx-auto mb-4 text-gray-400" />
<h3 class="text-lg font-semibold mb-2">
No novels found
</h3>
<p class="text-gray-600 dark:text-gray-400">
Check back later for newly added novels
</p>
</div>
</UContainer>
</template>
</UDashboardPanel>
</template>