| export interface FavoriteItem { |
| symbol: string |
| added_at: string |
| } |
|
|
| const FAVORITES_PREFIX = 'borsa.favorites.' |
|
|
| function normalizeSymbol(value: unknown): string { |
| return String(value || '').trim().toUpperCase() |
| } |
|
|
| |
| let _cache: FavoriteItem[] | null = null |
| let _cacheUserId: string | null = null |
|
|
| function invalidateCache() { |
| _cache = null |
| _cacheUserId = null |
| } |
|
|
| |
|
|
| 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 |
| |
| 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 { } |
| } |
| } |
| return items |
| } |
| } catch { } |
|
|
| |
| 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 { } |
| 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 { } |
| 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) |
| } |
|
|
| |
| |
|
|
| 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 } |
| } |
|
|