405 lines
24 KiB
HTML
405 lines
24 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Account Settings - Plugin Compass</title>
|
|
<link rel="icon" type="image/png" href="/assets/Plugin.png">
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" />
|
|
<script src="https://cdn.jsdelivr.net/npm/dodopayments-checkout@latest/dist/index.js"></script>
|
|
<script src="/posthog.js"></script>
|
|
<style>
|
|
:root {
|
|
--green: #008060;
|
|
--green-dark: #004c3f;
|
|
--green-light: #e3f5ef;
|
|
--border: #e5e7eb;
|
|
--muted: #6b7280;
|
|
--panel: #ffffff;
|
|
--bg: #f8fafc;
|
|
}
|
|
* { box-sizing: border-box; }
|
|
body { margin: 0; font-family: 'Inter', system-ui, -apple-system, sans-serif; background: var(--bg); color: #0f172a; min-height: 100vh; }
|
|
a { color: inherit; text-decoration: none; }
|
|
.nav-bar { background: #fff; border-bottom: 1px solid var(--border); padding: 16px 24px; position: sticky; top: 0; z-index: 100; }
|
|
.nav-content { max-width: 1200px; margin: 0 auto; display: flex; justify-content: space-between; align-items: center; }
|
|
.brand { display: flex; align-items: center; gap: 12px; }
|
|
.brand img { width: 32px; height: 32px; border-radius: 8px; }
|
|
.brand span { font-weight: 600; font-size: 18px; }
|
|
.nav-links { display: flex; gap: 12px; align-items: center; }
|
|
.user-chip { display: flex; align-items: center; gap: 10px; padding: 8px 12px; border: 1px solid var(--border); border-radius: 10px; cursor: pointer; }
|
|
.user-avatar { width: 32px; height: 32px; border-radius: 50%; background: var(--green); color: #fff; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 14px; }
|
|
.user-email { font-size: 13px; font-weight: 600; }
|
|
.dropdown { position: relative; }
|
|
.dropdown-menu { position: absolute; top: 100%; right: 0; margin-top: 8px; background: #fff; border: 1px solid var(--border); border-radius: 12px; box-shadow: 0 10px 25px rgba(0,0,0,0.1); min-width: 180px; display: none; padding: 6px; z-index: 1000; }
|
|
.dropdown-menu.active { display: block; }
|
|
.dropdown-item { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 8px; font-size: 14px; color: #475569; }
|
|
.dropdown-item:hover { background: #f8fafc; color: var(--green); }
|
|
.shell { max-width: 1200px; margin: 0 auto; padding: 32px 24px; }
|
|
.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 32px; }
|
|
@media (max-width: 900px) { .stats-grid { grid-template-columns: repeat(2, 1fr); } }
|
|
@media (max-width: 500px) { .stats-grid { grid-template-columns: 1fr; } }
|
|
.stat-card { background: #fff; border: 1px solid var(--border); border-radius: 12px; padding: 24px; }
|
|
.stat-card.highlight { background: linear-gradient(135deg, var(--green), var(--green-dark)); color: #fff; border: none; }
|
|
.stat-icon { width: 48px; height: 48px; border-radius: 10px; background: var(--green-light); color: var(--green); display: flex; align-items: center; justify-content: center; font-size: 20px; margin-bottom: 16px; }
|
|
.stat-card.highlight .stat-icon { background: rgba(255,255,255,0.2); color: #fff; }
|
|
.stat-label { font-size: 13px; color: var(--muted); margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
.stat-card.highlight .stat-label { color: rgba(255,255,255,0.8); }
|
|
.stat-value { font-size: 28px; font-weight: 700; }
|
|
.stat-sub { font-size: 12px; color: var(--muted); margin-top: 4px; }
|
|
.stat-card.highlight .stat-sub { color: rgba(255,255,255,0.8); }
|
|
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
|
.section-title { font-size: 20px; font-weight: 700; }
|
|
.main-grid { display: grid; grid-template-columns: 2fr 1fr; gap: 24px; }
|
|
@media (max-width: 900px) { .main-grid { grid-template-columns: 1fr; } }
|
|
.card { background: #fff; border: 1px solid var(--border); border-radius: 12px; padding: 24px; margin-bottom: 24px; }
|
|
.card h3 { font-size: 16px; font-weight: 700; margin: 0 0 16px; display: flex; align-items: center; gap: 10px; }
|
|
.card h3 i { color: var(--green); }
|
|
.field { margin-bottom: 16px; }
|
|
.field label { display: block; font-weight: 600; font-size: 13px; color: #475569; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.025em; }
|
|
.field-row { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
|
|
@media (max-width: 640px) { .field-row { grid-template-columns: 1fr; } }
|
|
input, select { width: 100%; padding: 12px 16px; border: 1px solid var(--border); border-radius: 8px; font-size: 14px; background: #fff; transition: all 0.2s; }
|
|
input:focus, select:focus { outline: none; border-color: var(--green); box-shadow: 0 0 0 3px rgba(0,128,96,0.1); }
|
|
select { appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%2364748b'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 12px center; background-size: 16px; padding-right: 40px; }
|
|
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; padding: 12px 20px; border-radius: 8px; font-weight: 600; font-size: 14px; cursor: pointer; transition: all 0.2s; border: 1px solid var(--border); background: #fff; color: #1e293b; }
|
|
.btn:hover { background: #f8fafc; }
|
|
.btn.primary { background: var(--green); color: #fff; border: none; }
|
|
.btn.primary:hover { background: var(--green-dark); }
|
|
.btn.danger { color: #dc2626; border-color: #fecaca; }
|
|
.btn.danger:hover { background: #fef2f2; }
|
|
.btn.full { width: 100%; }
|
|
.status { font-size: 13px; color: var(--muted); margin-bottom: 16px; }
|
|
.status.success { color: #059669; }
|
|
.status.error { color: #b91c1c; }
|
|
.profile-section { display: flex; align-items: center; gap: 16px; padding: 16px; background: var(--green-light); border-radius: 10px; margin-bottom: 16px; }
|
|
.profile-avatar { width: 48px; height: 48px; border-radius: 50%; background: var(--green); color: #fff; display: flex; align-items: center; justify-content: center; font-size: 20px; font-weight: 700; }
|
|
.profile-info h4 { margin: 0; font-size: 16px; }
|
|
.profile-info p { margin: 4px 0 0; font-size: 13px; color: var(--muted); }
|
|
.badge { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 6px; font-size: 11px; font-weight: 600; }
|
|
.badge.success { background: #ecfdf5; color: #059669; }
|
|
.payment-item { display: flex; align-items: center; gap: 12px; padding: 14px; border: 1px solid var(--border); border-radius: 8px; margin-bottom: 10px; }
|
|
.payment-item.default { border-color: var(--green); background: var(--green-light); }
|
|
.payment-icon { width: 40px; height: 28px; background: #f1f5f9; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 20px; }
|
|
.invoice-item { display: flex; align-items: center; justify-content: space-between; padding: 12px; border: 1px solid var(--border); border-radius: 8px; margin-bottom: 8px; }
|
|
.invoice-amount { font-weight: 700; color: var(--green); }
|
|
.empty-state { text-align: center; padding: 24px; color: var(--muted); }
|
|
.empty-state i { font-size: 28px; margin-bottom: 12px; opacity: 0.5; }
|
|
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(15,23,42,0.6); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; z-index: 10000; opacity: 0; visibility: hidden; transition: all 0.3s; }
|
|
.modal-overlay.active { opacity: 1; visibility: visible; }
|
|
.modal { background: #fff; border-radius: 16px; padding: 24px; max-width: 500px; width: 90%; }
|
|
.modal-header { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
|
|
.modal-icon { width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 20px; }
|
|
.modal-icon.warning { background: #fff7ed; color: #ea580c; }
|
|
.modal-icon.info { background: #eff6ff; color: #2563eb; }
|
|
.modal-title { font-size: 18px; font-weight: 700; }
|
|
.modal-body { color: #64748b; line-height: 1.6; margin-bottom: 24px; }
|
|
.modal-actions { display: flex; gap: 12px; justify-content: flex-end; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<nav class="nav-bar">
|
|
<div class="nav-content">
|
|
<a href="/" class="brand">
|
|
<img src="/assets/Plugin.png" alt="Plugin Compass">
|
|
<span>Plugin Compass</span>
|
|
</a>
|
|
<div class="nav-links">
|
|
<a href="/apps" class="btn" style="padding:8px 16px;"><i class="fa-solid fa-grid-2"></i> Apps</a>
|
|
<div class="dropdown">
|
|
<div class="user-chip">
|
|
<div class="user-avatar" id="nav-avatar">?</div>
|
|
<span class="user-email" id="nav-email">Loading...</span>
|
|
</div>
|
|
<div class="dropdown-menu" id="user-dropdown">
|
|
<a href="/apps" class="dropdown-item"><i class="fa-solid fa-grid-2"></i> My Apps</a>
|
|
<a href="/settings" class="dropdown-item"><i class="fa-solid fa-gear"></i> Settings</a>
|
|
<a href="/topup" class="dropdown-item"><i class="fa-solid fa-coins"></i> Tokens</a>
|
|
<a href="#" class="dropdown-item" id="logout-link"><i class="fa-solid fa-arrow-right-from-bracket"></i> Logout</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
<div class="shell">
|
|
<div class="stats-grid">
|
|
<div class="stat-card highlight">
|
|
<div class="stat-icon"><i class="fa-solid fa-user"></i></div>
|
|
<div class="stat-label">Account</div>
|
|
<div class="stat-value" id="settings-email" style="font-size:16px;">Loading...</div>
|
|
<div class="stat-sub" id="settings-status">Active</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-icon"><i class="fa-solid fa-crown"></i></div>
|
|
<div class="stat-label">Current Plan</div>
|
|
<div class="stat-value" id="settings-plan">-</div>
|
|
<div class="stat-sub" id="settings-billing-status">-</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-icon"><i class="fa-solid fa-calendar"></i></div>
|
|
<div class="stat-label">Renews On</div>
|
|
<div class="stat-value" id="settings-renews" style="font-size:18px;">-</div>
|
|
<div class="stat-sub">Next billing date</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-icon"><i class="fa-solid fa-credit-card"></i></div>
|
|
<div class="stat-label">Payment</div>
|
|
<div class="stat-value" id="payment-count">0</div>
|
|
<div class="stat-sub">Saved methods</div>
|
|
</div>
|
|
</div>
|
|
<div class="main-grid">
|
|
<div>
|
|
<div class="card">
|
|
<h3><i class="fa-solid fa-sliders"></i> Subscription Settings</h3>
|
|
<form id="settings-form">
|
|
<div class="field-row">
|
|
<div class="field">
|
|
<label for="plan">Plan</label>
|
|
<select id="plan"></select>
|
|
</div>
|
|
<div class="field">
|
|
<label for="billing-cycle">Cycle</label>
|
|
<select id="billing-cycle">
|
|
<option value="monthly">Monthly</option>
|
|
<option value="yearly">Yearly (20% off)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="field-row">
|
|
<div class="field">
|
|
<label for="currency">Currency</label>
|
|
<select id="currency">
|
|
<option value="usd">USD</option>
|
|
<option value="gbp">GBP</option>
|
|
<option value="eur">EUR</option>
|
|
</select>
|
|
</div>
|
|
<div class="field">
|
|
<label for="billing-email">Billing Email</label>
|
|
<input id="billing-email" type="email" placeholder="you@example.com" required />
|
|
</div>
|
|
</div>
|
|
<div class="status" id="settings-status-line"></div>
|
|
<button type="submit" class="btn primary full">Save Changes</button>
|
|
</form>
|
|
</div>
|
|
<div class="card">
|
|
<h3><i class="fa-solid fa-wallet"></i> Payment Methods</h3>
|
|
<div id="payment-methods-list">
|
|
<div class="empty-state"><i class="fa-regular fa-credit-card"></i><p>No payment methods saved</p></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div class="card">
|
|
<h3><i class="fa-solid fa-file-invoice"></i> Recent Invoices</h3>
|
|
<div id="invoices-list">
|
|
<div class="empty-state"><i class="fa-solid fa-file-invoice"></i><p>No invoices yet</p></div>
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<h3><i class="fa-solid fa-bolt"></i> Quick Actions</h3>
|
|
<a href="/topup" class="btn full" style="margin-bottom:12px;"><i class="fa-solid fa-coins"></i> Buy Top-ups</a>
|
|
<button class="btn danger full" id="cancel-plan"><i class="fa-solid fa-ban"></i> Cancel Plan</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-overlay" id="confirm-modal">
|
|
<div class="modal">
|
|
<div class="modal-header">
|
|
<div class="modal-icon warning" id="modal-icon"><i class="fa-solid fa-triangle-exclamation"></i></div>
|
|
<div class="modal-title" id="modal-title">Confirm</div>
|
|
</div>
|
|
<div class="modal-body" id="modal-body">Are you sure?</div>
|
|
<div class="modal-actions">
|
|
<button class="btn" id="modal-cancel">Cancel</button>
|
|
<button class="btn primary" id="modal-confirm">Confirm</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
document.querySelector('.user-chip').addEventListener('click', () => {
|
|
document.getElementById('user-dropdown').classList.toggle('active');
|
|
});
|
|
document.addEventListener('click', (e) => {
|
|
if (!e.target.closest('.dropdown')) document.getElementById('user-dropdown').classList.remove('active');
|
|
});
|
|
const el = {
|
|
email: document.getElementById('settings-email'),
|
|
navEmail: document.getElementById('nav-email'),
|
|
avatar: document.getElementById('nav-avatar'),
|
|
plan: document.getElementById('settings-plan'),
|
|
billingStatus: document.getElementById('settings-billing-status'),
|
|
renews: document.getElementById('settings-renews'),
|
|
status: document.getElementById('settings-status'),
|
|
billingEmail: document.getElementById('billing-email'),
|
|
billingCycle: document.getElementById('billing-cycle'),
|
|
currency: document.getElementById('currency'),
|
|
planSelect: document.getElementById('plan'),
|
|
form: document.getElementById('settings-form'),
|
|
statusLine: document.getElementById('settings-status-line'),
|
|
cancel: document.getElementById('cancel-plan'),
|
|
paymentMethodsList: document.getElementById('payment-methods-list'),
|
|
invoicesList: document.getElementById('invoices-list'),
|
|
paymentCount: document.getElementById('payment-count'),
|
|
};
|
|
let csrfToken = '';
|
|
let currentPlan = '';
|
|
let allInvoices = [];
|
|
function showConfirmModal({ title, body, icon, onConfirm }) {
|
|
const modal = document.getElementById('confirm-modal');
|
|
document.getElementById('modal-title').textContent = title;
|
|
document.getElementById('modal-body').textContent = body;
|
|
const iconEl = document.getElementById('modal-icon');
|
|
iconEl.className = `modal-icon ${icon || 'warning'}`;
|
|
iconEl.innerHTML = icon === 'info' ? '<i class="fa-solid fa-circle-info"></i>' : '<i class="fa-solid fa-triangle-exclamation"></i>';
|
|
modal.classList.add('active');
|
|
const handleConfirm = () => { onConfirm(); closeModal(); };
|
|
const closeModal = () => { modal.classList.remove('active'); document.getElementById('modal-confirm').removeEventListener('click', handleConfirm); document.getElementById('modal-cancel').removeEventListener('click', closeModal); };
|
|
document.getElementById('modal-confirm').addEventListener('click', handleConfirm);
|
|
document.getElementById('modal-cancel').addEventListener('click', closeModal);
|
|
}
|
|
function setStatus(msg, type = '') { if (el.statusLine) { el.statusLine.textContent = msg || ''; el.statusLine.className = `status ${type}`; } }
|
|
function formatDate(iso) { if (!iso) return '-'; const date = new Date(iso); return isNaN(date.getTime()) ? '-' : date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); }
|
|
function setAvatar(email) { const initial = (email || '?').trim().charAt(0).toUpperCase() || '?'; el.avatar.textContent = initial; }
|
|
function renderAccount(account) {
|
|
const email = account?.email || '';
|
|
el.email.textContent = email || 'Unknown';
|
|
el.navEmail.textContent = email || 'Unknown';
|
|
setAvatar(email);
|
|
el.plan.textContent = account?.plan || 'hobby';
|
|
el.billingStatus.textContent = account?.billingStatus || 'active';
|
|
el.renews.textContent = formatDate(account?.subscriptionRenewsAt);
|
|
el.planSelect.value = account?.plan || 'hobby';
|
|
el.billingEmail.value = account?.billingEmail || email || '';
|
|
el.currency.value = account?.subscriptionCurrency || 'usd';
|
|
el.status.textContent = account?.billingStatus === 'canceled' ? 'Canceled' : 'Active';
|
|
currentPlan = account?.plan || 'hobby';
|
|
}
|
|
async function loadAccount() {
|
|
try {
|
|
const resp = await fetch('/api/account', { credentials: 'same-origin' });
|
|
if (resp.status === 401) { window.location.href = '/login?next=/settings'; return; }
|
|
const data = await resp.json().catch(() => ({}));
|
|
renderAccount(data.account || {});
|
|
} catch (err) { setStatus('Unable to load account.', 'error'); }
|
|
}
|
|
async function loadPlans() {
|
|
try {
|
|
const resp = await fetch('/api/account/plans', { credentials: 'same-origin' });
|
|
const data = await resp.json().catch(() => ({}));
|
|
const plans = Array.isArray(data.plans) ? data.plans : ['hobby', 'starter', 'business', 'enterprise'];
|
|
el.planSelect.innerHTML = '';
|
|
plans.forEach(plan => {
|
|
const option = document.createElement('option');
|
|
option.value = plan;
|
|
option.textContent = plan === 'hobby' ? 'Hobby (free)' : plan === 'business' ? 'Professional' : plan.charAt(0).toUpperCase() + plan.slice(1);
|
|
el.planSelect.appendChild(option);
|
|
});
|
|
if (data.defaultPlan) el.planSelect.value = data.defaultPlan;
|
|
} catch (_) {
|
|
el.planSelect.innerHTML = '<option value="hobby">Hobby (free)</option><option value="starter">Starter</option><option value="business">Professional</option><option value="enterprise">Enterprise</option>';
|
|
}
|
|
}
|
|
async function saveAccount(event) {
|
|
if (event) event.preventDefault();
|
|
const payload = { plan: el.planSelect.value, billingCycle: el.billingCycle.value, currency: el.currency.value, billingEmail: el.billingEmail.value };
|
|
showConfirmModal({ title: 'Save Changes', body: 'Are you sure you want to update your account settings?', icon: 'info', onConfirm: async () => {
|
|
setStatus('Saving...');
|
|
try {
|
|
const resp = await fetch('/api/account', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, credentials: 'same-origin', body: JSON.stringify(payload) });
|
|
const data = await resp.json().catch(() => ({}));
|
|
if (!resp.ok) throw new Error(data.error || 'Save failed');
|
|
renderAccount(data.account || {});
|
|
setStatus('Account updated successfully', 'success');
|
|
} catch (err) { setStatus(err.message, 'error'); }
|
|
}});
|
|
}
|
|
async function loadCsrfToken() {
|
|
const csrfResp = await fetch('/api/csrf', { credentials: 'same-origin' });
|
|
if (csrfResp.ok) { const csrfData = await csrfResp.json().catch(() => ({})); csrfToken = csrfData.csrfToken || ''; }
|
|
}
|
|
function getCardIcon(brand) {
|
|
const b = (brand || '').toLowerCase();
|
|
if (b.includes('visa')) return '<i class="fa-brands fa-cc-visa" style="color:#1a1f71;"></i>';
|
|
if (b.includes('mastercard')) return '<i class="fa-brands fa-cc-mastercard" style="color:#eb001b;"></i>';
|
|
if (b.includes('amex')) return '<i class="fa-brands fa-cc-amex" style="color:#006fcf;"></i>';
|
|
return '<i class="fa-regular fa-credit-card"></i>';
|
|
}
|
|
function renderPaymentMethods(methods) {
|
|
if (!el.paymentMethodsList) return;
|
|
el.paymentCount.textContent = methods?.length || 0;
|
|
if (!Array.isArray(methods) || methods.length === 0) {
|
|
el.paymentMethodsList.innerHTML = '<div class="empty-state"><i class="fa-regular fa-credit-card"></i><p>No payment methods saved</p></div>';
|
|
return;
|
|
}
|
|
el.paymentMethodsList.innerHTML = methods.map((m, i) => `
|
|
<div class="payment-item ${i === 0 ? 'default' : ''}">
|
|
<div class="payment-icon">${getCardIcon(m.brand)}</div>
|
|
<div style="flex:1;"><div style="font-weight:600;font-size:14px;">${m.brand || 'Card'}</div><div style="font-size:12px;color:var(--muted);">•••• ${m.last4 || '••••'}</div></div>
|
|
${i === 0 ? '<span class="badge success">Default</span>' : ''}
|
|
</div>
|
|
`).join('');
|
|
}
|
|
async function loadPaymentMethods() {
|
|
if (!el.paymentMethodsList) return;
|
|
try {
|
|
const resp = await fetch('/api/account/payment-methods', { credentials: 'same-origin' });
|
|
if (resp.status === 404 || resp.status === 401) { renderPaymentMethods([]); return; }
|
|
const data = await resp.json().catch(() => ({}));
|
|
renderPaymentMethods(data.paymentMethods || []);
|
|
} catch (err) { renderPaymentMethods([]); }
|
|
}
|
|
function renderInvoices(invoices) {
|
|
if (!el.invoicesList) return;
|
|
allInvoices = invoices;
|
|
if (!Array.isArray(invoices) || invoices.length === 0) {
|
|
el.invoicesList.innerHTML = '<div class="empty-state"><i class="fa-solid fa-file-invoice"></i><p>No invoices yet</p></div>';
|
|
return;
|
|
}
|
|
const display = invoices.slice(0, 5);
|
|
el.invoicesList.innerHTML = display.map(inv => {
|
|
const date = new Date(inv.createdAt).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
|
const amount = inv.amount / 100;
|
|
return `<div class="invoice-item"><div><div style="font-weight:600;font-size:13px;">${inv.invoiceNumber}</div><div style="font-size:11px;color:var(--muted);">${date}</div></div><div class="invoice-amount" style="font-size:13px;">${inv.currency} ${amount.toFixed(2)}</div></div>`;
|
|
}).join('');
|
|
}
|
|
async function loadInvoices() {
|
|
if (!el.invoicesList) return;
|
|
try {
|
|
const resp = await fetch('/api/account/invoices', { credentials: 'same-origin' });
|
|
if (resp.status === 404 || resp.status === 401) { renderInvoices([]); return; }
|
|
const data = await resp.json().catch(() => ({}));
|
|
renderInvoices(data.invoices || []);
|
|
} catch (err) { renderInvoices([]); }
|
|
}
|
|
document.getElementById('logout-link').addEventListener('click', async (e) => {
|
|
e.preventDefault();
|
|
showConfirmModal({ title: 'Logout', body: 'Are you sure you want to logout?', icon: 'info', onConfirm: async () => {
|
|
await fetch('/api/logout', { method: 'POST', credentials: 'same-origin' });
|
|
window.location.href = '/login';
|
|
}});
|
|
});
|
|
el.cancel.addEventListener('click', () => {
|
|
showConfirmModal({ title: 'Cancel Subscription', body: 'Are you sure you want to cancel?', icon: 'warning', onConfirm: async () => {
|
|
setStatus('Canceling...');
|
|
try {
|
|
const resp = await fetch('/api/subscription/cancel', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, credentials: 'same-origin' });
|
|
const data = await resp.json().catch(() => ({}));
|
|
if (!resp.ok) throw new Error(data.error || 'Unable to cancel');
|
|
renderAccount(data.account || {});
|
|
setStatus('Subscription canceled', 'success');
|
|
} catch (err) { setStatus(err.message, 'error'); }
|
|
}});
|
|
});
|
|
el.form.addEventListener('submit', saveAccount);
|
|
loadCsrfToken().then(() => { loadAccount(); loadPlans(); loadPaymentMethods(); loadInvoices(); });
|
|
</script>
|
|
</body>
|
|
</html>
|