Implement Server-Sent Events for real-time updates and enhance API route handling. Added loading state and last update indicator in the home page. Improved folder and post detail fetching logic in the admin page. Added webhook notification on file changes.
Some checks failed
Deploy / build-and-deploy (push) Failing after 2s

This commit is contained in:
ZockerKatze
2025-06-24 07:23:34 +02:00
parent da5fbfa687
commit 7e2ada529d
7 changed files with 339 additions and 26 deletions

View File

@@ -31,26 +31,90 @@ export default function Home() {
const [tree, setTree] = useState<Node[]>([]);
const [currentPath, setCurrentPath] = useState<string[]>([]);
const [search, setSearch] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [lastUpdate, setLastUpdate] = useState<Date | null>(null);
// Get blog owner from env
const blogOwner = process.env.NEXT_PUBLIC_BLOG_OWNER || 'Anonymous';
useEffect(() => {
loadTree();
const interval = setInterval(loadTree, 500);
return () => clearInterval(interval);
// 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);
const response = await fetch('/api/posts');
const data = await response.json();
setTree(data);
setLastUpdate(new Date());
} catch (error) {
console.error('Fehler beim Laden der Beiträge:', error);
} finally {
setIsLoading(false);
}
};
// Manual refresh function
const handleRefresh = () => {
loadTree();
};
// Traverse the tree to the current path
const getCurrentNodes = (): Node[] => {
let nodes: Node[] = tree;
@@ -107,17 +171,41 @@ export default function Home() {
{/* Mobile-first header section */}
<div className="mb-6 sm:mb-8 space-y-4 sm:space-y-0 sm:flex sm:flex-row sm:gap-4 sm:items-center sm:justify-between">
<h1 className="text-2xl sm:text-3xl md:text-4xl font-bold text-center sm:text-left">{blogOwner}&apos;s Blog</h1>
<div className="w-full sm:w-auto">
<div className="w-full sm:w-auto flex gap-2">
<input
type="text"
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Suche nach Titel, Tag oder Text..."
className="w-full 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"
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"
/>
<button
onClick={handleRefresh}
disabled={isLoading}
className="px-4 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
title="Refresh content"
>
{isLoading ? (
<svg className="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
) : (
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
)}
</button>
</div>
</div>
{/* Last update indicator */}
{lastUpdate && (
<div className="text-xs text-gray-500 text-center sm:text-left mb-4">
Last updated: {lastUpdate.toLocaleTimeString()}
</div>
)}
{/* Search Results Section */}
{search.trim() && (
<div className="mb-8 sm:mb-10">