feat: scaffold Antonym fashion store
Anonymous Bitcoin-only fashion e-commerce with: - Vue 3 + Tailwind 4 frontend with glassmorphism dark/light design system - Express 5 + SQLite backend with BTCPay Server integration - Nostr identity (NIP-07/keypair) for anonymous purchase tracking - ChaCha20-Poly1305 encrypted shipping addresses - Admin panel with order/product/stock management - SVG logo splash animation with clip-path reveal - 5 seeded products across 4 categories Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
22
server/db/connection.ts
Normal file
22
server/db/connection.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import Database from 'better-sqlite3'
|
||||
import path from 'node:path'
|
||||
import fs from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const DB_PATH = path.join(__dirname, '..', '..', 'data', 'antonym.db')
|
||||
|
||||
let db: Database.Database | null = null
|
||||
|
||||
export function getDb(): Database.Database {
|
||||
if (!db) {
|
||||
const dir = path.dirname(DB_PATH)
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
db = new Database(DB_PATH)
|
||||
db.pragma('journal_mode = WAL')
|
||||
db.pragma('foreign_keys = ON')
|
||||
}
|
||||
return db
|
||||
}
|
||||
53
server/db/schema.ts
Normal file
53
server/db/schema.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type Database from 'better-sqlite3'
|
||||
|
||||
export function initSchema(db: Database.Database): void {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS products (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
price_sats INTEGER NOT NULL,
|
||||
images TEXT NOT NULL DEFAULT '[]',
|
||||
sizes TEXT NOT NULL DEFAULT '[]',
|
||||
category TEXT NOT NULL DEFAULT 'general',
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS orders (
|
||||
id TEXT PRIMARY KEY,
|
||||
nostr_pubkey TEXT,
|
||||
email TEXT,
|
||||
btcpay_invoice_id TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
shipping_address_encrypted TEXT,
|
||||
items TEXT NOT NULL,
|
||||
total_sats INTEGER NOT NULL,
|
||||
note TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS order_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
order_id TEXT NOT NULL REFERENCES orders(id),
|
||||
status TEXT NOT NULL,
|
||||
note TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS admin_sessions (
|
||||
token TEXT PRIMARY KEY,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
expires_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_orders_nostr ON orders(nostr_pubkey);
|
||||
CREATE INDEX IF NOT EXISTS idx_order_events_order ON order_events(order_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_products_slug ON products(slug);
|
||||
CREATE INDEX IF NOT EXISTS idx_products_category ON products(category);
|
||||
`)
|
||||
}
|
||||
19
server/db/seed.ts
Normal file
19
server/db/seed.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type Database from 'better-sqlite3'
|
||||
import { nanoid } from 'nanoid'
|
||||
|
||||
export function seedProducts(db: Database.Database): void {
|
||||
const count = db.prepare('SELECT COUNT(*) as n FROM products').get() as { n: number }
|
||||
if (count.n > 0) return
|
||||
|
||||
const products = [
|
||||
{ id: nanoid(), name: 'Shadow Hoodie', slug: 'shadow-hoodie', description: 'Heavyweight cotton hoodie with embroidered Antonym logo. Oversized fit.', price_sats: 250_000, images: JSON.stringify(['/images/shadow-hoodie.jpg']), sizes: JSON.stringify([{ size: 'S', stock: 10 }, { size: 'M', stock: 15 }, { size: 'L', stock: 12 }, { size: 'XL', stock: 8 }]), category: 'tops' },
|
||||
{ id: nanoid(), name: 'Inverse Tee', slug: 'inverse-tee', description: 'Premium organic cotton t-shirt. Minimalist design with contrast stitching.', price_sats: 85_000, images: JSON.stringify(['/images/inverse-tee.jpg']), sizes: JSON.stringify([{ size: 'S', stock: 20 }, { size: 'M', stock: 25 }, { size: 'L', stock: 18 }, { size: 'XL', stock: 10 }]), category: 'tops' },
|
||||
{ id: nanoid(), name: 'Contradiction Cargo', slug: 'contradiction-cargo', description: 'Relaxed cargo pants with hidden zip pockets. Washed black.', price_sats: 180_000, images: JSON.stringify(['/images/contradiction-cargo.jpg']), sizes: JSON.stringify([{ size: '28', stock: 8 }, { size: '30', stock: 12 }, { size: '32', stock: 15 }, { size: '34', stock: 10 }, { size: '36', stock: 6 }]), category: 'bottoms' },
|
||||
{ id: nanoid(), name: 'Paradox Cap', slug: 'paradox-cap', description: 'Unstructured six-panel cap with tonal embroidery. One size.', price_sats: 45_000, images: JSON.stringify(['/images/paradox-cap.jpg']), sizes: JSON.stringify([{ size: 'ONE SIZE', stock: 30 }]), category: 'accessories' },
|
||||
{ id: nanoid(), name: 'Duality Jacket', slug: 'duality-jacket', description: 'Reversible bomber jacket. Matte black one side, reflective silver the other.', price_sats: 450_000, images: JSON.stringify(['/images/duality-jacket.jpg']), sizes: JSON.stringify([{ size: 'S', stock: 5 }, { size: 'M', stock: 8 }, { size: 'L', stock: 6 }, { size: 'XL', stock: 4 }]), category: 'outerwear' },
|
||||
]
|
||||
|
||||
const insert = db.prepare('INSERT INTO products (id, name, slug, description, price_sats, images, sizes, category) VALUES (@id, @name, @slug, @description, @price_sats, @images, @sizes, @category)')
|
||||
const tx = db.transaction(() => { for (const p of products) insert.run(p) })
|
||||
tx()
|
||||
}
|
||||
Reference in New Issue
Block a user