166 lines
7.0 KiB
Cheetah
166 lines
7.0 KiB
Cheetah
{{define "page"}}
|
|
<header class="topbar">
|
|
<div class="container">
|
|
<div class="topbar-inner">
|
|
<a href="{{.Base}}/dashboard" class="topbar-brand">vault1984 <span class="port">account</span></a>
|
|
<nav class="topbar-nav">
|
|
<a href="{{.Base}}/dashboard" class="{{if eq .ActiveNav "dashboard"}}active{{end}}">Vaults</a>
|
|
<a href="{{.Base}}/settings" class="{{if eq .ActiveNav "settings"}}active{{end}}">Settings</a>
|
|
<div class="separator"></div>
|
|
<a href="#" id="logout-link">Sign out</a>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="container" style="flex:1">
|
|
<section class="section fade-in">
|
|
<div class="section-header">
|
|
<div>
|
|
<span class="label accent">Account</span>
|
|
<h1 style="margin-top:0.5rem">Your vaults</h1>
|
|
</div>
|
|
<a href="{{.Base}}/regions" class="btn btn-primary" id="new-vault-btn">
|
|
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
|
New vault
|
|
</a>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Vault list (loaded dynamically) -->
|
|
<section class="fade-in fade-in-delay-1">
|
|
<div class="card" id="vault-list" style="padding:0; overflow:hidden">
|
|
<div class="vault-row" style="justify-content:center;color:var(--text-tertiary);padding:2rem">
|
|
Loading…
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Plan info -->
|
|
<section class="section fade-in fade-in-delay-2">
|
|
<div class="card" style="display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap">
|
|
<div>
|
|
<span class="label" style="margin-bottom:0.25rem;display:block">Plan</span>
|
|
<div style="font-weight:600;font-size:1.0625rem">Consumer</div>
|
|
<div class="text-secondary" style="font-size:0.8125rem;margin-top:0.125rem" id="plan-info">$12/year</div>
|
|
</div>
|
|
<a href="{{.Base}}/settings" class="btn btn-ghost">Manage billing</a>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Quick stats -->
|
|
<section class="fade-in fade-in-delay-3" style="padding-bottom:3rem">
|
|
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:0.75rem">
|
|
<div class="card" style="text-align:center;padding:1.25rem">
|
|
<div style="font-size:1.75rem;font-weight:700;letter-spacing:-0.03em" id="stat-vaults">-</div>
|
|
<div class="label" style="margin-top:0.375rem">Vaults</div>
|
|
</div>
|
|
<div class="card" style="text-align:center;padding:1.25rem">
|
|
<div style="font-size:1.75rem;font-weight:700;letter-spacing:-0.03em;color:var(--accent)" id="stat-days">-</div>
|
|
<div class="label" style="margin-top:0.375rem">Days left</div>
|
|
</div>
|
|
<div class="card" style="text-align:center;padding:1.25rem">
|
|
<div style="font-size:1.75rem;font-weight:700;letter-spacing:-0.03em" id="stat-regions">-</div>
|
|
<div class="label" style="margin-top:0.375rem">Regions</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
{{end}}
|
|
|
|
{{define "scripts"}}
|
|
<script>
|
|
const BASE = '{{.Base}}';
|
|
const REGIONS = {
|
|
zurich:'Zürich, Switzerland', virginia:'Virginia, US', sanfrancisco:'San Francisco, US',
|
|
montreal:'Montréal, Canada', mexico:'Mexico City, Mexico', bogota:'Bogotá, Colombia',
|
|
saopaulo:'São Paulo, Brazil', santiago:'Santiago, Chile', buenosaires:'Buenos Aires, Argentina',
|
|
london:'London, UK', madrid:'Madrid, Spain',
|
|
istanbul:'Istanbul, Turkey', dubai:'Dubai, UAE', capetown:'Cape Town, South Africa',
|
|
mumbai:'Mumbai, India', singapore:'Singapore', hongkong:'Hong Kong',
|
|
seoul:'Seoul, South Korea', tokyo:'Tokyo, Japan', sydney:'Sydney, Australia',
|
|
almaty:'Almaty, Kazakhstan'
|
|
};
|
|
|
|
async function loadDashboard() {
|
|
try {
|
|
const resp = await fetch(BASE + '/api/vaults');
|
|
if (resp.status === 401) { window.location.href = BASE + '/login'; return; }
|
|
if (!resp.ok) throw new Error('Failed to load');
|
|
const data = await resp.json();
|
|
|
|
const list = document.getElementById('vault-list');
|
|
const vaults = data.vaults || [];
|
|
|
|
if (vaults.length === 0) {
|
|
list.innerHTML = `
|
|
<div style="display:flex;flex-direction:column;align-items:center;padding:3rem 2rem;gap:1rem;text-align:center">
|
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="var(--text-secondary)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="opacity:0.5">
|
|
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
|
</svg>
|
|
<h2 style="color:var(--text)">You have no vaults yet</h2>
|
|
<p style="color:var(--text-secondary);max-width:320px;font-size:0.9375rem;line-height:1.6">
|
|
Pick a region and create your first vault. Your data stays in that region — encrypted, zero-knowledge, yours.
|
|
</p>
|
|
<a href="${BASE}/regions" class="btn btn-primary btn-lg" style="margin-top:0.5rem">
|
|
Choose a region and create your vault
|
|
</a>
|
|
</div>`;
|
|
} else {
|
|
list.innerHTML = vaults.map(v => {
|
|
const expires = new Date(v.expires_at);
|
|
const days = Math.ceil((expires - new Date()) / 86400000);
|
|
const isHQ = v.region === 'zurich';
|
|
const status = days > 0 ? 'live' : 'expired';
|
|
const regionName = REGIONS[v.region] || v.region;
|
|
return `
|
|
<div class="vault-row">
|
|
<div style="display:flex;align-items:center;gap:0.75rem">
|
|
<span class="status-dot ${status}"></span>
|
|
<div>
|
|
<div class="vault-id">vault1984-${v.vault_id}</div>
|
|
<div class="vault-region">${regionName}</div>
|
|
</div>
|
|
</div>
|
|
<div class="vault-meta">
|
|
${isHQ ? '<span class="label gold">HQ</span>' : ''}
|
|
<span>${days > 0 ? 'Expires' : 'Expired'} ${expires.toLocaleDateString('en-US', {month:'short',day:'numeric',year:'numeric'})}</span>
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
}
|
|
|
|
// Stats
|
|
document.getElementById('stat-vaults').textContent = vaults.length;
|
|
const regions = new Set(vaults.map(v => v.region));
|
|
document.getElementById('stat-regions').textContent = regions.size;
|
|
|
|
if (vaults.length > 0) {
|
|
const earliest = vaults.map(v => new Date(v.expires_at)).sort((a,b) => a-b)[0];
|
|
const days = Math.max(0, Math.ceil((earliest - new Date()) / 86400000));
|
|
document.getElementById('stat-days').textContent = days;
|
|
} else {
|
|
document.getElementById('stat-days').textContent = '-';
|
|
}
|
|
|
|
// Hide "New vault" if at max
|
|
if (data.count >= data.max) {
|
|
document.getElementById('new-vault-btn').style.display = 'none';
|
|
}
|
|
|
|
} catch (err) {
|
|
document.getElementById('vault-list').innerHTML =
|
|
'<div class="vault-row" style="justify-content:center;color:var(--red)">Failed to load vaults</div>';
|
|
}
|
|
}
|
|
|
|
document.getElementById('logout-link')?.addEventListener('click', async (e) => {
|
|
e.preventDefault();
|
|
await fetch(BASE + '/api/auth/logout', { method: 'POST' });
|
|
window.location.href = BASE + '/login';
|
|
});
|
|
|
|
loadDashboard();
|
|
</script>
|
|
{{end}}
|