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

View file

@ -1,10 +1,7 @@
import { createContext, useContext } from "react"; 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< export const AnnuncioContext = createContext<AnnuncioData | null>(null);
Annunci,
"id" | "tipo" | "prezzo" | "codice"
> | null>(null);
export const useAnnuncio = () => { export const useAnnuncio = () => {
const context = useContext(AnnuncioContext); const context = useContext(AnnuncioContext);

View file

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

View file

@ -72,26 +72,55 @@ export const getCodici_AnnunciHandler = async (): Promise<string[]> => {
} }
}; };
export const getAnnunciByCod = async ({ export type AnnuncioData = Awaited<ReturnType<typeof getAnnuncioData>>;
cod, export const getAnnuncioData = async ({ cod }: { cod: string }) => {
}: {
cod: string;
}): Promise<AnnunciWithMedia | null> => {
try { try {
const annuncio = await db const annuncio = await db
.selectFrom("annunci") .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()]) .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)
.executeTakeFirst(); .executeTakeFirstOrThrow(
() => new Error(`Annuncio non trovato - codice: ${cod}`),
if (!annuncio || annuncio.tipo === null) { );
const { tipo, ...rest } = annuncio;
if (tipo === null) {
return null; return null;
} }
return { tipo, ...rest };
return annuncio as AnnunciWithMedia;
} catch (e) { } catch (e) {
throw new TRPCError({ throw new TRPCError({
cause: (e as Error).cause, cause: (e as Error).cause,