mailpilot sending
This commit is contained in:
10
.env.example
10
.env.example
@@ -70,13 +70,9 @@ JWT_SECRET=
|
||||
JWT_ACCESS_TOKEN_TTL=900
|
||||
JWT_REFRESH_TOKEN_TTL=604800
|
||||
|
||||
# Email (SMTP)
|
||||
SMTP_HOST=
|
||||
SMTP_PORT=587
|
||||
SMTP_SECURE=false
|
||||
SMTP_USER=
|
||||
SMTP_PASS=
|
||||
SMTP_FROM=
|
||||
# Email (MailPilot Transactional Email)
|
||||
MAILPILOT_URL=https://emailmarketing.modelrailway3d.co.uk
|
||||
MAILPILOT_TOKEN=
|
||||
|
||||
# Public URL
|
||||
PUBLIC_BASE_URL=
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"archiver": "^6.0.1",
|
||||
"bcrypt": "^6.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"nodemailer": "^7.0.7",
|
||||
"pdfkit": "^0.17.2",
|
||||
"sharp": "^0.33.5",
|
||||
"better-sqlite3": "^11.8.1",
|
||||
|
||||
516
chat/public/settings-layouts/layout-1-sidebar.html
Normal file
516
chat/public/settings-layouts/layout-1-sidebar.html
Normal file
@@ -0,0 +1,516 @@
|
||||
<!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;
|
||||
--sidebar-bg: #1e293b;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
background: var(--bg);
|
||||
color: #0f172a;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
}
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
background: var(--sidebar-bg);
|
||||
color: #fff;
|
||||
padding: 24px 0;
|
||||
position: fixed;
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.sidebar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 0 24px 24px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.sidebar-brand img { width: 32px; height: 32px; border-radius: 8px; }
|
||||
.sidebar-brand span { font-weight: 600; font-size: 18px; }
|
||||
.sidebar-nav { padding: 0 12px; }
|
||||
.sidebar-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
color: #94a3b8;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
margin-bottom: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.sidebar-item:hover { background: rgba(255,255,255,0.05); color: #fff; }
|
||||
.sidebar-item.active { background: var(--green); color: #fff; }
|
||||
.sidebar-section {
|
||||
padding: 16px 24px 8px;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
}
|
||||
.main { flex: 1; margin-left: 260px; padding: 32px 40px; }
|
||||
.header { margin-bottom: 32px; }
|
||||
.header h1 { font-size: 28px; font-weight: 700; margin: 0 0 8px; }
|
||||
.header p { color: var(--muted); margin: 0; }
|
||||
.content-card {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 28px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.content-card h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.content-card h2 i { color: var(--green); }
|
||||
.content-card .subtitle { color: var(--muted); font-size: 14px; margin-bottom: 20px; }
|
||||
.user-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, var(--green-light), transparent);
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.avatar {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
background: var(--green);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 22px;
|
||||
}
|
||||
.user-info h3 { margin: 0 0 4px; font-size: 18px; }
|
||||
.user-info p { margin: 0; color: var(--muted); font-size: 14px; }
|
||||
.badge { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 6px; font-size: 12px; font-weight: 600; }
|
||||
.badge.success { background: #ecfdf5; color: #059669; }
|
||||
.form-row { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; }
|
||||
@media (max-width: 768px) { .form-row { grid-template-columns: 1fr; } }
|
||||
.field { margin-bottom: 20px; }
|
||||
.field label { display: block; font-weight: 600; font-size: 13px; color: #475569; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.025em; }
|
||||
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; border-color: #cbd5e1; }
|
||||
.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.ghost { background: transparent; border-color: transparent; }
|
||||
.btn.ghost:hover { background: rgba(0,0,0,0.04); }
|
||||
.actions-bar { display: flex; gap: 12px; justify-content: flex-end; margin-top: 24px; padding-top: 20px; border-top: 1px solid var(--border); }
|
||||
.status { font-size: 13px; color: var(--muted); margin-top: 8px; }
|
||||
.status.success { color: #059669; }
|
||||
.status.error { color: #b91c1c; }
|
||||
.info-row { display: flex; justify-content: space-between; padding: 12px 0; border-bottom: 1px solid var(--border); }
|
||||
.info-row:last-child { border-bottom: none; }
|
||||
.info-label { color: var(--muted); font-size: 14px; }
|
||||
.info-value { font-weight: 600; font-size: 14px; }
|
||||
.payment-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.payment-card.default { border-color: var(--green); background: var(--green-light); }
|
||||
.card-icon { width: 48px; height: 32px; background: #f1f5f9; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 24px; }
|
||||
.empty-state { text-align: center; padding: 32px; color: var(--muted); }
|
||||
.empty-state i { font-size: 32px; margin-bottom: 12px; opacity: 0.5; }
|
||||
.invoice-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.invoice-item:hover { border-color: var(--green); }
|
||||
.invoice-amount { font-weight: 700; color: var(--green); }
|
||||
.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; }
|
||||
@media (max-width: 900px) {
|
||||
.sidebar { display: none; }
|
||||
.main { margin-left: 0; padding: 20px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-brand">
|
||||
<img src="/assets/Plugin.png" alt="Plugin Compass">
|
||||
<span>Plugin Compass</span>
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<div class="sidebar-section">Navigation</div>
|
||||
<a href="/apps" class="sidebar-item"><i class="fa-solid fa-grid-2"></i> My Apps</a>
|
||||
<a href="/feature-requests" class="sidebar-item"><i class="fa-solid fa-star"></i> Feature Requests</a>
|
||||
<a href="/topup" class="sidebar-item"><i class="fa-solid fa-coins"></i> Tokens Top-up</a>
|
||||
<div class="sidebar-section">Account</div>
|
||||
<a href="/settings" class="sidebar-item active"><i class="fa-solid fa-gear"></i> Settings</a>
|
||||
<a href="#" class="sidebar-item" id="logout-link"><i class="fa-solid fa-arrow-right-from-bracket"></i> Logout</a>
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="main">
|
||||
<div class="header">
|
||||
<h1>Account Settings</h1>
|
||||
<p>Manage your account, billing, and preferences</p>
|
||||
</div>
|
||||
<div class="content-card">
|
||||
<h2><i class="fa-solid fa-user"></i> Account Overview</h2>
|
||||
<p class="subtitle">Your account information and subscription status</p>
|
||||
<div class="user-display">
|
||||
<div class="avatar" id="settings-avatar">?</div>
|
||||
<div class="user-info">
|
||||
<h3 id="settings-email">Loading...</h3>
|
||||
<p><span class="badge success" id="settings-status">Active</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Current Plan</span>
|
||||
<span class="info-value" id="settings-plan">-</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Billing Status</span>
|
||||
<span class="info-value" id="settings-billing-status">-</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Renews On</span>
|
||||
<span class="info-value" id="settings-renews">-</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-card">
|
||||
<h2><i class="fa-solid fa-credit-card"></i> Plan & Billing</h2>
|
||||
<p class="subtitle">Update your subscription and billing preferences</p>
|
||||
<form id="settings-form">
|
||||
<div class="form-row">
|
||||
<div class="field">
|
||||
<label for="plan">Subscription Plan</label>
|
||||
<select id="plan"></select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="billing-cycle">Billing Cycle</label>
|
||||
<select id="billing-cycle">
|
||||
<option value="monthly">Monthly</option>
|
||||
<option value="yearly">Yearly (save 20%)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="field">
|
||||
<label for="currency">Currency</label>
|
||||
<select id="currency">
|
||||
<option value="usd">USD - US Dollar</option>
|
||||
<option value="gbp">GBP - British Pound</option>
|
||||
<option value="eur">EUR - Euro</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>
|
||||
<div class="actions-bar">
|
||||
<button type="submit" class="btn primary">Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="content-card">
|
||||
<h2><i class="fa-solid fa-wallet"></i> Payment Methods</h2>
|
||||
<p class="subtitle">Manage your saved payment methods</p>
|
||||
<div id="payment-methods-list">
|
||||
<div class="empty-state">
|
||||
<i class="fa-regular fa-credit-card"></i>
|
||||
<p>No payment methods saved yet</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-card">
|
||||
<h2><i class="fa-solid fa-file-invoice"></i> Invoices</h2>
|
||||
<p class="subtitle">View and download your payment invoices</p>
|
||||
<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="content-card">
|
||||
<h2><i class="fa-solid fa-sliders"></i> Billing Actions</h2>
|
||||
<p class="subtitle">Manage your subscription</p>
|
||||
<div class="actions-bar">
|
||||
<a href="/topup" class="btn"><i class="fa-solid fa-coins"></i> Buy Top-ups</a>
|
||||
<button class="btn danger" id="cancel-plan"><i class="fa-solid fa-ban"></i> Cancel Plan</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<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 Action</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>
|
||||
const el = {
|
||||
email: document.getElementById('settings-email'),
|
||||
avatar: document.getElementById('settings-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'),
|
||||
};
|
||||
let csrfToken = '';
|
||||
let currentPlan = '';
|
||||
let allInvoices = [];
|
||||
let currentInvoicePage = 1;
|
||||
const INVOICES_PER_PAGE = 5;
|
||||
const PLAN_ALIASES = { business: 'professional' };
|
||||
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 'Not scheduled'; const date = new Date(iso); return isNaN(date.getTime()) ? 'Not scheduled' : 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';
|
||||
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 settings.', '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 newPlan = el.planSelect.value;
|
||||
const payload = { plan: newPlan, 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;
|
||||
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 yet</p></div>';
|
||||
return;
|
||||
}
|
||||
el.paymentMethodsList.innerHTML = methods.map((m, i) => `
|
||||
<div class="payment-card ${i === 0 ? 'default' : ''}">
|
||||
<div class="card-icon">${getCardIcon(m.brand)}</div>
|
||||
<div style="flex:1;"><div style="font-weight:600;">${m.brand || 'Card'} ${i === 0 ? '<span class="badge success">Default</span>' : ''}</div><div style="font-size:13px;color:var(--muted);">•••• ${m.last4 || '••••'}</div></div>
|
||||
</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, 4);
|
||||
el.invoicesList.innerHTML = display.map(inv => {
|
||||
const date = new Date(inv.createdAt).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
|
||||
const amount = inv.amount / 100;
|
||||
return `<div class="invoice-item"><div><div style="font-weight:600;">${inv.invoiceNumber}</div><div style="font-size:12px;color:var(--muted);">${date}</div></div><div class="invoice-amount">${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? You will retain access until the end of your billing period.', 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>
|
||||
444
chat/public/settings-layouts/layout-10-wizard.html
Normal file
444
chat/public/settings-layouts/layout-10-wizard.html
Normal file
@@ -0,0 +1,444 @@
|
||||
<!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 { background: #fff; border-bottom: 1px solid var(--border); padding: 16px 32px; display: flex; justify-content: space-between; align-items: center; position: sticky; top: 0; z-index: 100; }
|
||||
.brand { display: flex; align-items: center; gap: 12px; }
|
||||
.brand img { width: 32px; height: 32px; border-radius: 8px; }
|
||||
.brand span { font-weight: 700; font-size: 18px; }
|
||||
.nav-links { display: flex; gap: 8px; align-items: center; }
|
||||
.nav-btn { padding: 10px 18px; border-radius: 8px; font-size: 14px; font-weight: 500; color: var(--muted); transition: all 0.2s; }
|
||||
.nav-btn:hover { background: #f1f5f9; color: #1e293b; }
|
||||
.nav-btn.active { background: var(--green-light); color: var(--green); }
|
||||
.user-menu { position: relative; }
|
||||
.user-avatar { width: 36px; height: 36px; border-radius: 50%; background: var(--green); color: #fff; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 14px; cursor: pointer; }
|
||||
.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: 900px; margin: 0 auto; padding: 40px 32px; }
|
||||
.wizard-steps { display: flex; align-items: center; gap: 16px; margin-bottom: 32px; padding: 20px; background: #fff; border: 1px solid var(--border); border-radius: 12px; }
|
||||
.wizard-step { display: flex; align-items: center; gap: 10px; padding: 10px 16px; border-radius: 8px; cursor: pointer; transition: all 0.2s; }
|
||||
.wizard-step:hover { background: #f8fafc; }
|
||||
.wizard-step.active { background: var(--green-light); }
|
||||
.wizard-step.completed { color: var(--green); }
|
||||
.wizard-step .step-num { width: 28px; height: 28px; border-radius: 50%; background: #e5e7eb; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 700; }
|
||||
.wizard-step.active .step-num { background: var(--green); color: #fff; }
|
||||
.wizard-step.completed .step-num { background: var(--green); color: #fff; }
|
||||
.wizard-step .step-label { font-size: 14px; font-weight: 500; color: var(--muted); }
|
||||
.wizard-step.active .step-label { color: var(--green); }
|
||||
.wizard-step.completed .step-label { color: var(--green); }
|
||||
.wizard-divider { flex: 1; height: 2px; background: var(--border); }
|
||||
.wizard-content { display: none; }
|
||||
.wizard-content.active { display: block; }
|
||||
.card { background: #fff; border: 1px solid var(--border); border-radius: 16px; padding: 32px; margin-bottom: 24px; }
|
||||
.card h2 { font-size: 20px; font-weight: 700; margin: 0 0 8px; }
|
||||
.card .subtitle { color: var(--muted); font-size: 14px; margin-bottom: 24px; }
|
||||
.profile-header { display: flex; align-items: center; gap: 20px; padding: 24px; background: linear-gradient(135deg, var(--green-light), #fff); border-radius: 12px; margin-bottom: 24px; }
|
||||
.profile-avatar { width: 72px; height: 72px; border-radius: 50%; background: var(--green); color: #fff; display: flex; align-items: center; justify-content: center; font-size: 28px; font-weight: 700; }
|
||||
.profile-info h3 { margin: 0; font-size: 20px; }
|
||||
.profile-info p { margin: 0; color: var(--muted); }
|
||||
.badge { display: inline-flex; align-items: center; gap: 6px; padding: 4px 12px; border-radius: 6px; font-size: 12px; font-weight: 600; }
|
||||
.badge.success { background: #ecfdf5; color: #059669; }
|
||||
.field { margin-bottom: 20px; }
|
||||
.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: 20px; }
|
||||
@media (max-width: 600px) { .field-row { grid-template-columns: 1fr; } }
|
||||
input, select { width: 100%; padding: 12px 16px; border: 1px solid var(--border); border-radius: 10px; font-size: 14px; background: #fff; transition: all 0.2s; }
|
||||
input:focus, select:focus { outline: none; border-color: var(--green); box-shadow: 0 0 0 4px 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 24px; border-radius: 10px; 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%; }
|
||||
.actions { display: flex; gap: 12px; justify-content: flex-end; margin-top: 24px; padding-top: 20px; border-top: 1px solid var(--border); }
|
||||
.status { font-size: 13px; color: var(--muted); }
|
||||
.status.success { color: #059669; }
|
||||
.status.error { color: #b91c1c; }
|
||||
.info-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
|
||||
@media (max-width: 700px) { .info-grid { grid-template-columns: 1fr; } }
|
||||
.info-card { padding: 20px; background: #f8fafc; border-radius: 12px; text-align: center; }
|
||||
.info-icon { width: 40px; height: 40px; border-radius: 10px; background: var(--green-light); color: var(--green); display: flex; align-items: center; justify-content: center; font-size: 18px; margin: 0 auto 12px; }
|
||||
.info-label { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 4px; }
|
||||
.info-value { font-size: 18px; font-weight: 700; }
|
||||
.payment-item { display: flex; align-items: center; gap: 16px; padding: 16px; border: 1px solid var(--border); border-radius: 12px; margin-bottom: 12px; }
|
||||
.payment-item.default { border-color: var(--green); background: var(--green-light); }
|
||||
.payment-icon { width: 52px; height: 36px; background: #f1f5f9; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 26px; }
|
||||
.empty-state { text-align: center; padding: 48px 24px; color: var(--muted); }
|
||||
.empty-state i { font-size: 40px; margin-bottom: 16px; opacity: 0.5; }
|
||||
.invoice-item { display: flex; align-items: center; justify-content: space-between; padding: 16px; border: 1px solid var(--border); border-radius: 10px; margin-bottom: 10px; transition: all 0.2s; }
|
||||
.invoice-item:hover { border-color: var(--green); box-shadow: 0 2px 8px rgba(0,128,96,0.08); }
|
||||
.invoice-amount { font-weight: 700; color: var(--green); font-size: 16px; }
|
||||
.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: 20px; padding: 28px; max-width: 440px; width: 90%; }
|
||||
.modal-header { display: flex; align-items: center; gap: 14px; margin-bottom: 18px; }
|
||||
.modal-icon { width: 44px; height: 44px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 22px; }
|
||||
.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; font-size: 15px; line-height: 1.6; margin-bottom: 24px; }
|
||||
.modal-actions { display: flex; gap: 12px; justify-content: flex-end; }
|
||||
@media (max-width: 600px) {
|
||||
.shell { padding: 24px 16px; }
|
||||
.wizard-steps { flex-direction: column; gap: 12px; text-align: center; padding: 24px; flex-direction: column; text-align: center; }
|
||||
.wizard-divider { width: 2px; height: 24px; margin: 0 auto; }
|
||||
.wizard-step { flex-direction: column; padding: 12px; flex-direction: column; padding: 12px; }
|
||||
.profile-header { flex-direction: column; text-align: center; }
|
||||
.profile-avatar { width: 64px; height: 64px; font-size: 26px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="nav">
|
||||
<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="nav-btn"><i class="fa-solid fa-grid-2"></i> Apps</a>
|
||||
<a href="/topup" class="nav-btn"><i class="fa-solid fa-coins"></i> Tokens</a>
|
||||
<a href="/settings" class="nav-btn active"><i class="fa-solid fa-gear"></i> Settings</a>
|
||||
<div class="dropdown">
|
||||
<div class="user-avatar" id="nav-avatar">?</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="#" class="dropdown-item" id="logout-link"><i class="fa-solid fa-arrow-right-from-bracket"></i> Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="shell">
|
||||
<div class="wizard-steps">
|
||||
<div class="wizard-step active" data-step="1">
|
||||
<div class="step-num">1</div>
|
||||
<div class="step-label">Account Overview</div>
|
||||
</div>
|
||||
<div class="wizard-divider"></div>
|
||||
<div class="wizard-step" data-step="2">
|
||||
<div class="step-num">2</div>
|
||||
<div class="step-label">Billing Settings</div>
|
||||
</div>
|
||||
<div class="wizard-divider"></div>
|
||||
<div class="wizard-step" data-step="3">
|
||||
<div class="step-num">3</div>
|
||||
<div class="step-label">Payments & Invoices</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wizard-content active" id="step-content-1">
|
||||
<div class="profile-header">
|
||||
<div class="profile-avatar" id="settings-avatar">?</div>
|
||||
<div class="profile-info">
|
||||
<h3 id="settings-email">Loading...</h3>
|
||||
<p><span class="badge success" id="settings-status">Active</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="info-card">
|
||||
<div class="info-icon"><i class="fa-solid fa-crown"></i></div>
|
||||
<div class="info-label">Current Plan</div>
|
||||
<div class="info-value" id="settings-plan">-</div>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<div class="info-icon"><i class="fa-solid fa-chart-line"></i></div>
|
||||
<div class="info-label">Status</div>
|
||||
<div class="info-value" id="settings-billing-status">-</div>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<div class="info-icon"><i class="fa-solid fa-calendar"></i></div>
|
||||
<div class="info-label">Renews On</div>
|
||||
<div class="info-value" id="settings-renews">-</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a href="/topup" class="btn"><i class="fa-solid fa-coins"></i> Buy Top-ups</a>
|
||||
<button class="btn primary" onclick="goToStep(2)">Continue <i class="fa-solid fa-arrow-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wizard-content" id="step-content-2">
|
||||
<div class="card">
|
||||
<h2>Billing Settings</h2>
|
||||
<p class="subtitle">Update your plan and billing preferences</p>
|
||||
<form id="settings-form">
|
||||
<div class="field-row">
|
||||
<div class="field">
|
||||
<label for="plan">Subscription Plan</label>
|
||||
<select id="plan"></select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="billing-cycle">Billing Cycle</label>
|
||||
<select id="billing-cycle">
|
||||
<option value="monthly">Monthly</option>
|
||||
<option value="yearly">Yearly (save 20%)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<div class="field">
|
||||
<label for="currency">Currency</label>
|
||||
<select id="currency">
|
||||
<option value="usd">USD - US Dollar</option>
|
||||
<option value="gbp">GBP - British Pound</option>
|
||||
<option value="eur">EUR - Euro</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>
|
||||
</form>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn" onclick="goToStep(1)"><i class="fa-solid fa-arrow-left"></i> Back</button>
|
||||
<button class="btn danger" id="cancel-plan"><i class="fa-solid fa-ban"></i> Cancel Plan</button>
|
||||
<button type="submit" form="settings-form" class="btn primary">Save Changes</button>
|
||||
<button class="btn primary" onclick="goToStep(3)">Continue <i class="fa-solid fa-arrow-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wizard-content" id="step-content-3">
|
||||
<div class="card">
|
||||
<h2>Payment Methods</h2>
|
||||
<p class="subtitle">Manage your saved payment methods</p>
|
||||
<div id="payment-methods-list">
|
||||
<div class="empty-state"><i class="fa-regular fa-credit-card"></i><p>No payment methods saved yet</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Invoice History</h2>
|
||||
<p class="subtitle">View and download your payment invoices</p>
|
||||
<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="actions">
|
||||
<button class="btn" onclick="goToStep(2)"><i class="fa-solid fa-arrow-left"></i> Back</button>
|
||||
<button class="btn primary" onclick="goToStep(1)"><i class="fa-solid fa-check"></i> Done</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<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>
|
||||
function goToStep(step) {
|
||||
document.querySelectorAll('.wizard-step').forEach(s => {
|
||||
s.classList.remove('active');
|
||||
const stepNum = parseInt(s.dataset.step);
|
||||
if (stepNum < step) s.classList.add('completed');
|
||||
else s.classList.remove('completed');
|
||||
});
|
||||
document.querySelector(`.wizard-step[data-step="${step}"]`).classList.add('active');
|
||||
document.querySelectorAll('.wizard-content').forEach(c => c.classList.remove('active'));
|
||||
document.getElementById(`step-content-${step}`).classList.add('active');
|
||||
}
|
||||
document.querySelectorAll('.wizard-step').forEach(step => {
|
||||
step.addEventListener('click', () => goToStep(parseInt(step.dataset.step)));
|
||||
});
|
||||
document.getElementById('nav-avatar').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'),
|
||||
avatar: document.getElementById('settings-avatar'),
|
||||
navAvatar: 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'),
|
||||
};
|
||||
let csrfToken = '';
|
||||
let currentPlan = '';
|
||||
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; el.navAvatar.textContent = initial; }
|
||||
function renderAccount(account) {
|
||||
const email = account?.email || '';
|
||||
el.email.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: 'Update your billing 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('Saved 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>';
|
||||
return '<i class="fa-regular fa-credit-card"></i>';
|
||||
}
|
||||
function renderPaymentMethods(methods) {
|
||||
if (!el.paymentMethodsList) return;
|
||||
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 yet</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:700;">${m.brand || 'Card'} ${i === 0 ? '<span class="badge success">Default</span>' : ''}</div><div style="font-size:13px;color:var(--muted);">•••• ${m.last4 || '••••'}</div></div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
async function loadPaymentMethods() {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
el.invoicesList.innerHTML = invoices.map(inv => {
|
||||
const date = new Date(inv.createdAt).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
|
||||
const amount = inv.amount / 100;
|
||||
return `<div class="invoice-item"><div><div style="font-weight:700;">${inv.invoiceNumber}</div><div style="font-size:13px;color:var(--muted);">${inv.description || ''} • ${date}</div></div><div class="invoice-amount">${inv.currency} ${amount.toFixed(2)}</div></div>`;
|
||||
}).join('');
|
||||
}
|
||||
async function loadInvoices() {
|
||||
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?', 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>
|
||||
443
chat/public/settings-layouts/layout-2-tabs.html
Normal file
443
chat/public/settings-layouts/layout-2-tabs.html
Normal file
@@ -0,0 +1,443 @@
|
||||
<!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: 16px; align-items: center; }
|
||||
.user-menu { position: relative; }
|
||||
.user-chip { display: flex; align-items: center; gap: 10px; padding: 8px 12px; border: 1px solid var(--border); border-radius: 10px; cursor: pointer; transition: all 0.2s; }
|
||||
.user-chip:hover { border-color: var(--green); }
|
||||
.user-avatar { width: 32px; height: 32px; border-radius: 50%; background: var(--green-light); color: var(--green); display: flex; align-items: center; justify-content: center; font-weight: 700; }
|
||||
.user-email { font-size: 13px; font-weight: 600; max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.user-dropdown { 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; }
|
||||
.user-dropdown.active { display: block; }
|
||||
.dropdown-item { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 8px; font-size: 14px; color: #475569; transition: all 0.2s; }
|
||||
.dropdown-item:hover { background: #f8fafc; color: var(--green); }
|
||||
.shell { max-width: 900px; margin: 0 auto; padding: 32px 24px; }
|
||||
.page-header { margin-bottom: 32px; }
|
||||
.page-header h1 { font-size: 28px; font-weight: 700; margin: 0 0 8px; }
|
||||
.page-header p { color: var(--muted); margin: 0; }
|
||||
.tabs { display: flex; gap: 4px; background: #fff; padding: 6px; border-radius: 12px; border: 1px solid var(--border); margin-bottom: 24px; }
|
||||
.tab { flex: 1; padding: 12px 20px; border-radius: 8px; font-weight: 600; font-size: 14px; text-align: center; cursor: pointer; transition: all 0.2s; color: var(--muted); background: transparent; border: none; }
|
||||
.tab:hover { background: #f8fafc; }
|
||||
.tab.active { background: var(--green); color: #fff; }
|
||||
.tab-content { display: none; }
|
||||
.tab-content.active { display: block; }
|
||||
.card { background: #fff; border: 1px solid var(--border); border-radius: 12px; padding: 28px; margin-bottom: 20px; }
|
||||
.card h2 { font-size: 18px; font-weight: 700; margin: 0 0 6px; }
|
||||
.card .subtitle { color: var(--muted); font-size: 14px; margin-bottom: 20px; }
|
||||
.profile-header { display: flex; align-items: center; gap: 20px; padding: 24px; background: linear-gradient(135deg, var(--green-light), #fff); border-radius: 12px; margin-bottom: 24px; }
|
||||
.profile-avatar { width: 72px; height: 72px; border-radius: 50%; background: var(--green); color: #fff; display: flex; align-items: center; justify-content: center; font-size: 28px; font-weight: 700; }
|
||||
.profile-info h3 { margin: 0 0 6px; font-size: 20px; }
|
||||
.profile-info p { margin: 0; color: var(--muted); }
|
||||
.badge { display: inline-flex; align-items: center; gap: 6px; padding: 4px 12px; border-radius: 6px; font-size: 12px; font-weight: 600; }
|
||||
.badge.success { background: #ecfdf5; color: #059669; }
|
||||
.form-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; }
|
||||
@media (max-width: 640px) { .form-grid { grid-template-columns: 1fr; } }
|
||||
.field { margin-bottom: 20px; }
|
||||
.field label { display: block; font-weight: 600; font-size: 13px; color: #475569; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.025em; }
|
||||
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; border-color: #cbd5e1; }
|
||||
.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.ghost { background: transparent; border-color: transparent; }
|
||||
.actions { display: flex; gap: 12px; justify-content: flex-end; padding-top: 20px; border-top: 1px solid var(--border); margin-top: 20px; }
|
||||
.status { font-size: 13px; color: var(--muted); }
|
||||
.status.success { color: #059669; }
|
||||
.status.error { color: #b91c1c; }
|
||||
.info-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
|
||||
.info-item { padding: 16px; background: #f8fafc; border-radius: 8px; }
|
||||
.info-label { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 6px; }
|
||||
.info-value { font-weight: 700; font-size: 16px; }
|
||||
.payment-item { display: flex; align-items: center; gap: 14px; padding: 16px; border: 1px solid var(--border); border-radius: 10px; margin-bottom: 10px; }
|
||||
.payment-item.default { border-color: var(--green); background: var(--green-light); }
|
||||
.payment-icon { width: 48px; height: 32px; background: #f1f5f9; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 24px; }
|
||||
.empty-state { text-align: center; padding: 40px 20px; color: var(--muted); }
|
||||
.empty-state i { font-size: 40px; margin-bottom: 16px; opacity: 0.5; }
|
||||
.invoice-row { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; border: 1px solid var(--border); border-radius: 8px; margin-bottom: 8px; }
|
||||
.invoice-row:hover { border-color: var(--green); }
|
||||
.invoice-amount { font-weight: 700; color: var(--green); }
|
||||
.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; }
|
||||
@media (max-width: 640px) {
|
||||
.tabs { flex-wrap: wrap; }
|
||||
.tab { flex: none; width: calc(50% - 4px); }
|
||||
.nav-links .user-email { display: none; }
|
||||
}
|
||||
</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 ghost"><i class="fa-solid fa-grid-2"></i> Apps</a>
|
||||
<div class="user-menu">
|
||||
<div class="user-chip" id="user-chip">
|
||||
<div class="user-avatar" id="nav-avatar">?</div>
|
||||
<span class="user-email" id="nav-email">Loading...</span>
|
||||
</div>
|
||||
<div class="user-dropdown" 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="page-header">
|
||||
<h1>Settings</h1>
|
||||
<p>Manage your account, billing, and preferences</p>
|
||||
</div>
|
||||
<div class="tabs">
|
||||
<button class="tab active" data-tab="account"><i class="fa-solid fa-user"></i> Account</button>
|
||||
<button class="tab" data-tab="billing"><i class="fa-solid fa-credit-card"></i> Billing</button>
|
||||
<button class="tab" data-tab="payments"><i class="fa-solid fa-wallet"></i> Payments</button>
|
||||
<button class="tab" data-tab="invoices"><i class="fa-solid fa-file-invoice"></i> Invoices</button>
|
||||
</div>
|
||||
<div class="tab-content active" id="tab-account">
|
||||
<div class="profile-header">
|
||||
<div class="profile-avatar" id="settings-avatar">?</div>
|
||||
<div class="profile-info">
|
||||
<h3 id="settings-email">Loading...</h3>
|
||||
<p><span class="badge success" id="settings-status">Active</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Account Details</h2>
|
||||
<p class="subtitle">Your subscription information</p>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<div class="info-label">Current Plan</div>
|
||||
<div class="info-value" id="settings-plan">-</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Billing Status</div>
|
||||
<div class="info-value" id="settings-billing-status">-</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Renews On</div>
|
||||
<div class="info-value" id="settings-renews">-</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Billing Email</div>
|
||||
<div class="info-value" id="billing-email-display">-</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-content" id="tab-billing">
|
||||
<div class="card">
|
||||
<h2>Subscription Settings</h2>
|
||||
<p class="subtitle">Update your plan and billing preferences</p>
|
||||
<form id="settings-form">
|
||||
<div class="form-grid">
|
||||
<div class="field">
|
||||
<label for="plan">Subscription Plan</label>
|
||||
<select id="plan"></select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="billing-cycle">Billing Cycle</label>
|
||||
<select id="billing-cycle">
|
||||
<option value="monthly">Monthly</option>
|
||||
<option value="yearly">Yearly (save 20%)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="currency">Currency</label>
|
||||
<select id="currency">
|
||||
<option value="usd">USD - US Dollar</option>
|
||||
<option value="gbp">GBP - British Pound</option>
|
||||
<option value="eur">EUR - Euro</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>
|
||||
<div class="actions">
|
||||
<button type="submit" class="btn primary">Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Danger Zone</h2>
|
||||
<p class="subtitle">Irreversible account actions</p>
|
||||
<div class="actions" style="border: none; padding-top: 0;">
|
||||
<a href="/topup" class="btn"><i class="fa-solid fa-coins"></i> Buy Top-ups</a>
|
||||
<button class="btn danger" id="cancel-plan"><i class="fa-solid fa-ban"></i> Cancel Plan</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-content" id="tab-payments">
|
||||
<div class="card">
|
||||
<h2>Payment Methods</h2>
|
||||
<p class="subtitle">Manage your saved payment methods</p>
|
||||
<div id="payment-methods-list">
|
||||
<div class="empty-state">
|
||||
<i class="fa-regular fa-credit-card"></i>
|
||||
<p>No payment methods saved yet</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-content" id="tab-invoices">
|
||||
<div class="card">
|
||||
<h2>Invoice History</h2>
|
||||
<p class="subtitle">View and download your payment invoices</p>
|
||||
<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>
|
||||
</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.querySelectorAll('.tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
document.getElementById(`tab-${tab.dataset.tab}`).classList.add('active');
|
||||
});
|
||||
});
|
||||
document.getElementById('user-chip').addEventListener('click', () => {
|
||||
document.getElementById('user-dropdown').classList.toggle('active');
|
||||
});
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('.user-menu')) document.getElementById('user-dropdown').classList.remove('active');
|
||||
});
|
||||
const el = {
|
||||
email: document.getElementById('settings-email'),
|
||||
navEmail: document.getElementById('nav-email'),
|
||||
avatar: document.getElementById('settings-avatar'),
|
||||
navAvatar: 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'),
|
||||
billingEmailDisplay: document.getElementById('billing-email-display'),
|
||||
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'),
|
||||
};
|
||||
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 'Not scheduled'; const date = new Date(iso); return isNaN(date.getTime()) ? 'Not scheduled' : date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); }
|
||||
function setAvatar(email) { const initial = (email || '?').trim().charAt(0).toUpperCase() || '?'; el.avatar.textContent = initial; el.navAvatar.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.billingEmailDisplay.textContent = 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;
|
||||
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 yet</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;">${m.brand || 'Card'} ${i === 0 ? '<span class="badge success">Default</span>' : ''}</div><div style="font-size:13px;color:var(--muted);">•••• ${m.last4 || '••••'}</div></div>
|
||||
</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;
|
||||
}
|
||||
el.invoicesList.innerHTML = invoices.map(inv => {
|
||||
const date = new Date(inv.createdAt).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
|
||||
const amount = inv.amount / 100;
|
||||
return `<div class="invoice-row"><div><div style="font-weight:600;">${inv.invoiceNumber}</div><div style="font-size:12px;color:var(--muted);">${inv.description || ''} • ${date}</div></div><div class="invoice-amount">${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? You will retain access until the end of your billing period.', 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>
|
||||
430
chat/public/settings-layouts/layout-3-accordion.html
Normal file
430
chat/public/settings-layouts/layout-3-accordion.html
Normal file
@@ -0,0 +1,430 @@
|
||||
<!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; }
|
||||
.nav-content { max-width: 800px; 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-avatar { width: 36px; height: 36px; border-radius: 50%; background: var(--green-light); color: var(--green); display: flex; align-items: center; justify-content: center; font-weight: 700; cursor: pointer; }
|
||||
.shell { max-width: 800px; margin: 0 auto; padding: 32px 24px; }
|
||||
.page-header { margin-bottom: 32px; text-align: center; }
|
||||
.page-header h1 { font-size: 32px; font-weight: 700; margin: 0 0 8px; }
|
||||
.page-header p { color: var(--muted); margin: 0; }
|
||||
.user-banner { display: flex; align-items: center; justify-content: center; gap: 16px; padding: 24px; background: linear-gradient(135deg, var(--green-light), #fff); border-radius: 16px; margin-bottom: 24px; }
|
||||
.user-banner-avatar { width: 64px; height: 64px; border-radius: 50%; background: var(--green); color: #fff; display: flex; align-items: center; justify-content: center; font-size: 26px; font-weight: 700; }
|
||||
.user-banner-info h2 { margin: 0; font-size: 20px; }
|
||||
.user-banner-info p { margin: 4px 0 0; color: var(--muted); font-size: 14px; }
|
||||
.badge { display: inline-flex; align-items: center; gap: 6px; padding: 4px 12px; border-radius: 6px; font-size: 12px; font-weight: 600; }
|
||||
.badge.success { background: #ecfdf5; color: #059669; }
|
||||
.accordion { margin-bottom: 12px; }
|
||||
.accordion-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 24px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.accordion-header:hover { border-color: var(--green); }
|
||||
.accordion-header.active { border-radius: 12px 12px 0 0; border-bottom-color: transparent; background: var(--green-light); }
|
||||
.accordion-title { display: flex; align-items: center; gap: 12px; font-weight: 600; font-size: 16px; }
|
||||
.accordion-title i { width: 20px; color: var(--green); }
|
||||
.accordion-icon { transition: transform 0.3s; color: var(--muted); }
|
||||
.accordion-header.active .accordion-icon { transform: rotate(180deg); }
|
||||
.accordion-content { display: none; padding: 24px; background: #fff; border: 1px solid var(--border); border-top: none; border-radius: 0 0 12px 12px; }
|
||||
.accordion-content.active { display: block; }
|
||||
.field { margin-bottom: 20px; }
|
||||
.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: 20px; }
|
||||
@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; border-color: #cbd5e1; }
|
||||
.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; }
|
||||
.actions { display: flex; gap: 12px; justify-content: flex-end; margin-top: 20px; padding-top: 20px; border-top: 1px solid var(--border); }
|
||||
.status { font-size: 13px; color: var(--muted); }
|
||||
.status.success { color: #059669; }
|
||||
.status.error { color: #b91c1c; }
|
||||
.info-row { display: flex; justify-content: space-between; padding: 12px 0; border-bottom: 1px solid var(--border); }
|
||||
.info-row:last-child { border-bottom: none; }
|
||||
.info-label { color: var(--muted); font-size: 14px; }
|
||||
.info-value { font-weight: 600; font-size: 14px; }
|
||||
.payment-item { display: flex; align-items: center; gap: 14px; padding: 16px; border: 1px solid var(--border); border-radius: 10px; margin-bottom: 10px; }
|
||||
.payment-item.default { border-color: var(--green); background: var(--green-light); }
|
||||
.payment-icon { width: 48px; height: 32px; background: #f1f5f9; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 24px; }
|
||||
.empty-state { text-align: center; padding: 40px 20px; color: var(--muted); }
|
||||
.empty-state i { font-size: 40px; margin-bottom: 16px; opacity: 0.5; }
|
||||
.invoice-item { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; border: 1px solid var(--border); border-radius: 8px; margin-bottom: 8px; }
|
||||
.invoice-item:hover { border-color: var(--green); }
|
||||
.invoice-amount { font-weight: 700; color: var(--green); }
|
||||
.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; }
|
||||
.dropdown { position: relative; display: inline-block; }
|
||||
.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; transition: all 0.2s; }
|
||||
.dropdown-item:hover { background: #f8fafc; color: var(--green); }
|
||||
</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-avatar" id="nav-avatar">?</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="page-header">
|
||||
<h1>Account Settings</h1>
|
||||
<p>Manage your account, billing, and preferences</p>
|
||||
</div>
|
||||
<div class="user-banner">
|
||||
<div class="user-banner-avatar" id="settings-avatar">?</div>
|
||||
<div class="user-banner-info">
|
||||
<h2 id="settings-email">Loading...</h2>
|
||||
<p><span class="badge success" id="settings-status">Active</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion">
|
||||
<div class="accordion-header active" data-accordion="account">
|
||||
<div class="accordion-title"><i class="fa-solid fa-user"></i> Account Overview</div>
|
||||
<i class="fa-solid fa-chevron-down accordion-icon"></i>
|
||||
</div>
|
||||
<div class="accordion-content active" id="accordion-account">
|
||||
<div class="info-row"><span class="info-label">Current Plan</span><span class="info-value" id="settings-plan">-</span></div>
|
||||
<div class="info-row"><span class="info-label">Billing Status</span><span class="info-value" id="settings-billing-status">-</span></div>
|
||||
<div class="info-row"><span class="info-label">Renews On</span><span class="info-value" id="settings-renews">-</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion">
|
||||
<div class="accordion-header" data-accordion="billing">
|
||||
<div class="accordion-title"><i class="fa-solid fa-credit-card"></i> Plan & Billing</div>
|
||||
<i class="fa-solid fa-chevron-down accordion-icon"></i>
|
||||
</div>
|
||||
<div class="accordion-content" id="accordion-billing">
|
||||
<form id="settings-form">
|
||||
<div class="field-row">
|
||||
<div class="field">
|
||||
<label for="plan">Subscription Plan</label>
|
||||
<select id="plan"></select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="billing-cycle">Billing Cycle</label>
|
||||
<select id="billing-cycle">
|
||||
<option value="monthly">Monthly</option>
|
||||
<option value="yearly">Yearly (save 20%)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<div class="field">
|
||||
<label for="currency">Currency</label>
|
||||
<select id="currency">
|
||||
<option value="usd">USD - US Dollar</option>
|
||||
<option value="gbp">GBP - British Pound</option>
|
||||
<option value="eur">EUR - Euro</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>
|
||||
<div class="actions">
|
||||
<button type="submit" class="btn primary">Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion">
|
||||
<div class="accordion-header" data-accordion="payments">
|
||||
<div class="accordion-title"><i class="fa-solid fa-wallet"></i> Payment Methods</div>
|
||||
<i class="fa-solid fa-chevron-down accordion-icon"></i>
|
||||
</div>
|
||||
<div class="accordion-content" id="accordion-payments">
|
||||
<div id="payment-methods-list">
|
||||
<div class="empty-state"><i class="fa-regular fa-credit-card"></i><p>No payment methods saved yet</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion">
|
||||
<div class="accordion-header" data-accordion="invoices">
|
||||
<div class="accordion-title"><i class="fa-solid fa-file-invoice"></i> Invoices</div>
|
||||
<i class="fa-solid fa-chevron-down accordion-icon"></i>
|
||||
</div>
|
||||
<div class="accordion-content" id="accordion-invoices">
|
||||
<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>
|
||||
<div class="accordion">
|
||||
<div class="accordion-header" data-accordion="actions">
|
||||
<div class="accordion-title"><i class="fa-solid fa-sliders"></i> Billing Actions</div>
|
||||
<i class="fa-solid fa-chevron-down accordion-icon"></i>
|
||||
</div>
|
||||
<div class="accordion-content" id="accordion-actions">
|
||||
<div class="actions" style="border:none;padding-top:0;">
|
||||
<a href="/topup" class="btn"><i class="fa-solid fa-coins"></i> Buy Top-ups</a>
|
||||
<button class="btn danger" 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.querySelectorAll('.accordion-header').forEach(header => {
|
||||
header.addEventListener('click', () => {
|
||||
const id = header.dataset.accordion;
|
||||
const content = document.getElementById(`accordion-${id}`);
|
||||
const isActive = header.classList.contains('active');
|
||||
document.querySelectorAll('.accordion-header').forEach(h => h.classList.remove('active'));
|
||||
document.querySelectorAll('.accordion-content').forEach(c => c.classList.remove('active'));
|
||||
if (!isActive) {
|
||||
header.classList.add('active');
|
||||
content.classList.add('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
document.getElementById('nav-avatar').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'),
|
||||
avatar: document.getElementById('settings-avatar'),
|
||||
navAvatar: 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'),
|
||||
};
|
||||
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 'Not scheduled'; const date = new Date(iso); return isNaN(date.getTime()) ? 'Not scheduled' : date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); }
|
||||
function setAvatar(email) { const initial = (email || '?').trim().charAt(0).toUpperCase() || '?'; el.avatar.textContent = initial; el.navAvatar.textContent = initial; }
|
||||
function renderAccount(account) {
|
||||
const email = account?.email || '';
|
||||
el.email.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;
|
||||
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 yet</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;">${m.brand || 'Card'} ${i === 0 ? '<span class="badge success">Default</span>' : ''}</div><div style="font-size:13px;color:var(--muted);">•••• ${m.last4 || '••••'}</div></div>
|
||||
</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;
|
||||
}
|
||||
el.invoicesList.innerHTML = invoices.map(inv => {
|
||||
const date = new Date(inv.createdAt).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
|
||||
const amount = inv.amount / 100;
|
||||
return `<div class="invoice-item"><div><div style="font-weight:600;">${inv.invoiceNumber}</div><div style="font-size:12px;color:var(--muted);">${date}</div></div><div class="invoice-amount">${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? You will retain access until the end of your billing period.', 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>
|
||||
404
chat/public/settings-layouts/layout-4-dashboard.html
Normal file
404
chat/public/settings-layouts/layout-4-dashboard.html
Normal file
@@ -0,0 +1,404 @@
|
||||
<!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>
|
||||
395
chat/public/settings-layouts/layout-5-centered.html
Normal file
395
chat/public/settings-layouts/layout-5-centered.html
Normal file
@@ -0,0 +1,395 @@
|
||||
<!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; }
|
||||
.top-bar { background: #fff; border-bottom: 1px solid var(--border); padding: 12px 24px; position: sticky; top: 0; z-index: 100; display: flex; justify-content: space-between; align-items: center; }
|
||||
.brand { display: flex; align-items: center; gap: 10px; }
|
||||
.brand img { width: 28px; height: 28px; border-radius: 6px; }
|
||||
.brand span { font-weight: 600; font-size: 16px; }
|
||||
.nav-items { display: flex; gap: 8px; align-items: center; }
|
||||
.nav-item { padding: 8px 14px; border-radius: 6px; font-size: 13px; font-weight: 500; color: var(--muted); transition: all 0.2s; }
|
||||
.nav-item:hover { background: #f1f5f9; color: #1e293b; }
|
||||
.nav-item.active { background: var(--green-light); color: var(--green); }
|
||||
.user-menu { position: relative; }
|
||||
.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; cursor: pointer; }
|
||||
.dropdown { position: absolute; top: 100%; right: 0; margin-top: 8px; background: #fff; border: 1px solid var(--border); border-radius: 10px; box-shadow: 0 10px 25px rgba(0,0,0,0.1); min-width: 160px; display: none; padding: 6px; z-index: 1000; }
|
||||
.dropdown.active { display: block; }
|
||||
.dropdown-item { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 6px; font-size: 13px; color: #475569; }
|
||||
.dropdown-item:hover { background: #f8fafc; color: var(--green); }
|
||||
.shell { max-width: 600px; margin: 0 auto; padding: 40px 24px; }
|
||||
.centered-header { text-align: center; margin-bottom: 40px; }
|
||||
.centered-avatar { width: 80px; height: 80px; border-radius: 50%; background: var(--green); color: #fff; display: flex; align-items: center; justify-content: center; font-size: 32px; font-weight: 700; margin: 0 auto 20px; }
|
||||
.centered-header h1 { font-size: 24px; font-weight: 700; margin: 0 0 8px; }
|
||||
.centered-header p { color: var(--muted); margin: 0; font-size: 15px; }
|
||||
.badge { display: inline-flex; align-items: center; gap: 6px; padding: 4px 12px; border-radius: 6px; font-size: 12px; font-weight: 600; }
|
||||
.badge.success { background: #ecfdf5; color: #059669; }
|
||||
.section { background: #fff; border: 1px solid var(--border); border-radius: 12px; margin-bottom: 20px; overflow: hidden; }
|
||||
.section-header { padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; background: #fafafa; }
|
||||
.section-title { font-size: 14px; font-weight: 600; display: flex; align-items: center; gap: 10px; }
|
||||
.section-title i { color: var(--green); }
|
||||
.section-body { padding: 20px; }
|
||||
.field { margin-bottom: 16px; }
|
||||
.field:last-child { margin-bottom: 0; }
|
||||
.field label { display: block; font-weight: 600; font-size: 12px; color: #475569; margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
.field-row { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
|
||||
@media (max-width: 480px) { .field-row { grid-template-columns: 1fr; } }
|
||||
input, select { width: 100%; padding: 10px 14px; 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 10px center; background-size: 14px; padding-right: 36px; }
|
||||
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; padding: 10px 18px; 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%; }
|
||||
.btn.sm { padding: 8px 14px; font-size: 13px; }
|
||||
.status { font-size: 12px; color: var(--muted); margin-top: 12px; text-align: center; }
|
||||
.status.success { color: #059669; }
|
||||
.status.error { color: #b91c1c; }
|
||||
.info-row { display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid var(--border); }
|
||||
.info-row:last-child { border-bottom: none; }
|
||||
.info-label { color: var(--muted); font-size: 13px; }
|
||||
.info-value { font-weight: 600; font-size: 13px; }
|
||||
.payment-item { display: flex; align-items: center; gap: 12px; padding: 12px; border: 1px solid var(--border); border-radius: 8px; margin-bottom: 8px; }
|
||||
.payment-item.default { border-color: var(--green); background: var(--green-light); }
|
||||
.payment-icon { width: 36px; height: 24px; background: #f1f5f9; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 18px; }
|
||||
.empty-state { text-align: center; padding: 24px; color: var(--muted); font-size: 13px; }
|
||||
.empty-state i { font-size: 28px; margin-bottom: 10px; opacity: 0.5; }
|
||||
.invoice-item { display: flex; align-items: center; justify-content: space-between; padding: 10px 12px; border: 1px solid var(--border); border-radius: 6px; margin-bottom: 6px; }
|
||||
.invoice-amount { font-weight: 600; color: var(--green); font-size: 13px; }
|
||||
.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: 400px; width: 90%; }
|
||||
.modal-header { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
|
||||
.modal-icon { width: 36px; height: 36px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 18px; }
|
||||
.modal-icon.warning { background: #fff7ed; color: #ea580c; }
|
||||
.modal-icon.info { background: #eff6ff; color: #2563eb; }
|
||||
.modal-title { font-size: 16px; font-weight: 700; }
|
||||
.modal-body { color: #64748b; font-size: 14px; line-height: 1.6; margin-bottom: 20px; }
|
||||
.modal-actions { display: flex; gap: 10px; justify-content: flex-end; }
|
||||
@media (max-width: 480px) {
|
||||
.shell { padding: 24px 16px; }
|
||||
.centered-avatar { width: 64px; height: 64px; font-size: 26px; }
|
||||
.section-body { padding: 16px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="top-bar">
|
||||
<a href="/" class="brand">
|
||||
<img src="/assets/Plugin.png" alt="Plugin Compass">
|
||||
<span>Plugin Compass</span>
|
||||
</a>
|
||||
<nav class="nav-items">
|
||||
<a href="/apps" class="nav-item"><i class="fa-solid fa-grid-2"></i> Apps</a>
|
||||
<a href="/topup" class="nav-item"><i class="fa-solid fa-coins"></i> Tokens</a>
|
||||
<a href="/settings" class="nav-item active"><i class="fa-solid fa-gear"></i> Settings</a>
|
||||
<div class="user-menu">
|
||||
<div class="user-avatar" id="nav-avatar">?</div>
|
||||
<div class="dropdown" 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="#" class="dropdown-item" id="logout-link"><i class="fa-solid fa-arrow-right-from-bracket"></i> Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<main class="shell">
|
||||
<div class="centered-header">
|
||||
<div class="centered-avatar" id="settings-avatar">?</div>
|
||||
<h1 id="settings-email">Loading...</h1>
|
||||
<p><span class="badge success" id="settings-status">Active</span></p>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<div class="section-title"><i class="fa-solid fa-user"></i> Account</div>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="info-row">
|
||||
<span class="info-label">Plan</span>
|
||||
<span class="info-value" id="settings-plan">-</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Status</span>
|
||||
<span class="info-value" id="settings-billing-status">-</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Renews</span>
|
||||
<span class="info-value" id="settings-renews">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<div class="section-title"><i class="fa-solid fa-credit-card"></i> Subscription</div>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<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">Billing</label>
|
||||
<select id="billing-cycle">
|
||||
<option value="monthly">Monthly</option>
|
||||
<option value="yearly">Yearly</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">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</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<div class="section-title"><i class="fa-solid fa-wallet"></i> Payments</div>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<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 class="section">
|
||||
<div class="section-header">
|
||||
<div class="section-title"><i class="fa-solid fa-file-invoice"></i> Invoices</div>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<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>
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<div class="section-title"><i class="fa-solid fa-bolt"></i> Actions</div>
|
||||
</div>
|
||||
<div class="section-body" style="display:flex;gap:10px;">
|
||||
<a href="/topup" class="btn sm"><i class="fa-solid fa-coins"></i> Top-up</a>
|
||||
<button class="btn danger sm" id="cancel-plan"><i class="fa-solid fa-ban"></i> Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<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 sm" id="modal-cancel">Cancel</button>
|
||||
<button class="btn primary sm" id="modal-confirm">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('nav-avatar').addEventListener('click', () => {
|
||||
document.getElementById('user-dropdown').classList.toggle('active');
|
||||
});
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('.user-menu')) document.getElementById('user-dropdown').classList.remove('active');
|
||||
});
|
||||
const el = {
|
||||
email: document.getElementById('settings-email'),
|
||||
avatar: document.getElementById('settings-avatar'),
|
||||
navAvatar: 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'),
|
||||
};
|
||||
let csrfToken = '';
|
||||
let currentPlan = '';
|
||||
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; el.navAvatar.textContent = initial; }
|
||||
function renderAccount(account) {
|
||||
const email = account?.email || '';
|
||||
el.email.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: '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('Saved 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>';
|
||||
return '<i class="fa-regular fa-credit-card"></i>';
|
||||
}
|
||||
function renderPaymentMethods(methods) {
|
||||
if (!el.paymentMethodsList) return;
|
||||
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;font-size:13px;"><span style="font-weight:600;">${m.brand || 'Card'}</span> •••• ${m.last4 || '••••'}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
async function loadPaymentMethods() {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
el.invoicesList.innerHTML = invoices.slice(0, 4).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:12px;">${inv.invoiceNumber}</div><div style="font-size:11px;color:var(--muted);">${date}</div></div><div class="invoice-amount">${inv.currency} ${amount.toFixed(2)}</div></div>`;
|
||||
}).join('');
|
||||
}
|
||||
async function loadInvoices() {
|
||||
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>
|
||||
384
chat/public/settings-layouts/layout-6-cards.html
Normal file
384
chat/public/settings-layouts/layout-6-cards.html
Normal file
@@ -0,0 +1,384 @@
|
||||
<!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: linear-gradient(135deg, var(--green-light) 0%, #fff 50%, #f0fdf4 100%); color: #0f172a; min-height: 100vh; }
|
||||
a { color: inherit; text-decoration: none; }
|
||||
.nav { padding: 20px 32px; display: flex; justify-content: space-between; align-items: center; }
|
||||
.brand { display: flex; align-items: center; gap: 12px; }
|
||||
.brand img { width: 36px; height: 36px; border-radius: 10px; }
|
||||
.brand span { font-weight: 700; font-size: 20px; }
|
||||
.nav-actions { display: flex; gap: 12px; align-items: center; }
|
||||
.nav-btn { padding: 10px 18px; border-radius: 10px; font-weight: 600; font-size: 14px; cursor: pointer; transition: all 0.2s; border: none; background: transparent; color: #475569; }
|
||||
.nav-btn:hover { background: rgba(0,128,96,0.1); color: var(--green); }
|
||||
.nav-btn.primary { background: var(--green); color: #fff; }
|
||||
.nav-btn.primary:hover { background: var(--green-dark); }
|
||||
.shell { max-width: 1400px; margin: 0 auto; padding: 0 32px 48px; }
|
||||
.hero { text-align: center; padding: 48px 0 40px; }
|
||||
.hero-avatar { width: 100px; height: 100px; border-radius: 50%; background: linear-gradient(135deg, var(--green), var(--green-dark)); color: #fff; display: flex; align-items: center; justify-content: center; font-size: 40px; font-weight: 700; margin: 0 auto 24px; box-shadow: 0 8px 24px rgba(0,128,96,0.3); }
|
||||
.hero h1 { font-size: 36px; font-weight: 700; margin: 0 0 8px; }
|
||||
.hero p { color: var(--muted); font-size: 16px; margin: 0; }
|
||||
.badge { display: inline-flex; align-items: center; gap: 6px; padding: 6px 14px; border-radius: 8px; font-size: 13px; font-weight: 600; }
|
||||
.badge.success { background: #ecfdf5; color: #059669; }
|
||||
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; }
|
||||
@media (max-width: 1100px) { .grid { grid-template-columns: repeat(2, 1fr); } }
|
||||
@media (max-width: 700px) { .grid { grid-template-columns: 1fr; } }
|
||||
.card { background: #fff; border: 1px solid var(--border); border-radius: 16px; padding: 28px; box-shadow: 0 4px 16px rgba(0,0,0,0.04); transition: all 0.3s; }
|
||||
.card:hover { box-shadow: 0 8px 24px rgba(0,0,0,0.08); transform: translateY(-2px); }
|
||||
.card-icon { width: 48px; height: 48px; border-radius: 12px; background: var(--green-light); color: var(--green); display: flex; align-items: center; justify-content: center; font-size: 22px; margin-bottom: 16px; }
|
||||
.card h3 { font-size: 18px; font-weight: 700; margin: 0 0 8px; }
|
||||
.card .subtitle { color: var(--muted); font-size: 14px; margin-bottom: 20px; }
|
||||
.card.wide { grid-column: span 2; }
|
||||
@media (max-width: 700px) { .card.wide { grid-column: span 1; } }
|
||||
.field { margin-bottom: 16px; }
|
||||
.field label { display: block; font-weight: 600; font-size: 12px; color: #475569; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
.field-row { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
|
||||
input, select { width: 100%; padding: 12px 16px; border: 2px solid var(--border); border-radius: 10px; font-size: 14px; background: #fff; transition: all 0.2s; }
|
||||
input:focus, select:focus { outline: none; border-color: var(--green); box-shadow: 0 0 0 4px 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: 10px; font-weight: 600; font-size: 14px; cursor: pointer; transition: all 0.2s; border: 2px solid var(--border); background: #fff; color: #1e293b; }
|
||||
.btn:hover { background: #f8fafc; border-color: #cbd5e1; }
|
||||
.btn.primary { background: var(--green); color: #fff; border-color: var(--green); }
|
||||
.btn.primary:hover { background: var(--green-dark); border-color: var(--green-dark); }
|
||||
.btn.danger { color: #dc2626; border-color: #fecaca; }
|
||||
.btn.danger:hover { background: #fef2f2; }
|
||||
.actions { display: flex; gap: 12px; margin-top: 20px; }
|
||||
.status { font-size: 13px; color: var(--muted); }
|
||||
.status.success { color: #059669; }
|
||||
.status.error { color: #b91c1c; }
|
||||
.info-grid { display: grid; gap: 12px; }
|
||||
.info-item { display: flex; justify-content: space-between; padding: 14px; background: #f8fafc; border-radius: 10px; }
|
||||
.info-label { color: var(--muted); font-size: 13px; }
|
||||
.info-value { font-weight: 600; font-size: 14px; }
|
||||
.payment-item { display: flex; align-items: center; gap: 14px; padding: 14px; border: 1px solid var(--border); border-radius: 10px; margin-bottom: 10px; }
|
||||
.payment-item.default { border-color: var(--green); background: var(--green-light); }
|
||||
.payment-icon { width: 44px; height: 30px; background: #f1f5f9; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 22px; }
|
||||
.empty-state { text-align: center; padding: 32px; color: var(--muted); }
|
||||
.empty-state i { font-size: 32px; margin-bottom: 12px; opacity: 0.5; }
|
||||
.invoice-item { display: flex; align-items: center; justify-content: space-between; padding: 12px 14px; border: 1px solid var(--border); border-radius: 8px; margin-bottom: 8px; }
|
||||
.invoice-item:hover { border-color: var(--green); }
|
||||
.invoice-amount { font-weight: 700; color: var(--green); }
|
||||
.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: 20px; padding: 28px; max-width: 440px; width: 90%; }
|
||||
.modal-header { display: flex; align-items: center; gap: 14px; margin-bottom: 18px; }
|
||||
.modal-icon { width: 44px; height: 44px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 22px; }
|
||||
.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; font-size: 15px; line-height: 1.6; margin-bottom: 24px; }
|
||||
.modal-actions { display: flex; gap: 12px; justify-content: flex-end; }
|
||||
.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 30px rgba(0,0,0,0.12); min-width: 180px; display: none; padding: 8px; z-index: 1000; }
|
||||
.dropdown-menu.active { display: block; }
|
||||
.dropdown-item { display: flex; align-items: center; gap: 12px; padding: 12px 14px; border-radius: 8px; font-size: 14px; color: #475569; }
|
||||
.dropdown-item:hover { background: #f8fafc; color: var(--green); }
|
||||
@media (max-width: 700px) {
|
||||
.nav { padding: 16px; }
|
||||
.shell { padding: 0 16px 32px; }
|
||||
.hero { padding: 32px 0 24px; }
|
||||
.hero h1 { font-size: 28px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="nav">
|
||||
<a href="/" class="brand">
|
||||
<img src="/assets/Plugin.png" alt="Plugin Compass">
|
||||
<span>Plugin Compass</span>
|
||||
</a>
|
||||
<div class="nav-actions">
|
||||
<a href="/apps" class="nav-btn"><i class="fa-solid fa-grid-2"></i> Apps</a>
|
||||
<div class="dropdown">
|
||||
<button class="nav-btn primary" id="user-menu-btn">
|
||||
<i class="fa-solid fa-user"></i> <span id="nav-email">Account</span>
|
||||
</button>
|
||||
<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>
|
||||
</nav>
|
||||
<main class="shell">
|
||||
<div class="hero">
|
||||
<div class="hero-avatar" id="settings-avatar">?</div>
|
||||
<h1 id="settings-email">Loading...</h1>
|
||||
<p><span class="badge success" id="settings-status">Active Member</span></p>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<div class="card-icon"><i class="fa-solid fa-chart-pie"></i></div>
|
||||
<h3>Account Overview</h3>
|
||||
<p class="subtitle">Your subscription details</p>
|
||||
<div class="info-grid">
|
||||
<div class="info-item"><span class="info-label">Plan</span><span class="info-value" id="settings-plan">-</span></div>
|
||||
<div class="info-item"><span class="info-label">Status</span><span class="info-value" id="settings-billing-status">-</span></div>
|
||||
<div class="info-item"><span class="info-label">Renews</span><span class="info-value" id="settings-renews">-</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon"><i class="fa-solid fa-wallet"></i></div>
|
||||
<h3>Payment Methods</h3>
|
||||
<p class="subtitle">Saved cards</p>
|
||||
<div id="payment-methods-list">
|
||||
<div class="empty-state"><i class="fa-regular fa-credit-card"></i><p>No cards saved</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon"><i class="fa-solid fa-file-invoice-dollar"></i></div>
|
||||
<h3>Recent Invoices</h3>
|
||||
<p class="subtitle">Payment history</p>
|
||||
<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 wide">
|
||||
<div class="card-icon"><i class="fa-solid fa-sliders"></i></div>
|
||||
<h3>Subscription Settings</h3>
|
||||
<p class="subtitle">Manage your plan and billing preferences</p>
|
||||
<form id="settings-form">
|
||||
<div class="field-row">
|
||||
<div class="field">
|
||||
<label for="plan">Subscription Plan</label>
|
||||
<select id="plan"></select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="billing-cycle">Billing Cycle</label>
|
||||
<select id="billing-cycle">
|
||||
<option value="monthly">Monthly</option>
|
||||
<option value="yearly">Yearly (save 20%)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="currency">Currency</label>
|
||||
<select id="currency">
|
||||
<option value="usd">USD - US Dollar</option>
|
||||
<option value="gbp">GBP - British Pound</option>
|
||||
<option value="eur">EUR - Euro</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>
|
||||
<div class="actions">
|
||||
<a href="/topup" class="btn"><i class="fa-solid fa-coins"></i> Buy Top-ups</a>
|
||||
<button class="btn danger" id="cancel-plan"><i class="fa-solid fa-ban"></i> Cancel Plan</button>
|
||||
<button type="submit" class="btn primary">Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<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.getElementById('user-menu-btn').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('settings-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'),
|
||||
};
|
||||
let csrfToken = '';
|
||||
let currentPlan = '';
|
||||
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 ? email.split('@')[0] : 'Account';
|
||||
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 Member';
|
||||
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;
|
||||
if (!Array.isArray(methods) || methods.length === 0) {
|
||||
el.paymentMethodsList.innerHTML = '<div class="empty-state"><i class="fa-regular fa-credit-card"></i><p>No cards 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;">${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() {
|
||||
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;
|
||||
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, 3);
|
||||
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">${inv.currency} ${amount.toFixed(2)}</div></div>`;
|
||||
}).join('');
|
||||
}
|
||||
async function loadInvoices() {
|
||||
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>
|
||||
426
chat/public/settings-layouts/layout-7-split.html
Normal file
426
chat/public/settings-layouts/layout-7-split.html
Normal file
@@ -0,0 +1,426 @@
|
||||
<!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; display: flex; flex-direction: column; }
|
||||
a { color: inherit; text-decoration: none; }
|
||||
.header { background: linear-gradient(135deg, var(--green) 0%, var(--green-dark) 100%); color: #fff; padding: 24px; }
|
||||
.header-content { max-width: 1000px; margin: 0 auto; display: flex; justify-content: space-between; align-items: center; }
|
||||
.brand { display: flex; align-items: center; gap: 12px; }
|
||||
.brand img { width: 36px; height: 36px; border-radius: 8px; background: #fff; }
|
||||
.brand span { font-weight: 700; font-size: 20px; }
|
||||
.header-nav { display: flex; gap: 16px; align-items: center; }
|
||||
.header-link { padding: 8px 16px; border-radius: 8px; font-size: 14px; font-weight: 500; color: rgba(255,255,255,0.8); transition: all 0.2s; }
|
||||
.header-link:hover { background: rgba(255,255,255,0.15); color: #fff; }
|
||||
.header-user { display: flex; align-items: center; gap: 12px; padding: 8px 12px; background: rgba(255,255,255,0.15); border-radius: 10px; cursor: pointer; }
|
||||
.header-avatar { width: 32px; height: 32px; border-radius: 50%; background: #fff; color: var(--green); display: flex; align-items: center; justify-content: center; font-weight: 700; }
|
||||
.header-email { font-weight: 600; font-size: 14px; }
|
||||
.main { flex: 1; padding: 32px 24px; }
|
||||
.container { max-width: 1000px; margin: 0 auto; }
|
||||
.page-title { font-size: 28px; font-weight: 700; margin: 0 0 8px; }
|
||||
.page-subtitle { color: var(--muted); margin: 0 0 32px; }
|
||||
.layout { display: grid; grid-template-columns: 240px 1fr; gap: 32px; }
|
||||
@media (max-width: 800px) { .layout { grid-template-columns: 1fr; } }
|
||||
.side-nav { background: #fff; border: 1px solid var(--border); border-radius: 12px; padding: 12px; height: fit-content; position: sticky; top: 24px; }
|
||||
.side-item { display: flex; align-items: center; gap: 12px; padding: 12px 16px; border-radius: 8px; font-size: 14px; font-weight: 500; color: var(--muted); cursor: pointer; transition: all 0.2s; }
|
||||
.side-item:hover { background: #f8fafc; color: #1e293b; }
|
||||
.side-item.active { background: var(--green-light); color: var(--green); font-weight: 600; }
|
||||
.side-item i { width: 18px; text-align: center; }
|
||||
.content-section { display: none; }
|
||||
.content-section.active { display: block; }
|
||||
.card { background: #fff; border: 1px solid var(--border); border-radius: 12px; padding: 28px; margin-bottom: 24px; }
|
||||
.card-header { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; }
|
||||
.card-icon { width: 40px; height: 40px; border-radius: 10px; background: var(--green-light); color: var(--green); display: flex; align-items: center; justify-content: center; font-size: 18px; }
|
||||
.card-title { font-size: 18px; font-weight: 700; margin: 0; }
|
||||
.profile-banner { display: flex; align-items: center; gap: 20px; padding: 24px; background: linear-gradient(135deg, var(--green-light), #fff); border-radius: 12px; margin-bottom: 24px; }
|
||||
.profile-avatar { width: 64px; height: 64px; border-radius: 50%; background: var(--green); color: #fff; display: flex; align-items: center; justify-content: center; font-size: 26px; font-weight: 700; }
|
||||
.profile-info h2 { margin: 0 0 4px; font-size: 20px; }
|
||||
.profile-info p { margin: 0; color: var(--muted); }
|
||||
.badge { display: inline-flex; align-items: center; gap: 6px; padding: 4px 12px; border-radius: 6px; font-size: 12px; font-weight: 600; }
|
||||
.badge.success { background: #ecfdf5; color: #059669; }
|
||||
.field { margin-bottom: 20px; }
|
||||
.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: 20px; }
|
||||
@media (max-width: 600px) { .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; }
|
||||
.actions { display: flex; gap: 12px; justify-content: flex-end; margin-top: 24px; padding-top: 20px; border-top: 1px solid var(--border); }
|
||||
.status { font-size: 13px; color: var(--muted); }
|
||||
.status.success { color: #059669; }
|
||||
.status.error { color: #b91c1c; }
|
||||
.info-table { width: 100%; }
|
||||
.info-row { display: flex; justify-content: space-between; padding: 12px 0; border-bottom: 1px solid var(--border); }
|
||||
.info-row:last-child { border-bottom: none; }
|
||||
.info-label { color: var(--muted); font-size: 14px; }
|
||||
.info-value { font-weight: 600; font-size: 14px; }
|
||||
.payment-item { display: flex; align-items: center; gap: 14px; padding: 16px; border: 1px solid var(--border); border-radius: 10px; margin-bottom: 12px; }
|
||||
.payment-item.default { border-color: var(--green); background: var(--green-light); }
|
||||
.payment-icon { width: 48px; height: 32px; background: #f1f5f9; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 24px; }
|
||||
.empty-state { text-align: center; padding: 40px 20px; color: var(--muted); }
|
||||
.empty-state i { font-size: 36px; margin-bottom: 16px; opacity: 0.5; }
|
||||
.invoice-item { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; border: 1px solid var(--border); border-radius: 8px; margin-bottom: 10px; }
|
||||
.invoice-item:hover { border-color: var(--green); }
|
||||
.invoice-amount { font-weight: 700; color: var(--green); }
|
||||
.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; }
|
||||
.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.15); 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); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-content">
|
||||
<a href="/" class="brand">
|
||||
<img src="/assets/Plugin.png" alt="Plugin Compass">
|
||||
<span>Plugin Compass</span>
|
||||
</a>
|
||||
<nav class="header-nav">
|
||||
<a href="/apps" class="header-link"><i class="fa-solid fa-grid-2"></i> Apps</a>
|
||||
<div class="dropdown">
|
||||
<div class="header-user" id="user-menu">
|
||||
<div class="header-avatar" id="nav-avatar">?</div>
|
||||
<span class="header-email" id="nav-email">Account</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>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<main class="main">
|
||||
<div class="container">
|
||||
<h1 class="page-title">Settings</h1>
|
||||
<p class="page-subtitle">Manage your account, billing, and preferences</p>
|
||||
<div class="layout">
|
||||
<nav class="side-nav">
|
||||
<div class="side-item active" data-section="profile"><i class="fa-solid fa-user"></i> Profile</div>
|
||||
<div class="side-item" data-section="billing"><i class="fa-solid fa-credit-card"></i> Billing</div>
|
||||
<div class="side-item" data-section="payments"><i class="fa-solid fa-wallet"></i> Payments</div>
|
||||
<div class="side-item" data-section="invoices"><i class="fa-solid fa-file-invoice"></i> Invoices</div>
|
||||
</nav>
|
||||
<div>
|
||||
<div class="content-section active" id="section-profile">
|
||||
<div class="profile-banner">
|
||||
<div class="profile-avatar" id="settings-avatar">?</div>
|
||||
<div class="profile-info">
|
||||
<h2 id="settings-email">Loading...</h2>
|
||||
<p><span class="badge success" id="settings-status">Active</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-icon"><i class="fa-solid fa-id-card"></i></div>
|
||||
<h3 class="card-title">Account Details</h3>
|
||||
</div>
|
||||
<div class="info-table">
|
||||
<div class="info-row"><span class="info-label">Current Plan</span><span class="info-value" id="settings-plan">-</span></div>
|
||||
<div class="info-row"><span class="info-label">Billing Status</span><span class="info-value" id="settings-billing-status">-</span></div>
|
||||
<div class="info-row"><span class="info-label">Renews On</span><span class="info-value" id="settings-renews">-</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-section" id="section-billing">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-icon"><i class="fa-solid fa-sliders"></i></div>
|
||||
<h3 class="card-title">Subscription Settings</h3>
|
||||
</div>
|
||||
<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>
|
||||
<div class="actions">
|
||||
<a href="/topup" class="btn"><i class="fa-solid fa-coins"></i> Top-ups</a>
|
||||
<button class="btn danger" id="cancel-plan"><i class="fa-solid fa-ban"></i> Cancel</button>
|
||||
<button type="submit" class="btn primary">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-section" id="section-payments">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-icon"><i class="fa-solid fa-wallet"></i></div>
|
||||
<h3 class="card-title">Payment Methods</h3>
|
||||
</div>
|
||||
<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 class="content-section" id="section-invoices">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-icon"><i class="fa-solid fa-file-invoice"></i></div>
|
||||
<h3 class="card-title">Invoice History</h3>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<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.querySelectorAll('.side-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
document.querySelectorAll('.side-item').forEach(i => i.classList.remove('active'));
|
||||
document.querySelectorAll('.content-section').forEach(s => s.classList.remove('active'));
|
||||
item.classList.add('active');
|
||||
document.getElementById(`section-${item.dataset.section}`).classList.add('active');
|
||||
});
|
||||
});
|
||||
document.getElementById('user-menu').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('settings-avatar'),
|
||||
navAvatar: 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'),
|
||||
};
|
||||
let csrfToken = '';
|
||||
let currentPlan = '';
|
||||
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; el.navAvatar.textContent = initial; }
|
||||
function renderAccount(account) {
|
||||
const email = account?.email || '';
|
||||
el.email.textContent = email || 'Unknown';
|
||||
el.navEmail.textContent = email ? email.split('@')[0] : 'Account';
|
||||
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: 'Update 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('Saved 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;
|
||||
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;">${m.brand || 'Card'} ${i === 0 ? '<span class="badge success">Default</span>' : ''}</div><div style="font-size:13px;color:var(--muted);">•••• ${m.last4 || '••••'}</div></div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
async function loadPaymentMethods() {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
el.invoicesList.innerHTML = invoices.map(inv => {
|
||||
const date = new Date(inv.createdAt).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
|
||||
const amount = inv.amount / 100;
|
||||
return `<div class="invoice-item"><div><div style="font-weight:600;">${inv.invoiceNumber}</div><div style="font-size:12px;color:var(--muted);">${inv.description || ''} • ${date}</div></div><div class="invoice-amount">${inv.currency} ${amount.toFixed(2)}</div></div>`;
|
||||
}).join('');
|
||||
}
|
||||
async function loadInvoices() {
|
||||
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?', 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>
|
||||
405
chat/public/settings-layouts/layout-8-compact.html
Normal file
405
chat/public/settings-layouts/layout-8-compact.html
Normal file
@@ -0,0 +1,405 @@
|
||||
<!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 { background: #fff; border-bottom: 1px solid var(--border); padding: 12px 24px; display: flex; justify-content: space-between; align-items: center; position: sticky; top: 0; z-index: 100; }
|
||||
.brand { display: flex; align-items: center; gap: 10px; }
|
||||
.brand img { width: 28px; height: 28px; border-radius: 6px; }
|
||||
.brand span { font-weight: 600; font-size: 16px; }
|
||||
.nav-right { display: flex; gap: 8px; align-items: center; }
|
||||
.nav-btn { padding: 8px 14px; border-radius: 8px; font-size: 13px; font-weight: 500; border: none; background: transparent; color: var(--muted); cursor: pointer; transition: all 0.2s; }
|
||||
.nav-btn:hover { background: #f1f5f9; color: #1e293b; }
|
||||
.nav-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; cursor: pointer; }
|
||||
.dropdown { position: relative; }
|
||||
.dropdown-menu { position: absolute; top: 100%; right: 0; margin-top: 8px; background: #fff; border: 1px solid var(--border); border-radius: 10px; box-shadow: 0 8px 20px rgba(0,0,0,0.1); min-width: 160px; 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: 6px; font-size: 13px; color: #475569; }
|
||||
.dropdown-item:hover { background: #f8fafc; color: var(--green); }
|
||||
.container { max-width: 1100px; margin: 0 auto; padding: 32px 24px; }
|
||||
.page-header { display: flex; align-items: center; gap: 24px; margin-bottom: 32px; padding-bottom: 24px; border-bottom: 1px solid var(--border); }
|
||||
.header-avatar { width: 72px; height: 72px; border-radius: 16px; background: linear-gradient(135deg, var(--green), var(--green-dark)); color: #fff; display: flex; align-items: center; justify-content: center; font-size: 28px; font-weight: 700; }
|
||||
.header-text h1 { font-size: 24px; font-weight: 700; margin: 0 0 6px; }
|
||||
.header-text p { color: var(--muted); margin: 0; font-size: 14px; }
|
||||
.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; }
|
||||
.settings-grid { display: grid; grid-template-columns: 1fr 320px; gap: 24px; }
|
||||
@media (max-width: 900px) { .settings-grid { grid-template-columns: 1fr; } }
|
||||
.main-content { }
|
||||
.sidebar { }
|
||||
.card { background: #fff; border: 1px solid var(--border); border-radius: 12px; padding: 24px; margin-bottom: 20px; }
|
||||
.card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
|
||||
.card-title { font-size: 16px; font-weight: 700; margin: 0; }
|
||||
.field { margin-bottom: 16px; }
|
||||
.field:last-child { margin-bottom: 0; }
|
||||
.field label { display: block; font-weight: 600; font-size: 12px; color: #475569; margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
.field-row { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
|
||||
@media (max-width: 600px) { .field-row { grid-template-columns: 1fr; } }
|
||||
input, select { width: 100%; padding: 10px 14px; 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 10px center; background-size: 14px; padding-right: 36px; }
|
||||
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; padding: 10px 18px; 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%; }
|
||||
.actions { display: flex; gap: 12px; margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--border); }
|
||||
.status { font-size: 12px; color: var(--muted); margin-bottom: 12px; }
|
||||
.status.success { color: #059669; }
|
||||
.status.error { color: #b91c1c; }
|
||||
.info-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
|
||||
.info-card { padding: 16px; background: #f8fafc; border-radius: 10px; }
|
||||
.info-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 4px; }
|
||||
.info-value { font-size: 16px; font-weight: 700; }
|
||||
.payment-item { display: flex; align-items: center; gap: 12px; padding: 12px; border: 1px solid var(--border); border-radius: 8px; margin-bottom: 8px; }
|
||||
.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: 18px; }
|
||||
.empty-state { text-align: center; padding: 20px; color: var(--muted); font-size: 13px; }
|
||||
.empty-state i { font-size: 24px; margin-bottom: 8px; opacity: 0.5; }
|
||||
.invoice-item { display: flex; align-items: center; justify-content: space-between; padding: 10px 12px; border: 1px solid var(--border); border-radius: 6px; margin-bottom: 6px; }
|
||||
.invoice-amount { font-weight: 600; color: var(--green); font-size: 13px; }
|
||||
.sidebar-card { background: #fff; border: 1px solid var(--border); border-radius: 12px; padding: 20px; margin-bottom: 16px; }
|
||||
.sidebar-card h4 { font-size: 13px; font-weight: 700; margin: 0 0 12px; color: #475569; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
.quick-action { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 8px; font-size: 13px; color: #475569; margin-bottom: 4px; transition: all 0.2s; cursor: pointer; }
|
||||
.quick-action:hover { background: #f8fafc; color: var(--green); }
|
||||
.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: 400px; width: 90%; }
|
||||
.modal-header { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
|
||||
.modal-icon { width: 36px; height: 36px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 18px; }
|
||||
.modal-icon.warning { background: #fff7ed; color: #ea580c; }
|
||||
.modal-icon.info { background: #eff6ff; color: #2563eb; }
|
||||
.modal-title { font-size: 16px; font-weight: 700; }
|
||||
.modal-body { color: #64748b; font-size: 14px; line-height: 1.6; margin-bottom: 20px; }
|
||||
.modal-actions { display: flex; gap: 10px; justify-content: flex-end; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="nav">
|
||||
<a href="/" class="brand">
|
||||
<img src="/assets/Plugin.png" alt="Plugin Compass">
|
||||
<span>Plugin Compass</span>
|
||||
</a>
|
||||
<div class="nav-right">
|
||||
<a href="/apps" class="nav-btn"><i class="fa-solid fa-grid-2"></i> Apps</a>
|
||||
<div class="dropdown">
|
||||
<div class="nav-avatar" id="nav-avatar">?</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>
|
||||
</nav>
|
||||
<div class="container">
|
||||
<div class="page-header">
|
||||
<div class="header-avatar" id="settings-avatar">?</div>
|
||||
<div class="header-text">
|
||||
<h1 id="settings-email">Loading...</h1>
|
||||
<p><span class="badge success" id="settings-status">Active</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-grid">
|
||||
<div class="main-content">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Account Overview</h3>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="info-card">
|
||||
<div class="info-label">Plan</div>
|
||||
<div class="info-value" id="settings-plan">-</div>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<div class="info-label">Status</div>
|
||||
<div class="info-value" id="settings-billing-status">-</div>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<div class="info-label">Renews</div>
|
||||
<div class="info-value" id="settings-renews">-</div>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<div class="info-label">Currency</div>
|
||||
<div class="info-value" id="settings-currency">USD</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Subscription Settings</h3>
|
||||
</div>
|
||||
<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">Billing Cycle</label>
|
||||
<select id="billing-cycle">
|
||||
<option value="monthly">Monthly</option>
|
||||
<option value="yearly">Yearly</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>
|
||||
<div class="actions">
|
||||
<button type="submit" class="btn primary">Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Payment Methods</h3>
|
||||
</div>
|
||||
<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 class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Recent Invoices</h3>
|
||||
</div>
|
||||
<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>
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-card">
|
||||
<h4>Quick Actions</h4>
|
||||
<a href="/topup" class="quick-action"><i class="fa-solid fa-coins"></i> Buy Top-ups</a>
|
||||
<a href="/apps" class="quick-action"><i class="fa-solid fa-grid-2"></i> View Apps</a>
|
||||
<a href="/feature-requests" class="quick-action"><i class="fa-solid fa-star"></i> Feature Requests</a>
|
||||
</div>
|
||||
<div class="sidebar-card">
|
||||
<h4>Danger Zone</h4>
|
||||
<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.getElementById('nav-avatar').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'),
|
||||
avatar: document.getElementById('settings-avatar'),
|
||||
navAvatar: document.getElementById('nav-avatar'),
|
||||
plan: document.getElementById('settings-plan'),
|
||||
billingStatus: document.getElementById('settings-billing-status'),
|
||||
renews: document.getElementById('settings-renews'),
|
||||
currencyDisplay: document.getElementById('settings-currency'),
|
||||
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'),
|
||||
};
|
||||
let csrfToken = '';
|
||||
let currentPlan = '';
|
||||
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' }); }
|
||||
function setAvatar(email) { const initial = (email || '?').trim().charAt(0).toUpperCase() || '?'; el.avatar.textContent = initial; el.navAvatar.textContent = initial; }
|
||||
function renderAccount(account) {
|
||||
const email = account?.email || '';
|
||||
el.email.textContent = email || 'Unknown';
|
||||
setAvatar(email);
|
||||
el.plan.textContent = account?.plan || 'hobby';
|
||||
el.billingStatus.textContent = account?.billingStatus || 'active';
|
||||
el.renews.textContent = formatDate(account?.subscriptionRenewsAt);
|
||||
el.currencyDisplay.textContent = (account?.subscriptionCurrency || 'usd').toUpperCase();
|
||||
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: 'Update 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('Saved', '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>';
|
||||
return '<i class="fa-regular fa-credit-card"></i>';
|
||||
}
|
||||
function renderPaymentMethods(methods) {
|
||||
if (!el.paymentMethodsList) return;
|
||||
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;font-size:13px;"><span style="font-weight:600;">${m.brand || 'Card'}</span> •••• ${m.last4 || '••••'}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
async function loadPaymentMethods() {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
el.invoicesList.innerHTML = invoices.slice(0, 5).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:12px;">${inv.invoiceNumber}</div><div style="font-size:11px;color:var(--muted);">${date}</div></div><div class="invoice-amount">${inv.currency} ${amount.toFixed(2)}</div></div>`;
|
||||
}).join('');
|
||||
}
|
||||
async function loadInvoices() {
|
||||
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?', 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?', 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('Canceled', 'success');
|
||||
} catch (err) { setStatus(err.message, 'error'); }
|
||||
}});
|
||||
});
|
||||
el.form.addEventListener('submit', saveAccount);
|
||||
loadCsrfToken().then(() => { loadAccount(); loadPlans(); loadPaymentMethods(); loadInvoices(); });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
401
chat/public/settings-layouts/layout-9-modern.html
Normal file
401
chat/public/settings-layouts/layout-9-modern.html
Normal file
@@ -0,0 +1,401 @@
|
||||
<!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 { background: #fff; border-bottom: 1px solid var(--border); padding: 16px 32px; 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: 700; font-size: 18px; }
|
||||
.nav-links { display: flex; gap: 24px; align-items: center; }
|
||||
.nav-link { font-size: 14px; font-weight: 500; color: var(--muted); transition: color 0.2s; }
|
||||
.nav-link:hover { color: var(--green); }
|
||||
.nav-link.active { color: var(--green); }
|
||||
.user-menu { position: relative; }
|
||||
.user-btn { display: flex; align-items: center; gap: 10px; padding: 8px 12px; border: 1px solid var(--border); border-radius: 10px; cursor: pointer; }
|
||||
.user-btn:hover { border-color: var(--green); }
|
||||
.user-avatar { width: 28px; height: 28px; border-radius: 50%; background: var(--green); color: #fff; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 12px; }
|
||||
.dropdown { position: absolute; top: 100%; right: 0; margin-top: 8px; background: #fff; border: 1px solid var(--border); border-radius: 10px; box-shadow: 0 8px 20px rgba(0,0,0,0.1); min-width: 160px; display: none; padding: 6px; z-index: 1000; }
|
||||
.dropdown.active { display: block; }
|
||||
.dropdown-item { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 6px; font-size: 13px; color: #475569; }
|
||||
.dropdown-item:hover { background: #f8fafc; color: var(--green); }
|
||||
.shell { max-width: 1100px; margin: 0 auto; padding: 40px 32px; }
|
||||
.header { display: flex; align-items: center; gap: 16px; margin-bottom: 32px; }
|
||||
.step-indicator { display: flex; align-items: center; gap: 16px; }
|
||||
.step-dot { width: 40px; height: 40px; border-radius: 50%; background: var(--green); color: #fff; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 16px; flex-shrink: 0; }
|
||||
.step-dot.inactive { background: #e5e7eb; color: #9ca3af; }
|
||||
.step-line { flex: 1; height: 2px; background: var(--border); margin: 0 16px; }
|
||||
.step-line.active { background: var(--green); }
|
||||
.page-content { background: #fff; border: 1px solid var(--border); border-radius: 16px; overflow: hidden; }
|
||||
.page-header { padding: 24px; background: linear-gradient(135deg, var(--green-light), #fff); border-bottom: 1px solid var(--border); }
|
||||
.page-header h1 { font-size: 22px; font-weight: 700; margin: 0; }
|
||||
.page-header p { color: var(--muted); margin: 0; font-size: 14px; }
|
||||
.page-body { padding: 32px; }
|
||||
.page-footer { padding: 20px; border-top: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; background: #fafafa; }
|
||||
.profile-section { display: flex; align-items: center; gap: 20px; padding: 24px; background: #f8fafc; border-radius: 12px; margin-bottom: 24px; }
|
||||
.profile-avatar { width: 64px; height: 64px; border-radius: 50%; background: var(--green); color: #fff; display: flex; align-items: center; justify-content: center; font-size: 26px; font-weight: 700; }
|
||||
.profile-info h2 { margin: 0; font-size: 18px; }
|
||||
.profile-info p { margin: 0; color: var(--muted); font-size: 14px; }
|
||||
.badge { display: inline-flex; align-items: center; gap: 6px; padding: 4px 12px; border-radius: 6px; font-size: 12px; font-weight: 600; }
|
||||
.badge.success { background: #ecfdf5; color: #059669; }
|
||||
.field { margin-bottom: 20px; }
|
||||
.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: 20px; }
|
||||
@media (max-width: 600px) { .field-row { grid-template-columns: 1fr; } }
|
||||
input, select { width: 100%; padding: 12px 16px; border: 1px solid var(--border); border-radius: 10px; font-size: 14px; background: #fff; transition: all 0.2s; }
|
||||
input:focus, select:focus { outline: none; border-color: var(--green); box-shadow: 0 0 0 4px 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 24px; border-radius: 10px; 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.ghost { background: transparent; border-color: var(--border); }
|
||||
.btn.danger { color: #dc2626; border-color: #fecaca; }
|
||||
.btn.danger:hover { background: #fef2f2; }
|
||||
.status { font-size: 13px; color: var(--muted); }
|
||||
.status.success { color: #059669; }
|
||||
.status.error { color: #b91c1c; }
|
||||
.info-table { width: 100%; }
|
||||
.info-row { display: flex; justify-content: space-between; padding: 14px 0; border-bottom: 1px solid var(--border); }
|
||||
.info-row:last-child { border-bottom: none; }
|
||||
.info-label { color: var(--muted); font-size: 14px; }
|
||||
.info-value { font-weight: 600; font-size: 14px; }
|
||||
.payment-item { display: flex; align-items: center; gap: 14px; padding: 16px; border: 1px solid var(--border); border-radius: 10px; margin-bottom: 12px; }
|
||||
.payment-item.default { border-color: var(--green); background: var(--green-light); }
|
||||
.payment-icon { width: 48px; height: 32px; background: #f1f5f9; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 24px; }
|
||||
.empty-state { text-align: center; padding: 48px 24px; color: var(--muted); }
|
||||
.empty-state i { font-size: 40px; margin-bottom: 16px; opacity: 0.5; }
|
||||
.invoice-item { display: flex; align-items: center; justify-content: space-between; padding: 16px; border: 1px solid var(--border); border-radius: 10px; margin-bottom: 10px; }
|
||||
.invoice-item:hover { border-color: var(--green); }
|
||||
.invoice-amount { font-weight: 700; color: var(--green); font-size: 16px; }
|
||||
.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; font-size: 15px; line-height: 1.6; margin-bottom: 24px; }
|
||||
.modal-actions { display: flex; gap: 12px; justify-content: flex-end; }
|
||||
.section-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 24px; }
|
||||
@media (max-width: 700px) { .section-grid { grid-template-columns: 1fr; } }
|
||||
.section-card { padding: 24px; border: 1px solid var(--border); border-radius: 12px; }
|
||||
.section-card h3 { font-size: 16px; font-weight: 700; margin: 0 0 16px; display: flex; align-items: center; gap: 10px; }
|
||||
.section-card h3 i { color: var(--green); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="nav">
|
||||
<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="nav-link">Apps</a>
|
||||
<a href="/topup" class="nav-link">Tokens</a>
|
||||
<a href="/settings" class="nav-link active">Settings</a>
|
||||
<div class="user-menu">
|
||||
<div class="user-btn">
|
||||
<div class="user-avatar" id="nav-avatar">?</div>
|
||||
</div>
|
||||
<div class="dropdown" 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="#" class="dropdown-item" id="logout-link"><i class="fa-solid fa-arrow-right-from-bracket"></i> Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="shell">
|
||||
<div class="header">
|
||||
<div class="step-indicator">
|
||||
<div class="step-dot" id="step-1">1</div>
|
||||
<div class="step-line active" id="line-1"></div>
|
||||
<div class="step-dot" id="step-2">2</div>
|
||||
<div class="step-line" id="line-2"></div>
|
||||
<div class="step-dot inactive" id="step-3">3</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-content">
|
||||
<div class="page-header">
|
||||
<h1>Account Settings</h1>
|
||||
<p>Manage your account, billing, and preferences</p>
|
||||
</div>
|
||||
<div class="page-body">
|
||||
<div class="profile-section">
|
||||
<div class="profile-avatar" id="settings-avatar">?</div>
|
||||
<div class="profile-info">
|
||||
<h2 id="settings-email">Loading...</h2>
|
||||
<p><span class="badge success" id="settings-status">Active</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-grid">
|
||||
<div class="section-card">
|
||||
<h3><i class="fa-solid fa-user"></i> Account Overview</h3>
|
||||
<div class="info-table">
|
||||
<div class="info-row"><span class="info-label">Plan</span><span class="info-value" id="settings-plan">-</span></div>
|
||||
<div class="info-row"><span class="info-label">Status</span><span class="info-value" id="settings-billing-status">-</span></div>
|
||||
<div class="info-row"><span class="info-label">Renews</span><span class="info-value" id="settings-renews">-</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-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</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-card" style="margin-top:24px;">
|
||||
<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">Billing Cycle</label>
|
||||
<select id="billing-cycle">
|
||||
<option value="monthly">Monthly</option>
|
||||
<option value="yearly">Yearly</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>
|
||||
</form>
|
||||
</div>
|
||||
<div class="section-card" style="margin-top:24px;">
|
||||
<h3><i class="fa-solid fa-file-invoice"></i> 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>
|
||||
<div class="page-footer">
|
||||
<button class="btn danger" id="cancel-plan"><i class="fa-solid fa-ban"></i> Cancel Plan</button>
|
||||
<div style="display:flex;gap:12px;">
|
||||
<a href="/topup" class="btn"><i class="fa-solid fa-coins"></i> Top-ups</a>
|
||||
<button type="submit" form="settings-form" class="btn primary">Save Changes</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-btn').addEventListener('click', () => {
|
||||
document.getElementById('user-dropdown').classList.toggle('active');
|
||||
});
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('.user-menu')) document.getElementById('user-dropdown').classList.remove('active');
|
||||
});
|
||||
const el = {
|
||||
email: document.getElementById('settings-email'),
|
||||
avatar: document.getElementById('settings-avatar'),
|
||||
navAvatar: 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'),
|
||||
};
|
||||
let csrfToken = '';
|
||||
let currentPlan = '';
|
||||
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; el.navAvatar.textContent = initial; }
|
||||
function renderAccount(account) {
|
||||
const email = account?.email || '';
|
||||
el.email.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: 'Update 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('Saved', '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>';
|
||||
return '<i class="fa-regular fa-credit-card"></i>';
|
||||
}
|
||||
function renderPaymentMethods(methods) {
|
||||
if (!el.paymentMethodsList) return;
|
||||
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</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:700;">${m.brand || 'Card'}</div><div style="font-size:13px;color:var(--muted);">•••• ${m.last4 || '••••'}</div></div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
async function loadPaymentMethods() {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
el.invoicesList.innerHTML = invoices.map(inv => {
|
||||
const date = new Date(inv.createdAt).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
|
||||
const amount = inv.amount / 100;
|
||||
return `<div class="invoice-item"><div><div style="font-weight:700;">${inv.invoiceNumber}</div><div style="font-size:13px;color:var(--muted);">${date}</div></div><div class="invoice-amount">${inv.currency} ${amount.toFixed(2)}</div></div>`;
|
||||
}).join('');
|
||||
}
|
||||
async function loadInvoices() {
|
||||
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?', 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?', 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('Canceled', 'success');
|
||||
} catch (err) { setStatus(err.message, 'error'); }
|
||||
}});
|
||||
});
|
||||
el.form.addEventListener('submit', saveAccount);
|
||||
loadCsrfToken().then(() => { loadAccount(); loadPlans(); loadPaymentMethods(); loadInvoices(); });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
236
chat/server.js
236
chat/server.js
@@ -12,7 +12,7 @@ const archiver = require('archiver');
|
||||
const AdmZip = require('adm-zip');
|
||||
const bcrypt = require('bcrypt');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const nodemailer = require('nodemailer');
|
||||
const nodemailer = null;
|
||||
const PDFDocument = require('pdfkit');
|
||||
const security = require('./security');
|
||||
const { createExternalWpTester, getExternalTestingConfig } = require('./external-wp-testing');
|
||||
@@ -397,19 +397,8 @@ const oauthStateStore = new Map();
|
||||
const OAUTH_USER_AGENT = process.env.OAUTH_USER_AGENT || 'shopify-ai-app-builder';
|
||||
const EMAIL_VERIFICATION_TTL_MS = Number(process.env.EMAIL_VERIFICATION_TTL_MS || 24 * 60 * 60 * 1000); // 24h
|
||||
const PASSWORD_RESET_TTL_MS = Number(process.env.PASSWORD_RESET_TTL_MS || 60 * 60 * 1000); // 1h
|
||||
const SMTP_HOST = process.env.SMTP_HOST || '';
|
||||
const SMTP_PORT = Number(process.env.SMTP_PORT || 587);
|
||||
const SMTP_SECURE = process.env.SMTP_SECURE === '1' || String(process.env.SMTP_SECURE || '').toLowerCase() === 'true';
|
||||
const SMTP_USER = process.env.SMTP_USER || process.env.SMTP_USERNAME || '';
|
||||
const SMTP_PASS = (() => {
|
||||
if (process.env.SMTP_PASS_FILE) {
|
||||
try {
|
||||
return fsSync.readFileSync(process.env.SMTP_PASS_FILE, 'utf8').trim();
|
||||
} catch (_) { /* fall back to env */ }
|
||||
}
|
||||
return process.env.SMTP_PASS || process.env.SMTP_PASSWORD || '';
|
||||
})();
|
||||
const SMTP_FROM = process.env.SMTP_FROM || process.env.EMAIL_FROM || '';
|
||||
const MAILPILOT_URL = process.env.MAILPILOT_URL || 'https://emailmarketing.modelrailway3d.co.uk';
|
||||
const MAILPILOT_TOKEN = process.env.MAILPILOT_TOKEN || '';
|
||||
const POSTHOG_API_KEY = process.env.POSTHOG_API_KEY || '';
|
||||
const POSTHOG_API_HOST = process.env.POSTHOG_API_HOST || 'https://app.posthog.com';
|
||||
const DODO_WEBHOOK_KEY = process.env.DODO_PAYMENTS_WEBHOOK_KEY || process.env.DODO_WEBHOOK_KEY || '';
|
||||
@@ -1580,27 +1569,98 @@ let usersDb = []; // In-memory user database cache
|
||||
let invoicesDb = []; // In-memory invoice database cache
|
||||
let mailTransport = null;
|
||||
|
||||
// Security Rate Limiting Data Structures
|
||||
const loginAttempts = new Map(); // { email:ip: { count, windowStart, lockedUntil } }
|
||||
const adminLoginAttempts = new Map(); // { ip: { count, windowStart, lockedUntil } }
|
||||
const apiRateLimit = new Map(); // { userId: { requests, windowStart } }
|
||||
const csrfTokens = new Map(); // { token: { userId, expiresAt } }
|
||||
const affiliateSessions = new Map();
|
||||
let affiliatesDb = [];
|
||||
let trackingData = {
|
||||
visits: [],
|
||||
summary: {
|
||||
totalVisits: 0,
|
||||
uniqueVisitors: new Set(),
|
||||
referrers: {},
|
||||
pages: {},
|
||||
dailyVisits: {},
|
||||
conversions: { signup: 0, paid: 0 },
|
||||
financials: { totalRevenue: 0, dailyRevenue: {} },
|
||||
referrersToUpgrade: {},
|
||||
conversionSources: {
|
||||
signup: { home: 0, pricing: 0, other: 0 },
|
||||
paid: { home: 0, pricing: 0, other: 0 }
|
||||
function summarizeMailConfig() {
|
||||
return {
|
||||
configured: !!MAILPILOT_TOKEN,
|
||||
url: MAILPILOT_URL,
|
||||
tokenSet: !!MAILPILOT_TOKEN,
|
||||
};
|
||||
}
|
||||
|
||||
function ensureMailTransport() {
|
||||
if (mailTransport) return mailTransport;
|
||||
|
||||
if (!MAILPILOT_TOKEN) {
|
||||
console.log('');
|
||||
console.log('┌─────────────────────────────────────────────────────────────┐');
|
||||
console.log('│ 📧 MAILPILOT NOT CONFIGURED │');
|
||||
console.log('├─────────────────────────────────────────────────────────────┤');
|
||||
console.log('│ Password reset and verification emails will NOT be sent. │');
|
||||
console.log('│ To enable real emails, configure MailPilot in .env file: │');
|
||||
console.log('│ │');
|
||||
console.log('│ MAILPILOT_URL=https://emailmarketing.modelrailway3d.co.uk│');
|
||||
console.log('│ MAILPILOT_TOKEN=tx_abc123def456... │');
|
||||
console.log('│ │');
|
||||
console.log('│ 💡 Tip: Emails will be logged below when triggered │');
|
||||
console.log('└─────────────────────────────────────────────────────────────┘');
|
||||
console.log('');
|
||||
mailTransport = {
|
||||
sendMail: async (payload) => {
|
||||
console.log('');
|
||||
console.log('┌─────────────────────────────────────────────────────────────┐');
|
||||
console.log('│ 📧 [MOCK EMAIL - Not Sent] │');
|
||||
console.log('├─────────────────────────────────────────────────────────────┤');
|
||||
console.log(`│ To: ${payload.to}`);
|
||||
console.log(`│ Subject: ${payload.subject}`);
|
||||
console.log('│ Body preview:');
|
||||
const preview = (payload.text || payload.html || '').slice(0, 200).replace(/\n/g, ' ');
|
||||
console.log(`│ ${preview}...`);
|
||||
console.log('│ │');
|
||||
console.log('│ Configure MAILPILOT_TOKEN in .env to send real emails │');
|
||||
console.log('└─────────────────────────────────────────────────────────────┘');
|
||||
console.log('');
|
||||
return { messageId: 'mock-' + Date.now() };
|
||||
},
|
||||
verify: (cb) => cb(null, true)
|
||||
};
|
||||
return mailTransport;
|
||||
}
|
||||
|
||||
log('MailPilot configured', summarizeMailConfig());
|
||||
mailTransport = { url: MAILPILOT_URL, token: MAILPILOT_TOKEN };
|
||||
return mailTransport;
|
||||
}
|
||||
|
||||
async function sendEmail({ to, subject, text, html }) {
|
||||
try {
|
||||
const transport = ensureMailTransport();
|
||||
|
||||
if (!MAILPILOT_TOKEN) {
|
||||
await transport.sendMail({ to, subject, text, html });
|
||||
return { messageId: 'mock-' + Date.now() };
|
||||
}
|
||||
|
||||
const payload = {
|
||||
to,
|
||||
subject,
|
||||
html: html || text || '',
|
||||
};
|
||||
|
||||
log('sending email via MailPilot', { to, subject });
|
||||
|
||||
const response = await fetch(`${MAILPILOT_URL}/api/transactional-emails/trigger/${MAILPILOT_TOKEN}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`MailPilot API error: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
log('email sent successfully via MailPilot', { to, status: result.status });
|
||||
return { messageId: result.status || 'sent' };
|
||||
} catch (err) {
|
||||
log('FAILED TO SEND EMAIL', {
|
||||
to,
|
||||
subject,
|
||||
error: String(err),
|
||||
hint: 'Check MAILPILOT_URL and MAILPILOT_TOKEN configuration in .env file'
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
},
|
||||
// Enhanced Analytics Tracking
|
||||
@@ -2030,12 +2090,9 @@ function sanitizeGitMessage(message) {
|
||||
|
||||
function summarizeMailConfig() {
|
||||
return {
|
||||
hostConfigured: !!SMTP_HOST,
|
||||
portConfigured: Number.isFinite(SMTP_PORT) && SMTP_PORT > 0,
|
||||
secure: SMTP_SECURE,
|
||||
hasUser: !!SMTP_USER,
|
||||
hasPass: !!SMTP_PASS,
|
||||
fromConfigured: !!SMTP_FROM,
|
||||
configured: !!MAILPILOT_TOKEN,
|
||||
url: MAILPILOT_URL,
|
||||
tokenSet: !!MAILPILOT_TOKEN,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3488,26 +3545,21 @@ async function trackAffiliateCommission(user, plan) {
|
||||
|
||||
function ensureMailTransport() {
|
||||
if (mailTransport) return mailTransport;
|
||||
const invalidPort = !Number.isFinite(SMTP_PORT) || SMTP_PORT <= 0;
|
||||
if (!SMTP_HOST || invalidPort || !SMTP_FROM || !SMTP_USER || !SMTP_PASS) {
|
||||
log('⚠️ SMTP configuration is incomplete. Emails will be logged to CONSOLE only (not sent).', summarizeMailConfig());
|
||||
|
||||
if (!MAILPILOT_TOKEN) {
|
||||
console.log('');
|
||||
console.log('┌─────────────────────────────────────────────────────────────┐');
|
||||
console.log('│ 📧 EMAIL/SMTP NOT CONFIGURED │');
|
||||
console.log('│ 📧 MAILPILOT NOT CONFIGURED │');
|
||||
console.log('├─────────────────────────────────────────────────────────────┤');
|
||||
console.log('│ Password reset and verification emails will NOT be sent. │');
|
||||
console.log('│ To enable real emails, configure SMTP in .env file: │');
|
||||
console.log('│ To enable real emails, configure MailPilot in .env file: │');
|
||||
console.log('│ │');
|
||||
console.log('│ SMTP_HOST=smtp.gmail.com (or your SMTP server) │');
|
||||
console.log('│ SMTP_PORT=587 │');
|
||||
console.log('│ SMTP_USER=your-email@gmail.com │');
|
||||
console.log('│ SMTP_PASS=your-app-password │');
|
||||
console.log('│ SMTP_FROM=noreply@yourdomain.com │');
|
||||
console.log('│ MAILPILOT_URL=https://emailmarketing.modelrailway3d.co.uk│');
|
||||
console.log('│ MAILPILOT_TOKEN=tx_abc123def456... │');
|
||||
console.log('│ │');
|
||||
console.log('│ 💡 Tip: Emails will be logged below when triggered │');
|
||||
console.log('└─────────────────────────────────────────────────────────────┘');
|
||||
console.log('');
|
||||
// Create a mock transport that logs to console
|
||||
mailTransport = {
|
||||
sendMail: async (payload) => {
|
||||
console.log('');
|
||||
@@ -3520,7 +3572,7 @@ function ensureMailTransport() {
|
||||
const preview = (payload.text || payload.html || '').slice(0, 200).replace(/\n/g, ' ');
|
||||
console.log(`│ ${preview}...`);
|
||||
console.log('│ │');
|
||||
console.log('│ Configure SMTP in .env to send real emails │');
|
||||
console.log('│ Configure MAILPILOT_TOKEN in .env to send real emails │');
|
||||
console.log('└─────────────────────────────────────────────────────────────┘');
|
||||
console.log('');
|
||||
return { messageId: 'mock-' + Date.now() };
|
||||
@@ -3530,52 +3582,48 @@ function ensureMailTransport() {
|
||||
return mailTransport;
|
||||
}
|
||||
|
||||
log('initializing mail transport', summarizeMailConfig());
|
||||
mailTransport = nodemailer.createTransport({
|
||||
host: SMTP_HOST,
|
||||
port: SMTP_PORT,
|
||||
secure: SMTP_SECURE,
|
||||
auth: { user: SMTP_USER, pass: SMTP_PASS },
|
||||
// Add timeouts to avoid long hangs
|
||||
connectionTimeout: 5000,
|
||||
greetingTimeout: 5000,
|
||||
socketTimeout: 5000,
|
||||
});
|
||||
|
||||
// Verify the connection asynchronously
|
||||
mailTransport.verify((error) => {
|
||||
if (error) {
|
||||
log('❌ SMTP verification failed', { error: String(error) });
|
||||
} else {
|
||||
log('✅ SMTP transport verified and ready');
|
||||
}
|
||||
});
|
||||
|
||||
log('MailPilot configured', summarizeMailConfig());
|
||||
mailTransport = { url: MAILPILOT_URL, token: MAILPILOT_TOKEN };
|
||||
return mailTransport;
|
||||
}
|
||||
|
||||
async function sendEmail({ to, subject, text, html }) {
|
||||
try {
|
||||
const transport = ensureMailTransport();
|
||||
const plain = text || undefined;
|
||||
|
||||
if (!MAILPILOT_TOKEN) {
|
||||
await transport.sendMail({ to, subject, text, html });
|
||||
return { messageId: 'mock-' + Date.now() };
|
||||
}
|
||||
|
||||
const payload = {
|
||||
from: SMTP_FROM || 'noreply@plugincompass.com',
|
||||
to,
|
||||
subject,
|
||||
html: html || text || '',
|
||||
};
|
||||
if (plain) payload.text = plain;
|
||||
if (html) payload.html = html;
|
||||
|
||||
log('sending email', { to, subject });
|
||||
const info = await transport.sendMail(payload);
|
||||
log('email sent successfully', { to, messageId: info.messageId });
|
||||
return info;
|
||||
log('sending email via MailPilot', { to, subject });
|
||||
|
||||
const response = await fetch(`${MAILPILOT_URL}/api/transactional-emails/trigger/${MAILPILOT_TOKEN}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`MailPilot API error: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
log('email sent successfully via MailPilot', { to, status: result.status });
|
||||
return { messageId: result.status || 'sent' };
|
||||
} catch (err) {
|
||||
log('❌ FAILED TO SEND EMAIL', {
|
||||
log('FAILED TO SEND EMAIL', {
|
||||
to,
|
||||
subject,
|
||||
error: String(err),
|
||||
hint: 'Check SMTP configuration in .env file'
|
||||
hint: 'Check MAILPILOT_URL and MAILPILOT_TOKEN configuration in .env file'
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
@@ -19004,24 +19052,20 @@ async function bootstrap() {
|
||||
planningChain: planSettings.planningChain
|
||||
});
|
||||
|
||||
// Log email/SMTP configuration
|
||||
const smtpConfig = summarizeMailConfig();
|
||||
console.log('[CONFIG] Email / SMTP:');
|
||||
console.log(' - SMTP Configured:', smtpConfig.hostConfigured ? 'YES ✓' : 'NO ✗');
|
||||
console.log(' - SMTP Host:', smtpConfig.hostConfigured ? SMTP_HOST : 'not configured');
|
||||
console.log(' - SMTP Port:', smtpConfig.portConfigured ? SMTP_PORT : 'not configured');
|
||||
console.log(' - SMTP Secure:', smtpConfig.secure ? 'YES (TLS/SSL)' : 'NO (STARTTLS)');
|
||||
console.log(' - From Address:', smtpConfig.fromConfigured ? SMTP_FROM : 'not configured');
|
||||
const mailConfig = summarizeMailConfig();
|
||||
console.log('[CONFIG] MailPilot Email:');
|
||||
console.log(' - MailPilot Configured:', mailConfig.configured ? 'YES ✓' : 'NO ✗');
|
||||
console.log(' - MailPilot URL:', MAILPILOT_URL);
|
||||
console.log('');
|
||||
if (!smtpConfig.hostConfigured) {
|
||||
console.log(' ⚠️ WARNING: Email is NOT configured. Password reset and verification');
|
||||
if (!mailConfig.configured) {
|
||||
console.log(' ⚠️ WARNING: MailPilot is NOT configured. Password reset and verification');
|
||||
console.log(' emails will be logged to console only. To enable real emails:');
|
||||
console.log(' 1. Edit the .env file');
|
||||
console.log(' 2. Set SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, and SMTP_FROM');
|
||||
console.log(' 2. Set MAILPILOT_TOKEN to your transactional email token');
|
||||
console.log(' 3. Restart the server');
|
||||
console.log(' 💡 Tip: Use /debug/email/preview?type=reset to preview the email template');
|
||||
} else {
|
||||
console.log(' ✓ Email/SMTP is configured and ready to send emails');
|
||||
console.log(' ✓ MailPilot is configured and ready to send emails');
|
||||
}
|
||||
console.log('');
|
||||
console.log('==============================');
|
||||
|
||||
@@ -100,14 +100,9 @@ services:
|
||||
- JWT_SECRET=${JWT_SECRET:-}
|
||||
- JWT_ACCESS_TOKEN_TTL=${JWT_ACCESS_TOKEN_TTL:-900}
|
||||
- JWT_REFRESH_TOKEN_TTL=${JWT_REFRESH_TOKEN_TTL:-604800}
|
||||
# SMTP configuration
|
||||
- SMTP_HOST=${SMTP_HOST:-}
|
||||
- SMTP_PORT=${SMTP_PORT:-587}
|
||||
- SMTP_SECURE=${SMTP_SECURE:-false}
|
||||
- SMTP_USER=${SMTP_USER:-}
|
||||
- SMTP_PASS=${SMTP_PASS:-}
|
||||
- SMTP_PASS_FILE=${SMTP_PASS_FILE:-}
|
||||
- SMTP_FROM=${SMTP_FROM:-}
|
||||
# MailPilot configuration
|
||||
- MAILPILOT_URL=${MAILPILOT_URL:-https://emailmarketing.modelrailway3d.co.uk}
|
||||
- MAILPILOT_TOKEN=${MAILPILOT_TOKEN:-}
|
||||
# Chutes AI
|
||||
- PLUGIN_COMPASS_CHUTES_API_KEY=${PLUGIN_COMPASS_CHUTES_API_KEY:-}
|
||||
- CHUTES_API_KEY=${CHUTES_API_KEY:-}
|
||||
|
||||
1
telemetry-id
Normal file
1
telemetry-id
Normal file
@@ -0,0 +1 @@
|
||||
35505120-2be5-4eed-831f-95863823f2b9
|
||||
Reference in New Issue
Block a user