This commit is contained in:
2026-03-04 00:18:56 +01:00
parent e01cbf85e5
commit 2fc23f1c93
4 changed files with 258 additions and 0 deletions
+2
View File
@@ -20,6 +20,7 @@ import '../styles/global.css';
<nav class="hidden md:flex space-x-4">
<a href="/" class="p-2 hover:text-purple-300 transition-colors duration-200">Home</a>
<a href="/catalog" class="p-2 hover:text-purple-300 transition-colors duration-200">Catalog</a>
<a href="/blog" class="p-2 hover:text-purple-300 transition-colors duration-200">Blog</a>
<a href="/howtos" class="p-2 hover:text-purple-300 transition-colors duration-200">How-tos</a>
<a href="/code" class="p-2 hover:text-purple-300 transition-colors duration-200">Code</a>
</nav>
@@ -35,6 +36,7 @@ import '../styles/global.css';
<nav id="mobile-menu" class="hidden md:hidden absolute top-full left-0 w-full bg-gray-800 flex-col items-center py-4 space-y-2 z-50">
<a href="/" class="block p-2 w-full text-center hover:bg-gray-700 transition-colors duration-200">Home</a>
<a href="/catalog" class="block p-2 w-full text-center hover:bg-gray-700 transition-colors duration-200">Catalog</a>
<a href="/blog" class="block p-2 w-full text-center hover:bg-gray-700 transition-colors duration-200">Blog</a>
<a href="/howtos" class="block p-2 w-full text-center hover:bg-gray-700 transition-colors duration-200">How-tos</a>
<a href="/code" class="block p-2 w-full text-center hover:bg-gray-700 transition-colors duration-200">Code</a>
</nav>
+254
View File
@@ -0,0 +1,254 @@
---
import Layout from '../layouts/Layout.astro';
const WIKIJS_BASE_URL = 'https://wiki.teletype.hu';
const WIKIJS_TOKEN = import.meta.env.WEBAPP_WIKIJS_TOKEN;
interface WikiPage {
id: number;
path: string;
title: string;
description: string;
render?: string;
updatedAt: string;
createdAt: string;
locale: string;
}
let blogPages: WikiPage[] = [];
let error: string | null = null;
let permissionWarning = false;
try {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'Accept': 'application/json',
};
if (WIKIJS_TOKEN) {
headers['Authorization'] = `Bearer ${WIKIJS_TOKEN}`;
}
// 1. Get the list of pages with the "blog" tag
const LIST_QUERY = `
{
pages {
list(orderBy: UPDATED, orderByDirection: DESC, tags: ["blog"]) {
id
path
title
description
updatedAt
createdAt
locale
}
}
}
`;
const listResponse = await fetch(`${WIKIJS_BASE_URL}/graphql`, {
method: 'POST',
headers,
body: JSON.stringify({ query: LIST_QUERY }),
});
const listJson = await listResponse.json();
if (listJson.errors) {
throw new Error(`GraphQL error: ${listJson.errors.map((e: any) => e.message).join(', ')}`);
}
const pages = listJson?.data?.pages?.list ?? [];
blogPages = pages.map((p: any) => ({
id: p.id,
path: p.path,
title: p.title || p.path,
description: p.description ?? '',
updatedAt: p.updatedAt,
createdAt: p.createdAt,
locale: p.locale,
}));
// 2. Try to fetch rendered HTML for each page
const idsToFetch = blogPages.slice(0, 10).map(p => p.id);
if (idsToFetch.length > 0) {
const SINGLE_PAGES_QUERY = `
{
${idsToFetch.map((id: number, index: number) => `
page_${index}: pages {
single(id: ${id}) {
id
render
}
}
`).join('\n')}
}
`;
const contentResponse = await fetch(`${WIKIJS_BASE_URL}/graphql`, {
method: 'POST',
headers,
body: JSON.stringify({ query: SINGLE_PAGES_QUERY }),
});
const contentJson = await contentResponse.json();
if (contentJson.errors) {
if (contentJson.errors.some((e: any) => e.message.includes('authorized') || e.message.includes('permission'))) {
console.warn('WikiJS: Permission denied for pages.single. Falling back to descriptions.');
permissionWarning = true;
} else {
console.error('WikiJS Content Error:', contentJson.errors);
}
}
// Merge rendered HTML if available
if (contentJson.data) {
Object.values(contentJson.data).forEach((item: any) => {
if (item?.single) {
const match = blogPages.find(p => p.id === item.single.id);
if (match) {
match.render = item.single.render;
}
}
});
}
}
} catch (e: any) {
console.error('Failed to fetch WikiJS data:', e);
error = `Failed to fetch wiki data: ${e.message}`;
}
function timeAgo(dateStr: string): string {
const now = new Date();
const date = new Date(dateStr);
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffMins < 1) return 'just now';
if (diffMins < 60) return `${diffMins}m ago`;
if (diffHours < 24) return `${diffHours}h ago`;
if (diffDays < 30) return `${diffDays}d ago`;
return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
}
---
<Layout>
<div class="bg-gray-50 min-h-screen pb-20">
<header class="relative overflow-hidden py-24 text-white text-center bg-indigo-900">
<div class="absolute inset-0 opacity-30">
<div class="hero-decor-blob -top-24 -left-24 h-96 w-96 bg-purple-600"></div>
<div class="hero-decor-blob -bottom-24 -right-24 h-96 w-96 bg-blue-600"></div>
</div>
<div class="hero-container">
<div class="hero-badge">
Community Stories
</div>
<h1 class="hero-title">
Developer <span class="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-400">Blog</span>
</h1>
<p class="hero-subtitle mb-10">
News, updates, and behind-the-scenes stories from the Teletype Games community.
</p>
</div>
</header>
<main class="w-full px-4 md:px-8 -mt-12 relative z-10">
{error && (
<div class="max-w-7xl mx-auto bg-red-50 border-l-4 border-red-500 p-6 rounded-r-xl shadow-lg mb-12 flex items-start gap-4" role="alert">
<span class="text-2xl">⚠️</span>
<div>
<h3 class="text-red-800 font-bold text-lg">Failed to connect to Wiki</h3>
<p class="text-red-700 mt-1">{error}</p>
</div>
</div>
)}
{permissionWarning && (
<div class="max-w-7xl mx-auto bg-amber-50 border-l-4 border-amber-500 p-6 rounded-r-xl shadow-lg mb-12 flex items-start gap-4" role="alert">
<span class="text-2xl">️</span>
<div>
<h3 class="text-amber-800 font-bold text-lg">Limited Access</h3>
<p class="text-amber-700 mt-1">Full content is hidden because the API token lacks <code>manage:pages</code> or <code>delete:pages</code> permissions in Wiki.js. Showing summaries instead.</p>
</div>
</div>
)}
{!error && blogPages.length === 0 && (
<div class="max-w-7xl mx-auto bg-white rounded-2xl shadow-xl p-12 text-center border border-gray-100">
<div class="text-6xl mb-4">📭</div>
<h2 class="text-2xl font-bold text-gray-900 mb-2">No blog posts found</h2>
<p class="text-gray-500 max-w-md mx-auto">We haven't published any blog posts yet. Check back soon!</p>
</div>
)}
{!error && blogPages.length > 0 && (
<div class="flex flex-col gap-12">
{blogPages.map((page) => {
const pageUrl = `${WIKIJS_BASE_URL}/${page.locale}/${page.path}`;
const isNew = (new Date().getTime() - new Date(page.createdAt).getTime()) < 7 * 24 * 60 * 60 * 1000;
return (
<article class="bg-white rounded-2xl shadow-xl border border-gray-100 overflow-hidden">
<div class="p-8 md:p-12">
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-4">
<div class="flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-gray-400">
{isNew && (
<span class="flex items-center gap-1 text-green-600 bg-green-50 px-2 py-0.5 rounded-full border border-green-100">
<span class="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse"></span>
New Post
</span>
)}
<span>{timeAgo(page.updatedAt)}</span>
</div>
</div>
<a href={pageUrl} target="_blank" class="text-indigo-600 hover:text-indigo-800 font-bold flex items-center gap-2 transition-colors">
View on Wiki <span>→</span>
</a>
</div>
<h2 class="text-3xl md:text-4xl font-black text-gray-900 mb-8 leading-tight">
{page.title}
</h2>
{page.render ? (
<div class="wiki-content prose prose-lg max-w-none text-gray-700 leading-relaxed" set:html={page.render} />
) : (
<div class="text-gray-600 text-lg leading-relaxed whitespace-pre-wrap">
{page.description || "No preview available. Click 'View on Wiki' to read the full post."}
</div>
)}
</div>
</article>
);
})}
</div>
)}
</main>
</div>
</Layout>
<style is:global>
.wiki-content h1 { @apply text-3xl font-bold mt-8 mb-4 text-gray-900; }
.wiki-content h2 { @apply text-2xl font-bold mt-6 mb-3 text-gray-800; }
.wiki-content h3 { @apply text-xl font-bold mt-4 mb-2 text-gray-800; }
.wiki-content p { @apply mb-4 text-gray-700; }
.wiki-content ul { @apply list-disc ml-6 mb-4 text-gray-700; }
.wiki-content ol { @apply list-decimal ml-6 mb-4 text-gray-700; }
.wiki-content li { @apply mb-1; }
.wiki-content a { @apply text-indigo-600 hover:underline; }
.wiki-content img { @apply max-w-full h-auto rounded-lg my-6 shadow-md; }
.wiki-content pre { @apply bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto my-6 font-mono text-sm; }
.wiki-content code { @apply bg-gray-100 text-pink-600 px-1 rounded font-mono text-sm; }
.wiki-content blockquote { @apply border-l-4 border-gray-300 pl-4 italic my-6 text-gray-600; }
.wiki-content table { @apply w-full border-collapse my-6; }
.wiki-content th, .wiki-content td { @apply border border-gray-200 p-3 text-left; }
.wiki-content th { @apply bg-gray-50 font-bold; }
</style>
+1
View File
@@ -119,6 +119,7 @@ services:
- NODE_ENV=development
- HOST=0.0.0.0
- WEBAPP_GITEA_TOKEN=${WEBAPP_GITEA_TOKEN}
- WEBAPP_WIKIJS_TOKEN=${WEBAPP_WIKIJS_TOKEN}
volumes:
- ./apps/frontend:/app
- /app/node_modules
+1
View File
@@ -24,3 +24,4 @@ MYSQL_ROOT_PASSWORD=
DB_PASSWORD=
UPDATE_SECRET=
DROP_PASSWORD=
WEBAPP_WIKIJS_TOKEN=