refactor: Remove AnnunciGrid component and update Annunci page to use CardAnnuncio directly

This commit is contained in:
Marco Pedone 2025-11-04 19:18:13 +01:00
parent 608e6a8e10
commit 7b0469467f
12 changed files with 290 additions and 263 deletions

View file

@ -1,14 +0,0 @@
import { CardAnnuncio } from "~/components/annuncio_card";
import type { AnnuncioRicerca } from "~/server/controllers/annunci.controller";
//TODO usare anche in accordion e dialog pag ricerca servizio
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-4 sm:grid-cols-2 sm:gap-6 lg:grid-cols-3">
{pagedata.map((annuncio) => (
<CardAnnuncio key={annuncio.codice} {...annuncio} />
))}
</div>
);
};

View file

@ -7,6 +7,7 @@ import LoadingButton from "~/components/custom_ui/loading-button";
import { Button, buttonVariants } from "~/components/ui/button"; import { Button, buttonVariants } from "~/components/ui/button";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import { useAnnuncio } from "~/providers/AnnuncioProvider"; import { useAnnuncio } from "~/providers/AnnuncioProvider";
import { useServizio } from "~/providers/ServizioProvider";
import type { SessionContextType } from "~/providers/SessionProvider"; import type { SessionContextType } from "~/providers/SessionProvider";
import type { AnnunciId } from "~/schemas/public/Annunci"; import type { AnnunciId } from "~/schemas/public/Annunci";
import type { UsersId } from "~/schemas/public/Users"; import type { UsersId } from "~/schemas/public/Users";
@ -108,10 +109,14 @@ const ServizioInteraction = ({
); );
} }
return <InteressatoButton annuncioId={annuncioId} />; return <InteressatoButtonUserOnly annuncioId={annuncioId} />;
}; };
const InteressatoButton = ({ annuncioId }: { annuncioId: AnnunciId }) => { const InteressatoButtonUserOnly = ({
annuncioId,
}: {
annuncioId: AnnunciId;
}) => {
const { data: hasInterest, isLoading } = const { data: hasInterest, isLoading } =
api.intrests.hasUserInterest.useQuery({ api.intrests.hasUserInterest.useQuery({
annuncioId, annuncioId,
@ -161,36 +166,57 @@ const InteressatoButton = ({ annuncioId }: { annuncioId: AnnunciId }) => {
); );
}; };
export const InteressatoButtonBatchV = ({ export const InteressatoButtonServizio = ({
annuncioId, annuncioId,
hasInterest,
}: { }: {
annuncioId: AnnunciId; annuncioId: AnnunciId;
hasInterest: boolean;
}) => { }) => {
const { isAdmin, servizioId, userId } = useServizio();
const utils = api.useUtils(); const utils = api.useUtils();
const { mutate: add } = api.intrests.addIntrest.useMutation({
const { mutate: adminAdd, isPending: isAdminPending } =
api.servizio.addServizioAnnunci.useMutation({
onError: (error) => {
console.error(error);
toast.error("Errore durante l'aggiunta dell'annuncio");
},
onSuccess: async () => {
await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
await utils.servizio.getAnnunciAvailableToAdd.invalidate({
servizioId,
});
await utils.servizio.AnnuncioServizioTipologiaMatch.invalidate({
userId,
});
await utils.servizio.getCompatibileAnnunci.invalidate({
servizioId,
});
toast.success("Annuncio aggiunto alla ricerca");
},
});
const { mutate: add, isPending } = api.intrests.addIntrest.useMutation({
onSuccess: async () => { onSuccess: async () => {
await utils.intrests.getUserInterests.invalidate(); await utils.intrests.getUserInterests.invalidate();
toast.success("Richiesta di interesse inviata con successo!"); toast.success("Richiesta di interesse inviata con successo!");
}, },
}); });
if (hasInterest) {
return (
<span className="flex w-full items-center justify-center gap-2 rounded-md bg-green-500 p-2 font-semibold text-sm text-white">
<BadgePlus className="inline size-5" />
Hai già mandato una richiesta
</span>
);
}
return ( return (
<Button <LoadingButton
aria-label="Sono interessato" aria-label="Sono interessato"
className="w-full" className="w-full"
//size="xl" loading={isPending || isAdminPending}
onClick={() => { onClick={() => {
if (isAdmin && servizioId) {
adminAdd({
servizioId,
annunci: [annuncioId],
});
return;
}
add({ add({
annuncioId, annuncioId,
}); });
@ -198,7 +224,7 @@ export const InteressatoButtonBatchV = ({
variant="info" variant="info"
> >
<BadgePlus className="size-6" /> <BadgePlus className="size-6" />
Sono interessato {isAdmin ? "Aggiungi annuncio" : "Sono interessato"}
</Button> </LoadingButton>
); );
}; };

View file

@ -35,6 +35,8 @@ import { VideoPlayer } from "./videoPlayer";
type CardAnnuncioProps = AnnuncioRicerca & { type CardAnnuncioProps = AnnuncioRicerca & {
className?: string; className?: string;
noLink?: boolean;
onlyFirstImage?: boolean;
}; };
export const CardAnnuncio = ({ export const CardAnnuncio = ({
@ -55,23 +57,36 @@ export const CardAnnuncio = ({
updated_at, updated_at,
images, images,
homepage, homepage,
noLink = false,
onlyFirstImage = false,
}: CardAnnuncioProps) => { }: CardAnnuncioProps) => {
const { t, locale } = useTranslation(); const { t, locale } = useTranslation();
const currentMonth = new Date().getMonth() + 1; const currentMonth = new Date().getMonth() + 1;
const isAvailableNow = consegna === 0 || consegna === currentMonth; const isAvailableNow = consegna === 0 || consegna === currentMonth;
const Wrapper = ({ children }: { children: React.ReactNode }) => {
if (noLink) {
return <div className="flex h-full flex-col gap-2 p-2">{children}</div>;
}
return (
<Link
className="flex h-full flex-col gap-2 p-2"
href={`/annuncio/${codice}`}
>
{children}
</Link>
);
};
return ( return (
<div <div
className={cn( className={cn(
"h-[31.1rem] rounded-md bg-white shadow-neutral-100 shadow-sm hover:shadow-md hover:outline-neutral-600", "relative h-[31.1rem] rounded-md bg-white shadow-neutral-100 shadow-sm outline outline-neutral-300 hover:shadow-md hover:outline-neutral-600",
className, className,
)} )}
id={`card-annuncio-${id}`} id={`card-annuncio-${id}`}
> >
<Link <Wrapper>
className="flex h-full flex-col gap-2 p-2"
href={`/annuncio/${codice}`} //duration-700 ease-in-out animate-in fade-in
>
<div <div
className={cn("group relative overflow-clip text-clip rounded-md")} className={cn("group relative overflow-clip text-clip rounded-md")}
> >
@ -120,69 +135,99 @@ export const CardAnnuncio = ({
</Badge> </Badge>
</div> </div>
)} )}
{!onlyFirstImage ? (
<Carousel opts={{ loop: true }}> <Carousel opts={{ loop: true }}>
<CarouselContent> <CarouselContent>
{images?.map((img, idx) => ( {images?.map((img, idx) => (
<CarouselItem key={`${img.img}key`}> <CarouselItem key={`${img.img}key`}>
<ImageFlbk <ImageFlbk
alt={t.card.alt_immagine} alt={t.card.alt_immagine}
blurDataURL={`/storage-api/get/${img.thumb}?image=true`} blurDataURL={`/storage-api/get/${img.thumb}?image=true`}
className={"h-64 w-full object-cover"} className={"h-64 w-full object-cover"}
height={1080} height={1080}
priority={idx === 0} priority={idx === 0}
src={`/storage-api/get/${img.img}?image=true`} src={`/storage-api/get/${img.img}?image=true`}
width={1920} 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> </CarouselItem>
); ))}
})} {videos?.map((video) => {
</CarouselContent> 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 <div
className="block opacity-0 transition-all duration-200 group-hover:opacity-60 disabled:group-hover:opacity-0" className="block opacity-0 transition-all duration-200 group-hover:opacity-60 disabled:group-hover:opacity-0"
onClick={(e) => e.preventDefault()} onClick={(e) => {
onKeyDown={(e) => e.stopPropagation()} e.preventDefault();
role="button" e.stopPropagation();
tabIndex={0} }}
> onKeyDown={(e) => e.stopPropagation()}
<CarouselPrevious role="button"
className="left-2 cursor-pointer" tabIndex={0}
disabled={!images || images.length === 1} >
/> <CarouselPrevious
<CarouselNext className="left-2 cursor-pointer"
className="right-2 cursor-pointer" disabled={!images || images.length === 1}
disabled={!images || images.length === 1} onClick={(e) => {
/> e.preventDefault();
</div> e.stopPropagation(); // Add this to the buttons too
</Carousel> }}
/>
<CarouselNext
className="right-2 cursor-pointer"
disabled={!images || images.length === 1}
onClick={(e) => {
e.preventDefault();
e.stopPropagation(); // Add this to the buttons too
}}
/>
</div>
</Carousel>
) : (
<>
{images && images.length > 0 && images[0] ? (
<ImageFlbk
alt={t.card.alt_immagine}
blurDataURL={`/storage-api/get/${images[0].thumb}?image=true`}
className={"h-64 w-full object-cover"}
height={1080}
priority={true}
src={`/storage-api/get/${images[0].img}?image=true`}
width={1920}
/>
) : (
<div className="flex h-64 w-full items-center justify-center bg-neutral-200 text-neutral-500"></div>
)}
</>
)}
</div> </div>
<div className="flex h-full grow flex-col gap-2"> <div className="flex h-full grow flex-col gap-2">
<div <div
@ -270,7 +315,7 @@ export const CardAnnuncio = ({
</div> </div>
</div> </div>
</div> </div>
</Link> </Wrapper>
</div> </div>
); );
}; };

View file

@ -15,17 +15,14 @@ const LoadingButton = ({
}: ButtonProps & { loading?: boolean }) => { }: ButtonProps & { loading?: boolean }) => {
return ( return (
<Button <Button
className={cn(buttonVariants({ className, size, variant }))} className={cn(
buttonVariants({ className, size, variant }),
loading && "text-transparent",
)}
disabled={loading || props.disabled}
{...props} {...props}
> >
{loading && ( {loading && <LoaderCircle className="absolute animate-spin text-muted" />}
<LoaderCircle
aria-hidden="true"
className="-ms-1 me-2 animate-spin"
size={16}
strokeWidth={2}
/>
)}
{props.children} {props.children}
</Button> </Button>
); );

View file

@ -6,21 +6,16 @@ import {
ExternalLink, ExternalLink,
FolderKanban, FolderKanban,
Pen, Pen,
Plus,
ShieldUser, ShieldUser,
Trash2, Trash2,
Wrench, Wrench,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useRef, useState } from "react"; import { useState } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { z } from "zod/v4"; import { z } from "zod/v4";
import { ExtIcon } from "~/components/area-riservata/allegati"; import { ExtIcon } from "~/components/area-riservata/allegati";
import { Confirm } from "~/components/confirm"; import { Confirm } from "~/components/confirm";
import {
AnimatedButton,
type AnimatedButtonRef,
} from "~/components/custom_ui/animated-button";
import { import {
Credenza, Credenza,
CredenzaBody, CredenzaBody,
@ -62,7 +57,6 @@ import { UploadModal } from "~/components/upload_modal";
import { cn, formatCurrency } from "~/lib/utils"; import { cn, formatCurrency } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider"; import { useTranslation } from "~/providers/I18nProvider";
import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider"; import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider";
import type { AnnunciId } from "~/schemas/public/Annunci";
import type { ServizioAnnunciUpdate } from "~/schemas/public/ServizioAnnunci"; import type { ServizioAnnunciUpdate } from "~/schemas/public/ServizioAnnunci";
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum"; import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
import type { FileMetadataWithAdmin } from "~/server/services/storage.service"; import type { FileMetadataWithAdmin } from "~/server/services/storage.service";
@ -117,7 +111,7 @@ export const AnnuncioActions = () => {
</PopoverTrigger> </PopoverTrigger>
<PopoverContent <PopoverContent
align="end" align="end"
className="flex w-fit flex-col items-center gap-2" className="flex w-fit flex-col items-center gap-4"
> >
<Link <Link
href={`/area-riservata/admin/edit-annuncio/${annuncioId}`} href={`/area-riservata/admin/edit-annuncio/${annuncioId}`}
@ -1213,51 +1207,3 @@ const SendConferma = ({
</Card> </Card>
); );
}; };
export const AddButton = ({ annuncioId }: { annuncioId: AnnunciId }) => {
const { servizioId, userId } = useServizio();
const utils = api.useUtils();
const { mutate } = api.servizio.addServizioAnnunci.useMutation({
onError: (error) => {
console.error(error);
toast.error("Errore durante l'aggiunta dell'annuncio");
},
onSuccess: async () => {
await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
await utils.servizio.getAnnunciAvailableToAdd.invalidate({
servizioId,
});
await utils.servizio.AnnuncioServizioTipologiaMatch.invalidate({
userId,
});
await utils.servizio.getCompatibileAnnunci.invalidate({
servizioId,
});
toast.success("Annuncio aggiunto alla ricerca");
},
});
const buttonRef = useRef<AnimatedButtonRef>(null);
return (
<AnimatedButton
className="group flex w-full gap-2"
onClick={async () => {
buttonRef.current?.sparkle();
//wait for the animation to finish
await new Promise((resolve) => setTimeout(resolve, 1000));
mutate({ annunci: [annuncioId], servizioId });
}}
ref={buttonRef}
sparklesClassName="fill-yellow-500 stroke-transparent"
sparklesIcon="thumbs-up"
sparklesNumber={15}
sparklesRadius={60}
sparklesScale={[1, 1.5]}
variant="info"
>
<Plus className="size-5" />
<span>Aggiungi agli annunci da contattare</span>
</AnimatedButton>
);
};

View file

@ -1,5 +1,5 @@
import { add } from "date-fns"; import { add } from "date-fns";
import { Package, PackageCheck, UserCircle } from "lucide-react"; import { CalendarClock, Package, PackageCheck, UserCircle } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect } from "react"; import { useEffect } from "react";
@ -209,7 +209,7 @@ export const ServizioContent = () => {
<ServizioCard <ServizioCard
content={ content={
<> <>
<div className="flex w-full flex-col gap-4 sm:flex-row sm:gap-8"> <div className="flex w-full flex-col gap-8 sm:flex-row sm:items-start">
<div className="flex flex-2 flex-col gap-2 sm:max-w-1/2"> <div className="flex flex-2 flex-col gap-2 sm:max-w-1/2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -225,10 +225,15 @@ export const ServizioContent = () => {
value={openContattiAnnunci.length * 10} value={openContattiAnnunci.length * 10}
/> />
</div> </div>
<span> <div className="flex flex-col flex-wrap gap-2 sm:flex-row sm:items-center">
{`Durata servizio: ${new Date(servizio.decorrenza).toLocaleDateString("it-IT")} - ${new Date(add(servizio.decorrenza, { days: 60 })).toLocaleDateString("it-IT")}`} <div className="flex items-center gap-2">
</span> <CalendarClock className="size-6 text-primary" />
{/* <ServizioDuration decorrenza={servizio.decorrenza} /> */} <span className="font-medium">Durata servizio:</span>
</div>
<span>
{`${new Date(servizio.decorrenza).toLocaleDateString("it-IT")} - ${new Date(add(servizio.decorrenza, { days: 60 })).toLocaleDateString("it-IT")}`}
</span>
</div>
</div> </div>
{annuncioConfermato.length > 0 && ( {annuncioConfermato.length > 0 && (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
@ -245,17 +250,17 @@ export const ServizioContent = () => {
> >
<h3> <h3>
{a.codice} - confermato il:{" "} {a.codice} - confermato il:{" "}
{/** biome-ignore lint/style/noNonNullAssertion: <already checked above> */} {a.accettato_conferma_at &&
{new Date(a.accettato_conferma_at!).toLocaleDateString( new Date(a.accettato_conferma_at).toLocaleDateString(
"it", "it",
{ {
day: "2-digit", day: "2-digit",
hour: "2-digit", hour: "2-digit",
minute: "2-digit", minute: "2-digit",
month: "2-digit", month: "2-digit",
year: "numeric", year: "numeric",
}, },
)} )}
</h3> </h3>
<SaldoButton <SaldoButton
annuncioId={a.annunci_id} annuncioId={a.annunci_id}

View file

@ -299,7 +299,7 @@ const EditPreferenze = () => {
Preferenze Preferenze
</CredenzaDescription> </CredenzaDescription>
</CredenzaHeader> </CredenzaHeader>
<CredenzaBody className="max-h-[80vh] w-full max-w-3xl overflow-y-auto pb-5"> <CredenzaBody className="max-h-[80vh] w-full max-w-3xl overflow-y-auto pb-5 sm:max-w-5xl">
{isLoading ? ( {isLoading ? (
<LoadingPage /> <LoadingPage />
) : ( ) : (

View file

@ -2,8 +2,7 @@ import { ExternalLink, Trash2 } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { InteressatoButtonBatchV } from "~/components/annuncio-interactions/annuncio_interactions"; import { InteressatoButtonServizio } from "~/components/annuncio-interactions/annuncio_interactions";
import { AddButton } from "~/components/servizio/annuncio_actions";
import { import {
AnnuncioCard, AnnuncioCard,
BasicAnnuncioCard, BasicAnnuncioCard,
@ -22,6 +21,7 @@ import {
import type { UsersId } from "~/schemas/public/Users"; import type { UsersId } from "~/schemas/public/Users";
import type { ServizioData } from "~/server/controllers/servizio.controller"; import type { ServizioData } from "~/server/controllers/servizio.controller";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import { CardAnnuncio } from "../annuncio_card";
import { LoadingPage } from "../loading"; import { LoadingPage } from "../loading";
import { import {
Dialog, Dialog,
@ -44,7 +44,7 @@ const DialogServizio = ({
return ( return (
<Dialog> <Dialog>
<DialogTrigger asChild>{trigger}</DialogTrigger> <DialogTrigger asChild>{trigger}</DialogTrigger>
<DialogContent className="md:max-w-5xl"> <DialogContent className="p-2 sm:max-w-5xl sm:p-6 md:max-w-7xl">
<DialogHeader> <DialogHeader>
<DialogTitle>{title}</DialogTitle> <DialogTitle>{title}</DialogTitle>
<DialogDescription className="sr-only"> <DialogDescription className="sr-only">
@ -221,25 +221,21 @@ export const AnnunciSelezionatiAccordion = ({
); );
}; };
export const AnnunciCompatibili = () => { export const AnnunciCompatibili = () => {
const { servizioId, userId, isAdmin } = useServizio(); const { servizioId } = useServizio();
const { data, isLoading } = api.servizio.getCompatibileAnnunci.useQuery({ const { data, isLoading } = api.servizio.getCompatibileAnnunci.useQuery({
servizioId, servizioId,
}); });
const { data: userIntrests } = api.intrests.getUserInterests.useQuery({
userId,
});
return ( return (
<DialogServizio <DialogServizio
title="Annunci compatibili" title="Annunci compatibili con le preferenze del servizio"
trigger={ trigger={
<Button className="w-full bg-purple-500 sm:w-fit"> <Button className="w-full bg-purple-500 sm:w-fit">
Esplora Annunci Compatibili Esplora Annunci Compatibili
</Button> </Button>
} }
> >
<div className="flex max-h-[80vh] flex-col gap-2 overflow-y-auto"> <div className="flex max-h-[80vh] flex-col gap-2 overflow-y-auto p-0.5 sm:p-2">
{isLoading ? ( {isLoading ? (
<LoadingPage /> <LoadingPage />
) : ( ) : (
@ -252,25 +248,23 @@ export const AnnunciCompatibili = () => {
</span> </span>
</div> </div>
) : ( ) : (
data.map((a) => ( <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 sm:gap-y-8 lg:grid-cols-3">
<BasicAnnuncioCard {data.map((annuncio) => (
className="border-2 border-neutral-500" <div
data={a} className="flex flex-col justify-center gap-1"
interactions={ key={annuncio.codice}
<> >
{isAdmin ? ( <CardAnnuncio
<AddButton annuncioId={a.id} /> {...annuncio}
) : ( className="outline outline-neutral-500"
<InteressatoButtonBatchV noLink
annuncioId={a.id} onlyFirstImage
hasInterest={userIntrests?.includes(a.id) || false} />
/>
)} <InteressatoButtonServizio annuncioId={annuncio.id} />
</> </div>
} ))}
key={a.id} </div>
/>
))
)} )}
</> </>
)} )}

View file

@ -471,42 +471,56 @@ export const FormNewServizio = ({
</FormLabel> </FormLabel>
<FormMessage /> <FormMessage />
</div> </div>
<Popover> {isLimited ? (
<PopoverTrigger asChild> <div className="space-y-0">
<FormControl> <p className="font-semibold text-lg">
<Button {field.value
className={cn( ? format(field.value, "dd/MM/yyyy")
"h-[42px] w-full pl-3 text-left font-medium", : "Errore data invalida"}
!field.value && "text-muted-foreground", </p>
`rounded-lg border border-neutral-300 bg-white text-primary shadow-xs outline-hidden file:border-0 file:bg-transparent file:font-medium file:text-sm hover:border-neutral-400 focus:border-neutral-400 disabled:cursor-not-allowed disabled:opacity-50 aria-expanded:border-neutral-400 dark:border-neutral-500 dark:bg-primary dark:text-white dark:aria-expanded:border-neutral-400 dark:focus:border-transparent dark:hover:border-neutral-400 dark:placeholder:text-neutral-400`, <p className="text-muted-foreground text-sm">
)} La scadenza non può essere modificata. Contatta il
id="scadenza_motivazione_transitoria" supporto se necessario.
variant={"outline"} </p>
> </div>
{field.value ? ( ) : (
format(field.value, "PPP", { <Popover>
locale: it, <PopoverTrigger asChild>
}) <FormControl>
) : ( <Button
<span>{t.anagrafica.scegli_data}</span> className={cn(
)} "h-[42px] w-full pl-3 text-left font-medium",
<CalendarIcon className="ml-auto size-4 opacity-50" /> !field.value && "text-muted-foreground",
</Button> `rounded-lg border border-neutral-300 bg-white text-primary shadow-xs outline-hidden file:border-0 file:bg-transparent file:font-medium file:text-sm hover:border-neutral-400 focus:border-neutral-400 disabled:cursor-not-allowed disabled:opacity-50 aria-expanded:border-neutral-400 dark:border-neutral-500 dark:bg-primary dark:text-white dark:aria-expanded:border-neutral-400 dark:focus:border-transparent dark:hover:border-neutral-400 dark:placeholder:text-neutral-400`,
</FormControl> )}
</PopoverTrigger> id="scadenza_motivazione_transitoria"
<PopoverContent align="start" className="w-auto p-0"> variant={"outline"}
<Calendar >
autoFocus {field.value ? (
captionLayout="dropdown" format(field.value, "PPP", {
defaultMonth={field.value || new Date()} locale: it,
endMonth={new Date(2050, 12)} })
locale={it} ) : (
mode="single" <span>{t.anagrafica.scegli_data}</span>
onSelect={field.onChange} )}
selected={field.value || undefined} <CalendarIcon className="ml-auto size-4 opacity-50" />
/> </Button>
</PopoverContent> </FormControl>
</Popover> </PopoverTrigger>
<PopoverContent align="start" className="w-auto p-0">
<Calendar
autoFocus
captionLayout="dropdown"
defaultMonth={field.value || new Date()}
endMonth={new Date(2050, 12)}
locale={it}
mode="single"
onSelect={field.onChange}
selected={field.value || undefined}
/>
</PopoverContent>
</Popover>
)}
</FormItem> </FormItem>
)} )}
/> />

View file

@ -15,9 +15,9 @@ import type { GetServerSideProps } from "next";
import Head from "next/head"; import Head from "next/head";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { AccordionComp } from "~/components/accordionComp"; import { AccordionComp } from "~/components/accordionComp";
import { AnnunciGrid } from "~/components/annunci_grid";
import { MapSection } from "~/components/annunci_map"; import { MapSection } from "~/components/annunci_map";
import { CTA_TipologiaModal } from "~/components/annunci_tutorial"; import { CTA_TipologiaModal } from "~/components/annunci_tutorial";
import { CardAnnuncio } from "~/components/annuncio_card";
import { CodiceBox } from "~/components/codiceRicerca"; import { CodiceBox } from "~/components/codiceRicerca";
import { Layout } from "~/components/Layout"; import { Layout } from "~/components/Layout";
import { LoadingPage } from "~/components/loading"; import { LoadingPage } from "~/components/loading";
@ -367,7 +367,11 @@ const AnnunciList = () => {
{status === "pending" ? ( {status === "pending" ? (
<LoadingPage /> <LoadingPage />
) : ( ) : (
<AnnunciGrid pagedata={data.annunci} /> <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">
{data.annunci.map((annuncio) => (
<CardAnnuncio key={annuncio.codice} {...annuncio} />
))}
</div>
)} )}
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">

View file

@ -152,7 +152,7 @@ const NewServizioModal = ({
Nuovo Servizio Nuovo Servizio
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-h-[80%] overflow-auto p-4 sm:max-w-5xl"> <DialogContent className="max-h-[80%] w-full overflow-auto p-4 sm:max-w-5xl">
<DialogHeader> <DialogHeader>
<DialogTitle>Creazione Servizio</DialogTitle> <DialogTitle>Creazione Servizio</DialogTitle>
<DialogDescription className="sr-only">desc</DialogDescription> <DialogDescription className="sr-only">desc</DialogDescription>

View file

@ -41,6 +41,7 @@ import {
import { db } from "~/server/db"; import { db } from "~/server/db";
import { NewMail, type NewMailProps } from "~/server/services/mailer"; import { NewMail, type NewMailProps } from "~/server/services/mailer";
import { getUser } from "~/server/services/user.service"; import { getUser } from "~/server/services/user.service";
import type { AnnuncioRicerca } from "./annunci.controller";
export const addServizio = async (data: NewServizio) => { export const addServizio = async (data: NewServizio) => {
try { try {
@ -1514,7 +1515,9 @@ export const SendContactEmail = async ({
} }
}; };
export const getCompatibileAnnunci = async (servizioId: ServizioServizioId) => { export const getCompatibileAnnunci = async (
servizioId: ServizioServizioId,
): Promise<AnnuncioRicerca[]> => {
try { try {
const servizio = await getServizioById(servizioId); const servizio = await getServizioById(servizioId);
if (!servizio) { if (!servizio) {
@ -1527,18 +1530,25 @@ export const getCompatibileAnnunci = async (servizioId: ServizioServizioId) => {
let qry = db let qry = db
.selectFrom("annunci") .selectFrom("annunci")
.select((eb) => [ .select((eb) => [
"annunci.id", "id",
"annunci.codice", "codice",
"annunci.prezzo", "comune",
"annunci.desc_en", "provincia",
"annunci.desc_it", "mq",
"annunci.titolo_en", "prezzo",
"annunci.titolo_it", "desc_en",
"annunci.tipo", "desc_it",
"annunci.consegna", "titolo_en",
"annunci.stato", "titolo_it",
"annunci.web", "tipo",
"annunci.updated_at", "consegna",
"stato",
"web",
"updated_at",
"homepage",
"numero_camere",
"url_video",
"modificato_il",
jsonArrayFrom( jsonArrayFrom(
eb eb
.selectFrom("images_refs") .selectFrom("images_refs")