CoreWiki 1.0.1

This commit is contained in:
root committed 2026-09-06 14:44:44 +02:00
1 parent 336cd68acc
commit 21d984a28e
5 files changed
+285 -157

No files matched your search

Regular → Executable
View File
File mode changed.
+1 -1
View File
@@ -4,7 +4,7 @@ services:
build: .
container_name: corewiki_app
ports:
- "8443:443"
- "8444:443"
volumes:
- ./src:/var/www/html
- ./data:/var/www/data
+259 -155
View File
@@ -15,23 +15,29 @@
tailwind.config = { theme: { extend: { colors: { darkbg: '#0f172a', darkcard: '#1e293b', darkborder: '#334155', accent: '#3b82f6' } } } }
</script>
<style>
.prose h1 { font-size: 2.25rem; font-weight: bold; margin-bottom: 1rem; color: white; border-bottom: 1px solid #334155; padding-bottom: 0.5rem;}
.prose h1 { font-size: 2rem; font-weight: bold; margin-bottom: 1rem; color: white; border-bottom: 1px solid #334155; padding-bottom: 0.5rem;}
@media (min-width: 768px) { .prose h1 { font-size: 2.25rem; } }
.prose h2 { font-size: 1.5rem; font-weight: bold; margin-top: 1.5rem; margin-bottom: 0.75rem; color: white;}
.prose p { margin-bottom: 1rem; line-height: 1.6; color: #cbd5e1; }
.prose code { background: #0f172a; padding: 0.2rem 0.4rem; border-radius: 4px; font-family: monospace; color: #60a5fa; }
.prose pre { background: #0f172a; padding: 1rem; border-radius: 8px; overflow-x: auto; margin-bottom: 1rem; border: 1px solid #334155; }
.prose pre code { background: transparent; padding: 0; color: #e2e8f0; }
.prose img { max-width: 100%; border-radius: 8px; border: 1px solid #334155; margin: 1rem 0;}
.prose code { background: #0f172a; padding: 0.2rem 0.4rem; border-radius: 4px; font-family: monospace; color: #60a5fa; word-break: break-all; }
.prose pre { background: #0f172a; padding: 1rem; border-radius: 8px; overflow-x: auto; margin-bottom: 1rem; border: 1px solid #334155; max-width: 100%; }
.prose pre code { background: transparent; padding: 0; color: #e2e8f0; word-break: normal; }
.prose img { max-width: 100%; border-radius: 8px; border: 1px solid #334155; margin: 1rem 0; height: auto;}
.prose a { color: #3b82f6; text-decoration: none; } .prose a:hover { text-decoration: underline; }
.prose ul { list-style-type: disc; padding-left: 1.5rem; margin-bottom: 1rem; color: #cbd5e1;}
textarea::-webkit-scrollbar { width: 8px; } textarea::-webkit-scrollbar-thumb { background-color: #334155; border-radius: 4px; }
.cat-children { margin-left: 1rem; padding-left: 0.5rem; border-left: 1px solid #334155; margin-top: 0.5rem; }
/* Fix für lesbare Mermaid-Graphen */
.mermaid { overflow-x: auto; text-align: center; padding: 1rem 0; margin-bottom: 1rem; }
.mermaid svg { max-width: none !important; min-width: 800px; height: auto; }
</style>
</head>
<body class="bg-darkbg text-gray-300 font-sans h-screen flex flex-col overflow-hidden">
<!-- LOGIN SCREEN -->
<div id="loginView" class="flex-1 flex items-center justify-center hidden">
<div class="bg-darkcard border border-darkborder rounded-xl p-8 shadow-2xl w-full max-w-sm">
<div id="loginView" class="flex-1 flex items-center justify-center hidden p-4">
<div class="bg-darkcard border border-darkborder rounded-xl p-6 md:p-8 shadow-2xl w-full max-w-sm">
<h2 class="text-2xl font-bold text-white mb-6 text-center">CoreSuite Login</h2>
<div id="loginError" class="text-red-500 text-sm text-center mb-4 hidden">Login fehlgeschlagen.</div>
<form id="loginForm" class="space-y-4">
@@ -44,77 +50,101 @@
<!-- MAIN APP -->
<div id="appView" class="flex-1 flex flex-col h-full hidden">
<header class="bg-darkcard border-b border-darkborder h-16 flex items-center justify-between px-6 shrink-0 z-10 shadow-sm">
<div class="flex items-center gap-3 w-64 cursor-pointer" onclick="openArticle(1)">
<div class="bg-accent p-1.5 rounded-lg text-white"><span class="text-lg">📚</span></div>
<span class="text-xl font-bold text-white tracking-wider">CoreWiki</span>
<!-- HEADER (Responsive) -->
<header class="bg-darkcard border-b border-darkborder h-16 flex items-center justify-between px-4 md:px-6 shrink-0 z-20 shadow-sm relative">
<!-- Mobile Menu Toggle -->
<button onclick="toggleMobileMenu()" class="md:hidden text-gray-400 hover:text-white mr-3 p-1">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg>
</button>
<div class="flex items-center gap-2 md:gap-3 md:w-64 cursor-pointer" onclick="openArticle(1)">
<div class="bg-accent p-1 md:p-1.5 rounded-lg text-white"><span class="text-base md:text-lg">📚</span></div>
<span class="text-lg md:text-xl font-bold text-white tracking-wider">CoreWiki</span>
</div>
<div class="flex items-center gap-6 justify-end">
<button onclick="createNewArticle()" class="bg-accent hover:bg-blue-500 text-white px-4 py-1.5 rounded-lg text-sm font-bold transition-colors flex items-center gap-2 shadow-lg shadow-accent/20">
<span>+ Neuer Artikel</span>
<div class="flex items-center gap-2 md:gap-6 justify-end flex-1">
<!-- LIVE SUCHE -->
<div class="relative hidden sm:block mr-2 md:mr-4">
<input type="text" id="searchInput" onkeyup="searchWiki()" placeholder="Artikel durchsuchen..." class="bg-darkbg border border-darkborder rounded p-1.5 px-3 text-sm text-white focus:outline-none focus:border-accent w-40 md:w-56 transition-all">
<div id="searchResults" class="absolute top-full left-0 mt-1 w-full bg-darkcard border border-darkborder rounded shadow-xl hidden z-50 max-h-64 overflow-y-auto"></div>
</div>
<button onclick="createNewArticle()" class="bg-accent hover:bg-blue-500 text-white px-3 py-1.5 md:px-4 md:py-1.5 rounded-lg text-xs md:text-sm font-bold transition-colors flex items-center gap-1 md:gap-2 shadow-lg shadow-accent/20">
<span class="md:hidden">+</span>
<span class="hidden md:inline">+ Neuer Artikel</span>
</button>
<div class="flex items-center gap-3 border-l border-darkborder pl-6">
<button onclick="openModal('pwdModal')" class="text-gray-400 hover:text-white px-2" title="Passwort ändern">🔑</button>
<button id="navAdmin" onclick="openModal('adminModal')" class="text-gray-400 hover:text-white px-2 hidden" title="System Settings">⚙️</button>
<div class="text-right ml-2 border-l border-darkborder pl-4">
<div class="flex items-center gap-2 md:gap-3 border-l border-darkborder pl-3 md:pl-6">
<button onclick="openModal('pwdModal')" class="text-gray-400 hover:text-white px-1 md:px-2" title="Passwort ändern">🔑</button>
<button id="navAdmin" onclick="openModal('adminModal')" class="text-gray-400 hover:text-white px-1 md:px-2 hidden" title="System Settings">⚙️</button>
<div class="text-right ml-1 md:ml-2 border-l border-darkborder pl-3 md:pl-4 hidden sm:block">
<p class="text-xs text-gray-500">Angemeldet</p>
<p class="text-sm text-white font-bold" id="userNameDisplay"></p>
</div>
<button onclick="logout()" class="text-gray-500 hover:text-red-500 ml-2" title="Abmelden">✖</button>
<button onclick="logout()" class="text-gray-500 hover:text-red-500 ml-1 md:ml-2" title="Abmelden">✖</button>
</div>
</div>
</header>
<!-- 3-SPALTEN LAYOUT -->
<div class="flex-1 flex overflow-hidden">
<!-- LINKE SPALTE: Katalog -->
<aside class="w-72 bg-darkcard border-r border-darkborder flex flex-col shrink-0">
<div class="p-5 flex-1 overflow-y-auto">
<h3 class="text-xs font-bold text-gray-500 uppercase tracking-wider mb-4">Bibliotheken</h3>
<!-- 3-SPALTEN LAYOUT (Mobile Optimized) -->
<div class="flex-1 flex overflow-hidden relative">
<!-- Overlay für mobiles Menü -->
<div id="mobileMenuOverlay" onclick="toggleMobileMenu()" class="fixed inset-0 bg-black/60 z-30 hidden md:hidden backdrop-blur-sm"></div>
<!-- LINKE SPALTE: Katalog (Off-Canvas on Mobile) -->
<aside id="leftSidebar" class="absolute md:relative z-40 inset-y-0 left-0 w-64 md:w-72 bg-darkcard border-r border-darkborder flex flex-col shrink-0 transform -translate-x-full md:translate-x-0 transition-transform duration-200 ease-in-out">
<div class="p-4 md:p-5 flex-1 overflow-y-auto">
<div class="flex justify-between items-center mb-4 md:mb-3">
<h3 class="text-xs font-bold text-gray-500 uppercase tracking-wider">Bibliotheken</h3>
<button onclick="toggleMobileMenu()" class="md:hidden text-gray-400 hover:text-white text-lg font-bold p-1">✕</button>
</div>
<nav class="space-y-4" id="categoryNav"></nav>
</div>
</aside>
<!-- MITTLERE SPALTE: Content & Editor -->
<main class="flex-1 bg-darkbg overflow-y-auto relative flex flex-col">
<div class="max-w-4xl w-full mx-auto p-10 flex-1 flex flex-col">
<main class="flex-1 bg-darkbg overflow-y-auto relative flex flex-col min-w-0">
<div class="max-w-4xl w-full mx-auto p-4 md:p-10 flex-1 flex flex-col">
<!-- LESE ANSICHT -->
<div id="readView" class="flex-1 flex flex-col">
<div class="border-b border-darkborder pb-4 mb-6 flex justify-between items-start">
<div>
<div class="flex items-center gap-2 text-xs text-accent font-bold mb-2"><span id="readCategory"></span></div>
<h1 class="text-3xl font-bold text-white mb-1" id="readTitle">Lade...</h1>
<div class="border-b border-darkborder pb-4 mb-4 md:mb-6 flex flex-wrap justify-between items-start gap-4">
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2 text-xs text-accent font-bold mb-1 md:mb-2"><span id="readCategory" class="truncate"></span></div>
<h1 class="text-2xl md:text-3xl font-bold text-white mb-1 break-words" id="readTitle">Lade...</h1>
<p class="text-xs text-gray-500">Von <span id="readAuthor"></span> am <span id="readDate"></span></p>
</div>
<div class="flex gap-2">
<div class="flex gap-2 shrink-0">
<button onclick="deleteArticle()" id="btnDelete" class="bg-darkbg border border-red-500/50 text-red-500 hover:bg-red-500 hover:text-white px-3 py-1.5 rounded text-xs transition-colors hidden">Löschen</button>
<button onclick="toggleEditMode()" id="btnEdit" class="bg-darkcard border border-darkborder hover:border-accent text-white px-4 py-1.5 rounded text-sm transition-colors hidden">✏️ Bearbeiten</button>
<button onclick="toggleEditMode()" class="bg-darkcard border border-darkborder hover:border-accent text-white px-3 md:px-4 py-1.5 rounded text-xs md:text-sm transition-colors">✏️ <span class="hidden sm:inline">Bearbeiten</span></button>
</div>
</div>
<article id="readContent" class="prose flex-1 pb-10">Lade Inhalt...</article>
<article id="readContent" class="prose flex-1 pb-10 w-full overflow-x-hidden">Lade Inhalt...</article>
</div>
<!-- EDITOR ANSICHT -->
<div id="editView" class="flex-1 flex flex-col hidden">
<div class="border-b border-darkborder pb-4 mb-6 flex justify-between items-center">
<div class="flex-1 flex gap-4 mr-4">
<select id="editCategory" class="bg-darkcard border border-darkborder rounded p-2 text-white text-sm outline-none focus:border-accent w-48"></select>
<input type="text" id="editTitle" class="flex-1 bg-darkcard border border-darkborder rounded p-2 text-white font-bold outline-none focus:border-accent" placeholder="Artikel Titel...">
<div class="border-b border-darkborder pb-4 mb-4 md:mb-6 flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
<div class="w-full md:flex-1 flex flex-col md:flex-row gap-3 md:gap-4 md:mr-4">
<select id="editCategory" class="bg-darkcard border border-darkborder rounded p-2 text-white text-sm outline-none focus:border-accent w-full md:w-48"></select>
<input type="text" id="editTitle" class="w-full md:flex-1 bg-darkcard border border-darkborder rounded p-2 text-white font-bold outline-none focus:border-accent" placeholder="Artikel Titel...">
</div>
<div class="flex gap-2">
<button onclick="toggleEditMode()" class="text-gray-400 hover:text-white px-4 py-1.5 rounded text-sm transition-colors">Abbrechen</button>
<button onclick="saveArticle()" class="bg-accent hover:bg-blue-500 text-white px-4 py-1.5 rounded text-sm font-bold transition-colors shadow-lg shadow-accent/20">💾 Speichern</button>
<div class="flex gap-2 w-full md:w-auto justify-end">
<button onclick="toggleEditMode()" class="text-gray-400 hover:text-white px-3 md:px-4 py-2 rounded text-sm transition-colors border border-transparent hover:border-gray-600">Abbrechen</button>
<button onclick="saveArticle()" class="bg-accent hover:bg-blue-500 text-white px-4 py-2 rounded text-sm font-bold transition-colors shadow-lg shadow-accent/20">💾 Speichern</button>
</div>
</div>
<div class="bg-blue-500/10 border border-accent/30 text-accent text-xs p-2 rounded mb-4 flex items-center justify-between">
<span>Markdown & HTML unterstützt. <b>Bilder per Drag & Drop!</b> Netzpläne via ```mermaid</span>
<div class="bg-blue-500/10 border border-accent/30 text-accent text-xs p-3 rounded mb-4">
Markdown & HTML unterstützt. <b>Bilder per Drag & Drop!</b> Netzpläne via ```mermaid
</div>
<textarea id="editContent" class="flex-1 w-full bg-darkcard border border-darkborder rounded-lg p-4 text-gray-300 font-mono text-sm outline-none focus:border-accent resize-none mb-10" placeholder="Schreibe deinen Artikel hier in Markdown..."></textarea>
<textarea id="editContent" class="flex-1 w-full bg-darkcard border border-darkborder rounded-lg p-3 md:p-4 text-gray-300 font-mono text-xs md:text-sm outline-none focus:border-accent resize-none mb-10" placeholder="Schreibe deinen Artikel hier in Markdown..."></textarea>
</div>
</div>
</main>
<!-- RECHTE SPALTE: Historie & Updates -->
<aside class="w-64 bg-darkcard border-l border-darkborder p-5 flex flex-col shrink-0">
<!-- RECHTE SPALTE: Historie & Updates (Nur Desktop/Tablet-Landscape) -->
<aside class="w-64 bg-darkcard border-l border-darkborder p-5 flex-col shrink-0 hidden lg:flex">
<h3 class="text-xs font-bold text-gray-500 uppercase tracking-wider mb-3">Zuletzt aktualisiert</h3>
<div id="latestNav" class="space-y-1 overflow-y-auto mb-8 pb-4 border-b border-darkborder"></div>
@@ -125,84 +155,86 @@
</div>
<!-- CORETEMPLATE MODALS -->
<div id="pwdModal" class="fixed inset-0 bg-black/80 hidden z-50 flex items-center justify-center backdrop-blur-sm">
<div class="bg-darkcard border border-darkborder rounded-xl p-6 shadow-2xl w-full max-w-sm">
<div class="flex justify-between items-center mb-4"><h2 class="text-lg font-bold text-white">Passwort ändern</h2><button onclick="closeModal('pwdModal')" class="text-gray-400 hover:text-white">✕</button></div>
<div id="pwdModal" class="fixed inset-0 bg-black/80 hidden z-50 flex items-center justify-center p-4 backdrop-blur-sm">
<div class="bg-darkcard border border-darkborder rounded-xl p-5 md:p-6 shadow-2xl w-full max-w-sm">
<div class="flex justify-between items-center mb-4"><h2 class="text-lg font-bold text-white">Passwort ändern</h2><button onclick="closeModal('pwdModal')" class="text-gray-400 hover:text-white text-xl">✕</button></div>
<div id="pwMsg" class="text-xs mb-3 text-green-400 hidden"></div>
<form id="passwordForm" class="space-y-4">
<input type="password" id="oldPassword" placeholder="Altes Passwort" required class="w-full bg-darkbg border border-darkborder rounded p-2 text-white outline-none focus:border-accent">
<input type="password" id="newPassword" placeholder="Neues Passwort (min. 4)" required class="w-full bg-darkbg border border-darkborder rounded p-2 text-white outline-none focus:border-accent">
<input type="password" id="newPasswordConfirm" placeholder="Passwort wiederholen" required class="w-full bg-darkbg border border-darkborder rounded p-2 text-white outline-none focus:border-accent">
<button type="submit" class="w-full bg-accent text-white font-bold py-2 rounded mt-2">Ändern</button>
<button type="submit" class="w-full bg-accent text-white font-bold py-2.5 rounded mt-2">Ändern</button>
</form>
</div>
</div>
<div id="adminModal" class="fixed inset-0 bg-black/80 hidden z-50 flex items-center justify-center backdrop-blur-sm">
<div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-5xl flex flex-col h-[85vh]">
<div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg rounded-t-xl">
<h2 class="text-xl font-bold text-white">System Administration</h2>
<div class="flex gap-4">
<button onclick="switchAdminTab('categories')" id="btn-admin-categories" class="text-accent font-bold">Kategorien</button>
<button onclick="switchAdminTab('users')" id="btn-admin-users" class="text-gray-400 hover:text-white">Benutzer</button>
<button onclick="switchAdminTab('settings')" id="btn-admin-settings" class="text-gray-400 hover:text-white">Settings</button>
<button onclick="switchAdminTab('logs')" id="btn-admin-logs" class="text-gray-400 hover:text-white">Audit Log</button>
<button onclick="closeModal('adminModal')" class="text-gray-500 hover:text-red-500 ml-4 border-l border-darkborder pl-4">✕ Schließen</button>
<div id="adminModal" class="fixed inset-0 bg-black/80 hidden z-50 flex items-center justify-center p-2 md:p-4 backdrop-blur-sm">
<div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-5xl flex flex-col max-h-[95vh] md:max-h-[85vh]">
<div class="p-4 md:p-5 border-b border-darkborder flex justify-between items-center bg-darkbg rounded-t-xl overflow-x-auto">
<h2 class="text-lg md:text-xl font-bold text-white whitespace-nowrap mr-4">System Administration</h2>
<div class="flex gap-2 md:gap-4 shrink-0">
<button onclick="switchAdminTab('categories')" id="btn-admin-categories" class="text-accent font-bold text-sm md:text-base px-2">Kategorien</button>
<button onclick="switchAdminTab('users')" id="btn-admin-users" class="text-gray-400 hover:text-white text-sm md:text-base px-2">Benutzer</button>
<button onclick="switchAdminTab('settings')" id="btn-admin-settings" class="text-gray-400 hover:text-white text-sm md:text-base px-2">Settings</button>
<button onclick="switchAdminTab('logs')" id="btn-admin-logs" class="text-gray-400 hover:text-white text-sm md:text-base px-2">Audit Log</button>
<button onclick="closeModal('adminModal')" class="text-gray-500 hover:text-red-500 ml-2 md:ml-4 border-l border-darkborder pl-2 md:pl-4 text-sm md:text-base">✕</button>
</div>
</div>
<div class="p-6 overflow-y-auto flex-1 bg-darkbg">
<div class="p-4 md:p-6 overflow-y-auto flex-1 bg-darkbg">
<!-- CATEGORIES TAB -->
<div id="admin-tab-categories" class="space-y-6">
<div class="bg-darkcard p-4 rounded border border-darkborder">
<form id="catForm" class="flex gap-4 items-end">
<form id="catForm" class="flex flex-col md:flex-row gap-4 items-start md:items-end">
<input type="hidden" id="catId" value="0">
<div class="w-32">
<label class="block text-xs text-gray-500 mb-1">Icon</label>
<select id="catIcon" required class="w-full bg-darkbg border border-darkborder rounded p-2 text-white">
<option value="📁">📁 Ordner</option>
<option value="💻">💻 Code</option>
<option value="🖥️">🖥️ Server</option>
<option value="🌐">🌐 Netzwerk</option>
<option value="📝">📝 Notizen</option>
<option value="⚙️">⚙️ Configs</option>
</select>
</div>
<div class="flex-1"><label class="block text-xs text-gray-500 mb-1">Kategorie-Name</label><input type="text" id="catName" required class="w-full bg-darkbg border border-darkborder rounded p-2 text-white"></div>
<div class="flex-1"><label class="block text-xs text-gray-500 mb-1">Unterkategorie von...</label><select id="catParentId" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white"><option value="">-- Keine (Hauptordner) --</option></select></div>
<button type="submit" class="bg-accent text-white font-bold py-2 px-6 rounded">Speichern</button>
<button type="button" onclick="resetCatForm()" class="bg-darkbg border border-darkborder text-gray-400 hover:text-white font-bold py-2 px-4 rounded">X</button>
<div class="w-full md:w-32">
<label class="block text-xs text-gray-500 mb-1">Icon</label>
<select id="catIcon" required class="w-full bg-darkbg border border-darkborder rounded p-2 text-white">
<option value="📁">📁 Ordner</option>
<option value="💻">💻 Code</option>
<option value="🖥️">🖥️ Server</option>
<option value="🌐">🌐 Netzwerk</option>
<option value="📝">📝 Notizen</option>
<option value="⚙️">⚙️ Configs</option>
</select>
</div>
<div class="w-full flex-1"><label class="block text-xs text-gray-500 mb-1">Kategorie-Name</label><input type="text" id="catName" required class="w-full bg-darkbg border border-darkborder rounded p-2 text-white"></div>
<div class="w-full flex-1"><label class="block text-xs text-gray-500 mb-1">Unterkategorie von...</label><select id="catParentId" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white"><option value="">-- Keine (Hauptordner) --</option></select></div>
<div class="flex gap-2 w-full md:w-auto">
<button type="submit" class="flex-1 bg-accent text-white font-bold py-2 px-6 rounded">Speichern</button>
<button type="button" onclick="resetCatForm()" class="bg-darkbg border border-darkborder text-gray-400 hover:text-white font-bold py-2 px-4 rounded">X</button>
</div>
</form>
</div>
<div id="adminCatList" class="space-y-1"></div>
<div id="adminCatList" class="space-y-2"></div>
</div>
<!-- USERS TAB -->
<div id="admin-tab-users" class="hidden space-y-6">
<div class="bg-darkcard p-4 rounded border border-darkborder">
<form id="userForm" class="flex gap-4 items-end">
<div class="flex-1"><label class="block text-xs text-gray-500 mb-1">Benutzername</label><input type="text" id="newUsername" required class="w-full bg-darkbg border border-darkborder rounded p-2 text-white"></div>
<div class="flex-1"><label class="block text-xs text-gray-500 mb-1">Passwort</label><input type="password" id="newUserPass" required class="w-full bg-darkbg border border-darkborder rounded p-2 text-white"></div>
<div class="w-32"><label class="block text-xs text-gray-500 mb-1">Rolle</label><select id="newUserRole" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white"><option value="user">Author</option><option value="admin">Admin</option></select></div>
<button type="submit" class="bg-accent text-white font-bold py-2 px-6 rounded">Anlegen</button>
<form id="userForm" class="flex flex-col md:flex-row gap-4 items-start md:items-end">
<div class="w-full flex-1"><label class="block text-xs text-gray-500 mb-1">Benutzername</label><input type="text" id="newUsername" required class="w-full bg-darkbg border border-darkborder rounded p-2 text-white"></div>
<div class="w-full flex-1"><label class="block text-xs text-gray-500 mb-1">Passwort</label><input type="password" id="newUserPass" required class="w-full bg-darkbg border border-darkborder rounded p-2 text-white"></div>
<div class="w-full md:w-32"><label class="block text-xs text-gray-500 mb-1">Rolle</label><select id="newUserRole" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white"><option value="user">Author</option><option value="admin">Admin</option></select></div>
<button type="submit" class="w-full md:w-auto bg-accent text-white font-bold py-2 px-6 rounded">Anlegen</button>
</form>
</div>
<table class="w-full text-left text-sm text-gray-400"><thead class="bg-darkcard border-b border-darkborder text-xs uppercase"><tr><th class="p-3">ID</th><th class="p-3">User</th><th class="p-3">Rolle</th><th class="p-3 text-right">Aktion</th></tr></thead><tbody id="usersTableBody" class="divide-y divide-darkborder"></tbody></table>
<div class="overflow-x-auto"><table class="w-full text-left text-sm text-gray-400 min-w-[400px]"><thead class="bg-darkcard border-b border-darkborder text-xs uppercase"><tr><th class="p-3">ID</th><th class="p-3">User</th><th class="p-3">Rolle</th><th class="p-3 text-right">Aktion</th></tr></thead><tbody id="usersTableBody" class="divide-y divide-darkborder"></tbody></table></div>
</div>
<!-- SETTINGS TAB -->
<div id="admin-tab-settings" class="hidden space-y-6">
<form id="settingsForm" class="flex gap-4 items-end bg-darkcard p-4 rounded border border-darkborder">
<div class="flex-1"><label class="block text-xs text-gray-500 mb-1">Config Key</label><input type="text" id="configKey" required class="w-full bg-darkbg border border-darkborder rounded p-2 text-white"></div>
<div class="flex-1"><label class="block text-xs text-gray-500 mb-1">Value</label><input type="text" id="configValue" required class="w-full bg-darkbg border border-darkborder rounded p-2 text-white"></div>
<button type="submit" class="bg-accent text-white font-bold py-2 px-6 rounded">Speichern</button>
<form id="settingsForm" class="flex flex-col md:flex-row gap-4 items-start md:items-end bg-darkcard p-4 rounded border border-darkborder">
<div class="w-full flex-1"><label class="block text-xs text-gray-500 mb-1">Config Key</label><input type="text" id="configKey" required class="w-full bg-darkbg border border-darkborder rounded p-2 text-white"></div>
<div class="w-full flex-1"><label class="block text-xs text-gray-500 mb-1">Value</label><input type="text" id="configValue" required class="w-full bg-darkbg border border-darkborder rounded p-2 text-white"></div>
<button type="submit" class="w-full md:w-auto bg-accent text-white font-bold py-2 px-6 rounded">Speichern</button>
</form>
<table class="w-full text-left text-sm text-gray-400"><thead class="bg-darkcard border-b border-darkborder text-xs uppercase"><tr><th class="p-3">Key</th><th class="p-3">Value</th><th class="p-3 text-right">Aktion</th></tr></thead><tbody id="settingsTableBody" class="divide-y divide-darkborder"></tbody></table>
<div class="overflow-x-auto"><table class="w-full text-left text-sm text-gray-400 min-w-[400px]"><thead class="bg-darkcard border-b border-darkborder text-xs uppercase"><tr><th class="p-3">Key</th><th class="p-3">Value</th><th class="p-3 text-right">Aktion</th></tr></thead><tbody id="settingsTableBody" class="divide-y divide-darkborder"></tbody></table></div>
</div>
<!-- LOGS TAB -->
<div id="admin-tab-logs" class="hidden">
<table class="w-full text-left text-sm text-gray-400"><thead class="bg-darkcard border-b border-darkborder text-xs uppercase"><tr><th class="p-3">Zeitstempel</th><th class="p-3">User</th><th class="p-3">Aktion</th><th class="p-3">Ziel</th></tr></thead><tbody id="logTableBody" class="divide-y divide-darkborder"></tbody></table>
<div id="admin-tab-logs" class="hidden overflow-x-auto">
<table class="w-full text-left text-sm text-gray-400 min-w-[500px]"><thead class="bg-darkcard border-b border-darkborder text-xs uppercase"><tr><th class="p-3">Zeitstempel</th><th class="p-3">User</th><th class="p-3">Aktion</th><th class="p-3">Ziel</th></tr></thead><tbody id="logTableBody" class="divide-y divide-darkborder"></tbody></table>
</div>
</div>
</div>
@@ -214,17 +246,25 @@
document.addEventListener('DOMContentLoaded', checkSession);
function checkSession() {
fetch('/api/me').then(res=>res.json()).then(data => {
if (data.success) {
document.getElementById('loginView').classList.add('hidden');
document.getElementById('appView').classList.remove('hidden');
document.getElementById('userNameDisplay').innerText = data.username;
isAdmin = (data.role === 'admin');
if (isAdmin) document.getElementById('navAdmin').classList.remove('hidden');
loadCategories(); loadLatest(); loadHistory(); openArticle(1);
} else {
document.getElementById('appView').classList.add('hidden');
document.getElementById('loginView').classList.remove('hidden');
fetch('/api/me')
.then(res => res.text()) // Antwort erst als rohen Text lesen
.then(text => {
try {
const data = JSON.parse(text); // Versuchen, es in JSON umzuwandeln
if (data.success) {
document.getElementById('loginView').classList.add('hidden');
document.getElementById('appView').classList.remove('hidden');
document.getElementById('userNameDisplay').innerText = data.username;
isAdmin = (data.role === 'admin');
if (isAdmin) document.getElementById('navAdmin').classList.remove('hidden');
loadCategories(); loadLatest(); loadHistory(); openArticle(1);
} else {
document.getElementById('appView').classList.add('hidden');
document.getElementById('loginView').classList.remove('hidden');
}
} catch (e) {
// Wenn es kein JSON ist, ist das Backend gecrasht. Fehler direkt anzeigen!
document.body.innerHTML = `<div class="p-10 text-white font-mono"><h2 class="text-red-500 font-bold text-2xl mb-4">Backend-Crash 💥</h2><p>Die PHP-Dateien werfen einen Fehler statt JSON aus:</p><div class="bg-black p-4 mt-4 rounded border border-red-500 overflow-auto">${text}</div></div>`;
}
});
}
@@ -241,6 +281,15 @@
function logout() { fetch('/api/logout').then(() => { location.reload(); }); }
// --- MOBILE MENU LOGIC ---
function toggleMobileMenu() {
const sidebar = document.getElementById('leftSidebar');
const overlay = document.getElementById('mobileMenuOverlay');
sidebar.classList.toggle('-translate-x-full');
overlay.classList.toggle('hidden');
}
// --- WIKI LOGIK (REKURSIVE KATEGORIEN) ---
function loadCategories() {
fetch('/api/categories').then(res=>res.json()).then(data => {
if(!data.success) return;
@@ -250,48 +299,37 @@
if(adminList) adminList.innerHTML = ''; if(adminSel) adminSel.innerHTML = '<option value="">-- Keine (Hauptordner) --</option>';
let allCats = data.data;
let mainCats = allCats.filter(c => c.parent_id == null);
// ROBUSTHEITS-CHECK FÜR LEERES WIKI
if(allCats.length === 0) {
nav.innerHTML = '<p class="text-xs text-gray-500 italic p-2 leading-relaxed">Noch keine Bibliotheken vorhanden.<br>Lege oben rechts unter ⚙️ deine erste Kategorie an!</p>';
sel.innerHTML = '<option value="1">Standard Kategorie</option>';
return;
}
let mainCats = allCats.filter(c => !c.parent_id);
// Admin Dropdowns füllen
allCats.forEach(c => {
let safeName = (c.name || 'Unbenannt').replace(/'/g, "\\'");
let safeIcon = c.icon || '📁';
let safeParent = c.parent_id || '';
sel.innerHTML += `<option value="${c.id}">${safeIcon} ${c.name || 'Unbenannt'}</option>`;
if(adminSel) adminSel.innerHTML += `<option value="${c.id}">${safeIcon} ${c.name || 'Unbenannt'}</option>`;
sel.innerHTML += `<option value="${c.id}">${c.icon} ${c.name}</option>`;
if(adminSel) adminSel.innerHTML += `<option value="${c.id}">${c.icon} ${c.name}</option>`;
if(adminList) {
let parentLabel = c.parent_id ? ' <span class="text-xs text-gray-500 ml-2">(Unterkategorie)</span>' : '';
adminList.innerHTML += `<div class="flex justify-between items-center bg-darkbg p-2 rounded mb-1 text-sm border border-darkborder"><span class="font-bold text-white">${safeIcon} ${c.name || 'Unbenannt'}${parentLabel}</span> <div><button onclick="editCategory(${c.id}, '${safeName}', '${safeIcon}', '${safeParent}')" class="text-blue-400 hover:text-white mr-3">✏️</button><button onclick="deleteCategory(${c.id})" class="text-red-500 hover:text-white">🗑️</button></div></div>`;
adminList.innerHTML += `<div class="flex justify-between items-center bg-darkbg p-3 rounded mb-2 text-sm border border-darkborder"><span class="font-bold text-white break-words">${c.icon} ${c.name}${parentLabel}</span> <div class="shrink-0 ml-2"><button onclick="editCategory(${c.id}, '${c.name.replace(/'/g, "\\'")}', '${c.icon}', '${c.parent_id || ''}')" class="text-blue-400 hover:text-white mr-3 text-lg">✏️</button><button onclick="deleteCategory(${c.id})" class="text-red-500 hover:text-white text-lg">🗑️</button></div></div>`;
}
});
// Rekursiver Baum-Aufbau für Navigation
mainCats.forEach(cat => { nav.innerHTML += buildCategoryTree(cat, allCats); });
}).catch(err => console.error(err));
});
}
function buildCategoryTree(cat, allCats) {
let children = allCats.filter(c => c.parent_id == cat.id);
let safeName = cat.name || 'Unbenannt'; let safeIcon = cat.icon || '📁';
let html = `<div><div class="flex items-center gap-2 text-white font-bold mb-2"><span>${safeIcon}</span> ${safeName}</div><ul class="space-y-1 border-l border-darkborder ml-3 pl-3">`;
let html = `<div><div class="flex items-center gap-2 text-white font-bold mb-2"><span>${cat.icon}</span> <span class="truncate">${cat.name}</span></div><ul class="space-y-1 border-l border-darkborder ml-3 pl-3">`;
let arts = cat.articles || [];
if(arts.length === 0 && children.length === 0) html += `<li class="text-xs text-gray-500 italic">Leer</li>`;
if(cat.articles.length === 0 && children.length === 0) html += `<li class="text-xs text-gray-500 italic py-1">Leer</li>`;
arts.forEach(art => {
cat.articles.forEach(art => {
let activeCls = (art.id == currentArticleId) ? 'text-accent font-bold' : 'text-gray-400 hover:text-white';
html += `<li onclick="openArticle(${art.id})" class="text-sm cursor-pointer truncate ${activeCls} transition-colors">${art.title}</li>`;
html += `<li onclick="openArticle(${art.id})" class="text-sm cursor-pointer truncate py-1 ${activeCls} transition-colors">${art.title}</li>`;
});
children.forEach(child => { html += `<li class="mt-3">` + buildCategoryTree(child, allCats) + `</li>`; });
children.forEach(child => {
html += `<li class="mt-3">` + buildCategoryTree(child, allCats) + `</li>`;
});
return html + `</ul></div>`;
}
@@ -299,7 +337,6 @@
fetch('/api/latest').then(res=>res.json()).then(data => {
if(!data.success) return;
const nav = document.getElementById('latestNav'); nav.innerHTML = '';
if(data.data.length === 0) nav.innerHTML = '<div class="text-xs text-gray-500 italic">Noch keine Updates</div>';
data.data.forEach(a => { nav.innerHTML += `<div onclick="openArticle(${a.id})" class="text-sm cursor-pointer p-2 hover:bg-darkbg rounded transition-colors border-l-2 border-transparent hover:border-blue-500"><div class="font-medium text-white truncate">${a.title}</div><div class="text-xs text-gray-500">Von ${a.author_name}</div></div>`; });
});
}
@@ -308,28 +345,13 @@
fetch('/api/history').then(res=>res.json()).then(data => {
if(!data.success) return;
const nav = document.getElementById('historyNav'); nav.innerHTML = '';
if(data.data.length === 0) nav.innerHTML = '<div class="text-xs text-gray-500 italic">Noch keine Historie</div>';
data.data.forEach(h => { nav.innerHTML += `<div onclick="openArticle(${h.id})" class="text-sm cursor-pointer p-2 hover:bg-darkbg rounded transition-colors border-l-2 border-transparent hover:border-accent"><div class="font-medium text-gray-300 truncate">${h.title}</div><div class="text-xs text-gray-500">${h.viewed_at.substring(0,16)}</div></div>`; });
});
}
function openArticle(id) {
fetch('/api/article?id=' + id).then(res=>res.json()).then(data => {
// FALLBACK WENN DB KOMPLETT LEER IST
if(!data.success) {
document.getElementById('readView').classList.remove('hidden');
document.getElementById('editView').classList.add('hidden');
document.getElementById('readCategory').innerText = 'System Info';
document.getElementById('readTitle').innerText = 'CoreWiki ist einsatzbereit!';
document.getElementById('readAuthor').innerText = '-';
document.getElementById('readDate').innerText = '-';
document.getElementById('readContent').innerHTML = '<p class="text-gray-300 bg-darkcard p-4 rounded-lg border border-darkborder border-l-4 border-l-accent">Das Wiki ist noch leer. Klicke oben rechts auf das <b>Zahnrad ⚙️ -> Kategorien</b>, um deine erste Bibliothek (z.B. "Infrastruktur") anzulegen. Klicke danach auf <b>+ Neuer Artikel</b>, um zu starten!</p>';
document.getElementById('btnDelete').classList.add('hidden');
document.getElementById('btnEdit').classList.add('hidden');
return;
}
document.getElementById('btnEdit').classList.remove('hidden');
if(!data.success) return;
currentArticleId = id; const art = data.data;
document.getElementById('readView').classList.remove('hidden'); document.getElementById('editView').classList.add('hidden');
document.getElementById('readCategory').innerText = art.category_name || 'Allgemein'; document.getElementById('readTitle').innerText = art.title;
@@ -341,6 +363,12 @@
if (isAdmin && id != 1) document.getElementById('btnDelete').classList.remove('hidden'); else document.getElementById('btnDelete').classList.add('hidden');
if(window.mermaid) setTimeout(() => { window.mermaid.run({ querySelector: '.mermaid' }); }, 100);
loadCategories(); loadHistory();
// Close mobile menu if open
if(window.innerWidth < 768) {
document.getElementById('leftSidebar').classList.add('-translate-x-full');
document.getElementById('mobileMenuOverlay').classList.add('hidden');
}
});
}
@@ -372,6 +400,7 @@
fetch('/api/article/delete', { method: 'POST', body: fd }).then(res=>res.json()).then(data => { if(data.success) { openArticle(1); loadLatest(); } else alert('Fehler'); });
}
// --- Kategorie Management ---
document.getElementById('catForm').addEventListener('submit', function(e) {
e.preventDefault(); const fd = new FormData();
fd.append('id', document.getElementById('catId').value); fd.append('name', document.getElementById('catName').value);
@@ -381,13 +410,17 @@
});
});
function resetCatForm() { document.getElementById('catForm').reset(); document.getElementById('catId').value = '0'; }
function editCategory(id, name, icon, parentId) { document.getElementById('catId').value = id; document.getElementById('catName').value = name; document.getElementById('catIcon').value = icon; document.getElementById('catParentId').value = parentId; }
function editCategory(id, name, icon, parentId) {
document.getElementById('catId').value = id; document.getElementById('catName').value = name;
document.getElementById('catIcon').value = icon; document.getElementById('catParentId').value = parentId;
}
function deleteCategory(id) {
if(!confirm('Kategorie löschen? (Artikel werden in Hauptkategorie verschoben)')) return;
const fd = new FormData(); fd.append('id', id);
fetch('/api/categories/delete', { method: 'POST', body: fd }).then(res=>res.json()).then(data => { if(data.success) loadCategories(); });
}
// --- DRAG & DROP BILDER UPLOAD ---
const editor = document.getElementById('editContent');
editor.addEventListener('dragover', (e) => { e.preventDefault(); editor.classList.add('border-accent'); });
editor.addEventListener('dragleave', (e) => { e.preventDefault(); editor.classList.remove('border-accent'); });
@@ -402,19 +435,20 @@
else editor.value = editor.value.replace(`![Lade Bild hoch...](${file.name})`, `*(Upload fehlgeschlagen)*`);
});
// --- CORETEMPLATE ADMIN LOGIK ---
function openModal(id) {
document.getElementById(id).classList.remove('hidden');
if(id === 'adminModal') { loadUsers(); loadSettings(); loadAuditLogs(); loadCategories(); }
if(id === 'adminModal') { loadUsers(); loadSettings(); loadAuditLogs(); }
}
function closeModal(id) { document.getElementById(id).classList.add('hidden'); }
function switchAdminTab(tab) {
['categories', 'users', 'settings', 'logs'].forEach(t => {
document.getElementById('admin-tab-'+t).classList.add('hidden');
document.getElementById('btn-admin-'+t).className = 'text-gray-400 hover:text-white';
document.getElementById('btn-admin-'+t).className = 'text-gray-400 hover:text-white text-sm md:text-base px-2';
});
document.getElementById('admin-tab-'+tab).classList.remove('hidden');
document.getElementById('btn-admin-'+tab).className = 'text-accent font-bold';
document.getElementById('btn-admin-'+tab).className = 'text-accent font-bold text-sm md:text-base px-2';
}
document.getElementById('passwordForm').addEventListener('submit', function(e) {
@@ -428,13 +462,83 @@
});
});
function loadAuditLogs() { fetch('/api/logs').then(res => res.json()).then(data => { if (data.success) { const tbody = document.getElementById('logTableBody'); tbody.innerHTML = ''; data.data.forEach(log => { tbody.innerHTML += `<tr><td class="p-3">${log.timestamp}</td><td class="p-3 font-bold">${log.username}</td><td class="p-3 text-white">${log.action}</td><td class="p-3">${log.target || '-'}</td></tr>`; }); } }); }
function loadSettings() { fetch('/api/settings').then(res => res.json()).then(data => { if (data.success) { const tbody = document.getElementById('settingsTableBody'); tbody.innerHTML = ''; data.data.forEach(setting => { tbody.innerHTML += `<tr><td class="p-3 font-bold">${setting.config_key}</td><td class="p-3">${setting.config_value}</td><td class="p-3 text-right"><button onclick="deleteSetting('${setting.config_key}')" class="text-red-500 hover:text-white">✖</button></td></tr>`; }); } }); }
function loadUsers() { fetch('/api/users').then(res => res.json()).then(data => { if (data.success) { const tbody = document.getElementById('usersTableBody'); tbody.innerHTML = ''; data.data.forEach(u => { let roleBadge = u.role === 'admin' ? '<span class="bg-red-500/20 text-red-500 px-2 py-1 rounded text-xs">Admin</span>' : '<span class="bg-blue-500/20 text-accent px-2 py-1 rounded text-xs">Author</span>'; tbody.innerHTML += `<tr><td class="p-3">${u.id}</td><td class="p-3 font-bold text-white">${u.username}</td><td class="p-3">${roleBadge}</td><td class="p-3 text-right"><button onclick="deleteUser(${u.id})" class="text-red-500 hover:text-white">🗑️</button></td></tr>`; }); } }); }
function deleteSetting(key) { if (!confirm(`Einstellung "${key}" löschen?`)) return; const fd = new FormData(); fd.append('config_key', key); fetch('/api/settings/delete', { method: 'POST', body: fd }).then(res => res.json()).then(data => { if (data.success) loadSettings(); }); }
function deleteUser(id) { if (!confirm('Benutzer löschen?')) return; const fd = new FormData(); fd.append('id', id); fetch('/api/users/delete', { method: 'POST', body: fd }).then(res => res.json()).then(data => { if (data.success) loadUsers(); else alert(data.error); }); }
document.getElementById('settingsForm').addEventListener('submit', function(e) { e.preventDefault(); const fd = new FormData(); fd.append('config_key', document.getElementById('configKey').value); fd.append('config_value', document.getElementById('configValue').value); fetch('/api/settings', { method: 'POST', body: fd }).then(res => res.json()).then(data => { if (data.success) { document.getElementById('configKey').value = ''; document.getElementById('configValue').value = ''; loadSettings(); } }); });
document.getElementById('userForm').addEventListener('submit', function(e) { e.preventDefault(); const fd = new FormData(); fd.append('username', document.getElementById('newUsername').value); fd.append('password', document.getElementById('newUserPass').value); fd.append('role', document.getElementById('newUserRole').value); fetch('/api/users', { method: 'POST', body: fd }).then(res => res.json()).then(data => { if (data.success) { document.getElementById('newUsername').value = ''; document.getElementById('newUserPass').value = ''; loadUsers(); } else alert(data.error); }); });
function loadAuditLogs() {
fetch('/api/logs').then(res => res.json()).then(data => {
if (data.success) {
const tbody = document.getElementById('logTableBody'); tbody.innerHTML = '';
data.data.forEach(log => { tbody.innerHTML += `<tr><td class="p-3">${log.timestamp}</td><td class="p-3 font-bold">${log.username}</td><td class="p-3 text-white">${log.action}</td><td class="p-3">${log.target || '-'}</td></tr>`; });
}
});
}
function loadSettings() {
fetch('/api/settings').then(res => res.json()).then(data => {
if (data.success) {
const tbody = document.getElementById('settingsTableBody'); tbody.innerHTML = '';
data.data.forEach(setting => { tbody.innerHTML += `<tr><td class="p-3 font-bold">${setting.config_key}</td><td class="p-3 break-all">${setting.config_value}</td><td class="p-3 text-right"><button onclick="deleteSetting('${setting.config_key}')" class="text-red-500 hover:text-white text-lg">✖</button></td></tr>`; });
}
});
}
function loadUsers() {
fetch('/api/users').then(res => res.json()).then(data => {
if (data.success) {
const tbody = document.getElementById('usersTableBody'); tbody.innerHTML = '';
data.data.forEach(u => {
let roleBadge = u.role === 'admin' ? '<span class="bg-red-500/20 text-red-500 px-2 py-1 rounded text-xs">Admin</span>' : '<span class="bg-blue-500/20 text-accent px-2 py-1 rounded text-xs">Author</span>';
tbody.innerHTML += `<tr><td class="p-3">${u.id}</td><td class="p-3 font-bold text-white break-words">${u.username}</td><td class="p-3">${roleBadge}</td><td class="p-3 text-right"><button onclick="deleteUser(${u.id})" class="text-red-500 hover:text-white text-lg">🗑️</button></td></tr>`;
});
}
});
}
function deleteSetting(key) {
if (!confirm(`Einstellung "${key}" löschen?`)) return; const fd = new FormData(); fd.append('config_key', key);
fetch('/api/settings/delete', { method: 'POST', body: fd }).then(res => res.json()).then(data => { if (data.success) loadSettings(); });
}
function deleteUser(id) {
if (!confirm('Benutzer löschen?')) return; const fd = new FormData(); fd.append('id', id);
fetch('/api/users/delete', { method: 'POST', body: fd }).then(res => res.json()).then(data => { if (data.success) loadUsers(); else alert(data.error); });
}
document.getElementById('settingsForm').addEventListener('submit', function(e) {
e.preventDefault(); const fd = new FormData(); fd.append('config_key', document.getElementById('configKey').value); fd.append('config_value', document.getElementById('configValue').value);
fetch('/api/settings', { method: 'POST', body: fd }).then(res => res.json()).then(data => { if (data.success) { document.getElementById('configKey').value = ''; document.getElementById('configValue').value = ''; loadSettings(); } });
});
document.getElementById('userForm').addEventListener('submit', function(e) {
e.preventDefault(); const fd = new FormData(); fd.append('username', document.getElementById('newUsername').value); fd.append('password', document.getElementById('newUserPass').value); fd.append('role', document.getElementById('newUserRole').value);
fetch('/api/users', { method: 'POST', body: fd }).then(res => res.json()).then(data => { if (data.success) { document.getElementById('newUsername').value = ''; document.getElementById('newUserPass').value = ''; loadUsers(); } else alert(data.error); });
});
// --- LIVE SUCHE ---
let searchTimeout;
function searchWiki() {
clearTimeout(searchTimeout);
const query = document.getElementById('searchInput').value;
const resBox = document.getElementById('searchResults');
if (query.length < 2) {
resBox.classList.add('hidden');
return;
}
searchTimeout = setTimeout(() => {
fetch('/api/search?q=' + encodeURIComponent(query))
.then(res => res.json())
.then(data => {
if (!data.success || data.data.length === 0) {
resBox.innerHTML = '<div class="p-3 text-xs text-gray-500 text-center">Nichts gefunden.</div>';
} else {
resBox.innerHTML = data.data.map(art =>
`<div onclick="openArticle(${art.id}); document.getElementById('searchResults').classList.add('hidden'); document.getElementById('searchInput').value='';" class="p-2 text-sm text-gray-300 hover:text-white hover:bg-accent cursor-pointer truncate border-b border-darkborder last:border-0">${art.title}</div>`
).join('');
}
resBox.classList.remove('hidden');
});
}, 300); // 300ms Debounce, um die Datenbank zu schonen
}
</script>
</body>
</html>
+2 -1
View File
@@ -24,7 +24,8 @@ if ($path === '/' or$path === '') {
} elseif ($path === '/api/article' and$method === 'GET') { handleGetArticle();
} elseif ($path === '/api/article' and$method === 'POST') { handleSaveArticle();
} elseif ($path === '/api/article/delete' and$method === 'POST') { handleDeleteArticle();
} elseif ($path === '/api/latest' and$method === 'GET') { handleGetLatestUpdates(); // NEU
} elseif ($path === '/api/latest' and$method === 'GET') { handleGetLatestUpdates();
} elseif ($path === '/api/search' and $method === 'GET') { handleSearch(); // NEU: Such-Endpunkt
} elseif ($path === '/api/history' and$method === 'GET') { handleGetHistory();
} elseif ($path === '/api/upload' and$method === 'POST') { handleImageUpload();
// Image Provider
+23
View File
@@ -99,4 +99,27 @@ function handleImageUpload() {
} else { echo json_encode(['success' => false]); }
exit;
}
// NEU: Suchfunktion für das Frontend
function handleSearch() {
global $pdo;
@session_start();
if (!isset($_SESSION['user_id'])) exit;
$q = trim($_GET['q'] ?? '');
if (strlen($q) < 2) {
echo json_encode(['success' => true, 'data' => []]);
exit;
}
// Wildcards für die LIKE-Abfrage anfügen
$searchTerm = "%$q%";
// Suche im Titel ODER im Content (limitiert auf 10 Ergebnisse)
$stmt = $pdo->prepare("SELECT id, title FROM articles WHERE title LIKE ? OR content_md LIKE ? ORDER BY title ASC LIMIT 10");
$stmt->execute([$searchTerm, $searchTerm]);
echo json_encode(['success' => true, 'data' => $stmt->fetchAll()]);
exit;
}
?>