import { useMemo, useEffect, useState } from 'react' import { useQuery } from '@tanstack/react-query' import axios from 'axios' import { trpc } from '../lib/trpc-client' import type { ProductPriceFlags } from '@packages/shared' import type { AllProductsApiType, AvailabilityApiType, StoresApiType, SlotsApiType, EssentialConstsApiType, BannersApiType, StoreWithProductsApiType, } from '@backend/trpc/router' import { CACHE_FILENAMES } from '@packages/shared' export const useGetEssentialConsts = () => { const query = trpc.common.essentialConsts.useQuery(undefined, { refetchInterval: 60000, }) return { ...query, refetch: query.refetch } } // --------------------------------------------------------------------------- // Persisted cache helpers — store fetched JSON keyed by its versioned URL. // Only refetch + repersist when the version (URL) changes. // --------------------------------------------------------------------------- interface PersistedCache { version: string data: T } const CACHE_STORAGE_KEYS = { products: 'cache_products', stores: 'cache_stores', slots: 'cache_slots', banners: 'cache_banners', availability: 'cache_availability', storeProducts: (storeId: number) => `cache_store_${storeId}`, } as const async function readPersistedCache(key: string): Promise | null> { try { const raw = localStorage.getItem(key) if (!raw) return null return JSON.parse(raw) as PersistedCache } catch { return null } } async function writePersistedCache(key: string, version: string, data: T): Promise { try { localStorage.setItem(key, JSON.stringify({ version, data })) } catch (e) { console.error('writePersistedCache error:', e) } } function usePersistedCache(storageKey: string, version: string | null) { const [initialData, setInitialData] = useState(undefined) const [isReady, setIsReady] = useState(false) useEffect(() => { let cancelled = false setIsReady(false) setInitialData(undefined) if (!version) { setIsReady(true) return } readPersistedCache(storageKey).then((persisted) => { if (cancelled) return if (persisted && persisted.version === version) { setInitialData(persisted.data) } setIsReady(true) }) return () => { cancelled = true } }, [storageKey, version]) return { initialData, isReady } } type ProductsResponse = AllProductsApiType type StoresResponse = StoresApiType type SlotsResponse = SlotsApiType type BannersResponse = BannersApiType type StoreWithProductsResponse = StoreWithProductsApiType type AvailabilityResponse = AvailabilityApiType type BaseProduct = AllProductsApiType['products'][number] type AvailabilityEntry = AvailabilityApiType['availability'][number] export type MergedProduct = BaseProduct & ProductPriceFlags function useCacheUrl(filename: string): string | null { const { data: essentialConsts } = useGetEssentialConsts() const assetsDomain = essentialConsts?.assetsDomain const apiCacheKey = essentialConsts?.apiCacheKey const cacheVersion = essentialConsts?.cacheVersion if ( !assetsDomain || !apiCacheKey || cacheVersion === undefined || cacheVersion === null ) { return null } return `${assetsDomain}${apiCacheKey}/v-${cacheVersion}/${filename}` } function useAvailabilityCacheUrl(): string | null { const { data: essentialConsts } = useGetEssentialConsts() const assetsDomain = essentialConsts?.assetsDomain const availabilityVersionNum = essentialConsts?.availabilityVersionNum if (!assetsDomain || availabilityVersionNum === undefined || availabilityVersionNum === null) { return null } return `${assetsDomain}av-${availabilityVersionNum}/${CACHE_FILENAMES.availability}` } function useSlotsCacheUrl(): string | null { const { data: essentialConsts } = useGetEssentialConsts() const assetsDomain = essentialConsts?.assetsDomain const slotsVersionNum = essentialConsts?.slotsVersionNum if (!assetsDomain || slotsVersionNum === undefined || slotsVersionNum === null) { return null } return `${assetsDomain}slots/v-${slotsVersionNum}/${CACHE_FILENAMES.slots}` } export function useAllProducts() { const cacheUrl = useCacheUrl(CACHE_FILENAMES.products) const { data: availabilityData } = useAvailability() const version = cacheUrl ?? '' const { initialData, isReady } = usePersistedCache(CACHE_STORAGE_KEYS.products, version) const productsQuery = useQuery({ queryKey: ['all-products', version], queryFn: async () => { if (!cacheUrl) { throw new Error('Cache URL not available') } const response = await axios.get(cacheUrl) await writePersistedCache(CACHE_STORAGE_KEYS.products, version, response.data) return response.data }, staleTime: 60000, enabled: !!cacheUrl && isReady, placeholderData: initialData, }) const mergedProducts = useMemo(() => { const rawProducts = productsQuery.data?.products || [] const availabilityById: Record = {} availabilityData?.availability?.forEach((entry: AvailabilityEntry) => { availabilityById[entry.id] = entry }) return rawProducts.map((product) => { const availability = availabilityById[product.id] return { ...product, price: availability ? Number(availability.price) : 0, marketPrice: availability?.marketPrice != null ? Number(availability.marketPrice) : null, flashPrice: availability?.flashPrice ?? null, isFlashAvailable: availability?.isFlashAvailable ?? false, isOutOfStock: availability?.isOutOfStock ?? false, isSuspended: availability?.isSuspended ?? false, } }) }, [productsQuery.data, availabilityData]) const mergedData = useMemo(() => { if (!productsQuery.data) return undefined return { ...productsQuery.data, products: mergedProducts, } as ProductsResponse & { products: MergedProduct[] } }, [productsQuery.data, mergedProducts]) return { ...productsQuery, data: mergedData, } } export function useAvailability() { const cacheUrl = useAvailabilityCacheUrl() const version = cacheUrl ?? '' const { initialData, isReady } = usePersistedCache(CACHE_STORAGE_KEYS.availability, version) return useQuery({ queryKey: ['availability', version], queryFn: async () => { if (!cacheUrl) { throw new Error('Cache URL not available') } const response = await axios.get(cacheUrl) await writePersistedCache(CACHE_STORAGE_KEYS.availability, version, response.data) return response.data }, staleTime: 60000, enabled: !!cacheUrl && isReady, placeholderData: initialData, }) } export function useStores() { const cacheUrl = useCacheUrl(CACHE_FILENAMES.stores) const version = cacheUrl ?? '' const { initialData, isReady } = usePersistedCache(CACHE_STORAGE_KEYS.stores, version) return useQuery({ queryKey: ['stores', version], queryFn: async () => { if (!cacheUrl) { throw new Error('Cache URL not available') } const response = await axios.get(cacheUrl) await writePersistedCache(CACHE_STORAGE_KEYS.stores, version, response.data) return response.data }, staleTime: 60000, enabled: !!cacheUrl && isReady, placeholderData: initialData, }) } export function useSlots() { const cacheUrl = useSlotsCacheUrl() const version = cacheUrl ?? '' const { initialData, isReady } = usePersistedCache(CACHE_STORAGE_KEYS.slots, version) return useQuery({ queryKey: ['slots', version], queryFn: async () => { if (!cacheUrl) { throw new Error('Cache URL not available') } const response = await axios.get(cacheUrl + '?v=123') await writePersistedCache(CACHE_STORAGE_KEYS.slots, version, response.data) return response.data }, staleTime: 60000, enabled: !!cacheUrl && isReady, placeholderData: initialData, }) } export function useBanners() { const cacheUrl = useCacheUrl(CACHE_FILENAMES.banners) const version = cacheUrl ?? '' const { initialData, isReady } = usePersistedCache(CACHE_STORAGE_KEYS.banners, version) return useQuery({ queryKey: ['banners', version], queryFn: async () => { if (!cacheUrl) { throw new Error('Cache URL not available') } const response = await axios.get(cacheUrl) await writePersistedCache(CACHE_STORAGE_KEYS.banners, version, response.data) return response.data }, staleTime: 60000, enabled: !!cacheUrl && isReady, placeholderData: initialData, }) } export function useStoreWithProducts(storeId: number) { const { data: essentialConsts } = useGetEssentialConsts() const assetsDomain = essentialConsts?.assetsDomain const apiCacheKey = essentialConsts?.apiCacheKey const cacheVersion = essentialConsts?.cacheVersion const cacheUrl = assetsDomain && apiCacheKey ? `${assetsDomain}${apiCacheKey}/v-${cacheVersion}/stores/${storeId}.json` : null const version = cacheUrl ?? '' const { initialData, isReady } = usePersistedCache(CACHE_STORAGE_KEYS.storeProducts(storeId), version) return useQuery({ queryKey: ['store-with-products', storeId, version], queryFn: async () => { if (!cacheUrl) { throw new Error('Cache URL not available') } const response = await axios.get(cacheUrl) await writePersistedCache(CACHE_STORAGE_KEYS.storeProducts(storeId), version, response.data) return response.data }, staleTime: 60000, enabled: !!cacheUrl && isReady, placeholderData: initialData, }) }