/** * {{PROJECT_NAME}} — noy-db vanilla starter. * * A minimal, no-framework integration of noy-db. Everything this file * needs to do: * * 1. Prompt for a secret (derives the encryption key). * 2. Open an encrypted vault backed by IndexedDB. * 3. Render a table of invoices. * 4. Let the user add, refresh, and lock. * * The entire app is ~200 lines — the library does the heavy lifting. */ import { createNoydb, type Noydb, type Vault, type Collection } from '@noy-db/hub' import { toBrowserIdb } from '@noy-db/to-browser-idb' import './style.css' // ─── Domain type ──────────────────────────────────────────────────── interface Invoice { id: string client: string amount: number status: 'draft' | 'open' | 'paid' | 'overdue' issueDate: string // ISO-8601 } // ─── DOM references ──────────────────────────────────────────────── const statusEl = document.querySelector('#status')! const statusTextEl = document.querySelector('.status-text')! const controlsEl = document.querySelector('#controls')! const invoicesSection = document.querySelector('#invoices')! const invoicesBody = document.querySelector('#invoices-body')! const addBtn = document.querySelector('#add-invoice')! const refreshBtn = document.querySelector('#refresh')! const closeBtn = document.querySelector('#close-vault')! // ─── App state ───────────────────────────────────────────────────── let db: Noydb | null = null let vault: Vault | null = null let invoices: Collection | null = null // ─── Lifecycle ───────────────────────────────────────────────────── async function unlock() { // In a real app you would build a proper modal. For the starter we // use the browser prompt — it blocks, it's ugly, and it gets the // secret in two lines. const secret = prompt( 'Enter secret for {{PROJECT_NAME}}\n\n' + 'This derives the master encryption key. Same secret every time.\n' + 'Lose it and the data is unrecoverable (by design).', ) if (!secret) { showStatus('Cancelled — reload to try again.') return } showStatus('Unlocking vault…') db = await createNoydb({ store: toBrowserIdb({ prefix: '{{PROJECT_NAME}}' }), user: 'owner', secret: secret, }) vault = await db.openVault('demo') invoices = vault.collection('invoices') statusEl.hidden = true controlsEl.hidden = false invoicesSection.hidden = false await render() } async function render() { if (!invoices) return const rows = await invoices.list() rows.sort((a, b) => a.issueDate.localeCompare(b.issueDate)) invoicesBody.innerHTML = '' if (rows.length === 0) { const tr = document.createElement('tr') tr.innerHTML = `No invoices yet — click "Add invoice" to create one.` invoicesBody.appendChild(tr) return } for (const inv of rows) { const tr = document.createElement('tr') tr.innerHTML = ` ${inv.id} ${escapeHtml(inv.client)} ${inv.amount.toLocaleString()} ${inv.status} ` invoicesBody.appendChild(tr) } } async function addInvoice() { if (!invoices) return const client = prompt('Client name?') || 'Unnamed' const amountStr = prompt('Amount?') || '0' const amount = Number.parseFloat(amountStr) if (Number.isNaN(amount)) { alert('Amount must be a number.') return } const id = `inv-${Date.now()}-${Math.random().toString(36).slice(2, 6)}` await invoices.put(id, { id, client, amount, status: 'draft', issueDate: new Date().toISOString().slice(0, 10), }) await render() } async function closeVault() { if (!db) return await db.close() db = null vault = null invoices = null controlsEl.hidden = true invoicesSection.hidden = true showStatus('Vault closed. Reload the page to unlock again.') } // ─── Helpers ─────────────────────────────────────────────────────── function showStatus(text: string) { statusEl.hidden = false statusTextEl.textContent = text } function escapeHtml(s: string): string { const div = document.createElement('div') div.textContent = s return div.innerHTML } // ─── Wire everything up ──────────────────────────────────────────── addBtn.addEventListener('click', () => void addInvoice()) refreshBtn.addEventListener('click', () => void render()) closeBtn.addEventListener('click', () => void closeVault()) window.addEventListener('DOMContentLoaded', () => void unlock())