Initial commit
This commit is contained in:
commit
92e96b9107
85 changed files with 24969 additions and 0 deletions
158
app/pages/articles/index.vue
Normal file
158
app/pages/articles/index.vue
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
<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">
|
||||
{{ 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 "";
|
||||
const candidate =
|
||||
article.slug ||
|
||||
article.stem ||
|
||||
article._path ||
|
||||
article._id ||
|
||||
article.id ||
|
||||
"";
|
||||
const segments = candidate.split("/").filter(Boolean);
|
||||
return segments[segments.length - 1] || "";
|
||||
};
|
||||
|
||||
const getArticlePath = (article) => {
|
||||
const slug = getArticleSlug(article);
|
||||
return slug ? `/articles/${slug}` : "/articles";
|
||||
};
|
||||
|
||||
// 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue