Refactor Annuncio Card Component and Remove Legacy Code

- Updated `CardAnnuncio` component to enhance functionality and styling.
- Removed `CardAnnuncio2` component as it was redundant.
- Added new icons and badges for better visual representation of availability and status.
- Improved layout and responsiveness of the card.
- Updated translations in English and Italian for consistency.
- Enhanced pagination controls in the `Annunci` page with new button styles.
- Adjusted footer components to accept additional class names for customization.
- Cleaned up unused CSS animations and styles in `globals.css`.
- Modified server-side controller to include homepage field in the Annuncio data structure.
This commit is contained in:
Marco Pedone 2025-11-03 18:49:27 +01:00
parent 4ca5c59ffe
commit 1fc855393f
17 changed files with 316 additions and 604 deletions

View file

@ -27,6 +27,7 @@ import { WhatsAppIcon2 } from "~/components/svgs";
import { Button } from "~/components/ui/button";
import { Separator } from "~/components/ui/separator";
import { UserViewContext, UserViewProvider } from "~/lib/userViewContext";
import { cn } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider";
import {
EnforcedSessionContext,
@ -37,41 +38,55 @@ import type { ValidSession } from "~/server/api/trpc";
import { api } from "~/utils/api";
interface Props {
bodyClassName?: string;
containerClassName?: string;
children: ReactNode;
noFooter?: boolean;
footerClassName?: string;
}
export const Layout = ({ children, noFooter }: Props) => {
export const Layout = ({
children,
noFooter,
bodyClassName,
containerClassName,
footerClassName,
}: Props) => {
const { data: bannerData } = api.settings.getBannerData.useQuery({
area_riservata: false,
});
return (
<div className="flex min-h-screen w-full flex-col">
<div className={cn("flex min-h-screen w-full flex-col", bodyClassName)}>
<SiteHeader />
{bannerData?.map((banner) => (
<div key={`banner-elem-${banner.idbanner}`}>
{BannerFactory(banner)}
</div>
))}
<main className="h-full flex-1 grow">{children}</main>
<main className={cn("h-full flex-1 grow", containerClassName)}>
{children}
</main>
{!noFooter && <Footer />}
{!noFooter && <Footer className={footerClassName} />}
{/* <CookieConsent /> */}
</div>
);
};
interface AreaRiservataLayoutProps extends Props {
ignoreSessionCheck?: boolean;
noSidebar?: boolean;
}
export const AreaRiservataLayout = ({
children,
noSidebar,
noFooter,
ignoreSessionCheck,
}: {
children: ReactNode;
noSidebar?: boolean;
noFooter?: boolean;
ignoreSessionCheck?: boolean;
}) => {
bodyClassName,
containerClassName,
footerClassName,
}: AreaRiservataLayoutProps) => {
const { data: bannerData } = api.settings.getBannerData.useQuery({
area_riservata: true,
});
@ -79,43 +94,70 @@ export const AreaRiservataLayout = ({
if (!ignoreSessionCheck) {
if (session.status === "pending")
return (
<div className="flex h-full min-h-screen flex-col text-clip">
<div
className={cn(
"flex h-full min-h-screen flex-col text-clip",
bodyClassName,
)}
>
<SiteHeader />
<main className="flex h-full flex-1 overflow-auto">
<main
className={cn(
"flex h-full flex-1 overflow-auto",
containerClassName,
)}
>
<div className="flex h-auto w-full flex-col md:flex-row">
<LoadingPage />
</div>
</main>
{!noFooter && <MiniFooter />}
{!noFooter && <MiniFooter className={footerClassName} />}
</div>
);
if (session.status !== "success" || !session.user) {
window.location.reload();
return (
<div className="flex h-full min-h-screen flex-col text-clip">
<div
className={cn(
"flex h-full min-h-screen flex-col text-clip",
bodyClassName,
)}
>
<SiteHeader />
<main className="flex h-full flex-1 overflow-auto">
<main
className={cn(
"flex h-full flex-1 overflow-auto",
containerClassName,
)}
>
<div className="flex h-auto w-full flex-col md:flex-row">
<LoadingPage />
</div>
</main>
{!noFooter && <MiniFooter />}
{!noFooter && <MiniFooter className={footerClassName} />}
</div>
);
}
}
return (
<div className="flex h-full min-h-screen flex-col text-clip">
<div
className={cn(
"flex h-full min-h-screen flex-col text-clip",
bodyClassName,
)}
>
<SiteHeader />
{bannerData?.map((banner) => (
<div key={`banner-elem-${banner.idbanner}`}>
{BannerFactory(banner)}
</div>
))}
<main className="flex h-full flex-1 overflow-auto">
<main
className={cn("flex h-full flex-1 overflow-auto", containerClassName)}
>
<div className="flex h-auto w-full flex-col md:flex-row">
{noSidebar ? null : (
<Sidebar
@ -128,7 +170,7 @@ export const AreaRiservataLayout = ({
</EnforcedSessionContext.Provider>
</div>
</main>
{!noFooter && <MiniFooter />}
{!noFooter && <MiniFooter className={footerClassName} />}
{/* <CookieConsent /> */}
</div>

View file

@ -10,6 +10,7 @@ import { cn } from "~/lib/utils";
interface AccordionCompProps extends InputHTMLAttributes<HTMLDivElement> {
texts: Faq[];
defaultOpen?: number;
}
/**
@ -29,7 +30,7 @@ interface AccordionCompProps extends InputHTMLAttributes<HTMLDivElement> {
* ```
*/
export const AccordionComp = forwardRef<HTMLDivElement, AccordionCompProps>(
({ texts, className }, ref) => {
({ texts, defaultOpen, className }, ref) => {
if (!texts || texts.length === 0) {
return null; // Return null if no texts are provided
}
@ -38,7 +39,14 @@ export const AccordionComp = forwardRef<HTMLDivElement, AccordionCompProps>(
className={cn("mx-auto flex max-w-xl justify-center", className)}
ref={ref}
>
<Accordion className="w-full" collapsible type="single">
<Accordion
className="w-full"
collapsible
defaultValue={
defaultOpen !== undefined ? `item-${defaultOpen}` : undefined
}
type="single"
>
{texts.map((text, index) => (
// biome-ignore lint/suspicious/noArrayIndexKey: <index is safe here>
<AccordionItem key={`ac-item-${index}`} value={`item-${index}`}>

View file

@ -1,11 +1,9 @@
import { CardAnnuncio } from "~/components/annuncio_card2";
import { getTitoloTranslation } from "~/lib/annuncio_details";
import { useTranslation } from "~/providers/I18nProvider";
import { CardAnnuncio } from "~/components/annuncio_card";
import type { AnnuncioRicerca } from "~/server/controllers/annunci.controller";
export const AnnunciGrid = ({ pagedata }: { pagedata: AnnuncioRicerca[] }) => {
return (
<div className="relative z-0 mx-auto grid max-w-7xl grid-flow-row grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
<div className="relative z-0 mx-auto grid max-w-7xl grid-flow-row grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-6 lg:grid-cols-3">
{pagedata.map((annuncio) => (
<CardAnnuncio key={annuncio.codice} {...annuncio} />
))}

View file

@ -1,4 +1,13 @@
import { BedDouble, CalendarClock, Maximize2, Ruler } from "lucide-react";
import {
BedDouble,
CalendarClock,
MapPin,
Maximize2,
Ruler,
Siren,
Star,
TrafficCone,
} from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
@ -20,69 +29,95 @@ import {
import { camereTesti, handleConsegna } from "~/lib/annuncio_details";
import { cn, formatCurrency } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider";
import type { AnnuncioRicerca } from "~/server/controllers/annunci.controller";
import { Badge } from "./ui/badge";
import { VideoPlayer } from "./videoPlayer";
type CardAnnuncioProps = {
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;
type CardAnnuncioProps = AnnuncioRicerca & {
className?: string;
videos?: string[] | undefined;
updated_at: Date | null;
images: { img: string; thumb: string }[];
};
export const CardAnnuncio = ({
id,
codice,
prezzo,
titolo,
titolo_it,
titolo_en,
mq,
comune,
provincia,
consegna,
camere,
//immagini,
numero_camere: camere,
tipo,
stato,
className,
videos,
url_video: videos,
updated_at,
images,
homepage,
}: CardAnnuncioProps) => {
const { t } = useTranslation();
const { t, locale } = useTranslation();
const currentMonth = new Date().getMonth() + 1;
const isAvailableNow = consegna === 0 || consegna === currentMonth;
return (
<div
className={cn(
"h-[464px] rounded-md bg-white shadow-neutral-100 shadow-sm outline outline-neutral-400 hover:shadow-md hover:outline-neutral-600",
"h-[31.1rem] rounded-md bg-white shadow-neutral-100 shadow-sm hover:shadow-md hover:outline-neutral-600",
className,
)}
id={`card-annuncio-${id}`}
>
<Link
className="block p-2"
//target="_blank"
className="flex h-full flex-col gap-2 p-2"
href={`/annuncio/${codice}`} //duration-700 ease-in-out animate-in fade-in
>
<div className="group relative overflow-clip text-clip rounded-md outline outline-neutral-300">
<div
className={cn("group relative overflow-clip text-clip rounded-md")}
>
<div
className={cn(
"absolute inset-0 z-20 size-full rounded-md",
homepage &&
(isAvailableNow
? "border-[.35rem] border-green-500"
: "border-[.35rem] border-blue-500"),
stato === "Trattativa" && "border-[.35rem] border-violet-500",
)}
></div>
{stato !== "Trattativa" && homepage && (
<div className="absolute top-1 z-20 flex w-full justify-center px-2">
<Badge
className={cn(
"flex items-center gap-1.5 rounded-t-none px-1 py-0.5 font-semibold text-base text-white shadow-lg [&>svg]:size-4.5",
isAvailableNow ? "bg-green-500" : "bg-blue-500",
)}
>
{isAvailableNow ? (
<>
<span>Disponibile Subito</span>
<Siren />
</>
) : (
<>
<span>In Evidenza</span>
<Star />
</>
)}
</Badge>
</div>
)}
{stato === "Trattativa" && (
<div>
<div className="absolute z-20 h-56 w-full">
<div className="absolute bottom-0 w-full touch-none select-none rounded-b-md bg-violet-500 text-center text-lg text-white">
Annuncio in Trattativa
</div>
</div>
<div className="absolute top-1 z-20 flex w-full justify-center px-2">
<Badge
className={cn(
"flex items-center gap-1.5 rounded-t-none bg-violet-500 px-1 py-0.5 font-semibold text-base text-white shadow-lg [&>svg]:size-4.5",
)}
>
<span>In Trattativa</span>
<TrafficCone />
</Badge>
</div>
)}
@ -93,7 +128,7 @@ export const CardAnnuncio = ({
<ImageFlbk
alt={t.card.alt_immagine}
blurDataURL={`/storage-api/get/${img.thumb}?image=true`}
className={"h-56 w-full rounded-md object-cover"}
className={"h-64 w-full object-cover"}
height={1080}
priority={idx === 0}
src={`/storage-api/get/${img.img}?image=true`}
@ -149,27 +184,27 @@ export const CardAnnuncio = ({
</div>
</Carousel>
</div>
<div className="flex h-full grow flex-col gap-2">
<div
className={cn(
"mt-2 flex w-full items-center justify-center rounded-md",
"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 className="font-semibold text-white">Affitto {tipo}</div>
</div>
<div className="mt-2 space-y-2">
<div className="flex flex-row justify-around">
<div className="text-center">
<div className="flex flex-row justify-between gap-3 px-4">
<div className="w-full text-center">
<span className="sr-only">{t.card.codice}</span>
<span className="font-semibold text-red-600 text-xl">
<span className="font-semibold text-red-500 text-xl">
Cod: {codice}
</span>
</div>
<div className="text-center">
<div className="w-full text-center">
<span className="sr-only">{t.card.prezzo}</span>
<span className="font-semibold text-xl">
@ -177,26 +212,26 @@ export const CardAnnuncio = ({
</span>
</div>
</div>
<div className="mb-3 text-center text-muted-foreground">
<div className="flex h-full grow flex-col justify-between">
<div className="px-4 text-center text-muted-foreground">
<span className="sr-only">{t.card.titolo}</span>
<span className="line-clamp-2 min-h-[3.5rem] font-medium text-base md:text-lg">
{titolo}
<span className="line-clamp-3 overflow-ellipsis font-medium text-base">
{locale === "it" ? titolo_it : titolo_en}
</span>
</div>
<div className="flex flex-col gap-4">
<div className="text-center text-muted-foreground">
<span className="sr-only">{t.card.indirizzo}</span>
<MapPin className="mr-1 mb-1 inline-block size-4" />
<span className="font-medium text-sm">
{comune && provincia && `${comune} (${provincia})`}
</span>
</div>
</div>
<div className="mt-4 flex items-center justify-around gap-2 text-muted-foreground text-xs">
<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" />
<div className="flex h-9 items-center justify-around gap-1.5 text-neutral-700 text-xs">
<div className="flex h-full w-full items-center justify-center gap-1 rounded-md bg-neutral-100">
<Ruler className="size-4 text-neutral-900" />
<div>
<p className="font-medium">
@ -207,8 +242,8 @@ export const CardAnnuncio = ({
</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" />
<div className="flex h-full w-full items-center justify-center gap-1 rounded-md bg-neutral-100">
<BedDouble className="size-4 text-neutral-900" />
<div>
<p className="font-medium">
@ -216,8 +251,8 @@ export const CardAnnuncio = ({
</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" />
<div className="flex h-full w-full items-center justify-center gap-1 rounded-md bg-neutral-100">
<CalendarClock className="size-4 text-neutral-900" />
<div>
<p className="truncate font-medium">
@ -232,6 +267,9 @@ export const CardAnnuncio = ({
</div>
</div>
</div>
</div>
</div>
</div>
</Link>
</div>
);
@ -273,7 +311,7 @@ export const CarouselAnnuncio = ({
<ImageFlbk
alt={img}
className={cn(
"aspect-square max-h-92 w-full rounded-md object-cover sm:max-h-80",
"aspect-square max-h-92 w-full rounded-md object-cover sm:max-h-80 xl:max-h-[25rem]",
immagini.length === 1 && "object-contain",
)}
height={400}

View file

@ -1,404 +0,0 @@
import { BedDouble, CalendarClock, Maximize2, Ruler } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
import { ImageFlbk } from "~/components/ImageWithFallback";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "~/components/ui/carousel";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog";
import { camereTesti, handleConsegna } from "~/lib/annuncio_details";
import { cn, formatCurrency } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider";
import { VideoPlayer } from "./videoPlayer";
type CardAnnuncioProps = {
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;
updated_at: Date | null;
images: { img: string; thumb: string }[];
};
export const CardAnnuncio = ({
id,
codice,
prezzo,
titolo,
mq,
comune,
provincia,
consegna,
camere,
//immagini,
tipo,
stato,
className,
videos,
updated_at,
images,
}: CardAnnuncioProps) => {
const { t } = useTranslation();
return (
<div
className={cn(
"h-[464px] rounded-md bg-white shadow-neutral-100 shadow-sm hover:shadow-md hover:outline-neutral-600",
className,
)}
id={`card-annuncio-${id}`}
>
<Link
className="block p-2"
//target="_blank"
href={`/annuncio/${codice}`} //duration-700 ease-in-out animate-in fade-in
>
<div className="group relative overflow-clip text-clip rounded-md">
<div className="absolute top-2 left-2 z-20 rounded-md bg-white px-2 py-1 font-semibold text-neutral-800 text-sm shadow-md shadow-neutral-300 backdrop-blur-sm">
Cod: {codice}
</div>
{stato === "Trattativa" && (
<div>
<div className="absolute z-20 h-56 w-full">
<div className="absolute bottom-0 w-full touch-none select-none rounded-b-md bg-violet-500 text-center text-lg text-white">
Annuncio in Trattativa
</div>
</div>
</div>
)}
<Carousel opts={{ loop: true }}>
<CarouselContent>
{images?.map((img, idx) => (
<CarouselItem key={`${img.img}key`}>
<ImageFlbk
alt={t.card.alt_immagine}
blurDataURL={`/storage-api/get/${img.thumb}?image=true`}
className={"h-64 w-full object-cover"}
height={1080}
priority={idx === 0}
src={`/storage-api/get/${img.img}?image=true`}
width={1920}
/>
</CarouselItem>
))}
{videos?.map((video) => {
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
cacheKey={updated_at?.toString() || new Date().toString()}
className="h-56"
coverImage={
images?.[0]
? `/storage-api/get/${images[0].img}?image=true`
: ""
}
key={`videoplayer-${video}`}
videoSrc={video}
/>
</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={!images || images.length === 1}
/>
<CarouselNext
className="right-2 cursor-pointer"
disabled={!images || images.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>
<span className="font-semibold text-red-600 text-xl">
Cod: {codice}
</span>
</div>
<div className="text-center">
<span className="sr-only">{t.card.prezzo}</span>
<span className="font-semibold text-xl">
{formatCurrency(prezzo / 1e2)}
</span>
</div>
</div>
<div className="mb-3 text-center text-muted-foreground">
<span className="sr-only">{t.card.titolo}</span>
<span className="line-clamp-2 min-h-[3.5rem] font-medium text-base md:text-lg">
{titolo}
</span>
</div>
<div className="text-center text-muted-foreground">
<span className="sr-only">{t.card.indirizzo}</span>
<span className="font-medium text-sm">
{comune && provincia && `${comune} (${provincia})`}
</span>
</div>
</div>
<div className="mt-4 flex items-center justify-around gap-2 text-muted-foreground text-xs">
<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" />
<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" />
<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" />
<div>
<p className="truncate font-medium">
{handleConsegna({
aggiornamento: t.card.in_aggiornamento,
consegna: consegna,
consegna_da: t.card.consegna_da,
mesi: t.preferenze.mesi,
subito: t.card.consegna_subito,
})}
</p>
</div>
</div>
</div>
</Link>
</div>
);
};
export const CarouselAnnuncio = ({
single,
immagini,
videos,
updated_at,
}: {
single?: boolean;
immagini: string[] | null;
videos: string[] | null;
updated_at: Date | null;
}) => {
const [openModal, setOpenModal] = useState(false);
const [idxModal, setIdxModal] = useState(0);
function handleOpenModal(position: number) {
setIdxModal(position);
setOpenModal(true);
}
return (
<>
<div className="mx-auto my-4 w-full text-clip rounded-md">
<div className="relative">
<Carousel opts={{ align: "start", loop: true }}>
<CarouselContent>
{immagini?.map((img, idx) => (
<CarouselItem
className={cn(
"group relative flex items-center justify-center",
immagini && immagini.length === 1
? ""
: !single && "md:basis-1/2 lg:basis-1/3",
)}
key={`${img}-cF`}
>
<ImageFlbk
alt={img}
className={cn(
"aspect-square max-h-92 w-full rounded-md object-cover sm:max-h-80",
immagini.length === 1 && "object-contain",
)}
height={400}
onClick={() => handleOpenModal(idx)}
priority={idx === 0}
src={`/storage-api/get/${img}?image=true&${updated_at?.toString() || new Date().toString()}`}
width={800}
/>
<Maximize2
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(idx)}
/>
</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
cacheKey={updated_at?.toString() || new Date().toString()}
className="h-72"
coverImage={
immagini?.[0]
? `/storage-api/get/${immagini[0]}?image=true`
: ""
}
key={`videoplayer-${idx}-${openModal}`}
videoSrc={video}
/>
<Maximize2
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>
);
})}
</CarouselContent>
<CarouselPrevious
className="left-2 cursor-pointer opacity-60 disabled:opacity-0"
disabled={!immagini || immagini.length === 1}
type="button"
/>
<CarouselNext
className="right-2 cursor-pointer opacity-60 disabled:opacity-0"
disabled={!immagini || immagini.length === 1}
type="button"
/>
</Carousel>
</div>
</div>
<Dialog onOpenChange={setOpenModal} open={openModal}>
<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">
<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}-cD`}>
<Image
alt={`carousel-img-${idx}`}
className="mx-auto h-[90vh] w-full rounded-md object-contain sm:w-[80vw]"
height={1080}
src={`/storage-api/get/${img}?image=true&${updated_at?.toString() || new Date().toString()}`}
width={1920}
/>
</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
cacheKey={updated_at?.toString() || new Date().toString()}
className="absolute mx-auto h-[90vh] w-full max-w-fit rounded-md object-contain"
coverImage={
immagini?.[0]
? `/storage-api/get/${immagini[0]}?image=true`
: ""
}
videoSrc={video}
/>
</CarouselItem>
);
})}
</CarouselContent>
<CarouselPrevious
className="left-2 cursor-pointer opacity-60 disabled:opacity-0"
disabled={!immagini || immagini.length === 1}
type="button"
/>
<CarouselNext
className="right-2 cursor-pointer opacity-60 disabled:opacity-0"
disabled={!immagini || immagini.length === 1}
type="button"
/>
</Carousel>
</DialogContent>
</Dialog>
</>
);
};

View file

@ -1,12 +1,22 @@
import Link from "next/link";
import BlurryDivider from "~/components/blurry_divider";
import { LogoSvg } from "~/components/svgs";
import { cn } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider";
export const Footer = () => {
type FooterProps = {
className?: string;
};
export const Footer = ({ className }: FooterProps) => {
const { t } = useTranslation();
return (
<div className="mt-auto">
<footer className="bottom-0 z-40 w-full bg-muted text-muted-foreground">
<footer
className={cn(
"bottom-0 z-40 w-full bg-muted text-muted-foreground",
className,
)}
>
<div className="mx-auto max-w-6xl px-4 py-6 sm:px-6 lg:px-8">
<div className="text-center">
<Link
@ -73,11 +83,16 @@ export const Footer = () => {
</div>
);
};
export const MiniFooter = () => {
export const MiniFooter = ({ className }: FooterProps) => {
const { t } = useTranslation();
return (
<div className="mt-auto flex w-full">
<footer className="z-40 w-full bg-neutral-100 text-neutral-700 dark:bg-primary dark:text-white">
<footer
className={cn(
"z-40 w-full bg-neutral-100 text-neutral-700 dark:bg-primary dark:text-white",
className,
)}
>
<div className="mx-auto max-w-5xl px-2 py-2 sm:px-6 lg:px-8">
<div className="text-center">
<Link

View file

@ -278,9 +278,9 @@ export const en: LangDict = {
annulla: "Cancel",
annunci: {
accordion_minifaq: [
Faqs.cosa_significa_transitorio,
Faqs.posso_visitarli,
Faqs.posso_contattarvi,
Faqs.cosa_significa_transitorio,
Faqs.come_confermare,
],
comune: "City",

View file

@ -283,9 +283,9 @@ export const it: LangDict = {
annulla: "Annulla",
annunci: {
accordion_minifaq: [
Faqs.cosa_significa_transitorio,
Faqs.posso_visitarli,
Faqs.posso_contattarvi,
Faqs.cosa_significa_transitorio,
Faqs.come_confermare,
],
comune: "Comune",

View file

@ -1,8 +1,9 @@
import { keepPreviousData } from "@tanstack/react-query";
import {
ChevronDown,
ChevronLeft,
ChevronRight,
ChevronsRight,
ChevronsLeft,
ChevronUp,
Filter,
FilterX,
@ -18,6 +19,7 @@ import { AnnunciGrid } from "~/components/annunci_grid";
import { MapSection } from "~/components/annunci_map";
import { CTA_TipologiaModal } from "~/components/annunci_tutorial";
import { CodiceBox } from "~/components/codiceRicerca";
import { Layout } from "~/components/Layout";
import { LoadingPage } from "~/components/loading";
import { Status500 } from "~/components/status-page";
import { Badge } from "~/components/ui/badge";
@ -60,23 +62,52 @@ const Annunci = ({ options }: AnnunciPageProps) => {
<meta content={t.heads.annunci_description} name="description" />
</Head>
<main className="mx-auto w-full max-w-7xl space-y-5 bg-background2 px-2 py-5 md:px-8">
<div className="inline-block rounded-md bg-primary/10 px-3 py-1 text-primary text-sm">
<main className="relative mx-auto w-full max-w-7xl space-y-5 bg-background2 px-2 py-5 md:px-8">
<div className="inline-block rounded-md bg-accent px-3 py-1 text-primary text-sm outline outline-neutral-500">
{t.annunci.titolo}
</div>
<GoBackButton />
<Ricerca />
<AccordionComp
className="max-w-6xl px-4 py-4 md:py-8"
defaultOpen={0}
texts={t.annunci.accordion_minifaq}
/>
</main>
</RicercaProvider>
);
};
const GoBackButton = () => {
const { page, setPage } = useRicerca();
if (page >= 2) {
return (
<Button
className="absolute right-0 h-auto py-0 text-base text-neutral-600 underline underline-offset-1 sm:hidden"
onClick={async () => {
await setPage(0);
}}
variant="link"
>
Torna all'inizio
</Button>
);
}
};
export default Annunci;
Annunci.getLayout = (page: React.ReactNode) => {
return (
<Layout
containerClassName="bg-background2"
footerClassName="bg-background2"
>
{page}
</Layout>
);
};
export const getServerSideProps = (async () => {
const helper = generateSSGHelper();
const options = await helper.annunci.getAnnunciOptions.fetch();
@ -94,11 +125,11 @@ const Ricerca = () => {
}, [page]);
return (
<>
<div className="space-y-5">
<RicercaFilters />
<br />
{map ? <MapSection /> : <AnnunciList />}
</>
</div>
);
};
@ -338,63 +369,53 @@ const AnnunciList = () => {
) : (
<AnnunciGrid pagedata={data.annunci} />
)}
<br />
<div className="mx-auto grid max-w-sm grid-cols-5 items-center justify-between gap-2">
<div className="flex items-center justify-center">
<button
aria-label="Reset Page"
className={cn(
"rounded-lg bg-red-500/80 px-4 py-2 text-white hover:bg-red-500 active:shadow-lg",
page < 2 ? "invisible" : "visible",
)}
disabled={page < 2}
<div className="flex items-center justify-center gap-2">
<div className="flex w-24 items-center justify-end gap-2">
{page >= 2 && (
<Button
aria-label="Go to first page"
onClick={async () => {
await setPage(0);
}}
type="button"
title="Go to first page"
variant="outline"
>
<ChevronsRight className="rotate-180" height={30} />
</button>
</div>
<div className="flex items-center justify-center">
<button
aria-label="Previous Page"
className={cn(
"rounded-lg bg-red-500/80 px-4 py-2 text-white hover:bg-red-500 active:shadow-lg",
page < 1 ? "invisible" : "visible",
<ChevronsLeft className="h-4 w-4" />
</Button>
)}
<Button
aria-label="Previous page"
disabled={page === 0}
onClick={async () => {
await setPage((old) => (old ? Math.max(old - 1, 0) : 0));
}}
type="button"
title="Previous page"
variant="outline"
>
<ChevronRight className="rotate-180" height={30} />
</button>
<ChevronLeft className="h-4 w-4" />
</Button>
</div>
<div className="flex items-center justify-center">
<span className="font-semibold text-lg text-neutral-500">
<div className="mx-4 w-12 rounded-md bg-white py-2 text-center font-medium text-lg">
{page + 1}
</span>
</div>
<div className="flex items-center justify-center">
<button
aria-label="Next Page"
className={cn(
"rounded-lg bg-red-500/80 px-4 py-2 text-white hover:bg-red-500 active:shadow-lg",
data?.hasMore ? "visible" : "invisible",
)}
<div className="flex w-24 items-center justify-start gap-2">
<Button
aria-label="Next page"
disabled={isPlaceholderData || !data?.hasMore}
onClick={async () => {
await setPage((old) =>
old ? (data?.hasMore ? old + 1 : old) : 1,
);
}}
type="button"
title="Next page"
variant="outline"
>
<ChevronRight height={30} />
</button>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
</>

View file

@ -132,7 +132,7 @@ const AnnuncioView = ({ cod, flag }: Omit<AnnuncioProps, "meta">) => {
return (
<AnnuncioContext.Provider value={data}>
<TouchProvider>
<div className="mx-auto w-full px-2 sm:px-8">
<div className="mx-auto w-full max-w-[100rem] px-2 sm:px-8">
<CarouselAnnuncio
immagini={data.images.map((img) => img.img)}
updated_at={data.updated_at}

View file

@ -12,7 +12,7 @@ const ChiSiamo: NextPage = () => {
<meta content={t.heads.main_description} name="description" />
</Head>
<main className="mx-auto w-full max-w-7xl space-y-5 px-2 py-5 md:px-8">
<div className="inline-block rounded-md bg-primary/10 px-3 py-1 text-primary text-sm">
<div className="inline-block rounded-md bg-accent px-3 py-1 text-primary text-sm outline outline-neutral-500">
{t.chi_siamo.title}
</div>
<div className="mx-auto w-full max-w-6xl space-y-5 sm:space-y-14">

View file

@ -68,7 +68,7 @@ const Contatto: NextPage = () => {
<meta content={t.heads.main_description} name="description" />
</Head>
<main className="mx-auto w-full max-w-7xl px-2 py-5 md:px-8">
<div className="inline-block rounded-md bg-primary/10 px-3 py-1 text-primary text-sm">
<div className="inline-block rounded-md bg-accent px-3 py-1 text-primary text-sm outline outline-neutral-500">
{t.contatti}
</div>

View file

@ -20,7 +20,7 @@ const Guida: NextPage = () => {
<meta content={t.heads.main_description} name="description" />
</Head>
<main className="mx-auto w-full max-w-7xl px-2 py-5 md:px-8">
<div className="inline-block rounded-md bg-primary/10 px-3 py-1 text-primary text-sm">
<div className="inline-block rounded-md bg-accent px-3 py-1 text-primary text-sm outline outline-neutral-500">
{t.guida}
</div>

View file

@ -44,7 +44,7 @@ const Prezzi: NextPage = () => {
<meta content={t.heads.prezzi_description} name="description" />
</Head>
<section className="mx-auto max-w-7xl px-2 py-5 md:px-8">
<div className="inline-block rounded-md bg-primary/10 px-3 py-1 text-primary text-sm">
<div className="inline-block rounded-md bg-accent px-3 py-1 text-primary text-sm outline outline-neutral-500">
{t.prezzi_titolo}
</div>
<div className="mt-5 space-y-14">

View file

@ -17,7 +17,7 @@ const Proprietari: NextPage = () => {
<meta content={t.proprietari.description} name="description" />
</Head>
<main className="mx-auto w-full max-w-7xl space-y-5 px-2 py-5 md:px-8">
<div className="inline-block rounded-md bg-primary/10 px-3 py-1 text-primary text-sm">
<div className="inline-block rounded-md bg-accent px-3 py-1 text-primary text-sm outline outline-neutral-500">
{t.proprietari.titolo}
</div>
<div className="mx-auto w-full space-y-5 sm:space-y-14">

View file

@ -214,6 +214,7 @@ export const get_AnnunciPositionsHandler = async ({
"lon_secondario",
"lat_secondario",
"updated_at",
"homepage",
jsonArrayFrom(
eb
.selectFrom("images_refs")
@ -268,6 +269,7 @@ export type AnnuncioRicerca = Pick<
| "stato"
| "url_video"
| "updated_at"
| "homepage"
> & {
images: Pick<ImagesRefs, "img" | "thumb">[];
};
@ -286,7 +288,10 @@ export const getCursor_AnnunciHandler = async ({
comune?: string;
consegna?: number;
sort?: "Recenti" | "Prezzo" | "Consegna" | "Mq";
}) => {
}): Promise<{
annunci: AnnuncioRicerca[];
hasMore: boolean;
}> => {
try {
let query = db
.selectFrom("annunci")
@ -306,6 +311,7 @@ export const getCursor_AnnunciHandler = async ({
"stato",
"url_video",
"updated_at",
"homepage",
jsonArrayFrom(
eb
.selectFrom("images_refs")

View file

@ -80,7 +80,7 @@
--accordion-down: accordion-down 0.2s ease-out;
--accordion-up: accordion-up 0.2s ease-out;
--caret-blink: caret-blink 1.25s ease-out infinite;
--border-pulse: border-pulse 2s infinite;
@keyframes accordion-down {
0% {
@ -141,19 +141,7 @@
}
}
@keyframes border-pulse {
0%: {
border-color: hsl(var(--border)) / 10;
}
50%: {
border-color: hsl(var(--border));
}
100%: {
border-color: hsl(var(--border)) / 10;
}
}
--animate-expand: expand 0.3s ease-out forwards;
@keyframes expand {