152 lines
4.5 KiB
Vue
152 lines
4.5 KiB
Vue
<template>
|
|
<div>
|
|
<!-- Loading State -->
|
|
<div v-if="pending" class="text-center py-8">
|
|
<div class="text-gray-600 dark:text-gray-400">Loading article...</div>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div v-else-if="!article" class="text-center py-8">
|
|
<div class="text-red-600 dark:text-red-400">Article not found</div>
|
|
<NuxtLink
|
|
to="/articles"
|
|
class="text-blue-600 hover:text-blue-800 mt-4 inline-block"
|
|
>
|
|
← Back to Articles
|
|
</NuxtLink>
|
|
</div>
|
|
|
|
<!-- Article Content -->
|
|
<article v-else class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-8">
|
|
<!-- Header -->
|
|
<div class="border-b border-gray-200 dark:border-gray-700 pb-6 mb-6">
|
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-4">
|
|
{{ article.title }}
|
|
</h1>
|
|
|
|
<div
|
|
class="flex flex-wrap gap-4 text-sm text-gray-600 dark:text-gray-400"
|
|
>
|
|
<span>By {{ article.author || "Unknown" }}</span>
|
|
<span v-if="article.publishedAt">
|
|
{{ new Date(article.publishedAt).toLocaleDateString() }}
|
|
</span>
|
|
<span
|
|
v-if="article.category"
|
|
class="px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded"
|
|
>
|
|
{{ article.category }}
|
|
</span>
|
|
<span
|
|
v-if="article.accessLevel && article.accessLevel !== 'public'"
|
|
class="text-orange-600 dark:text-orange-400"
|
|
>
|
|
🔒 {{ article.accessLevel }}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Edit Button (for future use) -->
|
|
<!-- <div v-if="canEdit" class="mt-4">
|
|
<NuxtLink
|
|
:to="`${article._path}/edit`"
|
|
class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700"
|
|
>
|
|
Edit Article
|
|
</NuxtLink>
|
|
</div> -->
|
|
</div>
|
|
|
|
<!-- Access Control Warning -->
|
|
<div
|
|
v-if="article.accessLevel === 'member'"
|
|
class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4 mb-6"
|
|
>
|
|
<p class="text-blue-800 dark:text-blue-200">
|
|
👥 This content is for Baby Ghosts members only
|
|
</p>
|
|
</div>
|
|
<div
|
|
v-else-if="article.accessLevel === 'cohort'"
|
|
class="bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-lg p-4 mb-6"
|
|
>
|
|
<p class="text-purple-800 dark:text-purple-200">
|
|
🔒 This content is for a specific cohort
|
|
</p>
|
|
</div>
|
|
<div
|
|
v-else-if="article.accessLevel === 'admin'"
|
|
class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-6"
|
|
>
|
|
<p class="text-red-800 dark:text-red-200">
|
|
🔐 This is internal/admin content
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Article Body -->
|
|
<ContentRenderer
|
|
:value="article"
|
|
class="prose prose-lg prose-gray dark:prose-invert max-w-none"
|
|
/>
|
|
|
|
<!-- Tags -->
|
|
<div
|
|
v-if="article.tags?.length"
|
|
class="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700"
|
|
>
|
|
<div class="flex flex-wrap gap-2">
|
|
<span
|
|
v-for="tag in article.tags"
|
|
:key="tag"
|
|
class="px-3 py-1 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-full text-sm"
|
|
>
|
|
#{{ tag }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Navigation -->
|
|
<div
|
|
class="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700 flex gap-4"
|
|
>
|
|
<NuxtLink
|
|
to="/articles"
|
|
class="text-blue-600 hover:text-blue-800 dark:text-blue-400"
|
|
>
|
|
← Back to Articles
|
|
</NuxtLink>
|
|
</div>
|
|
</article>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
const route = useRoute();
|
|
const slug = route.params.slug;
|
|
|
|
// Fetch article from Nuxt Content
|
|
const { data: article, pending } = await useAsyncData(
|
|
`article-${slug}`,
|
|
async () => {
|
|
// Query for the specific article by stem (filename without extension)
|
|
const articles = await queryCollection("articles").all();
|
|
return articles.find((a) => a.stem === slug);
|
|
},
|
|
);
|
|
|
|
// SEO
|
|
useHead({
|
|
title: () => article.value?.title || "Article",
|
|
meta: [
|
|
{ name: "description", content: () => article.value?.description || "" },
|
|
],
|
|
});
|
|
|
|
// Watch for route changes
|
|
watch(
|
|
() => route.params.slug,
|
|
() => {
|
|
// Route change will trigger new useAsyncData
|
|
},
|
|
{ immediate: true },
|
|
);
|
|
</script>
|