infoalloggi-monorepo/apps/infoalloggi/src/components/prezzi.tsx
Marco Pedone 60e4b187bf Refactor: Update Zod schemas and replace custom types with imported schemas across various files
- Replaced custom Zod types with imported schemas in stripe router and user router.
- Updated payment controller to use new enum imports for order types and payment statuses.
- Refactored payment tests to utilize the new enum structure.
- Removed deprecated zod_types file and created new zod_chat_types file for chat-related schemas.
- Adjusted service files to align with the new order type enums and schemas.
- Enhanced the Caratteristiche type definition using Zod for better validation.

Co-authored-by: Copilot <copilot@github.com>
2026-04-28 20:32:19 +02:00

322 lines
9.6 KiB
TypeScript

import { ExternalLink } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { AnimatedNumber } from "~/components/custom_ui/animated-number";
import { LoadingPage } from "~/components/loading";
import { Button } from "~/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
import { cn } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider";
import type TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
import { tipologiaPosizioneEnum } from "~/schemas/public/TipologiaPosizioneEnum";
import { api } from "~/utils/api";
export const PricingChoice = () => {
const { t } = useTranslation();
const { data: prezziario, isLoading } =
api.prezziario.getPrezziPerTipologiaAll.useQuery();
if (isLoading) return <LoadingPage />;
if (!prezziario) return null;
const pricingBreve = prezziario.transitorio.saldi.map((s) => ({
downPayment: prezziario.transitorio.acconto.prezzo_cent / 100,
secondPayment: s.prezzo_cent / 100,
}));
const pricingStabile = prezziario.stabile.saldi.map((s) => ({
downPayment: prezziario.stabile.acconto.prezzo_cent / 100,
secondPayment: s.prezzo_cent / 100,
}));
return (
<div className="flex flex-col items-center justify-center gap-4">
<h2 className="font-semibold text-3xl">Scegli il tuo servizio</h2>
<Tabs className="mb-2 w-full max-w-xl" defaultValue="transitorio">
<TabsList className="flex h-auto w-full items-center justify-between gap-1 p-1 dark:bg-primary">
<TabsTrigger
className="w-full cursor-pointer border-0 bg-transitorio px-2 text-xl data-[state=active]:border-0 data-[state=active]:bg-transitorio sm:px-3 dark:border-0 dark:text-fxd-foreground dark:data-[state=active]:border-0 dark:data-[state=active]:bg-transitorio dark:data-[state=active]:text-fxd-foreground"
value="transitorio"
>
Transitorio
</TabsTrigger>
<TabsTrigger
className="w-full cursor-pointer border-0 bg-stabile px-2 text-xl data-[state=active]:border-0 data-[state=active]:bg-stabile sm:px-3 dark:border-0 dark:text-fxd-foreground dark:data-[state=active]:border-0 dark:data-[state=active]:bg-stabile dark:data-[state=active]:text-fxd-foreground"
value="stabile"
>
Stabile
</TabsTrigger>
</TabsList>
<TabsContent className="w-full" value="transitorio">
<PricingComponent
className="border-transitorio bg-transitorio text-fxd-foreground"
cta={t.pricing_cmp.cta_breve}
opzioni={[
t.pricing_cmp.breve_option1,
t.pricing_cmp.breve_option2,
t.pricing_cmp.breve_option3,
t.pricing_cmp.breve_option4,
t.pricing_cmp.breve_option5,
]}
pricingData={pricingBreve}
tipo={tipologiaPosizioneEnum.enum.Transitorio}
title={t.prezzi.titolo_transitori}
/>
</TabsContent>
<TabsContent value="stabile">
<PricingComponent
className="border-stabile bg-stabile text-fxd-foreground"
cta={t.pricing_cmp.cta_stabile}
opzioni={[
t.pricing_cmp.stabile_option1,
t.pricing_cmp.stabile_option2,
t.pricing_cmp.stabile_option3,
]}
pricingData={pricingStabile}
tipo={tipologiaPosizioneEnum.enum.Stabile}
title={t.prezzi.titolo_stabile}
/>
</TabsContent>
</Tabs>
<Link aria-label="Link to pricing page" href="/prezzi">
<Button
className="flex h-12 flex-row gap-2 px-6 py-4 text-2xl hover:underline"
data-umami-event="scopri_prezzi_btn"
>
<span>Scopri di più sui prezzi</span>
<ExternalLink />
</Button>
</Link>
</div>
);
};
export type PricingData = { downPayment: number; secondPayment: number }[];
export const PricingComponent = ({
pricingData,
opzioni,
cta,
statico = false,
defaultValue = 0,
className,
title,
tipo,
optionsBtnClassName,
}: {
pricingData: PricingData;
opzioni: string[];
title: string;
cta: string;
statico?: boolean;
defaultValue?: number;
className?: string;
tipo: TipologiaPosizioneEnum;
optionsBtnClassName?: string;
}) => {
const { t } = useTranslation();
const [Value, setValue] = useState<number>(defaultValue);
const p = pricingData[Value];
const downPayment = p?.downPayment || 0;
const secondPayment = p?.secondPayment || 0;
const customFormat = (num: number) => `${num}`;
const [intervalId, setIntervalId] = useState<NodeJS.Timeout | null>(null);
useEffect(() => {
if (statico) return;
const interval = setInterval(() => {
setValue((prevValue) => (prevValue + 1) % pricingData.length);
}, 2500);
setIntervalId(interval);
return () => clearInterval(interval);
}, []);
const ClearCarousel = () => {
if (intervalId) {
clearInterval(intervalId);
}
};
return (
<Card className={cn("w-full max-w-xl gap-4 rounded-md py-4", className)}>
<CardHeader className="py-2">
<CardTitle className="pt-1 text-center font-semibold text-3xl">
{title}
</CardTitle>
<CardDescription className="sr-only">
Prezzo del servizio
</CardDescription>
</CardHeader>
<CardContent className="flex w-full flex-col items-center justify-center gap-4 p-0 px-2 xs:px-6 py-0 text-fxd-foreground sm:gap-6 sm:py-4">
<div
className="flex w-full items-center justify-center gap-0 xs:gap-x-4 xs:gap-y-1"
data-role="wrapper"
>
<div className="flex w-2/5 xs:w-1/3 flex-col items-center gap-2">
<h2 className="truncate font-semibold text-2xl" data-role="title">
{t.acconto}
</h2>
<p
className="font-semibold text-2xl xs:text-3xl sm:text-6xl"
data-role="amount"
>
{downPayment}
</p>
</div>
<div className="flex w-2/5 xs:w-1/3 flex-col items-center gap-2 text-center">
<h2 className="font-semibold text-2xl" data-role="title">
{t.saldo}
</h2>
<p
className="font-semibold text-2xl xs:text-3xl sm:text-6xl"
data-role="amount"
>
<AnimatedNumber
format={customFormat}
precision={secondPayment % 1 === 0 ? 0 : 2}
value={secondPayment}
/>
</p>
</div>
<div className="flex w-2/5 xs:w-1/3 flex-col items-center gap-2 text-center">
<h2 className="font-semibold text-2xl" data-role="title">
Contratto
</h2>
{tipo === tipologiaPosizioneEnum.enum.Transitorio ? (
<div className="flex flex-col justify-center leading-none">
<span
className="font-semibold text-2xl xs:text-3xl sm:text-6xl"
data-role="amount"
>
0
</span>
</div>
) : (
<Link
className="flex h-8 xs:h-10 flex-col justify-center leading-none sm:h-15"
href={"/prezzi#contratti"}
id="contratto-link"
>
<Button>
Più info <ExternalLink className="size-4" />
</Button>
</Link>
)}
</div>
</div>
<div className="flex w-full flex-wrap items-center justify-center gap-2">
{opzioni.map((opzione, idx) => (
<Button
className={cn(
"h-9 xs:h-10 rounded-md border-none px-3 xs:px-4 xs:py-2",
idx === Value ? "fade-in-0 animate-in" : "",
optionsBtnClassName,
)}
key={opzione}
onClick={() => {
ClearCarousel();
setValue(idx);
}}
size="default"
variant={idx === Value ? "default" : "outline"}
>
{opzione}
</Button>
))}
</div>
<h2 className="text-center font-semibold text-white text-xl">{cta}</h2>
</CardContent>
</Card>
);
};
export const PricingComponentConsulenza = ({
pricingData,
opzioni,
cta,
statico = false,
className,
title,
}: {
pricingData: number[];
opzioni: string[];
title: string;
cta: string;
statico?: boolean;
className?: string;
}) => {
const [Value, setValue] = useState<number>(0);
const p = pricingData[Value];
const customFormat = (num: number) => `${num}`;
const [intervalId, setIntervalId] = useState<NodeJS.Timeout | null>(null);
useEffect(() => {
if (statico) return;
const interval = setInterval(() => {
setValue((prevValue) => (prevValue + 1) % pricingData.length);
}, 2500);
setIntervalId(interval);
return () => clearInterval(interval);
}, []);
const ClearCarousel = () => {
if (intervalId) {
clearInterval(intervalId);
}
};
return (
<Card className={cn("w-full max-w-xl gap-2 py-2 sm:gap-4", className)}>
<CardHeader className="p-3 px-5">
<CardTitle className="text-center font-semibold text-3xl sm:pt-1">
{title}
</CardTitle>
<CardDescription className="sr-only">
Prezzo del servizio
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col items-center justify-center gap-8 p-0 px-2 xs:px-6 pb-4 sm:pt-4">
<div className="flex w-full items-center justify-center gap-0 xs:gap-4">
<div className="flex flex-col items-center gap-2">
<p className="font-semibold text-3xl xs:text-6xl">
<AnimatedNumber format={customFormat} value={p || 0} />
</p>
</div>
</div>
<div className="flex w-full flex-wrap items-center justify-center gap-2">
{opzioni.map((opzione, idx) => (
<Button
className={cn(
"h-9 xs:h-10 rounded-md border-none px-3 xs:px-4 xs:py-2",
idx === Value ? "fade-in-0 animate-in" : "",
)}
key={opzione}
onClick={() => {
ClearCarousel();
setValue(idx);
}}
size="default"
variant={idx === Value ? "default" : "outline"}
>
{opzione}
</Button>
))}
</div>
<h2 className="text-center font-semibold text-white text-xl">{cta}</h2>
</CardContent>
</Card>
);
};