refactor: streamline AnnuncioCard and CompatibiliDialog components, enhance data handling and styling

This commit is contained in:
Marco Pedone 2025-11-17 19:01:21 +01:00
parent b741f7ce72
commit de034c0eb7
5 changed files with 155 additions and 291 deletions

View file

@ -1,53 +1,17 @@
import { ExternalLink, UnfoldVertical } from "lucide-react"; import { ExternalLink } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import {
Credenza,
CredenzaBody,
CredenzaClose,
CredenzaContent,
CredenzaDescription,
CredenzaFooter,
CredenzaHeader,
CredenzaTitle,
CredenzaTrigger,
} from "~/components/custom_ui/credenza";
import { ImageFlbk } from "~/components/ImageWithFallback"; import { ImageFlbk } from "~/components/ImageWithFallback";
import { AnnuncioActions } from "~/components/servizio/annuncio_actions"; import { AnnuncioActions } from "~/components/servizio/annuncio_actions";
import { Interactions } from "~/components/servizio/interactions"; import { Interactions } from "~/components/servizio/interactions";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "~/components/ui/carousel";
import { handleConsegna } from "~/lib/annuncio_details"; import { handleConsegna } from "~/lib/annuncio_details";
import { replaceWithBr } from "~/lib/newlineToBr";
import { cn, formatCurrency } from "~/lib/utils"; import { cn, formatCurrency } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider"; import { useTranslation } from "~/providers/I18nProvider";
import { useServizioAnnuncio } from "~/providers/ServizioProvider"; import { useServizioAnnuncio } from "~/providers/ServizioProvider";
import type { Annunci } from "~/schemas/public/Annunci"; import type { AnnuncioRicerca } from "~/server/controllers/annunci.controller";
import { Badge } from "../ui/badge"; import { AnnuncioDettaglio, TipoBadge } from "./annuncio_dettaglio";
type AnnuncioData = Pick<
Annunci,
| "id"
| "codice"
| "prezzo"
| "desc_en"
| "desc_it"
| "titolo_en"
| "titolo_it"
| "tipo"
| "consegna"
| "stato"
| "web"
| "media_updated_at"
> & {
images: Pick<{ img: string; thumb: string }, "img">[];
};
export const AnnuncioCard = ({ className }: { className?: string }) => { export const AnnuncioCard = ({ className }: { className?: string }) => {
const { data } = useServizioAnnuncio(); const { data } = useServizioAnnuncio();
@ -57,6 +21,7 @@ export const AnnuncioCard = ({ className }: { className?: string }) => {
className={className} className={className}
data={{ data={{
...data, ...data,
modificato_il: data.modificato_il ? new Date(data.modificato_il) : null,
media_updated_at: data.media_updated_at media_updated_at: data.media_updated_at
? new Date(data.media_updated_at) ? new Date(data.media_updated_at)
: null, : null,
@ -74,7 +39,7 @@ export const BasicAnnuncioCard = ({
actions?: React.ReactNode; actions?: React.ReactNode;
interactions?: React.ReactNode; interactions?: React.ReactNode;
className?: string; className?: string;
data: AnnuncioData; data: AnnuncioRicerca;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -136,125 +101,3 @@ export const BasicAnnuncioCard = ({
</Card> </Card>
); );
}; };
const TipoBadge = ({ tipo }: { tipo: Annunci["tipo"] }) => {
return (
<Badge
className={cn(
"font-semibold text-md",
tipo === "Transitorio" && "bg-transitorio",
tipo === "Stabile" && "bg-stabile",
tipo === "Cessione" && "bg-blue-400",
tipo === "Vendita" && "bg-green-400",
)}
>
Affitto {tipo}
</Badge>
);
};
const AnnuncioDettaglio = ({ data }: { data: AnnuncioData }) => {
const { locale, t } = useTranslation();
return (
<Credenza>
<CredenzaTrigger asChild>
<Button aria-label="Dettagli Annuncio" variant="outline">
<UnfoldVertical className="size-6" /> Dettagli annuncio
</Button>
</CredenzaTrigger>
<CredenzaContent className="max-h-[90vh] md:max-w-6xl">
<CredenzaHeader>
<CredenzaTitle className="text-xl">
Annuncio {data.codice}
</CredenzaTitle>
<CredenzaDescription className="sr-only">
Annuncio {data.codice}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody className="scrollbar-default max-h-[80vh] overflow-y-auto pb-5">
<div className="flex flex-col gap-2 px-2">
<Carousel opts={{ loop: true }}>
<CarouselContent>
{data?.images?.map((img, idx) => (
<CarouselItem key={img.img}>
<ImageFlbk
alt={`${data.codice} - ${idx}`}
className={"h-80 w-full object-contain sm:h-[35rem]"}
height={200}
priority={idx === 0}
src={`/storage-api/get/${img.img}?image=true&${data.media_updated_at?.toString() || new Date().toString()}`}
width={400}
/>
</CarouselItem>
))}
</CarouselContent>
<div
className="block opacity-0 transition-all duration-200 group-hover:opacity-60 disabled:group-hover:opacity-0"
onClick={(e) => e.preventDefault()}
onKeyDown={(e) => e.stopPropagation()}
role="button"
tabIndex={0}
>
<CarouselPrevious
className="left-2 cursor-pointer"
disabled={!data.images || data.images.length === 1}
/>
<CarouselNext
className="right-2 cursor-pointer"
disabled={!data.images || data.images.length === 1}
/>
</div>
</Carousel>
<div>
<TipoBadge tipo={data.tipo} />
<div className="mt-1 space-y-2">
<div className="flex flex-row justify-around">
<div className="text-center">
<dt className="sr-only">{t.card.codice}</dt>
<dd className="font-semibold text-lg">
Cod: {data.codice}
</dd>
</div>
<div className="text-center">
<dt className="sr-only">{t.card.prezzo}</dt>
<dd className="font-semibold text-lg">
{formatCurrency(data.prezzo / 1e2)}
</dd>
</div>
</div>
<div className="mb-3">
<dt className="sr-only">{t.card.titolo}</dt>
<dd className="font-semibold text-base md:text-lg">
{locale === "it" ? data.titolo_it : data.titolo_en}
</dd>
</div>
<div className="mb-3">
<dd
className="text-base"
dangerouslySetInnerHTML={{
__html: replaceWithBr(
(locale === "it" ? data.desc_it : data.desc_en) || "",
),
}}
/>
</div>
</div>
</div>
</div>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button aria-label="Chiudi Credenza">{t.chiudi}</Button>
</CredenzaClose>
</CredenzaFooter>
</CredenzaContent>
</Credenza>
);
};

View file

@ -0,0 +1,118 @@
import { ExternalLink, UnfoldVertical } from "lucide-react";
import Link from "next/link";
import { replaceWithBr } from "~/lib/newlineToBr";
import { cn, formatCurrency } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider";
import type { Annunci } from "~/schemas/public/Annunci";
import type { AnnuncioRicerca } from "~/server/controllers/annunci.controller";
import { CarouselAnnuncio } from "../annuncio_card";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../ui/dialog";
export const TipoBadge = ({ tipo }: { tipo: Annunci["tipo"] }) => {
return (
<Badge
className={cn(
"font-semibold text-md",
tipo === "Transitorio" && "bg-transitorio",
tipo === "Stabile" && "bg-stabile",
tipo === "Cessione" && "bg-blue-400",
tipo === "Vendita" && "bg-green-400",
)}
>
Affitto {tipo}
</Badge>
);
};
export const AnnuncioDettaglio = ({ data }: { data: AnnuncioRicerca }) => {
const { locale, t } = useTranslation();
return (
<Dialog>
<DialogTrigger asChild>
<Button aria-label="Dettagli Annuncio" variant="outline">
<UnfoldVertical className="size-6" /> Dettagli annuncio
</Button>
</DialogTrigger>
<DialogContent className="max-h-[70vh] max-w-xl overflow-y-auto sm:max-w-6xl">
<DialogHeader className="flex-row items-center gap-4">
<DialogTitle className="text-xl">Annuncio {data.codice}</DialogTitle>
<Link href={`/annuncio/${data.codice}`} target="_blank">
<Button size="sm" variant="info">
<span>Vai alla pagina completa</span>{" "}
<ExternalLink className="size-4" />
</Button>
</Link>
<DialogDescription className="sr-only">
Annuncio {data.codice}
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-2 px-2">
<div className="w-full contain-inline-size">
<CarouselAnnuncio
immagini={data.images.map((img) => img.img)}
updated_at={data.media_updated_at}
videos={data.url_video}
/>
</div>
<div>
<div className="flex items-center gap-4">
<TipoBadge tipo={data.tipo} />
{data.stato === "Trattativa" && (
<Badge className="bg-violet-500 font-semibold text-md">
Annuncio in Trattativa
</Badge>
)}
</div>
<div className="mt-1 space-y-2">
<div className="flex flex-row justify-around">
<div className="text-center">
<dt className="sr-only">{t.card.codice}</dt>
<dd className="font-semibold text-lg">Cod: {data.codice}</dd>
</div>
<div className="text-center">
<dt className="sr-only">{t.card.prezzo}</dt>
<dd className="font-semibold text-lg">
{formatCurrency(data.prezzo / 1e2)}
</dd>
</div>
</div>
<div className="mb-3">
<dt className="sr-only">{t.card.titolo}</dt>
<dd className="font-semibold text-base md:text-lg">
{locale === "it" ? data.titolo_it : data.titolo_en}
</dd>
</div>
<div className="mb-3">
<dd
className="text-base"
dangerouslySetInnerHTML={{
__html: replaceWithBr(
(locale === "it" ? data.desc_it : data.desc_en) || "",
),
}}
/>
</div>
</div>
</div>
</div>
</DialogContent>
</Dialog>
);
};

View file

@ -1,17 +1,9 @@
import { ExternalLink, UnfoldVertical } from "lucide-react";
import Link from "next/link";
import { useState } from "react"; import { useState } from "react";
import { replaceWithBr } from "~/lib/newlineToBr";
import { cn, formatCurrency } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider";
import { useServizio } from "~/providers/ServizioProvider"; import { useServizio } from "~/providers/ServizioProvider";
import type { Annunci } from "~/schemas/public/Annunci";
import type { AnnuncioRicerca } from "~/server/controllers/annunci.controller";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import { CardAnnuncio, CarouselAnnuncio } from "../annuncio_card"; import { CardAnnuncio } from "../annuncio_card";
import { InteressatoButtonServizio } from "../annuncio-interactions/annuncio_interactions"; import { InteressatoButtonServizio } from "../annuncio-interactions/annuncio_interactions";
import { LoadingPage } from "../loading"; import { LoadingPage } from "../loading";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { import {
Dialog, Dialog,
@ -25,6 +17,7 @@ import {
} from "../ui/dialog"; } from "../ui/dialog";
import { Label } from "../ui/label"; import { Label } from "../ui/label";
import { Switch } from "../ui/switch"; import { Switch } from "../ui/switch";
import { AnnuncioDettaglio } from "./annuncio_dettaglio";
export const AnnunciCompatibili = () => { export const AnnunciCompatibili = () => {
const { servizioId, isAdmin } = useServizio(); const { servizioId, isAdmin } = useServizio();
@ -36,7 +29,7 @@ export const AnnunciCompatibili = () => {
return ( return (
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button className="w-full bg-purple-500 sm:w-fit"> <Button className="w-full bg-purple-500 hover:bg-purple-600 sm:w-fit dark:text-white">
Esplora Annunci Compatibili Esplora Annunci Compatibili
</Button> </Button>
</DialogTrigger> </DialogTrigger>
@ -67,12 +60,10 @@ export const AnnunciCompatibili = () => {
) : ( ) : (
<> <>
{!data || data.length === 0 ? ( {!data || data.length === 0 ? (
<div className="flex items-center justify-center gap-1 rounded-md bg-white p-4 sm:flex"> <span>
<span> Nessun annuncio compatibile trovato. Amplia la ricerca per
Nessun annuncio compatibile trovato. Amplia la ricerca per trovare annunci compatibili.
trovare annunci compatibili. </span>
</span>
</div>
) : ( ) : (
<div className="relative z-0 mx-auto grid max-w-full grid-flow-row grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-6 sm:gap-y-10 lg:grid-cols-3 2xl:grid-cols-4"> <div className="relative z-0 mx-auto grid max-w-full grid-flow-row grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-6 sm:gap-y-10 lg:grid-cols-3 2xl:grid-cols-4">
{data.map((annuncio) => ( {data.map((annuncio) => (
@ -109,106 +100,3 @@ export const AnnunciCompatibili = () => {
</Dialog> </Dialog>
); );
}; };
const TipoBadge = ({ tipo }: { tipo: Annunci["tipo"] }) => {
return (
<Badge
className={cn(
"font-semibold text-md",
tipo === "Transitorio" && "bg-transitorio",
tipo === "Stabile" && "bg-stabile",
tipo === "Cessione" && "bg-blue-400",
tipo === "Vendita" && "bg-green-400",
)}
>
Affitto {tipo}
</Badge>
);
};
const AnnuncioDettaglio = ({ data }: { data: AnnuncioRicerca }) => {
const { locale, t } = useTranslation();
return (
<Dialog>
<DialogTrigger asChild>
<Button aria-label="Dettagli Annuncio" variant="outline">
<UnfoldVertical className="size-6" /> Dettagli annuncio
</Button>
</DialogTrigger>
<DialogContent className="max-h-[70vh] max-w-xl overflow-y-auto sm:max-w-6xl">
<DialogHeader className="flex-row items-center gap-4">
<DialogTitle className="text-xl">Annuncio {data.codice}</DialogTitle>
<Link href={`/annuncio/${data.codice}`} target="_blank">
<Button size="sm" variant="info">
<span>Vai alla pagina completa</span>{" "}
<ExternalLink className="size-4" />
</Button>
</Link>
<DialogDescription className="sr-only">
Annuncio {data.codice}
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-2 px-2">
<div className="w-full contain-inline-size">
<CarouselAnnuncio
immagini={data.images.map((img) => img.img)}
updated_at={data.media_updated_at}
videos={data.url_video}
/>
</div>
<div>
<div className="flex items-center gap-4">
<TipoBadge tipo={data.tipo} />
{data.stato === "Trattativa" && (
<Badge className="bg-violet-500 font-semibold text-md">
Annuncio in Trattativa
</Badge>
)}
</div>
<div className="mt-1 space-y-2">
<div className="flex flex-row justify-around">
<div className="text-center">
<dt className="sr-only">{t.card.codice}</dt>
<dd className="font-semibold text-lg">Cod: {data.codice}</dd>
</div>
<div className="text-center">
<dt className="sr-only">{t.card.prezzo}</dt>
<dd className="font-semibold text-lg">
{formatCurrency(data.prezzo / 1e2)}
</dd>
</div>
</div>
<div className="mb-3">
<dt className="sr-only">{t.card.titolo}</dt>
<dd className="font-semibold text-base md:text-lg">
{locale === "it" ? data.titolo_it : data.titolo_en}
</dd>
</div>
<div className="mb-3">
<dd
className="text-base"
dangerouslySetInnerHTML={{
__html: replaceWithBr(
(locale === "it" ? data.desc_it : data.desc_en) || "",
),
}}
/>
</div>
</div>
</div>
</div>
<DialogFooter>
<DialogClose asChild></DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View file

@ -93,16 +93,23 @@ export const intrestsRouter = createTRPCRouter({
.select((eb) => [ .select((eb) => [
"annunci.id", "annunci.id",
"annunci.codice", "annunci.codice",
"annunci.comune",
"annunci.provincia",
"annunci.prezzo", "annunci.prezzo",
"annunci.consegna",
"annunci.numero_camere",
"annunci.mq",
"annunci.tipo",
"annunci.titolo_it",
"annunci.titolo_en",
"annunci.desc_en", "annunci.desc_en",
"annunci.desc_it", "annunci.desc_it",
"annunci.titolo_en", "annunci.modificato_il",
"annunci.titolo_it",
"annunci.tipo",
"annunci.consegna",
"annunci.stato", "annunci.stato",
"annunci.web", "annunci.url_video",
"annunci.media_updated_at", "annunci.media_updated_at",
"annunci.homepage",
"annunci.web",
jsonArrayFrom( jsonArrayFrom(
eb eb
.selectFrom("images_refs") .selectFrom("images_refs")

View file

@ -605,6 +605,7 @@ export const setupSaldoConsulenzaServizio = async ({
}); });
} }
}; };
export type ServizioData = Awaited< export type ServizioData = Awaited<
ReturnType<typeof getAllServizioAnnunci> ReturnType<typeof getAllServizioAnnunci>
>[number]; >[number];
@ -679,16 +680,23 @@ export const getAllServizioAnnunci = async (userId: UsersId) => {
.select((eb) => [ .select((eb) => [
"annunci.id", "annunci.id",
"annunci.codice", "annunci.codice",
"annunci.comune",
"annunci.provincia",
"annunci.prezzo", "annunci.prezzo",
"annunci.consegna",
"annunci.numero_camere",
"annunci.mq",
"annunci.tipo",
"annunci.titolo_it",
"annunci.titolo_en",
"annunci.desc_en", "annunci.desc_en",
"annunci.desc_it", "annunci.desc_it",
"annunci.titolo_en", "annunci.modificato_il",
"annunci.titolo_it",
"annunci.tipo",
"annunci.consegna",
"annunci.stato", "annunci.stato",
"annunci.web", "annunci.url_video",
"annunci.media_updated_at", "annunci.media_updated_at",
"annunci.homepage",
"annunci.web",
jsonArrayFrom( jsonArrayFrom(
eb eb
.selectFrom("images_refs") .selectFrom("images_refs")