mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-17 21:48:07 +01:00
up
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user