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

View file

@ -1,4 +1,5 @@
import { CircleCheck, ExternalLink, Trash2 } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import toast from "react-hot-toast";
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 { api } from "~/utils/api";
import { Confirm } from "../confirm";
import { ImageFlbk } from "../ImageWithFallback";
import { LoadingPage } from "../loading";
import { LoadingSpinner } from "../spinner";
import { Button } from "../ui/button";
@ -146,7 +146,7 @@ export const DocumentiUploadSection = ({
{isPending ? (
<LoadingSpinner className="size-24 stroke-neutral-300" />
) : (
<ImageFlbk
<Image
alt={title}
className="size-24 rounded-md border object-cover"
height={96}

View file

@ -1,6 +1,6 @@
import { ExternalLink } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { ImageFlbk } from "~/components/ImageWithFallback";
import { AnnuncioActions } from "~/components/servizio/annuncio_actions";
import { Interactions } from "~/components/servizio/interactions";
import { Button } from "~/components/ui/button";
@ -52,7 +52,7 @@ export const BasicAnnuncioCard = ({
</CardHeader>
<CardContent className="px-4">
<div className="flex w-full flex-col gap-4 md:flex-row md:gap-10">
<ImageFlbk
<Image
alt={data.codice}
className="h-auto max-h-72 w-full rounded-md bg-[#e6e9ec] object-contain sm:max-w-xs"
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 { verifyToken } from "~/server/auth/jwt";
const STALE_KEY = "stale-resource";
export const apisMiddleware = async (req: NextRequest) => {
const { pathname, searchParams } = req.nextUrl;
@ -68,16 +69,28 @@ export const apisMiddleware = async (req: NextRequest) => {
// Handle failed responses
if (!response.ok) {
console.error(
`Storage API error: ${response.status} for ${storageUrl}, referrer: ${req.referrer}, url: ${req.url}`,
);
// console.error(
// `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
if (isImageRequest || isNextImageRequest) {
const url = req.nextUrl.clone();
url.pathname = "/fallback-image.png";
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
@ -85,10 +98,32 @@ export const apisMiddleware = async (req: NextRequest) => {
const url = req.nextUrl.clone();
url.pathname = "/fallback-video.png";
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

View file

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