wiki_ghostguild/app/pages/articles/index.vue

154 lines
4.7 KiB
Vue

<template>
<div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-6">
Articles
</h1>
<!-- Search and Filters -->
<div class="bg-white dark:bg-gray-800 p-4 rounded-lg shadow mb-6">
<div class="flex gap-4">
<input
v-model="search"
type="text"
placeholder="Search articles..."
class="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
/>
<select
v-model="selectedCategory"
class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
>
<option value="">All Categories</option>
<option value="operations">Operations</option>
<option value="funding">Funding</option>
<option value="strategy">Strategy</option>
<option value="templates">Templates</option>
<option value="programs">Programs</option>
</select>
</div>
</div>
<!-- Articles List -->
<div v-if="filteredArticles.length" class="space-y-4">
<article
v-for="article in filteredArticles"
:key="article._path || article.stem || getArticleSlug(article)"
class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow hover:shadow-lg transition-shadow"
>
<div class="flex justify-between items-start">
<div class="flex-1">
<NuxtLink
:to="getArticlePath(article)"
class="text-xl font-semibold text-blue-600 hover:text-blue-800 dark:text-blue-400"
>
{{ article.title }}
</NuxtLink>
<p
class="text-gray-600 dark:text-gray-400 mt-2 font-serif text-lg leading-relaxed"
>
{{ article.description }}
</p>
<div
class="mt-3 flex items-center gap-4 text-sm text-gray-500 dark:text-gray-400"
>
<span class="px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded">
{{ article.category || "uncategorized" }}
</span>
<span>By {{ article.author || "Unknown" }}</span>
<span v-if="article.publishedAt">
{{ new Date(article.publishedAt).toLocaleDateString() }}
</span>
<span
v-if="article.accessLevel && article.accessLevel !== 'public'"
class="text-orange-600 dark:text-orange-400"
>
🔒 {{ article.accessLevel }}
</span>
</div>
</div>
</div>
</article>
</div>
<div v-else class="text-center py-8">
<p class="text-gray-600 dark:text-gray-400">
{{
search || selectedCategory
? "No articles found matching your filters"
: "No articles available"
}}
</p>
</div>
</div>
</template>
<script setup>
const search = ref("");
const selectedCategory = ref("");
// Fetch all articles from Nuxt Content
const { data: articles } = await useAsyncData("articles", () =>
queryCollection("articles").all(),
);
// Helper function to extract title from body
const getArticleTitle = (article) => {
// Try to get title from frontmatter first
if (
article.title &&
typeof article.title === "string" &&
article.title !== "[object Object]"
) {
return article.title;
}
// Fallback: extract from first H1 in body
if (article.meta?.body?.value && Array.isArray(article.meta.body.value)) {
const h1 = article.meta.body.value.find(
(node) => Array.isArray(node) && node[0] === "h1",
);
if (h1 && h1[2]) {
return h1[2];
}
}
return "Untitled";
};
// Resolve the correct Nuxt route for an article entry
const getArticleSlug = (article) => {
if (!article) return "";
// stem is the filename without extension
return article.stem || "";
};
const getArticlePath = (article) => {
if (!article) return "/articles";
const slug = getArticleSlug(article);
return `/articles/${slug}`;
};
// Filter and search articles
const filteredArticles = computed(() => {
if (!articles.value) return [];
return articles.value.filter((article) => {
const title = getArticleTitle(article);
const matchesSearch =
!search.value ||
title?.toLowerCase().includes(search.value.toLowerCase()) ||
article.description?.toLowerCase().includes(search.value.toLowerCase());
const matchesCategory =
!selectedCategory.value || article.category === selectedCategory.value;
return matchesSearch && matchesCategory;
});
});
// Watch for search changes
watch(search, () => {
// Trigger reactivity
});
</script>