| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"/> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> |
| <title>RECOM β Admin Dashboard</title> |
| <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet"/> |
| <style> |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } |
| :root { --orange: #F26419; --peach: #F9A87E; --cream: #FFF3EC; --deep: #3D1A00; --mid: #7A3B10; --light-orange: #FDDCC4; } |
| body { font-family: 'DM Sans', sans-serif; background: var(--cream); min-height: 100vh; padding-bottom: 60px; } |
| |
| header { |
| background: #fff; border-bottom: 1.5px solid var(--light-orange); |
| padding: 18px 32px; display: flex; align-items: center; justify-content: space-between; |
| position: sticky; top: 0; z-index: 10; |
| box-shadow: 0 2px 16px rgba(242,100,25,0.07); |
| } |
| .logo { display: flex; align-items: center; gap: 12px; } |
| .logo-mark { |
| width: 38px; height: 38px; background: var(--orange); border-radius: 10px; |
| display: flex; align-items: center; justify-content: center; |
| color: #fff; font-family: 'Playfair Display', serif; font-size: 18px; font-weight: 700; |
| } |
| .logo-text { font-family: 'Playfair Display', serif; font-size: 18px; font-weight: 700; color: var(--deep); } |
| .logo-text span { color: var(--orange); } |
| .admin-badge { |
| background: var(--light-orange); color: var(--orange); |
| font-size: 11px; font-weight: 600; letter-spacing: 1.5px; |
| text-transform: uppercase; padding: 4px 12px; border-radius: 100px; |
| border: 1px solid var(--peach); |
| } |
| .logout { |
| background: none; border: 1.5px solid var(--light-orange); color: var(--mid); |
| padding: 8px 16px; border-radius: 10px; font-family: 'DM Sans', sans-serif; |
| font-size: 13px; cursor: pointer; text-decoration: none; |
| transition: border-color 0.2s, color 0.2s; |
| } |
| .logout:hover { border-color: var(--orange); color: var(--orange); } |
| |
| .container { max-width: 900px; margin: 0 auto; padding: 32px 20px; } |
| |
| .stats { |
| display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); |
| gap: 16px; margin-bottom: 32px; |
| } |
| .stat-card { |
| background: #fff; border: 1.5px solid var(--light-orange); border-radius: 16px; |
| padding: 20px 22px; animation: fadeUp 0.4s ease both; |
| } |
| .stat-label { font-size: 12px; letter-spacing: 1px; text-transform: uppercase; color: var(--mid); margin-bottom: 8px; font-weight: 500; } |
| .stat-value { font-family: 'Playfair Display', serif; font-size: 32px; color: var(--orange); font-weight: 700; } |
| |
| .section-title { |
| font-family: 'Playfair Display', serif; font-size: 20px; color: var(--deep); |
| margin-bottom: 16px; display: flex; align-items: center; gap: 10px; |
| } |
| |
| .empty { |
| background: #fff; border: 1.5px solid var(--light-orange); border-radius: 18px; |
| padding: 60px 20px; text-align: center; color: var(--mid); |
| } |
| .empty-icon { font-size: 40px; margin-bottom: 12px; } |
| .empty p { font-size: 15px; font-weight: 300; } |
| |
| .cards-grid { display: grid; gap: 16px; } |
| |
| .reg-card { |
| background: #fff; border: 1.5px solid var(--light-orange); border-radius: 18px; |
| padding: 22px 24px; animation: fadeUp 0.4s ease both; |
| transition: box-shadow 0.2s, opacity 0.3s, transform 0.3s; |
| position: relative; |
| } |
| .reg-card:hover { box-shadow: 0 8px 28px rgba(242,100,25,0.12); } |
| .reg-card.removing { opacity: 0; transform: scale(0.96); } |
| |
| .reg-header { |
| display: flex; align-items: flex-start; justify-content: space-between; |
| margin-bottom: 14px; flex-wrap: wrap; gap: 8px; |
| } |
| .reg-name { font-family: 'Playfair Display', serif; font-size: 20px; color: var(--deep); } |
| .reg-id { |
| background: var(--light-orange); color: var(--orange); |
| font-size: 12px; font-weight: 600; padding: 4px 12px; border-radius: 100px; |
| } |
| .reg-chips { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; } |
| .chip { |
| background: var(--cream); border: 1px solid var(--light-orange); |
| border-radius: 100px; padding: 4px 12px; font-size: 13px; color: var(--mid); |
| } |
| .chip strong { color: var(--deep); } |
| .reg-details { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } |
| .detail { display: flex; align-items: flex-start; gap: 8px; font-size: 14px; color: var(--mid); } |
| .detail-icon { font-size: 16px; flex-shrink: 0; } |
| .detail-text { line-height: 1.5; } |
| |
| .reg-footer { |
| display: flex; align-items: center; justify-content: space-between; |
| margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--light-orange); |
| flex-wrap: wrap; gap: 10px; |
| } |
| .reg-date { font-size: 12px; color: var(--mid); font-weight: 300; } |
| |
| |
| .delete-btn { |
| display: flex; align-items: center; gap: 6px; |
| background: none; border: 1.5px solid #ffcccc; color: #cc3300; |
| padding: 7px 14px; border-radius: 10px; font-family: 'DM Sans', sans-serif; |
| font-size: 13px; font-weight: 500; cursor: pointer; |
| transition: background 0.2s, border-color 0.2s, transform 0.15s; |
| } |
| .delete-btn:hover { background: #fff0f0; border-color: #cc3300; transform: translateY(-1px); } |
| .delete-btn:active { transform: translateY(0); } |
| |
| |
| .confirm-overlay { |
| display: none; position: fixed; inset: 0; |
| background: rgba(61,26,0,0.35); backdrop-filter: blur(4px); |
| z-index: 100; align-items: center; justify-content: center; |
| } |
| .confirm-overlay.show { display: flex; animation: fadeIn 0.2s ease; } |
| .confirm-box { |
| background: #fff; border-radius: 20px; padding: 32px 28px; |
| width: min(360px, 90vw); box-shadow: 0 20px 60px rgba(61,26,0,0.2); |
| animation: popUp 0.25s cubic-bezier(0.34,1.56,0.64,1) both; |
| text-align: center; |
| } |
| .confirm-icon { font-size: 36px; margin-bottom: 14px; } |
| .confirm-box h3 { font-family: 'Playfair Display', serif; font-size: 20px; color: var(--deep); margin-bottom: 8px; } |
| .confirm-box p { font-size: 14px; color: var(--mid); font-weight: 300; margin-bottom: 24px; line-height: 1.6; } |
| .confirm-name { color: var(--orange); font-weight: 600; } |
| .confirm-actions { display: flex; gap: 10px; } |
| .btn-cancel { |
| flex: 1; padding: 12px; background: var(--cream); border: 1.5px solid var(--light-orange); |
| border-radius: 12px; font-family: 'DM Sans', sans-serif; font-size: 14px; |
| color: var(--mid); cursor: pointer; transition: border-color 0.2s; |
| } |
| .btn-cancel:hover { border-color: var(--mid); } |
| .btn-confirm-delete { |
| flex: 1; padding: 12px; background: #cc3300; border: none; |
| border-radius: 12px; font-family: 'DM Sans', sans-serif; font-size: 14px; |
| font-weight: 600; color: #fff; cursor: pointer; |
| transition: background 0.2s, transform 0.15s; |
| box-shadow: 0 4px 14px rgba(204,51,0,0.3); |
| } |
| .btn-confirm-delete:hover { background: #aa2800; transform: translateY(-1px); } |
| |
| |
| .toast { |
| position: fixed; bottom: 28px; left: 50%; transform: translateX(-50%) translateY(80px); |
| background: var(--deep); color: #fff; padding: 12px 24px; |
| border-radius: 100px; font-size: 14px; font-weight: 500; |
| z-index: 200; transition: transform 0.3s cubic-bezier(0.34,1.56,0.64,1); |
| white-space: nowrap; |
| } |
| .toast.show { transform: translateX(-50%) translateY(0); } |
| |
| @keyframes fadeUp { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: translateY(0); } } |
| @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } |
| @keyframes popUp { from { transform: scale(0.85); opacity: 0; } to { transform: scale(1); opacity: 1; } } |
| </style> |
| </head> |
| <body> |
|
|
| <header> |
| <div class="logo"> |
| <div class="logo-mark">R</div> |
| <div class="logo-text">RE<span>COM</span></div> |
| <div class="admin-badge">Admin</div> |
| </div> |
| <a href="/admin/logout" class="logout">Logout</a> |
| </header> |
|
|
| <div class="container"> |
|
|
| <div class="stats"> |
| <div class="stat-card" style="animation-delay:0.1s"> |
| <div class="stat-label">Total Registrations</div> |
| <div class="stat-value" id="totalCount">{{ students|length }}</div> |
| </div> |
| <div class="stat-card" style="animation-delay:0.2s"> |
| <div class="stat-label">Total Revenue</div> |
| <div class="stat-value" id="totalRevenue">βΉ{{ students|length * 200 }}</div> |
| </div> |
| </div> |
|
|
| <div class="section-title">π All Registrations</div> |
|
|
| {% if students|length == 0 %} |
| <div class="empty" id="emptyState"> |
| <div class="empty-icon">π</div> |
| <p>No registrations yet. They'll appear here once students sign up.</p> |
| </div> |
| {% else %} |
| <div class="empty" id="emptyState" style="display:none"> |
| <div class="empty-icon">π</div> |
| <p>No registrations yet. They'll appear here once students sign up.</p> |
| </div> |
| <div class="cards-grid" id="cardsGrid"> |
| {% for s in students|reverse %} |
| <div class="reg-card" id="card-{{ s.id }}" style="animation-delay: {{ loop.index0 * 0.05 }}s"> |
| <div class="reg-header"> |
| <div class="reg-name">{{ s.name }}</div> |
| <div class="reg-id">#{{ s.id }}</div> |
| </div> |
| <div class="reg-chips"> |
| <div class="chip">Class <strong>{{ s.class }}</strong></div> |
| <div class="chip">Section <strong>{{ s.section }}</strong></div> |
| </div> |
| <div class="reg-details"> |
| <div class="detail"> |
| <span class="detail-icon">π</span> |
| <div class="detail-text">{{ s.get('phone', 'β') }}</div> |
| </div> |
| <div class="detail"> |
| <span class="detail-icon">π¬</span> |
| <div class="detail-text">{{ s.get('whatsapp', 'β') }}</div> |
| </div> |
| <div class="detail" style="grid-column: 1 / -1"> |
| <span class="detail-icon">π</span> |
| <div class="detail-text">{{ s.get('address', 'β') }}</div> |
| </div> |
| </div> |
| <div class="reg-footer"> |
| <div class="reg-date">π Registered on {{ s.get('registered_at', 'β') }}</div> |
| <button class="delete-btn" onclick="confirmDelete({{ s.id }}, '{{ s.name }}')"> |
| ποΈ Mark Complete & Delete |
| </button> |
| </div> |
| </div> |
| {% endfor %} |
| </div> |
| {% endif %} |
|
|
| </div> |
|
|
| |
| <div class="confirm-overlay" id="confirmOverlay"> |
| <div class="confirm-box"> |
| <div class="confirm-icon">β
</div> |
| <h3>Mark as Complete?</h3> |
| <p>This will remove <span class="confirm-name" id="confirmName"></span> from the list. Only do this once their record is fully done and delivered.</p> |
| <div class="confirm-actions"> |
| <button class="btn-cancel" onclick="closeConfirm()">Cancel</button> |
| <button class="btn-confirm-delete" onclick="doDelete()">Yes, Delete</button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="toast" id="toast">β
Record marked as complete!</div> |
|
|
| <script> |
| let pendingId = null; |
| |
| function confirmDelete(id, name) { |
| pendingId = id; |
| document.getElementById("confirmName").textContent = name; |
| document.getElementById("confirmOverlay").classList.add("show"); |
| } |
| |
| function closeConfirm() { |
| document.getElementById("confirmOverlay").classList.remove("show"); |
| pendingId = null; |
| } |
| |
| async function doDelete() { |
| if (!pendingId) return; |
| closeConfirm(); |
| |
| const card = document.getElementById("card-" + pendingId); |
| card.classList.add("removing"); |
| |
| const res = await fetch("/admin/delete/" + pendingId, { method: "POST" }); |
| const result = await res.json(); |
| |
| setTimeout(() => { |
| card.remove(); |
| updateStats(result.total); |
| showToast(); |
| if (result.total === 0) { |
| document.getElementById("emptyState").style.display = "block"; |
| } |
| }, 300); |
| |
| pendingId = null; |
| } |
| |
| function updateStats(total) { |
| document.getElementById("totalCount").textContent = total; |
| document.getElementById("totalRevenue").textContent = "βΉ" + (total * 200); |
| } |
| |
| function showToast() { |
| const toast = document.getElementById("toast"); |
| toast.classList.add("show"); |
| setTimeout(() => toast.classList.remove("show"), 3000); |
| } |
| |
| |
| document.getElementById("confirmOverlay").addEventListener("click", function(e) { |
| if (e.target === this) closeConfirm(); |
| }); |
| </script> |
| </body> |
| </html> |
|
|