anicove / api /templates /shared /admin_announcements.html
mwask's picture
tracking and nav
79d1711
Raw
History Blame Contribute Delete
19.3 kB
{% 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);
}
/* List */
.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); }
}
/* Modal */
.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>
<!-- Create/Edit Modal -->
<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>
<!-- Toast -->
<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');
}
};
// Close modal on backdrop click
document.getElementById('aa-modal-overlay').addEventListener('click', function(e) {
if (e.target === this) closeModal();
});
// Load on init
loadAnnouncements();
})();
</script>
{% endblock %}