Skip to content

Commit bc3deb3

Browse files
committed
feat: enhance field reports handling in dashboard with image preloading and visibility filtering
1 parent c9416c7 commit bc3deb3

5 files changed

Lines changed: 95 additions & 19 deletions

File tree

components/dashboard/Dashboard.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -330,19 +330,20 @@ export function Dashboard() {
330330
[selectedTokens]
331331
);
332332

333-
// Field reports filtering
334-
const { visibleReports, dismissReport, resetDismissed } = useFieldReports({
333+
// Image preloading for field reports
334+
const { isImageLoaded, resetPreloaded } = useImagePreloader({
335335
reports: reportsData?.reports ?? [],
336336
currentDate: date,
337337
selectedVoucherAddresses,
338-
maxVisible: 3,
339338
});
340339

341-
// Image preloading for field reports
342-
const { resetPreloaded } = useImagePreloader({
340+
// Field reports filtering
341+
const { visibleReports, dismissReport, resetDismissed } = useFieldReports({
343342
reports: reportsData?.reports ?? [],
344343
currentDate: date,
345344
selectedVoucherAddresses,
345+
isImageLoaded,
346+
maxVisible: 3,
346347
});
347348

348349
// Reset dismissed reports and preloaded images when animation restarts from beginning

components/dashboard/FieldReportCard.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Individual field report card with animations
33
*/
44

5-
import React from "react";
5+
import React, { useState } from "react";
66
import { CloseIcon } from "@components/icons";
77
import type { VisibleReport } from "@/types";
88

@@ -12,6 +12,9 @@ export interface FieldReportCardProps {
1212
}
1313

1414
export function FieldReportCard({ report, onDismiss }: FieldReportCardProps) {
15+
const [imgLoaded, setImgLoaded] = useState(false);
16+
const [imgError, setImgError] = useState(false);
17+
1518
const handleDismiss = (e: React.MouseEvent) => {
1619
e.preventDefault();
1720
e.stopPropagation();
@@ -26,12 +29,17 @@ export function FieldReportCard({ report, onDismiss }: FieldReportCardProps) {
2629
className="block w-72 max-w-[calc(100vw-2rem)] bg-white/95 backdrop-blur-sm rounded-lg shadow-xl border border-gray-200 overflow-hidden hover:shadow-2xl hover:scale-[1.02] transition-all cursor-pointer"
2730
>
2831
{/* Image */}
29-
{report.image_url && (
32+
{report.image_url && !imgError && (
3033
<div className="relative h-32 w-full">
34+
{!imgLoaded && (
35+
<div className="absolute inset-0 bg-gray-200 animate-pulse" />
36+
)}
3137
<img
3238
src={report.image_url}
3339
alt={report.title}
34-
className="w-full h-full object-cover"
40+
className={`w-full h-full object-cover transition-opacity duration-300 ${imgLoaded ? "opacity-100" : "opacity-0"}`}
41+
onLoad={() => setImgLoaded(true)}
42+
onError={() => setImgError(true)}
3543
/>
3644
<div className="absolute inset-0 bg-gradient-to-t from-black/30 to-transparent" />
3745
<button

hooks/dashboard/useFieldReports.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export interface UseFieldReportsOptions {
1010
reports: FieldReport[];
1111
currentDate: number;
1212
selectedVoucherAddresses: Set<string>;
13+
isImageLoaded?: (url: string) => boolean;
1314
maxVisible?: number;
1415
}
1516

@@ -24,6 +25,7 @@ export function useFieldReports({
2425
reports,
2526
currentDate,
2627
selectedVoucherAddresses,
28+
isImageLoaded,
2729
maxVisible = 3,
2830
}: UseFieldReportsOptions): UseFieldReportsReturn {
2931
const [dismissedIds, setDismissedIds] = useState<Set<number>>(new Set());
@@ -53,6 +55,10 @@ export function useFieldReports({
5355
// Skip future reports
5456
if (!hasStarted) continue;
5557

58+
// Skip reports whose image hasn't loaded yet
59+
if (isImageLoaded && report.image_url && !isImageLoaded(report.image_url))
60+
continue;
61+
5662
// Determine visibility state
5763
const wasVisible = prevVisibleRef.current.has(report.id);
5864
let visibility: ReportVisibility;
@@ -73,7 +79,7 @@ export function useFieldReports({
7379

7480
// Limit visible count
7581
return active.slice(0, maxVisible);
76-
}, [reports, currentDate, selectedVoucherAddresses, dismissedIds, maxVisible]);
82+
}, [reports, currentDate, selectedVoucherAddresses, dismissedIds, isImageLoaded, maxVisible]);
7783

7884
// Update tracking ref after render
7985
useEffect(() => {
Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,117 @@
11
/**
22
* Hook for preloading field report images based on timeline position
3+
* and tracking their load completion state
34
*/
45

5-
import { useCallback, useEffect, useRef } from "react";
6+
import { useCallback, useEffect, useRef, useState } from "react";
67
import type { FieldReport } from "@/types";
78

89
export interface UseImagePreloaderOptions {
910
reports: FieldReport[];
1011
currentDate: number;
1112
selectedVoucherAddresses: Set<string>;
12-
preloadWindowMs?: number; // Default: 7 days ahead
13+
preloadWindowMs?: number; // Default: 30 days ahead
1314
}
1415

1516
export interface UseImagePreloaderReturn {
17+
isImageLoaded: (url: string) => boolean;
1618
resetPreloaded: () => void;
1719
}
1820

19-
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
21+
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
2022

2123
export function useImagePreloader({
2224
reports,
2325
currentDate,
2426
selectedVoucherAddresses,
25-
preloadWindowMs = SEVEN_DAYS_MS,
27+
preloadWindowMs = THIRTY_DAYS_MS,
2628
}: UseImagePreloaderOptions): UseImagePreloaderReturn {
2729
const preloadedUrls = useRef<Set<string>>(new Set());
30+
const [loadedUrls, setLoadedUrls] = useState<Set<string>>(new Set());
2831

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
2970
useEffect(() => {
3071
const upcomingCutoff = currentDate + preloadWindowMs;
3172

3273
for (const report of reports) {
3374
if (!report.image_url) continue;
3475
if (preloadedUrls.current.has(report.image_url)) continue;
3576

36-
// Check if report has any vouchers matching selected ones
3777
const hasMatch = report.vouchers.some((v) =>
3878
selectedVoucherAddresses.has(v)
3979
);
4080
if (!hasMatch) continue;
4181

42-
// Preload if within window or currently visible (past period_from)
4382
if (report.period_from <= upcomingCutoff) {
83+
const url = report.image_url;
84+
preloadedUrls.current.add(url);
4485
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;
47101
}
48102
}
49103
}, [reports, currentDate, selectedVoucherAddresses, preloadWindowMs]);
50104

105+
const isImageLoaded = useCallback(
106+
(url: string) => loadedUrls.has(url),
107+
[loadedUrls]
108+
);
109+
51110
const resetPreloaded = useCallback(() => {
52111
preloadedUrls.current.clear();
112+
eagerPreloadDone.current = false;
113+
setLoadedUrls(new Set());
53114
}, []);
54115

55-
return { resetPreloaded };
116+
return { isImageLoaded, resetPreloaded };
56117
}

tsconfig.tsbuildinfo

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)