borsa / nextjs-app /src /lib /favorites.ts
veteroner's picture
fix: read eligibility from scan work snapshot
2cecfa4
export interface FavoriteItem {
symbol: string
added_at: string
}
const FAVORITES_PREFIX = 'borsa.favorites.'
function normalizeSymbol(value: unknown): string {
return String(value || '').trim().toUpperCase()
}
/** Cached in-memory store to avoid repeated fetches within same page */
let _cache: FavoriteItem[] | null = null
let _cacheUserId: string | null = null
function invalidateCache() {
_cache = null
_cacheUserId = null
}
// --------------- Async (Supabase-backed) API ---------------
export async function loadFavoritesAsync(userId?: string | null): Promise<FavoriteItem[]> {
if (!userId) return []
if (_cache && _cacheUserId === userId) return _cache
try {
const resp = await fetch('/api/favorites')
const json = await resp.json().catch(() => null)
if (resp.ok && json?.items) {
const items: FavoriteItem[] = (json.items as Record<string, unknown>[]).map((r) => ({
symbol: String(r.symbol || ''),
added_at: String(r.created_at || r.added_at || new Date().toISOString()),
}))
items.sort((a, b) => (a.added_at < b.added_at ? 1 : -1))
_cache = items
_cacheUserId = userId
// Migrate from localStorage if Supabase was empty but localStorage has data
if (items.length === 0 && typeof window !== 'undefined') {
const key = `${FAVORITES_PREFIX}${userId}`
const stored = window.localStorage.getItem(key)
if (stored) {
try {
const local = JSON.parse(stored) as { symbol?: string; added_at?: string }[]
if (Array.isArray(local) && local.length > 0) {
for (const li of local) {
const sym = normalizeSymbol(li.symbol)
if (sym) {
await fetch('/api/favorites', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ symbol: sym }),
})
}
}
window.localStorage.removeItem(key)
invalidateCache()
return loadFavoritesAsync(userId)
}
} catch { /* ignore */ }
}
}
return items
}
} catch { /* network error */ }
// Fallback to localStorage
return loadFavorites(userId)
}
export async function addFavoriteAsync(symbol: string, _userId?: string | null): Promise<FavoriteItem[]> {
const sym = normalizeSymbol(symbol)
if (!sym) return _cache || []
try {
await fetch('/api/favorites', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ symbol: sym }),
})
invalidateCache()
} catch { /* fallback handled below */ }
return loadFavoritesAsync(_userId)
}
export async function removeFavoriteAsync(symbol: string, _userId?: string | null): Promise<FavoriteItem[]> {
const sym = normalizeSymbol(symbol)
if (!sym) return _cache || []
try {
await fetch(`/api/favorites?symbol=${encodeURIComponent(sym)}`, { method: 'DELETE' })
invalidateCache()
} catch { /* fallback */ }
return loadFavoritesAsync(_userId)
}
export async function toggleFavoriteAsync(
symbol: string,
userId?: string | null
): Promise<{ next: FavoriteItem[]; nowFavorite: boolean }> {
const sym = normalizeSymbol(symbol)
if (!sym) return { next: [], nowFavorite: false }
const items = await loadFavoritesAsync(userId)
const exists = items.some((i) => i.symbol === sym)
if (exists) {
const next = await removeFavoriteAsync(sym, userId)
return { next, nowFavorite: false }
}
const next = await addFavoriteAsync(sym, userId)
return { next, nowFavorite: true }
}
export async function isFavoriteAsync(symbol: string, userId?: string | null): Promise<boolean> {
const sym = normalizeSymbol(symbol)
if (!sym) return false
const items = await loadFavoritesAsync(userId)
return items.some((i) => i.symbol === sym)
}
// --------------- Synchronous (localStorage) fallback API ---------------
// Kept for SSR and anonymous users
function safeParseJson(value: string | null): unknown {
if (!value) return null
try { return JSON.parse(value) } catch { return null }
}
function normalizeItem(input: unknown): FavoriteItem | null {
if (!input || typeof input !== 'object') return null
const rec = input as Record<string, unknown>
const symbol = normalizeSymbol(rec.symbol)
if (!symbol) return null
const addedAt = typeof rec.added_at === 'string' && rec.added_at ? rec.added_at : new Date().toISOString()
return { symbol, added_at: addedAt }
}
export function getFavoritesKey(userId?: string | null): string {
return `${FAVORITES_PREFIX}${userId || 'anonymous'}`
}
export function loadFavorites(userId?: string | null): FavoriteItem[] {
if (typeof window === 'undefined') return []
const key = getFavoritesKey(userId)
const parsed = safeParseJson(window.localStorage.getItem(key))
if (!Array.isArray(parsed)) return []
const items = parsed.map(normalizeItem).filter(Boolean) as FavoriteItem[]
items.sort((a, b) => (a.added_at < b.added_at ? 1 : a.added_at > b.added_at ? -1 : 0))
const seen = new Set<string>()
const deduped: FavoriteItem[] = []
for (const it of items) {
if (seen.has(it.symbol)) continue
seen.add(it.symbol)
deduped.push(it)
}
return deduped
}
export function saveFavorites(items: FavoriteItem[], userId?: string | null): void {
if (typeof window === 'undefined') return
const key = getFavoritesKey(userId)
window.localStorage.setItem(key, JSON.stringify(items))
}
export function isFavorite(symbol: string, userId?: string | null): boolean {
const sym = normalizeSymbol(symbol)
if (!sym) return false
return loadFavorites(userId).some((i) => i.symbol === sym)
}
export function addFavorite(symbol: string, userId?: string | null): FavoriteItem[] {
const sym = normalizeSymbol(symbol)
if (!sym || typeof window === 'undefined') return []
const existing = loadFavorites(userId)
if (existing.some((i) => i.symbol === sym)) return existing
const next: FavoriteItem[] = [{ symbol: sym, added_at: new Date().toISOString() }, ...existing]
saveFavorites(next, userId)
return next
}
export function removeFavorite(symbol: string, userId?: string | null): FavoriteItem[] {
const sym = normalizeSymbol(symbol)
if (!sym || typeof window === 'undefined') return []
const existing = loadFavorites(userId)
const next = existing.filter((i) => i.symbol !== sym)
saveFavorites(next, userId)
return next
}
export function toggleFavorite(symbol: string, userId?: string | null): { next: FavoriteItem[]; nowFavorite: boolean } {
const sym = normalizeSymbol(symbol)
if (!sym || typeof window === 'undefined') return { next: [], nowFavorite: false }
const existing = loadFavorites(userId)
const exists = existing.some((i) => i.symbol === sym)
if (exists) {
const next = existing.filter((i) => i.symbol !== sym)
saveFavorites(next, userId)
return { next, nowFavorite: false }
}
const next: FavoriteItem[] = [{ symbol: sym, added_at: new Date().toISOString() }, ...existing]
saveFavorites(next, userId)
return { next, nowFavorite: true }
}