freshyo/apps/web-ui/src/hooks/prominent-api-hooks.ts
2026-09-06 18:05:20 +05:30

311 lines
9.9 KiB
TypeScript

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<T> {
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<T>(key: string): Promise<PersistedCache<T> | null> {
try {
const raw = localStorage.getItem(key)
if (!raw) return null
return JSON.parse(raw) as PersistedCache<T>
} catch {
return null
}
}
async function writePersistedCache<T>(key: string, version: string, data: T): Promise<void> {
try {
localStorage.setItem(key, JSON.stringify({ version, data }))
} catch (e) {
console.error('writePersistedCache error:', e)
}
}
function usePersistedCache<T>(storageKey: string, version: string | null) {
const [initialData, setInitialData] = useState<T | undefined>(undefined)
const [isReady, setIsReady] = useState(false)
useEffect(() => {
let cancelled = false
setIsReady(false)
setInitialData(undefined)
if (!version) {
setIsReady(true)
return
}
readPersistedCache<T>(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<ProductsResponse>(CACHE_STORAGE_KEYS.products, version)
const productsQuery = useQuery<ProductsResponse>({
queryKey: ['all-products', version],
queryFn: async () => {
if (!cacheUrl) {
throw new Error('Cache URL not available')
}
const response = await axios.get<ProductsResponse>(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<number, AvailabilityEntry> = {}
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<AvailabilityResponse>(CACHE_STORAGE_KEYS.availability, version)
return useQuery<AvailabilityResponse>({
queryKey: ['availability', version],
queryFn: async () => {
if (!cacheUrl) {
throw new Error('Cache URL not available')
}
const response = await axios.get<AvailabilityResponse>(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<StoresResponse>(CACHE_STORAGE_KEYS.stores, version)
return useQuery<StoresResponse>({
queryKey: ['stores', version],
queryFn: async () => {
if (!cacheUrl) {
throw new Error('Cache URL not available')
}
const response = await axios.get<StoresResponse>(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<SlotsResponse>(CACHE_STORAGE_KEYS.slots, version)
return useQuery<SlotsResponse>({
queryKey: ['slots', version],
queryFn: async () => {
if (!cacheUrl) {
throw new Error('Cache URL not available')
}
const response = await axios.get<SlotsResponse>(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<BannersResponse>(CACHE_STORAGE_KEYS.banners, version)
return useQuery<BannersResponse>({
queryKey: ['banners', version],
queryFn: async () => {
if (!cacheUrl) {
throw new Error('Cache URL not available')
}
const response = await axios.get<BannersResponse>(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<StoreWithProductsResponse>(CACHE_STORAGE_KEYS.storeProducts(storeId), version)
return useQuery<StoreWithProductsResponse>({
queryKey: ['store-with-products', storeId, version],
queryFn: async () => {
if (!cacheUrl) {
throw new Error('Cache URL not available')
}
const response = await axios.get<StoreWithProductsResponse>(cacheUrl)
await writePersistedCache(CACHE_STORAGE_KEYS.storeProducts(storeId), version, response.data)
return response.data
},
staleTime: 60000,
enabled: !!cacheUrl && isReady,
placeholderData: initialData,
})
}