'use client'; import { useEffect, useState } from 'react'; import Link from 'next/link'; import { format } from 'date-fns'; import React from 'react'; interface Post { type: 'post'; slug: string; title: string; date: string; tags: string[]; summary: string; content: string; createdAt: string; pinned: boolean; } interface Folder { type: 'folder'; name: string; path: string; children: (Folder | Post)[]; emoji?: string; } type Node = Folder | Post; export default function Home() { const [tree, setTree] = useState([]); const [currentPath, setCurrentPath] = useState([]); const [search, setSearch] = useState(''); const [isLoading, setIsLoading] = useState(false); const [lastUpdate, setLastUpdate] = useState(null); const [error, setError] = useState(null); // Get blog owner from env const blogOwner = process.env.NEXT_PUBLIC_BLOG_OWNER || 'Anonymous'; useEffect(() => { loadTree(); // Set up Server-Sent Events for real-time updates (optional) let eventSource: EventSource | null = null; let fallbackInterval: NodeJS.Timeout | null = null; const setupSSE = () => { try { eventSource = new EventSource('/api/posts/stream'); eventSource.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.type === 'update') { loadTree(); } } catch (error) { console.error('Error parsing SSE data:', error); } }; eventSource.onerror = (error) => { console.error('SSE connection error:', error); if (eventSource) { eventSource.close(); eventSource = null; } // Fallback to minimal polling if SSE fails fallbackInterval = setInterval(loadTree, 30000); // 30 seconds }; eventSource.onopen = () => { console.log('SSE connection established'); // Clear any fallback interval if SSE is working if (fallbackInterval) { clearInterval(fallbackInterval); fallbackInterval = null; } }; } catch (error) { console.error('Failed to establish SSE connection:', error); // Fallback to minimal polling if SSE is not supported fallbackInterval = setInterval(loadTree, 30000); // 30 seconds } }; setupSSE(); return () => { if (eventSource) { eventSource.close(); } if (fallbackInterval) { clearInterval(fallbackInterval); } }; }, []); const loadTree = async () => { try { setIsLoading(true); setError(null); const response = await fetch('/api/posts'); if (!response.ok) { throw new Error(`API error: ${response.status}`); } const data = await response.json(); setTree(data); setLastUpdate(new Date()); } catch (error) { console.error('Fehler beim Laden der Beiträge:', error); setError(error instanceof Error ? error.message : String(error)); } finally { setIsLoading(false); } }; // Manual refresh function const handleRefresh = () => { loadTree(); }; // Traverse the tree to the current path const getCurrentNodes = (): Node[] => { let nodes: Node[] = tree; for (const segment of currentPath) { const folder = nodes.find( (n) => n.type === 'folder' && n.name === segment ) as Folder | undefined; if (folder) { nodes = folder.children; } else { break; } } return nodes; }; const nodes = getCurrentNodes(); // Breadcrumbs const breadcrumbs = [ { name: 'Startseite', path: [] }, ...currentPath.map((name, idx) => ({ name, path: currentPath.slice(0, idx + 1), })), ]; // Helper to strip YAML frontmatter function stripFrontmatter(md: string): string { if (!md) return ''; if (md.startsWith('---')) { const end = md.indexOf('---', 3); if (end !== -1) return md.slice(end + 3).replace(/^\s+/, ''); } return md; } // Helper to recursively collect all posts from the tree function collectPosts(nodes: Node[]): Post[] { let posts: Post[] = []; for (const node of nodes) { if (node.type === 'post' && node.slug !== 'about') { posts.push(node); } else if (node.type === 'folder') { posts = posts.concat(collectPosts(node.children)); } } return posts; } // Filter posts by search function filterPosts(posts: Post[]): Post[] { if (!search.trim()) return posts; const q = search.trim().toLowerCase(); return posts.filter(post => post.title.toLowerCase().includes(q) || post.summary.toLowerCase().includes(q) || post.tags.some(tag => tag.toLowerCase().includes(q)) ); } return (
{/* Error display */} {error && (
Fehler: {error}
)} {/* Mobile-first header section */}

{blogOwner}'s Blog

setSearch(e.target.value)} placeholder="Suche nach Titel, Tag oder Text..." className="flex-1 sm:w-80 border border-gray-300 rounded-lg px-4 py-3 text-base focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
{/* Last update indicator */} {lastUpdate && (
Aktualisiert: {lastUpdate.toLocaleTimeString()}
)} {/* Search Results Section */} {search.trim() && (

Suchergebnisse

{(() => { const posts = filterPosts(collectPosts(tree)); if (posts.length === 0) { return
Keine Beiträge gefunden.
; } return posts.map((post: any) => { // Determine folder path from slug let folderPath = ''; if (post.slug.includes('/')) { folderPath = post.slug.split('/').slice(0, -1).join('/'); } return (
{post.pinned && ( 📌 )}

{post.title}

{folderPath && (
in {folderPath}
)}
{post.date ? (
Veröffentlicht: {format(new Date(post.date), 'd. MMMM yyyy')}
) : (
⚙️ ⚙️
In Bearbeitung
)}
Erstellt: {format(new Date(post.createdAt), 'd. MMMM yyyy HH:mm')}

{stripFrontmatter(post.summary)}

{post.tags.map((tag: string) => { const q = search.trim().toLowerCase(); const isMatch = q && tag.toLowerCase().includes(q); return ( {tag} ); })}
); }); })()}
)} {/* Normal Content (folders and posts) only if not searching */} {!search.trim() && ( <> {/* Mobile-friendly breadcrumbs */}
{/* Folders */} {nodes.filter((n) => n.type === 'folder').map((folder: any) => (
setCurrentPath([...currentPath, folder.name])} > {folder.emoji || '📁'} {folder.name}
))} {/* Posts */} {(() => { const posts = nodes.filter((n) => n.type === 'post' && n.slug !== 'about'); const pinnedPosts = posts.filter((post: any) => post.pinned); const unpinnedPosts = posts.filter((post: any) => !post.pinned); return [...pinnedPosts, ...unpinnedPosts].map((post: any) => (
{post.pinned && ( 📌 )}

{post.title}

{post.date ? (
Veröffentlicht: {format(new Date(post.date), 'd. MMMM yyyy')}
) : (
⚙️ ⚙️
In Bearbeitung
)}
Erstellt: {format(new Date(post.createdAt), 'd. MMMM yyyy HH:mm')}

{stripFrontmatter(post.summary)}

{post.tags.map((tag: string) => ( {tag} ))}
)); })()}
)}
); }