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 { useSession } from "~/providers/SessionProvider";
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
import type { AnnuncioData } from "~/server/controllers/annunci.controller";
import { generateSSGHelper } from "~/server/utils/ssgHelper";
import { api } from "~/utils/api";
type AnnuncioProps = {
cod: string;
data: AnnuncioData;
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.
*/
const AnnuncioDettaglio: NextPage<AnnuncioProps> = ({
cod,
data,
flag,
meta,
}: AnnuncioProps) => {
return (
<main>
<Head>
<title>
{meta ? `${cod} | ${meta.title}` : "Annuncio non trovato"}
</title>
<meta content={meta?.description} property="description" />
<meta content={meta?.ogUrl} property="og:url" />
<title>{`${data.codice} | ${data.titolo_it}`}</title>
<meta content={data.desc_it || ""} property="description" />
<meta content={data.ogUrl} property="og:url" />
<meta content="website" property="og:type" />
<meta
content={meta ? `${cod} | ${meta.title}` : "Annuncio non trovato"}
content={`${data.codice} | ${data.titolo_it}`}
property="og:title"
/>
<meta content={meta?.description} property="og:description" />
<meta content={meta?.ogImage} property="og:image" />
<meta content={data.desc_it || ""} property="og:description" />
<meta content={data.ogImage} property="og:image" />
<meta content="1200" property="og:image:width" />
<meta content="630" property="og:image:height" />
<meta
@ -111,37 +103,29 @@ const AnnuncioDettaglio: NextPage<AnnuncioProps> = ({
property="og:logo"
/>
<meta content="Infoalloggi.it" property="og:site_name" />
<link href={meta?.ogUrl || ""} rel="canonical" />
<link href={data.ogUrl || ""} rel="canonical" />
<link
href={`${env.NEXT_PUBLIC_BASE_URL}/annuncio/${cod}`}
href={`${env.NEXT_PUBLIC_BASE_URL}/annuncio/${data.codice}`}
hrefLang="it"
rel="alternate"
/>
<link
href={`${env.NEXT_PUBLIC_BASE_URL}/en/annuncio/${cod}`}
href={`${env.NEXT_PUBLIC_BASE_URL}/en/annuncio/${data.codice}`}
hrefLang="en"
rel="alternate"
/>
</Head>
<AnnuncioView cod={cod} flag={flag} />
<AnnuncioView data={data} flag={flag} />
</main>
);
};
const AnnuncioView = ({ cod, flag }: Omit<AnnuncioProps, "meta">) => {
const AnnuncioView = ({ data, flag }: AnnuncioProps) => {
const session = useSession();
useStaleImageReload();
const { data, isLoading } = api.annunci.getAnnuncioData.useQuery(
{
cod: cod,
},
{ refetchOnMount: false },
);
const isDesktop = useMediaQuery("(min-width: 768px)");
if (isLoading || session.status === "pending") return <LoadingPage />;
if (
!data ||
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">
<CarouselAnnuncio
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}
/>
@ -221,21 +207,31 @@ export async function getStaticProps(
const cod = context.params?.cod;
if (typeof cod !== "string") {
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({
id: "ANNUNCIO_INTERACTIONS_DISABLED",
});
return {
props: {
cod,
data: annuncio.data,
//cod,
flag,
meta,
//meta,
trpcState: ssg.dehydrate(),
},
revalidate: 21600, // 6 hours - ensures fresh data 4x per day
@ -674,7 +670,7 @@ const AnnuncioFooter = () => {
coverImage={getStorageUrl({
storageId: video.thumb,
params: {
cacheKey: media_updated_at?.toISOString(),
cacheKey: media_updated_at || undefined,
media: "image",
},
})}
@ -682,7 +678,7 @@ const AnnuncioFooter = () => {
videoSrc={getStorageUrl({
storageId: video.video,
params: {
cacheKey: media_updated_at?.toISOString(),
cacheKey: media_updated_at || undefined,
media: "video",
},
})}

View file

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

View file

@ -1,5 +1,6 @@
import { TRPCError } from "@trpc/server";
import { env } from "~/env";
import type { Result } from "~/lib/result";
import { getStorageUrl } from "~/lib/storage_utils";
import type {
Annunci,
@ -72,8 +73,48 @@ export const getCodici_AnnunciHandler = async (): Promise<string[]> => {
}
};
export type AnnuncioData = Awaited<ReturnType<typeof getAnnuncioData>>;
export const getAnnuncioData = async ({ cod }: { cod: string }) => {
export type AnnuncioData = Pick<
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 {
const annuncio = await db
.selectFrom("annunci")
@ -107,43 +148,18 @@ export const getAnnuncioData = async ({ cod }: { cod: string }) => {
"piano_palazzo",
"lat_secondario",
"lon_secondario",
"media_updated_at",
])
.select((_eb) => [withImages(), withVideos()])
.where("annunci.web", "=", true)
.where("tipo", "is not", null)
.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();
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 =
annuncio.images && annuncio.images.length > 0 && annuncio.images[0]
@ -156,17 +172,21 @@ export const getAnnunciMetaByCod = async ({ cod }: { cod: string }) => {
host: env.BASE_URL,
})
: `${env.BASE_URL}/og.jpg`;
return {
description: annuncio.desc_it || "",
ogImage,
success: true,
data: {
...rest,
media_updated_at: media_updated_at?.toISOString() || null,
tipo,
ogUrl: `${env.BASE_URL}/annuncio/${cod}`,
title: annuncio.titolo_it || "",
ogImage,
},
};
} catch (e) {
throw new TRPCError({
cause: (e as Error).cause,
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",
"numero",
"locatore",
"tipo_locatore"
"tipo_locatore",
])
.where("id", "=", annuncioId)
.executeTakeFirstOrThrow(() => {