refactor
This commit is contained in:
@@ -12,6 +12,14 @@ async function ensureDataDir() {
|
|||||||
try { await fs.mkdir(DATA_DIR, { recursive: true }); } catch {}
|
try { await fs.mkdir(DATA_DIR, { recursive: true }); } catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyReverseOverrides(payload: any, reverseMap: Record<string, boolean>): any {
|
||||||
|
if (!payload) return payload;
|
||||||
|
if (Array.isArray(payload?.data)) {
|
||||||
|
return { ...payload, data: payload.data.map((c: any) => (reverseMap[c.id] ? { ...c, variants: { ...(c.variants || {}), reverseHolofoil: true } } : c)) };
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
const { searchParams } = new URL(req.url);
|
const { searchParams } = new URL(req.url);
|
||||||
const q = searchParams.get('q') ?? undefined;
|
const q = searchParams.get('q') ?? undefined;
|
||||||
@@ -34,16 +42,7 @@ export async function GET(req: NextRequest) {
|
|||||||
try {
|
try {
|
||||||
const raw = await fs.readFile(CARDS_FILE, 'utf-8');
|
const raw = await fs.readFile(CARDS_FILE, 'utf-8');
|
||||||
if (raw) {
|
if (raw) {
|
||||||
const json = JSON.parse(raw);
|
const json = applyReverseOverrides(JSON.parse(raw), reverseMap);
|
||||||
// Apply overrides on the fly to cached payload
|
|
||||||
if (Array.isArray(json?.data)) {
|
|
||||||
json.data = json.data.map((c: any) => {
|
|
||||||
if (reverseMap[c.id]) {
|
|
||||||
c.variants = { ...(c.variants || {}), reverseHolofoil: true };
|
|
||||||
}
|
|
||||||
return c;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const ts = json?.updatedAt ? Date.parse(json.updatedAt) : NaN;
|
const ts = json?.updatedAt ? Date.parse(json.updatedAt) : NaN;
|
||||||
const fresh = Number.isFinite(ts) && (Date.now() - ts) < TTL_MS;
|
const fresh = Number.isFinite(ts) && (Date.now() - ts) < TTL_MS;
|
||||||
if (fresh) {
|
if (fresh) {
|
||||||
@@ -56,9 +55,7 @@ export async function GET(req: NextRequest) {
|
|||||||
// Fetch fresh from API
|
// Fetch fresh from API
|
||||||
const data = await fetchMagikarpCards({ q, page, pageSize });
|
const data = await fetchMagikarpCards({ q, page, pageSize });
|
||||||
// Apply overrides to fresh data
|
// Apply overrides to fresh data
|
||||||
const patched = Array.isArray(data?.data)
|
const patched = applyReverseOverrides(data, reverseMap);
|
||||||
? { ...data, data: data.data.map((c: any) => (reverseMap[c.id] ? { ...c, variants: { ...(c.variants || {}), reverseHolofoil: true } } : c)) }
|
|
||||||
: data;
|
|
||||||
const payload = { ...patched, cached: false, updatedAt: new Date().toISOString() };
|
const payload = { ...patched, cached: false, updatedAt: new Date().toISOString() };
|
||||||
// Write cache best-effort
|
// Write cache best-effort
|
||||||
try { await fs.writeFile(CARDS_FILE, JSON.stringify(payload, null, 2), 'utf-8'); } catch {}
|
try { await fs.writeFile(CARDS_FILE, JSON.stringify(payload, null, 2), 'utf-8'); } catch {}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ 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';
|
||||||
import SetFilter from '@/components/SetFilter';
|
import SetFilter from '@/components/SetFilter';
|
||||||
|
import { sortCards, detectVariants } from '@/lib/cards';
|
||||||
|
import { loadOverridesClient, REVERSE_OVERRIDE_EVENT } from '@/lib/overrides';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const [cards, setCards] = useState<TcgCard[]>([]);
|
const [cards, setCards] = useState<TcgCard[]>([]);
|
||||||
@@ -32,13 +34,8 @@ export default function Page() {
|
|||||||
// Load reverse overrides at start
|
// Load reverse overrides at start
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
const map = await loadOverridesClient();
|
||||||
const res = await fetch('/api/magikarp/override', { cache: 'no-store' });
|
if (map && typeof map === 'object') setOverrides(map);
|
||||||
if (!res.ok) return;
|
|
||||||
const json = await res.json();
|
|
||||||
const map = (json?.data?.reverseHolofoil || {}) as Record<string, boolean>;
|
|
||||||
if (map && typeof map === 'object') setOverrides(map);
|
|
||||||
} catch {}
|
|
||||||
})();
|
})();
|
||||||
// Listen for UI-initiated override changes
|
// Listen for UI-initiated override changes
|
||||||
function onOverrideChanged(ev: Event) {
|
function onOverrideChanged(ev: Event) {
|
||||||
@@ -49,8 +46,8 @@ export default function Page() {
|
|||||||
setOverrides((prev) => ({ ...prev, [id]: !!reverse }));
|
setOverrides((prev) => ({ ...prev, [id]: !!reverse }));
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
window.addEventListener('reverse-override-changed', onOverrideChanged as EventListener);
|
window.addEventListener(REVERSE_OVERRIDE_EVENT, onOverrideChanged as EventListener);
|
||||||
return () => window.removeEventListener('reverse-override-changed', onOverrideChanged as EventListener);
|
return () => window.removeEventListener(REVERSE_OVERRIDE_EVENT, onOverrideChanged as EventListener);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -61,23 +58,7 @@ export default function Page() {
|
|||||||
const res = await fetch('/api/magikarp?pageSize=250');
|
const res = await fetch('/api/magikarp?pageSize=250');
|
||||||
if (!res.ok) throw new Error('Failed to fetch');
|
if (!res.ok) throw new Error('Failed to fetch');
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
const list: TcgCard[] = json.data || [];
|
const list: TcgCard[] = (json.data || []).slice().sort(sortCards);
|
||||||
list.sort((a, b) => {
|
|
||||||
const da = Date.parse(a.set.releaseDate || '');
|
|
||||||
const db = Date.parse(b.set.releaseDate || '');
|
|
||||||
if (Number.isFinite(da) && Number.isFinite(db)) {
|
|
||||||
if (da !== db) return da - db; // oldest first
|
|
||||||
} else if (Number.isFinite(da)) {
|
|
||||||
return -1;
|
|
||||||
} else if (Number.isFinite(db)) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
// tie-breaker by card number if numeric
|
|
||||||
const na = parseInt(a.number, 10);
|
|
||||||
const nb = parseInt(b.number, 10);
|
|
||||||
if (Number.isFinite(na) && Number.isFinite(nb)) return na - nb;
|
|
||||||
return a.number.localeCompare(b.number);
|
|
||||||
});
|
|
||||||
setCards(list);
|
setCards(list);
|
||||||
if (json.note) setNote(String(json.note));
|
if (json.note) setNote(String(json.note));
|
||||||
if (json.updatedAt) setUpdatedAt(String(json.updatedAt));
|
if (json.updatedAt) setUpdatedAt(String(json.updatedAt));
|
||||||
@@ -98,22 +79,7 @@ export default function Page() {
|
|||||||
const res = await fetch('/api/magikarp?pageSize=250&refresh=1', { cache: 'no-store' });
|
const res = await fetch('/api/magikarp?pageSize=250&refresh=1', { cache: 'no-store' });
|
||||||
if (!res.ok) throw new Error(`Failed to refresh: ${res.body}`);
|
if (!res.ok) throw new Error(`Failed to refresh: ${res.body}`);
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
const list: TcgCard[] = json.data || [];
|
const list: TcgCard[] = (json.data || []).slice().sort(sortCards);
|
||||||
list.sort((a, b) => {
|
|
||||||
const da = Date.parse(a.set.releaseDate || '');
|
|
||||||
const db = Date.parse(b.set.releaseDate || '');
|
|
||||||
if (Number.isFinite(da) && Number.isFinite(db)) {
|
|
||||||
if (da !== db) return da - db; // oldest first
|
|
||||||
} else if (Number.isFinite(da)) {
|
|
||||||
return -1;
|
|
||||||
} else if (Number.isFinite(db)) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
const na = parseInt(a.number, 10);
|
|
||||||
const nb = parseInt(b.number, 10);
|
|
||||||
if (Number.isFinite(na) && Number.isFinite(nb)) return na - nb;
|
|
||||||
return a.number.localeCompare(b.number);
|
|
||||||
});
|
|
||||||
setCards(list);
|
setCards(list);
|
||||||
if (json.note) setNote(String(json.note)); else setNote(null);
|
if (json.note) setNote(String(json.note)); else setNote(null);
|
||||||
if (json.updatedAt) setUpdatedAt(String(json.updatedAt));
|
if (json.updatedAt) setUpdatedAt(String(json.updatedAt));
|
||||||
@@ -144,12 +110,7 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function availableVariants(card: TcgCard): VariantKey[] {
|
function availableVariants(card: TcgCard): VariantKey[] {
|
||||||
const list: VariantKey[] = ['base'];
|
return detectVariants(card, overrides);
|
||||||
const hasHolo = !!(card.variants?.holofoil || (card as any).tcgplayer?.prices?.holofoil);
|
|
||||||
const hasReverse = !!(card.variants?.reverseHolofoil || (card as any).tcgplayer?.prices?.reverseHolofoil) || !!overrides[card.id];
|
|
||||||
if (hasHolo) list.push('holofoil');
|
|
||||||
if (hasReverse) list.push('reverseHolofoil');
|
|
||||||
return list;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFullyCollected(card: TcgCard): boolean {
|
function isFullyCollected(card: TcgCard): boolean {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { TcgCard } from '@/types/pokemon';
|
|||||||
import SetBadge from './SetBadge';
|
import SetBadge from './SetBadge';
|
||||||
import type { VariantKey, VariantState } from '@/lib/checklist';
|
import type { VariantKey, VariantState } from '@/lib/checklist';
|
||||||
import { useUsdGbpRate } from '@/lib/useExchangeRate';
|
import { useUsdGbpRate } from '@/lib/useExchangeRate';
|
||||||
|
import { dispatchReverseOverride, loadOverridesClient } from '@/lib/overrides';
|
||||||
|
|
||||||
export default function CardItem({ card, checked, onToggle }:{ card: TcgCard; checked: VariantState; onToggle: (id:string, key: VariantKey)=>void }) {
|
export default function CardItem({ card, checked, onToggle }:{ card: TcgCard; checked: VariantState; onToggle: (id:string, key: VariantKey)=>void }) {
|
||||||
const rate = useUsdGbpRate();
|
const rate = useUsdGbpRate();
|
||||||
@@ -42,19 +43,12 @@ export default function CardItem({ card, checked, onToggle }:{ card: TcgCard; ch
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
const map = await loadOverridesClient();
|
||||||
const res = await fetch('/api/magikarp/override', { cache: 'no-store' });
|
if (!cancelled && map && typeof map === 'object') {
|
||||||
if (!res.ok) return;
|
if (map[card.id]) {
|
||||||
const json = await res.json();
|
setReverseOverride(true);
|
||||||
const map = json?.data?.reverseHolofoil || {};
|
setReverseRemoved(false);
|
||||||
if (!cancelled && map && typeof map === 'object') {
|
|
||||||
if (map[card.id]) {
|
|
||||||
setReverseOverride(true);
|
|
||||||
setReverseRemoved(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
@@ -118,7 +112,7 @@ export default function CardItem({ card, checked, onToggle }:{ card: TcgCard; ch
|
|||||||
});
|
});
|
||||||
setReverseOverride(true);
|
setReverseOverride(true);
|
||||||
// Notify app to recompute availableVariants immediately
|
// Notify app to recompute availableVariants immediately
|
||||||
try { window.dispatchEvent(new CustomEvent('reverse-override-changed', { detail: { id: card.id, reverse: true } })); } catch {}
|
dispatchReverseOverride(card.id, true);
|
||||||
} catch {}
|
} catch {}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -142,7 +136,7 @@ export default function CardItem({ card, checked, onToggle }:{ card: TcgCard; ch
|
|||||||
setReverseOverride(false);
|
setReverseOverride(false);
|
||||||
setReverseRemoved(true);
|
setReverseRemoved(true);
|
||||||
// Notify app to recompute availableVariants immediately
|
// Notify app to recompute availableVariants immediately
|
||||||
try { window.dispatchEvent(new CustomEvent('reverse-override-changed', { detail: { id: card.id, reverse: false } })); } catch {}
|
dispatchReverseOverride(card.id, false);
|
||||||
} catch {}
|
} catch {}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
28
src/lib/cards.ts
Normal file
28
src/lib/cards.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import type { TcgCard } from '@/types/pokemon';
|
||||||
|
import type { VariantKey } from '@/lib/checklist';
|
||||||
|
|
||||||
|
export function sortCards(a: TcgCard, b: TcgCard): number {
|
||||||
|
const da = Date.parse(a.set.releaseDate || '');
|
||||||
|
const db = Date.parse(b.set.releaseDate || '');
|
||||||
|
if (Number.isFinite(da) && Number.isFinite(db)) {
|
||||||
|
if (da !== db) return da - db; // oldest first
|
||||||
|
} else if (Number.isFinite(da)) {
|
||||||
|
return -1;
|
||||||
|
} else if (Number.isFinite(db)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
// tie-breaker by card number if numeric
|
||||||
|
const na = parseInt(a.number, 10);
|
||||||
|
const nb = parseInt(b.number, 10);
|
||||||
|
if (Number.isFinite(na) && Number.isFinite(nb)) return na - nb;
|
||||||
|
return a.number.localeCompare(b.number);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function detectVariants(card: TcgCard, overrides?: Record<string, boolean>): VariantKey[] {
|
||||||
|
const list: VariantKey[] = ['base'];
|
||||||
|
const hasHolo = !!(card.variants?.holofoil || (card as any).tcgplayer?.prices?.holofoil);
|
||||||
|
const hasReverse = !!(card.variants?.reverseHolofoil || (card as any).tcgplayer?.prices?.reverseHolofoil) || !!overrides?.[card.id];
|
||||||
|
if (hasHolo) list.push('holofoil');
|
||||||
|
if (hasReverse) list.push('reverseHolofoil');
|
||||||
|
return list;
|
||||||
|
}
|
||||||
19
src/lib/overrides.ts
Normal file
19
src/lib/overrides.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export type OverridesMap = Record<string, boolean>;
|
||||||
|
|
||||||
|
export const REVERSE_OVERRIDE_EVENT = 'reverse-override-changed';
|
||||||
|
|
||||||
|
export async function loadOverridesClient(): Promise<OverridesMap> {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/magikarp/override', { cache: 'no-store' });
|
||||||
|
if (!res.ok) return {};
|
||||||
|
const json = await res.json();
|
||||||
|
const map = (json?.data?.reverseHolofoil || {}) as OverridesMap;
|
||||||
|
return map && typeof map === 'object' ? map : {};
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dispatchReverseOverride(id: string, reverse: boolean): void {
|
||||||
|
try { window.dispatchEvent(new CustomEvent(REVERSE_OVERRIDE_EVENT, { detail: { id, reverse } })); } catch {}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user