This commit is contained in:
HugoRCD
2025-06-23 16:46:51 +02:00
parent 7f20093993
commit 2835ea669b
2 changed files with 150 additions and 51 deletions

View File

@@ -11,6 +11,7 @@ const [{ data: page }, { data: surround }] = await Promise.all([
}).order('date', 'DESC')
})
])
if (!page.value) {
throw createError({ statusCode: 404, statusMessage: 'Article not found', fatal: true })
}
@@ -40,59 +41,143 @@ const formatDate = (dateString: string) => {
year: 'numeric',
month: 'short',
day: 'numeric'
})
}).toUpperCase()
}
const getCategoryVariant = (category: string) => {
switch (category?.toLowerCase()) {
case 'release': return 'solid'
case 'tutorial': return 'soft'
case 'improvement': return 'soft'
default: return 'soft'
}
}
const getCategoryIcon = (category: string) => {
switch (category?.toLowerCase()) {
case 'release': return 'i-lucide-rocket'
case 'tutorial': return 'i-lucide-book-open'
case 'improvement': return 'i-lucide-trending-up'
default: return 'i-lucide-file-text'
}
}
</script>
<template>
<UMain class="mt-20 px-2">
<UContainer class="relative min-h-screen">
<ULink
to="/blog"
class="text-sm flex items-center gap-1 w-fit"
>
<UIcon name="lucide:chevron-left" />
Blog
</ULink>
<UPage v-if="page">
<div class="flex flex-col gap-3 mt-8">
<div class="flex text-xs text-muted items-center justify-center gap-2">
<span v-if="page.date">
<div v-if="page" class="min-h-screen">
<div class="border-b border-default">
<UContainer class="py-4">
<ULink to="/blog" class="flex items-center gap-2 text-sm">
<UIcon name="i-lucide-chevron-left" class="size-4" />
Back to Blog
</ULink>
</UContainer>
</div>
<div class="py-16 sm:pt-20 pb-10">
<UContainer class="max-w-4xl">
<div class="text-center space-y-6">
<div class="flex items-center justify-center gap-4 text-sm">
<UBadge
v-if="page.category"
:variant="getCategoryVariant(page.category)"
size="sm"
class="font-mono text-xs gap-2"
>
<UIcon :name="getCategoryIcon(page.category)" class="size-3" />
{{ page.category?.toUpperCase() }}
</UBadge>
<span class="text-muted font-mono text-xs">
{{ formatDate(page.date) }}
</span>
<span v-if="page.date && page.minRead">
-
</span>
<span v-if="page.minRead">
{{ page.minRead }} min read
<span v-if="page.minRead" class="text-muted font-mono text-xs">
{{ page.minRead }} MIN READ
</span>
</div>
<Motion
:initial="{ opacity: 0, y: 20 }"
:animate="{ opacity: 1, y: 0 }"
:transition="{ duration: 0.6 }"
>
<h1 class="text-3xl sm:text-4xl lg:text-5xl font-bold text-highlighted leading-tight">
{{ page.title }}
</h1>
</Motion>
<Motion
:initial="{ opacity: 0, y: 20 }"
:animate="{ opacity: 1, y: 0 }"
:transition="{ delay: 0.1, duration: 0.6 }"
>
<p class="text-lg text-muted max-w-2xl mx-auto leading-relaxed">
{{ page.description }}
</p>
</Motion>
<Motion
v-if="page.authors?.length"
:initial="{ opacity: 0, y: 20 }"
:animate="{ opacity: 1, y: 0 }"
:transition="{ delay: 0.2, duration: 0.6 }"
class="flex justify-center"
>
<UAvatarGroup>
<ULink
v-for="(author, index) in page.authors"
:key="index"
:to="author.to"
raw
>
<UAvatar v-bind="author.avatar" />
</ULink>
</UAvatarGroup>
</Motion>
</div>
</UContainer>
</div>
<div v-if="page.image" class="py-4">
<UContainer class="max-w-6xl">
<Motion
:initial="{ opacity: 0, y: 30 }"
:animate="{ opacity: 1, y: 0 }"
:transition="{ delay: 0.3, duration: 0.8 }"
>
<NuxtImg
:src="page.image"
:alt="page.title"
class="rounded-lg w-full h-[400px] object-cover object-center max-w-5xl mx-auto"
class="w-full max-h-[400px] object-cover object-center max-w-5xl mx-auto"
/>
<h1 class="text-4xl text-center font-medium max-w-3xl mx-auto mt-4">
{{ page.title }}
</h1>
<p class="text-muted text-center max-w-2xl mx-auto">
{{ page.description }}
</p>
<div class="mt-4 flex justify-center flex-wrap items-center gap-6">
<UUser v-for="(author, index) in page.authors" :key="index" v-bind="author" :description="author.to ? `@${author.to.split('/').pop()}` : undefined" />
</div>
</div>
<UPageBody class="max-w-3xl mx-auto">
</Motion>
</UContainer>
</div>
<div class="py-12 sm:py-16">
<UContainer class="max-w-3xl">
<Motion
:initial="{ opacity: 0, y: 20 }"
:animate="{ opacity: 1, y: 0 }"
:transition="{ delay: 0.4, duration: 0.6 }"
>
<ContentRenderer
v-if="page.body"
:value="page"
/>
</Motion>
<USeparator v-if="surround?.length" />
<UContentSurround :surround="surround" />
</UPageBody>
</UPage>
</UContainer>
</UMain>
<div v-if="surround?.length" class="mt-16 pt-8 border-t border-default">
<Motion
:initial="{ opacity: 0, y: 20 }"
:animate="{ opacity: 1, y: 0 }"
:transition="{ delay: 0.6, duration: 0.6 }"
>
<UContentSurround :surround="surround" />
</Motion>
</div>
</UContainer>
</div>
</div>
</template>

View File

@@ -20,12 +20,31 @@ const selectedFilter = ref('all')
const searchQuery = ref('')
const availableFilters = computed(() => {
return [
{ key: 'all', label: 'ALL', count: posts.value?.length || 0 },
{ key: 'release', label: 'NEW RELEASES', count: posts.value?.filter(p => p.category?.toLowerCase() === 'release').length || 0 },
{ key: 'tutorial', label: 'TUTORIALS', count: posts.value?.filter(p => p.category?.toLowerCase() === 'tutorial').length || 0 },
{ key: 'improvement', label: 'IMPROVEMENTS', count: posts.value?.filter(p => p.category?.toLowerCase() === 'improvement').length || 0 }
if (!posts.value?.length) return [{ key: 'all', label: 'ALL', count: 0 }]
const postsData = posts.value
const categories = new Set(postsData.map(post => post.category?.toLowerCase()).filter(Boolean))
const filters = [
{ key: 'all', label: 'ALL', count: postsData.length }
]
categories.forEach((category) => {
const count = postsData.filter(p => p.category?.toLowerCase() === category).length
const label = category.replace(/\b\w/g, l => l.toUpperCase()).replace(/([a-z])([A-Z])/g, '$1 $2')
filters.push({
key: category,
label: label,
count
})
})
return filters.sort((a, b) => {
if (a.key === 'all') return -1
if (b.key === 'all') return 1
return b.count - a.count
})
})
const filteredPosts = computed(() => {
@@ -128,22 +147,17 @@ const getCategoryIcon = (category: string) => {
<UInput
v-model="searchQuery"
placeholder="Search posts..."
icon="i-lucide-search"
class="w-full sm:w-64"
size="sm"
:ui="{
base: 'rounded-none'
}"
>
<template #leading>
<UIcon name="i-lucide-search" class="w-4 h-4 text-muted" />
</template>
</UInput>
/>
</div>
<UButton
variant="ghost"
size="sm"
class="rounded-none text-xs sm:text-sm whitespace-nowrap"
class="rounded-none whitespace-nowrap"
icon="i-lucide-external-link"
label="Follow @nuxt_js on X"
to="https://x.com/nuxt_js"