Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>Admin_Control | Duochrome</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { --bg-color: #1A1A1D; --text-color: #f3f2ec; --shadow-color: #f3f2ec; } | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { font-family: "Roboto Mono", monospace; background: var(--bg-color); color: var(--text-color); padding: 40px 20px; } | |
| .container { max-width: 800px; margin: 0 auto; } | |
| .box { border: 1px solid var(--text-color); padding: 40px; box-shadow: 8px 8px 0 var(--shadow-color); margin-top: 10vh; } | |
| label { display: block; margin-top: 20px; font-weight: 700; font-size: 0.8rem; opacity: 0.7; } | |
| input, textarea, select { width: 100%; background: transparent; border: 1px solid var(--text-color); color: var(--text-color); padding: 12px; margin-top: 8px; font-family: inherit; outline: none; } | |
| select option { color: #000; } | |
| .btn { padding: 12px 24px; font-weight: 700; cursor: pointer; border: 1px solid var(--text-color); text-transform: uppercase; font-family: inherit; margin-top: 20px; } | |
| .btn-main { background: var(--text-color); color: var(--bg-color); } | |
| .btn-alt { background: transparent; color: var(--text-color); border-color: var(--text-color); } | |
| .tabs { display: flex; border: 1px solid var(--text-color); margin: 30px 0; overflow-x: auto;} | |
| .tab { flex: 1; padding: 15px; background: none; color: var(--text-color); border: none; border-right: 1px solid var(--text-color); cursor: pointer; text-transform: uppercase; font-weight: 700; white-space: nowrap;} | |
| .tab:last-child { border-right: none; } | |
| .tab.active { background: var(--text-color); color: var(--bg-color); } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div id="loginSection" class="box"> | |
| <h2 style="margin-bottom: 20px;">SYSTEM_AUTH</h2> | |
| <input type="password" id="adminPass" placeholder="ENTER_SECRET_KEY"> | |
| <button class="btn btn-main" onclick="attemptLogin()" style="width: 100%;">AUTHORIZE</button> | |
| </div> | |
| <div id="adminPanel" style="display: none;"> | |
| <header style="display: flex; justify-content: space-between; border-bottom: 1px solid; padding-bottom: 20px;"> | |
| <h1>ADMIN_PANEL</h1> | |
| <button class="btn" onclick="sessionStorage.clear(); location.reload()">EXIT</button> | |
| </header> | |
| <div class="tabs"> | |
| <button class="tab active" id="btn-post" onclick="switchTab('post')">EDITOR</button> | |
| <button class="tab" id="btn-manage" onclick="switchTab('manage')">MANAGE_DATA</button> | |
| <button class="tab" id="btn-link" onclick="switchTab('link')">ADD_LINK</button> | |
| <button class="tab" id="btn-manage-links" onclick="switchTab('manage-links')">MANAGE_LINKS</button> | |
| </div> | |
| <div id="tab-post"> | |
| <label>TITLE_HEADER</label> | |
| <input type="text" id="postTitle" placeholder="Post Title" oninput="autoSlug(this.value)"> | |
| <label>URL_SLUG (Auto-Generated)</label> | |
| <input type="text" id="postSlug" placeholder="post-title"> | |
| <label>EXCERPT (Preview text)</label> | |
| <input type="text" id="postExcerpt" placeholder="A brief summary of this log..."> | |
| <label>TAGS (Comma-separated)</label> | |
| <input type="text" id="postTags" placeholder="e.g., asr, whisper, python, fastapi"> | |
| <label>MARKDOWN_CONTENT</label> | |
| <textarea id="postContent" style="height: 300px;" placeholder="# Hello World..."></textarea> | |
| <div style="display: flex; gap: 10px;"> | |
| <button class="btn btn-main" id="submitBtn" onclick="submitArticle()">EXECUTE_UPLOAD</button> | |
| <button class="btn btn-alt" id="cancelBtn" onclick="cancelEdit()" style="display: none;">CANCEL_EDIT</button> | |
| </div> | |
| </div> | |
| <div id="tab-manage" style="display: none;"> | |
| <div id="articleList"></div> | |
| </div> | |
| <div id="tab-link" style="display: none;"> | |
| <label>RESOURCE_TITLE</label> | |
| <input type="text" id="linkTitle" placeholder="e.g., Evaluating Whisper Medium on FLEURS"> | |
| <label>TARGET_URL</label> | |
| <input type="url" id="linkUrl" placeholder="https://..."> | |
| <label>CATEGORY</label> | |
| <select id="linkCategory"> | |
| <option value="Research Paper">Research Paper</option> | |
| <option value="GitHub Repo">GitHub Repo</option> | |
| <option value="Dataset">Dataset</option> | |
| <option value="Article/Tutorial">Article/Tutorial</option> | |
| </select> | |
| <label>DESCRIPTION (Optional)</label> | |
| <input type="text" id="linkDesc" placeholder="Brief note on why this is saved..."> | |
| <div style="display: flex; gap: 10px;"> | |
| <button class="btn btn-main" id="submitLinkBtn" onclick="submitLink()">SAVE_LINK</button> | |
| <button class="btn btn-alt" id="cancelLinkBtn" onclick="cancelLinkEdit()" style="display: none;">CANCEL_EDIT</button> | |
| </div> | |
| </div> | |
| <div id="tab-manage-links" style="display: none;"> | |
| <div id="linkList"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const API = '/api'; | |
| let globalArticles = []; | |
| let globalLinks = []; | |
| let currentEditId = null; | |
| let currentEditLinkId = null; | |
| function autoSlug(val) { | |
| if (currentEditId) return; | |
| document.getElementById('postSlug').value = val.toLowerCase().trim() | |
| .replace(/[^a-z0-9]+/g, '-') | |
| .replace(/(^-|-$)/g, ''); | |
| } | |
| async function attemptLogin() { | |
| const pass = document.getElementById('adminPass').value; | |
| sessionStorage.setItem('key', pass); | |
| const res = await fetch(`${API}/auth/verify`, { | |
| headers: { 'X-Admin-Password': pass } | |
| }); | |
| if (res.ok) { | |
| document.getElementById('loginSection').style.display = 'none'; | |
| document.getElementById('adminPanel').style.display = 'block'; | |
| loadArticles(); | |
| loadLinks(); | |
| } else { | |
| alert('ACCESS_DENIED: Invalid Key'); | |
| sessionStorage.clear(); | |
| } | |
| } | |
| // --- ARTICLE LOGIC --- | |
| async function submitArticle() { | |
| const data = { | |
| title: document.getElementById('postTitle').value, | |
| slug: document.getElementById('postSlug').value, | |
| excerpt: document.getElementById('postExcerpt').value, | |
| content: document.getElementById('postContent').value, | |
| tags: document.getElementById('postTags').value.split(',').map(t => t.trim()).filter(Boolean) | |
| }; | |
| const url = currentEditId ? `${API}/articles/${currentEditId}` : `${API}/articles`; | |
| const method = currentEditId ? 'PUT' : 'POST'; | |
| const res = await fetch(url, { | |
| method: method, | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'X-Admin-Password': sessionStorage.getItem('key') | |
| }, | |
| body: JSON.stringify(data) | |
| }); | |
| if (res.ok) { | |
| alert(currentEditId ? '✓ UPDATE_SUCCESS' : '✓ UPLOAD_SUCCESS'); | |
| cancelEdit(); | |
| loadArticles(); | |
| switchTab('manage'); | |
| } else { alert('✗ OPERATION_FAILED'); } | |
| } | |
| async function loadArticles() { | |
| const res = await fetch(`${API}/articles?published_only=false`); | |
| globalArticles = await res.json(); | |
| document.getElementById('articleList').innerHTML = globalArticles.map(a => ` | |
| <div style="border: 1px solid; padding: 20px; margin-bottom: 15px; display: flex; justify-content: space-between; align-items: center;"> | |
| <span style="text-transform: uppercase; font-weight: 700;">${a.title}</span> | |
| <div> | |
| <button class="btn btn-main" style="margin: 0; padding: 5px 15px; font-size: 0.7rem; margin-right: 10px;" onclick="editArt(${a.id})">EDIT</button> | |
| <button class="btn" style="margin: 0; padding: 5px 15px; font-size: 0.7rem;" onclick="deleteArt(${a.id})">PURGE</button> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| function editArt(id) { | |
| const article = globalArticles.find(a => a.id === id); | |
| if (!article) return; | |
| currentEditId = id; | |
| document.getElementById('postTitle').value = article.title; | |
| document.getElementById('postSlug').value = article.slug; | |
| document.getElementById('postExcerpt').value = article.excerpt || ''; | |
| document.getElementById('postTags').value = (article.tags || []).join(', '); | |
| document.getElementById('postContent').value = article.content; | |
| document.getElementById('submitBtn').innerText = "UPDATE_DATA"; | |
| document.getElementById('cancelBtn').style.display = "block"; | |
| switchTab('post'); | |
| } | |
| function cancelEdit() { | |
| currentEditId = null; | |
| document.getElementById('postTitle').value = ''; | |
| document.getElementById('postSlug').value = ''; | |
| document.getElementById('postExcerpt').value = ''; | |
| document.getElementById('postTags').value = ''; | |
| document.getElementById('postContent').value = ''; | |
| document.getElementById('submitBtn').innerText = "EXECUTE_UPLOAD"; | |
| document.getElementById('cancelBtn').style.display = "none"; | |
| } | |
| async function deleteArt(id) { | |
| if (!confirm('CONFIRM_DATA_PURGE?')) return; | |
| await fetch(`${API}/articles/${id}`, { | |
| method: 'DELETE', | |
| headers: { 'X-Admin-Password': sessionStorage.getItem('key') } | |
| }); | |
| loadArticles(); | |
| } | |
| // --- LINK LOGIC --- | |
| async function submitLink() { | |
| const data = { | |
| title: document.getElementById('linkTitle').value, | |
| url: document.getElementById('linkUrl').value, | |
| category: document.getElementById('linkCategory').value, | |
| description: document.getElementById('linkDesc').value, | |
| tags: [] | |
| }; | |
| const url = currentEditLinkId ? `${API}/links/${currentEditLinkId}` : `${API}/links`; | |
| const method = currentEditLinkId ? 'PUT' : 'POST'; | |
| const res = await fetch(url, { | |
| method: method, | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'X-Admin-Password': sessionStorage.getItem('key') | |
| }, | |
| body: JSON.stringify(data) | |
| }); | |
| if (res.ok) { | |
| alert(currentEditLinkId ? '✓ LINK_UPDATED' : '✓ LINK_SAVED'); | |
| cancelLinkEdit(); | |
| loadLinks(); | |
| switchTab('manage-links'); | |
| } else { alert('✗ OPERATION_FAILED'); } | |
| } | |
| async function loadLinks() { | |
| const res = await fetch(`${API}/links`); | |
| globalLinks = await res.json(); | |
| document.getElementById('linkList').innerHTML = globalLinks.map(l => ` | |
| <div style="border: 1px solid; padding: 20px; margin-bottom: 15px; display: flex; justify-content: space-between; align-items: center;"> | |
| <div> | |
| <span style="text-transform: uppercase; font-weight: 700; display: block;">${l.title}</span> | |
| <span style="font-size: 0.8rem; opacity: 0.7;">[${l.category}] ${l.url}</span> | |
| </div> | |
| <div> | |
| <button class="btn btn-main" style="margin: 0; padding: 5px 15px; font-size: 0.7rem; margin-right: 10px;" onclick="editLink(${l.id})">EDIT</button> | |
| <button class="btn" style="margin: 0; padding: 5px 15px; font-size: 0.7rem;" onclick="deleteLink(${l.id})">PURGE</button> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| function editLink(id) { | |
| const link = globalLinks.find(l => l.id === id); | |
| if (!link) return; | |
| currentEditLinkId = id; | |
| document.getElementById('linkTitle').value = link.title; | |
| document.getElementById('linkUrl').value = link.url; | |
| document.getElementById('linkCategory').value = link.category; | |
| document.getElementById('linkDesc').value = link.description || ''; | |
| document.getElementById('submitLinkBtn').innerText = "UPDATE_LINK"; | |
| document.getElementById('cancelLinkBtn').style.display = "block"; | |
| switchTab('link'); | |
| } | |
| function cancelLinkEdit() { | |
| currentEditLinkId = null; | |
| document.getElementById('linkTitle').value = ''; | |
| document.getElementById('linkUrl').value = ''; | |
| document.getElementById('linkCategory').value = 'Research Paper'; | |
| document.getElementById('linkDesc').value = ''; | |
| document.getElementById('submitLinkBtn').innerText = "SAVE_LINK"; | |
| document.getElementById('cancelLinkBtn').style.display = "none"; | |
| } | |
| async function deleteLink(id) { | |
| if (!confirm('CONFIRM_LINK_PURGE?')) return; | |
| await fetch(`${API}/links/${id}`, { | |
| method: 'DELETE', | |
| headers: { 'X-Admin-Password': sessionStorage.getItem('key') } | |
| }); | |
| loadLinks(); | |
| } | |
| // --- TAB NAVIGATION --- | |
| function switchTab(t) { | |
| document.getElementById('tab-post').style.display = t === 'post' ? 'block' : 'none'; | |
| document.getElementById('tab-manage').style.display = t === 'manage' ? 'block' : 'none'; | |
| document.getElementById('tab-link').style.display = t === 'link' ? 'block' : 'none'; | |
| document.getElementById('tab-manage-links').style.display = t === 'manage-links' ? 'block' : 'none'; | |
| document.getElementById('btn-post').classList.toggle('active', t === 'post'); | |
| document.getElementById('btn-manage').classList.toggle('active', t === 'manage'); | |
| document.getElementById('btn-link').classList.toggle('active', t === 'link'); | |
| document.getElementById('btn-manage-links').classList.toggle('active', t === 'manage-links'); | |
| } | |
| if(sessionStorage.getItem('key')) { | |
| document.getElementById('adminPass').value = sessionStorage.getItem('key'); | |
| attemptLogin(); | |
| } | |
| </script> | |
| </body> | |
| </html> |