| "use client"; |
|
|
| import { useState, useEffect, useCallback } from "react"; |
|
|
| const cache = new Map<string, { data: any; timestamp: number }>(); |
| const CACHE_TTL = 5 * 60 * 1000; |
| const CACHE_KEY_PREFIX = "citytracker_cache_"; |
|
|
| |
| const getFullUrl = (url: string) => { |
| const baseUrl = process.env.NEXT_PUBLIC_API_URL || ""; |
| return url.startsWith("http") ? url : `${baseUrl}${url}`; |
| }; |
|
|
| export function useCachedFetch<T>(url: string, options?: RequestInit) { |
| const fullUrl = url ? getFullUrl(url) : ""; |
|
|
| |
| const [data, setData] = useState<T | null>(() => { |
| if (!fullUrl) return null; |
| |
| if (cache.has(fullUrl)) { |
| return cache.get(fullUrl)!.data; |
| } |
| |
| if (typeof window !== "undefined") { |
| try { |
| const stored = localStorage.getItem(CACHE_KEY_PREFIX + fullUrl); |
| if (stored) { |
| const parsed = JSON.parse(stored); |
| |
| cache.set(fullUrl, parsed); |
| return parsed.data; |
| } |
| } catch (e) { |
| console.warn("Cache parse error", e); |
| } |
| } |
| return null; |
| }); |
|
|
| |
| const [loading, setLoading] = useState(() => { |
| if (!fullUrl) return true; |
| |
| |
| |
| const cached = cache.get(fullUrl); |
| if (cached && Date.now() - cached.timestamp < CACHE_TTL) { |
| return false; |
| } |
| return true; |
| }); |
|
|
| const [error, setError] = useState<Error | null>(null); |
|
|
| const fetchData = useCallback(async (isRevalidating = false) => { |
| if (!fullUrl) return; |
|
|
| const cached = cache.get(fullUrl); |
| const isCacheValid = cached && (Date.now() - cached.timestamp < CACHE_TTL); |
|
|
| |
| |
| |
| |
| if (!isRevalidating) { |
| if (isCacheValid) { |
| setLoading(false); |
| } else { |
| setLoading(true); |
| } |
| } |
|
|
| try { |
| const token = typeof window !== "undefined" ? localStorage.getItem("token") : null; |
| const headers = { |
| "Content-Type": "application/json", |
| ...(token ? { Authorization: `Bearer ${token}` } : {}), |
| ...options?.headers, |
| }; |
|
|
| const res = await fetch(fullUrl, { ...options, headers }); |
| |
| if (!res.ok) { |
| |
| if (res.status === 401) localStorage.removeItem("token"); |
| throw new Error(`Fetch error: ${res.status}`); |
| } |
|
|
| const freshData = await res.json(); |
| const cacheEntry = { data: freshData, timestamp: Date.now() }; |
|
|
| |
| cache.set(fullUrl, cacheEntry); |
| |
| |
| if (typeof window !== "undefined") { |
| try { |
| localStorage.setItem(CACHE_KEY_PREFIX + fullUrl, JSON.stringify(cacheEntry)); |
| } catch (e) { |
| console.warn("Quota exceeded likely", e); |
| } |
| } |
|
|
| setData(freshData); |
| setError(null); |
| } catch (err) { |
| console.error("Fetch failed:", err); |
| if (!data) setError(err as Error); |
| } finally { |
| if (!isRevalidating) setLoading(false); |
| } |
| }, [fullUrl, JSON.stringify(options)]); |
|
|
| useEffect(() => { |
| fetchData(); |
| }, [fetchData]); |
|
|
| const revalidate = () => fetchData(true); |
|
|
| return { data, loading, error, revalidate }; |
| } |
|
|