"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; export interface PinnedPair { fromCode: string; toCode: string; } const PINNED_PAIRS_STORAGE_KEY = "nexcurrency:pinned-pairs:v1"; const CODE_PATTERN = /^[A-Z0-9]{2,12}$/; function normalizeCode(value: unknown): string | null { if (typeof value !== "string") { return null; } const normalized = value.trim().toUpperCase(); if (!CODE_PATTERN.test(normalized)) { return null; } return normalized; } function normalizePair(value: unknown): PinnedPair | null { if (!value || typeof value !== "object") { return null; } const record = value as Record; const fromCode = normalizeCode(record.fromCode); const toCode = normalizeCode(record.toCode); if (!fromCode || !toCode || fromCode === toCode) { return null; } return { fromCode, toCode }; } function toPairKey(pair: PinnedPair): string { return `${pair.fromCode}:${pair.toCode}`; } function dedupeAndClampPairs(pairs: PinnedPair[], limit: number): PinnedPair[] { const keys = new Set(); const normalized: PinnedPair[] = []; for (const pair of pairs) { const key = toPairKey(pair); if (keys.has(key)) { continue; } keys.add(key); normalized.push(pair); if (normalized.length >= limit) { break; } } return normalized; } export function usePinnedPairs(limit = 6) { const [pinnedPairs, setPinnedPairs] = useState([]); const [isReady, setIsReady] = useState(false); const isLimitReached = pinnedPairs.length >= limit; useEffect(() => { if (typeof window === "undefined") { return; } try { const raw = window.localStorage.getItem(PINNED_PAIRS_STORAGE_KEY); if (!raw) { setPinnedPairs([]); setIsReady(true); return; } const parsed = JSON.parse(raw) as unknown; const parsedPairs = Array.isArray(parsed) ? parsed.map(normalizePair).filter((pair): pair is PinnedPair => pair !== null) : []; setPinnedPairs(dedupeAndClampPairs(parsedPairs, limit)); } catch { setPinnedPairs([]); } finally { setIsReady(true); } }, [limit]); useEffect(() => { if (!isReady || typeof window === "undefined") { return; } try { window.localStorage.setItem( PINNED_PAIRS_STORAGE_KEY, JSON.stringify(pinnedPairs), ); } catch { // Intentionally ignored: localStorage may be unavailable. } }, [isReady, pinnedPairs]); const isPinnedPair = useCallback( (pair: PinnedPair) => pinnedPairs.some((entry) => toPairKey(entry) === toPairKey(pair)), [pinnedPairs], ); const addPinnedPair = useCallback( (pair: PinnedPair) => { const normalized = normalizePair(pair); if (!normalized) { return; } setPinnedPairs((current) => { const exists = current.some( (entry) => toPairKey(entry) === toPairKey(normalized), ); if (!exists && current.length >= limit) { return current; } return dedupeAndClampPairs( [ normalized, ...current.filter((entry) => toPairKey(entry) !== toPairKey(normalized)), ], limit, ); }); }, [limit], ); const removePinnedPair = useCallback((pair: PinnedPair) => { const normalized = normalizePair(pair); if (!normalized) { return; } setPinnedPairs((current) => current.filter((entry) => toPairKey(entry) !== toPairKey(normalized)), ); }, []); const togglePinnedPair = useCallback( (pair: PinnedPair) => { if (isPinnedPair(pair)) { removePinnedPair(pair); return; } addPinnedPair(pair); }, [addPinnedPair, isPinnedPair, removePinnedPair], ); return useMemo( () => ({ pinnedPairs, isReady, isLimitReached, isPinnedPair, addPinnedPair, removePinnedPair, togglePinnedPair, limit, }), [ pinnedPairs, isReady, isLimitReached, isPinnedPair, addPinnedPair, removePinnedPair, togglePinnedPair, limit, ], ); }