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: .
|
build: .
|
||||||
container_name: corewiki_app
|
container_name: corewiki_app
|
||||||
ports:
|
ports:
|
||||||
- "8443:443"
|
- "8444:443"
|
||||||
volumes:
|
volumes:
|
||||||
- ./src:/var/www/html
|
- ./src:/var/www/html
|
||||||
- ./data:/var/www/data
|
- ./data:/var/www/data
|
||||||
|
|||||||
+259
-155
@@ -15,23 +15,29 @@
|
|||||||
tailwind.config = { theme: { extend: { colors: { darkbg: '#0f172a', darkcard: '#1e293b', darkborder: '#334155', accent: '#3b82f6' } } } }
|
tailwind.config = { theme: { extend: { colors: { darkbg: '#0f172a', darkcard: '#1e293b', darkborder: '#334155', accent: '#3b82f6' } } } }
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<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 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 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 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; }
|
.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; }
|
.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;}
|
.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 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;}
|
.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; }
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-darkbg text-gray-300 font-sans h-screen flex flex-col overflow-hidden">
|
<body class="bg-darkbg text-gray-300 font-sans h-screen flex flex-col overflow-hidden">
|
||||||
|
|
||||||
<!-- LOGIN SCREEN -->
|
<!-- LOGIN SCREEN -->
|
||||||
<div id="loginView" class="flex-1 flex items-center justify-center hidden">
|
<div id="loginView" class="flex-1 flex items-center justify-center hidden p-4">
|
||||||
<div class="bg-darkcard border border-darkborder rounded-xl p-8 shadow-2xl w-full max-w-sm">
|
<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>
|
<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>
|
<div id="loginError" class="text-red-500 text-sm text-center mb-4 hidden">Login fehlgeschlagen.</div>
|
||||||
<form id="loginForm" class="space-y-4">
|
<form id="loginForm" class="space-y-4">
|
||||||
@@ -44,77 +50,101 @@
|
|||||||
|
|
||||||
<!-- MAIN APP -->
|
<!-- MAIN APP -->
|
||||||
<div id="appView" class="flex-1 flex flex-col h-full hidden">
|
<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">
|
<!-- HEADER (Responsive) -->
|
||||||
<div class="flex items-center gap-3 w-64 cursor-pointer" onclick="openArticle(1)">
|
<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">
|
||||||
<div class="bg-accent p-1.5 rounded-lg text-white"><span class="text-lg">📚</span></div>
|
<!-- Mobile Menu Toggle -->
|
||||||
<span class="text-xl font-bold text-white tracking-wider">CoreWiki</span>
|
<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>
|
||||||
|
|
||||||
<div class="flex items-center gap-6 justify-end">
|
<div class="flex items-center gap-2 md:gap-6 justify-end flex-1">
|
||||||
<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>
|
<!-- 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>
|
</button>
|
||||||
<div class="flex items-center gap-3 border-l border-darkborder pl-6">
|
<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-2" title="Passwort ändern">🔑</button>
|
<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-2 hidden" title="System Settings">⚙️</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-2 border-l border-darkborder pl-4">
|
<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-xs text-gray-500">Angemeldet</p>
|
||||||
<p class="text-sm text-white font-bold" id="userNameDisplay"></p>
|
<p class="text-sm text-white font-bold" id="userNameDisplay"></p>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- 3-SPALTEN LAYOUT -->
|
<!-- 3-SPALTEN LAYOUT (Mobile Optimized) -->
|
||||||
<div class="flex-1 flex overflow-hidden">
|
<div class="flex-1 flex overflow-hidden relative">
|
||||||
<!-- LINKE SPALTE: Katalog -->
|
|
||||||
<aside class="w-72 bg-darkcard border-r border-darkborder flex flex-col shrink-0">
|
<!-- Overlay für mobiles Menü -->
|
||||||
<div class="p-5 flex-1 overflow-y-auto">
|
<div id="mobileMenuOverlay" onclick="toggleMobileMenu()" class="fixed inset-0 bg-black/60 z-30 hidden md:hidden backdrop-blur-sm"></div>
|
||||||
<h3 class="text-xs font-bold text-gray-500 uppercase tracking-wider mb-4">Bibliotheken</h3>
|
|
||||||
|
<!-- 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>
|
<nav class="space-y-4" id="categoryNav"></nav>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- MITTLERE SPALTE: Content & Editor -->
|
<!-- MITTLERE SPALTE: Content & Editor -->
|
||||||
<main class="flex-1 bg-darkbg overflow-y-auto relative 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-10 flex-1 flex flex-col">
|
<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 id="readView" class="flex-1 flex flex-col">
|
||||||
<div class="border-b border-darkborder pb-4 mb-6 flex justify-between items-start">
|
<div class="border-b border-darkborder pb-4 mb-4 md:mb-6 flex flex-wrap justify-between items-start gap-4">
|
||||||
<div>
|
<div class="min-w-0 flex-1">
|
||||||
<div class="flex items-center gap-2 text-xs text-accent font-bold mb-2"><span id="readCategory"></span></div>
|
<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-3xl font-bold text-white mb-1" id="readTitle">Lade...</h1>
|
<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>
|
<p class="text-xs text-gray-500">Von <span id="readAuthor"></span> am <span id="readDate"></span></p>
|
||||||
</div>
|
</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="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>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
|
<!-- EDITOR ANSICHT -->
|
||||||
<div id="editView" class="flex-1 flex flex-col hidden">
|
<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="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="flex-1 flex gap-4 mr-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-48"></select>
|
<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="flex-1 bg-darkcard border border-darkborder rounded p-2 text-white font-bold outline-none focus:border-accent" placeholder="Artikel Titel...">
|
<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>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2 w-full md:w-auto justify-end">
|
||||||
<button onclick="toggleEditMode()" class="text-gray-400 hover:text-white px-4 py-1.5 rounded text-sm transition-colors">Abbrechen</button>
|
<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-1.5 rounded text-sm font-bold transition-colors shadow-lg shadow-accent/20">💾 Speichern</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>
|
</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">
|
<div class="bg-blue-500/10 border border-accent/30 text-accent text-xs p-3 rounded mb-4">
|
||||||
<span>Markdown & HTML unterstützt. <b>Bilder per Drag & Drop!</b> Netzpläne via ```mermaid</span>
|
Markdown & HTML unterstützt. <b>Bilder per Drag & Drop!</b> Netzpläne via ```mermaid
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- RECHTE SPALTE: Historie & Updates -->
|
<!-- RECHTE SPALTE: Historie & Updates (Nur Desktop/Tablet-Landscape) -->
|
||||||
<aside class="w-64 bg-darkcard border-l border-darkborder p-5 flex flex-col shrink-0">
|
<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>
|
<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>
|
<div id="latestNav" class="space-y-1 overflow-y-auto mb-8 pb-4 border-b border-darkborder"></div>
|
||||||
|
|
||||||
@@ -125,84 +155,86 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CORETEMPLATE MODALS -->
|
<!-- CORETEMPLATE MODALS -->
|
||||||
<div id="pwdModal" class="fixed inset-0 bg-black/80 hidden z-50 flex items-center justify-center backdrop-blur-sm">
|
<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-6 shadow-2xl w-full max-w-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">✕</button></div>
|
<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>
|
<div id="pwMsg" class="text-xs mb-3 text-green-400 hidden"></div>
|
||||||
<form id="passwordForm" class="space-y-4">
|
<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="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="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">
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="adminModal" class="fixed inset-0 bg-black/80 hidden z-50 flex items-center justify-center backdrop-blur-sm">
|
<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 h-[85vh]">
|
<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-5 border-b border-darkborder flex justify-between items-center bg-darkbg rounded-t-xl">
|
<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-xl font-bold text-white">System Administration</h2>
|
<h2 class="text-lg md:text-xl font-bold text-white whitespace-nowrap mr-4">System Administration</h2>
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-2 md:gap-4 shrink-0">
|
||||||
<button onclick="switchAdminTab('categories')" id="btn-admin-categories" class="text-accent font-bold">Kategorien</button>
|
<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">Benutzer</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">Settings</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">Audit Log</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-4 border-l border-darkborder pl-4">✕ Schließen</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>
|
</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 -->
|
<!-- CATEGORIES TAB -->
|
||||||
<div id="admin-tab-categories" class="space-y-6">
|
<div id="admin-tab-categories" class="space-y-6">
|
||||||
<div class="bg-darkcard p-4 rounded border border-darkborder">
|
<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">
|
<input type="hidden" id="catId" value="0">
|
||||||
<div class="w-32">
|
<div class="w-full md:w-32">
|
||||||
<label class="block text-xs text-gray-500 mb-1">Icon</label>
|
<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">
|
<select id="catIcon" required class="w-full bg-darkbg border border-darkborder rounded p-2 text-white">
|
||||||
<option value="📁">📁 Ordner</option>
|
<option value="📁">📁 Ordner</option>
|
||||||
<option value="💻">💻 Code</option>
|
<option value="💻">💻 Code</option>
|
||||||
<option value="🖥️">🖥️ Server</option>
|
<option value="🖥️">🖥️ Server</option>
|
||||||
<option value="🌐">🌐 Netzwerk</option>
|
<option value="🌐">🌐 Netzwerk</option>
|
||||||
<option value="📝">📝 Notizen</option>
|
<option value="📝">📝 Notizen</option>
|
||||||
<option value="⚙️">⚙️ Configs</option>
|
<option value="⚙️">⚙️ Configs</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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="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="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="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>
|
||||||
<button type="submit" class="bg-accent text-white font-bold py-2 px-6 rounded">Speichern</button>
|
<div class="flex gap-2 w-full md:w-auto">
|
||||||
<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>
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="adminCatList" class="space-y-1"></div>
|
<div id="adminCatList" class="space-y-2"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- USERS TAB -->
|
<!-- USERS TAB -->
|
||||||
<div id="admin-tab-users" class="hidden space-y-6">
|
<div id="admin-tab-users" class="hidden space-y-6">
|
||||||
<div class="bg-darkcard p-4 rounded border border-darkborder">
|
<div class="bg-darkcard p-4 rounded border border-darkborder">
|
||||||
<form id="userForm" class="flex gap-4 items-end">
|
<form id="userForm" class="flex flex-col md:flex-row gap-4 items-start md: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="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="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 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>
|
<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="bg-accent text-white font-bold py-2 px-6 rounded">Anlegen</button>
|
<button type="submit" class="w-full md:w-auto bg-accent text-white font-bold py-2 px-6 rounded">Anlegen</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- SETTINGS TAB -->
|
<!-- SETTINGS TAB -->
|
||||||
<div id="admin-tab-settings" class="hidden space-y-6">
|
<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">
|
<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="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">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>
|
<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="bg-accent text-white font-bold py-2 px-6 rounded">Speichern</button>
|
<button type="submit" class="w-full md:w-auto bg-accent text-white font-bold py-2 px-6 rounded">Speichern</button>
|
||||||
</form>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- LOGS TAB -->
|
<!-- LOGS TAB -->
|
||||||
<div id="admin-tab-logs" class="hidden">
|
<div id="admin-tab-logs" class="hidden overflow-x-auto">
|
||||||
<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>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -214,17 +246,25 @@
|
|||||||
document.addEventListener('DOMContentLoaded', checkSession);
|
document.addEventListener('DOMContentLoaded', checkSession);
|
||||||
|
|
||||||
function checkSession() {
|
function checkSession() {
|
||||||
fetch('/api/me').then(res=>res.json()).then(data => {
|
fetch('/api/me')
|
||||||
if (data.success) {
|
.then(res => res.text()) // Antwort erst als rohen Text lesen
|
||||||
document.getElementById('loginView').classList.add('hidden');
|
.then(text => {
|
||||||
document.getElementById('appView').classList.remove('hidden');
|
try {
|
||||||
document.getElementById('userNameDisplay').innerText = data.username;
|
const data = JSON.parse(text); // Versuchen, es in JSON umzuwandeln
|
||||||
isAdmin = (data.role === 'admin');
|
if (data.success) {
|
||||||
if (isAdmin) document.getElementById('navAdmin').classList.remove('hidden');
|
document.getElementById('loginView').classList.add('hidden');
|
||||||
loadCategories(); loadLatest(); loadHistory(); openArticle(1);
|
document.getElementById('appView').classList.remove('hidden');
|
||||||
} else {
|
document.getElementById('userNameDisplay').innerText = data.username;
|
||||||
document.getElementById('appView').classList.add('hidden');
|
isAdmin = (data.role === 'admin');
|
||||||
document.getElementById('loginView').classList.remove('hidden');
|
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(); }); }
|
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() {
|
function loadCategories() {
|
||||||
fetch('/api/categories').then(res=>res.json()).then(data => {
|
fetch('/api/categories').then(res=>res.json()).then(data => {
|
||||||
if(!data.success) return;
|
if(!data.success) return;
|
||||||
@@ -250,48 +299,37 @@
|
|||||||
if(adminList) adminList.innerHTML = ''; if(adminSel) adminSel.innerHTML = '<option value="">-- Keine (Hauptordner) --</option>';
|
if(adminList) adminList.innerHTML = ''; if(adminSel) adminSel.innerHTML = '<option value="">-- Keine (Hauptordner) --</option>';
|
||||||
|
|
||||||
let allCats = data.data;
|
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 => {
|
allCats.forEach(c => {
|
||||||
let safeName = (c.name || 'Unbenannt').replace(/'/g, "\\'");
|
sel.innerHTML += `<option value="${c.id}">${c.icon} ${c.name}</option>`;
|
||||||
let safeIcon = c.icon || '📁';
|
if(adminSel) adminSel.innerHTML += `<option value="${c.id}">${c.icon} ${c.name}</option>`;
|
||||||
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>`;
|
|
||||||
|
|
||||||
if(adminList) {
|
if(adminList) {
|
||||||
let parentLabel = c.parent_id ? ' <span class="text-xs text-gray-500 ml-2">(Unterkategorie)</span>' : '';
|
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); });
|
mainCats.forEach(cat => { nav.innerHTML += buildCategoryTree(cat, allCats); });
|
||||||
}).catch(err => console.error(err));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCategoryTree(cat, allCats) {
|
function buildCategoryTree(cat, allCats) {
|
||||||
let children = allCats.filter(c => c.parent_id == cat.id);
|
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>${cat.icon}</span> <span class="truncate">${cat.name}</span></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>${safeIcon}</span> ${safeName}</div><ul class="space-y-1 border-l border-darkborder ml-3 pl-3">`;
|
|
||||||
|
|
||||||
let arts = cat.articles || [];
|
if(cat.articles.length === 0 && children.length === 0) html += `<li class="text-xs text-gray-500 italic py-1">Leer</li>`;
|
||||||
if(arts.length === 0 && children.length === 0) html += `<li class="text-xs text-gray-500 italic">Leer</li>`;
|
|
||||||
|
|
||||||
arts.forEach(art => {
|
cat.articles.forEach(art => {
|
||||||
let activeCls = (art.id == currentArticleId) ? 'text-accent font-bold' : 'text-gray-400 hover:text-white';
|
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>`;
|
return html + `</ul></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,7 +337,6 @@
|
|||||||
fetch('/api/latest').then(res=>res.json()).then(data => {
|
fetch('/api/latest').then(res=>res.json()).then(data => {
|
||||||
if(!data.success) return;
|
if(!data.success) return;
|
||||||
const nav = document.getElementById('latestNav'); nav.innerHTML = '';
|
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>`; });
|
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 => {
|
fetch('/api/history').then(res=>res.json()).then(data => {
|
||||||
if(!data.success) return;
|
if(!data.success) return;
|
||||||
const nav = document.getElementById('historyNav'); nav.innerHTML = '';
|
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>`; });
|
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) {
|
function openArticle(id) {
|
||||||
fetch('/api/article?id=' + id).then(res=>res.json()).then(data => {
|
fetch('/api/article?id=' + id).then(res=>res.json()).then(data => {
|
||||||
// FALLBACK WENN DB KOMPLETT LEER IST
|
if(!data.success) return;
|
||||||
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');
|
|
||||||
currentArticleId = id; const art = data.data;
|
currentArticleId = id; const art = data.data;
|
||||||
document.getElementById('readView').classList.remove('hidden'); document.getElementById('editView').classList.add('hidden');
|
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;
|
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 (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);
|
if(window.mermaid) setTimeout(() => { window.mermaid.run({ querySelector: '.mermaid' }); }, 100);
|
||||||
loadCategories(); loadHistory();
|
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'); });
|
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) {
|
document.getElementById('catForm').addEventListener('submit', function(e) {
|
||||||
e.preventDefault(); const fd = new FormData();
|
e.preventDefault(); const fd = new FormData();
|
||||||
fd.append('id', document.getElementById('catId').value); fd.append('name', document.getElementById('catName').value);
|
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 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) {
|
function deleteCategory(id) {
|
||||||
if(!confirm('Kategorie löschen? (Artikel werden in Hauptkategorie verschoben)')) return;
|
if(!confirm('Kategorie löschen? (Artikel werden in Hauptkategorie verschoben)')) return;
|
||||||
const fd = new FormData(); fd.append('id', id);
|
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(); });
|
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');
|
const editor = document.getElementById('editContent');
|
||||||
editor.addEventListener('dragover', (e) => { e.preventDefault(); editor.classList.add('border-accent'); });
|
editor.addEventListener('dragover', (e) => { e.preventDefault(); editor.classList.add('border-accent'); });
|
||||||
editor.addEventListener('dragleave', (e) => { e.preventDefault(); editor.classList.remove('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)*`);
|
else editor.value = editor.value.replace(``, `*(Upload fehlgeschlagen)*`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- CORETEMPLATE ADMIN LOGIK ---
|
||||||
function openModal(id) {
|
function openModal(id) {
|
||||||
document.getElementById(id).classList.remove('hidden');
|
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 closeModal(id) { document.getElementById(id).classList.add('hidden'); }
|
||||||
|
|
||||||
function switchAdminTab(tab) {
|
function switchAdminTab(tab) {
|
||||||
['categories', 'users', 'settings', 'logs'].forEach(t => {
|
['categories', 'users', 'settings', 'logs'].forEach(t => {
|
||||||
document.getElementById('admin-tab-'+t).classList.add('hidden');
|
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('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) {
|
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 loadAuditLogs() {
|
||||||
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>`; }); } }); }
|
fetch('/api/logs').then(res => res.json()).then(data => {
|
||||||
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>`; }); } }); }
|
if (data.success) {
|
||||||
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(); }); }
|
const tbody = document.getElementById('logTableBody'); tbody.innerHTML = '';
|
||||||
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); }); }
|
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>`; });
|
||||||
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 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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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 === 'GET') { handleGetArticle();
|
||||||
} elseif ($path === '/api/article' and$method === 'POST') { handleSaveArticle();
|
} elseif ($path === '/api/article' and$method === 'POST') { handleSaveArticle();
|
||||||
} elseif ($path === '/api/article/delete' and$method === 'POST') { handleDeleteArticle();
|
} 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/history' and$method === 'GET') { handleGetHistory();
|
||||||
} elseif ($path === '/api/upload' and$method === 'POST') { handleImageUpload();
|
} elseif ($path === '/api/upload' and$method === 'POST') { handleImageUpload();
|
||||||
// Image Provider
|
// Image Provider
|
||||||
|
|||||||
@@ -99,4 +99,27 @@ function handleImageUpload() {
|
|||||||
} else { echo json_encode(['success' => false]); }
|
} else { echo json_encode(['success' => false]); }
|
||||||
exit;
|
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