feat: implement AnnuncioDisplay component and integrate it into ServizioContent for better display of selected announcements

This commit is contained in:
Marco Pedone 2026-01-09 13:13:17 +01:00
parent 6c4c57fd69
commit 76bcf640ca
4 changed files with 106 additions and 187 deletions

View file

@ -35,6 +35,70 @@ export const AnnuncioCard = ({
/> />
); );
}; };
export const AnnuncioDisplay = ({
data,
confirming,
}: {
data: AnnuncioRicerca;
confirming?: boolean;
}) => {
const { t } = useTranslation();
return (
<div
className={cn(
"grid @md:grid-cols-[12rem_1fr] grid-cols-[7rem_1fr] gap-x-4 gap-y-2 rounded-md md:max-w-3xl md:border md:border-white md:bg-[#e6e9ec]/50 lg:gap-x-4 xl:gap-x-6 dark:md:border-primary dark:md:bg-card",
confirming && "xl:w-1/2",
)}
>
<div className="row-span-1 md:row-span-3">
<Image
alt={data.codice}
className="size-24 rounded-md bg-[#e6e9ec] object-cover md:size-48"
height={1080}
priority
src={
(data?.images[0] &&
getStorageUrl({
storageId: data.images[0].img,
params: {
cacheKey: data.media_updated_at?.toISOString(),
media: "image",
},
})) ||
"/fallback-image.png"
}
width={1920}
/>
</div>
<div className="col-span-1 flex w-full max-w-xl flex-wrap gap-5 md:col-span-1 md:mt-4 md:pr-8">
<span className="text-wrap">{data.titolo_it}</span>
</div>
<div className="col-span-2 flex w-full max-w-xl items-center justify-between gap-2 md:col-span-1 md:pr-8">
<TipoBadge mini tipo={data.tipo} />
<span className="max-w-auto truncate">
{handleConsegna({
aggiornamento: t.card.in_aggiornamento,
consegna: data.consegna,
consegna_da: t.card.consegna_da,
mesi: t.parametri.mesi,
subito: t.card.consegna_subito,
})}
</span>
<span className="font-semibold">
{formatCurrency(data.prezzo / 1e2)}
</span>
</div>
<div className="col-span-2 w-full max-w-xl md:col-span-1 md:pr-8">
<AnnuncioDettaglio data={data} />
</div>
</div>
);
};
export const BasicAnnuncioCard = ({ export const BasicAnnuncioCard = ({
actions, actions,
interactions, interactions,
@ -48,8 +112,6 @@ export const BasicAnnuncioCard = ({
data: AnnuncioRicerca; data: AnnuncioRicerca;
confirming?: boolean; confirming?: boolean;
}) => { }) => {
const { t } = useTranslation();
return ( return (
<Card className={cn("w-full gap-2 border-white py-3", className)}> <Card className={cn("w-full gap-2 border-white py-3", className)}>
<CardHeader className="flex flex-row flex-wrap items-center justify-between space-y-0 px-3 pb-2"> <CardHeader className="flex flex-row flex-wrap items-center justify-between space-y-0 px-3 pb-2">
@ -64,56 +126,7 @@ export const BasicAnnuncioCard = ({
)} )}
id="container" id="container"
> >
<div <AnnuncioDisplay confirming={confirming} data={data} />
className={cn(
"grid @md:grid-cols-[12rem_1fr] grid-cols-[7rem_1fr] gap-x-4 gap-y-2 rounded-md md:max-w-3xl md:border md:border-white md:bg-[#e6e9ec]/50 lg:gap-x-6 xl:gap-x-8 dark:md:border-primary dark:md:bg-card",
confirming && "xl:w-1/2",
)}
>
<div className="row-span-1 md:row-span-3">
<Image
alt={data.codice}
className="size-24 rounded-md bg-[#e6e9ec] object-cover md:size-48"
height={1080}
priority
src={
(data?.images[0] &&
getStorageUrl({
storageId: data.images[0].img,
params: {
cacheKey: data.media_updated_at?.toISOString(),
media: "image",
},
})) ||
"/fallback-image.png"
}
width={1920}
/>
</div>
<div className="col-span-1 flex w-full max-w-xl flex-wrap gap-5 md:col-span-1 md:mt-4 md:pr-8">
<span className="text-wrap">{data.titolo_it}</span>
</div>
<div className="col-span-2 flex w-full max-w-xl items-center justify-between gap-2 md:col-span-1 md:pr-8">
<TipoBadge mini tipo={data.tipo} />
<span className="max-w-auto truncate">
{handleConsegna({
aggiornamento: t.card.in_aggiornamento,
consegna: data.consegna,
consegna_da: t.card.consegna_da,
mesi: t.parametri.mesi,
subito: t.card.consegna_subito,
})}
</span>
<span className="font-semibold">
{formatCurrency(data.prezzo / 1e2)}
</span>
</div>
<div className="col-span-2 w-full max-w-xl md:col-span-1 md:pr-8">
<AnnuncioDettaglio data={data} />
</div>
</div>
<div <div
className={cn( className={cn(
"flex w-full flex-col gap-4", "flex w-full flex-col gap-4",
@ -127,19 +140,3 @@ export const BasicAnnuncioCard = ({
</Card> </Card>
); );
}; };
export const AnnuncioCardSkeleton = () => {
return (
<Card className="hidden w-full gap-2 rounded-md border-border py-3 opacity-30 lg:flex">
<CardHeader className="flex flex-row flex-wrap items-center justify-between space-y-0 px-3 pb-2">
<CardTitle className="h-8 w-64 rounded-md bg-muted" />
</CardHeader>
<CardContent className="h-max px-3">
<div className="flex w-full flex-col gap-4">
<div className="h-[194px] w-full rounded-md bg-muted" />
<div className="h-8 w-48 rounded-md bg-muted" />
</div>
</CardContent>
</Card>
);
};

View file

@ -6,7 +6,7 @@ import { useEffect } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { import {
AnnuncioCard, AnnuncioCard,
AnnuncioCardSkeleton, AnnuncioDisplay,
} from "~/components/servizio/annuncio_card"; } from "~/components/servizio/annuncio_card";
import { SaldoButton } from "~/components/servizio/interactions"; import { SaldoButton } from "~/components/servizio/interactions";
import { ServizioActions } from "~/components/servizio/servizio_actions"; import { ServizioActions } from "~/components/servizio/servizio_actions";
@ -26,7 +26,6 @@ import {
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import { AlarmClockSVG } from "../svgs"; import { AlarmClockSVG } from "../svgs";
import { AnnunciCompatibili } from "./compatibili_dialog"; import { AnnunciCompatibili } from "./compatibili_dialog";
import { AnnunciSelezionatiDialog } from "./servizio_annunci_accordions";
const ServizioCard = ({ const ServizioCard = ({
cardClassName, cardClassName,
@ -163,7 +162,34 @@ export const ServizioContent = () => {
)} )}
</div> </div>
{isAdmin && <AnnunciSelezionatiDialog annunci={servizio.annunci} />} {servizio.annunci.length > 0 && (
<div className="w-full space-y-2">
<p>
Sono già stati selezionati {servizio.annunci.length} annunci
che potrebbero interessarti.
</p>
<div className="grid grid-cols-2 gap-4">
{servizio.annunci.map((data) => (
<div className="" key={data.id}>
<AnnuncioDisplay
data={{
...data,
modificato_il: data.modificato_il
? new Date(data.modificato_il)
: null,
media_updated_at: data.media_updated_at
? new Date(data.media_updated_at)
: null,
}}
/>
</div>
))}
{servizio.annunci.length % 2 === 1 && (
<div className="size-full rounded-md border border-[var(--pattern-fg)] bg-[repeating-linear-gradient(45deg,_var(--pattern-fg)_0,_var(--pattern-fg)_1px,_transparent_0,_transparent_50%)] bg-[size:10px_10px] bg-fixed opacity-10 [--pattern-fg:#696969] dark:[--pattern-fg:#e1e1e1]" />
)}
</div>
</div>
)}
</div> </div>
} }
contentClassName={cn(!isAdmin && "gap-0")} contentClassName={cn(!isAdmin && "gap-0")}
@ -297,7 +323,9 @@ export const ServizioContent = () => {
</ServizioAnnuncioProvider> </ServizioAnnuncioProvider>
); );
})} })}
{servizio.annunci.length % 2 === 1 && <AnnuncioCardSkeleton />} {servizio.annunci.length % 2 === 1 && (
<div className="size-full rounded-md border border-[var(--pattern-fg)] bg-[repeating-linear-gradient(45deg,_var(--pattern-fg)_0,_var(--pattern-fg)_1px,_transparent_0,_transparent_50%)] bg-[size:10px_10px] bg-fixed opacity-10 [--pattern-fg:#696969] dark:[--pattern-fg:#e1e1e1]" />
)}
</div> </div>
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">

View file

@ -1,11 +1,6 @@
import { ExternalLink, Trash2 } from "lucide-react"; import { Trash2 } from "lucide-react";
import Link from "next/link";
import type { ReactNode } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { import { BasicAnnuncioCard } from "~/components/servizio/annuncio_card";
AnnuncioCard,
BasicAnnuncioCard,
} from "~/components/servizio/annuncio_card";
import { import {
Accordion, Accordion,
AccordionContent, AccordionContent,
@ -13,105 +8,8 @@ import {
AccordionTrigger, AccordionTrigger,
} from "~/components/ui/accordion"; } from "~/components/ui/accordion";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import {
ServizioAnnuncioProvider,
useServizio,
} from "~/providers/ServizioProvider";
import type { UsersId } from "~/schemas/public/Users"; import type { UsersId } from "~/schemas/public/Users";
import type { ServizioData } from "~/server/controllers/servizio.controller";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../ui/dialog";
const DialogServizio = ({
trigger,
title,
children,
}: {
trigger: ReactNode;
title: string;
children: ReactNode;
}) => {
return (
<Dialog>
<DialogTrigger asChild>{trigger}</DialogTrigger>
<DialogContent className="max-w-xl p-2 sm:max-w-5xl sm:p-6 md:max-w-7xl">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription className="sr-only">
Servizio Dialog
</DialogDescription>
</DialogHeader>
{children}
</DialogContent>
</Dialog>
);
};
export const AnnunciSelezionatiDialog = ({
annunci,
}: {
annunci: ServizioData["annunci"];
}) => {
const { servizioId, userId } = useServizio();
return (
<DialogServizio
title="Annunci selezionati"
trigger={
<div className="relative flex w-full sm:w-fit">
<Button
className="w-full text-lg sm:w-fit"
size="xl"
variant="orange"
>
Annunci selezionati
</Button>
<span className="-translate-y-1/2 absolute top-0 right-0 flex min-w-6 origin-center translate-x-1/2 items-center justify-center rounded-full bg-destructive px-1 text-white">
{annunci.length || 0}
</span>
</div>
}
>
<div className="flex max-h-[80vh] flex-col gap-2 overflow-y-auto">
{annunci.length > 0 ? (
annunci.map((data) => {
return (
<ServizioAnnuncioProvider
data={data}
key={data.id}
servizioId={servizioId}
userId={userId}
>
<AnnuncioCard className="border-2 border-sky-500" />
</ServizioAnnuncioProvider>
);
})
) : (
<div className="flex items-center justify-center gap-1 rounded-md bg-white p-4 sm:flex">
<span>
Nessun annuncio selezionato, fai una ricerca nella pagina{" "}
<Link
aria-label="Annunci Compatibili"
className="inline-flex items-center gap-1 text-blue-500 underline"
href="/annunci"
>
Annunci
<ExternalLink className="size-4" />
</Link>
</span>
</div>
)}
</div>
</DialogServizio>
);
};
export const AnnunciRichiesti = ({ userId }: { userId: UsersId }) => { export const AnnunciRichiesti = ({ userId }: { userId: UsersId }) => {
const { data, isLoading } = api.intrests.getUserInterestsAnnunci.useQuery({ const { data, isLoading } = api.intrests.getUserInterestsAnnunci.useQuery({

View file

@ -14,17 +14,13 @@
@custom-variant search-cancel (&::-webkit-search-cancel-button); @custom-variant search-cancel (&::-webkit-search-cancel-button);
@utility stripes-* { /*
background-image: linear-gradient(45deg, STRIPED BACKGROUND EXAMPLE:
--value([color]) 12.5%, <div className="border border-[var(--pattern-fg)]
transparent 12.5%, bg-[repeating-linear-gradient(45deg,_var(--pattern-fg)_0,_var(--pattern-fg)_1px,_transparent_0,_transparent_50%)]
transparent 50%, bg-[size:10px_10px] bg-fixed
--value([color]) 50%, [--pattern-fg:#696969] dark:[--pattern-fg:#e1e1e1]" />
--value([color]) 62.5%, */
transparent 62.5%,
transparent 100%);
background-size: 10.66px 10.66px;
}
@theme inline { @theme inline {
--font-sans: var(--font-inter); --font-sans: var(--font-inter);