Download Backend/fastapi/templates/subscriptions_manage.html from dhyeye/streamio: direct link, hf CLI and curl.
- Browser
- Download file 37.6 kB
-
https://huggingface.co/spaces/dhyeye/streamio/resolve/main/Backend/fastapi/templates/subscriptions_manage.html
- Command line
-
hf download hf://spaces/dhyeye/streamio/Backend/fastapi/templates/subscriptions_manage.html
-
curl -L -o subscriptions_manage.html https://huggingface.co/spaces/dhyeye/streamio/resolve/main/Backend/fastapi/templates/subscriptions_manage.html
37.6 kB
| {% extends "base.html" %} | |
| {% block title %}Subscription Management - Telegram Stremio{% endblock %} | |
| {% block content %} | |
| <style> | |
| .subs-shell { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1.5rem; | |
| } | |
| .page-hero, | |
| .section-panel, | |
| .table-shell, | |
| .mobile-user-card, | |
| .empty-state, | |
| .modal-glass { | |
| background: color-mix(in srgb, var(--card) 50%, transparent); | |
| backdrop-filter: blur(20px); | |
| -webkit-backdrop-filter: blur(20px); | |
| border: 1px solid var(--border); | |
| box-shadow: 0 12px 40px rgba(0, 0, 0, 0.22); | |
| } | |
| .page-hero, | |
| .section-panel, | |
| .table-shell, | |
| .empty-state { | |
| border-radius: 1.5rem; | |
| } | |
| .hero-glow { | |
| position: relative; | |
| overflow: hidden; | |
| isolation: isolate; | |
| } | |
| .hero-glow::before { | |
| content: ""; | |
| position: absolute; | |
| inset: -30% auto auto -10%; | |
| width: 16rem; | |
| height: 16rem; | |
| border-radius: 9999px; | |
| background: color-mix(in srgb, var(--primary) 18%, transparent); | |
| filter: blur(50px); | |
| z-index: -1; | |
| pointer-events: none; | |
| } | |
| .hero-glow::after { | |
| content: ""; | |
| position: absolute; | |
| inset: auto -5% -35% auto; | |
| width: 14rem; | |
| height: 14rem; | |
| border-radius: 9999px; | |
| background: color-mix(in srgb, var(--accent) 16%, transparent); | |
| filter: blur(50px); | |
| z-index: -1; | |
| pointer-events: none; | |
| } | |
| .section-title { | |
| color: var(--text); | |
| } | |
| .section-muted { | |
| color: var(--text-sec); | |
| } | |
| .glass-btn { | |
| border: 1px solid color-mix(in srgb, var(--border) 80%, transparent); | |
| background: color-mix(in srgb, var(--card) 92%, var(--bg)); | |
| color: var(--text); | |
| transition: all 0.25s ease; | |
| box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14); | |
| } | |
| .glass-btn:hover { | |
| transform: translateY(-1px); | |
| border-color: color-mix(in srgb, var(--primary) 35%, var(--border)); | |
| box-shadow: 0 10px 28px rgba(0, 0, 0, 0.18); | |
| } | |
| .glass-btn-primary { | |
| background: linear-gradient( | |
| 135deg, | |
| color-mix(in srgb, var(--primary) 88%, white 8%), | |
| color-mix(in srgb, var(--accent) 82%, var(--primary) 18%) | |
| ); | |
| color: white; | |
| border: 1px solid color-mix(in srgb, var(--primary) 50%, transparent); | |
| box-shadow: 0 10px 26px color-mix(in srgb, var(--primary) 22%, transparent); | |
| } | |
| .glass-btn-primary:hover { | |
| filter: brightness(1.05); | |
| box-shadow: 0 14px 34px color-mix(in srgb, var(--primary) 28%, transparent); | |
| } | |
| .glass-btn-danger { | |
| background: linear-gradient(135deg, #ef4444, #dc2626); | |
| color: #ffffff; | |
| border-color: transparent; | |
| } | |
| .glass-btn-warning { | |
| background: linear-gradient(135deg, #f59e0b, #d97706); | |
| color: #ffffff; | |
| border-color: transparent; | |
| } | |
| .glass-btn-success { | |
| background: linear-gradient(135deg, #22c55e, #16a34a); | |
| color: #ffffff; | |
| border-color: transparent; | |
| } | |
| .glass-input, | |
| .glass-select { | |
| width: 100%; | |
| border-radius: 1rem; | |
| border: 1px solid color-mix(in srgb, var(--border) 90%, transparent); | |
| background: color-mix(in srgb, var(--bg) 68%, transparent); | |
| color: var(--text); | |
| padding: 0.85rem 1rem; | |
| outline: none; | |
| transition: all 0.2s ease; | |
| box-shadow: inset 0 1px 0 rgba(255,255,255,0.02); | |
| } | |
| .glass-input::placeholder { | |
| color: color-mix(in srgb, var(--text-sec) 85%, transparent); | |
| } | |
| .glass-input:focus, | |
| .glass-select:focus { | |
| border-color: color-mix(in srgb, var(--primary) 65%, var(--border)); | |
| box-shadow: 0 0 0 4px color-mix(in srgb, var(--primary) 12%, transparent); | |
| } | |
| .plan-card { | |
| position: relative; | |
| overflow: hidden; | |
| border-radius: 1.5rem; | |
| background: linear-gradient( | |
| 180deg, | |
| color-mix(in srgb, var(--card) 96%, var(--bg)), | |
| color-mix(in srgb, var(--card) 88%, var(--bg)) | |
| ); | |
| border: 1px solid color-mix(in srgb, var(--border) 90%, transparent); | |
| box-shadow: 0 12px 30px rgba(0,0,0,0.18); | |
| transition: transform 0.28s ease, border-color 0.28s ease, box-shadow 0.28s ease; | |
| } | |
| .plan-card::before { | |
| content: ""; | |
| position: absolute; | |
| inset: -25% auto auto -5%; | |
| width: 8rem; | |
| height: 8rem; | |
| border-radius: 9999px; | |
| background: color-mix(in srgb, var(--primary) 18%, transparent); | |
| filter: blur(36px); | |
| pointer-events: none; | |
| } | |
| .plan-card:hover { | |
| transform: translateY(-4px); | |
| border-color: color-mix(in srgb, var(--primary) 30%, var(--border)); | |
| box-shadow: 0 18px 42px rgba(0,0,0,0.24); | |
| } | |
| .plan-actions { | |
| opacity: 0; | |
| transform: translateY(-4px); | |
| transition: all 0.22s ease; | |
| } | |
| .plan-card:hover .plan-actions { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| .metric-badge { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 0.45rem; | |
| padding: 0.45rem 0.8rem; | |
| border-radius: 9999px; | |
| font-size: 0.78rem; | |
| font-weight: 700; | |
| letter-spacing: 0.03em; | |
| border: 1px solid transparent; | |
| } | |
| .badge-active { | |
| background: color-mix(in srgb, #22c55e 16%, transparent); | |
| color: #86efac; | |
| border-color: color-mix(in srgb, #22c55e 28%, transparent); | |
| } | |
| .badge-expired { | |
| background: color-mix(in srgb, #ef4444 14%, transparent); | |
| color: #fca5a5; | |
| border-color: color-mix(in srgb, #ef4444 26%, transparent); | |
| } | |
| .table-shell { | |
| overflow: hidden; | |
| } | |
| .subs-table { | |
| width: 100%; | |
| border-collapse: separate; | |
| border-spacing: 0; | |
| } | |
| .subs-table thead th { | |
| background: color-mix(in srgb, var(--bg) 65%, transparent); | |
| color: var(--text-sec); | |
| font-size: 0.72rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.08em; | |
| border-bottom: 1px solid color-mix(in srgb, var(--border) 92%, transparent); | |
| } | |
| .subs-table tbody tr { | |
| transition: background 0.22s ease; | |
| } | |
| .subs-table tbody tr:hover { | |
| background: color-mix(in srgb, var(--primary) 6%, transparent); | |
| } | |
| .subs-table td { | |
| border-bottom: 1px solid color-mix(in srgb, var(--border) 70%, transparent); | |
| color: var(--text); | |
| } | |
| .subs-table tbody tr:last-child td { | |
| border-bottom: none; | |
| } | |
| .action-chip { | |
| border-radius: 9999px; | |
| padding: 0.45rem 0.8rem; | |
| font-size: 0.72rem; | |
| font-weight: 700; | |
| border: 1px solid transparent; | |
| transition: all 0.2s ease; | |
| white-space: nowrap; | |
| } | |
| .action-chip:hover { | |
| transform: translateY(-1px); | |
| } | |
| .mobile-user-card { | |
| border-radius: 1.35rem; | |
| padding: 1rem; | |
| } | |
| .mobile-user-card + .mobile-user-card { | |
| margin-top: 0.9rem; | |
| } | |
| .mobile-grid { | |
| display: none; | |
| } | |
| .pagination-shell { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 1rem; | |
| flex-wrap: wrap; | |
| } | |
| .pagination-text { | |
| color: var(--text-sec); | |
| } | |
| .pagination-btn { | |
| min-width: 92px; | |
| } | |
| .modal-backdrop { | |
| background: rgba(4, 8, 18, 0.58); | |
| backdrop-filter: blur(10px); | |
| -webkit-backdrop-filter: blur(10px); | |
| } | |
| .modal-glass { | |
| border-radius: 1.6rem; | |
| padding: 1.4rem; | |
| border: 1px solid color-mix(in srgb, var(--border) 95%, transparent); | |
| width: min(92vw, 30rem); | |
| } | |
| .modal-field label { | |
| color: var(--text-sec); | |
| font-size: 0.82rem; | |
| font-weight: 700; | |
| margin-bottom: 0.55rem; | |
| display: block; | |
| } | |
| .text-soft { | |
| color: var(--text-sec); | |
| } | |
| .no-scrollbar::-webkit-scrollbar { | |
| display: none; | |
| } | |
| .no-scrollbar { | |
| -ms-overflow-style: none; | |
| scrollbar-width: none; | |
| } | |
| @media (max-width: 1024px) { | |
| .subs-table th, | |
| .subs-table td { | |
| padding-left: 1rem; | |
| padding-right: 1rem; | |
| } | |
| } | |
| @media (max-width: 767px) { | |
| .desktop-table-wrap { | |
| display: none; | |
| } | |
| .mobile-grid { | |
| display: block; | |
| } | |
| .page-hero { | |
| padding: 1.25rem; | |
| border-radius: 1.35rem; | |
| } | |
| .section-panel { | |
| padding: 1rem; | |
| border-radius: 1.35rem; | |
| } | |
| .pagination-shell { | |
| flex-direction: column; | |
| align-items: stretch; | |
| } | |
| .pagination-shell .controls { | |
| width: 100%; | |
| display: grid; | |
| grid-template-columns: 1fr auto 1fr; | |
| gap: 0.6rem; | |
| align-items: center; | |
| } | |
| .pagination-btn { | |
| width: 100%; | |
| } | |
| .plan-actions { | |
| opacity: 1; | |
| transform: none; | |
| } | |
| } | |
| </style> | |
| <div class="subs-shell py-2 sm:py-4"> | |
| <!-- Hero --> | |
| <section class="page-hero hero-glow p-5 sm:p-6 lg:p-8"> | |
| <div class="flex flex-col gap-5 lg:flex-row lg:items-center lg:justify-between"> | |
| <div> | |
| <div class="inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-bold tracking-[0.18em] uppercase mb-3" | |
| style="border-color: color-mix(in srgb, var(--primary) 28%, var(--border)); color: var(--text-sec); background: color-mix(in srgb, var(--primary) 10%, transparent);"> | |
| <i class="fas fa-layer-group text-[11px]" style="color: var(--primary);"></i> | |
| Subscription Control Center | |
| </div> | |
| <h1 class="text-2xl sm:text-3xl lg:text-4xl font-extrabold tracking-tight section-title"> | |
| Subscription Management | |
| </h1> | |
| <p class="mt-2 text-sm sm:text-base section-muted max-w-2xl"> | |
| Create plans, manage users, and monitor active or expired subscriptions. | |
| </p> | |
| </div> | |
| <div class="flex flex-wrap gap-3"> | |
| <button onclick="openPlanModal()" class="glass-btn glass-btn-primary rounded-2xl px-5 py-3 text-sm font-bold inline-flex items-center gap-2"> | |
| <i class="fas fa-plus"></i> | |
| Add Plan | |
| </button> | |
| <button onclick="fetchSubscribers()" class="glass-btn rounded-2xl px-5 py-3 text-sm font-bold inline-flex items-center gap-2"> | |
| <i class="fas fa-rotate-right"></i> | |
| Refresh Users | |
| </button> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Plans --> | |
| <section class="section-panel p-4 sm:p-5 lg:p-6"> | |
| <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-5"> | |
| <div> | |
| <h2 class="text-lg sm:text-xl font-bold section-title">Subscription Plans</h2> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 sm:gap-5" id="plans-container"> | |
| <div class="empty-state col-span-full text-center px-6 py-10 text-soft"> | |
| <i class="fas fa-spinner fa-spin mr-2"></i> Loading plans... | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Users --> | |
| <section class="section-panel p-4 sm:p-5 lg:p-6"> | |
| <div class="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-4 mb-5"> | |
| <div> | |
| <h2 class="text-lg sm:text-xl font-bold section-title">User Subscriptions</h2> | |
| <p class="text-sm section-muted mt-1">Search, filter, paginate, and manage subscription access.</p> | |
| </div> | |
| <div class="w-full lg:w-auto grid grid-cols-1 sm:grid-cols-3 gap-3"> | |
| <input | |
| id="sub-search" | |
| type="text" | |
| placeholder="Search by name, username or ID..." | |
| class="glass-input" | |
| oninput="subPage=1; renderSubPage()" | |
| > | |
| <select id="sub-filter" class="glass-select" onchange="subPage=1; renderSubPage()"> | |
| <option value="all">All Status</option> | |
| <option value="active">Active</option> | |
| <option value="expired">Expired</option> | |
| </select> | |
| <select id="sub-page-size" class="glass-select" onchange="subPage=1; renderSubPage()"> | |
| <option value="10">10 / page</option> | |
| <option value="25">25 / page</option> | |
| <option value="50">50 / page</option> | |
| </select> | |
| </div> | |
| </div> | |
| <!-- Desktop table --> | |
| <div class="desktop-table-wrap table-shell"> | |
| <div class="overflow-x-auto no-scrollbar"> | |
| <table class="subs-table min-w-[780px] text-sm"> | |
| <thead> | |
| <tr> | |
| <th class="px-6 py-4 text-left font-semibold">User Details</th> | |
| <th class="px-6 py-4 text-center font-semibold">Status</th> | |
| <th class="px-6 py-4 text-center font-semibold">Expiry</th> | |
| <th class="px-6 py-4 text-right font-semibold">Actions</th> | |
| </tr> | |
| </thead> | |
| <tbody id="subscribers-tbody"> | |
| <tr> | |
| <td colspan="4" class="px-6 py-8 text-center text-soft"> | |
| <i class="fas fa-spinner fa-spin mr-2"></i> Loading... | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <!-- Mobile cards --> | |
| <div id="subscribers-mobile" class="mobile-grid mt-1"> | |
| <div class="empty-state text-center px-4 py-8 text-soft"> | |
| <i class="fas fa-spinner fa-spin mr-2"></i> Loading... | |
| </div> | |
| </div> | |
| <!-- Pagination --> | |
| <div class="pagination-shell mt-5" id="sub-pagination"> | |
| <div id="sub-page-info" class="pagination-text text-sm"></div> | |
| <div class="controls"> | |
| <button id="sub-btn-prev" onclick="subChangePage(-1)" | |
| class="glass-btn pagination-btn rounded-xl px-4 py-2.5 text-sm font-semibold"> | |
| ‹ Prev | |
| </button> | |
| <span id="sub-page-num" class="text-sm font-bold text-center section-title px-2"></span> | |
| <button id="sub-btn-next" onclick="subChangePage(1)" | |
| class="glass-btn pagination-btn rounded-xl px-4 py-2.5 text-sm font-semibold"> | |
| Next › | |
| </button> | |
| </div> | |
| </div> | |
| </section> | |
| </div> | |
| <!-- Plan Modal --> | |
| <div id="plan-modal" class="fixed inset-0 hidden z-50 modal-backdrop items-center justify-center p-4"> | |
| <div class="modal-glass"> | |
| <div class="flex items-start justify-between gap-4 mb-5"> | |
| <div> | |
| <h3 class="text-xl font-bold section-title" id="plan-modal-title">Add New Plan</h3> | |
| <p class="text-sm text-soft mt-1">Create or update a subscription plan.</p> | |
| </div> | |
| <button onclick="closePlanModal()" class="glass-btn rounded-full w-10 h-10 flex items-center justify-center"> | |
| <i class="fas fa-xmark"></i> | |
| </button> | |
| </div> | |
| <form id="plan-form" onsubmit="submitPlanForm(event)" class="space-y-4"> | |
| <input type="hidden" id="plan-id"> | |
| <div class="modal-field"> | |
| <label for="plan-days">Duration (Days)</label> | |
| <input type="number" id="plan-days" class="glass-input" required min="1"> | |
| </div> | |
| <div class="modal-field"> | |
| <label for="plan-price">Price (₹)</label> | |
| <input type="number" id="plan-price" class="glass-input" required min="0" step="0.01"> | |
| </div> | |
| <div class="flex flex-col-reverse sm:flex-row justify-end gap-3 pt-2"> | |
| <button type="button" onclick="closePlanModal()" class="glass-btn rounded-2xl px-5 py-3 text-sm font-bold"> | |
| Cancel | |
| </button> | |
| <button type="submit" class="glass-btn glass-btn-primary rounded-2xl px-5 py-3 text-sm font-bold"> | |
| Save Plan | |
| </button> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| <!-- User Action Modal --> | |
| <div id="user-modal" class="fixed inset-0 hidden z-50 modal-backdrop items-center justify-center p-4"> | |
| <div class="modal-glass"> | |
| <div class="flex items-start justify-between gap-4 mb-5"> | |
| <div> | |
| <h3 class="text-xl font-bold section-title" id="user-modal-title">Manage User</h3> | |
| <div class="text-sm text-soft mt-1" id="user-modal-desc"></div> | |
| </div> | |
| <button onclick="closeUserModal()" class="glass-btn rounded-full w-10 h-10 flex items-center justify-center"> | |
| <i class="fas fa-xmark"></i> | |
| </button> | |
| </div> | |
| <form id="user-form" onsubmit="submitUserForm(event)" class="space-y-4"> | |
| <input type="hidden" id="manage-user-id"> | |
| <input type="hidden" id="manage-action"> | |
| <div class="modal-field" id="days-input-group"> | |
| <label for="manage-days">Days to <span id="days-action-label"></span></label> | |
| <input type="number" id="manage-days" class="glass-input" min="1" value="30"> | |
| </div> | |
| <div class="flex flex-col-reverse sm:flex-row justify-end gap-3 pt-2"> | |
| <button type="button" onclick="closeUserModal()" class="glass-btn rounded-2xl px-5 py-3 text-sm font-bold"> | |
| Cancel | |
| </button> | |
| <button type="submit" class="glass-btn glass-btn-primary rounded-2xl px-5 py-3 text-sm font-bold" id="btn-user-save"> | |
| Confirm | |
| </button> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| <script> | |
| let allUsers = []; | |
| let subPage = 1; | |
| function formatExpiry(expiryValue) { | |
| if (!expiryValue) return 'N/A'; | |
| const d = new Date(expiryValue); | |
| return d.toLocaleDateString('en-IN', { | |
| timeZone: 'Asia/Kolkata', | |
| day: '2-digit', | |
| month: 'short', | |
| year: 'numeric' | |
| }) + ' ' + d.toLocaleTimeString('en-IN', { | |
| timeZone: 'Asia/Kolkata', | |
| hour: '2-digit', | |
| minute: '2-digit' | |
| }); | |
| } | |
| function getUserDisplayName(u) { | |
| let name = u.first_name || ''; | |
| if (u.username) name += (name ? ' ' : '') + `(@${u.username})`; | |
| if (!name) name = `User ${u._id}`; | |
| return name; | |
| } | |
| function getStatusBadge(status) { | |
| return status === 'active' | |
| ? '<span class="metric-badge badge-active"><i class="fas fa-circle text-[8px]"></i> Active</span>' | |
| : '<span class="metric-badge badge-expired"><i class="fas fa-circle text-[8px]"></i> Expired</span>'; | |
| } | |
| async function fetchPlans() { | |
| const container = document.getElementById('plans-container'); | |
| container.innerHTML = ` | |
| <div class="empty-state col-span-full text-center px-6 py-10 text-soft"> | |
| <i class="fas fa-spinner fa-spin mr-2"></i> Loading plans... | |
| </div> | |
| `; | |
| try { | |
| const res = await fetch('/api/admin/subscriptions/plans'); | |
| if (!res.ok) throw new Error('Failed to fetch plans'); | |
| const data = await res.json(); | |
| const plans = data.data || data.plans || []; | |
| if (!plans.length) { | |
| container.innerHTML = ` | |
| <div class="empty-state col-span-full text-center px-6 py-10 text-soft"> | |
| <i class="fas fa-box-open text-2xl mb-3"></i> | |
| <div class="font-semibold mb-1">No plans found</div> | |
| <div class="text-sm">Create your first subscription plan.</div> | |
| </div> | |
| `; | |
| return; | |
| } | |
| container.innerHTML = plans.map(p => ` | |
| <div class="plan-card p-5 sm:p-6"> | |
| <div class="plan-actions absolute right-4 top-4 flex gap-2"> | |
| <button onclick="openPlanModal('${p._id}', ${p.days}, ${p.price})" | |
| class="glass-btn rounded-full w-9 h-9 flex items-center justify-center" title="Edit plan"> | |
| <i class="fas fa-pen text-sm"></i> | |
| </button> | |
| <button onclick="deletePlan('${p._id}')" | |
| class="glass-btn glass-btn-danger rounded-full w-9 h-9 flex items-center justify-center" title="Delete plan"> | |
| <i class="fas fa-trash text-sm"></i> | |
| </button> | |
| </div> | |
| <div class="flex items-start gap-4"> | |
| <div class="w-14 h-14 rounded-2xl flex items-center justify-center shrink-0" | |
| style="background: color-mix(in srgb, var(--primary) 16%, transparent); border: 1px solid color-mix(in srgb, var(--primary) 24%, transparent);"> | |
| <i class="fas fa-calendar-alt text-xl" style="color: var(--primary);"></i> | |
| </div> | |
| <div class="min-w-0"> | |
| <div class="text-sm font-semibold section-muted">${p.days} Days Plan</div> | |
| <div class="mt-1 text-3xl font-extrabold section-title tracking-tight">₹${p.price}</div> | |
| <div class="mt-3 inline-flex items-center gap-2 text-xs font-bold rounded-full px-3 py-1" | |
| style="background: color-mix(in srgb, var(--accent) 12%, transparent); color: var(--text-sec); border: 1px solid color-mix(in srgb, var(--accent) 20%, transparent);"> | |
| <i class="fas fa-bolt" style="color: var(--accent);"></i> | |
| Premium Access | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } catch (err) { | |
| console.error(err); | |
| container.innerHTML = ` | |
| <div class="empty-state col-span-full text-center px-6 py-10 text-soft"> | |
| <i class="fas fa-triangle-exclamation mr-2"></i> Failed to load plans. | |
| </div> | |
| `; | |
| } | |
| } | |
| function openPlanModal(id = '', days = '', price = '') { | |
| document.getElementById('plan-id').value = id; | |
| document.getElementById('plan-days').value = days; | |
| document.getElementById('plan-price').value = price; | |
| document.getElementById('plan-modal-title').innerText = id ? 'Edit Plan' : 'Add New Plan'; | |
| const modal = document.getElementById('plan-modal'); | |
| modal.classList.remove('hidden'); | |
| modal.classList.add('flex'); | |
| document.body.classList.add('overflow-hidden'); | |
| } | |
| function closePlanModal() { | |
| const modal = document.getElementById('plan-modal'); | |
| modal.classList.add('hidden'); | |
| modal.classList.remove('flex'); | |
| document.body.classList.remove('overflow-hidden'); | |
| } | |
| async function submitPlanForm(e) { | |
| e.preventDefault(); | |
| const id = document.getElementById('plan-id').value; | |
| const payload = { | |
| days: parseInt(document.getElementById('plan-days').value), | |
| price: parseFloat(document.getElementById('plan-price').value) | |
| }; | |
| try { | |
| const res = await fetch(id ? `/api/admin/subscriptions/plans/${id}` : '/api/admin/subscriptions/plans', { | |
| method: id ? 'PUT' : 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(payload) | |
| }); | |
| if (!res.ok) throw new Error('Failed to save plan'); | |
| closePlanModal(); | |
| fetchPlans(); | |
| } catch (err) { | |
| console.error(err); | |
| showToast('Failed to save plan.', 'error', 'Save Failed'); | |
| } | |
| } | |
| async function deletePlan(id) { | |
| if (!confirm('Delete this plan?')) return; | |
| try { | |
| const res = await fetch(`/api/admin/subscriptions/plans/${id}`, { method: 'DELETE' }); | |
| if (!res.ok) throw new Error('Failed'); | |
| fetchPlans(); | |
| } catch (err) { | |
| console.error(err); | |
| showToast('Failed to delete plan.', 'error', 'Delete Failed'); | |
| } | |
| } | |
| function getSubFiltered() { | |
| const q = document.getElementById('sub-search').value.toLowerCase().trim(); | |
| const status = document.getElementById('sub-filter').value; | |
| return allUsers.filter(u => { | |
| const name = ((u.first_name || '') + ' ' + (u.username || '') + ' ' + (u._id || '')).toLowerCase(); | |
| const matchQ = !q || name.includes(q); | |
| const matchStatus = status === 'all' || u.subscription_status === status; | |
| return matchQ && matchStatus; | |
| }); | |
| } | |
| function renderDesktopRows(slice) { | |
| const tbody = document.getElementById('subscribers-tbody'); | |
| if (!slice.length) { | |
| tbody.innerHTML = ` | |
| <tr> | |
| <td colspan="4" class="px-6 py-10 text-center text-soft"> | |
| No users match your search. | |
| </td> | |
| </tr> | |
| `; | |
| return; | |
| } | |
| tbody.innerHTML = slice.map(u => { | |
| const expiry = formatExpiry(u.subscription_expiry); | |
| const name = getUserDisplayName(u); | |
| const safeName = name.replace(/'/g, "\\'"); | |
| const statusBadge = getStatusBadge(u.subscription_status); | |
| return ` | |
| <tr> | |
| <td class="px-6 py-4"> | |
| <div class="font-semibold section-title">${name}</div> | |
| <div class="text-xs text-soft mt-1">ID: ${u._id}</div> | |
| </td> | |
| <td class="px-6 py-4 text-center">${statusBadge}</td> | |
| <td class="px-6 py-4 text-center text-sm section-title">${expiry}</td> | |
| <td class="px-6 py-4"> | |
| <div class="flex justify-end gap-2 flex-wrap"> | |
| <button onclick="openUserModal(${u._id}, '${safeName}', 'extend')" | |
| class="action-chip glass-btn-success"> | |
| Extend | |
| </button> | |
| <button onclick="openUserModal(${u._id}, '${safeName}', 'reduce')" | |
| class="action-chip glass-btn-warning"> | |
| Reduce | |
| </button> | |
| <button onclick="openUserModal(${u._id}, '${safeName}', 'delete')" | |
| class="action-chip glass-btn-danger"> | |
| Revoke | |
| </button> | |
| </div> | |
| </td> | |
| </tr> | |
| `; | |
| }).join(''); | |
| } | |
| function renderMobileCards(slice) { | |
| const mobileWrap = document.getElementById('subscribers-mobile'); | |
| if (!slice.length) { | |
| mobileWrap.innerHTML = ` | |
| <div class="empty-state text-center px-4 py-8 text-soft"> | |
| No users match your search. | |
| </div> | |
| `; | |
| return; | |
| } | |
| mobileWrap.innerHTML = slice.map(u => { | |
| const expiry = formatExpiry(u.subscription_expiry); | |
| const name = getUserDisplayName(u); | |
| const safeName = name.replace(/'/g, "\\'"); | |
| const statusBadge = getStatusBadge(u.subscription_status); | |
| return ` | |
| <div class="mobile-user-card"> | |
| <div class="flex items-start justify-between gap-3 mb-3"> | |
| <div class="min-w-0"> | |
| <div class="font-semibold section-title break-words">${name}</div> | |
| <div class="text-xs text-soft mt-1 break-all">ID: ${u._id}</div> | |
| </div> | |
| <div class="shrink-0">${statusBadge}</div> | |
| </div> | |
| <div class="grid grid-cols-1 gap-3 mb-4"> | |
| <div class="rounded-2xl px-4 py-3" | |
| style="background: color-mix(in srgb, var(--bg) 55%, transparent); border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);"> | |
| <div class="text-[11px] uppercase tracking-[0.14em] text-soft font-bold mb-1">Expiry</div> | |
| <div class="text-sm font-semibold section-title">${expiry}</div> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-3 gap-2"> | |
| <button onclick="openUserModal(${u._id}, '${safeName}', 'extend')" | |
| class="action-chip glass-btn-success w-full"> | |
| Extend | |
| </button> | |
| <button onclick="openUserModal(${u._id}, '${safeName}', 'reduce')" | |
| class="action-chip glass-btn-warning w-full"> | |
| Reduce | |
| </button> | |
| <button onclick="openUserModal(${u._id}, '${safeName}', 'delete')" | |
| class="action-chip glass-btn-danger w-full"> | |
| Revoke | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| }).join(''); | |
| } | |
| function renderSubPage() { | |
| const filtered = getSubFiltered(); | |
| const pageSize = parseInt(document.getElementById('sub-page-size').value); | |
| const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize)); | |
| if (subPage > totalPages) subPage = totalPages; | |
| const start = filtered.length ? (subPage - 1) * pageSize : 0; | |
| const slice = filtered.slice(start, start + pageSize); | |
| document.getElementById('sub-page-info').textContent = | |
| filtered.length | |
| ? `Showing ${start + 1}–${Math.min(start + pageSize, filtered.length)} of ${filtered.length}` | |
| : 'Showing 0 of 0'; | |
| document.getElementById('sub-page-num').textContent = `Page ${subPage} / ${totalPages}`; | |
| const prevBtn = document.getElementById('sub-btn-prev'); | |
| const nextBtn = document.getElementById('sub-btn-next'); | |
| prevBtn.disabled = subPage <= 1; | |
| nextBtn.disabled = subPage >= totalPages; | |
| prevBtn.style.opacity = subPage <= 1 ? '0.45' : '1'; | |
| nextBtn.style.opacity = subPage >= totalPages ? '0.45' : '1'; | |
| renderDesktopRows(slice); | |
| renderMobileCards(slice); | |
| } | |
| function subChangePage(dir) { | |
| const filtered = getSubFiltered(); | |
| const pageSize = parseInt(document.getElementById('sub-page-size').value); | |
| const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize)); | |
| subPage += dir; | |
| if (subPage < 1) subPage = 1; | |
| if (subPage > totalPages) subPage = totalPages; | |
| renderSubPage(); | |
| } | |
| async function fetchSubscribers() { | |
| document.getElementById('subscribers-tbody').innerHTML = ` | |
| <tr> | |
| <td colspan="4" class="px-6 py-8 text-center text-soft"> | |
| <i class="fas fa-spinner fa-spin mr-2"></i> Loading... | |
| </td> | |
| </tr> | |
| `; | |
| document.getElementById('subscribers-mobile').innerHTML = ` | |
| <div class="empty-state text-center px-4 py-8 text-soft"> | |
| <i class="fas fa-spinner fa-spin mr-2"></i> Loading... | |
| </div> | |
| `; | |
| try { | |
| const res = await fetch('/api/admin/subscriptions/users'); | |
| if (!res.ok) throw new Error('Failed to fetch users'); | |
| const data = await res.json(); | |
| allUsers = data.data || []; | |
| subPage = 1; | |
| renderSubPage(); | |
| } catch (err) { | |
| console.error(err); | |
| document.getElementById('subscribers-tbody').innerHTML = ` | |
| <tr> | |
| <td colspan="4" class="px-6 py-8 text-center text-soft"> | |
| <i class="fas fa-triangle-exclamation mr-2"></i> Failed to load users. | |
| </td> | |
| </tr> | |
| `; | |
| document.getElementById('subscribers-mobile').innerHTML = ` | |
| <div class="empty-state text-center px-4 py-8 text-soft"> | |
| <i class="fas fa-triangle-exclamation mr-2"></i> Failed to load users. | |
| </div> | |
| `; | |
| } | |
| } | |
| function openUserModal(userId, userName, action) { | |
| document.getElementById('manage-user-id').value = userId; | |
| document.getElementById('manage-action').value = action; | |
| let title = ''; | |
| let desc = ''; | |
| let btnClasses = 'glass-btn glass-btn-primary rounded-2xl px-5 py-3 text-sm font-bold'; | |
| let showDays = true; | |
| if (action === 'extend') { | |
| title = 'Extend Subscription'; | |
| desc = `Add extra days to <b>${userName}</b>'s subscription.`; | |
| document.getElementById('days-action-label').innerText = 'add'; | |
| btnClasses = 'glass-btn glass-btn-success rounded-2xl px-5 py-3 text-sm font-bold'; | |
| } else if (action === 'reduce') { | |
| title = 'Reduce Subscription'; | |
| desc = `Subtract days from <b>${userName}</b>'s subscription.`; | |
| document.getElementById('days-action-label').innerText = 'subtract'; | |
| btnClasses = 'glass-btn glass-btn-warning rounded-2xl px-5 py-3 text-sm font-bold'; | |
| } else { | |
| title = 'Revoke Subscription'; | |
| desc = `<b>Warning:</b> This will remove <b>${userName}</b>'s subscription access completely.`; | |
| showDays = false; | |
| btnClasses = 'glass-btn glass-btn-danger rounded-2xl px-5 py-3 text-sm font-bold'; | |
| } | |
| document.getElementById('user-modal-title').innerText = title; | |
| document.getElementById('user-modal-desc').innerHTML = desc; | |
| document.getElementById('days-input-group').style.display = showDays ? 'block' : 'none'; | |
| document.getElementById('btn-user-save').className = btnClasses; | |
| if (!showDays) { | |
| document.getElementById('manage-days').value = 0; | |
| document.getElementById('manage-days').removeAttribute('required'); | |
| } else { | |
| document.getElementById('manage-days').setAttribute('required', 'required'); | |
| if (!document.getElementById('manage-days').value || document.getElementById('manage-days').value === '0') { | |
| document.getElementById('manage-days').value = 30; | |
| } | |
| } | |
| const modal = document.getElementById('user-modal'); | |
| modal.classList.remove('hidden'); | |
| modal.classList.add('flex'); | |
| document.body.classList.add('overflow-hidden'); | |
| } | |
| function closeUserModal() { | |
| const modal = document.getElementById('user-modal'); | |
| modal.classList.add('hidden'); | |
| modal.classList.remove('flex'); | |
| document.body.classList.remove('overflow-hidden'); | |
| } | |
| async function submitUserForm(e) { | |
| e.preventDefault(); | |
| const userId = document.getElementById('manage-user-id').value; | |
| const action = document.getElementById('manage-action').value; | |
| const days = document.getElementById('manage-days').value; | |
| try { | |
| const res = await fetch(`/api/admin/subscriptions/users/${userId}/manage`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ action, days: parseInt(days) || 0 }) | |
| }); | |
| if (!res.ok) throw new Error('Failed to update user'); | |
| closeUserModal(); | |
| showToast('Subscription updated successfully.', 'success', 'Updated'); | |
| fetchSubscribers(); | |
| } catch (err) { | |
| console.error(err); | |
| showToast('Failed to update user subscription.', 'error', 'Update Failed'); | |
| } | |
| } | |
| document.addEventListener('click', (e) => { | |
| const planModal = document.getElementById('plan-modal'); | |
| const userModal = document.getElementById('user-modal'); | |
| if (e.target === planModal) closePlanModal(); | |
| if (e.target === userModal) closeUserModal(); | |
| }); | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'Escape') { | |
| closePlanModal(); | |
| closeUserModal(); | |
| } | |
| }); | |
| document.addEventListener('DOMContentLoaded', () => { | |
| fetchPlans(); | |
| fetchSubscribers(); | |
| }); | |
| </script> | |
| {% endblock %} | |