diff --git a/index.html b/index.html
index 714b706..a595b9e 100644
--- a/index.html
+++ b/index.html
@@ -5,6 +5,12 @@
Antonym
+
+
+
+
+
+
diff --git a/public/images/contradiction-cargo.svg b/public/images/contradiction-cargo.svg
new file mode 100644
index 0000000..c3dc062
--- /dev/null
+++ b/public/images/contradiction-cargo.svg
@@ -0,0 +1,10 @@
+
diff --git a/public/images/duality-jacket.svg b/public/images/duality-jacket.svg
new file mode 100644
index 0000000..bfeeb03
--- /dev/null
+++ b/public/images/duality-jacket.svg
@@ -0,0 +1,10 @@
+
diff --git a/public/images/inverse-tee.svg b/public/images/inverse-tee.svg
new file mode 100644
index 0000000..5f27da2
--- /dev/null
+++ b/public/images/inverse-tee.svg
@@ -0,0 +1,10 @@
+
diff --git a/public/images/paradox-cap.svg b/public/images/paradox-cap.svg
new file mode 100644
index 0000000..7de4a66
--- /dev/null
+++ b/public/images/paradox-cap.svg
@@ -0,0 +1,10 @@
+
diff --git a/public/images/shadow-hoodie.svg b/public/images/shadow-hoodie.svg
new file mode 100644
index 0000000..7e57ad4
--- /dev/null
+++ b/public/images/shadow-hoodie.svg
@@ -0,0 +1,10 @@
+
diff --git a/server/db/seed.ts b/server/db/seed.ts
index 14823da..1f16d22 100644
--- a/server/db/seed.ts
+++ b/server/db/seed.ts
@@ -6,11 +6,11 @@ export function seedProducts(db: Database.Database): void {
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' },
+ { 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.svg']), 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.svg']), 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.svg']), 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.svg']), 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.svg']), 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)')
diff --git a/server/index.ts b/server/index.ts
index 445553b..5d0ac16 100644
--- a/server/index.ts
+++ b/server/index.ts
@@ -12,6 +12,8 @@ import { adminRouter } from './routes/admin.js'
import { adminProductsRouter } from './routes/adminProducts.js'
import { adminOrdersRouter } from './routes/adminOrders.js'
import { uploadRouter } from './routes/upload.js'
+import { adminNostrRouter } from './routes/adminNostr.js'
+import { ordersByPubkeyRouter } from './routes/ordersByPubkey.js'
const app = express()
const port = Number(process.env.PORT) || 3141
@@ -32,6 +34,8 @@ app.use('/api/admin', adminRouter)
app.use('/api/admin/products', adminProductsRouter)
app.use('/api/admin/orders', adminOrdersRouter)
app.use('/api/admin/upload', uploadRouter)
+app.use('/api/admin', adminNostrRouter)
+app.use('/api/orders/by-pubkey', ordersByPubkeyRouter)
app.get('/api/health', (_req, res) => { res.json({ ok: true, timestamp: new Date().toISOString() }) })
diff --git a/server/routes/adminNostr.ts b/server/routes/adminNostr.ts
new file mode 100644
index 0000000..b444c0e
--- /dev/null
+++ b/server/routes/adminNostr.ts
@@ -0,0 +1,26 @@
+import { Router } from 'express'
+import { adminAuth } from '../middleware/adminAuth.js'
+
+export const adminNostrRouter = Router()
+adminNostrRouter.use(adminAuth)
+
+adminNostrRouter.get('/nostr-info', async (_req, res) => {
+ const privkeyHex = process.env.NOSTR_PRIVATE_KEY
+ if (!privkeyHex) {
+ res.status(404).json({ error: { code: 'NOT_CONFIGURED', message: 'NOSTR_PRIVATE_KEY not set' } })
+ return
+ }
+
+ try {
+ const { getPublicKey } = await import('nostr-tools/pure')
+ const { npubEncode } = await import('nostr-tools/nip19')
+ const privkey = Uint8Array.from(Buffer.from(privkeyHex, 'hex'))
+ const pubkey = getPublicKey(privkey)
+ const npub = npubEncode(pubkey)
+ privkey.fill(0)
+
+ res.json({ npub, pubkey })
+ } catch {
+ res.status(500).json({ error: { code: 'NOSTR_ERROR', message: 'Failed to derive public key' } })
+ }
+})
diff --git a/server/routes/ordersByPubkey.ts b/server/routes/ordersByPubkey.ts
new file mode 100644
index 0000000..0c10cfa
--- /dev/null
+++ b/server/routes/ordersByPubkey.ts
@@ -0,0 +1,45 @@
+import { Router } from 'express'
+import { getDb } from '../db/connection.js'
+import type { OrderItem, OrderStatus } from '../../shared/types.js'
+
+export const ordersByPubkeyRouter = Router()
+
+interface OrderRow {
+ id: string
+ nostr_pubkey: string | null
+ email: string | null
+ btcpay_invoice_id: string | null
+ status: string
+ items: string
+ total_sats: number
+ note: string | null
+ created_at: string
+ updated_at: string
+}
+
+ordersByPubkeyRouter.get('/:pubkey', (req, res) => {
+ const pubkey = req.params.pubkey
+
+ if (!/^[0-9a-f]{64}$/.test(pubkey)) {
+ res.status(400).json({ error: { code: 'INVALID_PUBKEY', message: 'Invalid hex pubkey' } })
+ return
+ }
+
+ const db = getDb()
+ const rows = db.prepare(
+ 'SELECT * FROM orders WHERE nostr_pubkey = ? ORDER BY created_at DESC'
+ ).all(pubkey) as OrderRow[]
+
+ res.json(rows.map((row) => ({
+ id: row.id,
+ nostrPubkey: row.nostr_pubkey,
+ email: row.email,
+ btcpayInvoiceId: row.btcpay_invoice_id,
+ status: row.status as OrderStatus,
+ items: JSON.parse(row.items) as OrderItem[],
+ totalSats: row.total_sats,
+ note: row.note,
+ createdAt: row.created_at,
+ updatedAt: row.updated_at,
+ })))
+})
diff --git a/src/components/SeoMeta.vue b/src/components/SeoMeta.vue
new file mode 100644
index 0000000..de71aa7
--- /dev/null
+++ b/src/components/SeoMeta.vue
@@ -0,0 +1,44 @@
+
+
+
+
+
diff --git a/src/components/admin/AdminShell.vue b/src/components/admin/AdminShell.vue
index efc315a..bfcaac6 100644
--- a/src/components/admin/AdminShell.vue
+++ b/src/components/admin/AdminShell.vue
@@ -23,6 +23,7 @@ async function handleLogout() {