CoreWiki 1.0.1
This commit is contained in:
1 parent
336cd68acc
commit
21d984a28e
5 files changed
+285
-157
No files matched your search
Regular → Executable
File mode changed.
+1
-1
@@ -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
@@ -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(``, `*(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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
?>
|
||||
Reference in new issue
Block a user