|
1 | 1 | /** |
2 | 2 | * Hook for preloading field report images based on timeline position |
| 3 | + * and tracking their load completion state |
3 | 4 | */ |
4 | 5 |
|
5 | | -import { useCallback, useEffect, useRef } from "react"; |
| 6 | +import { useCallback, useEffect, useRef, useState } from "react"; |
6 | 7 | import type { FieldReport } from "@/types"; |
7 | 8 |
|
8 | 9 | export interface UseImagePreloaderOptions { |
9 | 10 | reports: FieldReport[]; |
10 | 11 | currentDate: number; |
11 | 12 | selectedVoucherAddresses: Set<string>; |
12 | | - preloadWindowMs?: number; // Default: 7 days ahead |
| 13 | + preloadWindowMs?: number; // Default: 30 days ahead |
13 | 14 | } |
14 | 15 |
|
15 | 16 | export interface UseImagePreloaderReturn { |
| 17 | + isImageLoaded: (url: string) => boolean; |
16 | 18 | resetPreloaded: () => void; |
17 | 19 | } |
18 | 20 |
|
19 | | -const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000; |
| 21 | +const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000; |
20 | 22 |
|
21 | 23 | export function useImagePreloader({ |
22 | 24 | reports, |
23 | 25 | currentDate, |
24 | 26 | selectedVoucherAddresses, |
25 | | - preloadWindowMs = SEVEN_DAYS_MS, |
| 27 | + preloadWindowMs = THIRTY_DAYS_MS, |
26 | 28 | }: UseImagePreloaderOptions): UseImagePreloaderReturn { |
27 | 29 | const preloadedUrls = useRef<Set<string>>(new Set()); |
| 30 | + const [loadedUrls, setLoadedUrls] = useState<Set<string>>(new Set()); |
28 | 31 |
|
| 32 | + // Eagerly preload all matching report images on initial data load |
| 33 | + const eagerPreloadDone = useRef(false); |
| 34 | + useEffect(() => { |
| 35 | + if (eagerPreloadDone.current || reports.length === 0) return; |
| 36 | + eagerPreloadDone.current = true; |
| 37 | + |
| 38 | + for (const report of reports) { |
| 39 | + if (!report.image_url) continue; |
| 40 | + if (preloadedUrls.current.has(report.image_url)) continue; |
| 41 | + |
| 42 | + const hasMatch = report.vouchers.some((v) => |
| 43 | + selectedVoucherAddresses.has(v) |
| 44 | + ); |
| 45 | + if (!hasMatch) continue; |
| 46 | + |
| 47 | + const url = report.image_url; |
| 48 | + preloadedUrls.current.add(url); |
| 49 | + const img = new Image(); |
| 50 | + img.onload = () => { |
| 51 | + setLoadedUrls((prev) => { |
| 52 | + const next = new Set(prev); |
| 53 | + next.add(url); |
| 54 | + return next; |
| 55 | + }); |
| 56 | + }; |
| 57 | + img.onerror = () => { |
| 58 | + // Mark as loaded on error too so the card isn't blocked forever |
| 59 | + setLoadedUrls((prev) => { |
| 60 | + const next = new Set(prev); |
| 61 | + next.add(url); |
| 62 | + return next; |
| 63 | + }); |
| 64 | + }; |
| 65 | + img.src = url; |
| 66 | + } |
| 67 | + }, [reports, selectedVoucherAddresses]); |
| 68 | + |
| 69 | + // Window-based preloading for any new reports not yet handled |
29 | 70 | useEffect(() => { |
30 | 71 | const upcomingCutoff = currentDate + preloadWindowMs; |
31 | 72 |
|
32 | 73 | for (const report of reports) { |
33 | 74 | if (!report.image_url) continue; |
34 | 75 | if (preloadedUrls.current.has(report.image_url)) continue; |
35 | 76 |
|
36 | | - // Check if report has any vouchers matching selected ones |
37 | 77 | const hasMatch = report.vouchers.some((v) => |
38 | 78 | selectedVoucherAddresses.has(v) |
39 | 79 | ); |
40 | 80 | if (!hasMatch) continue; |
41 | 81 |
|
42 | | - // Preload if within window or currently visible (past period_from) |
43 | 82 | if (report.period_from <= upcomingCutoff) { |
| 83 | + const url = report.image_url; |
| 84 | + preloadedUrls.current.add(url); |
44 | 85 | const img = new Image(); |
45 | | - img.src = report.image_url; |
46 | | - preloadedUrls.current.add(report.image_url); |
| 86 | + img.onload = () => { |
| 87 | + setLoadedUrls((prev) => { |
| 88 | + const next = new Set(prev); |
| 89 | + next.add(url); |
| 90 | + return next; |
| 91 | + }); |
| 92 | + }; |
| 93 | + img.onerror = () => { |
| 94 | + setLoadedUrls((prev) => { |
| 95 | + const next = new Set(prev); |
| 96 | + next.add(url); |
| 97 | + return next; |
| 98 | + }); |
| 99 | + }; |
| 100 | + img.src = url; |
47 | 101 | } |
48 | 102 | } |
49 | 103 | }, [reports, currentDate, selectedVoucherAddresses, preloadWindowMs]); |
50 | 104 |
|
| 105 | + const isImageLoaded = useCallback( |
| 106 | + (url: string) => loadedUrls.has(url), |
| 107 | + [loadedUrls] |
| 108 | + ); |
| 109 | + |
51 | 110 | const resetPreloaded = useCallback(() => { |
52 | 111 | preloadedUrls.current.clear(); |
| 112 | + eagerPreloadDone.current = false; |
| 113 | + setLoadedUrls(new Set()); |
53 | 114 | }, []); |
54 | 115 |
|
55 | | - return { resetPreloaded }; |
| 116 | + return { isImageLoaded, resetPreloaded }; |
56 | 117 | } |
0 commit comments