| {% extends "shared/base.html" %} |
|
|
| {% block title %}Announcements - AniCove Admin{% endblock %} |
| {% block meta_description %}Manage site announcements for AniCove.{% endblock %} |
|
|
| {% block extra_css %} |
| <style> |
| :root { |
| --aa-bg: #080808; |
| --aa-surface: #111; |
| --aa-border: #1e1e1e; |
| --aa-muted: #777; |
| --aa-accent: #3b82f6; |
| --aa-success: #22c55e; |
| --aa-warning: #f59e0b; |
| --aa-error: #ef4444; |
| } |
| |
| .aa-page { |
| max-width: 1000px; |
| margin: 0 auto; |
| padding: 32px 20px; |
| } |
| |
| .aa-header { |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| margin-bottom: 24px; |
| flex-wrap: wrap; |
| gap: 12px; |
| } |
| |
| .aa-header h1 { |
| font-size: 1.4rem; |
| font-weight: 700; |
| margin: 0; |
| color: #fff; |
| display: flex; |
| align-items: center; |
| gap: 10px; |
| } |
| |
| .aa-btn { |
| display: inline-flex; |
| align-items: center; |
| gap: 6px; |
| padding: 8px 16px; |
| border-radius: 8px; |
| font-size: 0.82rem; |
| font-weight: 600; |
| font-family: var(--font-sans); |
| cursor: pointer; |
| border: 1px solid var(--aa-border); |
| background: var(--aa-surface); |
| color: #ddd; |
| transition: all 0.2s; |
| text-decoration: none; |
| } |
| |
| .aa-btn:hover { |
| border-color: rgba(255,255,255,0.1); |
| color: #fff; |
| } |
| |
| .aa-btn-primary { |
| background: #3b82f6; |
| border-color: #3b82f6; |
| color: #fff; |
| } |
| |
| .aa-btn-primary:hover { |
| background: #2563eb; |
| border-color: #2563eb; |
| } |
| |
| .aa-btn-danger { |
| color: var(--aa-error); |
| } |
| |
| .aa-btn-danger:hover { |
| background: rgba(239,68,68,0.1); |
| border-color: rgba(239,68,68,0.3); |
| } |
| |
| |
| .aa-list { |
| display: flex; |
| flex-direction: column; |
| gap: 10px; |
| } |
| |
| .aa-card { |
| background: var(--aa-surface); |
| border: 1px solid var(--aa-border); |
| border-radius: 10px; |
| padding: 16px 18px; |
| display: flex; |
| align-items: flex-start; |
| gap: 14px; |
| transition: border-color 0.2s; |
| } |
| |
| .aa-card:hover { |
| border-color: rgba(255,255,255,0.06); |
| } |
| |
| .aa-card-inactive { |
| opacity: 0.5; |
| } |
| |
| .aa-card-inactive:hover { |
| opacity: 0.7; |
| } |
| |
| .aa-type-badge { |
| flex-shrink: 0; |
| width: 36px; |
| height: 36px; |
| border-radius: 8px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-size: 1rem; |
| } |
| |
| .aa-type-badge.info { background: rgba(59,130,246,0.12); color: var(--aa-accent); } |
| .aa-type-badge.warning { background: rgba(245,158,11,0.12); color: var(--aa-warning); } |
| .aa-type-badge.important { background: rgba(239,68,68,0.12); color: var(--aa-error); } |
| .aa-type-badge.maintenance { background: rgba(168,85,247,0.12); color: #a855f7; } |
| |
| .aa-card-body { |
| flex: 1; |
| min-width: 0; |
| } |
| |
| .aa-card-title { |
| font-size: 0.9rem; |
| font-weight: 600; |
| color: #eee; |
| margin-bottom: 3px; |
| } |
| |
| .aa-card-message { |
| font-size: 0.8rem; |
| color: var(--aa-muted); |
| line-height: 1.5; |
| display: -webkit-box; |
| -webkit-line-clamp: 2; |
| -webkit-box-orient: vertical; |
| overflow: hidden; |
| } |
| |
| .aa-card-meta { |
| font-size: 0.68rem; |
| color: #555; |
| margin-top: 6px; |
| display: flex; |
| align-items: center; |
| gap: 10px; |
| } |
| |
| .aa-card-actions { |
| display: flex; |
| align-items: center; |
| gap: 6px; |
| flex-shrink: 0; |
| } |
| |
| .aa-toggle-btn { |
| width: 40px; |
| height: 22px; |
| border-radius: 11px; |
| border: none; |
| cursor: pointer; |
| position: relative; |
| transition: background 0.2s; |
| background: #333; |
| flex-shrink: 0; |
| } |
| |
| .aa-toggle-btn.active { |
| background: var(--aa-success); |
| } |
| |
| .aa-toggle-btn::after { |
| content: ''; |
| position: absolute; |
| width: 18px; |
| height: 18px; |
| border-radius: 50%; |
| background: #fff; |
| top: 2px; |
| left: 2px; |
| transition: transform 0.2s; |
| } |
| |
| .aa-toggle-btn.active::after { |
| transform: translateX(18px); |
| } |
| |
| .aa-empty { |
| padding: 40px; |
| text-align: center; |
| color: #555; |
| } |
| |
| .aa-loading { |
| padding: 40px; |
| text-align: center; |
| color: var(--aa-muted); |
| } |
| |
| .aa-spinner { |
| display: inline-block; |
| width: 22px; |
| height: 22px; |
| border: 2px solid rgba(255,255,255,0.1); |
| border-top-color: var(--aa-accent); |
| border-radius: 50%; |
| animation: aaSpin 0.6s linear infinite; |
| } |
| |
| @keyframes aaSpin { |
| to { transform: rotate(360deg); } |
| } |
| |
| |
| .aa-modal-overlay { |
| position: fixed; |
| inset: 0; |
| z-index: 10000; |
| background: rgba(0,0,0,0.6); |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| padding: 20px; |
| } |
| |
| .aa-modal { |
| background: #111; |
| border: 1px solid var(--aa-border); |
| border-radius: 14px; |
| padding: 24px; |
| width: 100%; |
| max-width: 500px; |
| max-height: 90vh; |
| overflow-y: auto; |
| } |
| |
| .aa-modal h2 { |
| font-size: 1.1rem; |
| font-weight: 600; |
| margin: 0 0 18px 0; |
| color: #fff; |
| } |
| |
| .aa-form-group { |
| margin-bottom: 14px; |
| } |
| |
| .aa-form-label { |
| display: block; |
| font-size: 0.78rem; |
| font-weight: 600; |
| color: #999; |
| margin-bottom: 5px; |
| } |
| |
| .aa-form-input, |
| .aa-form-textarea, |
| .aa-form-select { |
| width: 100%; |
| padding: 9px 12px; |
| background: #1a1a1a; |
| border: 1px solid #222; |
| border-radius: 8px; |
| color: #eee; |
| font-size: 0.85rem; |
| font-family: var(--font-sans); |
| outline: none; |
| transition: border-color 0.2s; |
| box-sizing: border-box; |
| } |
| |
| .aa-form-textarea { |
| min-height: 80px; |
| resize: vertical; |
| } |
| |
| .aa-form-input:focus, |
| .aa-form-textarea:focus, |
| .aa-form-select:focus { |
| border-color: var(--aa-accent); |
| } |
| |
| .aa-form-row { |
| display: flex; |
| gap: 12px; |
| } |
| |
| .aa-form-row .aa-form-group { |
| flex: 1; |
| } |
| |
| .aa-modal-actions { |
| display: flex; |
| gap: 8px; |
| justify-content: flex-end; |
| margin-top: 18px; |
| } |
| |
| .aa-toast { |
| position: fixed; |
| bottom: 24px; |
| right: 24px; |
| background: #111; |
| border: 1px solid var(--aa-border); |
| border-radius: 10px; |
| padding: 12px 20px; |
| color: #bbb; |
| font-size: 0.85rem; |
| box-shadow: 0 10px 30px rgba(0,0,0,0.5); |
| z-index: 99999; |
| transform: translateY(20px); |
| opacity: 0; |
| transition: all 0.3s; |
| pointer-events: none; |
| } |
| |
| .aa-toast.visible { |
| transform: translateY(0); |
| opacity: 1; |
| } |
| |
| @media (max-width: 600px) { |
| .aa-card { |
| flex-direction: column; |
| } |
| .aa-card-actions { |
| align-self: flex-end; |
| } |
| } |
| </style> |
| {% endblock %} |
|
|
| {% block content %} <div class="aa-page"> |
| <a href="/admin" style="display:inline-flex;align-items:center;gap:5px;font-size:0.78rem;color:#555;text-decoration:none;margin-bottom:14px;transition:color 0.2s;" onmouseover="this.style.color='#fff'" onmouseout="this.style.color='#555'"> |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg> |
| Back to Dashboard |
| </a> |
| <div class="aa-header"> |
| <h1> |
| <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color: var(--aa-warning);"> |
| <polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon> |
| </svg> |
| Announcements |
| </h1> |
| <button class="aa-btn aa-btn-primary" onclick="openCreateModal()"> |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <line x1="12" y1="5" x2="12" y2="19"></line> |
| <line x1="5" y1="12" x2="19" y2="12"></line> |
| </svg> |
| New Announcement |
| </button> |
| </div> |
|
|
| <div class="aa-list" id="aa-list"> |
| <div class="aa-loading"><div class="aa-spinner"></div></div> |
| </div> |
| </div> |
|
|
| |
| <div id="aa-modal-overlay" class="aa-modal-overlay" style="display: none;"> |
| <div class="aa-modal"> |
| <h2 id="aa-modal-title">New Announcement</h2> |
| <form id="aa-form" onsubmit="handleSave(event)"> |
| <input type="hidden" id="aa-edit-id" value=""> |
| <div class="aa-form-group"> |
| <label class="aa-form-label">Title</label> |
| <input class="aa-form-input" id="aa-title" required placeholder="e.g. Site Maintenance Tonight" maxlength="200"> |
| </div> |
| <div class="aa-form-group"> |
| <label class="aa-form-label">Message</label> |
| <textarea class="aa-form-textarea" id="aa-message" required placeholder="Detailed message for users..." maxlength="2000"></textarea> |
| </div> |
| <div class="aa-form-row"> |
| <div class="aa-form-group"> |
| <label class="aa-form-label">Type</label> |
| <select class="aa-form-select" id="aa-type"> |
| <option value="info">Info</option> |
| <option value="warning">Warning</option> |
| <option value="important">Important</option> |
| <option value="maintenance">Maintenance</option> |
| </select> |
| </div> |
| <div class="aa-form-group"> |
| <label class="aa-form-label">Priority</label> |
| <input class="aa-form-input" id="aa-priority" type="number" value="0" min="0" max="100"> |
| </div> |
| </div> |
| <div class="aa-form-group" style="display: flex; align-items: center; gap: 8px;"> |
| <input type="checkbox" id="aa-active" style="width: 16px; height: 16px; accent-color: #22c55e;"> |
| <label class="aa-form-label" for="aa-active" style="margin: 0; cursor: pointer;">Active (visible to users)</label> |
| </div> |
| <div class="aa-modal-actions"> |
| <button type="button" class="aa-btn" onclick="closeModal()">Cancel</button> |
| <button type="submit" class="aa-btn aa-btn-primary" id="aa-save-btn">Save</button> |
| </div> |
| </form> |
| </div> |
| </div> |
|
|
| |
| <div class="aa-toast" id="aa-toast"></div> |
| {% endblock %} |
|
|
| {% block extra_js %} |
| <script> |
| (function() { |
| const toastEl = document.getElementById('aa-toast'); |
| let editingId = null; |
| |
| function showToast(msg) { |
| toastEl.textContent = msg; |
| toastEl.classList.add('visible'); |
| setTimeout(() => toastEl.classList.remove('visible'), 3000); |
| } |
| |
| async function loadAnnouncements() { |
| const list = document.getElementById('aa-list'); |
| try { |
| const res = await fetch('/api/announcements'); |
| const data = await res.json(); |
| if (!data.success) { |
| list.innerHTML = '<div class="aa-empty">Failed to load announcements.</div>'; |
| return; |
| } |
| |
| if (data.announcements.length === 0) { |
| list.innerHTML = '<div class="aa-empty">No announcements yet. Create one to get started!</div>'; |
| return; |
| } |
| |
| list.innerHTML = data.announcements.map(a => { |
| const typeIcons = { |
| info: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>', |
| warning: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>', |
| important: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>', |
| maintenance: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>', |
| }; |
| |
| const activeClass = a.active ? '' : 'aa-card-inactive'; |
| const date = new Date(a.created_at).toLocaleDateString(); |
| |
| return ` |
| <div class="aa-card ${activeClass}"> |
| <div class="aa-type-badge ${a.type}">${typeIcons[a.type] || typeIcons.info}</div> |
| <div class="aa-card-body"> |
| <div class="aa-card-title">${escapeHtml(a.title)}</div> |
| <div class="aa-card-message">${escapeHtml(a.message)}</div> |
| <div class="aa-card-meta"> |
| <span>${date}</span> |
| <span>Priority: ${a.priority}</span> |
| <span>${a.active ? '<span style="color:var(--aa-success)">● Active</span>' : '<span style="color:#555">○ Inactive</span>'}</span> |
| </div> |
| </div> |
| <div class="aa-card-actions"> |
| <button class="aa-toggle-btn ${a.active ? 'active' : ''}" onclick="toggleActive('${a._id}', ${!a.active})" title="Toggle active"></button> |
| <button class="aa-btn" onclick="openEditModal('${a._id}')" title="Edit"> |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg> |
| </button> |
| <button class="aa-btn aa-btn-danger" onclick="deleteAnnouncement('${a._id}')" title="Delete"> |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg> |
| </button> |
| </div> |
| </div> |
| `; |
| }).join(''); |
| } catch (e) { |
| list.innerHTML = '<div class="aa-empty">Network error loading announcements.</div>'; |
| } |
| } |
| |
| function escapeHtml(text) { |
| const d = document.createElement('div'); |
| d.textContent = text; |
| return d.innerHTML; |
| } |
| |
| window.openCreateModal = function() { |
| editingId = null; |
| document.getElementById('aa-modal-title').textContent = 'New Announcement'; |
| document.getElementById('aa-edit-id').value = ''; |
| document.getElementById('aa-title').value = ''; |
| document.getElementById('aa-message').value = ''; |
| document.getElementById('aa-type').value = 'info'; |
| document.getElementById('aa-priority').value = '0'; |
| document.getElementById('aa-active').checked = true; |
| document.getElementById('aa-save-btn').textContent = 'Create'; |
| document.getElementById('aa-modal-overlay').style.display = 'flex'; |
| }; |
| |
| window.openEditModal = async function(id) { |
| editingId = id; |
| document.getElementById('aa-modal-title').textContent = 'Edit Announcement'; |
| document.getElementById('aa-edit-id').value = id; |
| document.getElementById('aa-save-btn').textContent = 'Save Changes'; |
| document.getElementById('aa-modal-overlay').style.display = 'flex'; |
| |
| try { |
| const res = await fetch('/api/announcements?page=1'); |
| const data = await res.json(); |
| const ann = data.announcements.find(a => a._id === id); |
| if (ann) { |
| document.getElementById('aa-title').value = ann.title; |
| document.getElementById('aa-message').value = ann.message; |
| document.getElementById('aa-type').value = ann.type; |
| document.getElementById('aa-priority').value = ann.priority; |
| document.getElementById('aa-active').checked = ann.active; |
| } |
| } catch (e) {} |
| }; |
| |
| window.closeModal = function() { |
| document.getElementById('aa-modal-overlay').style.display = 'none'; |
| editingId = null; |
| }; |
| |
| window.handleSave = async function(e) { |
| e.preventDefault(); |
| const id = document.getElementById('aa-edit-id').value; |
| const isEdit = id && id.length > 0; |
| |
| const payload = { |
| title: document.getElementById('aa-title').value.trim(), |
| message: document.getElementById('aa-message').value.trim(), |
| type: document.getElementById('aa-type').value, |
| priority: parseInt(document.getElementById('aa-priority').value) || 0, |
| active: document.getElementById('aa-active').checked, |
| }; |
| |
| if (!payload.title || !payload.message) { |
| showToast('Title and message are required.'); |
| return; |
| } |
| |
| try { |
| let res; |
| if (isEdit) { |
| res = await fetch('/api/announcements/' + id, { |
| method: 'PATCH', |
| headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify(payload), |
| }); |
| } else { |
| res = await fetch('/api/announcements', { |
| method: 'POST', |
| headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify(payload), |
| }); |
| } |
| const data = await res.json(); |
| if (!data.success) { |
| showToast(data.message || 'Failed to save'); |
| return; |
| } |
| showToast(isEdit ? 'Announcement updated!' : 'Announcement created!'); |
| closeModal(); |
| loadAnnouncements(); |
| } catch (e) { |
| showToast('Network error'); |
| } |
| }; |
| |
| window.toggleActive = async function(id, active) { |
| try { |
| const res = await fetch('/api/announcements/' + id + '/toggle', { |
| method: 'POST', |
| headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({ active: active }), |
| }); |
| const data = await res.json(); |
| if (data.success) { |
| loadAnnouncements(); |
| } else { |
| showToast(data.message || 'Failed to toggle'); |
| } |
| } catch (e) { |
| showToast('Network error'); |
| } |
| }; |
| |
| window.deleteAnnouncement = async function(id) { |
| if (!confirm('Delete this announcement?')) return; |
| try { |
| const res = await fetch('/api/announcements/' + id, { method: 'DELETE' }); |
| const data = await res.json(); |
| if (data.success) { |
| showToast('Announcement deleted'); |
| loadAnnouncements(); |
| } else { |
| showToast(data.message || 'Failed to delete'); |
| } |
| } catch (e) { |
| showToast('Network error'); |
| } |
| }; |
| |
| |
| document.getElementById('aa-modal-overlay').addEventListener('click', function(e) { |
| if (e.target === this) closeModal(); |
| }); |
| |
| |
| loadAnnouncements(); |
| })(); |
| </script> |
| {% endblock %} |
|
|