infoalloggi-monorepo/apps/infoalloggi/src/components/annuncio_card.tsx

382 lines
11 KiB
TypeScript
Raw Normal View History

2025-08-04 17:45:44 +02:00
import { BedDouble, CalendarClock, Maximize2, Ruler } from "lucide-react";
import Image from "next/image";
2025-08-28 18:27:07 +02:00
import Link from "next/link";
2025-08-04 17:45:44 +02:00
import { useState } from "react";
2025-08-28 18:27:07 +02:00
import { ImageFlbk } from "~/components/ImageWithFallback";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "~/components/ui/carousel";
2025-08-04 17:45:44 +02:00
import {
2025-08-28 18:27:07 +02:00
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
2025-08-04 17:45:44 +02:00
} from "~/components/ui/dialog";
import { camereTesti, handleConsegna } from "~/lib/annuncio_details";
2025-08-28 18:27:07 +02:00
import { cn, formatCurrency } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider";
import { CacheKey } from "~/utils/imageCache";
import { VideoPlayer } from "./videoPlayer";
2025-08-04 17:45:44 +02:00
type CardAnnuncioProps = {
2025-08-28 18:27:07 +02:00
id: number;
codice: string;
prezzo: number;
titolo: string | null;
mq: string | null;
comune: string | null;
provincia: string | null;
consegna: number | null;
camere: number | null;
immagini: string[] | undefined;
tipo: string | null;
stato: string | null;
className?: string;
videos?: string[] | undefined;
2025-08-04 17:45:44 +02:00
};
2025-08-28 10:29:04 +02:00
export const CardAnnuncio = ({
2025-08-28 18:27:07 +02:00
id,
codice,
prezzo,
titolo,
mq,
comune,
provincia,
consegna,
camere,
immagini,
tipo,
stato,
className,
videos,
2025-08-04 17:45:44 +02:00
}: CardAnnuncioProps) => {
2025-08-28 18:27:07 +02:00
const { t } = useTranslation();
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
return (
<div
className={cn(
2025-10-10 16:18:43 +02:00
"h-[464px] rounded-md bg-white shadow-neutral-100 shadow-sm outline outline-neutral-200 hover:shadow-md hover:outline-neutral-300",
2025-08-28 18:27:07 +02:00
className,
)}
id={`card-annuncio-${id}`}
>
<Link
2025-08-29 16:18:32 +02:00
className="block p-2"
2025-08-28 18:27:07 +02:00
//target="_blank"
2025-08-29 16:18:32 +02:00
href={`/annuncio/${codice}`} //duration-700 ease-in-out animate-in fade-in
2025-08-28 18:27:07 +02:00
>
2025-10-10 16:18:43 +02:00
<div className="group relative text-clip rounded-md">
2025-08-28 18:27:07 +02:00
{stato === "Trattativa" && (
<div>
<div className="absolute z-20 h-56 w-full">
2025-10-10 16:18:43 +02:00
<div className="absolute bottom-0 w-full touch-none select-none rounded-b-md bg-violet-500 text-center text-lg text-white">
2025-08-28 18:27:07 +02:00
Annuncio in Trattativa
</div>
</div>
</div>
)}
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
<Carousel opts={{ loop: true }}>
<CarouselContent>
{immagini?.map((img, idx) => (
<CarouselItem key={`${img}-${idx}`}>
<ImageFlbk
alt={t.card.alt_immagine}
className={
"h-56 w-full rounded-md object-cover outline outline-neutral-200"
2025-08-28 18:27:07 +02:00
}
2025-08-29 16:18:32 +02:00
height={400}
priority={idx === 0}
src={`${img}?${CacheKey}`}
2025-08-29 16:18:32 +02:00
width={800}
2025-08-28 18:27:07 +02:00
/>
</CarouselItem>
))}
{videos?.map((video, idx) => {
if (!video) return null;
if (video.includes("youtu")) {
return null;
}
return (
<CarouselItem
className={cn("group relative")}
key={`videoplayer-${video}`}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<VideoPlayer
2025-10-10 16:18:43 +02:00
className="h-56"
coverImage={immagini?.[0] ? immagini[0] : ""}
key={`videoplayer-${idx}`}
videoSrc={`${video}?${CacheKey}`}
/>
</CarouselItem>
);
})}
2025-08-28 18:27:07 +02:00
</CarouselContent>
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
<div
2025-08-29 16:18:32 +02:00
className="block opacity-0 transition-all duration-200 group-hover:opacity-60 disabled:group-hover:opacity-0"
onClick={(e) => e.preventDefault()}
2025-08-28 18:27:07 +02:00
onKeyDown={(e) => e.stopPropagation()}
role="button"
tabIndex={0}
>
<CarouselPrevious
className="left-2 cursor-pointer"
disabled={!immagini || immagini.length === 1}
/>
<CarouselNext
className="right-2 cursor-pointer"
disabled={!immagini || immagini.length === 1}
/>
</div>
</Carousel>
</div>
<div
className={cn(
"mt-2 flex w-full items-center justify-center rounded-md",
tipo === "Transitorio" && "bg-transitorio",
tipo === "Stabile" && "bg-stabile",
tipo === "Cessione" && "bg-blue-400",
tipo === "Vendita" && "bg-green-400",
)}
>
<div className="font-semibold text-white">{tipo}</div>
</div>
<div className="mt-2 space-y-2">
<div className="flex flex-row justify-around">
<div className="text-center">
<span className="sr-only">{t.card.codice}</span>
2025-08-04 17:45:44 +02:00
2025-10-10 16:18:43 +02:00
<span className="font-semibold text-red-600 text-xl">
2025-08-28 18:27:07 +02:00
Cod: {codice}
</span>
</div>
<div className="text-center">
<span className="sr-only">{t.card.prezzo}</span>
2025-08-04 17:45:44 +02:00
2025-10-10 16:18:43 +02:00
<span className="font-semibold text-xl">
2025-08-28 18:27:07 +02:00
{formatCurrency(prezzo / 1e2)}
</span>
</div>
</div>
2025-08-04 17:45:44 +02:00
2025-10-10 16:18:43 +02:00
<div className="mb-3 text-center text-muted-foreground">
2025-08-28 18:27:07 +02:00
<span className="sr-only">{t.card.titolo}</span>
2025-08-04 17:45:44 +02:00
2025-10-10 16:18:43 +02:00
<span className="line-clamp-2 min-h-[3.5rem] font-medium text-base md:text-lg">
2025-08-28 18:27:07 +02:00
{titolo}
</span>
</div>
2025-10-10 16:18:43 +02:00
<div className="text-center text-muted-foreground">
2025-08-28 18:27:07 +02:00
<span className="sr-only">{t.card.indirizzo}</span>
2025-10-10 16:18:43 +02:00
<span className="font-medium text-sm">
2025-08-28 18:27:07 +02:00
{comune && provincia && `${comune} (${provincia})`}
</span>
</div>
</div>
2025-08-04 17:45:44 +02:00
2025-10-10 16:18:43 +02:00
<div className="mt-4 flex items-center justify-around gap-2 text-muted-foreground text-xs">
2025-08-28 18:27:07 +02:00
<div className="flex w-full items-center justify-center gap-1 rounded-md bg-neutral-100 py-3">
<Ruler className="size-4 text-indigo-700" />
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
<div>
<p className="font-medium">
{mq ? Number.parseFloat(mq) : 0}{" "}
<span>
m<sup>2</sup>
</span>
</p>
</div>
</div>
<div className="flex w-full items-center justify-center gap-1 rounded-md bg-neutral-100 py-3">
<BedDouble className="size-4 text-indigo-700" />
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
<div>
<p className="font-medium">
{camereTesti({ camere: camere, testi: t.card })}
</p>
</div>
</div>
<div className="flex w-full items-center justify-center gap-1 rounded-md bg-neutral-100 py-3">
<CalendarClock className="size-4 text-indigo-700" />
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
<div>
<p className="truncate font-medium">
{handleConsegna({
2025-08-29 16:18:32 +02:00
aggiornamento: t.card.in_aggiornamento,
2025-08-28 18:27:07 +02:00
consegna: consegna,
consegna_da: t.card.consegna_da,
mesi: t.preferenze.mesi,
subito: t.card.consegna_subito,
})}
</p>
</div>
</div>
</div>
</Link>
</div>
);
2025-08-04 17:45:44 +02:00
};
export const CarouselAnnuncio = ({
2025-08-28 18:27:07 +02:00
single,
immagini,
videos,
2025-08-04 17:45:44 +02:00
}: {
2025-08-28 18:27:07 +02:00
single?: boolean;
immagini: string[] | null;
videos: string[] | null;
2025-08-04 17:45:44 +02:00
}) => {
2025-08-28 18:27:07 +02:00
const [openModal, setOpenModal] = useState(false);
const [idxModal, setIdxModal] = useState(0);
function handleOpenModal(position: number) {
setIdxModal(position);
setOpenModal(true);
}
return (
<>
2025-10-10 16:18:43 +02:00
<div className="mx-auto my-4 w-full text-clip rounded-md">
2025-08-28 18:27:07 +02:00
<div className="relative">
2025-08-29 16:18:32 +02:00
<Carousel opts={{ align: "start", loop: true }}>
2025-08-28 18:27:07 +02:00
<CarouselContent>
{immagini?.map((img, idx) => (
<CarouselItem
className={cn(
2025-10-20 11:35:48 +02:00
"group relative flex items-center justify-center",
2025-08-28 18:27:07 +02:00
immagini && immagini.length === 1
? ""
: !single && "md:basis-1/2 lg:basis-1/3",
)}
2025-08-29 16:18:32 +02:00
key={`${img}-${idx}-cF`}
2025-08-28 18:27:07 +02:00
>
<ImageFlbk
alt={img}
className={cn(
2025-10-20 11:35:48 +02:00
"h-72 w-full rounded-md object-cover sm:h-80",
2025-08-28 18:27:07 +02:00
immagini.length === 1 && "object-contain",
)}
2025-08-29 16:18:32 +02:00
height={400}
onClick={() => handleOpenModal(idx)}
priority={idx === 0}
src={`${img}?${CacheKey}`}
2025-08-29 16:18:32 +02:00
width={800}
2025-08-28 18:27:07 +02:00
/>
<Maximize2
2025-10-10 16:18:43 +02:00
className="absolute right-2 bottom-2 size-7 origin-bottom-right cursor-pointer rounded-md bg-white/70 stroke-2 p-1 transition-all duration-300 ease-in-out hover:scale-150 group-hover:opacity-100 sm:opacity-0"
2025-08-29 16:18:32 +02:00
onClick={() => handleOpenModal(idx)}
2025-08-28 18:27:07 +02:00
/>
</CarouselItem>
))}
{videos?.map((video, idx) => {
if (!video) return null;
if (video.includes("youtu")) {
return null;
}
return (
<CarouselItem
className={cn(
"group relative",
immagini && immagini.length === 1
? ""
: !single && "md:basis-1/2 lg:basis-1/3",
)}
key={`videoplayer-${video}`}
>
<VideoPlayer
2025-10-10 16:18:43 +02:00
className="h-72"
coverImage={immagini?.[0] ? immagini[0] : ""}
key={`videoplayer-${idx}-${openModal}`}
videoSrc={`${video}?${CacheKey}`}
/>
<Maximize2
2025-10-10 16:18:43 +02:00
className="absolute right-2 bottom-2 size-7 origin-bottom-right cursor-pointer rounded-md bg-white/70 stroke-2 p-1 transition-all duration-300 ease-in-out hover:scale-150 group-hover:opacity-100 sm:opacity-0"
onClick={() =>
handleOpenModal(immagini?.length || 0 + idx)
}
/>
</CarouselItem>
);
})}
2025-08-28 18:27:07 +02:00
</CarouselContent>
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
<CarouselPrevious
className="left-2 cursor-pointer opacity-60 disabled:opacity-0"
disabled={!immagini || immagini.length === 1}
2025-08-29 16:18:32 +02:00
type="button"
2025-08-28 18:27:07 +02:00
/>
<CarouselNext
className="right-2 cursor-pointer opacity-60 disabled:opacity-0"
disabled={!immagini || immagini.length === 1}
2025-08-29 16:18:32 +02:00
type="button"
2025-08-28 18:27:07 +02:00
/>
</Carousel>
</div>
</div>
2025-08-29 16:18:32 +02:00
<Dialog onOpenChange={setOpenModal} open={openModal}>
2025-10-10 16:18:43 +02:00
<DialogContent className="flex h-full w-full max-w-full items-center justify-center border-transparent border-none bg-white p-0 md:max-w-[80vw] md:p-6 [&_#dialog-close>svg]:size-8">
2025-08-28 18:27:07 +02:00
<DialogHeader className="absolute top-0 right-0 left-0 z-10">
<DialogTitle className="sr-only">Image Dialog</DialogTitle>
<DialogDescription className="sr-only">Immagine</DialogDescription>
</DialogHeader>
<Carousel opts={{ loop: true, startIndex: idxModal }}>
<CarouselContent>
{immagini?.map((img, idx) => (
<CarouselItem key={`${img}-${idx}-cD`}>
<Image
alt={`carousel-img-${idx}`}
2025-10-10 16:18:43 +02:00
className="mx-auto h-[90vh] w-full rounded-md object-contain sm:w-[80vw]"
2025-08-28 18:27:07 +02:00
height={1080}
src={`${img}?${CacheKey}`}
2025-08-29 16:18:32 +02:00
width={1920}
2025-08-28 18:27:07 +02:00
/>
</CarouselItem>
))}
{videos?.map((video) => {
if (!video) return null;
if (video.includes("youtu")) {
return null;
}
return (
<CarouselItem
className="relative flex items-center justify-center"
key={`carousel-videoplayer-${video}`}
>
<VideoPlayer
2025-10-10 16:18:43 +02:00
className="absolute mx-auto h-[90vh] w-full max-w-fit rounded-md object-contain"
coverImage={immagini?.[0] ? immagini[0] : ""}
videoSrc={`${video}?${CacheKey}`}
/>
</CarouselItem>
);
})}
2025-08-28 18:27:07 +02:00
</CarouselContent>
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
<CarouselPrevious
className="left-2 cursor-pointer opacity-60 disabled:opacity-0"
disabled={!immagini || immagini.length === 1}
2025-08-29 16:18:32 +02:00
type="button"
2025-08-28 18:27:07 +02:00
/>
<CarouselNext
className="right-2 cursor-pointer opacity-60 disabled:opacity-0"
disabled={!immagini || immagini.length === 1}
2025-08-29 16:18:32 +02:00
type="button"
2025-08-28 18:27:07 +02:00
/>
</Carousel>
</DialogContent>
</Dialog>
</>
);
2025-08-04 17:45:44 +02:00
};