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

347 lines
12 KiB
Vue

<script setup lang="ts">
import type { WebNovel } from '~/types'
const router = useRouter()
const featured = ref<WebNovel[]>([])
const trending = ref<WebNovel[]>([])
const recent = ref<WebNovel[]>([])
const isLoading = ref(true)
const currentFeaturedIndex = ref(0)
const nextFeatured = () => {
if (featured.value.length > 0) {
currentFeaturedIndex.value = (currentFeaturedIndex.value + 1) % featured.value.length
}
}
const prevFeatured = () => {
if (featured.value.length > 0) {
currentFeaturedIndex.value = (currentFeaturedIndex.value - 1 + featured.value.length) % featured.value.length
}
}
const currentFeatured = computed(() => featured.value[currentFeaturedIndex.value])
const loadNovels = async () => {
try {
// Load homepage configuration to get featured/trending/recent novel slugs
const homepageConfig = await queryCollection('content').path('/novels/homepage').first()
if (!homepageConfig) {
console.error('Homepage configuration not found')
return
}
console.log('Homepage config:', homepageConfig)
// Collect all unique slugs from featured, trending, and recent
const allSlugs = new Set<string>([
...(homepageConfig.meta.featured || []),
...(homepageConfig.meta.trending || []),
...(homepageConfig.meta.recent || [])
])
console.log(`Querying ${allSlugs.size} unique novels`)
// Create a map of slug -> novel for quick lookup
const novelMap = new Map<string, WebNovel>()
// Query only the novels we need
for (const slug of allSlugs) {
const item = await queryCollection('content').path(`/novels/${slug}`).first() as WebNovel | null
if (item) {
const novel: WebNovel = {
id: slug,
title: item.title || 'Unknown',
author: item.meta.author || 'Unknown',
description: item.description || item.meta.body?.text || '',
cover: `/images/${slug}/cover.png`,
status: item.meta.status || 'ongoing',
genres: item.meta.genres || [],
rating: item.meta.rating || 0,
views: item.meta.views || 0,
followers: item.meta.followers || 0,
chapters: item.meta.chapters || 0,
language: item.meta.language || 'English',
tags: item.meta.tags || [],
createdAt: item.createdAt || new Date().toISOString(),
updatedAt: item.updatedAt || new Date().toISOString()
}
novelMap.set(slug, novel)
}
}
console.log(`Loaded ${novelMap.size} novels from cache`)
// Get featured novels using configured slugs
featured.value = (homepageConfig.meta.featured || [])
.map((slug: string) => novelMap.get(slug))
.filter(Boolean)
// Get trending novels using configured slugs
trending.value = (homepageConfig.meta.trending || [])
.map((slug: string) => novelMap.get(slug))
.filter(Boolean)
// Get recent novels using configured slugs
recent.value = (homepageConfig.meta.recent || [])
.map((slug: string) => novelMap.get(slug))
.filter(Boolean)
console.log('Featured:', featured.value.length, 'Trending:', trending.value.length, 'Recent:', recent.value.length)
} catch (error) {
console.error('Error loading novels:', error)
} finally {
isLoading.value = false
}
}
onMounted(() => {
loadNovels()
})
</script>
<template>
<UDashboardPanel id="home">
<template #header>
<UDashboardNavbar title="WebNovel Platform" :ui="{ right: 'gap-3' }">
<template #leading>
<UDashboardSidebarCollapse />
</template>
<template #right>
<!-- <UTooltip text="Search (Ctrl+K)" :shortcuts="['Ctrl', 'K']">
<UButton
icon="i-lucide-search"
color="neutral"
variant="ghost"
square
@click="$refs.search?.open()"
/>
</UTooltip>
<UButton
icon="i-lucide-flame"
color="primary"
size="md"
@click="router.push('/novels')"
>
Explore
</UButton> -->
</template>
</UDashboardNavbar>
</template>
<template #body>
<!-- Featured Hero Slider - Full Width -->
<section v-if="!isLoading && currentFeatured" class="-m-4 md:-m-6 mb-0">
<div class="relative h-[400px] md:h-[500px]">
<!-- Background Image with Gradient Overlay -->
<img
:src="currentFeatured.cover"
:alt="currentFeatured.title"
class="absolute inset-0 w-full h-full object-cover"
>
<div class="absolute inset-0 bg-gradient-to-r from-black/90 via-black/70 to-transparent" />
<!-- Content -->
<div class="relative h-full flex items-center">
<div class="container mx-auto px-4 md:px-8 max-w-2xl">
<div class="space-y-4">
<!-- Title -->
<h1 class="text-3xl md:text-5xl font-bold text-white line-clamp-2">
{{ currentFeatured.title }}
</h1>
<!-- Genres -->
<div class="flex flex-wrap gap-2">
<UBadge
v-for="genre in currentFeatured.genres?.slice(0, 3)"
:key="genre"
color="primary"
size="md"
>
{{ genre }}
</UBadge>
</div>
<!-- Description -->
<p class="text-gray-200 text-sm md:text-base line-clamp-3 md:line-clamp-4">
{{ currentFeatured.description }}
</p>
<!-- Author and Stats -->
<div class="flex flex-wrap items-center gap-4 text-sm text-gray-300">
<span class="font-medium">{{ typeof currentFeatured.author === 'string' ? currentFeatured.author : currentFeatured.author?.name }}</span>
<div class="flex items-center gap-1">
<UIcon name="i-lucide-star" class="size-4 text-yellow-400" />
<span>{{ (currentFeatured.rating || 0).toFixed(1) }}</span>
</div>
<div class="flex items-center gap-1">
<UIcon name="i-lucide-book" class="size-4" />
<span>{{ currentFeatured.chapters || 0 }} chapters</span>
</div>
</div>
<!-- Read Button -->
<UButton
size="lg"
color="primary"
icon="i-lucide-book-open"
@click="router.push(`/novels/${currentFeatured.id}`)"
>
Start Reading
</UButton>
</div>
</div>
</div>
<!-- Navigation Arrows -->
<button
v-if="featured.length > 1"
class="absolute left-4 top-1/2 -translate-y-1/2 bg-black/50 hover:bg-black/70 text-white p-2 rounded-full transition-colors"
@click="prevFeatured"
>
<UIcon name="i-lucide-chevron-left" class="size-6" />
</button>
<button
v-if="featured.length > 1"
class="absolute right-4 top-1/2 -translate-y-1/2 bg-black/50 hover:bg-black/70 text-white p-2 rounded-full transition-colors"
@click="nextFeatured"
>
<UIcon name="i-lucide-chevron-right" class="size-6" />
</button>
<!-- Page Indicator -->
<div
v-if="featured.length > 1"
class="absolute bottom-4 right-4 bg-black/50 text-white px-3 py-1 rounded text-sm font-medium"
>
NO. {{ currentFeaturedIndex + 1 }} / {{ featured.length }}
</div>
</div>
</section>
<!-- Featured Skeleton -->
<section v-if="isLoading" class="-m-4 md:-m-6 mb-0">
<USkeleton class="h-[400px] md:h-[500px]" />
</section>
<UContainer class="space-y-8 py-8">
<!-- Trending Section -->
<section class="space-y-3">
<div class="flex items-center justify-between">
<h2 class="text-xl md:text-2xl font-bold">
Trending Now
</h2>
<UButton
variant="ghost"
trailing-icon="i-lucide-arrow-right"
size="sm"
>
More
</UButton>
</div>
<div v-if="isLoading" class="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3">
<USkeleton v-for="i in 6" :key="i" class="h-48 rounded-lg" />
</div>
<div v-else class="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3">
<div
v-for="novel in trending.slice(0, 6)"
:key="novel.id"
class="cursor-pointer group"
@click="router.push(`/novels/${novel.id}`)"
>
<div class="relative overflow-hidden rounded-lg">
<img
:src="novel.cover"
:alt="novel.title"
class="w-full aspect-[2/3] object-cover group-hover:scale-105 transition-transform duration-300"
>
<div class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent" />
<div class="absolute bottom-0 left-0 right-0 p-2">
<h3 class="font-semibold text-white text-xs md:text-sm line-clamp-2">
{{ novel.title }}
</h3>
<div class="flex items-center gap-1 mt-1">
<UIcon name="i-lucide-star" class="size-3 text-yellow-400" />
<span class="text-xs text-white">{{ (novel.rating || 0).toFixed(1) }}</span>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Recently Updated Section -->
<section class="space-y-3">
<div class="flex items-center justify-between">
<h2 class="text-xl md:text-2xl font-bold">
Recently Updated
</h2>
<UButton
variant="ghost"
trailing-icon="i-lucide-arrow-right"
size="sm"
>
More
</UButton>
</div>
<div v-if="isLoading" class="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3">
<USkeleton v-for="i in 6" :key="i" class="h-48 rounded-lg" />
</div>
<div v-else class="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3">
<div
v-for="novel in recent.slice(0, 6)"
:key="novel.id"
class="cursor-pointer group"
@click="router.push(`/novels/${novel.id}`)"
>
<div class="relative overflow-hidden rounded-lg">
<img
:src="novel.cover"
:alt="novel.title"
class="w-full aspect-[2/3] object-cover group-hover:scale-105 transition-transform duration-300"
>
<div class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent" />
<div class="absolute bottom-0 left-0 right-0 p-2">
<h3 class="font-semibold text-white text-xs md:text-sm line-clamp-2">
{{ novel.title }}
</h3>
<p class="text-xs text-gray-300 mt-1">
{{ novel.chapters || 0 }} ch
</p>
</div>
</div>
</div>
</div>
</section>
<!-- CTA Section -->
<section class="bg-gradient-to-r from-primary-500 to-primary-600 rounded-lg p-8 text-center text-white">
<h3 class="text-2xl font-bold mb-2">
Discover Your Next Favorite Novel
</h3>
<p class="mb-4 opacity-90">
Explore thousands of web novels from talented authors
</p>
<UButton
color="neutral"
size="lg"
icon="i-lucide-arrow-right"
@click="router.push('/titles/search')"
>
Browse Now
</UButton>
</section>
</UContainer>
</template>
</UDashboardPanel>
</template>