feat: implement Result type for handling success and error states in API responses

feat: create AnnuncioNotFound page to handle missing announcements gracefully
feat: refactor Annuncio component to use structured AnnuncioData and improve SEO metadata handling
refactor: remove unused getAnnuncioMetaByCod function and streamline getAnnuncioData logic
This commit is contained in:
Marco Pedone 2026-03-19 10:20:41 +01:00
parent fd60f33bd5
commit a87b9f57a7
5 changed files with 102 additions and 89 deletions

View file

@ -0,0 +1,3 @@
export type Result<T> =
| { success: true; data: T }
| { success: false; message: string };

View file

@ -0,0 +1,4 @@
import FailedAnnuncioLoading from "~/components/failed-loading";
export default function AnnuncioNotFound() {
return <FailedAnnuncioLoading />;
}

View file

@ -66,18 +66,13 @@ import { useTranslation } from "~/providers/I18nProvider";
import { buildRicercaUrl } from "~/providers/RicercaProvider"; import { buildRicercaUrl } from "~/providers/RicercaProvider";
import { useSession } from "~/providers/SessionProvider"; import { useSession } from "~/providers/SessionProvider";
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum"; import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
import type { AnnuncioData } from "~/server/controllers/annunci.controller";
import { generateSSGHelper } from "~/server/utils/ssgHelper"; import { generateSSGHelper } from "~/server/utils/ssgHelper";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
type AnnuncioProps = { type AnnuncioProps = {
cod: string; data: AnnuncioData;
flag: string; flag: string;
meta: {
ogImage: string;
title: string;
description: string;
ogUrl: string;
} | null;
}; };
/** /**
@ -85,25 +80,22 @@ type AnnuncioProps = {
* Questa pagina mostra i dettagli di un annuncio specifico, identificato dal codice (cod) passato come parametro. Utilizza getStaticProps per prefetchare i dati dell'annuncio e le relative informazioni meta per SEO e condivisione sui social media. Se l'annuncio è in stato "Sospeso" o se si verifica un errore durante il caricamento, viene mostrata una pagina di errore. * Questa pagina mostra i dettagli di un annuncio specifico, identificato dal codice (cod) passato come parametro. Utilizza getStaticProps per prefetchare i dati dell'annuncio e le relative informazioni meta per SEO e condivisione sui social media. Se l'annuncio è in stato "Sospeso" o se si verifica un errore durante il caricamento, viene mostrata una pagina di errore.
*/ */
const AnnuncioDettaglio: NextPage<AnnuncioProps> = ({ const AnnuncioDettaglio: NextPage<AnnuncioProps> = ({
cod, data,
flag, flag,
meta,
}: AnnuncioProps) => { }: AnnuncioProps) => {
return ( return (
<main> <main>
<Head> <Head>
<title> <title>{`${data.codice} | ${data.titolo_it}`}</title>
{meta ? `${cod} | ${meta.title}` : "Annuncio non trovato"} <meta content={data.desc_it || ""} property="description" />
</title> <meta content={data.ogUrl} property="og:url" />
<meta content={meta?.description} property="description" />
<meta content={meta?.ogUrl} property="og:url" />
<meta content="website" property="og:type" /> <meta content="website" property="og:type" />
<meta <meta
content={meta ? `${cod} | ${meta.title}` : "Annuncio non trovato"} content={`${data.codice} | ${data.titolo_it}`}
property="og:title" property="og:title"
/> />
<meta content={meta?.description} property="og:description" /> <meta content={data.desc_it || ""} property="og:description" />
<meta content={meta?.ogImage} property="og:image" /> <meta content={data.ogImage} property="og:image" />
<meta content="1200" property="og:image:width" /> <meta content="1200" property="og:image:width" />
<meta content="630" property="og:image:height" /> <meta content="630" property="og:image:height" />
<meta <meta
@ -111,37 +103,29 @@ const AnnuncioDettaglio: NextPage<AnnuncioProps> = ({
property="og:logo" property="og:logo"
/> />
<meta content="Infoalloggi.it" property="og:site_name" /> <meta content="Infoalloggi.it" property="og:site_name" />
<link href={meta?.ogUrl || ""} rel="canonical" /> <link href={data.ogUrl || ""} rel="canonical" />
<link <link
href={`${env.NEXT_PUBLIC_BASE_URL}/annuncio/${cod}`} href={`${env.NEXT_PUBLIC_BASE_URL}/annuncio/${data.codice}`}
hrefLang="it" hrefLang="it"
rel="alternate" rel="alternate"
/> />
<link <link
href={`${env.NEXT_PUBLIC_BASE_URL}/en/annuncio/${cod}`} href={`${env.NEXT_PUBLIC_BASE_URL}/en/annuncio/${data.codice}`}
hrefLang="en" hrefLang="en"
rel="alternate" rel="alternate"
/> />
</Head> </Head>
<AnnuncioView cod={cod} flag={flag} /> <AnnuncioView data={data} flag={flag} />
</main> </main>
); );
}; };
const AnnuncioView = ({ cod, flag }: Omit<AnnuncioProps, "meta">) => { const AnnuncioView = ({ data, flag }: AnnuncioProps) => {
const session = useSession(); const session = useSession();
useStaleImageReload(); useStaleImageReload();
const { data, isLoading } = api.annunci.getAnnuncioData.useQuery(
{
cod: cod,
},
{ refetchOnMount: false },
);
const isDesktop = useMediaQuery("(min-width: 768px)"); const isDesktop = useMediaQuery("(min-width: 768px)");
if (isLoading || session.status === "pending") return <LoadingPage />;
if ( if (
!data || !data ||
session.status === "error" || session.status === "error" ||
@ -156,7 +140,9 @@ const AnnuncioView = ({ cod, flag }: Omit<AnnuncioProps, "meta">) => {
<div className="mx-auto w-full max-w-400 px-2 sm:px-8"> <div className="mx-auto w-full max-w-400 px-2 sm:px-8">
<CarouselAnnuncio <CarouselAnnuncio
immagini={data.images} immagini={data.images}
updated_at={data.media_updated_at} updated_at={
data.media_updated_at ? new Date(data.media_updated_at) : null
}
videos={data.videos} videos={data.videos}
/> />
@ -221,21 +207,31 @@ export async function getStaticProps(
const cod = context.params?.cod; const cod = context.params?.cod;
if (typeof cod !== "string") { if (typeof cod !== "string") {
return { return {
notFound: true, redirect: {
destination: "/annuncio-non-trovato",
permanent: false,
},
};
}
const annuncio = await ssg.annunci.getAnnuncioData.fetch({ cod: cod });
if (!annuncio.success || annuncio.data.stato === "Sospeso") {
return {
redirect: {
destination: "/annuncio-non-trovato",
permanent: false,
},
}; };
} }
await ssg.annunci.getAnnuncioData.prefetch({ cod: cod });
const meta = await ssg.annunci.getAnnuncioMeta.fetch({ cod: cod });
const flag = await ssg.flags.GetFlagValue.fetch({ const flag = await ssg.flags.GetFlagValue.fetch({
id: "ANNUNCIO_INTERACTIONS_DISABLED", id: "ANNUNCIO_INTERACTIONS_DISABLED",
}); });
return { return {
props: { props: {
cod, data: annuncio.data,
//cod,
flag, flag,
meta, //meta,
trpcState: ssg.dehydrate(), trpcState: ssg.dehydrate(),
}, },
revalidate: 21600, // 6 hours - ensures fresh data 4x per day revalidate: 21600, // 6 hours - ensures fresh data 4x per day
@ -674,7 +670,7 @@ const AnnuncioFooter = () => {
coverImage={getStorageUrl({ coverImage={getStorageUrl({
storageId: video.thumb, storageId: video.thumb,
params: { params: {
cacheKey: media_updated_at?.toISOString(), cacheKey: media_updated_at || undefined,
media: "image", media: "image",
}, },
})} })}
@ -682,7 +678,7 @@ const AnnuncioFooter = () => {
videoSrc={getStorageUrl({ videoSrc={getStorageUrl({
storageId: video.video, storageId: video.video,
params: { params: {
cacheKey: media_updated_at?.toISOString(), cacheKey: media_updated_at || undefined,
media: "video", media: "video",
}, },
})} })}

View file

@ -13,7 +13,6 @@ import {
get_AnnunciPositionsHandler, get_AnnunciPositionsHandler,
getAnnunciById_rawImgUrls, getAnnunciById_rawImgUrls,
getAnnunciListHandler, getAnnunciListHandler,
getAnnunciMetaByCod,
getAnnuncioData, getAnnuncioData,
getAnnunciRicerca, getAnnunciRicerca,
getCodici_AnnunciHandler, getCodici_AnnunciHandler,
@ -58,6 +57,7 @@ export const annunciRouter = createTRPCRouter({
cod: z.string(), cod: z.string(),
}), }),
) )
.query(async ({ input }) => { .query(async ({ input }) => {
return await getAnnuncioData({ ...input }); return await getAnnuncioData({ ...input });
}), }),
@ -83,16 +83,6 @@ export const annunciRouter = createTRPCRouter({
.query(async ({ input }) => { .query(async ({ input }) => {
return await getAnnunciById_rawImgUrls({ ...input }); return await getAnnunciById_rawImgUrls({ ...input });
}), }),
getAnnuncioMeta: publicProcedure
.input(
z.object({
cod: z.string(),
}),
)
.query(async ({ input }) => {
return await getAnnunciMetaByCod({ ...input });
}),
getCodici: publicProcedure.query(async () => { getCodici: publicProcedure.query(async () => {
return await getCodici_AnnunciHandler(); return await getCodici_AnnunciHandler();
}), }),

View file

@ -1,5 +1,6 @@
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { env } from "~/env"; import { env } from "~/env";
import type { Result } from "~/lib/result";
import { getStorageUrl } from "~/lib/storage_utils"; import { getStorageUrl } from "~/lib/storage_utils";
import type { import type {
Annunci, Annunci,
@ -72,8 +73,48 @@ export const getCodici_AnnunciHandler = async (): Promise<string[]> => {
} }
}; };
export type AnnuncioData = Awaited<ReturnType<typeof getAnnuncioData>>; export type AnnuncioData = Pick<
export const getAnnuncioData = async ({ cod }: { cod: string }) => { Annunci,
| "anno"
| "accessori"
| "caratteristiche"
| "categorie"
| "desc_en"
| "desc_it"
| "titolo_it"
| "titolo_en"
| "stato"
| "id"
| "codice"
| "comune"
| "consegna"
| "external_videos"
| "prezzo"
| "numero_camere"
| "numero_bagni"
| "numero_balconi"
| "numero_postiauto"
| "numero_box"
| "numero_vani"
| "numero_terrazzi"
| "mq"
| "piano"
| "piano_palazzo"
| "lat_secondario"
| "lon_secondario"
> & {
tipo: string;
media_updated_at: string | null;
images: Pick<ImagesRefs, "img" | "thumb">[];
videos: Pick<VideosRefs, "video" | "thumb">[];
ogUrl: string;
ogImage: string;
};
export const getAnnuncioData = async ({
cod,
}: {
cod: string;
}): Promise<Result<AnnuncioData>> => {
try { try {
const annuncio = await db const annuncio = await db
.selectFrom("annunci") .selectFrom("annunci")
@ -107,43 +148,18 @@ export const getAnnuncioData = async ({ cod }: { cod: string }) => {
"piano_palazzo", "piano_palazzo",
"lat_secondario", "lat_secondario",
"lon_secondario", "lon_secondario",
"media_updated_at",
]) ])
.select((_eb) => [withImages(), withVideos()]) .select((_eb) => [withImages(), withVideos()])
.where("annunci.web", "=", true) .where("annunci.web", "=", true)
.where("tipo", "is not", null) .where("tipo", "is not", null)
.where("codice", "=", cod) .where("codice", "=", cod)
.executeTakeFirstOrThrow(
() => new Error(`Annuncio non trovato - codice: ${cod}`),
);
const { tipo, ...rest } = annuncio;
if (tipo === null) {
return null;
}
return { tipo, ...rest };
} catch (e) {
throw new TRPCError({
cause: (e as Error).cause,
code: "INTERNAL_SERVER_ERROR",
message: `Errore query getAnnunciByCod: ${(e as Error).message}`,
});
}
};
export const getAnnunciMetaByCod = async ({ cod }: { cod: string }) => {
try {
const annuncio = await db
.selectFrom("annunci")
.select((_eb) => [
"titolo_it",
"desc_it",
withImages(),
"media_updated_at",
])
.where("annunci.web", "=", true)
.where("codice", "=", cod)
.executeTakeFirst(); .executeTakeFirst();
if (!annuncio) { if (!annuncio) {
return null; return { success: false, message: "Annuncio non trovato" };
}
const { tipo, media_updated_at, ...rest } = annuncio;
if (tipo === null) {
return { success: false, message: "Annuncio non trovato" };
} }
const ogImage = const ogImage =
annuncio.images && annuncio.images.length > 0 && annuncio.images[0] annuncio.images && annuncio.images.length > 0 && annuncio.images[0]
@ -156,17 +172,21 @@ export const getAnnunciMetaByCod = async ({ cod }: { cod: string }) => {
host: env.BASE_URL, host: env.BASE_URL,
}) })
: `${env.BASE_URL}/og.jpg`; : `${env.BASE_URL}/og.jpg`;
return { return {
description: annuncio.desc_it || "", success: true,
ogImage, data: {
ogUrl: `${env.BASE_URL}/annuncio/${cod}`, ...rest,
title: annuncio.titolo_it || "", media_updated_at: media_updated_at?.toISOString() || null,
tipo,
ogUrl: `${env.BASE_URL}/annuncio/${cod}`,
ogImage,
},
}; };
} catch (e) { } catch (e) {
throw new TRPCError({ throw new TRPCError({
cause: (e as Error).cause,
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",
message: `Errore query getAnnunciMetaByCod: ${(e as Error).message}`, message: `Errore query getAnnunciByCod: ${(e as Error).message}`,
}); });
} }
}; };
@ -493,7 +513,7 @@ export const getProprietarioDataHandler = async ({
"lon", "lon",
"numero", "numero",
"locatore", "locatore",
"tipo_locatore" "tipo_locatore",
]) ])
.where("id", "=", annuncioId) .where("id", "=", annuncioId)
.executeTakeFirstOrThrow(() => { .executeTakeFirstOrThrow(() => {