feat: enhance UI components and improve functionality across various modules

This commit is contained in:
Marco Pedone 2025-12-04 17:16:49 +01:00
parent 7ae27d8e30
commit a268afee74
12 changed files with 225 additions and 111 deletions

View file

@ -53,7 +53,7 @@ export const AccordionComp = forwardRef<HTMLDivElement, AccordionCompProps>(
key={`ac-item-${index}`}
value={`item-${index}`}
>
<AccordionTrigger className="cursor-pointer gap-4 text-left text-lg">
<AccordionTrigger className="cursor-pointer gap-4 text-left text-md">
{text.title}
</AccordionTrigger>
<AccordionContent className="text-base">

View file

@ -174,13 +174,8 @@ export const InteressatoButtonServizio = ({
const { isAdmin, servizioId, userId } = useServizio();
const utils = api.useUtils();
const { mutate: adminAdd, isPending: isAdminPending } =
api.servizio.addServizioAnnunci.useMutation({
onError: (error) => {
console.error(error);
toast.error("Errore durante l'aggiunta dell'annuncio");
},
onSuccess: async () => {
const invalidateAll = async () => {
await utils.intrests.getUserInterests.invalidate();
await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
await utils.servizio.getAnnunciAvailableToAdd.invalidate({
servizioId,
@ -191,6 +186,15 @@ export const InteressatoButtonServizio = ({
await utils.servizio.getCompatibileAnnunci.invalidate({
servizioId,
});
};
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 invalidateAll();
toast.success("Annuncio aggiunto alla ricerca");
},
@ -198,7 +202,7 @@ export const InteressatoButtonServizio = ({
const { mutate: add, isPending } = api.intrests.addIntrest.useMutation({
onSuccess: async () => {
await utils.intrests.getUserInterests.invalidate();
await invalidateAll();
toast.success("Richiesta di interesse inviata con successo!");
},
});

View file

@ -1,3 +1,4 @@
import { CircleHelp } from "lucide-react";
import dynamic from "next/dynamic";
import { AccordionComp } from "~/components/accordionComp";
import { LoadingPage } from "~/components/loading";
@ -12,6 +13,16 @@ import {
import { useTranslation } from "~/providers/I18nProvider";
import type { UsersId } from "~/schemas/public/Users";
import { api } from "~/utils/api";
import {
Credenza,
CredenzaBody,
CredenzaContent,
CredenzaDescription,
CredenzaHeader,
CredenzaTitle,
CredenzaTrigger,
} from "../custom_ui/credenza";
import { Button } from "../ui/button";
const TimeserieBarChart = dynamic(
() => import("~/components/timeserieBarChart").then((mod) => mod.default),
@ -82,37 +93,39 @@ export const AdminDashboard = () => {
};
export const UserDashboard = ({ userId }: { userId: UsersId }) => {
// const { data: servizi, isLoading: loadingServizi } =
// api.servizio.getAllServizioAnnunci.useQuery({ userId });
const { t } = useTranslation();
return (
<div className="mx-1 flex-1 overflow-auto">
<div className="mx-auto pt-4">
<div className="mx-3 items-start justify-between md:flex">
<div className="flex max-w-lg items-center gap-3">
<div className="mt-2 flex w-full flex-1 flex-col items-start justify-center gap-4 overflow-auto p-2 md:gap-6">
<h3 className="font-bold text-2xl">Le tue ricerche</h3>
</div>
</div>
<div className="flex flex-col space-y-5 px-2">
<div className="flex w-full flex-1 grow flex-col space-y-5">
<TabRicerca isAdmin={false} userId={userId} />
{/* {loadingServizi ? (
<LoadingPage />
) : (
servizi?.map((servizio, idx) => (
<ServizioContainer
key={idx}
servizio={servizio}
userId={userId}
isAdmin={false}
/>
))
)} */}
</div>
<div className="mx-auto">
<Credenza>
<CredenzaTrigger asChild>
<Button variant="secondary">
<span>Domande frequenti</span>
<CircleHelp />
</Button>
</CredenzaTrigger>
<CredenzaContent className="max-h-[90vh]">
<CredenzaHeader>
<CredenzaTitle className="text-center font-semibold text-2xl">
Domande frequenti
</CredenzaTitle>
<CredenzaDescription className="sr-only">
Domande frequenti
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody className="max-h-[80vh] overflow-auto pb-5">
<AccordionComp
className="w-full max-w-full px-2 pt-4"
className="w-full max-w-full"
texts={t.area_riservata.dash_accordion_minifaq}
/>
</CredenzaBody>
</CredenzaContent>
</Credenza>
</div>
</div>
);

View file

@ -1,9 +1,11 @@
import { CheckCircle } from "lucide-react";
import { useState } from "react";
import { useServizio } from "~/providers/ServizioProvider";
import { api } from "~/utils/api";
import { CardAnnuncio } from "../annuncio_card";
import { InteressatoButtonServizio } from "../annuncio-interactions/annuncio_interactions";
import { LoadingPage } from "../loading";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import {
Dialog,
@ -65,7 +67,7 @@ export const AnnunciCompatibili = () => {
trovare annunci compatibili.
</span>
) : (
<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-14 sm:grid-cols-2 sm:gap-6 sm:gap-y-10 lg:grid-cols-3 2xl:grid-cols-4">
{data.map((annuncio) => (
<div
className="flex flex-col justify-center gap-1"
@ -82,7 +84,19 @@ export const AnnunciCompatibili = () => {
<AnnuncioDettaglio data={annuncio} />
</div>
<div className="w-full">
<InteressatoButtonServizio annuncioId={annuncio.id} />
{annuncio.alreadyRequested ? (
<Badge
className="flex h-9 w-full items-center justify-center gap-2 text-base [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
variant="success"
>
<span>Richiesta inviata</span>
<CheckCircle />
</Badge>
) : (
<InteressatoButtonServizio
annuncioId={annuncio.id}
/>
)}
</div>
</div>
</div>

View file

@ -1,5 +1,4 @@
import { add } from "date-fns";
import { CalendarClock, Package, PackageCheck, UserCircle } from "lucide-react";
import { ArrowRight, Package, PackageCheck } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect } from "react";
@ -15,7 +14,6 @@ import {
CardHeader,
CardTitle,
} from "~/components/ui/card";
import { Progress } from "~/components/ui/progress";
import { cn } from "~/lib/utils";
import {
ServizioAnnuncioProvider,
@ -44,7 +42,7 @@ const ServizioCard = ({
return (
<Card
className={cn(
"gap-2 rounded-t-none border-primary border-t-0",
"gap-2 rounded-none rounded-b-md border-primary border-t-0",
cardClassName,
)}
>
@ -134,7 +132,10 @@ export const ServizioContent = () => {
aria-label="Attiva Servizio"
href={`/servizio/onboard/${servizio.servizio_id}`}
>
<Button>Procedi all&apos;attivazione</Button>
<Button variant="success">
<span>Attiva il Servizio Ora!</span>
<ArrowRight />
</Button>
</Link>
{isAdmin && (
<Button
@ -158,9 +159,7 @@ export const ServizioContent = () => {
}
header={
<div className="flex w-full flex-col gap-2">
<span className="text-foreground/70 text-lg">
Servizio non attivo
</span>
<span className="text-lg">Servizio non ancora attivato</span>
<ServizioActions />
</div>
}
@ -202,9 +201,6 @@ export const ServizioContent = () => {
const annunciSelezionati = servizio.annunci.filter(
(a) => a.user_confirmed_at === null,
);
const openContattiAnnunci = servizio.annunci.filter(
(a) => a.open_contatti_at !== null,
);
const annunciInConferma = servizio.annunci.filter(
(a) => a.user_confirmed_at !== null,
@ -214,7 +210,7 @@ export const ServizioContent = () => {
<ServizioCard
content={
<>
<div className="flex w-full flex-col gap-8 sm:flex-row sm:items-start">
{/* <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 items-center justify-between">
<div className="flex items-center gap-2">
@ -239,7 +235,7 @@ export const ServizioContent = () => {
{`${new Date(servizio.decorrenza).toLocaleDateString("it-IT")} - ${new Date(add(servizio.decorrenza, { days: 60 })).toLocaleDateString("it-IT")}`}
</span>
</div>
</div>
</div> */}
{annuncioConfermato.length > 0 && (
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
@ -291,8 +287,9 @@ export const ServizioContent = () => {
);
})}
</div>
{annunciSelezionati.length > 0 && (
<AnnunciSelezionatiAccordion annunci={annunciSelezionati} />
)}
<div className="flex items-center justify-center">
<AnnunciCompatibili />
</div>
@ -304,6 +301,7 @@ export const ServizioContent = () => {
<ServizioActions />
</>
}
headerClassName="pb-0"
/>
);
};

View file

@ -1,5 +1,16 @@
import { add } from "date-fns";
import { Bug, Calculator, ClockAlert, Plus, ShieldUser } from "lucide-react";
import {
Bug,
Calculator,
CalendarClock,
ChevronDown,
ChevronUp,
ClockAlert,
Cog,
Info,
Plus,
UserCircle,
} from "lucide-react";
import { useState } from "react";
import toast from "react-hot-toast";
import {
@ -44,40 +55,122 @@ import {
FormEditServizio,
} from "~/forms/FormEditServizioAdmin";
import { FormNewServizio, type FormValues } from "~/forms/FormNewServizio";
import { cn } from "~/lib/utils";
import { useServizio } from "~/providers/ServizioProvider";
import type { AnnunciId } from "~/schemas/public/Annunci";
import type { ServizioServizioId } from "~/schemas/public/Servizio";
import type { UsersId } from "~/schemas/public/Users";
import { api } from "~/utils/api";
import { Progress } from "../ui/progress";
export const ServizioActions = () => {
const { servizio, isAdmin, userId } = useServizio();
return (
<div className="flex w-full flex-col flex-wrap justify-between gap-3">
{isAdmin && (
<div className="flex items-center gap-3">
<span className="font-semibold">Azioni Admin:</span>
<AddAnnuncio servizioId={servizio.servizio_id} userId={userId} />
<EditServizioAdmin />
</div>
)}
<ServizioInfos />
</div>
);
};
const ServizioInfos = () => {
const { servizio } = useServizio();
const [open, setOpen] = useState(false);
const openContattiAnnunci = servizio.annunci.filter(
(a) => a.open_contatti_at !== null,
);
if (!servizio.decorrenza) return null;
const dec_txt = new Date(servizio.decorrenza).toLocaleDateString("it-IT");
const end_txt = new Date(
add(servizio.decorrenza, { days: 60 }),
).toLocaleDateString("it-IT");
return (
<Collapsible className="space-y-3" onOpenChange={setOpen} open={open}>
<CollapsibleTrigger asChild>
{open ? (
<Button className="w-full justify-between" variant="outline">
<span className="flex items-center gap-2">
<Info />
<span>Informazioni servizio</span>
</span>
<span>
<ChevronUp />
</span>
</Button>
) : (
<Button className="w-full justify-between" variant="outline">
<div className="flex items-center gap-6">
<span className="flex items-center gap-2">
<UserCircle className="size-4 text-primary" />
<span>{openContattiAnnunci.length} / 10</span>
</span>
<span className="flex items-center gap-2">
<CalendarClock className="size-4 text-primary" />
<span>{end_txt}</span>
</span>
</div>
<div className="flex items-center gap-2">
<span>Dettagli</span>
<ChevronDown />
</div>
</Button>
)}
</CollapsibleTrigger>
<CollapsibleContent className="flex w-full flex-col gap-8 rounded-md border bg-card p-2 sm:items-start sm:gap-4">
<ServizioBasicActions />
<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:w-1/2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<UserCircle className="text-primary" />
<span className="font-medium">Contatti sbloccati</span>
</div>
<span className="text-sm">{openContattiAnnunci.length} / 10</span>
</div>
<Progress className="h-2" value={openContattiAnnunci.length * 10} />
</div>
<div className="flex flex-col flex-wrap gap-2 sm:w-1/2 sm:flex-row sm:items-center">
<div className="flex items-center gap-2">
<CalendarClock className="text-primary" />
<span className="font-medium">Durata servizio:</span>
</div>
<span>
{dec_txt} - {end_txt}
</span>
</div>
</div>
</CollapsibleContent>
</Collapsible>
);
};
export const ServizioBasicActions = ({ className }: { className?: string }) => {
const { servizio, isAdmin } = useServizio();
const canUserEdit =
servizio.isOkAcconto &&
servizio.decorrenza !== null &&
!servizio.annunci.some((a) => a.user_confirmed_at !== null);
if (!isAdmin && !canUserEdit) {
return null; // If the user is not an admin and cannot edit, do not render the actions
}
if (isAdmin || canUserEdit) {
return (
<div className="flex w-full flex-col flex-wrap justify-between gap-3 sm:flex-row">
{(isAdmin || canUserEdit) && (
<div className="flex flex-wrap items-center gap-3">
{/* <span className="hidden sm:block">Azioni: </span> */}
<div className={cn("flex flex-wrap items-center gap-3", className)}>
<EditPreferenze />
<InterruzioneServizio />
</div>
)}
{isAdmin && (
<div className="flex items-center gap-3">
<AddAnnuncio servizioId={servizio.servizio_id} userId={userId} />
<EditServizioAdmin />
</div>
)}
</div>
);
}
return null;
};
export const AddAnnuncio = ({
servizioId,
@ -300,6 +393,7 @@ const EditPreferenze = () => {
(servizio.isInterrotto ||
servizio.annunci.some((a) => a.hasConfermaAdmin))
}
variant="secondary"
>
<Calculator />
Preferenze
@ -362,8 +456,8 @@ const EditServizioAdmin = () => {
<Credenza onOpenChange={setOpen} open={open}>
<CredenzaTrigger asChild>
<Button aria-label="Modifica Servizio Admin">
<ShieldUser />
Admin
<Cog />
Modifica
</Button>
</CredenzaTrigger>
<CredenzaContent className="max-h-[90vh] w-full sm:max-w-5xl">

View file

@ -1,4 +1,4 @@
import { ChevronDownCircle, ChevronRightCircle, Package } from "lucide-react";
import { Building, ChevronDown, ChevronUp } from "lucide-react";
import { useState } from "react";
import { LoadingPage } from "~/components/loading";
import { ServizioContent } from "~/components/servizio/main";
@ -84,24 +84,13 @@ const Row = ({
isOpen && "rounded-b-none",
)}
>
<CollapsibleTrigger className="grid w-full cursor-pointer grid-cols-8 items-center">
<div className="col-span-2 flex items-center justify-center text-primary sm:col-span-1">
{isOpen ? (
<ChevronDownCircle className="fill-background" />
) : (
<ChevronRightCircle className="fill-background" />
)}
</div>
<div className="col-span-6 flex items-center gap-2 font-semibold text-primary sm:text-lg">
<Package className="size-6" />
<CollapsibleTrigger className="flex w-full cursor-pointer items-center justify-between gap-4 px-3 sm:px-4">
<div className="flex items-center gap-2 font-semibold text-primary sm:text-lg">
<Building className="size-6 fill-background" />
Affitto {servizio.tipologia}
</div>
<div className="hidden text-primary text-sm sm:col-span-2 sm:contents sm:text-base">
{servizio.created_at.toLocaleDateString("it", {
day: "2-digit",
month: "short",
})}
<div className="float-end flex items-center justify-start pl-2 text-primary">
{isOpen ? <ChevronUp /> : <ChevronDown />}
</div>
</CollapsibleTrigger>
</div>

View file

@ -20,6 +20,8 @@ const badgeVariants = cva(
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
success:
"bg-success border-transparent text-white [a&]hover:bg-success/90 dark:text-white",
},
},
},

View file

@ -70,7 +70,7 @@ const Annunci = ({ options }: AnnunciPageProps) => {
<meta content={t.heads.annunci_description} name="description" />
</Head>
<main className="relative mx-auto w-full max-w-10xl space-y-5 bg-background2 px-2 py-5 md:px-8">
<main className="relative mx-auto w-full max-w-10xl space-y-5 bg-background px-2 py-5 md:px-8">
<Badge className="bg-muted py-1 text-foreground text-sm outline outline-muted-foreground">
{t.annunci.titolo}
</Badge>
@ -89,14 +89,7 @@ const Annunci = ({ options }: AnnunciPageProps) => {
export default Annunci;
Annunci.getLayout = (page: React.ReactNode) => {
return (
<Layout
containerClassName="bg-background2"
footerClassName="bg-background2"
>
{page}
</Layout>
);
return <Layout>{page}</Layout>;
};
export const getServerSideProps = (async () => {

View file

@ -52,7 +52,7 @@ const SessionWrapper = ({ userId }: { userId: UsersId }) => {
if (!data) return <Status500 />;
return (
<main className="mx-1 mt-2 flex flex-1 flex-col items-start gap-4 p-2 md:gap-8">
<main className="mx-1 mt-2 flex flex-1 flex-col items-start gap-4 p-2 md:gap-6">
<div className="grid w-full auto-rows-max items-start gap-2 md:gap-4">
<div className="overflow-auto border-none">
<h3 className="font-bold text-2xl">Ordini</h3>

View file

@ -42,6 +42,7 @@ import { db } from "~/server/db";
import { NewMail, type NewMailProps } from "~/server/services/mailer";
import { getUser } from "~/server/services/user.service";
import { withImages, withVideos } from "~/utils/kysely-helper";
import { GetUserInterests } from "../services/interests.service";
import type { AnnuncioRicerca } from "./annunci.controller";
export const addServizio = async (data: NewServizio) => {
@ -1622,7 +1623,7 @@ export const getCompatibileAnnunci = async ({
}: {
servizioId: ServizioServizioId;
adminOverride?: boolean;
}): Promise<AnnuncioRicerca[]> => {
}): Promise<(AnnuncioRicerca & { alreadyRequested: boolean })[]> => {
try {
const servizio = await getServizioById(servizioId);
if (!servizio) {
@ -1632,10 +1633,12 @@ export const getCompatibileAnnunci = async ({
});
}
const interests = await GetUserInterests(servizio.user_id);
let qry = db
.selectFrom("annunci")
.distinctOn("annunci.id")
.select((_eb) => [
.select((eb) => [
"id",
"codice",
"comune",
@ -1657,6 +1660,13 @@ export const getCompatibileAnnunci = async ({
"modificato_il",
withImages(),
withVideos(),
eb
.case()
.when("id", "in", interests)
.then(true)
.else(false)
.end()
.as("alreadyRequested"),
])
.where("web", "=", true)
.where("stato", "!=", "Sospeso")

View file

@ -44,7 +44,6 @@
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-background2: var(--background2);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
@ -281,8 +280,6 @@
--radius: 0.625rem;
--backgroundbkp: oklch(1 0 0);
--background: oklch(70.716% 0.07403 219.01);
--background2bkp: oklch(91.587% 0.08721 83.88);
--background2: oklch(70.716% 0.07403 219.01);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);