refactor: remove ImageWithFallback component and replace with Next.js Image in various components

This commit is contained in:
Marco Pedone 2025-12-05 16:36:17 +01:00
parent a268afee74
commit cfbeadb93b
7 changed files with 102 additions and 34 deletions

View file

@ -1,19 +0,0 @@
import Image from "next/image";
import type { ComponentProps } from "react";
const fallbackImage = "/fallback-image.png";
export const ImageFlbk = (props: ComponentProps<typeof Image>) => {
return (
<Image
{...props}
onError={(e) => {
e.currentTarget.src = fallbackImage;
}}
onLoad={(e) => {
if (e.currentTarget.naturalWidth === 0) {
e.currentTarget.src = fallbackImage;
}
}}
/>
);
};

View file

@ -8,10 +8,10 @@ import {
Star, Star,
TrafficCone, TrafficCone,
} from "lucide-react"; } from "lucide-react";
import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { type RefObject, useEffect, useRef, useState } from "react"; import { type RefObject, useEffect, useRef, useState } from "react";
import { useIntersection } from "react-use"; import { useIntersection } from "react-use";
import { ImageFlbk } from "~/components/ImageWithFallback";
import { import {
Carousel, Carousel,
CarouselContent, CarouselContent,
@ -216,7 +216,7 @@ export const CardAnnuncio = ({
<CarouselContent> <CarouselContent>
{images?.map((img, idx) => ( {images?.map((img, idx) => (
<CarouselItem key={`${img.img}key`}> <CarouselItem key={`${img.img}key`}>
<ImageFlbk <Image
alt={t.card.alt_immagine} alt={t.card.alt_immagine}
blurDataURL={getStorageUrl({ blurDataURL={getStorageUrl({
storageId: img.thumb, storageId: img.thumb,
@ -298,7 +298,7 @@ export const CardAnnuncio = ({
) : ( ) : (
<> <>
{images && images.length > 0 && images[0] ? ( {images && images.length > 0 && images[0] ? (
<ImageFlbk <Image
alt={t.card.alt_immagine} alt={t.card.alt_immagine}
blurDataURL={getStorageUrl({ blurDataURL={getStorageUrl({
storageId: images[0].thumb, storageId: images[0].thumb,
@ -448,7 +448,7 @@ export const CarouselAnnuncio = ({
)} )}
key={`${img.img}-cF`} key={`${img.img}-cF`}
> >
<ImageFlbk <Image
alt={img.img} alt={img.img}
className={cn( className={cn(
"aspect-square max-h-92 w-full rounded-md bg-[#e6e9ec] object-cover sm:max-h-80 xl:max-h-[25rem]", "aspect-square max-h-92 w-full rounded-md bg-[#e6e9ec] object-cover sm:max-h-80 xl:max-h-[25rem]",
@ -536,7 +536,7 @@ export const CarouselAnnuncio = ({
<CarouselContent> <CarouselContent>
{immagini?.map((img, idx) => ( {immagini?.map((img, idx) => (
<CarouselItem key={`${img.img}-cD`}> <CarouselItem key={`${img.img}-cD`}>
<ImageFlbk <Image
alt={`carousel-img-${idx}`} alt={`carousel-img-${idx}`}
className="mx-auto h-[90vh] w-full rounded-md bg-[#e6e9ec] object-contain sm:w-[80vw]" className="mx-auto h-[90vh] w-full rounded-md bg-[#e6e9ec] object-contain sm:w-[80vw]"
height={1080} height={1080}

View file

@ -1,4 +1,5 @@
import { CircleCheck, ExternalLink, Trash2 } from "lucide-react"; import { CircleCheck, ExternalLink, Trash2 } from "lucide-react";
import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { getStorageUrl } from "~/lib/storage_utils"; import { getStorageUrl } from "~/lib/storage_utils";
@ -8,7 +9,6 @@ import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage";
import type { CompleteUserData } from "~/server/services/user.service"; import type { CompleteUserData } from "~/server/services/user.service";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import { Confirm } from "../confirm"; import { Confirm } from "../confirm";
import { ImageFlbk } from "../ImageWithFallback";
import { LoadingPage } from "../loading"; import { LoadingPage } from "../loading";
import { LoadingSpinner } from "../spinner"; import { LoadingSpinner } from "../spinner";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
@ -146,7 +146,7 @@ export const DocumentiUploadSection = ({
{isPending ? ( {isPending ? (
<LoadingSpinner className="size-24 stroke-neutral-300" /> <LoadingSpinner className="size-24 stroke-neutral-300" />
) : ( ) : (
<ImageFlbk <Image
alt={title} alt={title}
className="size-24 rounded-md border object-cover" className="size-24 rounded-md border object-cover"
height={96} height={96}

View file

@ -1,6 +1,6 @@
import { ExternalLink } from "lucide-react"; import { ExternalLink } from "lucide-react";
import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { ImageFlbk } from "~/components/ImageWithFallback";
import { AnnuncioActions } from "~/components/servizio/annuncio_actions"; import { AnnuncioActions } from "~/components/servizio/annuncio_actions";
import { Interactions } from "~/components/servizio/interactions"; import { Interactions } from "~/components/servizio/interactions";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
@ -52,7 +52,7 @@ export const BasicAnnuncioCard = ({
</CardHeader> </CardHeader>
<CardContent className="px-4"> <CardContent className="px-4">
<div className="flex w-full flex-col gap-4 md:flex-row md:gap-10"> <div className="flex w-full flex-col gap-4 md:flex-row md:gap-10">
<ImageFlbk <Image
alt={data.codice} alt={data.codice}
className="h-auto max-h-72 w-full rounded-md bg-[#e6e9ec] object-contain sm:max-w-xs" className="h-auto max-h-72 w-full rounded-md bg-[#e6e9ec] object-contain sm:max-w-xs"
height={1080} height={1080}

View file

@ -0,0 +1,50 @@
"use client";
import { deleteCookie } from "cookies-next";
import { useEffect } from "react";
const STALE_KEY = "stale-resource";
const RELOAD_STORAGE_KEY = "stale-img-reload";
const RELOAD_COOLDOWN_MS = 10 * 1000;
export const useStaleImageReload = () => {
useEffect(() => {
// Check for stale resource cookie
const checkStaleCookie = () => {
const hasStale = document.cookie.includes(`${STALE_KEY}=1`);
if (hasStale) {
console.log("Stale resource cookie detected");
// Clear the cookie
deleteCookie(STALE_KEY);
attemptReload();
}
};
// Check on mount
checkStaleCookie();
// Also check periodically for dynamically loaded images
const interval = setInterval(checkStaleCookie, 1000);
return () => clearInterval(interval);
}, []);
};
function attemptReload() {
const lastAttempt = sessionStorage.getItem(RELOAD_STORAGE_KEY);
const now = Date.now();
if (!lastAttempt) {
sessionStorage.setItem(RELOAD_STORAGE_KEY, now.toString());
window.location.reload();
return;
}
const timeSinceLastAttempt = now - parseInt(lastAttempt, 10);
if (timeSinceLastAttempt > RELOAD_COOLDOWN_MS) {
sessionStorage.setItem(RELOAD_STORAGE_KEY, now.toString());
window.location.reload();
}
}

View file

@ -3,6 +3,7 @@ import { env } from "~/env";
import { TOKEN_CONFIG } from "~/server/auth/configs"; import { TOKEN_CONFIG } from "~/server/auth/configs";
import { verifyToken } from "~/server/auth/jwt"; import { verifyToken } from "~/server/auth/jwt";
const STALE_KEY = "stale-resource";
export const apisMiddleware = async (req: NextRequest) => { export const apisMiddleware = async (req: NextRequest) => {
const { pathname, searchParams } = req.nextUrl; const { pathname, searchParams } = req.nextUrl;
@ -68,16 +69,28 @@ export const apisMiddleware = async (req: NextRequest) => {
// Handle failed responses // Handle failed responses
if (!response.ok) { if (!response.ok) {
console.error( // console.error(
`Storage API error: ${response.status} for ${storageUrl}, referrer: ${req.referrer}, url: ${req.url}`, // `Storage API error: ${response.status} for ${storageUrl}, referrer: ${req.referrer}, url: ${req.url}`,
); // );
// 404 = File not found (likely stale URL after media update)
// 410 = Gone (explicitly removed)
const isStaleResource =
response.status === 404 || response.status === 410;
// Return fallback for image requests // Return fallback for image requests
if (isImageRequest || isNextImageRequest) { if (isImageRequest || isNextImageRequest) {
const url = req.nextUrl.clone(); const url = req.nextUrl.clone();
url.pathname = "/fallback-image.png"; url.pathname = "/fallback-image.png";
url.search = ""; url.search = "";
return NextResponse.rewrite(url); const response = NextResponse.rewrite(url);
if (isStaleResource) {
response.cookies.set(STALE_KEY, "1", {
maxAge: 5,
path: "/",
});
}
return response;
} }
// Return fallback for video requests // Return fallback for video requests
@ -85,10 +98,32 @@ export const apisMiddleware = async (req: NextRequest) => {
const url = req.nextUrl.clone(); const url = req.nextUrl.clone();
url.pathname = "/fallback-video.png"; url.pathname = "/fallback-video.png";
url.search = ""; url.search = "";
return NextResponse.rewrite(url); const response = NextResponse.rewrite(url);
if (isStaleResource) {
response.cookies.set(STALE_KEY, "1", {
maxAge: 5,
path: "/",
});
} }
return new NextResponse("Storage API error", { status: response.status }); return response;
}
// For non-media requests (e.g., documents), return JSON with reload hint
if (isStaleResource) {
return NextResponse.json(
{ message: "Stale resource, please reload." },
{ status: 410 },
);
}
return new NextResponse(`Storage API error: ${response.status}`, {
status: response.status,
});
}
//ignore if 204 no content, is the favicon request
if (response.status === 204) {
return new NextResponse(null, { status: 200 });
} }
// Get response data // Get response data

View file

@ -51,6 +51,7 @@ import {
type CaratteristicheFiltered, type CaratteristicheFiltered,
filteredCaratteristiche, filteredCaratteristiche,
} from "~/hooks/schedaAnnuncioUtils"; } from "~/hooks/schedaAnnuncioUtils";
import { useStaleImageReload } from "~/hooks/staleImage";
import { useMediaQuery } from "~/hooks/use-media-query"; import { useMediaQuery } from "~/hooks/use-media-query";
import { handleConsegna } from "~/lib/annuncio_details"; import { handleConsegna } from "~/lib/annuncio_details";
import { replaceWithBr } from "~/lib/newlineToBr"; import { replaceWithBr } from "~/lib/newlineToBr";
@ -106,6 +107,7 @@ const AnnuncioDettaglio: NextPage<AnnuncioProps> = ({
const AnnuncioView = ({ cod, flag }: Omit<AnnuncioProps, "meta">) => { const AnnuncioView = ({ cod, flag }: Omit<AnnuncioProps, "meta">) => {
const session = useSession(); const session = useSession();
const { locale } = useTranslation(); const { locale } = useTranslation();
useStaleImageReload();
const { data, isLoading } = api.annunci.getAnnuncio.useQuery( const { data, isLoading } = api.annunci.getAnnuncio.useQuery(
{ {