This commit is contained in:
Marco Pedone 2025-12-15 12:23:41 +01:00
parent 94b603b83c
commit 7de7d90b95
16 changed files with 15 additions and 660 deletions

View file

@ -192,7 +192,7 @@ export const NewColumn = () => {
);
};
export const NewCard = ({ columnId }: { columnId: PotenzialiGroupsId }) => {
const NewCard = ({ columnId }: { columnId: PotenzialiGroupsId }) => {
const [newItemName, setNewItemName] = useState("");
const [newItemDescrizione, setNewItemDescrizione] = useState("");
const [newItemUserid, setNewItemUserid] = useState<UsersId | null>(null);

View file

@ -5,7 +5,7 @@ import { cn } from "~/lib/utils";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
export interface NumberInputProps
interface NumberInputProps
extends Omit<NumericFormatProps, "value" | "onValueChange"> {
stepper?: number;
thousandSeparator?: string;

View file

@ -1,128 +0,0 @@
import { type AnimationSequence, useAnimate } from "framer-motion";
import {
type ComponentPropsWithoutRef,
forwardRef,
useImperativeHandle,
} from "react";
import { IconMatrix, type IconType } from "~/components/IconComponents";
import { Button } from "~/components/ui/button";
import { cn } from "~/lib/utils";
const randomNumberBetween = (min: number, max: number) => {
return Math.floor(Math.random() * (max - min + 1) + min);
};
export interface AnimatedButtonRef {
sparkle: () => void;
}
interface AnimatedButtonProps extends ComponentPropsWithoutRef<typeof Button> {
sparklesRadius?: number;
sparklesNumber?: number;
sparklesClassName?: string;
sparklesIcon?: IconType;
sparklesScale?: [number, number];
}
export const AnimatedButton = forwardRef<
AnimatedButtonRef,
AnimatedButtonProps
>(
(
{
sparklesRadius = 100,
sparklesNumber = 20,
sparklesClassName = "fill-blue-500 stroke-transparent",
sparklesIcon = "star",
sparklesScale = [1.5, 2.5],
...props
},
ref,
) => {
const [scope, animate] = useAnimate();
const Sparkles = () => {
const sparkles = Array.from({ length: sparklesNumber });
const sparklesAnimation: AnimationSequence = sparkles.map((_, index) => [
`.sparkle-${index}`,
{
opacity: 1,
scale: randomNumberBetween(sparklesScale[0], sparklesScale[1]),
x: randomNumberBetween(-sparklesRadius, sparklesRadius),
y: randomNumberBetween(-sparklesRadius, sparklesRadius),
},
{
at: "<",
delay: index * 0.01,
duration: 0.4,
},
]);
const sparklesFadeOut: AnimationSequence = sparkles.map((_, index) => [
`.sparkle-${index}`,
{
opacity: 0,
scale: 0,
},
{
at: "<",
duration: 0.3,
},
]);
const sparklesReset: AnimationSequence = sparkles.map((_, index) => [
`.sparkle-${index}`,
{
x: 0,
y: 0,
},
{
duration: 0.000001,
},
]);
animate([
...sparklesReset,
["button", { scale: 0.8 }, { at: "<", duration: 0.1 }],
["button", { scale: 1 }, { duration: 0.1 }],
...sparklesAnimation,
["button", { duration: 0.000001 }],
...sparklesFadeOut,
]);
};
useImperativeHandle(ref, () => ({
sparkle: Sparkles,
}));
return (
<div className="w-full" ref={scope}>
<Button
{...props}
aria-label="Animated Button"
className={cn("relative", props.className)}
>
{props.children}
<span
aria-hidden
className="pointer-events-none absolute inset-0 z-10 block"
>
{Array.from({ length: sparklesNumber }).map((_, index) => (
<IconMatrix
className={cn(
`absolute top-1/2 left-1/2 opacity-0 sparkle-${index}`,
sparklesClassName,
)}
// biome-ignore lint/suspicious/noArrayIndexKey: <index is safe here>
key={index}
type={sparklesIcon}
/>
))}
</span>
</Button>
</div>
);
},
);
AnimatedButton.displayName = "AnimatedButton";

View file

@ -311,75 +311,3 @@ export const PricingComponentConsulenza = ({
</Card>
);
};
export const PrezziShow = () => {
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="mx-auto flex w-full flex-col items-center justify-center gap-8 py-6 text-primary sm:max-w-5xl sm:flex-row">
<div className="flex w-full flex-col gap-2 text-start sm:w-2/5 sm:text-left">
<h1 className="font-bold text-3xl text-primary">
Quanto costa il servizio?
</h1>
<p className="text-lg text-muted-foreground">
Il nostro servizio è pensato per essere accessibile e conveniente, con
tariffe chiare e senza sorprese. Basate sul tipo di affitto che
desideri, le nostre tariffe sono suddivise in due categorie
principali: Affitto Transitorio e Affitto Stabile.
</p>
</div>
<Tabs className="w-full sm:w-3/5" defaultValue="transitorio">
<TabsList className="flex h-auto w-full items-center justify-between">
<TabsTrigger className="w-full text-xl" value="transitorio">
Affitto Transitorio
</TabsTrigger>
<TabsTrigger className="w-full text-xl" value="stabile">
Affitto Stabile
</TabsTrigger>
</TabsList>
<TabsContent value="transitorio">
<PricingComponent
className="border-transitorio bg-transitorio"
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.Transitorio}
title={t.prezzi.titolo_transitori}
/>
</TabsContent>
<TabsContent value="stabile">
<PricingComponent
className="border-stabile bg-stabile"
cta={t.pricing_cmp.cta_stabile}
opzioni={[
t.pricing_cmp.stabile_option1,
t.pricing_cmp.stabile_option2,
]}
pricingData={pricingStabile}
tipo={TipologiaPosizioneEnum.Stabile}
title={t.prezzi.titolo_stabile}
/>
</TabsContent>
</Tabs>
</div>
);
};

View file

@ -229,7 +229,7 @@ export function SchedaAnnuncioStampabile({ data }: { data: Annunci }) {
);
}
export function SchedaAnnuncio({ data }: { data: Annunci }) {
function SchedaAnnuncio({ data }: { data: Annunci }) {
const companyAddress =
"Via Beata Giovanna, 1 a Bassano del Grappa (VI)\nTel. +39 0424529869\nEmail: arca@infoalloggi.it";

View file

@ -1,128 +0,0 @@
import { add, differenceInDays } from "date-fns";
import { CalendarClock, CalendarDays, Clock, Hourglass } from "lucide-react";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "~/components/ui/accordion";
import { Card, CardContent } from "~/components/ui/card";
import { Progress } from "~/components/ui/progress"; // Import the Progress component
import { useMediaQuery } from "~/hooks/use-media-query";
export const ServizioDuration = ({
decorrenza,
}: {
decorrenza: Date | null;
}) => {
const isDesktop = useMediaQuery("(min-width: 768px)");
if (!decorrenza) {
return null; // If decorrenza is null, do not render the component
}
return (
<Accordion
collapsible
defaultValue={isDesktop ? "validity" : undefined}
type="single"
>
<AccordionItem value="validity">
<AccordionTrigger>
<div className="flex items-center gap-2 font-medium text-base">
<Clock className="size-6 text-primary" />
Validità servizio
</div>
</AccordionTrigger>
<AccordionContent>
<ServiceDurationDisplay
decorrenza={decorrenza}
recessoDays={14}
scadenza={add(decorrenza, { days: 60 })}
/>
</AccordionContent>
</AccordionItem>
</Accordion>
);
};
interface ServiceDurationDisplayProps {
decorrenza: Date;
scadenza: Date;
recessoDays: number;
}
/**
* A sleek, light-themed component for displaying service duration,
* including start date, end date, and trial period, with a progress bar.
*/
function ServiceDurationDisplay({
decorrenza,
scadenza,
recessoDays,
}: ServiceDurationDisplayProps) {
// Calculate remaining days
const diffDays = differenceInDays(scadenza, new Date()) + 1;
const remainingDaysText = diffDays >= 0 ? `${diffDays} giorni` : "Concluso";
const progressPercentage = ((60 - diffDays) / 60) * 100; // Assuming 60 days is the full duration for 100% progress
return (
<Card className="w-full rounded-xl p-2 shadow-lg sm:py-3">
<CardContent className="flex flex-col gap-5 px-1">
<div className="flex w-full flex-col justify-between gap-4 sm:flex-row sm:items-center">
<div className="flex items-center gap-4">
<CalendarDays aria-hidden="true" className="h-6 w-6 text-primary" />
<div>
<div className="text-muted-foreground text-sm">Decorrenza</div>
<div className="font-semibold text-foreground text-lg">
{decorrenza.toLocaleDateString("it-IT", {
day: "2-digit",
month: "long",
})}
</div>
</div>
</div>
<div className="flex items-center gap-4">
<CalendarClock
aria-hidden="true"
className="h-6 w-6 text-primary"
/>
<div>
<div className="text-muted-foreground text-sm">Scadenza</div>
<div className="font-semibold text-foreground text-lg">
{scadenza.toLocaleDateString("it-IT", {
day: "2-digit",
month: "long",
})}
</div>
</div>
</div>
<div className="flex items-center gap-4">
<Hourglass aria-hidden="true" className="h-6 w-6 text-primary" />
<div>
<div className="text-muted-foreground text-sm">
Periodo di recesso
</div>
<div className="font-semibold text-foreground text-lg">
{recessoDays} giorni
</div>
</div>
</div>
</div>
{/* Progress Bar */}
<div className="pt-2">
<Progress
aria-label="Service duration progress"
className="h-2"
value={progressPercentage}
/>
<div className="mt-1 text-right text-muted-foreground text-sm">
{remainingDaysText}
</div>
</div>
</CardContent>
</Card>
);
}

View file

@ -154,7 +154,7 @@ const ServizioInfos = () => {
);
};
export const ServizioBasicActions = ({ className }: { className?: string }) => {
const ServizioBasicActions = ({ className }: { className?: string }) => {
const { servizio, isAdmin } = useServizio();
const canUserEdit =

View file

@ -1,65 +0,0 @@
import { cn } from "~/lib/utils";
import { run } from "~/utils/utils";
export function SubscriptionStatus({
status,
errorMsg,
reset,
}: {
status: "idle" | "connecting" | "pending" | "error";
errorMsg: string | undefined;
reset: () => void;
}) {
return (
<div
className={cn(
"rounded-full p-2 text-sm transition-colors",
run(() => {
switch (status) {
case "idle":
case "connecting":
return "bg-white text-gray-500 dark:bg-gray-900 dark:text-gray-400";
case "error":
return "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200";
case "pending":
return "bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-200";
}
}),
)}
>
{run(() => {
switch (status) {
case "idle":
case "connecting":
// treat idle and connecting the same
return (
<div>
Connecting...
{errorMsg && " (There are connection problems)"} {errorMsg}
</div>
);
case "error":
// something went wrong
return (
<div>
Error - <em>{errorMsg}</em>
<button
className="hover:underline"
onClick={() => {
reset();
}}
type="button"
>
Try Again
</button>
</div>
);
case "pending":
// we are polling for new messages
return <div>Connected - awaiting messages</div>;
}
})}
</div>
);
}

View file

@ -1,231 +0,0 @@
"use client";
import { Slot } from "@radix-ui/react-slot";
import * as React from "react";
import { cn } from "~/lib/utils";
type TimelineContextProps = {
orientation: "horizontal" | "vertical";
};
const TimelineContext = React.createContext<TimelineContextProps | null>(null);
function useTimeline() {
const context = React.useContext(TimelineContext);
if (!context) {
throw new Error("useTimeline must be used within a <Timeline />.");
}
return context;
}
interface TimelineProps extends React.ComponentPropsWithoutRef<"ol"> {
orientation?: "horizontal" | "vertical";
}
function Timeline({
className,
orientation = "vertical",
...props
}: TimelineProps) {
return (
<TimelineContext value={{ orientation }}>
<ol
className={cn(
"flex",
orientation === "vertical" && "flex-col",
className,
)}
//role="list"
data-orientation={orientation}
data-slot="timeline"
{...props}
/>
</TimelineContext>
);
}
interface TimelineItemProps extends React.ComponentPropsWithoutRef<"li"> {
asChild?: boolean;
}
function TimelineItem({ className, asChild, ...props }: TimelineItemProps) {
const { orientation } = useTimeline();
const Comp = asChild ? Slot : "li";
return (
<Comp
className={cn(
"flex gap-4",
orientation === "horizontal" && "flex-col",
className,
)}
data-orientation={orientation}
data-slot="timeline-item"
{...props}
/>
);
}
interface TimelineSeparatorProps extends React.ComponentPropsWithoutRef<"div"> {
asChild?: boolean;
}
function TimelineSeparator({
className,
asChild,
...props
}: TimelineSeparatorProps) {
const { orientation } = useTimeline();
const Comp = asChild ? Slot : "div";
return (
<Comp
className={cn(
"flex items-center",
orientation === "vertical" && "flex-col",
className,
)}
data-orientation={orientation}
data-slot="timeline-separator"
{...props}
/>
);
}
interface TimelineDotProps extends React.ComponentPropsWithoutRef<"div"> {
variant?: "default" | "outline";
asChild?: boolean;
}
function TimelineDot({
variant = "default",
className,
asChild,
...props
}: TimelineDotProps) {
const { orientation } = useTimeline();
const Comp = asChild ? Slot : "div";
return (
<Comp
className={cn(
"flex size-4 items-center justify-center empty:after:block empty:after:rounded-full empty:after:outline-current [&_svg:not([class*='size-'])]:size-4",
orientation === "vertical" && "mt-1",
variant === "default" && "empty:after:size-2.5 empty:after:bg-current",
variant === "outline" && "empty:after:size-2 empty:after:outline",
className,
)}
data-orientation={orientation}
data-slot="timeline-dot"
{...props}
/>
);
}
interface TimelineConnectorProps extends React.ComponentPropsWithoutRef<"div"> {
asChild?: boolean;
}
function TimelineConnector({
className,
asChild,
...props
}: TimelineConnectorProps) {
const { orientation } = useTimeline();
const Comp = asChild ? Slot : "div";
return (
<Comp
className={cn(
"flex-1 bg-border",
orientation === "vertical" && "my-2 w-0.5",
orientation === "horizontal" && "mx-2 h-0.5",
className,
)}
data-orientation={orientation}
data-slot="timeline-connector"
{...props}
/>
);
}
interface TimelineContentProps extends React.ComponentPropsWithoutRef<"div"> {
asChild?: boolean;
}
function TimelineContent({
className,
asChild,
...props
}: TimelineContentProps) {
const { orientation } = useTimeline();
const Comp = asChild ? Slot : "div";
return (
<Comp
className={cn(
"flex-1",
orientation === "vertical" && "pb-7 first:text-right last:text-left",
orientation === "horizontal" && "pr-7",
className,
)}
data-orientation={orientation}
data-slot="timeline-content"
{...props}
/>
);
}
interface TimelineTitleProps extends React.ComponentPropsWithoutRef<"div"> {
asChild?: boolean;
}
function TimelineTitle({ className, asChild, ...props }: TimelineTitleProps) {
const { orientation } = useTimeline();
const Comp = asChild ? Slot : "div";
return (
<Comp
className={className}
data-orientation={orientation}
data-slot="timeline-title"
{...props}
/>
);
}
interface TimelineDescriptionProps
extends React.ComponentPropsWithoutRef<"div"> {
asChild?: boolean;
}
function TimelineDescription({
className,
asChild,
...props
}: TimelineDescriptionProps) {
const { orientation } = useTimeline();
const Comp = asChild ? Slot : "div";
return (
<Comp
className={cn("text-[0.8em] text-muted-foreground", className)}
data-orientation={orientation}
data-slot="timeline-description"
{...props}
/>
);
}
export {
Timeline,
TimelineItem,
TimelineSeparator,
TimelineDot,
TimelineConnector,
TimelineContent,
TimelineTitle,
TimelineDescription,
useTimeline,
};

View file

@ -28,7 +28,7 @@ import type { UsersId } from "~/schemas/public/Users";
import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage";
import { api } from "~/utils/api";
export type UploadCallback = (args: {
type UploadCallback = (args: {
storageIds: string[];
userStorageIds: UsersStorageUserStorageId[];
}) => void;

View file

@ -38,18 +38,3 @@ export const camereTesti = ({
}
return "N/A";
};
export const getTitoloTranslation = ({
locale,
titolo_it,
titolo_en,
}: {
locale: string;
titolo_it: string | null;
titolo_en: string | null;
}) => {
if (locale === "it") {
return titolo_it != null ? titolo_it : "";
}
return titolo_en != null ? titolo_en : "";
};

View file

@ -3,25 +3,25 @@ import type { Nazioni } from "~/schemas/public/Nazioni";
import type { Provincie } from "~/schemas/public/Provincie";
export const ITALY_CATASTO_CODE = "0000";
export const isNazioneCatastoValid = (str: string) => {
const isNazioneCatastoValid = (str: string) => {
if (str.length !== 4) return false;
if (str === ITALY_CATASTO_CODE) return true;
return /^Z[0-9]{3}$/.test(str);
};
export const isComuneCatastoValid = (str: string) => {
const isComuneCatastoValid = (str: string) => {
if (str.length !== 4) return false;
return /^[A-Z][0-9]{3}$/.test(str);
};
export const getComuneFromCatasto = (
const getComuneFromCatasto = (
codice_catasto: string,
comuni_options: Comuni[],
): Comuni | undefined => {
return comuni_options.find((c) => c.catasto === codice_catasto);
};
export const getNazioneFromCatasto = (
const getNazioneFromCatasto = (
codice_catasto: string,
nazioni_options: Nazioni[],
): Nazioni | undefined => {

View file

@ -7,7 +7,7 @@ import { api } from "~/utils/api";
type UserViewContextType = UserWAnagrafica & { chatid: ChatsChatid | null };
export const UserViewContext = createContext<UserViewContextType | null>(null);
const UserViewContext = createContext<UserViewContextType | null>(null);
export const UserViewProvider: FC<{ children: ReactNode; userId: UsersId }> = ({
children,

View file

@ -19,26 +19,26 @@ import {
updatePotenziali,
} from "~/server/services/potenziali.service";
export const zPotenzialiId = z.custom<PotenzialiId>(
const zPotenzialiId = z.custom<PotenzialiId>(
(val) => typeof val === "string" && val.length > 0,
{ message: "Invalid PotenzialiId" },
);
export const zNewPotenziali = z.custom<NewPotenziali>(
const zNewPotenziali = z.custom<NewPotenziali>(
(val) => {
return typeof val === "object" && val !== null;
},
{ message: "Invalid NewPotenziali" },
);
export const zPotenzialiUpdate = z.custom<PotenzialiUpdate>(
const zPotenzialiUpdate = z.custom<PotenzialiUpdate>(
(val) => {
return typeof val === "object" && val !== null;
},
{ message: "Invalid PotenzialiUpdate" },
);
export const zPotenzialiGroupId = z.custom<PotenzialiGroupsId>(
const zPotenzialiGroupId = z.custom<PotenzialiGroupsId>(
(val) => typeof val === "string" && val.length > 0,
{ message: "Invalid PotenzialiGroupsId" },
);

View file

@ -1,9 +1,8 @@
import { TRPCError } from "@trpc/server";
import type { Comuni, ComuniId } from "~/schemas/public/Comuni";
import type { ComuniId } from "~/schemas/public/Comuni";
import type { NazioniId } from "~/schemas/public/Nazioni";
import { db } from "../db";
export type Comune = Pick<Comuni, "id" | "nome" | "sigla">;
export const getComuni = async () => {
try {
return await db.selectFrom("comuni").selectAll().execute();

View file

@ -30,8 +30,3 @@ export async function comparePasswords({
export function generateSalt() {
return crypto.randomBytes(16).toString("hex").normalize();
}
export function generateTemporaryPassword() {
// Generate readable temporary password (e.g., "Abc12345")
return crypto.randomBytes(4).toString("hex");
}