add local storage in json for collection
This commit is contained in:
20
data/checklist.json
Normal file
20
data/checklist.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"det1-8": {
|
||||||
|
"base": true,
|
||||||
|
"holofoil": true
|
||||||
|
},
|
||||||
|
"xy7-19": {
|
||||||
|
"base": true,
|
||||||
|
"reverseHolofoil": true
|
||||||
|
},
|
||||||
|
"sm115-15": {
|
||||||
|
"base": true,
|
||||||
|
"reverseHolofoil": true
|
||||||
|
},
|
||||||
|
"sm75-19": {
|
||||||
|
"base": true
|
||||||
|
},
|
||||||
|
"mcd16-4": {
|
||||||
|
"base": true
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/app/api/checklist/route.ts
Normal file
45
src/app/api/checklist/route.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { NextRequest } from 'next/server';
|
||||||
|
import { promises as fs } from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const DATA_DIR = path.join(process.cwd(), 'data');
|
||||||
|
const FILE_PATH = path.join(DATA_DIR, 'checklist.json');
|
||||||
|
|
||||||
|
async function ensureFile(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
await fs.access(FILE_PATH);
|
||||||
|
} catch {
|
||||||
|
// create empty object file on first use
|
||||||
|
await fs.writeFile(FILE_PATH, JSON.stringify({}, null, 2), 'utf-8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
await ensureFile();
|
||||||
|
const raw = await fs.readFile(FILE_PATH, 'utf-8');
|
||||||
|
const data = raw ? JSON.parse(raw) : {};
|
||||||
|
return Response.json({ data }, { status: 200 });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('GET /api/checklist error', err);
|
||||||
|
return Response.json({ data: {} }, { status: 200 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PUT(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await req.json();
|
||||||
|
if (typeof body !== 'object' || body === null || Array.isArray(body)) {
|
||||||
|
return Response.json({ error: 'Invalid payload' }, { status: 400 });
|
||||||
|
}
|
||||||
|
await ensureFile();
|
||||||
|
await fs.writeFile(FILE_PATH, JSON.stringify(body, null, 2), 'utf-8');
|
||||||
|
return Response.json({ ok: true }, { status: 200 });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('PUT /api/checklist error', err);
|
||||||
|
return Response.json({ error: 'Write failed' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import type { TcgCard } from '@/types/pokemon';
|
import type { TcgCard } from '@/types/pokemon';
|
||||||
import { loadChecklist, saveChecklist } from '@/lib/checklist';
|
import { loadChecklistServer, saveChecklistServer } from '@/lib/checklist';
|
||||||
import type { ChecklistV2, VariantKey } from '@/lib/checklist';
|
import type { ChecklistV2, VariantKey } from '@/lib/checklist';
|
||||||
import CardGrid from '@/components/CardGrid';
|
import CardGrid from '@/components/CardGrid';
|
||||||
import Header from '@/components/Header';
|
import Header from '@/components/Header';
|
||||||
@@ -18,7 +18,10 @@ export default function Page() {
|
|||||||
const [tab, setTab] = useState<'uncollected' | 'collected'>('uncollected');
|
const [tab, setTab] = useState<'uncollected' | 'collected'>('uncollected');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setChecklist(loadChecklist());
|
(async () => {
|
||||||
|
const data = await loadChecklistServer();
|
||||||
|
setChecklist(data);
|
||||||
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -48,7 +51,8 @@ export default function Page() {
|
|||||||
// Clean up empty records to keep storage tidy
|
// Clean up empty records to keep storage tidy
|
||||||
const hasAny = Object.values(current).some(Boolean);
|
const hasAny = Object.values(current).some(Boolean);
|
||||||
if (hasAny) next[id] = current; else delete next[id];
|
if (hasAny) next[id] = current; else delete next[id];
|
||||||
saveChecklist(next);
|
// fire-and-forget save to server; fallback handled in lib
|
||||||
|
void saveChecklistServer(next);
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,27 +18,23 @@ export function loadChecklistV1(): Set<string> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// New storage (v2)
|
// Local-only helpers (kept for migration / fallback)
|
||||||
export function loadChecklist(): ChecklistV2 {
|
export function loadChecklistLocal(): ChecklistV2 {
|
||||||
if (typeof window === 'undefined') return {};
|
if (typeof window === 'undefined') return {};
|
||||||
try {
|
try {
|
||||||
const v2 = localStorage.getItem(CHECKLIST_KEY_V2);
|
const v2 = localStorage.getItem(CHECKLIST_KEY_V2);
|
||||||
if (v2) return JSON.parse(v2) as ChecklistV2;
|
if (v2) return JSON.parse(v2) as ChecklistV2;
|
||||||
// migrate from v1 if present
|
|
||||||
const v1 = loadChecklistV1();
|
const v1 = loadChecklistV1();
|
||||||
if (v1.size === 0) return {};
|
if (v1.size === 0) return {};
|
||||||
const migrated: ChecklistV2 = {};
|
const migrated: ChecklistV2 = {};
|
||||||
v1.forEach((id) => {
|
v1.forEach((id) => { migrated[id] = { base: true }; });
|
||||||
migrated[id] = { base: true };
|
|
||||||
});
|
|
||||||
saveChecklist(migrated);
|
|
||||||
return migrated;
|
return migrated;
|
||||||
} catch {
|
} catch {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveChecklist(state: ChecklistV2) {
|
export function saveChecklistLocal(state: ChecklistV2) {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(CHECKLIST_KEY_V2, JSON.stringify(state));
|
localStorage.setItem(CHECKLIST_KEY_V2, JSON.stringify(state));
|
||||||
@@ -46,3 +42,43 @@ export function saveChecklist(state: ChecklistV2) {
|
|||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Server-backed persistence
|
||||||
|
export async function loadChecklistServer(): Promise<ChecklistV2> {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/checklist', { cache: 'no-store' });
|
||||||
|
if (!res.ok) throw new Error('failed');
|
||||||
|
const json = await res.json();
|
||||||
|
const serverData = (json?.data || {}) as ChecklistV2;
|
||||||
|
// If server is empty, try migrate from local and push
|
||||||
|
const hasServerData = serverData && Object.keys(serverData).length > 0;
|
||||||
|
if (!hasServerData && typeof window !== 'undefined') {
|
||||||
|
const local = loadChecklistLocal();
|
||||||
|
const hasLocal = Object.keys(local).length > 0;
|
||||||
|
if (hasLocal) {
|
||||||
|
await saveChecklistServer(local);
|
||||||
|
// Clear legacy keys after successful push
|
||||||
|
try { localStorage.removeItem(CHECKLIST_KEY_V2); } catch {}
|
||||||
|
try { localStorage.removeItem(CHECKLIST_KEY_V1); } catch {}
|
||||||
|
return local;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return serverData || {};
|
||||||
|
} catch {
|
||||||
|
// As a fallback (e.g., during dev with API not ready), use local
|
||||||
|
return loadChecklistLocal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveChecklistServer(state: ChecklistV2): Promise<void> {
|
||||||
|
try {
|
||||||
|
await fetch('/api/checklist', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(state),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// best-effort: persist locally so user doesn't lose progress offline
|
||||||
|
saveChecklistLocal(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user