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
Some checks failed
Deploy / build-and-deploy (push) Failing after 2s
This commit is contained in:
@@ -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}'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">
|
||||
|
||||
Reference in New Issue
Block a user