feat: refactor Annuncio context and related data fetching for improved structure and clarity

This commit is contained in:
Marco Pedone 2026-03-17 18:47:30 +01:00
parent 337ca80fec
commit 58ea254180
4 changed files with 133 additions and 139 deletions

View file

@ -25,7 +25,7 @@ import { TouchProvider } from "~/components/custom_ui/HybridTooltip";
import Input from "~/components/custom_ui/input";
import { ComeFunziona } from "~/components/expand_guida";
import FailedAnnuncioLoading from "~/components/failed-loading";
import { IconMatrix, type IconType } from "~/components/IconComponents";
import { IconMatrix } from "~/components/IconComponents";
import InformationBubble from "~/components/InformationBubble";
import { LoadingPage } from "~/components/loading";
import { MappaDialogFullscreen } from "~/components/MapDialog";
@ -54,10 +54,7 @@ import {
import { Label } from "~/components/ui/label";
import { VideoPlayer } from "~/components/videoPlayer";
import { env } from "~/env";
import {
type CaratteristicheFiltered,
filteredCaratteristiche,
} from "~/hooks/schedaAnnuncioUtils";
import { filteredCaratteristiche } from "~/hooks/schedaAnnuncioUtils";
import { useStaleImageReload } from "~/hooks/staleImage";
import { useMediaQuery } from "~/hooks/use-media-query";
import { handleConsegna } from "~/lib/annuncio_details";
@ -68,9 +65,7 @@ import { AnnuncioContext, useAnnuncio } from "~/providers/AnnuncioProvider";
import { useTranslation } from "~/providers/I18nProvider";
import { buildRicercaUrl } from "~/providers/RicercaProvider";
import { useSession } from "~/providers/SessionProvider";
import type { Annunci } from "~/schemas/public/Annunci";
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
import type { VideosRefs } from "~/schemas/public/VideosRefs";
import { generateSSGHelper } from "~/server/utils/ssgHelper";
import { api } from "~/utils/api";
@ -135,10 +130,9 @@ 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(
const { data, isLoading } = api.annunci.getAnnuncioData.useQuery(
{
cod: cod,
},
@ -156,13 +150,6 @@ const AnnuncioView = ({ cod, flag }: Omit<AnnuncioProps, "meta">) => {
return <FailedAnnuncioLoading />;
}
const titolo = (locale === "it" ? data.titolo_it : data.titolo_en) || "";
const description = (locale === "it" ? data.desc_it : data.desc_en) || "";
const filteredCaratt = data.caratteristiche
? filteredCaratteristiche(data.caratteristiche)
: null;
return (
<AnnuncioContext.Provider value={data}>
<TouchProvider>
@ -176,7 +163,7 @@ const AnnuncioView = ({ cod, flag }: Omit<AnnuncioProps, "meta">) => {
<div className="flex flex-col sm:py-8 md:flex-row-reverse md:gap-2">
<div className="mb-6 flex flex-col md:mb-0 md:w-1/3">
<div className="flex w-full flex-col flex-nowrap items-center justify-center gap-6">
<CardInfos data={data} />
<CardInfos />
{session.user?.isAdmin ? (
<>
<Link
@ -217,21 +204,10 @@ const AnnuncioView = ({ cod, flag }: Omit<AnnuncioProps, "meta">) => {
</div>
</div>
<div className="mx-2 md:w-2/3">
<TestiAnnuncio
description={description}
filteredCaratt={filteredCaratt}
titolo={titolo}
/>
<TestiAnnuncio />
</div>
</div>
<AnnuncioFooter
comune={data.comune}
consegna={data.consegna}
external_videos={data.external_videos || []}
tipo={data.tipo}
updated_at={data.media_updated_at}
videos={data.videos}
/>
<AnnuncioFooter />
</div>
</TouchProvider>
</AnnuncioContext.Provider>
@ -248,7 +224,7 @@ export async function getStaticProps(
notFound: true,
};
}
await ssg.annunci.getAnnuncio.prefetch({ cod: cod });
await ssg.annunci.getAnnuncioData.prefetch({ cod: cod });
const meta = await ssg.annunci.getAnnuncioMeta.fetch({ cod: cod });
@ -388,42 +364,30 @@ const Pricing = ({ isDesktop }: { isDesktop: boolean }) => {
);
};
const TestiAnnuncio = ({
titolo,
description,
filteredCaratt,
}: {
titolo: string;
description: string;
filteredCaratt:
| {
text: string;
icon: IconType;
value: string | null;
}[]
| null;
}) => {
const TestiAnnuncio = () => {
const { locale } = useTranslation();
const { titolo_it, titolo_en, desc_it, desc_en } = useAnnuncio();
return (
<>
<h1 className="font-bold text-3xl uppercase">{titolo}</h1>
<h1 className="font-bold text-3xl uppercase">
{locale === "it" ? titolo_it : titolo_en}
</h1>
<h1
className="py-6"
dangerouslySetInnerHTML={{
__html: replaceWithBr(description),
__html: replaceWithBr((locale === "it" ? desc_it : desc_en) || ""),
}}
/>
{filteredCaratt && (
<CaratteristicheAccordion filteredCaratt={filteredCaratt} />
)}
<CaratteristicheAccordion />
</>
);
};
const CaratteristicheAccordion = ({
filteredCaratt,
}: {
filteredCaratt: CaratteristicheFiltered;
}) => {
const CaratteristicheAccordion = () => {
const { caratteristiche } = useAnnuncio();
if (!caratteristiche) return null;
return (
<Accordion
className="my-5"
@ -437,43 +401,59 @@ const CaratteristicheAccordion = ({
>
<AccordionTrigger>Altre caratteristiche</AccordionTrigger>
<AccordionContent>
{Object.entries(filteredCaratt).map(([key, scheda_value]) => {
const { text, icon, value } = scheda_value;
if (value === null) return null;
return (
<div
className="flex flex-row items-center gap-2 text-base"
key={key}
>
<IconMatrix className="size-4" type={icon} />
{text}: {value}
</div>
);
})}
{Object.entries(filteredCaratteristiche(caratteristiche)).map(
([key, scheda_value]) => {
const { text, icon, value } = scheda_value;
if (value === null) return null;
return (
<div
className="flex flex-row items-center gap-2 text-base"
key={key}
>
<IconMatrix className="size-4" type={icon} />
{text}: {value}
</div>
);
},
)}
</AccordionContent>
</AccordionItem>
</Accordion>
);
};
const CardInfos = ({ data }: { data: Annunci }) => {
const CardInfos = () => {
const { t } = useTranslation();
const {
codice,
prezzo,
tipo,
numero_camere,
numero_bagni,
caratteristiche,
numero_postiauto,
mq,
piano,
stato,
consegna,
lat_secondario,
lon_secondario,
} = useAnnuncio();
return (
<Card className="relative w-full bg-secondary text-secondary-foreground outline outline-secondary-foreground">
<CardHeader>
<CardTitle className="text-center font-bold text-3xl md:pl-8 lg:pl-0">
{t.annunci.codice} {data.codice}
{t.annunci.codice} {codice}
</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4 p-4 py-0">
<div className="grid grid-cols-2 gap-3 font-semibold">
<div className="relative">
<div className="flex h-10 items-center justify-center rounded-md bg-primary-foreground text-center text-primary">
{data.prezzo && (data.prezzo / 1e2).toFixed(2).replace(".", ",")}{" "}
{prezzo && (prezzo / 1e2).toFixed(2).replace(".", ",")}{" "}
{t.annunci.euro_mese}
</div>
{data.prezzo && (
{prezzo && (
<InformationBubble
icon="euro"
iconClassName="h-[19.5px] w-[19.5px] bg-orange-500 text-white rounded-full"
@ -483,7 +463,7 @@ const CardInfos = ({ data }: { data: Annunci }) => {
{t.annunci.prezzo_giorno}
</span>
<div className="text-sm">
{(data.prezzo / 1e2 / 30).toFixed(2).replace(".", ",")}{" "}
{(prezzo / 1e2 / 30).toFixed(2).replace(".", ",")}{" "}
{t.annunci.euro_giorno}
</div>
</div>
@ -494,13 +474,13 @@ const CardInfos = ({ data }: { data: Annunci }) => {
<div
className={cn(
"h-10 rounded-md bg-primary-foreground p-2 text-center text-primary outline-2",
data.tipo === "Transitorio" && "outline-transitorio",
data.tipo === "Stabile" && "outline-stabile",
tipo === "Transitorio" && "outline-transitorio",
tipo === "Stabile" && "outline-stabile",
)}
>
{data.tipo}
{tipo}
</div>
{data.tipo === "Transitorio" && (
{tipo === "Transitorio" && (
<InformationBubble
href="/guida#faq-transitori"
target="_blank"
@ -516,30 +496,30 @@ const CardInfos = ({ data }: { data: Annunci }) => {
<BedDouble />
<span className="line-clamp-1 break-all">
{(() => {
if (!data.numero_camere) return "N/A";
if (data.numero_camere > 1)
return `${data.numero_camere} ${t.card.camere2}`;
if (data.numero_camere === 1)
return `${data.numero_camere} ${t.card.camere1}`;
if (!numero_camere) return "N/A";
if (numero_camere > 1)
return `${numero_camere} ${t.card.camere2}`;
if (numero_camere === 1)
return `${numero_camere} ${t.card.camere1}`;
})()}
</span>
</div>
<div className="flex justify-center gap-2 rounded-md bg-primary-foreground p-2 text-primary">
<Bath />
<span className="line-clamp-1 break-all">
{data.numero_bagni || 0} {t.annunci.bagni}
{numero_bagni || 0} {t.annunci.bagni}
</span>
</div>
<div className="flex justify-center gap-2 rounded-md bg-primary-foreground p-2 text-primary">
<Armchair />
<span className="line-clamp-1 break-all">
{data.caratteristiche?.Scheda_Arredi || ""}
{caratteristiche?.Scheda_Arredi || ""}
</span>
</div>
<div className="flex justify-center gap-2 rounded-md bg-primary-foreground p-2 text-primary">
<Car />
<span className="line-clamp-1 break-all">
{data.numero_postiauto || 0} {t.annunci.posti_auto}
{numero_postiauto || 0} {t.annunci.posti_auto}
</span>
</div>
</div>
@ -550,21 +530,19 @@ const CardInfos = ({ data }: { data: Annunci }) => {
<div className="flex justify-center gap-2 rounded-md bg-primary-foreground p-2 text-primary">
<Ruler />
<span className="line-clamp-1 break-all">
{data.mq ? Number(data.mq).toLocaleString("it-IT") : "--"} m
<sup>2</sup>
{mq ? Number(mq).toLocaleString("it-IT") : "--"} m<sup>2</sup>
</span>
</div>
<div className="flex justify-center gap-2 rounded-md bg-primary-foreground p-2 text-primary">
{data.piano !== null && data.piano !== "" && (
{piano !== null && piano !== "" && (
<>
<ArrowUp01 />
<span className="line-clamp-1 break-all">
{(() => {
if (data.piano === "0") return t.annunci.p_terra;
if (data.piano === "0.5") return t.annunci.p_rialzato;
if (data.piano)
return `${data.piano}° ${t.annunci.piano}`;
if (piano === "0") return t.annunci.p_terra;
if (piano === "0.5") return t.annunci.p_rialzato;
if (piano) return `${piano}° ${t.annunci.piano}`;
return "--";
})()}
</span>
@ -575,7 +553,7 @@ const CardInfos = ({ data }: { data: Annunci }) => {
</div>
<div className="text-center font-bold text-base">
{data.stato === "Trattativa" ? (
{stato === "Trattativa" ? (
<div className="flex items-center justify-center gap-1 rounded-md py-1.5 text-trattativa outline-2 outline-trattativa dark:bg-secondary-foreground">
<TrafficCone className="inline-block size-5" />
<span>{t.annunci.in_aggiornamento}</span>
@ -587,7 +565,7 @@ const CardInfos = ({ data }: { data: Annunci }) => {
<span className="text-red-500">
{handleConsegna({
aggiornamento: t.card.in_aggiornamento,
consegna: data.consegna,
consegna: consegna,
consegna_da: t.card.consegna_da,
mesi: t.parametri.mesi,
subito: t.card.consegna_subito,
@ -597,10 +575,7 @@ const CardInfos = ({ data }: { data: Annunci }) => {
)}
</div>
<div className="flex flex-row justify-around gap-3">
<MappaDialogFullscreen
lat={data.lat_secondario}
long={data.lon_secondario}
/>
<MappaDialogFullscreen lat={lat_secondario} long={lon_secondario} />
<ShareComponent />
</div>
@ -677,28 +652,16 @@ const ShareComponent = () => {
);
};
const AnnuncioFooter = ({
tipo,
comune,
consegna,
videos,
external_videos,
updated_at,
}: {
tipo: string;
comune: string | null;
consegna: number | null;
videos: Pick<VideosRefs, "video" | "thumb">[];
external_videos: string[];
updated_at: Date | null;
}) => {
const AnnuncioFooter = () => {
const { tipo, comune, consegna, videos, external_videos, media_updated_at } =
useAnnuncio();
const { t } = useTranslation();
return (
<div className="flex flex-col gap-2">
<p
className={cn(
"px-2 font-semibold text-lg",
videos.length === 0 && external_videos.length === 0 && "hidden",
videos.length === 0 && external_videos?.length === 0 && "hidden",
)}
>
Video:
@ -710,17 +673,23 @@ const AnnuncioFooter = ({
className="h-96 max-w-96"
coverImage={getStorageUrl({
storageId: video.thumb,
params: { cacheKey: updated_at?.toISOString(), media: "image" },
params: {
cacheKey: media_updated_at?.toISOString(),
media: "image",
},
})}
key={`videoplayer-${video.video}`} // Cache busting
videoSrc={getStorageUrl({
storageId: video.video,
params: { cacheKey: updated_at?.toISOString(), media: "video" },
params: {
cacheKey: media_updated_at?.toISOString(),
media: "video",
},
})}
/>
);
})}
{external_videos.map((videoUrl) => {
{external_videos?.map((videoUrl) => {
const embedUrl = getYouTubeEmbedUrl(videoUrl);
if (!embedUrl) return null;
return (

View file

@ -1,10 +1,7 @@
import { createContext, useContext } from "react";
import type { Annunci } from "~/schemas/public/Annunci";
import type { AnnuncioData } from "~/server/controllers/annunci.controller";
export const AnnuncioContext = createContext<Pick<
Annunci,
"id" | "tipo" | "prezzo" | "codice"
> | null>(null);
export const AnnuncioContext = createContext<AnnuncioData | null>(null);
export const useAnnuncio = () => {
const context = useContext(AnnuncioContext);

View file

@ -11,10 +11,10 @@ import {
import {
editAnnuncioHandler,
get_AnnunciPositionsHandler,
getAnnunciByCod,
getAnnunciById_rawImgUrls,
getAnnunciListHandler,
getAnnunciMetaByCod,
getAnnuncioData,
getAnnunciRicerca,
getCodici_AnnunciHandler,
getOptions_AnnunciHandler,
@ -52,15 +52,14 @@ export const annunciRouter = createTRPCRouter({
getAnnunciOptions: publicProcedure.query(async () => {
return await getOptions_AnnunciHandler();
}),
/** TODO da limitare per evitare leak */
getAnnuncio: publicProcedure
getAnnuncioData: publicProcedure
.input(
z.object({
cod: z.string(),
}),
)
.query(async ({ input }) => {
return await getAnnunciByCod({ ...input });
return await getAnnuncioData({ ...input });
}),
getAnnuncioFull: adminProcedure
.input(

View file

@ -72,26 +72,55 @@ export const getCodici_AnnunciHandler = async (): Promise<string[]> => {
}
};
export const getAnnunciByCod = async ({
cod,
}: {
cod: string;
}): Promise<AnnunciWithMedia | null> => {
export type AnnuncioData = Awaited<ReturnType<typeof getAnnuncioData>>;
export const getAnnuncioData = async ({ cod }: { cod: string }) => {
try {
const annuncio = await db
.selectFrom("annunci")
.selectAll()
.select([
"anno",
"accessori",
"caratteristiche",
"categorie",
"desc_en",
"desc_it",
"tipo",
"titolo_it",
"titolo_en",
"stato",
"media_updated_at",
"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",
"media_updated_at",
])
.select((_eb) => [withImages(), withVideos()])
.where("annunci.web", "=", true)
.where("tipo", "is not", null)
.where("codice", "=", cod)
.executeTakeFirst();
if (!annuncio || annuncio.tipo === null) {
.executeTakeFirstOrThrow(
() => new Error(`Annuncio non trovato - codice: ${cod}`),
);
const { tipo, ...rest } = annuncio;
if (tipo === null) {
return null;
}
return annuncio as AnnunciWithMedia;
return { tipo, ...rest };
} catch (e) {
throw new TRPCError({
cause: (e as Error).cause,