349 lines
12 KiB
Vue
349 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"
|
|
@click="router.push('/titles/search')"
|
|
>
|
|
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"
|
|
@click="router.push('/titles/search')"
|
|
>
|
|
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>
|