'use client'; import { useState, useEffect, useCallback, useRef } from 'react'; import { useRouter } from 'next/navigation'; import Link from 'next/link'; interface Post { slug: string; title: string; date: string; tags: string[]; summary: string; content: string; pinned: boolean; } interface Folder { type: 'folder'; name: string; path: string; children: (Post | Folder)[]; } interface Post { type: 'post'; slug: string; title: string; date: string; tags: string[]; summary: string; content: string; pinned: boolean; } type Node = Post | Folder; export default function AdminPage() { const [isAuthenticated, setIsAuthenticated] = useState(false); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [nodes, setNodes] = useState([]); const [currentPath, setCurrentPath] = useState([]); const [newPost, setNewPost] = useState({ title: '', date: new Date().toISOString().split('T')[0], tags: '', summary: '', content: '', }); const [newFolderName, setNewFolderName] = useState(''); const [isDragging, setIsDragging] = useState(false); const [deleteConfirm, setDeleteConfirm] = useState<{ show: boolean; item: Node | null }>({ show: false, item: null, }); const [showManageContent, setShowManageContent] = useState(false); const [managePath, setManagePath] = useState([]); const [pinned, setPinned] = useState(() => { if (typeof window !== 'undefined') { return JSON.parse(localStorage.getItem('pinnedPosts') || '[]'); } return []; }); const [pinFeedback, setPinFeedback] = useState(null); const [showChangePassword, setShowChangePassword] = useState(false); const [changePwOld, setChangePwOld] = useState(''); const [changePwNew, setChangePwNew] = useState(''); const [changePwConfirm, setChangePwConfirm] = useState(''); const [changePwFeedback, setChangePwFeedback] = useState(null); const router = useRouter(); const usernameRef = useRef(null); const passwordRef = useRef(null); useEffect(() => { // Check if already authenticated const auth = localStorage.getItem('adminAuth'); if (auth === 'true') { setIsAuthenticated(true); loadContent(); const interval = setInterval(loadContent, 500); return () => clearInterval(interval); } }, []); useEffect(() => { localStorage.setItem('pinnedPosts', JSON.stringify(pinned)); }, [pinned]); const loadContent = async () => { try { const response = await fetch('/api/posts'); const data = await response.json(); setNodes(data); } catch (error) { console.error('Error loading content:', error); } }; const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); const form = e.target as HTMLFormElement; const formData = new FormData(form); const user = (formData.get('username') as string) || usernameRef.current?.value || ''; const pass = (formData.get('password') as string) || passwordRef.current?.value || ''; if (user !== 'admin') { alert('Ungültiger Benutzername'); return; } // Check password via API const res = await fetch('/api/admin/password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: pass, mode: 'login' }), }); const data = await res.json(); if (res.ok && data.success) { setIsAuthenticated(true); localStorage.setItem('adminAuth', 'true'); loadContent(); } else { alert(data.error || 'Ungültiges Passwort'); } }; const handleLogout = () => { setIsAuthenticated(false); localStorage.removeItem('adminAuth'); router.push('/'); }; const handleCreatePost = async (e: React.FormEvent) => { e.preventDefault(); try { const response = await fetch('/api/admin/posts', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ ...newPost, tags: newPost.tags.split(',').map(tag => tag.trim()), path: currentPath.join('/'), }), }); if (response.ok) { setNewPost({ title: '', date: new Date().toISOString().split('T')[0], tags: '', summary: '', content: '', }); loadContent(); } else { alert('Error creating post'); } } catch (error) { console.error('Error creating post:', error); alert('Error creating post'); } }; const handleCreateFolder = async (e: React.FormEvent) => { e.preventDefault(); if (!newFolderName.trim()) { alert('Please enter a folder name'); return; } try { const response = await fetch('/api/admin/folders', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ name: newFolderName, path: currentPath.join('/'), }), }); if (response.ok) { setNewFolderName(''); loadContent(); } else { alert('Error creating folder'); } } catch (error) { console.error('Error creating folder:', error); alert('Error creating folder'); } }; // Get current directory contents const getCurrentNodes = (): Node[] => { let currentNodes: Node[] = nodes; for (const segment of currentPath) { const folder = currentNodes.find( (n) => n.type === 'folder' && n.name === segment ) as Folder | undefined; if (folder) { currentNodes = folder.children; } else { break; } } return currentNodes; }; const currentNodes = getCurrentNodes(); // Breadcrumbs const breadcrumbs = [ { name: 'Root', path: [] }, ...currentPath.map((name, idx) => ({ name, path: currentPath.slice(0, idx + 1), })), ]; // Get nodes for manage content const getManageNodes = (): Node[] => { let currentNodes: Node[] = nodes; for (const segment of managePath) { const folder = currentNodes.find( (n) => n.type === 'folder' && n.name === segment ) as Folder | undefined; if (folder) { currentNodes = folder.children; } else { break; } } return currentNodes; }; const manageNodes = getManageNodes(); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragging(true); }, []); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); }, []); const handleDrop = useCallback(async (e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); const files = Array.from(e.dataTransfer.files); const markdownFiles = files.filter(file => file.name.endsWith('.md')); if (markdownFiles.length === 0) { alert('Please drop only Markdown files'); return; } for (const file of markdownFiles) { try { const content = await file.text(); const formData = new FormData(); formData.append('file', file); formData.append('path', currentPath.join('/')); const response = await fetch('/api/admin/upload', { method: 'POST', body: formData, }); if (!response.ok) { throw new Error(`Failed to upload ${file.name}`); } } catch (error) { console.error(`Error uploading ${file.name}:`, error); alert(`Error uploading ${file.name}`); } } loadContent(); }, [currentPath]); const handleDelete = async (node: Node) => { setDeleteConfirm({ show: true, item: node }); }; const confirmDelete = async () => { if (!deleteConfirm.item) return; try { const itemPath = currentPath.join('/'); const itemName = deleteConfirm.item.type === 'folder' ? deleteConfirm.item.name : deleteConfirm.item.slug; console.log('Deleting item:', { path: itemPath, name: itemName, type: deleteConfirm.item.type }); const response = await fetch('/api/admin/delete', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ path: itemPath, name: itemName, type: deleteConfirm.item.type, }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Failed to delete item'); } console.log('Delete successful:', data); await loadContent(); setDeleteConfirm({ show: false, item: null }); } catch (error) { console.error('Error deleting item:', error); alert(error instanceof Error ? error.message : 'Error deleting item'); } }; const handlePin = async (slug: string) => { setPinned((prev) => { const newPinned = prev.includes(slug) ? prev.filter((s) => s !== slug) : [slug, ...prev]; // Update pinned.json on the server fetch('/api/admin/posts', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ pinned: newPinned }), }) .then((res) => { if (!res.ok) { res.json().then((data) => { setPinFeedback(data.error || 'Fehler beim Aktualisieren der angehefteten Beiträge'); }); } else { setPinFeedback( newPinned.includes(slug) ? 'Beitrag angeheftet!' : 'Beitrag gelöst!' ); setTimeout(() => setPinFeedback(null), 2000); } }) .catch((err) => { setPinFeedback('Fehler beim Aktualisieren der angehefteten Beiträge'); }); return newPinned; }); }; // Password change handler const handleChangePassword = async (e: React.FormEvent) => { e.preventDefault(); setChangePwFeedback(null); if (!changePwOld || !changePwNew || !changePwConfirm) { setChangePwFeedback('Bitte alle Felder ausfüllen.'); return; } if (changePwNew !== changePwConfirm) { setChangePwFeedback('Die neuen Passwörter stimmen nicht überein.'); return; } // Check old password const res = await fetch('/api/admin/password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: changePwOld, mode: 'login' }), }); const data = await res.json(); if (!res.ok || !data.success) { setChangePwFeedback('Altes Passwort ist falsch.'); return; } // Set new password const res2 = await fetch('/api/admin/password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: changePwNew }), }); const data2 = await res2.json(); if (res2.ok && data2.success) { setChangePwFeedback('Passwort erfolgreich geändert!'); setChangePwOld(''); setChangePwNew(''); setChangePwConfirm(''); setTimeout(() => setShowChangePassword(false), 1500); } else { setChangePwFeedback(data2.error || 'Fehler beim Ändern des Passworts.'); } }; return (
{pinFeedback && (
{pinFeedback}
)} {!isAuthenticated ? (

Admin Login

setUsername(e.target.value)} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500" required autoComplete="username" />
setPassword(e.target.value)} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500" required autoComplete="current-password" />
) : (

Admin Dashboard

{/* Password Change Modal */} {showChangePassword && (

Passwort ändern

setChangePwOld(e.target.value)} className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2" required autoComplete="current-password" />
setChangePwNew(e.target.value)} className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2" required autoComplete="new-password" />
setChangePwConfirm(e.target.value)} className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2" required autoComplete="new-password" />
{changePwFeedback && (
{changePwFeedback}
)}
)} {/* Breadcrumbs with back button */}
{currentPath.length > 0 && ( )}
{breadcrumbs.map((crumb, index) => (
{index > 0 && /}
))}
{/* Create Folder Form */}

Create New Folder

setNewFolderName(e.target.value)} placeholder="Folder name" className="flex-1 rounded-md border border-gray-300 px-3 py-2" required />
{/* Drag and Drop Zone */}

Drag and drop Markdown files here

Files will be uploaded to: {currentPath.join('/') || 'root'}

{/* Create Post Form */}

Create New Post

setNewPost({ ...newPost, title: e.target.value })} className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2" required />
setNewPost({ ...newPost, date: e.target.value })} className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2" required />
setNewPost({ ...newPost, tags: e.target.value })} className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2" placeholder="tag1, tag2, tag3" />