diff --git a/estore/src/app/admin/page.tsx b/estore/src/app/admin/page.tsx index f521299..75859d6 100644 --- a/estore/src/app/admin/page.tsx +++ b/estore/src/app/admin/page.tsx @@ -1514,6 +1514,9 @@ export default function AdminPage() { const [fetchedAt, setFetchedAt] = useState(null) const [refreshing, setRefreshing] = useState(false) const [refreshMsg, setRefreshMsg] = useState('') + const [restoring, setRestoring] = useState(false) + const [restoreMsg, setRestoreMsg] = useState('') + const restoreInputRef = useRef(null) // Category creation state (for the Categories tab) const [newCatName, setNewCatName] = useState('') @@ -1590,6 +1593,43 @@ export default function AdminPage() { setCatMessage(cat.id ? `Created: ${cat.name}` : 'Failed to create category') } + function handleBackup() { + window.location.href = BASE + '/api/admin/backup' + } + + async function handleRestore(e: React.ChangeEvent) { + const file = e.target.files?.[0] + if (!file) return + if (!confirm('This will overwrite your current item overrides, delivery rates, hours, occasions, and category settings. Continue?')) { + if (restoreInputRef.current) restoreInputRef.current.value = '' + return + } + setRestoring(true) + setRestoreMsg('') + try { + const text = await file.text() + const bundle = JSON.parse(text) + const res = await fetch(BASE + '/api/admin/backup', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(bundle), + }) + const data = await res.json() + if (res.ok) { + setRestoreMsg(`Restored: ${data.restored.join(', ')}`) + load() + } else { + setRestoreMsg(`Error: ${data.error}`) + } + } catch { + setRestoreMsg('Invalid backup file') + } finally { + setRestoring(false) + if (restoreInputRef.current) restoreInputRef.current.value = '' + } + setTimeout(() => setRestoreMsg(''), 5000) + } + function handleSaved(id: string, patch: Partial) { setItems((prev) => prev.map((item) => @@ -1630,6 +1670,21 @@ export default function AdminPage() { + + {refreshMsg && ( @@ -1637,6 +1692,11 @@ export default function AdminPage() { {refreshMsg}

)} + {restoreMsg && ( +

+ {restoreMsg} +

+ )} {fetchedAt && !refreshMsg && (

Cache: {new Date(fetchedAt).toLocaleString()} diff --git a/estore/src/app/api/admin/backup/route.ts b/estore/src/app/api/admin/backup/route.ts new file mode 100644 index 0000000..a64dab8 --- /dev/null +++ b/estore/src/app/api/admin/backup/route.ts @@ -0,0 +1,68 @@ +import { NextRequest, NextResponse } from 'next/server' +import { readFileSync, existsSync, writeFileSync } from 'fs' +import path from 'path' + +export const dynamic = 'force-dynamic' + +const DATA = path.join(process.cwd(), 'data') + +const FILES = [ + 'item-overrides', + 'delivery-rates', + 'categories-display', + 'occasions', + 'hours', + 'vinyl-config', +] as const + +type FileKey = typeof FILES[number] + +function readFile(key: FileKey): unknown { + const p = path.join(DATA, `${key}.json`) + if (!existsSync(p)) return null + try { return JSON.parse(readFileSync(p, 'utf-8')) } catch { return null } +} + +/** GET — download a JSON bundle of all config files */ +export async function GET() { + const bundle: Record = { + version: 1, + exportedAt: new Date().toISOString(), + } + for (const key of FILES) { + bundle[key] = readFile(key) + } + return new NextResponse(JSON.stringify(bundle, null, 2), { + headers: { + 'Content-Type': 'application/json', + 'Content-Disposition': `attachment; filename="bpb-config-${new Date().toISOString().slice(0, 10)}.json"`, + }, + }) +} + +/** POST — restore from a previously downloaded bundle */ +export async function POST(req: NextRequest) { + let bundle: Record + try { + bundle = await req.json() + } catch { + return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) + } + + const restored: string[] = [] + const skipped: string[] = [] + + for (const key of FILES) { + if (key === 'vinyl-config') continue // never overwrite vinyl-config from restore + const value = bundle[key] + if (value == null) { skipped.push(key); continue } + try { + writeFileSync(path.join(DATA, `${key}.json`), JSON.stringify(value, null, 2), 'utf-8') + restored.push(key) + } catch (err) { + return NextResponse.json({ error: `Failed to write ${key}: ${String(err)}` }, { status: 500 }) + } + } + + return NextResponse.json({ ok: true, restored, skipped }) +}