Initial commit
This commit is contained in:
commit
92e96b9107
85 changed files with 24969 additions and 0 deletions
163
app/pages/articles/[slug].vue
Normal file
163
app/pages/articles/[slug].vue
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
<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 -->
|
||||
<div class="prose prose-lg dark:prose-invert max-w-none">
|
||||
<ContentRenderer :value="article" />
|
||||
</div>
|
||||
|
||||
<!-- 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;
|
||||
|
||||
const getArticleSlug = (article) => {
|
||||
if (!article) return "";
|
||||
const candidate =
|
||||
article.slug ||
|
||||
article.stem ||
|
||||
article._path ||
|
||||
article._id ||
|
||||
article.id ||
|
||||
"";
|
||||
const segments = candidate.split("/").filter(Boolean);
|
||||
return segments[segments.length - 1] || "";
|
||||
};
|
||||
|
||||
// Fetch article from Nuxt Content
|
||||
const { data: article, pending } = await useAsyncData(
|
||||
`article-${slug}`,
|
||||
async () => {
|
||||
const articles = await queryCollection("articles").all();
|
||||
return articles.find((a) => getArticleSlug(a) === 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue