This commit is contained in:
Marco Pedone 2025-08-29 16:18:32 +02:00
parent f46702275b
commit 5755b00d7b
319 changed files with 9017 additions and 8869 deletions

View file

@ -1,16 +1,20 @@
{ {
"typescript.tsdk": "node_modules\\typescript\\lib", "editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit"
},
"editor.formatOnSave": true,
"npm.exclude": "**/.next/**",
"sqltools.connections": [ "sqltools.connections": [
{ {
"previewLimit": 50, "database": "postgres",
"server": "localhost",
"port": 5432,
"driver": "PostgreSQL", "driver": "PostgreSQL",
"name": "DB", "name": "DB",
"database": "postgres", "password": "rootpost",
"username": "postgres", "port": 5432,
"password": "rootpost" "previewLimit": 50,
"server": "localhost",
"username": "postgres"
} }
], ],
"editor.formatOnSave": true, "typescript.tsdk": "node_modules\\typescript\\lib"
} }

View file

@ -16,15 +16,15 @@
* ``` * ```
*/ */
export type Prettify<T> = { export type Prettify<T> = {
[K in keyof T]: T[K]; [K in keyof T]: T[K];
} & {}; } & {};
// //
export type PrettifyR<T> = T extends object export type PrettifyR<T> = T extends object
? T extends infer O ? T extends infer O
? { [K in keyof O]: PrettifyR<O[K]> } ? { [K in keyof O]: PrettifyR<O[K]> }
: never : never
: T; : T;
/** /**
* @example * @example

View file

@ -1,10 +1,14 @@
{ {
"$schema": "https://biomejs.dev/schemas/2.2.2/schema.json", "$schema": "https://biomejs.dev/schemas/2.2.2/schema.json",
"vcs": { "assist": {
"enabled": true, "actions": {
"root": "../../", "source": {
"clientKind": "git", "organizeImports": "on",
"useIgnoreFile": false "useSortedAttributes": "on",
"useSortedKeys": "on"
}
},
"enabled": true
}, },
"files": { "files": {
@ -21,6 +25,7 @@
"headers", "headers",
"schemas" "schemas"
], ],
"ignoreUnknown": true,
"includes": [ "includes": [
"**", "**",
"!node_modules", "!node_modules",
@ -49,60 +54,56 @@
"!src/i18n/nazioni.ts", "!src/i18n/nazioni.ts",
"!src/i18n/provincie.ts", "!src/i18n/provincie.ts",
"!src/styles/globals.css" "!src/styles/globals.css"
], ]
"ignoreUnknown": true
}, },
"formatter": { "formatter": {
"enabled": true, "enabled": true,
"indentStyle": "tab" "indentStyle": "tab"
}, },
"linter": {
"enabled": true,
"domains": {
"next": "recommended",
"react": "recommended"
},
"rules": {
"recommended": true,
"a11y": {
"noNoninteractiveElementInteractions": "error",
"useSemanticElements": "off",
"noAutofocus": "off"
},
"suspicious": {
"useIterableCallbackReturn": "off",
"noArrayIndexKey": "off",
"noUnknownAtRules": "off"
},
"complexity": {
"noUselessFragments": "off"
},
"correctness": {
"useHookAtTopLevel": "off",
"useExhaustiveDependencies": "off",
"useParseIntRadix": "off"
},
"performance": {
"noImgElement": "off"
},
"security": {
"noDangerouslySetInnerHtml": "off"
}
}
},
"javascript": { "javascript": {
"formatter": { "formatter": {
"quoteStyle": "double" "quoteStyle": "double"
} }
}, },
"assist": { "linter": {
"domains": {
"next": "recommended",
"react": "recommended"
},
"enabled": true, "enabled": true,
"actions": { "rules": {
"source": { "a11y": {
"organizeImports": "on" "noAutofocus": "off",
"noNoninteractiveElementInteractions": "error",
"useSemanticElements": "off"
},
"complexity": {
"noUselessFragments": "off"
},
"correctness": {
"useExhaustiveDependencies": "off",
"useHookAtTopLevel": "off",
"useParseIntRadix": "off"
},
"performance": {
"noImgElement": "off"
},
"recommended": true,
"security": {
"noDangerouslySetInnerHtml": "off"
},
"suspicious": {
"noArrayIndexKey": "off",
"noUnknownAtRules": "off",
"useIterableCallbackReturn": "off"
} }
} }
},
"vcs": {
"clientKind": "git",
"enabled": true,
"root": "../../",
"useIgnoreFile": false
} }
} }

View file

@ -1,21 +1,21 @@
{ {
"$schema": "https://ui.shadcn.com/schema.json", "$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york", "aliases": {
"rsc": false, "components": "~/components",
"tsx": true, "hooks": "~/hooks",
"tailwind": { "lib": "~/lib",
"config": "", "ui": "~/components/ui",
"css": "src/styles/global.css", "utils": "~/lib/utils"
"baseColor": "neutral", },
"cssVariables": true, "iconLibrary": "lucide",
"prefix": "" "rsc": false,
}, "style": "new-york",
"aliases": { "tailwind": {
"components": "~/components", "baseColor": "neutral",
"utils": "~/lib/utils", "config": "",
"ui": "~/components/ui", "css": "src/styles/global.css",
"lib": "~/lib", "cssVariables": true,
"hooks": "~/hooks" "prefix": ""
}, },
"iconLibrary": "lucide" "tsx": true
} }

View file

@ -1,40 +1,40 @@
import { Button, Heading, Row, Section, Text } from "@react-email/components"; import { Button, Heading, Row, Section, Text } from "@react-email/components";
import Base from "./base"; import Base from "./base";
export type VerificaEmail_NewMail = { export type VerificaEmail_NewMail = {
mailType: "verificaEmail"; mailType: "verificaEmail";
props: VerificaEmailProps; props: VerificaEmailProps;
}; };
type VerificaEmailProps = { type VerificaEmailProps = {
token: string; token: string;
userId: string; userId: string;
redirect?: string; redirect?: string;
}; };
const VerificaEmail = ({ token, userId, redirect }: VerificaEmailProps) => { const VerificaEmail = ({ token, userId, redirect }: VerificaEmailProps) => {
return ( return (
<Base preview="Verifica la tua email" noreply> <Base noreply preview="Verifica la tua email">
<> <>
<Heading className="text-center text-3xl leading-8"> <Heading className="text-center text-3xl leading-8">
Verifica la tua email Verifica la tua email
</Heading> </Heading>
<Section className="px-4 text-center text-base md:px-12"> <Section className="px-4 text-center text-base md:px-12">
<Row> <Row>
<Text> <Text>
Clicca sul pulsante qui sotto per verificare la tua email. Clicca sul pulsante qui sotto per verificare la tua email.
</Text> </Text>
</Row> </Row>
<Button <Button
className="mx-auto box-border inline-flex h-10 items-center justify-center rounded-md bg-neutral-100 px-4 py-2 text-center align-middle font-semibold whitespace-nowrap text-neutral-900" className="mx-auto box-border inline-flex h-10 items-center justify-center rounded-md bg-neutral-100 px-4 py-2 text-center align-middle font-semibold whitespace-nowrap text-neutral-900"
href={`${process.env.NEXT_PUBLIC_BASE_URL}/auth/verifica-email-action?t=${token}&u=${userId}${redirect ? `&r=${redirect}` : ""}`} href={`${process.env.NEXT_PUBLIC_BASE_URL}/auth/verifica-email-action?t=${token}&u=${userId}${redirect ? `&r=${redirect}` : ""}`}
> >
Clicca qui per verificare Clicca qui per verificare
</Button> </Button>
</Section> </Section>
</> </>
</Base> </Base>
); );
}; };
export default VerificaEmail; export default VerificaEmail;

View file

@ -42,13 +42,13 @@ const Base = ({
<Body className="py-2 text-center font-sans text-base text-neutral-700"> <Body className="py-2 text-center font-sans text-base text-neutral-700">
<Container className="max-w-xl rounded-lg border border-solid border-[#eaeaea] pt-2 pb-10"> <Container className="max-w-xl rounded-lg border border-solid border-[#eaeaea] pt-2 pb-10">
<Img <Img
referrerPolicy="no-referrer"
width={230.5}
height={44.25}
// src="https://lh3.google.com/u/0/d/1nEurjVWPiz0F8QRLhKcT4ZSujeFRChBs"
src="cid:logo"
alt="Infoalloggi.it" alt="Infoalloggi.it"
className="mx-auto my-2 scale-80" className="mx-auto my-2 scale-80"
height={44.25}
// src="https://lh3.google.com/u/0/d/1nEurjVWPiz0F8QRLhKcT4ZSujeFRChBs"
referrerPolicy="no-referrer"
src="cid:logo"
width={230.5}
/> />
<Section className="max-w-lg px-8"> <Section className="max-w-lg px-8">

View file

@ -1,48 +1,48 @@
import { Button, Heading, Row, Section, Text } from "@react-email/components"; import { Button, Heading, Row, Section, Text } from "@react-email/components";
import Base from "./base"; import Base from "./base";
export type Generic_NewMail = { export type Generic_NewMail = {
mailType: "generic"; mailType: "generic";
props: GenericProps; props: GenericProps;
}; };
type GenericProps = { type GenericProps = {
title: string; title: string;
noreply?: boolean; noreply?: boolean;
testo: string; testo: string;
link?: { link?: {
href: string; href: string;
label?: string; label?: string;
}; };
}; };
const Generic = ({ const Generic = ({
title, title,
noreply = false, noreply = false,
testo, testo,
link = undefined, link = undefined,
}: GenericProps) => { }: GenericProps) => {
return ( return (
<Base preview={title} noreply={noreply}> <Base noreply={noreply} preview={title}>
<> <>
<Heading className="text-center text-3xl leading-8">{title}</Heading> <Heading className="text-center text-3xl leading-8">{title}</Heading>
<Section className="px-5 text-left text-base md:px-12"> <Section className="px-5 text-left text-base md:px-12">
<Row> <Row>
<Text>{testo}</Text> <Text>{testo}</Text>
</Row> </Row>
{link && ( {link && (
<Section className="mb-5"> <Section className="mb-5">
<Button <Button
className="mx-auto box-border inline-flex h-10 items-center justify-center rounded-md bg-neutral-100 px-4 py-2 text-center align-middle font-semibold whitespace-nowrap text-neutral-900" className="mx-auto box-border inline-flex h-10 items-center justify-center rounded-md bg-neutral-100 px-4 py-2 text-center align-middle font-semibold whitespace-nowrap text-neutral-900"
href={`${link.href}`} href={`${link.href}`}
> >
{link.label} {link.label}
</Button> </Button>
</Section> </Section>
)} )}
</Section> </Section>
</> </>
</Base> </Base>
); );
}; };
export default Generic; export default Generic;

View file

@ -61,11 +61,11 @@ const OnboardingServizio = ({
<Section className="mb-[30px]" key={index}> <Section className="mb-[30px]" key={index}>
<Row className="mb-[12px]"> <Row className="mb-[12px]">
<Img <Img
src={`${process.env.NEXT_PUBLIC_BASE_URL}/go-api/images/get/${annuncio.immagine}`}
width="100%"
height="140px"
alt={`Annuncio image - ${annuncio.codice}`} alt={`Annuncio image - ${annuncio.codice}`}
className="block w-[240px] rounded-[4px] object-cover object-center" className="block w-[240px] rounded-[4px] object-cover object-center"
height="140px"
src={`${process.env.NEXT_PUBLIC_BASE_URL}/go-api/images/get/${annuncio.immagine}`}
width="100%"
/> />
</Row> </Row>
<Row className="block w-full sm:hidden"> <Row className="block w-full sm:hidden">
@ -83,8 +83,8 @@ const OnboardingServizio = ({
{(annuncio.prezzo / 1e2).toFixed(2)} {(annuncio.prezzo / 1e2).toFixed(2)}
</Text> </Text>
<Link <Link
href={`${process.env.NEXT_PUBLIC_BASE_URL}/annuncio/${annuncio.codice}`}
className="block text-[14px] font-semibold text-indigo-600 no-underline" className="block text-[14px] font-semibold text-indigo-600 no-underline"
href={`${process.env.NEXT_PUBLIC_BASE_URL}/annuncio/${annuncio.codice}`}
> >
Scheda dell&apos;annuncio Scheda dell&apos;annuncio
</Link> </Link>

View file

@ -1,36 +1,36 @@
import { Button, Heading, Row, Section, Text } from "@react-email/components"; import { Button, Heading, Row, Section, Text } from "@react-email/components";
import Base from "./base"; import Base from "./base";
export type PagamentoErrore_NewMail = { export type PagamentoErrore_NewMail = {
mailType: "pagamentoErrore"; mailType: "pagamentoErrore";
}; };
const PagamentoErrore = () => { const PagamentoErrore = () => {
return ( return (
<Base preview="Errore Pagamento"> <Base preview="Errore Pagamento">
<> <>
<Heading className="text-center text-3xl leading-8"> <Heading className="text-center text-3xl leading-8">
Errore Pagamento Errore Pagamento
</Heading> </Heading>
<Section className="px-5 text-left text-base md:px-12"> <Section className="px-5 text-left text-base md:px-12">
<Row> <Row>
<Text> <Text>
Gentile Cliente, la informiamo che il pagamento non è andato a Gentile Cliente, la informiamo che il pagamento non è andato a
buon fine. Si prega di riprovare o contattare il nostro servizio buon fine. Si prega di riprovare o contattare il nostro servizio
clienti per ulteriori informazioni. clienti per ulteriori informazioni.
</Text> </Text>
</Row> </Row>
<Section className="mb-5"> <Section className="mb-5">
<Button <Button
className="mx-auto box-border inline-flex h-10 items-center justify-center rounded-md bg-neutral-100 px-4 py-2 text-center align-middle font-semibold whitespace-nowrap text-neutral-900" className="mx-auto box-border inline-flex h-10 items-center justify-center rounded-md bg-neutral-100 px-4 py-2 text-center align-middle font-semibold whitespace-nowrap text-neutral-900"
href={`${process.env.NEXT_PUBLIC_BASE_URL}/area-riservata/dashboard`} href={`${process.env.NEXT_PUBLIC_BASE_URL}/area-riservata/dashboard`}
> >
Vai al tuo account Vai al tuo account
</Button> </Button>
</Section> </Section>
</Section> </Section>
</> </>
</Base> </Base>
); );
}; };
export default PagamentoErrore; export default PagamentoErrore;

View file

@ -1,36 +1,36 @@
import { Button, Heading, Row, Section, Text } from "@react-email/components"; import { Button, Heading, Row, Section, Text } from "@react-email/components";
import Base from "./base"; import Base from "./base";
export type PagamentoRicezione_NewMail = { export type PagamentoRicezione_NewMail = {
mailType: "pagamentoRicezione"; mailType: "pagamentoRicezione";
}; };
const PagamentoRicezione = () => { const PagamentoRicezione = () => {
return ( return (
<Base preview="Ricezione Pagamento"> <Base preview="Ricezione Pagamento">
<> <>
<Heading className="text-center text-3xl leading-8"> <Heading className="text-center text-3xl leading-8">
Ricezione Pagamento Ricezione Pagamento
</Heading> </Heading>
<Section className="px-5 text-left text-base md:px-12"> <Section className="px-5 text-left text-base md:px-12">
<Row> <Row>
<Text> <Text>
Gentile Cliente, le confermiamo la ricezione del pagamento. Le Gentile Cliente, le confermiamo la ricezione del pagamento. Le
invieremo una mail di conferma quando il pagamento sarà stato invieremo una mail di conferma quando il pagamento sarà stato
processato. processato.
</Text> </Text>
</Row> </Row>
<Section className="mb-5"> <Section className="mb-5">
<Button <Button
className="mx-auto box-border inline-flex h-10 items-center justify-center rounded-md bg-neutral-100 px-4 py-2 text-center align-middle font-semibold whitespace-nowrap text-neutral-900" className="mx-auto box-border inline-flex h-10 items-center justify-center rounded-md bg-neutral-100 px-4 py-2 text-center align-middle font-semibold whitespace-nowrap text-neutral-900"
href={`${process.env.NEXT_PUBLIC_BASE_URL}/area-riservata/dashboard`} href={`${process.env.NEXT_PUBLIC_BASE_URL}/area-riservata/dashboard`}
> >
Vai al tuo account Vai al tuo account
</Button> </Button>
</Section> </Section>
</Section> </Section>
</> </>
</Base> </Base>
); );
}; };
export default PagamentoRicezione; export default PagamentoRicezione;

View file

@ -1,39 +1,39 @@
import { Heading, Row, Section, Text } from "@react-email/components"; import { Heading, Row, Section, Text } from "@react-email/components";
import Base from "./base"; import Base from "./base";
export type Recesso_NewMail = { export type Recesso_NewMail = {
mailType: "recesso"; mailType: "recesso";
}; };
const Recesso = () => { const Recesso = () => {
return ( return (
<Base preview="Richiesta di recesso"> <Base preview="Richiesta di recesso">
<> <>
<Heading className="text-center text-3xl leading-8"> <Heading className="text-center text-3xl leading-8">
Richiesta di recesso Richiesta di recesso
</Heading> </Heading>
<Section className="px-5 text-left text-base md:px-12"> <Section className="px-5 text-left text-base md:px-12">
<Row> <Row>
<Text> <Text>
Gentile cliente, abbiamo ricevuto la tua richiesta di recesso. Gentile cliente, abbiamo ricevuto la tua richiesta di recesso.
Procederemo ad elaborare la richiesta e termineremo i servizi Procederemo ad elaborare la richiesta e termineremo i servizi
attualemte attivi nel tuo account. attualemte attivi nel tuo account.
</Text> </Text>
<Text> <Text>
Per ulteriori informazioni e chiarimenti non esiti a contattarci Per ulteriori informazioni e chiarimenti non esiti a contattarci
all&apos;indirizzo{" "} all&apos;indirizzo{" "}
<a <a
href="mailto:amministrazione@infoalloggi.it" href="mailto:amministrazione@infoalloggi.it"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
amministrazione@infoalloggi.it{" "} amministrazione@infoalloggi.it{" "}
</a> </a>
</Text> </Text>
<Text>Cordiali saluti</Text> <Text>Cordiali saluti</Text>
<br /> <br />
</Row> </Row>
</Section> </Section>
</> </>
</Base> </Base>
); );
}; };
export default Recesso; export default Recesso;

View file

@ -1,35 +1,35 @@
import { Button, Heading, Row, Section, Text } from "@react-email/components"; import { Button, Heading, Row, Section, Text } from "@react-email/components";
import Base from "./base"; import Base from "./base";
export type RegistrazioneAvvenuta_NewMail = { export type RegistrazioneAvvenuta_NewMail = {
mailType: "registrazioneAvvenuta"; mailType: "registrazioneAvvenuta";
}; };
const RegistrazioneAvvenuta = () => { const RegistrazioneAvvenuta = () => {
return ( return (
<Base preview="Registrazione Avvenuta"> <Base preview="Registrazione Avvenuta">
<> <>
<Heading className="text-center text-3xl leading-8"> <Heading className="text-center text-3xl leading-8">
Registrazione avvenuta Registrazione avvenuta
</Heading> </Heading>
<Section className="px-5 text-left text-base md:px-12"> <Section className="px-5 text-left text-base md:px-12">
<Row> <Row>
<Text> <Text>
La tua registrazione è avvenuta con successo! Ora puoi accedere al La tua registrazione è avvenuta con successo! Ora puoi accedere al
nostro sito con le tue credenziali. nostro sito con le tue credenziali.
</Text> </Text>
</Row> </Row>
<Section className="mb-5"> <Section className="mb-5">
<Button <Button
className="mx-auto box-border inline-flex h-10 items-center justify-center rounded-md bg-neutral-100 px-4 py-2 text-center align-middle font-semibold whitespace-nowrap text-neutral-900" className="mx-auto box-border inline-flex h-10 items-center justify-center rounded-md bg-neutral-100 px-4 py-2 text-center align-middle font-semibold whitespace-nowrap text-neutral-900"
href={`${process.env.NEXT_PUBLIC_BASE_URL}/area-riservata/dashboard`} href={`${process.env.NEXT_PUBLIC_BASE_URL}/area-riservata/dashboard`}
> >
Vai al tuo account Vai al tuo account
</Button> </Button>
</Section> </Section>
</Section> </Section>
</> </>
</Base> </Base>
); );
}; };
export default RegistrazioneAvvenuta; export default RegistrazioneAvvenuta;

View file

@ -1,6 +1,5 @@
{ {
"$schema": "https://unpkg.com/knip@5/schema.json", "$schema": "https://unpkg.com/knip@5/schema.json",
"project": ["src/**", "**", "!src/schemas/public/*.ts", "!emails/**"],
"ignore": [ "ignore": [
".kanelrc.js", ".kanelrc.js",
"TypeHelpers.type.ts", "TypeHelpers.type.ts",
@ -36,5 +35,6 @@
"@radix-ui/react-slot", "@radix-ui/react-slot",
"@radix-ui/react-tooltip", "@radix-ui/react-tooltip",
"@radix-ui/react-popover" "@radix-ui/react-popover"
] ],
"project": ["src/**", "**", "!src/schemas/public/*.ts", "!emails/**"]
} }

View file

@ -7,86 +7,86 @@ import { env } from "node:process";
/** @type {import("next").NextConfig} */ /** @type {import("next").NextConfig} */
const nextConfig = { const nextConfig = {
output: env.NODE_ENV === "production" ? "standalone" : undefined, // This is for docker builds
reactStrictMode: true,
compiler: { compiler: {
removeConsole: false, removeConsole: false,
}, },
i18n: {
locales: ["it", "en"],
defaultLocale: "it",
},
images: {
remotePatterns: [
{
protocol: "https",
hostname: "*.googleusercontent.com",
port: "",
pathname: "**",
},
{
hostname: env.BACKENDSERVER_URL || "localhost:1323",
},
],
minimumCacheTTL: 86400, // 1 day in seconds
},
typescript: {
ignoreBuildErrors: true,
},
devIndicators: false, devIndicators: false,
rewrites: async () => {
return [
{
source: "/api/panel",
destination: env.NODE_ENV === "production" ? "/404" : "/api/panel",
},
{
source: "/go-api/images/get/:slug*",
destination: `${env.BACKENDSERVER_URL}/images/get/:slug*`,
},
{
source: "/go-api/storage/get/:slug*",
destination: `${env.BACKENDSERVER_URL}/storage/get/:slug*`,
has: [
{ type: "cookie", key: "access_token" },
{
type: "query",
key: "token",
value:
"(?<token>^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$)",
},
],
},
{
source: "/go-api/storage/upload:slug*",
destination: `${env.BACKENDSERVER_URL}/storage/upload:slug*`,
has: [
{ type: "cookie", key: "access_token" },
{
type: "query",
key: "token",
value:
"(?<token>^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$)",
},
],
},
];
},
headers: async () => { headers: async () => {
return [ return [
{ {
source: "/:path*",
headers: [ headers: [
{ {
key: "X-Frame-Options", key: "X-Frame-Options",
value: "", value: "",
}, },
], ],
source: "/:path*",
}, },
]; ];
}, },
i18n: {
defaultLocale: "it",
locales: ["it", "en"],
},
images: {
minimumCacheTTL: 86400, // 1 day in seconds
remotePatterns: [
{
hostname: "*.googleusercontent.com",
pathname: "**",
port: "",
protocol: "https",
},
{
hostname: env.BACKENDSERVER_URL || "localhost:1323",
},
],
},
output: env.NODE_ENV === "production" ? "standalone" : undefined, // This is for docker builds
reactStrictMode: true,
rewrites: async () => {
return [
{
destination: env.NODE_ENV === "production" ? "/404" : "/api/panel",
source: "/api/panel",
},
{
destination: `${env.BACKENDSERVER_URL}/images/get/:slug*`,
source: "/go-api/images/get/:slug*",
},
{
destination: `${env.BACKENDSERVER_URL}/storage/get/:slug*`,
has: [
{ key: "access_token", type: "cookie" },
{
key: "token",
type: "query",
value:
"(?<token>^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$)",
},
],
source: "/go-api/storage/get/:slug*",
},
{
destination: `${env.BACKENDSERVER_URL}/storage/upload:slug*`,
has: [
{ key: "access_token", type: "cookie" },
{
key: "token",
type: "query",
value:
"(?<token>^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$)",
},
],
source: "/go-api/storage/upload:slug*",
},
];
},
typescript: {
ignoreBuildErrors: true,
},
}; };
export default nextConfig; export default nextConfig;

View file

@ -1,22 +1,6 @@
{ {
"name": "infoalloggi", "ct3aMetadata": {
"version": "0.1.0", "initVersion": "7.9.0"
"private": true,
"scripts": {
"build": "next build",
"dev": "node ./headers/dev.js && next dev --turbopack",
"dev-https": "node ./headers/dev-https.js && cross-env NEXT_PUBLIC_BASE_URL='https://localhost:3000' INTERNAL_BASE_URL='https://localhost:3000' next dev --experimental-https --turbopack",
"idev": "cross-env NODE_OPTIONS='--inspect' next dev --turbopack",
"lint": "node ./headers/lint.js && biome lint && node check-env-vars.js",
"start": "next start",
"types": "node ./headers/type.js && tsc --noEmit",
"pg-typegen": "node ./headers/pg-typegen.js && npx kanel",
"email": "cross-env NEXT_PUBLIC_BASE_URL='https://localhost:3000' email dev --port 3500",
"test": "npm run lint && npm run types",
"knip": "knip --no-exit-code",
"stripe-dev": "stripe listen --forward-to https://localhost:3000/api/stripe-webhook",
"d": "npm run-p dev stripe-dev -cl",
"test-kr": "node ./kyselyRules/rules/select.test.js && node ./kyselyRules/rules/where.test.js&& node ./kyselyRules/rules/null.test.js"
}, },
"dependencies": { "dependencies": {
"@fattureincloud/fattureincloud-ts-sdk": "^2.1.1", "@fattureincloud/fattureincloud-ts-sdk": "^2.1.1",
@ -122,7 +106,23 @@
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"typescript": "^5.9.2" "typescript": "^5.9.2"
}, },
"ct3aMetadata": { "name": "infoalloggi",
"initVersion": "7.9.0" "private": true,
} "scripts": {
"build": "next build",
"d": "npm run-p dev stripe-dev -cl",
"dev": "node ./headers/dev.js && next dev --turbopack",
"dev-https": "node ./headers/dev-https.js && cross-env NEXT_PUBLIC_BASE_URL='https://localhost:3000' INTERNAL_BASE_URL='https://localhost:3000' next dev --experimental-https --turbopack",
"email": "cross-env NEXT_PUBLIC_BASE_URL='https://localhost:3000' email dev --port 3500",
"idev": "cross-env NODE_OPTIONS='--inspect' next dev --turbopack",
"knip": "knip --no-exit-code",
"lint": "node ./headers/lint.js && biome lint && node check-env-vars.js",
"pg-typegen": "node ./headers/pg-typegen.js && npx kanel",
"start": "next start",
"stripe-dev": "stripe listen --forward-to https://localhost:3000/api/stripe-webhook",
"test": "npm run lint && npm run types",
"test-kr": "node ./kyselyRules/rules/select.test.js && node ./kyselyRules/rules/where.test.js&& node ./kyselyRules/rules/null.test.js",
"types": "node ./headers/type.js && tsc --noEmit"
},
"version": "0.1.0"
} }

View file

@ -1,39 +1,39 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import "~/styles/globals.css"; import "~/styles/globals.css";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Home", description: "Welcome to Next.js",
description: "Welcome to Next.js", icons: {
icons: { apple: {
apple: { sizes: "180x180",
url: "/favicon/apple-touch-icon.png", type: "image/png",
sizes: "180x180", url: "/favicon/apple-touch-icon.png",
type: "image/png", },
}, icon: [
icon: [ { sizes: "32x32", type: "image/png", url: "/favicon/favicon-32x32.png" },
{ url: "/favicon/favicon-32x32.png", sizes: "32x32", type: "image/png" }, { sizes: "16x16", type: "image/png", url: "/favicon/favicon-16x16.png" },
{ url: "/favicon/favicon-16x16.png", sizes: "16x16", type: "image/png" }, ],
], other: [
other: [ {
{ color: "#5bbad5",
rel: "mask-icon", rel: "mask-icon",
url: "/favicon/safari-pinned-tab.svg", url: "/favicon/safari-pinned-tab.svg",
color: "#5bbad5", },
}, ],
], },
}, manifest: "/site.webmanifest",
manifest: "/site.webmanifest", title: "Home",
}; };
export default function RootLayout({ export default function RootLayout({
// Layouts must accept a children prop. // Layouts must accept a children prop.
// This will be populated with nested layouts or pages // This will be populated with nested layouts or pages
children, children,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( return (
<html lang="en"> <html lang="en">
<body>{children}</body> <body>{children}</body>
</html> </html>
); );
} }

View file

@ -2,19 +2,19 @@ import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
import { db } from "~/server/db"; import { db } from "~/server/db";
export async function getDataFromDB() { export async function getDataFromDB() {
try { try {
// Example query - adjust to your database schema // Example query - adjust to your database schema
const data = await db const data = await db
.selectFrom("annunci") .selectFrom("annunci")
.select("codice") .select("codice")
.where("web", "=", true) .where("web", "=", true)
.where("tipo", "=", TipologiaPosizioneEnum.Transitorio) .where("tipo", "=", TipologiaPosizioneEnum.Transitorio)
.where("stato", "=", "Attivo") .where("stato", "=", "Attivo")
.execute(); .execute();
return data; return data;
} catch (error) { } catch (error) {
console.error("Database error:", error); console.error("Database error:", error);
return []; return [];
} }
} }

View file

@ -216,15 +216,15 @@ export const IconMatrixTooltip = () => {
<div className="grid grid-cols-4 gap-2"> <div className="grid grid-cols-4 gap-2">
{iconOptions.map((icon) => ( {iconOptions.map((icon) => (
<button <button
type="button" className="bg-muted flex flex-col items-center rounded-xl p-2 hover:opacity-75"
key={icon}
onClick={async () => { onClick={async () => {
await navigator.clipboard.writeText(icon); await navigator.clipboard.writeText(icon);
toast("Copiato", { icon: "📋" }); toast("Copiato", { icon: "📋" });
}} }}
key={icon} type="button"
className="bg-muted flex flex-col items-center rounded-xl p-2 hover:opacity-75"
> >
<IconMatrix type={icon} className="size-6" /> <IconMatrix className="size-6" type={icon} />
<span className="text-sm">{icon}</span> <span className="text-sm">{icon}</span>
</button> </button>
))} ))}
@ -310,16 +310,16 @@ export const ShieldExclamationIcon = ({
className?: string; className?: string;
}) => ( }) => (
<svg <svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className={className} className={className}
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
> >
<title>Shield</title> <title>Shield</title>
<path <path
fillRule="evenodd"
d="M11.484 2.17a.75.75 0 0 1 1.032 0 11.209 11.209 0 0 0 7.877 3.08.75.75 0 0 1 .722.515 12.74 12.74 0 0 1 .635 3.985c0 5.942-4.064 10.933-9.563 12.348a.749.749 0 0 1-.374 0C6.314 20.683 2.25 15.692 2.25 9.75c0-1.39.223-2.73.635-3.985a.75.75 0 0 1 .722-.516l.143.001c2.996 0 5.718-1.17 7.734-3.08ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75ZM12 15a.75.75 0 0 0-.75.75v.008c0 .414.336.75.75.75h.008a.75.75 0 0 0 .75-.75v-.008a.75.75 0 0 0-.75-.75H12Z"
clipRule="evenodd" clipRule="evenodd"
d="M11.484 2.17a.75.75 0 0 1 1.032 0 11.209 11.209 0 0 0 7.877 3.08.75.75 0 0 1 .722.515 12.74 12.74 0 0 1 .635 3.985c0 5.942-4.064 10.933-9.563 12.348a.749.749 0 0 1-.374 0C6.314 20.683 2.25 15.692 2.25 9.75c0-1.39.223-2.73.635-3.985a.75.75 0 0 1 .722-.516l.143.001c2.996 0 5.718-1.17 7.734-3.08ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75ZM12 15a.75.75 0 0 0-.75.75v.008c0 .414.336.75.75.75h.008a.75.75 0 0 0 .75-.75v-.008a.75.75 0 0 0-.75-.75H12Z"
fillRule="evenodd"
/> />
</svg> </svg>
); );
@ -330,49 +330,49 @@ export const QuestionMarkCircleIcon = ({
className?: string; className?: string;
}) => ( }) => (
<svg <svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className={className} className={className}
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
> >
<title>QuestionMark</title> <title>QuestionMark</title>
<path <path
fillRule="evenodd"
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm11.378-3.917c-.89-.777-2.366-.777-3.255 0a.75.75 0 0 1-.988-1.129c1.454-1.272 3.776-1.272 5.23 0 1.513 1.324 1.513 3.518 0 4.842a3.75 3.75 0 0 1-.837.552c-.676.328-1.028.774-1.028 1.152v.75a.75.75 0 0 1-1.5 0v-.75c0-1.279 1.06-2.107 1.875-2.502.182-.088.351-.199.503-.331.83-.727.83-1.857 0-2.584ZM12 18a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z"
clipRule="evenodd" clipRule="evenodd"
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm11.378-3.917c-.89-.777-2.366-.777-3.255 0a.75.75 0 0 1-.988-1.129c1.454-1.272 3.776-1.272 5.23 0 1.513 1.324 1.513 3.518 0 4.842a3.75 3.75 0 0 1-.837.552c-.676.328-1.028.774-1.028 1.152v.75a.75.75 0 0 1-1.5 0v-.75c0-1.279 1.06-2.107 1.875-2.502.182-.088.351-.199.503-.331.83-.727.83-1.857 0-2.584ZM12 18a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z"
fillRule="evenodd"
/> />
</svg> </svg>
); );
export const HomeIcon = ({ className }: { className?: string }) => ( export const HomeIcon = ({ className }: { className?: string }) => (
<svg <svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className={className} className={className}
fill="none"
stroke="currentColor"
strokeWidth={1.5}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
> >
<title>Home</title> <title>Home</title>
<path <path
d="m2.25 12 8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
d="m2.25 12 8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
/> />
</svg> </svg>
); );
const CheckCircleIcon = ({ className }: { className?: string }) => ( const CheckCircleIcon = ({ className }: { className?: string }) => (
<svg <svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className={className} className={className}
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
> >
<title>CheckCircle</title> <title>CheckCircle</title>
<path <path
fillRule="evenodd"
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z"
clipRule="evenodd" clipRule="evenodd"
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z"
fillRule="evenodd"
/> />
</svg> </svg>
); );

View file

@ -6,14 +6,14 @@ export const ImageFlbk = (props: ComponentProps<typeof Image>) => {
return ( return (
<Image <Image
{...props} {...props}
onError={(e) => {
e.currentTarget.src = fallbackImage;
}}
onLoad={(e) => { onLoad={(e) => {
if (e.currentTarget.naturalWidth === 0) { if (e.currentTarget.naturalWidth === 0) {
e.currentTarget.src = fallbackImage; e.currentTarget.src = fallbackImage;
} }
}} }}
onError={(e) => {
e.currentTarget.src = fallbackImage;
}}
/> />
); );
}; };

View file

@ -59,20 +59,20 @@ const InformationBubble = ({
)} )}
> >
<IconMatrix <IconMatrix
type={icon}
className={cn("size-6 text-indigo-500", iconClassName)} className={cn("size-6 text-indigo-500", iconClassName)}
type={icon}
/> />
</HybridTooltipTrigger> </HybridTooltipTrigger>
<HybridTooltipContent <HybridTooltipContent
side={side}
className="border-border -mb-1.5 bg-white shadow-sm [&>span>svg]:invisible" className="border-border -mb-1.5 bg-white shadow-sm [&>span>svg]:invisible"
side={side}
> >
{href ? ( {href ? (
<Link <Link
className="flex items-center justify-center text-sm text-blue-500 underline-offset-2 hover:underline"
href={href} href={href}
target={target} target={target}
className="flex items-center justify-center text-sm text-blue-500 underline-offset-2 hover:underline"
> >
<ExternalLink className="mr-1 size-4" /> <ExternalLink className="mr-1 size-4" />
<p>{txt}</p> <p>{txt}</p>

View file

@ -145,8 +145,8 @@ export const AreaRiservataLayoutUserView = ({
<Head> <Head>
<title>{t.heads.area_riservata_titolo}</title> <title>{t.heads.area_riservata_titolo}</title>
<meta <meta
name="description"
content={t.heads.area_riservata_description} content={t.heads.area_riservata_description}
name="description"
/> />
</Head> </Head>
<div className="flex h-auto w-full flex-col md:flex-row"> <div className="flex h-auto w-full flex-col md:flex-row">
@ -191,8 +191,8 @@ const UserViewHeader = () => {
<Link href={`https://wa.me/${data.telefono}`} target="_blank"> <Link href={`https://wa.me/${data.telefono}`} target="_blank">
<Button <Button
className="flex items-center gap-2" className="flex items-center gap-2"
variant="success"
size="sm" size="sm"
variant="success"
> >
<WhatsAppIcon className="size-4 fill-white" /> <WhatsAppIcon className="size-4 fill-white" />
WhatsApp WhatsApp
@ -203,8 +203,8 @@ const UserViewHeader = () => {
<Link href={`/area-riservata/admin/user-view/edit-user/${data.id}`}> <Link href={`/area-riservata/admin/user-view/edit-user/${data.id}`}>
<Button <Button
className="flex items-center gap-2 border" className="flex items-center gap-2 border"
variant={pathname.includes("edit-user") ? "secondary" : "outline"}
size="sm" size="sm"
variant={pathname.includes("edit-user") ? "secondary" : "outline"}
> >
<UserCog /> Profilo <UserCog /> Profilo
</Button> </Button>
@ -212,8 +212,8 @@ const UserViewHeader = () => {
<Link href={`/area-riservata/admin/user-view/servizio/${data.id}`}> <Link href={`/area-riservata/admin/user-view/servizio/${data.id}`}>
<Button <Button
className="flex items-center gap-2 border" className="flex items-center gap-2 border"
variant={pathname.includes("servizio") ? "secondary" : "outline"}
size="sm" size="sm"
variant={pathname.includes("servizio") ? "secondary" : "outline"}
> >
<Tickets /> Servizi <Tickets /> Servizi
</Button> </Button>
@ -222,8 +222,8 @@ const UserViewHeader = () => {
<Link href={`/area-riservata/admin/user-view/ricerca/${data.id}`}> <Link href={`/area-riservata/admin/user-view/ricerca/${data.id}`}>
<Button <Button
className="flex items-center gap-2 border" className="flex items-center gap-2 border"
variant={pathname.includes("ricerca") ? "secondary" : "outline"}
size="sm" size="sm"
variant={pathname.includes("ricerca") ? "secondary" : "outline"}
> >
<Search /> Ricerca <Search /> Ricerca
</Button> </Button>
@ -231,10 +231,10 @@ const UserViewHeader = () => {
<Link href={`/area-riservata/admin/user-view/comunicazioni/${data.id}`}> <Link href={`/area-riservata/admin/user-view/comunicazioni/${data.id}`}>
<Button <Button
className="flex items-center gap-2 border" className="flex items-center gap-2 border"
size="sm"
variant={ variant={
pathname.includes("comunicazioni") ? "secondary" : "outline" pathname.includes("comunicazioni") ? "secondary" : "outline"
} }
size="sm"
> >
<Mail /> Comunicazioni <Mail /> Comunicazioni
</Button> </Button>
@ -243,8 +243,8 @@ const UserViewHeader = () => {
<Link href={`/area-riservata/admin/user-view/ordini/${data.id}`}> <Link href={`/area-riservata/admin/user-view/ordini/${data.id}`}>
<Button <Button
className="flex items-center gap-2 border" className="flex items-center gap-2 border"
variant={pathname.includes("ordini") ? "secondary" : "outline"}
size="sm" size="sm"
variant={pathname.includes("ordini") ? "secondary" : "outline"}
> >
<ShoppingBag /> Ordini <ShoppingBag /> Ordini
</Button> </Button>
@ -252,8 +252,8 @@ const UserViewHeader = () => {
<Link href={`/area-riservata/admin/user-view/allegati/${data.id}`}> <Link href={`/area-riservata/admin/user-view/allegati/${data.id}`}>
<Button <Button
className="flex items-center gap-2 border" className="flex items-center gap-2 border"
variant={pathname.includes("allegati") ? "secondary" : "outline"}
size="sm" size="sm"
variant={pathname.includes("allegati") ? "secondary" : "outline"}
> >
<Paperclip /> Allegati <Paperclip /> Allegati
</Button> </Button>
@ -272,21 +272,21 @@ const UserViewHeader = () => {
> >
<Button <Button
className="flex items-center gap-2 border" className="flex items-center gap-2 border"
variant={!data.chatid ? "ghost" : "outline"}
size="sm" size="sm"
variant={!data.chatid ? "ghost" : "outline"}
> >
<MessagesSquare /> Chat <MessagesSquare /> Chat
</Button> </Button>
</Link> </Link>
<Button <Button
size="sm"
className="flex gap-2" className="flex gap-2"
onClick={() => onClick={() =>
swap({ swap({
id: data.id, id: data.id,
}) })
} }
size="sm"
> >
<ExternalLink /> Login <ExternalLink /> Login
</Button> </Button>

View file

@ -27,7 +27,7 @@ export const MappaDialogFullscreen = ({
const { t } = useTranslation(); const { t } = useTranslation();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog onOpenChange={setOpen} open={open}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button className="w-auto grow" disabled={!lat || !long}> <Button className="w-auto grow" disabled={!lat || !long}>
<MapIcon className="size-5" /> {t.mappa} <MapIcon className="size-5" /> {t.mappa}
@ -42,18 +42,18 @@ export const MappaDialogFullscreen = ({
{lat && long && ( {lat && long && (
<> <>
<MapComp <MapComp
height="100%"
markerType="home"
posix={{ posix={{
popupTxt:
"Posizione esatta fornita dopo la richiesta di contatto",
pos: { pos: {
lat: Number.parseFloat(lat), lat: Number.parseFloat(lat),
lng: Number.parseFloat(long), lng: Number.parseFloat(long),
}, },
popupTxt:
"Posizione esatta fornita dopo la richiesta di contatto",
}} }}
markerType="home"
scrollWheelZoom scrollWheelZoom
width="100%" width="100%"
height="100%"
zoom={14} zoom={14}
/> />
</> </>

View file

@ -38,7 +38,7 @@ export const AccordionComp = forwardRef<HTMLDivElement, AccordionCompProps>(
className={cn("mx-auto flex max-w-xl justify-center", className)} className={cn("mx-auto flex max-w-xl justify-center", className)}
ref={ref} ref={ref}
> >
<Accordion type="single" collapsible className="w-full"> <Accordion className="w-full" collapsible type="single">
{texts.map((text, index) => ( {texts.map((text, index) => (
<AccordionItem key={`ac-item-${index}`} value={`item-${index}`}> <AccordionItem 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-lg">

View file

@ -58,13 +58,13 @@ export const AcquistoProcessing = ({
</h3> </h3>
{!generatedIntent ? ( {!generatedIntent ? (
<PricePreparation <PricePreparation
packId={packId}
onProcedi={() => { onProcedi={() => {
setGeneratedIntent(true); setGeneratedIntent(true);
mutate({ mutate({
paymentId, paymentId,
}); });
}} }}
packId={packId}
/> />
) : isPending ? ( ) : isPending ? (
<LoadingPage /> <LoadingPage />
@ -85,9 +85,9 @@ export const AcquistoProcessing = ({
stripe={stripePromise} stripe={stripePromise}
> >
<CheckoutForm <CheckoutForm
titolo={stripeIntent.titolo}
prezzo={stripeIntent.prezzo}
descrizione={stripeIntent.descrizione} descrizione={stripeIntent.descrizione}
prezzo={stripeIntent.prezzo}
titolo={stripeIntent.titolo}
/> />
</Elements> </Elements>
</> </>
@ -136,9 +136,9 @@ export const PricePreparation = ({
{prezziario.testo_condizioni && ( {prezziario.testo_condizioni && (
<Link <Link
aria-label="Leggi le condizioni" aria-label="Leggi le condizioni"
className="block w-fit"
href={`/servizio/condizioni/${prezziario.testo_condizioni}`} href={`/servizio/condizioni/${prezziario.testo_condizioni}`}
target="_blank" target="_blank"
className="block w-fit"
> >
<Button className="flex w-full items-center gap-2 text-sm"> <Button className="flex w-full items-center gap-2 text-sm">
<span>{t.acquisto.leggi_condizioni}</span> <span>{t.acquisto.leggi_condizioni}</span>
@ -148,11 +148,11 @@ export const PricePreparation = ({
)} )}
<Button <Button
onClick={onProcedi}
className="flex w-full items-center gap-2 text-lg" className="flex w-full items-center gap-2 text-lg"
variant="info"
size="xl"
disabled={isLoading || !prezziario} disabled={isLoading || !prezziario}
onClick={onProcedi}
size="xl"
variant="info"
> >
<span>{t.acquisto.procedi_pagamento}</span> <ArrowRight /> <span>{t.acquisto.procedi_pagamento}</span> <ArrowRight />
</Button> </Button>
@ -188,10 +188,10 @@ const CheckoutForm = ({
setIsLoading(true); setIsLoading(true);
const { error } = await stripe.confirmPayment({ const { error } = await stripe.confirmPayment({
elements,
confirmParams: { confirmParams: {
return_url: `${env.NEXT_PUBLIC_BASE_URL}/servizio/acquisto-processing`, return_url: `${env.NEXT_PUBLIC_BASE_URL}/servizio/acquisto-processing`,
}, },
elements,
}); });
if (error.type === "card_error" || error.type === "validation_error") { if (error.type === "card_error" || error.type === "validation_error") {
setMessage(error.message || "An unexpected error occurred."); setMessage(error.message || "An unexpected error occurred.");
@ -218,7 +218,7 @@ const CheckoutForm = ({
</span> </span>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<form id="payment-form" onSubmit={handleSubmit} className="space-y-5"> <form className="space-y-5" id="payment-form" onSubmit={handleSubmit}>
<PaymentElement <PaymentElement
id="payment-element" id="payment-element"
options={{ layout: "accordion" }} options={{ layout: "accordion" }}
@ -226,10 +226,10 @@ const CheckoutForm = ({
<LoadingButton <LoadingButton
aria-label="Procedi al pagamento" aria-label="Procedi al pagamento"
type="submit"
className="w-full" className="w-full"
disabled={isLoading || !stripe || !elements} disabled={isLoading || !stripe || !elements}
loading={isLoading || !stripe || !elements} loading={isLoading || !stripe || !elements}
type="submit"
> >
{t.procedi} {t.procedi}
</LoadingButton> </LoadingButton>

View file

@ -22,7 +22,7 @@ export function ReceiptGenerator({ data }: { data: PurchaseData }) {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h1 className="text-2xl font-bold">Ricevuta di cortesia</h1> <h1 className="text-2xl font-bold">Ricevuta di cortesia</h1>
<Button onClick={() => reactToPrintFn()} className="print:hidden"> <Button className="print:hidden" onClick={() => reactToPrintFn()}>
Stampa Stampa
</Button> </Button>
</div> </div>
@ -79,10 +79,10 @@ function Receipt({ data }: ReceiptProps) {
<div className="relative size-24"> <div className="relative size-24">
<Image <Image
src={"/favicon/favicon.png"}
alt={`${companyName} logo`} alt={`${companyName} logo`}
fill
className="object-contain" className="object-contain"
fill
src={"/favicon/favicon.png"}
/> />
</div> </div>
</div> </div>
@ -131,7 +131,7 @@ function Receipt({ data }: ReceiptProps) {
</thead> </thead>
<tbody> <tbody>
{data.items.map((item, idx) => ( {data.items.map((item, idx) => (
<tr key={idx} className="border-b"> <tr className="border-b" key={idx}>
<td className="py-2">{item.description}</td> <td className="py-2">{item.description}</td>
<td className="py-2 text-right"> <td className="py-2 text-right">
{formatCurrency(item.price)} {formatCurrency(item.price)}

View file

@ -45,12 +45,12 @@ export const AllegatoIframe = ({
> >
{/** biome-ignore lint/a11y/noNoninteractiveElementInteractions: <need interaction> */} {/** biome-ignore lint/a11y/noNoninteractiveElementInteractions: <need interaction> */}
<iframe <iframe
title="Allegato" allowFullScreen
ref={if_ref}
src={requestUrl}
className="h-full w-full" className="h-full w-full"
onLoad={handleIframeLoad} onLoad={handleIframeLoad}
allowFullScreen ref={if_ref}
src={requestUrl}
title="Allegato"
/> />
</div> </div>
); );

View file

@ -10,23 +10,23 @@ export const AnnunciGrid = ({ pagedata }: { pagedata: AnnuncioRicerca[] }) => {
<div className="relative z-0 mx-auto grid max-w-7xl grid-flow-row grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3"> <div className="relative z-0 mx-auto grid max-w-7xl grid-flow-row grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{pagedata.map((annuncio) => ( {pagedata.map((annuncio) => (
<CardAnnuncio <CardAnnuncio
id={annuncio.id} camere={annuncio.numero_camere}
key={annuncio.codice}
codice={annuncio.codice} codice={annuncio.codice}
comune={annuncio.comune}
consegna={annuncio.consegna}
id={annuncio.id}
immagini={annuncio.url_immagini || undefined}
key={annuncio.codice}
mq={annuncio.mq}
prezzo={annuncio.prezzo} prezzo={annuncio.prezzo}
provincia={annuncio.provincia}
stato={annuncio.stato}
tipo={annuncio.tipo}
titolo={getTitoloTranslation({ titolo={getTitoloTranslation({
locale: locale, locale: locale,
titolo_it: annuncio.titolo_it,
titolo_en: annuncio.titolo_en, titolo_en: annuncio.titolo_en,
titolo_it: annuncio.titolo_it,
})} })}
mq={annuncio.mq}
comune={annuncio.comune}
provincia={annuncio.provincia}
consegna={annuncio.consegna}
camere={annuncio.numero_camere}
immagini={annuncio.url_immagini || undefined}
tipo={annuncio.tipo}
stato={annuncio.stato}
/> />
))} ))}
</div> </div>

View file

@ -30,9 +30,9 @@ export const MapSection = () => {
return ( return (
<div className="relative z-50 flex h-[60vh] w-full items-center justify-center sm:h-[65vh]"> <div className="relative z-50 flex h-[60vh] w-full items-center justify-center sm:h-[65vh]">
<MapDisplay <MapDisplay
selectTipo={tipo}
selectComune={comune} selectComune={comune}
selectConsegna={consegna} selectConsegna={consegna}
selectTipo={tipo}
setSelected={setSelected} setSelected={setSelected}
/> />
<SelectedComp selected={selected} /> <SelectedComp selected={selected} />
@ -53,9 +53,9 @@ const MapDisplay = memo(
setSelected: Dispatch<SetStateAction<AnnuncioRicercaWPosition | null>>; setSelected: Dispatch<SetStateAction<AnnuncioRicercaWPosition | null>>;
}) => { }) => {
const { data, isLoading } = api.annunci.getMapping.useQuery({ const { data, isLoading } = api.annunci.getMapping.useQuery({
tipologia: selectTipo,
comune: selectComune, comune: selectComune,
consegna: selectConsegna || undefined, consegna: selectConsegna || undefined,
tipologia: selectTipo,
}); });
if (isLoading) return <LoadingPage />; if (isLoading) return <LoadingPage />;
@ -65,12 +65,12 @@ const MapDisplay = memo(
.map((a) => { .map((a) => {
if (a.lat_secondario && a.lon_secondario) { if (a.lat_secondario && a.lon_secondario) {
return { return {
markerTxt: `${a.prezzo / 1e2}`,
onClick: () => setSelected(a),
pos: { pos: {
lat: parseFloat(a.lat_secondario), lat: parseFloat(a.lat_secondario),
lng: parseFloat(a.lon_secondario), lng: parseFloat(a.lon_secondario),
}, },
onClick: () => setSelected(a),
markerTxt: `${a.prezzo / 1e2}`,
}; };
} }
return undefined; return undefined;
@ -79,15 +79,15 @@ const MapDisplay = memo(
return ( return (
<MapComp <MapComp
isGroup
groupData={posizioni} groupData={posizioni}
posix={{ pos: { lat: 45.764718, lng: 11.73067 } }}
width="100%"
height="100%" height="100%"
zoom={13} isGroup
scrollWheelZoom
markerType="price" markerType="price"
onMapClick={() => setSelected(null)} onMapClick={() => setSelected(null)}
posix={{ pos: { lat: 45.764718, lng: 11.73067 } }}
scrollWheelZoom
width="100%"
zoom={13}
/> />
); );
}, },
@ -104,27 +104,27 @@ const SelectedComp = memo(
<AnimatePresence initial={false}> <AnimatePresence initial={false}>
{selected && ( {selected && (
<motion.a <motion.a
aria-label="Visualizza Dettagli Annuncio"
href={`/annuncio/${selected.codice}`}
target="_blank"
className="group absolute right-0 bottom-0 left-0 z-20 mx-2 mb-2 flex flex-col gap-2 rounded-md bg-white p-4 ring-neutral-500 hover:ring-1"
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
aria-label="Visualizza Dettagli Annuncio"
className="group absolute right-0 bottom-0 left-0 z-20 mx-2 mb-2 flex flex-col gap-2 rounded-md bg-white p-4 ring-neutral-500 hover:ring-1"
exit={{ opacity: 0, scale: 0 }} exit={{ opacity: 0, scale: 0 }}
href={`/annuncio/${selected.codice}`}
initial={{ opacity: 0, scale: 0 }}
key={selected.id}
target="_blank"
transition={{ transition={{
duration: 0.1, duration: 0.1,
scale: { type: "spring", visualDuration: 0.1, bounce: 0.2 }, scale: { bounce: 0.2, type: "spring", visualDuration: 0.1 },
}} }}
key={selected.id}
> >
<div className="flex gap-2"> <div className="flex gap-2">
{selected.url_immagini?.[0] ? ( {selected.url_immagini?.[0] ? (
<Image <Image
alt="a"
className="size-24 rounded-md object-cover sm:size-40"
height={500}
src={selected.url_immagini[0]} src={selected.url_immagini[0]}
width={500} width={500}
height={500}
className="size-24 rounded-md object-cover sm:size-40"
alt="a"
/> />
) : ( ) : (
<div className="size-20 bg-gray-200 sm:size-40" /> <div className="size-20 bg-gray-200 sm:size-40" />
@ -142,11 +142,11 @@ const SelectedComp = memo(
</div> </div>
<p> <p>
{handleConsegna({ {handleConsegna({
aggiornamento: t.card.in_aggiornamento,
consegna: selected.consegna, consegna: selected.consegna,
consegna_da: t.card.consegna_da, consegna_da: t.card.consegna_da,
mesi: t.preferenze.mesi, mesi: t.preferenze.mesi,
subito: t.card.consegna_subito, subito: t.card.consegna_subito,
aggiornamento: t.card.in_aggiornamento,
})} })}
</p> </p>
</div> </div>

View file

@ -45,16 +45,16 @@ export const CTA_TipologiaModal = () => {
return ( return (
<Dialog <Dialog
open={open}
onOpenChange={async (v) => { onOpenChange={async (v) => {
setOpen(v); setOpen(v);
if (!v) { if (!v) {
await update(null); await update(null);
} }
}} }}
open={open}
> >
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="info" className="flex items-center gap-2"> <Button className="flex items-center gap-2" variant="info">
<CircleHelp />{" "} <CircleHelp />{" "}
<span className="hidden sm:block"> <span className="hidden sm:block">
{t.tipologia_tutorial.btn_title} {t.tipologia_tutorial.btn_title}
@ -78,8 +78,8 @@ export const CTA_TipologiaModal = () => {
<p>{t.tipologia_tutorial.transitorio_desc}</p> <p>{t.tipologia_tutorial.transitorio_desc}</p>
<Button <Button
aria-label={t.tipologia_tutorial.transitorio_cta} aria-label={t.tipologia_tutorial.transitorio_cta}
onClick={async () => await update("Transitorio")}
className="bg-transitorio flex w-full items-center gap-2" className="bg-transitorio flex w-full items-center gap-2"
onClick={async () => await update("Transitorio")}
> >
{t.tipologia_tutorial.transitorio_cta} {t.tipologia_tutorial.transitorio_cta}
@ -93,8 +93,8 @@ export const CTA_TipologiaModal = () => {
<p>{t.tipologia_tutorial.stabile_desc}</p> <p>{t.tipologia_tutorial.stabile_desc}</p>
<Button <Button
aria-label={t.tipologia_tutorial.stabile_cta} aria-label={t.tipologia_tutorial.stabile_cta}
onClick={async () => await update("Stabile")}
className="bg-stabile flex w-full items-center gap-2" className="bg-stabile flex w-full items-center gap-2"
onClick={async () => await update("Stabile")}
> >
{t.tipologia_tutorial.stabile_cta} {t.tipologia_tutorial.stabile_cta}
@ -104,9 +104,9 @@ export const CTA_TipologiaModal = () => {
<Button <Button
aria-label={t.tipologia_tutorial.alone} aria-label={t.tipologia_tutorial.alone}
variant="outline"
onClick={async () => await update(null)}
className="flex w-full items-center gap-2" className="flex w-full items-center gap-2"
onClick={async () => await update(null)}
variant="outline"
> >
{t.tipologia_tutorial.alone} {t.tipologia_tutorial.alone}
</Button> </Button>

View file

@ -34,9 +34,9 @@ export const AnnuncioInteractions = ({
{session.user ? ( {session.user ? (
tipo ? ( tipo ? (
<ServizioInteraction <ServizioInteraction
userId={session.user.id}
tipologia={tipo}
annuncioId={id} annuncioId={id}
tipologia={tipo}
userId={session.user.id}
/> />
) : ( ) : (
<span>Errore</span> <span>Errore</span>
@ -72,12 +72,12 @@ const ServizioInteraction = ({
}) => { }) => {
const { data: servizio, isLoading } = const { data: servizio, isLoading } =
api.servizio.AnnuncioServizioTipologiaMatch.useQuery({ api.servizio.AnnuncioServizioTipologiaMatch.useQuery({
userId,
tipologia,
annuncioId, annuncioId,
tipologia,
userId,
}); });
if (isLoading) return <LoadingButton loading className="w-full" />; if (isLoading) return <LoadingButton className="w-full" loading />;
if ( if (
servizio === undefined || servizio === undefined ||
servizio.status === "invalid" || servizio.status === "invalid" ||
@ -97,10 +97,10 @@ const ServizioInteraction = ({
Annuncio già salvato! Annuncio già salvato!
</div> </div>
<Link <Link
href={`/area-riservata/dashboard`}
aria-label="Vai alla tua area riservata" aria-label="Vai alla tua area riservata"
href={`/area-riservata/dashboard`}
> >
<Button variant="success" className="flex w-full items-center gap-2"> <Button className="flex w-full items-center gap-2" variant="success">
Vai alla tua area riservata <ArrowRight /> Vai alla tua area riservata <ArrowRight />
</Button> </Button>
</Link> </Link>
@ -149,15 +149,15 @@ export const InteressatoButton = ({
return ( return (
<Button <Button
className="flex w-full items-center gap-2"
variant="info"
//size="xl"
aria-label="Sono interessato" aria-label="Sono interessato"
className="flex w-full items-center gap-2"
//size="xl"
onClick={() => { onClick={() => {
add({ add({
annuncioId, annuncioId,
}); });
}} }}
variant="info"
> >
<BadgePlus className="size-6" /> <BadgePlus className="size-6" />
Sono interessato Sono interessato
@ -191,15 +191,15 @@ export const InteressatoButtonBatchV = ({
return ( return (
<Button <Button
className="flex w-full items-center gap-2"
variant="info"
//size="xl"
aria-label="Sono interessato" aria-label="Sono interessato"
className="flex w-full items-center gap-2"
//size="xl"
onClick={() => { onClick={() => {
add({ add({
annuncioId, annuncioId,
}); });
}} }}
variant="info"
> >
<BadgePlus className="size-6" /> <BadgePlus className="size-6" />
Sono interessato Sono interessato

View file

@ -19,8 +19,8 @@ export const ContattoAnnuncio = () => {
<Credenza> <Credenza>
<CredenzaTrigger asChild> <CredenzaTrigger asChild>
<Button <Button
variant="destructive"
className="flex w-full items-center gap-2" className="flex w-full items-center gap-2"
variant="destructive"
> >
<MessageCircleQuestion className="size-6" /> Contattaci <MessageCircleQuestion className="size-6" /> Contattaci
</Button> </Button>

View file

@ -63,9 +63,9 @@ export const CardAnnuncio = ({
id={`card-annuncio-${id}`} id={`card-annuncio-${id}`}
> >
<Link <Link
href={`/annuncio/${codice}`} className="block p-2"
//target="_blank" //target="_blank"
className="block p-2" //duration-700 ease-in-out animate-in fade-in href={`/annuncio/${codice}`} //duration-700 ease-in-out animate-in fade-in
> >
<div className="group relative rounded-xl text-clip"> <div className="group relative rounded-xl text-clip">
{stato === "Trattativa" && ( {stato === "Trattativa" && (
@ -84,24 +84,24 @@ export const CardAnnuncio = ({
<CarouselItem key={`${img}-${idx}`}> <CarouselItem key={`${img}-${idx}`}>
<ImageFlbk <ImageFlbk
alt={t.card.alt_immagine} alt={t.card.alt_immagine}
width={800}
height={400}
src={img}
priority={idx === 0}
className={ className={
"h-56 w-full rounded-xl object-cover outline outline-neutral-200" "h-56 w-full rounded-xl object-cover outline outline-neutral-200"
} }
height={400}
priority={idx === 0}
src={img}
width={800}
/> />
</CarouselItem> </CarouselItem>
))} ))}
</CarouselContent> </CarouselContent>
<div <div
className="block opacity-0 transition-all duration-200 group-hover:opacity-60 disabled:group-hover:opacity-0"
onClick={(e) => e.preventDefault()}
onKeyDown={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()}
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={(e) => e.preventDefault()}
className="block opacity-0 transition-all duration-200 group-hover:opacity-60 disabled:group-hover:opacity-0"
> >
<CarouselPrevious <CarouselPrevious
className="left-2 cursor-pointer" className="left-2 cursor-pointer"
@ -187,11 +187,11 @@ export const CardAnnuncio = ({
<div> <div>
<p className="truncate font-medium"> <p className="truncate font-medium">
{handleConsegna({ {handleConsegna({
aggiornamento: t.card.in_aggiornamento,
consegna: consegna, consegna: consegna,
consegna_da: t.card.consegna_da, consegna_da: t.card.consegna_da,
mesi: t.preferenze.mesi, mesi: t.preferenze.mesi,
subito: t.card.consegna_subito, subito: t.card.consegna_subito,
aggiornamento: t.card.in_aggiornamento,
})} })}
</p> </p>
</div> </div>
@ -220,52 +220,52 @@ export const CarouselAnnuncio = ({
<> <>
<div className="mx-auto my-4 w-full rounded-md text-clip"> <div className="mx-auto my-4 w-full rounded-md text-clip">
<div className="relative"> <div className="relative">
<Carousel opts={{ loop: true, align: "start" }}> <Carousel opts={{ align: "start", loop: true }}>
<CarouselContent> <CarouselContent>
{immagini?.map((img, idx) => ( {immagini?.map((img, idx) => (
<CarouselItem <CarouselItem
key={`${img}-${idx}-cF`}
className={cn( className={cn(
"group relative", "group relative",
immagini && immagini.length === 1 immagini && immagini.length === 1
? "" ? ""
: !single && "md:basis-1/2 lg:basis-1/3", : !single && "md:basis-1/2 lg:basis-1/3",
)} )}
key={`${img}-${idx}-cF`}
> >
<ImageFlbk <ImageFlbk
priority={idx === 0}
alt={img} alt={img}
width={800}
height={400}
onClick={() => handleOpenModal(idx)}
src={img}
className={cn( className={cn(
"h-72 w-full rounded-md object-cover", "h-72 w-full rounded-md object-cover",
immagini.length === 1 && "object-contain", immagini.length === 1 && "object-contain",
)} )}
height={400}
onClick={() => handleOpenModal(idx)}
priority={idx === 0}
src={img}
width={800}
/> />
<Maximize2 <Maximize2
onClick={() => handleOpenModal(idx)}
className="absolute right-2 bottom-2 size-6 rounded-md bg-white/70 stroke-2 p-1 transition-opacity duration-150 group-hover:opacity-100 sm:opacity-0" className="absolute right-2 bottom-2 size-6 rounded-md bg-white/70 stroke-2 p-1 transition-opacity duration-150 group-hover:opacity-100 sm:opacity-0"
onClick={() => handleOpenModal(idx)}
/> />
</CarouselItem> </CarouselItem>
))} ))}
</CarouselContent> </CarouselContent>
<CarouselPrevious <CarouselPrevious
type="button"
className="left-2 cursor-pointer opacity-60 disabled:opacity-0" className="left-2 cursor-pointer opacity-60 disabled:opacity-0"
disabled={!immagini || immagini.length === 1} disabled={!immagini || immagini.length === 1}
type="button"
/> />
<CarouselNext <CarouselNext
type="button"
className="right-2 cursor-pointer opacity-60 disabled:opacity-0" className="right-2 cursor-pointer opacity-60 disabled:opacity-0"
disabled={!immagini || immagini.length === 1} disabled={!immagini || immagini.length === 1}
type="button"
/> />
</Carousel> </Carousel>
</div> </div>
</div> </div>
<Dialog open={openModal} onOpenChange={setOpenModal}> <Dialog onOpenChange={setOpenModal} open={openModal}>
<DialogContent className="flex h-full w-full max-w-full items-center justify-center border-none border-transparent bg-white p-0 md:max-w-[80vw] md:p-6 [&_#dialog-close>svg]:size-8"> <DialogContent className="flex h-full w-full max-w-full items-center justify-center border-none border-transparent bg-white p-0 md:max-w-[80vw] md:p-6 [&_#dialog-close>svg]:size-8">
<DialogHeader className="absolute top-0 right-0 left-0 z-10"> <DialogHeader className="absolute top-0 right-0 left-0 z-10">
<DialogTitle className="sr-only">Image Dialog</DialogTitle> <DialogTitle className="sr-only">Image Dialog</DialogTitle>
@ -277,24 +277,24 @@ export const CarouselAnnuncio = ({
<CarouselItem key={`${img}-${idx}-cD`}> <CarouselItem key={`${img}-${idx}-cD`}>
<Image <Image
alt={`carousel-img-${idx}`} alt={`carousel-img-${idx}`}
width={1920} className="mx-auto h-[90vh] w-full rounded-md object-contain sm:w-[80vw]"
height={1080} height={1080}
src={img} src={img}
className="mx-auto h-[90vh] w-full rounded-md object-contain sm:w-[80vw]" width={1920}
/> />
</CarouselItem> </CarouselItem>
))} ))}
</CarouselContent> </CarouselContent>
<CarouselPrevious <CarouselPrevious
type="button"
className="left-2 cursor-pointer opacity-60 disabled:opacity-0" className="left-2 cursor-pointer opacity-60 disabled:opacity-0"
disabled={!immagini || immagini.length === 1} disabled={!immagini || immagini.length === 1}
type="button"
/> />
<CarouselNext <CarouselNext
type="button"
className="right-2 cursor-pointer opacity-60 disabled:opacity-0" className="right-2 cursor-pointer opacity-60 disabled:opacity-0"
disabled={!immagini || immagini.length === 1} disabled={!immagini || immagini.length === 1}
type="button"
/> />
</Carousel> </Carousel>
</DialogContent> </DialogContent>

View file

@ -128,24 +128,24 @@ export const AllegatiComp = ({
<h1 className="text-xl font-semibold">{t.allegati.da_info}</h1> <h1 className="text-xl font-semibold">{t.allegati.da_info}</h1>
{from_admin && ( {from_admin && (
<FileList <FileList
files={from_admin} delete_onClick={(id) => deleteFile({ fileId: id, userId })}
isAdmin={isAdmin}
selectHandler={(id, filename) => {
setEditData({ id, filename });
setEditOpen(true);
}}
delete_onClick={(id) => deleteFile({ userId, fileId: id })}
download_onClick={(file) => download_onClick={(file) =>
handleDownload({ handleDownload({
file, file,
getToken: async () => await getToken(), getToken: async () => await getToken(),
}) })
} }
files={from_admin}
isAdmin={isAdmin}
selectHandler={(id, filename) => {
setEditData({ filename, id });
setEditOpen(true);
}}
/> />
)} )}
<Separator /> <Separator />
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<UploadModal userId={userId} isAdmin={isAdmin} />{" "} <UploadModal isAdmin={isAdmin} userId={userId} />{" "}
<span className="text-sm">Max 5 MB per file</span> <span className="text-sm">Max 5 MB per file</span>
</div> </div>
<div className="space-y-2 rounded-md border border-orange-500 p-2"> <div className="space-y-2 rounded-md border border-orange-500 p-2">
@ -158,26 +158,26 @@ export const AllegatiComp = ({
{not_from_admin && ( {not_from_admin && (
<FileList <FileList
files={not_from_admin} delete_onClick={(id) => deleteFile({ fileId: id, userId })}
isAdmin={isAdmin}
selectHandler={(id, filename) => {
setEditData({ id, filename });
setEditOpen(true);
}}
delete_onClick={(id) => deleteFile({ userId, fileId: id })}
download_onClick={(file) => download_onClick={(file) =>
handleDownload({ handleDownload({
file, file,
getToken: async () => await getToken(), getToken: async () => await getToken(),
}) })
} }
files={not_from_admin}
isAdmin={isAdmin}
selectHandler={(id, filename) => {
setEditData({ filename, id });
setEditOpen(true);
}}
/> />
)} )}
<EditFileDialog <EditFileDialog
open={editOpen}
onOpenChange={setEditOpen}
data={editData} data={editData}
handleEditSubmit={handleEditSubmit} handleEditSubmit={handleEditSubmit}
onOpenChange={setEditOpen}
open={editOpen}
/> />
</div> </div>
); );
@ -241,35 +241,35 @@ const EditFileDialog = ({
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog onOpenChange={onOpenChange} open={open}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>{t.modifica}</DialogTitle> <DialogTitle>{t.modifica}</DialogTitle>
<DialogDescription className="sr-only">Edit</DialogDescription> <DialogDescription className="sr-only">Edit</DialogDescription>
</DialogHeader> </DialogHeader>
{data && ( {data && (
<form onSubmit={handleEditSubmit} method="post"> <form method="post" onSubmit={handleEditSubmit}>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<label htmlFor="id" className="sr-only"> <label className="sr-only" htmlFor="id">
ID ID
</label> </label>
<Input <Input
type="text"
className="hidden" className="hidden"
name="id" name="id"
value={data.id}
readOnly readOnly
type="text"
value={data.id}
/> />
<label htmlFor="filename" className="sr-only"> <label className="sr-only" htmlFor="filename">
filename filename
</label> </label>
<Input <Input
type="text"
placeholder="Modifica messaggio"
className="mt-0" className="mt-0"
defaultValue={data.filename || undefined}
id="filename" id="filename"
name="filename" name="filename"
defaultValue={data.filename || undefined} placeholder="Modifica messaggio"
type="text"
/> />
<Button type="submit">{t.salva}</Button> <Button type="submit">{t.salva}</Button>
</div> </div>
@ -306,8 +306,8 @@ function FileList({
<div className="divide-y"> <div className="divide-y">
{files.map((file) => ( {files.map((file) => (
<div <div
key={file.id}
className="hover:bg-muted/50 grid grid-cols-12 items-center gap-2 p-4 text-sm transition-colors" className="hover:bg-muted/50 grid grid-cols-12 items-center gap-2 p-4 text-sm transition-colors"
key={file.id}
> >
<div className="col-span-6 flex items-center gap-2 md:col-span-8"> <div className="col-span-6 flex items-center gap-2 md:col-span-8">
<div className="shrink-0"> <div className="shrink-0">
@ -315,9 +315,9 @@ function FileList({
</div> </div>
<Link <Link
aria-label="Allegato" aria-label="Allegato"
className="text-foreground truncate font-medium hover:underline"
href={`/area-riservata/allegato-view/${file.id}`} href={`/area-riservata/allegato-view/${file.id}`}
target="_blank" target="_blank"
className="text-foreground truncate font-medium hover:underline"
> >
{file.filename} {file.filename}
</Link> </Link>
@ -329,16 +329,16 @@ function FileList({
<div className="col-span-2 flex justify-end gap-1 md:col-span-1"> <div className="col-span-2 flex justify-end gap-1 md:col-span-1">
<Button <Button
aria-label="Download" aria-label="Download"
variant="ghost"
size="icon"
asChild asChild
className="size-8" className="size-8"
size="icon"
title="Download" title="Download"
variant="ghost"
> >
<Button <Button
variant="outline"
className="flex gap-1 text-sm" className="flex gap-1 text-sm"
onClick={() => download_onClick(file)} onClick={() => download_onClick(file)}
variant="outline"
> >
<Download className="size-4" /> <Download className="size-4" />
</Button> </Button>
@ -347,7 +347,7 @@ function FileList({
{isAdmin && ( {isAdmin && (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="size-8"> <Button className="size-8" size="icon" variant="ghost">
<MoreVertical className="size-4" /> <MoreVertical className="size-4" />
<span className="sr-only">More options</span> <span className="sr-only">More options</span>
</Button> </Button>
@ -364,8 +364,8 @@ function FileList({
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
aria-label="Elimina" aria-label="Elimina"
onClick={() => delete_onClick(file.id)}
className="text-destructive focus:text-destructive" className="text-destructive focus:text-destructive"
onClick={() => delete_onClick(file.id)}
> >
<Trash2 className="mr-2 size-4" /> <Trash2 className="mr-2 size-4" />
Elimina Elimina

View file

@ -25,32 +25,32 @@ export const EmailAccordion = ({ userId }: { userId: UsersId }) => {
return ( return (
<div className="mx-auto py-4"> <div className="mx-auto py-4">
<Accordion <Accordion
type="single"
collapsible
className="space-y-4" className="space-y-4"
value={openIndex} collapsible
onValueChange={setOpenIndex} onValueChange={setOpenIndex}
type="single"
value={openIndex}
> >
{data.map((email, index) => ( {data.map((email, index) => (
<AccordionItem <AccordionItem
value={String(index)}
key={index}
className="space-y-2 border-none" className="space-y-2 border-none"
key={index}
value={String(index)}
> >
<AccordionTrigger className="rounded-md bg-muted px-4 py-2"> <AccordionTrigger className="rounded-md bg-muted px-4 py-2">
Email &ldquo;{email.title}&rdquo; del{" "} Email &ldquo;{email.title}&rdquo; del{" "}
{new Date(email.created_at).toLocaleDateString("it-IT", { {new Date(email.created_at).toLocaleDateString("it-IT", {
year: "numeric",
month: "2-digit",
day: "2-digit", day: "2-digit",
month: "2-digit",
year: "numeric",
})} })}
</AccordionTrigger> </AccordionTrigger>
<AccordionContent> <AccordionContent>
{openIndex === String(index) && ( {openIndex === String(index) && (
<iframe <iframe
title="Email Content"
srcDoc={email.html}
height={mobile ? "600px" : "500px"} height={mobile ? "600px" : "500px"}
srcDoc={email.html}
title="Email Content"
width="100%" width="100%"
/> />
)} )}

View file

@ -43,20 +43,20 @@ export const AdminDashboard = () => {
</CardContent> </CardContent>
</Card> </Card>
<TimeserieBarChart <TimeserieBarChart
title="Utenti registrati"
timeserie={stats.userTimeserie} timeserie={stats.userTimeserie}
title="Utenti registrati"
/> />
<TimeserieBarChartStacked <TimeserieBarChartStacked
timeserie={stats.serviziTimeserie.data}
title="Servizi creati" title="Servizi creati"
valueALabel={stats.serviziTimeserie.valueALabel} valueALabel={stats.serviziTimeserie.valueALabel}
valueBLabel={stats.serviziTimeserie.valueBLabel} valueBLabel={stats.serviziTimeserie.valueBLabel}
timeserie={stats.serviziTimeserie.data}
/> />
<TimeserieBarChart <TimeserieBarChart
title="Contatti generati"
timeserie={stats.contattiTimeserie} timeserie={stats.contattiTimeserie}
title="Contatti generati"
/> />
</div> </div>
)} )}
@ -79,7 +79,7 @@ export const UserDashboard = ({ userId }: { userId: UsersId }) => {
</div> </div>
<div className="flex flex-col space-y-5 px-2"> <div className="flex flex-col space-y-5 px-2">
<TabRicerca userId={userId} isAdmin={false} /> <TabRicerca isAdmin={false} userId={userId} />
{/* {loadingServizi ? ( {/* {loadingServizi ? (
<LoadingPage /> <LoadingPage />
) : ( ) : (
@ -94,8 +94,8 @@ export const UserDashboard = ({ userId }: { userId: UsersId }) => {
)} */} )} */}
</div> </div>
<AccordionComp <AccordionComp
texts={t.area_riservata.dash_accordion_minifaq}
className="w-full max-w-full px-2 pt-4" className="w-full max-w-full px-2 pt-4"
texts={t.area_riservata.dash_accordion_minifaq}
/> />
</div> </div>
</div> </div>

View file

@ -45,6 +45,9 @@ export const OrdiniModal = ({
}) => { }) => {
const utils = api.useUtils(); const utils = api.useUtils();
const { mutate: remove } = api.servizio.removeOrder.useMutation({ const { mutate: remove } = api.servizio.removeOrder.useMutation({
onError: (error) => {
toast.error(`Errore durante la rimozione ordine: ${error.message}`);
},
onSuccess: async () => { onSuccess: async () => {
toast.success("Ordine rimosso con successo!"); toast.success("Ordine rimosso con successo!");
await utils.servizio.getOrdini.invalidate({ await utils.servizio.getOrdini.invalidate({
@ -52,19 +55,19 @@ export const OrdiniModal = ({
}); });
setModalOpen(false); setModalOpen(false);
}, },
onError: (error) => {
toast.error(`Errore durante la rimozione ordine: ${error.message}`);
},
}); });
const { mutate: update } = api.servizio.updateOrder.useMutation({ const { mutate: update } = api.servizio.updateOrder.useMutation({
onError: (error) => {
toast.error(`Errore durante la modifica: ${error.message}`);
},
onSuccess: async (data) => { onSuccess: async (data) => {
if (data.type === OrderTypeEnum.Acconto && data.isActive) { if (data.type === OrderTypeEnum.Acconto && data.isActive) {
updateServizio({ updateServizio({
servizioId: data.servizio_id,
data: { data: {
isOkAcconto: true,
decorrenza: new Date(), decorrenza: new Date(),
isOkAcconto: true,
}, },
servizioId: data.servizio_id,
}); });
} }
toast.success("Ordine aggiornato con successo!"); toast.success("Ordine aggiornato con successo!");
@ -75,19 +78,16 @@ export const OrdiniModal = ({
ordineId, ordineId,
}); });
}, },
onError: (error) => {
toast.error(`Errore durante la modifica: ${error.message}`);
},
}); });
const { mutate: updateServizio } = api.servizio.updateServizio.useMutation({ const { mutate: updateServizio } = api.servizio.updateServizio.useMutation({
onError: (error) => {
toast.error(error.message);
},
onSuccess: async () => { onSuccess: async () => {
toast.success("Servizio modificato con successo"); toast.success("Servizio modificato con successo");
await utils.servizio.getUserServizi.invalidate(); await utils.servizio.getUserServizi.invalidate();
}, },
onError: (error) => {
toast.error(error.message);
},
}); });
const { data } = api.servizio.getOrdineById.useQuery({ const { data } = api.servizio.getOrdineById.useQuery({
@ -97,10 +97,10 @@ export const OrdiniModal = ({
return ( return (
<Credenza <Credenza
open={open}
onOpenChange={(b) => { onOpenChange={(b) => {
setModalOpen(b); setModalOpen(b);
}} }}
open={open}
> >
<CredenzaContent className="max-h-[90vh] max-w-2xl"> <CredenzaContent className="max-h-[90vh] max-w-2xl">
<CredenzaHeader> <CredenzaHeader>
@ -114,9 +114,9 @@ export const OrdiniModal = ({
<p> <p>
Data Ordine: Data Ordine:
{new Date(data.created_at).toLocaleDateString("it-IT", { {new Date(data.created_at).toLocaleDateString("it-IT", {
year: "numeric",
month: "2-digit",
day: "2-digit", day: "2-digit",
month: "2-digit",
year: "numeric",
})} })}
</p> </p>
<p>Tipologia: {data.type.toString()}</p> <p>Tipologia: {data.type.toString()}</p>
@ -145,7 +145,7 @@ export const OrdiniModal = ({
href={`/area-riservata/payment-recap/${data.ordine_id}`} href={`/area-riservata/payment-recap/${data.ordine_id}`}
target="_blank" target="_blank"
> >
<Button variant="outline" className="flex items-center gap-2"> <Button className="flex items-center gap-2" variant="outline">
Ricevuta di cortesia <ReceiptText /> Ricevuta di cortesia <ReceiptText />
</Button> </Button>
</Link> </Link>
@ -164,7 +164,7 @@ export const OrdiniModal = ({
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="outline" aria-label="Modifica Status"> <Button aria-label="Modifica Status" variant="outline">
Modifica Status Modifica Status
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
@ -172,13 +172,13 @@ export const OrdiniModal = ({
<DropdownMenuLabel>Status</DropdownMenuLabel> <DropdownMenuLabel>Status</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuRadioGroup <DropdownMenuRadioGroup
value={data.isActive ? "true" : "false"}
onValueChange={(v) => { onValueChange={(v) => {
update({ update({
ordineId,
data: { isActive: v === "true" }, data: { isActive: v === "true" },
ordineId,
}); });
}} }}
value={data.isActive ? "true" : "false"}
> >
<DropdownMenuRadioItem value="true"> <DropdownMenuRadioItem value="true">
Attivo Attivo
@ -191,13 +191,13 @@ export const OrdiniModal = ({
</DropdownMenu> </DropdownMenu>
<Confirm <Confirm
title="Elimina ordine"
description="Sei sicuro di voler eliminare l'ordine?" description="Sei sicuro di voler eliminare l'ordine?"
onConfirm={() => { onConfirm={() => {
remove({ ordineId }); remove({ ordineId });
}} }}
title="Elimina ordine"
> >
<Button variant="destructive" aria-label="Elimina Ordine"> <Button aria-label="Elimina Ordine" variant="destructive">
<Trash2 className="size-6" /> <Trash2 className="size-6" />
Elimina Ordine Elimina Ordine
</Button> </Button>

View file

@ -32,13 +32,14 @@ export const Sidebar = ({
return ( return (
<motion.nav <motion.nav
animate={{ width: minimized ? 65 : 192 }}
aria-label="Sidebar Menu"
className={cn( className={cn(
"text-accent-foreground z-30 hidden h-auto w-48 shrink-0 space-y-4 border-r bg-white pt-3 md:block", "text-accent-foreground z-30 hidden h-auto w-48 shrink-0 space-y-4 border-r bg-white pt-3 md:block",
className, className,
minimized && "cursor-pointer", minimized && "cursor-pointer",
//defaultOpen && "w-[65px]", //defaultOpen && "w-[65px]",
)} )}
animate={{ width: minimized ? 65 : 192 }}
onClick={ onClick={
minimized minimized
? (e) => { ? (e) => {
@ -48,7 +49,6 @@ export const Sidebar = ({
} }
: undefined : undefined
} }
aria-label="Sidebar Menu"
> >
<div <div
className={cn( className={cn(
@ -61,9 +61,9 @@ export const Sidebar = ({
)} )}
<Button <Button
aria-label="Toggle Sidebar" aria-label="Toggle Sidebar"
variant="ghost"
onClick={() => toggleSidebar(!minimized)}
className="px-0 text-base hover:bg-transparent focus-visible:bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0" className="px-0 text-base hover:bg-transparent focus-visible:bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0"
onClick={() => toggleSidebar(!minimized)}
variant="ghost"
> >
{minimized ? ( {minimized ? (
<Expand className="size-6" /> <Expand className="size-6" />
@ -76,9 +76,9 @@ export const Sidebar = ({
<div className={cn("px-4 text-lg", minimized && "px-0")}> <div className={cn("px-4 text-lg", minimized && "px-0")}>
<ARMinimizableLinks <ARMinimizableLinks
isAdmin={isAdmin} isAdmin={isAdmin}
setOpen={toggleSidebar}
pathname={pathname}
minimized={minimized} minimized={minimized}
pathname={pathname}
setOpen={toggleSidebar}
/> />
</div> </div>
</motion.nav> </motion.nav>

View file

@ -82,13 +82,13 @@ export const BannerFactory = (bannerData: Banners) => {
bannerData.cta_href && bannerData.cta_href &&
bannerData.cta_icon && ( bannerData.cta_icon && (
<Link <Link
href={bannerData.cta_href}
className="inline-flex items-center gap-x-1 font-semibold underline duration-150 hover:text-indigo-100" className="inline-flex items-center gap-x-1 font-semibold underline duration-150 hover:text-indigo-100"
href={bannerData.cta_href}
> >
{bannerData.cta_text} {bannerData.cta_text}
<IconMatrix <IconMatrix
type={bannerData.cta_icon as IconType}
className="size-4" className="size-4"
type={bannerData.cta_icon as IconType}
/> />
</Link> </Link>
)} )}
@ -96,12 +96,12 @@ export const BannerFactory = (bannerData: Banners) => {
</div> </div>
{bannerData.is_unskippable === false && ( {bannerData.is_unskippable === false && (
<button <button
type="button"
className={cn( className={cn(
"rounded-lg p-2 ring-offset-2 duration-150 hover:bg-indigo-500 focus:ring", "rounded-lg p-2 ring-offset-2 duration-150 hover:bg-indigo-500 focus:ring",
closeHoverColor, closeHoverColor,
)} )}
onClick={onClick} onClick={onClick}
type="button"
> >
<X className="size-5" /> <X className="size-5" />
</button> </button>

View file

@ -45,17 +45,17 @@ export const ChatAttachments = ({
<Credenza onOpenChange={(o) => o && refetch()}> <Credenza onOpenChange={(o) => o && refetch()}>
<CredenzaTrigger asChild> <CredenzaTrigger asChild>
<Button <Button
variant="ghost"
size="icon"
className="relative size-9"
aria-label="Allegati Chat" aria-label="Allegati Chat"
className="relative size-9"
size="icon"
variant="ghost"
> >
{attachments && attachments.length > 0 && ( {attachments && attachments.length > 0 && (
<span className="absolute -top-1 right-0 rounded-full bg-blue-500 px-1 text-xs font-bold text-white"> <span className="absolute -top-1 right-0 rounded-full bg-blue-500 px-1 text-xs font-bold text-white">
{attachments.length} {attachments.length}
</span> </span>
)} )}
<Paperclip size={20} className="text-muted-foreground" /> <Paperclip className="text-muted-foreground" size={20} />
</Button> </Button>
</CredenzaTrigger> </CredenzaTrigger>
<CredenzaContent className="max-h-[90vh]"> <CredenzaContent className="max-h-[90vh]">
@ -92,19 +92,19 @@ export const ChatAttachments = ({
return ( return (
<li <li
key={`${file.filename}-${idx}`}
className="flex items-center justify-between border-b px-3 py-1" className="flex items-center justify-between border-b px-3 py-1"
key={`${file.filename}-${idx}`}
> >
<Link <Link
aria-label="Visualizza Allegato" aria-label="Visualizza Allegato"
href={`/area-riservata/allegato-view/${file.id}`}
target="_blank"
className={cn( className={cn(
buttonVariants({ buttonVariants({
variant: "link", variant: "link",
}), }),
"flex h-8 items-center gap-2 truncate p-0 text-base", "flex h-8 items-center gap-2 truncate p-0 text-base",
)} )}
href={`/area-riservata/allegato-view/${file.id}`}
target="_blank"
> >
{file.ext && <ExtIcon ext={file.ext} />} {file.ext && <ExtIcon ext={file.ext} />}
<span className="truncate">{file.filename}</span> <span className="truncate">{file.filename}</span>
@ -127,13 +127,13 @@ export const ChatAttachments = ({
<CredenzaFooter className="flex flex-row items-center justify-between gap-2"> <CredenzaFooter className="flex flex-row items-center justify-between gap-2">
<Link <Link
aria-label="Visualizza Allegati" aria-label="Visualizza Allegati"
href="/area-riservata/allegati"
className={cn( className={cn(
buttonVariants({ buttonVariants({
variant: "outline", variant: "outline",
}), }),
"relative w-full", "relative w-full",
)} )}
href="/area-riservata/allegati"
> >
<Paperclip /> {t.allegati.i_tuoi_allegati} <Paperclip /> {t.allegati.i_tuoi_allegati}
{attachments && attachments.length > 0 && ( {attachments && attachments.length > 0 && (

View file

@ -96,14 +96,19 @@ export default function ChatBottombar({
return ( return (
<div className="flex min-h-[74px] w-full items-center justify-between gap-2 p-2"> <div className="flex min-h-[74px] w-full items-center justify-between gap-2 p-2">
<div className="flex"> <div className="flex">
<ChatAttachments userData={userData} chatUserData={chatUserData} /> <ChatAttachments chatUserData={chatUserData} userData={userData} />
</div> </div>
<div className="relative w-full"> <div className="relative w-full">
<AutosizeTextarea <AutosizeTextarea
autoComplete="off" autoComplete="off"
value={message} className="bg-background flex h-14 min-h-14 w-full resize-none items-center overflow-hidden rounded-xl border pr-8 ring-0 outline-hidden hover:border-neutral-400 focus:border-neutral-600 focus:ring-0 focus-visible:ring-0 focus-visible:ring-offset-0"
minHeight={56} minHeight={56}
ref={inputRef} name="message"
onBlur={() => setIsFocused(false)}
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => {
setMessage(e.target.value);
}}
onFocus={() => setIsFocused(true)}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") { if (e.key === "Enter") {
e.preventDefault(); e.preventDefault();
@ -114,18 +119,13 @@ export default function ChatBottombar({
} }
} }
}} }}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => {
setMessage(e.target.value);
}}
name="message"
placeholder="Aa" placeholder="Aa"
className="bg-background flex h-14 min-h-14 w-full resize-none items-center overflow-hidden rounded-xl border pr-8 ring-0 outline-hidden hover:border-neutral-400 focus:border-neutral-600 focus:ring-0 focus-visible:ring-0 focus-visible:ring-offset-0" ref={inputRef}
value={message}
/> />
{isMobile && ( {isMobile && (
<div className="absolute top-0 right-2 flex h-full items-center"> <div className="absolute top-0 right-2 flex h-full items-center">
<Popover open={openEmoji} onOpenChange={setOpenEmoji}> <Popover onOpenChange={setOpenEmoji} open={openEmoji}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button variant="ghost"> <Button variant="ghost">
<SmilePlusIcon className="stroke-muted-foreground" /> <SmilePlusIcon className="stroke-muted-foreground" />
@ -133,8 +133,8 @@ export default function ChatBottombar({
</PopoverTrigger> </PopoverTrigger>
<PopoverContent align="end" className="w-fit p-0"> <PopoverContent align="end" className="w-fit p-0">
<EmojiPicker <EmojiPicker
locale={locale as "it" | "en"}
className="h-84" className="h-84"
locale={locale as "it" | "en"}
onEmojiSelect={({ emoji }) => { onEmojiSelect={({ emoji }) => {
setMessage(message + emoji); setMessage(message + emoji);
// if (inputRef.current) { // if (inputRef.current) {
@ -154,16 +154,16 @@ export default function ChatBottombar({
</div> </div>
<Button <Button
aria-label="Send Message" aria-label="Send Message"
variant="ghost"
className="shrink-0" className="shrink-0"
onClick={ onClick={
message.length !== 0 ? () => handleSend() : () => handleThumbsUp() message.length !== 0 ? () => handleSend() : () => handleThumbsUp()
} }
variant="ghost"
> >
{message.length !== 0 ? ( {message.length !== 0 ? (
<SendHorizontal size={20} className="text-muted-foreground" /> <SendHorizontal className="text-muted-foreground" size={20} />
) : ( ) : (
<ThumbsUp size={20} className="text-muted-foreground" /> <ThumbsUp className="text-muted-foreground" size={20} />
)} )}
</Button> </Button>
</div> </div>

View file

@ -9,19 +9,19 @@ import { cn } from "~/lib/utils";
const chatBubbleVariant = cva( const chatBubbleVariant = cva(
"flex gap-2 max-w-[60%] items-end relative group", "flex gap-2 max-w-[60%] items-end relative group",
{ {
defaultVariants: {
layout: "default",
variant: "received",
},
variants: { variants: {
layout: {
ai: "max-w-full w-full items-center",
default: "",
},
variant: { variant: {
received: "self-start", received: "self-start",
sent: "self-end flex-row-reverse", sent: "self-end flex-row-reverse",
}, },
layout: {
default: "",
ai: "max-w-full w-full items-center",
},
},
defaultVariants: {
variant: "received",
layout: "default",
}, },
}, },
); );
@ -34,7 +34,7 @@ const ChatBubble = React.forwardRef<HTMLDivElement, ChatBubbleProps>(
({ className, variant, layout, children, ...props }, ref) => ( ({ className, variant, layout, children, ...props }, ref) => (
<div <div
className={cn( className={cn(
chatBubbleVariant({ variant, layout, className }), chatBubbleVariant({ className, layout, variant }),
"group relative", "group relative",
)} )}
ref={ref} ref={ref}
@ -43,8 +43,8 @@ const ChatBubble = React.forwardRef<HTMLDivElement, ChatBubbleProps>(
{React.Children.map(children, (child) => {React.Children.map(children, (child) =>
React.isValidElement(child) && typeof child.type !== "string" React.isValidElement(child) && typeof child.type !== "string"
? React.cloneElement(child, { ? React.cloneElement(child, {
variant,
layout, layout,
variant,
} as Partial<React.ComponentProps<typeof child.type>>) } as Partial<React.ComponentProps<typeof child.type>>)
: child, : child,
)} )}
@ -67,20 +67,20 @@ export const ChatBubbleReadStatus = ({ isread }: ChatBubbleReadStatusProps) => {
// ChatBubbleMessage // ChatBubbleMessage
const chatBubbleMessageVariants = cva("p-2", { const chatBubbleMessageVariants = cva("p-2", {
defaultVariants: {
layout: "default",
variant: "received",
},
variants: { variants: {
layout: {
ai: "border-t w-full rounded-none bg-transparent",
default: "",
},
variant: { variant: {
received: received:
"bg-secondary text-secondary-foreground rounded-r-lg rounded-tl-lg", "bg-secondary text-secondary-foreground rounded-r-lg rounded-tl-lg",
sent: "bg-primary text-primary-foreground rounded-l-lg rounded-tr-lg", sent: "bg-primary text-primary-foreground rounded-l-lg rounded-tr-lg",
}, },
layout: {
default: "",
ai: "border-t w-full rounded-none bg-transparent",
},
},
defaultVariants: {
variant: "received",
layout: "default",
}, },
}); });
@ -100,7 +100,7 @@ const ChatBubbleMessage = React.forwardRef<
) => ( ) => (
<div <div
className={cn( className={cn(
chatBubbleMessageVariants({ variant, layout, className }), chatBubbleMessageVariants({ className, layout, variant }),
"max-w-full break-words whitespace-pre-wrap", "max-w-full break-words whitespace-pre-wrap",
)} )}
ref={ref} ref={ref}
@ -149,10 +149,10 @@ const ChatBubbleAction: React.FC<ChatBubbleActionProps> = ({
}) => ( }) => (
<Button <Button
aria-label="Chat Bubble Action" aria-label="Chat Bubble Action"
variant={variant}
size={size}
className={className} className={className}
onClick={onClick} onClick={onClick}
size={size}
variant={variant}
{...props} {...props}
> >
{icon} {icon}
@ -173,7 +173,6 @@ const ChatBubbleActionWrapper = React.forwardRef<
<> <>
{show && ( {show && (
<div <div
ref={ref}
className={cn( className={cn(
"absolute top-1/2 flex -translate-y-1/2 opacity-0 transition-opacity duration-200 group-hover:opacity-100", "absolute top-1/2 flex -translate-y-1/2 opacity-0 transition-opacity duration-200 group-hover:opacity-100",
variant === "sent" variant === "sent"
@ -181,6 +180,7 @@ const ChatBubbleActionWrapper = React.forwardRef<
: "-right-1 translate-x-full", : "-right-1 translate-x-full",
className, className,
)} )}
ref={ref}
{...props} {...props}
> >
{children} {children}

View file

@ -79,27 +79,26 @@ export const ChatList = ({
}; };
const { mutate: deleteMutation } = api.messagesSSE.delete.useMutation({ const { mutate: deleteMutation } = api.messagesSSE.delete.useMutation({
onError: () => {
toast.error("Error deleting message", { id: "deleteMessage" });
},
onMutate: () => { onMutate: () => {
toast("Deleting message...", { icon: "🗑️", id: "deleteMessage" }); toast("Deleting message...", { icon: "🗑️", id: "deleteMessage" });
}, },
onSuccess: () => { onSuccess: () => {
toast("Message deleted", { icon: "🗑️", id: "deleteMessage" }); toast("Message deleted", { icon: "🗑️", id: "deleteMessage" });
}, },
onError: () => {
toast.error("Error deleting message", { id: "deleteMessage" });
},
}); });
const { mutate: editMutation } = api.messagesSSE.edit.useMutation({ const { mutate: editMutation } = api.messagesSSE.edit.useMutation({
onError: () => {
toast.error("Error editing message", { id: "editMessage" });
},
onMutate: () => { onMutate: () => {
toast("Editing message...", { icon: "📝", id: "editMessage" }); toast("Editing message...", { icon: "📝", id: "editMessage" });
}, },
onSuccess: () => { onSuccess: () => {
toast("Message edited", { icon: "📝", id: "editMessage" }); toast("Message edited", { icon: "📝", id: "editMessage" });
}, },
onError: () => {
toast.error("Error editing message", { id: "editMessage" });
},
}); });
const handleEditSubmit = useCallback( const handleEditSubmit = useCallback(
@ -112,8 +111,8 @@ export const ChatList = ({
} }
editMutation({ editMutation({
chatId, chatId,
messageId: formData.get("message-id") as MessagesMessageid,
message: formData.get("message-edit") as string, message: formData.get("message-edit") as string,
messageId: formData.get("message-id") as MessagesMessageid,
}); });
setEditModalOpen(false); setEditModalOpen(false);
}, },
@ -141,15 +140,15 @@ export const ChatList = ({
{query.hasNextPage && ( {query.hasNextPage && (
<LoadingButton <LoadingButton
aria-label="Load More Messages" aria-label="Load More Messages"
loading={query.isFetchingNextPage} className="mb-3"
variant="secondary"
key="load-more"
disabled={!query.hasNextPage || query.isFetchingNextPage} disabled={!query.hasNextPage || query.isFetchingNextPage}
key="load-more"
loading={query.isFetchingNextPage}
onClick={async () => { onClick={async () => {
setLocked(true); setLocked(true);
await query.fetchNextPage(); await query.fetchNextPage();
}} }}
className="mb-3" variant="secondary"
> >
{query.isFetchingNextPage {query.isFetchingNextPage
? "Caricamento..." ? "Caricamento..."
@ -175,24 +174,24 @@ export const ChatList = ({
return ( return (
<motion.div <motion.div
key={message.messageid} animate={{ opacity: 1, scale: 1, x: 0, y: 0 }}
layout
initial={{ opacity: 0, scale: 1, y: 50, x: 0 }}
animate={{ opacity: 1, scale: 1, y: 0, x: 0 }}
exit={{ opacity: 0, scale: 1, y: 1, x: 0 }}
transition={{
opacity: { duration: 0.1 },
layout: {
type: "spring",
bounce: 0.3,
duration: 0.3,
},
}}
style={{ originX: 0.5, originY: 0.5 }}
className={cn( className={cn(
"flex flex-col gap-2 pb-4", "flex flex-col gap-2 pb-4",
isNextSameSender && "pb-0.5", isNextSameSender && "pb-0.5",
)} )}
exit={{ opacity: 0, scale: 1, x: 0, y: 1 }}
initial={{ opacity: 0, scale: 1, x: 0, y: 50 }}
key={message.messageid}
layout
style={{ originX: 0.5, originY: 0.5 }}
transition={{
layout: {
bounce: 0.3,
duration: 0.3,
type: "spring",
},
opacity: { duration: 0.1 },
}}
> >
{!isPrevSameDay && ( {!isPrevSameDay && (
<span className="mx-auto rounded-md bg-muted px-2 py-1 text-center text-xs text-muted-foreground"> <span className="mx-auto rounded-md bg-muted px-2 py-1 text-center text-xs text-muted-foreground">
@ -207,7 +206,7 @@ export const ChatList = ({
{isNextSameSender ? ( {isNextSameSender ? (
<div className="size-6"></div> <div className="size-6"></div>
) : ( ) : (
<UserAvatar userId={message.sender} className="size-6" /> <UserAvatar className="size-6" userId={message.sender} />
)} )}
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
@ -225,8 +224,8 @@ export const ChatList = ({
</p> </p>
)} )}
<ChatBubbleMessage <ChatBubbleMessage
isLoading={false}
className={cn(isNextSameSender && "rounded-lg")} className={cn(isNextSameSender && "rounded-lg")}
isLoading={false}
> >
{message.message} {message.message}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -239,9 +238,9 @@ export const ChatList = ({
"it", "it",
{ {
day: "2-digit", day: "2-digit",
month: "short",
hour: "2-digit", hour: "2-digit",
minute: "2-digit", minute: "2-digit",
month: "short",
}, },
)} )}
/> />
@ -252,8 +251,8 @@ export const ChatList = ({
<ChatBubbleActionWrapper show={userData.isAdmin}> <ChatBubbleActionWrapper show={userData.isAdmin}>
<ChatBubbleAction <ChatBubbleAction
className="size-7 hover:bg-red-50 hover:text-red-500" className="size-7 hover:bg-red-50 hover:text-red-500"
key={"delete"}
icon={<Trash2 className="size-4" />} icon={<Trash2 className="size-4" />}
key={"delete"}
onClick={() => { onClick={() => {
setMsgSelected(message); setMsgSelected(message);
handleDeleteOpen(true); handleDeleteOpen(true);
@ -261,8 +260,8 @@ export const ChatList = ({
/> />
<ChatBubbleAction <ChatBubbleAction
className="size-7 hover:bg-blue-50 hover:text-blue-500" className="size-7 hover:bg-blue-50 hover:text-blue-500"
key={"edit"}
icon={<Pencil className="size-4" />} icon={<Pencil className="size-4" />}
key={"edit"}
onClick={() => { onClick={() => {
setMsgSelected(message); setMsgSelected(message);
handleEditOpen(true); handleEditOpen(true);
@ -282,18 +281,18 @@ export const ChatList = ({
</AnimatePresence> </AnimatePresence>
</ChatMessageList> </ChatMessageList>
<EditMessageDialog <EditMessageDialog
open={editModalOpen}
onOpenChange={handleEditOpen}
msgEdit={msgSelected}
handleEditSubmit={handleEditSubmit} handleEditSubmit={handleEditSubmit}
msgEdit={msgSelected}
onOpenChange={handleEditOpen}
open={editModalOpen}
/> />
<DeleteMessageDialog <DeleteMessageDialog
open={deleteModalOpen}
onOpenChange={handleDeleteOpen}
msgDelete={msgSelected}
handleDeleteSubmit={(messageId) => { handleDeleteSubmit={(messageId) => {
deleteMutation({ chatId, messageId }); deleteMutation({ chatId, messageId });
}} }}
msgDelete={msgSelected}
onOpenChange={handleDeleteOpen}
open={deleteModalOpen}
/> />
</div> </div>
); );
@ -316,24 +315,24 @@ const Typers = ({
return ( return (
<motion.div <motion.div
animate={{ opacity: 1, scale: 1, x: 0, y: 0 }}
className="flex flex-col gap-2 py-2"
exit={{ opacity: 0, scale: 1, x: 0, y: 1 }}
initial={{ opacity: 0, scale: 1, x: 0, y: 50 }}
key={`typers${messageLenght}${index}`} key={`typers${messageLenght}${index}`}
layout layout
initial={{ opacity: 0, scale: 1, y: 50, x: 0 }} style={{ originX: 0.5, originY: 0.5 }}
animate={{ opacity: 1, scale: 1, y: 0, x: 0 }}
exit={{ opacity: 0, scale: 1, y: 1, x: 0 }}
transition={{ transition={{
opacity: { duration: 0.1 },
layout: { layout: {
type: "spring",
bounce: 0.3, bounce: 0.3,
duration: 0.3, duration: 0.3,
type: "spring",
}, },
opacity: { duration: 0.1 },
}} }}
style={{ originX: 0.5, originY: 0.5 }}
className="flex flex-col gap-2 py-2"
> >
<ChatBubble variant={variant}> <ChatBubble variant={variant}>
<UserAvatar userId={typer.userId} className="size-6" /> <UserAvatar className="size-6" userId={typer.userId} />
<ChatBubbleMessage isLoading={true}></ChatBubbleMessage> <ChatBubbleMessage isLoading={true}></ChatBubbleMessage>
</ChatBubble> </ChatBubble>
@ -357,36 +356,36 @@ const EditMessageDialog = ({
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog onOpenChange={onOpenChange} open={open}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>{t.modifica}</DialogTitle> <DialogTitle>{t.modifica}</DialogTitle>
<DialogDescription className="sr-only">Edit</DialogDescription> <DialogDescription className="sr-only">Edit</DialogDescription>
</DialogHeader> </DialogHeader>
{msgEdit && ( {msgEdit && (
<form onSubmit={handleEditSubmit} method="post"> <form method="post" onSubmit={handleEditSubmit}>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<label htmlFor="id" className="sr-only"> <label className="sr-only" htmlFor="id">
ID ID
</label> </label>
<Input <Input
id="id"
type="text"
className="hidden" className="hidden"
id="id"
name="message-id" name="message-id"
value={msgEdit.messageid}
readOnly readOnly
type="text"
value={msgEdit.messageid}
/> />
<label htmlFor="message-edit" className="sr-only"> <label className="sr-only" htmlFor="message-edit">
Modifica messaggio Modifica messaggio
</label> </label>
<Input <Input
id="message-edit"
type="text"
placeholder="Modifica messaggio"
className="mt-0" className="mt-0"
name="message-edit"
defaultValue={msgEdit.message || undefined} defaultValue={msgEdit.message || undefined}
id="message-edit"
name="message-edit"
placeholder="Modifica messaggio"
type="text"
/> />
<Button type="submit">{t.salva}</Button> <Button type="submit">{t.salva}</Button>
</div> </div>
@ -412,7 +411,7 @@ const DeleteMessageDialog = ({
return ( return (
<> <>
{msgDelete && ( {msgDelete && (
<AlertDialog open={open} onOpenChange={onOpenChange}> <AlertDialog onOpenChange={onOpenChange} open={open}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Vuoi eliminare il messaggio?</AlertDialogTitle> <AlertDialogTitle>Vuoi eliminare il messaggio?</AlertDialogTitle>

View file

@ -4,18 +4,18 @@ import { cn } from "~/lib/utils";
type ChatMessageListProps = HTMLAttributes<HTMLDivElement>; type ChatMessageListProps = HTMLAttributes<HTMLDivElement>;
const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>( const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
({ className, children, ...props }, ref) => ( ({ className, children, ...props }, ref) => (
<div <div
className={cn( className={cn(
"flex h-full w-full flex-col overflow-y-auto p-4", "flex h-full w-full flex-col overflow-y-auto p-4",
className, className,
)} )}
ref={ref} ref={ref}
{...props} {...props}
> >
{children} {children}
</div> </div>
), ),
); );
ChatMessageList.displayName = "ChatMessageList"; ChatMessageList.displayName = "ChatMessageList";

View file

@ -104,18 +104,18 @@ export const ChatSidebar = ({ chatId }: { chatId: ChatsChatid | null }) => {
{isDesktop ? ( {isDesktop ? (
<div className="relative"> <div className="relative">
<Search className="text-muted-foreground absolute top-[0.8rem] left-2 size-4" /> <Search className="text-muted-foreground absolute top-[0.8rem] left-2 size-4" />
<label htmlFor="search" className="sr-only"> <label className="sr-only" htmlFor="search">
Search Chats Search Chats
</label> </label>
<Input placeholder="Search" className="m-0 pl-8" id="search" /> <Input className="m-0 pl-8" id="search" placeholder="Search" />
</div> </div>
) : ( ) : (
<Button <Button
aria-label="Open Search Dialog" aria-label="Open Search Dialog"
onClick={() => setSearchDialogOpen(true)}
variant="ghost"
size="icon"
className="size-8" className="size-8"
onClick={() => setSearchDialogOpen(true)}
size="icon"
variant="ghost"
> >
<Search size={20} /> <Search size={20} />
</Button> </Button>
@ -123,16 +123,16 @@ export const ChatSidebar = ({ chatId }: { chatId: ChatsChatid | null }) => {
<SearchUserComponent <SearchUserComponent
activeChats={activeChats} activeChats={activeChats}
handleChatSelection={handleChatSelection}
isOpen={searchDialogOpen} isOpen={searchDialogOpen}
OpenChange={setSearchDialogOpen} OpenChange={setSearchDialogOpen}
handleChatSelection={handleChatSelection}
/> />
<Button <Button
aria-label="Open Add User Dialog" aria-label="Open Add User Dialog"
onClick={() => setAddDialogOpen(true)}
variant="ghost"
size="icon"
className="size-8" className="size-8"
onClick={() => setAddDialogOpen(true)}
size="icon"
variant="ghost"
> >
<SquarePen size={20} /> <SquarePen size={20} />
</Button> </Button>
@ -150,21 +150,21 @@ export const ChatSidebar = ({ chatId }: { chatId: ChatsChatid | null }) => {
<ContextMenuTrigger asChild> <ContextMenuTrigger asChild>
<Button <Button
aria-label="Select Chat" aria-label="Select Chat"
onClick={() => handleChatSelection(chat.id)}
className={cn( className={cn(
buttonVariants({ buttonVariants({
variant: getVariant(chat.lastmessage),
size: !isDesktop ? "icon" : "xl", size: !isDesktop ? "icon" : "xl",
variant: getVariant(chat.lastmessage),
}), }),
!isDesktop ? "p-1" : "justify-start gap-4", !isDesktop ? "p-1" : "justify-start gap-4",
"h-fit w-full", "h-fit w-full",
)} )}
onClick={() => handleChatSelection(chat.id)}
> >
<UserAvatar <UserAvatar
username={chat.userinfo.username}
userId={chat.userinfo.id}
className="size-10" className="size-10"
userId={chat.userinfo.id}
username={chat.userinfo.username}
/> />
{isDesktop && ( {isDesktop && (
@ -173,10 +173,10 @@ export const ChatSidebar = ({ chatId }: { chatId: ChatsChatid | null }) => {
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{chat.etichette.map((etichetta) => ( {chat.etichette.map((etichetta) => (
<Etichetta <Etichetta
className="text-xs"
color_hex={etichetta.color_hex}
key={etichetta.id_etichetta} key={etichetta.id_etichetta}
title={etichetta.title} title={etichetta.title}
color_hex={etichetta.color_hex}
className="text-xs"
/> />
))} ))}
</div> </div>
@ -188,10 +188,10 @@ export const ChatSidebar = ({ chatId }: { chatId: ChatsChatid | null }) => {
{chat.lastmessage && {chat.lastmessage &&
new Date(chat.lastmessage.time).toLocaleString("it", { new Date(chat.lastmessage.time).toLocaleString("it", {
day: "2-digit", day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit", hour: "2-digit",
minute: "2-digit", minute: "2-digit",
month: "2-digit",
year: "numeric",
})} })}
</span> </span>
</div> </div>
@ -200,17 +200,17 @@ export const ChatSidebar = ({ chatId }: { chatId: ChatsChatid | null }) => {
</ContextMenuTrigger> </ContextMenuTrigger>
<ChatSidebarContextMenu <ChatSidebarContextMenu
chatid={chat.id} chatid={chat.id}
setSelectedChat={setSelectedChat}
setDeleteModalOpen={setDeleteModalOpen} setDeleteModalOpen={setDeleteModalOpen}
setEditEtichettaModalOpen={setEditEtichettaModalOpen} setEditEtichettaModalOpen={setEditEtichettaModalOpen}
setSelectedChat={setSelectedChat}
/> />
</ContextMenu> </ContextMenu>
))} ))}
{selectedChat && ( {selectedChat && (
<> <>
<AlertDialog <AlertDialog
open={deleteModalOpen}
onOpenChange={setDeleteModalOpen} onOpenChange={setDeleteModalOpen}
open={deleteModalOpen}
> >
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
@ -240,9 +240,9 @@ export const ChatSidebar = ({ chatId }: { chatId: ChatsChatid | null }) => {
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
<EtichetteModal <EtichetteModal
open={editEtichettaModalOpen}
onOpenChange={setEditEtichettaModalOpen}
chatId={selectedChat} chatId={selectedChat}
onOpenChange={setEditEtichettaModalOpen}
open={editEtichettaModalOpen}
/> />
</> </>
)} )}
@ -263,10 +263,10 @@ const SearchUserComponent = ({
handleChatSelection: (chatId: ChatsChatid) => void; handleChatSelection: (chatId: ChatsChatid) => void;
}) => { }) => {
return ( return (
<CommandDialog open={isOpen} onOpenChange={OpenChange}> <CommandDialog onOpenChange={OpenChange} open={isOpen}>
<CommandInput <CommandInput
placeholder="Digita il nome utente..."
className="border-none ring-0 focus:ring-0 focus-visible:ring-0" className="border-none ring-0 focus:ring-0 focus-visible:ring-0"
placeholder="Digita il nome utente..."
/> />
<CommandList> <CommandList>
<CommandEmpty>Nessun risultato</CommandEmpty> <CommandEmpty>Nessun risultato</CommandEmpty>
@ -284,9 +284,9 @@ const SearchUserComponent = ({
> >
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<UserAvatar <UserAvatar
username={chat.userinfo.username}
userId={chat.userinfo.id}
className="size-8" className="size-8"
userId={chat.userinfo.id}
username={chat.userinfo.username}
/> />
<span> <span>
@ -320,10 +320,10 @@ const NewUserComponent = ({
}, },
}); });
return ( return (
<CommandDialog open={isOpen} onOpenChange={OpenChange}> <CommandDialog onOpenChange={OpenChange} open={isOpen}>
<CommandInput <CommandInput
placeholder="Digita il nome utente..."
className="border-none ring-0 focus:ring-0 focus-visible:ring-0" className="border-none ring-0 focus:ring-0 focus-visible:ring-0"
placeholder="Digita il nome utente..."
/> />
<CommandList> <CommandList>
<CommandEmpty>Nessun risultato</CommandEmpty> <CommandEmpty>Nessun risultato</CommandEmpty>
@ -343,9 +343,9 @@ const NewUserComponent = ({
> >
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<UserAvatar <UserAvatar
username={user.username}
userId={user.id}
className="size-8" className="size-8"
userId={user.id}
username={user.username}
/> />
<span> <span>
@ -384,13 +384,13 @@ const ChatSidebarContextMenu = ({
<ContextMenuSubContent className="w-48"> <ContextMenuSubContent className="w-48">
<ContextMenuItem asChild> <ContextMenuItem asChild>
<button <button
type="button"
aria-label="Edit Chat Labels" aria-label="Edit Chat Labels"
className="w-full cursor-pointer"
onClick={() => { onClick={() => {
setSelectedChat(chatid); setSelectedChat(chatid);
setEditEtichettaModalOpen(true); setEditEtichettaModalOpen(true);
}} }}
className="w-full cursor-pointer" type="button"
> >
Modifica Modifica
</button> </button>
@ -401,8 +401,8 @@ const ChatSidebarContextMenu = ({
<ContextMenuItem> <ContextMenuItem>
<Link <Link
aria-label="View User Profile" aria-label="View User Profile"
target="_blank"
href={`/area-riservata/admin/user-view/edit-user/${chatData.user_ref}`} href={`/area-riservata/admin/user-view/edit-user/${chatData.user_ref}`}
target="_blank"
> >
Profilo Profilo
</Link> </Link>
@ -411,8 +411,8 @@ const ChatSidebarContextMenu = ({
<ContextMenuItem> <ContextMenuItem>
<Link <Link
aria-label="View User Orders" aria-label="View User Orders"
target="_blank"
href={`/area-riservata/admin/user-view/ordini/${chatData.user_ref}`} href={`/area-riservata/admin/user-view/ordini/${chatData.user_ref}`}
target="_blank"
> >
Ordini Ordini
</Link> </Link>
@ -420,8 +420,8 @@ const ChatSidebarContextMenu = ({
<ContextMenuItem> <ContextMenuItem>
<Link <Link
aria-label="View User Attachments" aria-label="View User Attachments"
target="_blank"
href={`/area-riservata/admin/user-view/allegati/${chatData.user_ref}`} href={`/area-riservata/admin/user-view/allegati/${chatData.user_ref}`}
target="_blank"
> >
Allegati Allegati
</Link> </Link>
@ -431,11 +431,11 @@ const ChatSidebarContextMenu = ({
<ContextMenuSubContent className="w-48"> <ContextMenuSubContent className="w-48">
<ContextMenuItem <ContextMenuItem
aria-label="Edit Chat" aria-label="Edit Chat"
className="cursor-pointer rounded-md bg-red-500 text-white hover:bg-red-500/90 focus:bg-red-500/90 focus:text-white"
onClick={() => { onClick={() => {
setSelectedChat(chatid); setSelectedChat(chatid);
setDeleteModalOpen(true); setDeleteModalOpen(true);
}} }}
className="cursor-pointer rounded-md bg-red-500 text-white hover:bg-red-500/90 focus:bg-red-500/90 focus:text-white"
> >
Elimina Chat Elimina Chat
</ContextMenuItem> </ContextMenuItem>

View file

@ -88,17 +88,17 @@ export default function ChatTopbar({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{!userData.isAdmin ? ( {!userData.isAdmin ? (
<Image <Image
src={"/favicon/favicon.png"}
alt="Infoalloggi logo" alt="Infoalloggi logo"
width={40}
height={40}
className="size-10 rounded-full" className="size-10 rounded-full"
height={40}
src={"/favicon/favicon.png"}
width={40}
/> />
) : ( ) : (
<UserAvatar <UserAvatar
className="size-10" className="size-10"
username={chatUserData.username}
userId={chatUserData.id} userId={chatUserData.id}
username={chatUserData.username}
/> />
)} )}
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
@ -118,8 +118,8 @@ export default function ChatTopbar({
{userData.isAdmin && ( {userData.isAdmin && (
<AdminPopover <AdminPopover
chatUserData={chatUserData} chatUserData={chatUserData}
handleSwapUser={handleSwapUser}
handleDeleteChat={handleDeleteChat} handleDeleteChat={handleDeleteChat}
handleSwapUser={handleSwapUser}
/> />
)} )}
</div> </div>
@ -137,7 +137,7 @@ const UserStatus = ({
<div className="flex flex-row items-center gap-2"> <div className="flex flex-row items-center gap-2">
{onlineStatus.map((user, idx) => {onlineStatus.map((user, idx) =>
user.isAdmin ? ( user.isAdmin ? (
<TooltipProvider key={idx} delayDuration={0}> <TooltipProvider delayDuration={0} key={idx}>
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger>
<span <span
@ -145,7 +145,7 @@ const UserStatus = ({
"flex flex-row items-center text-xs font-bold text-green-500", "flex flex-row items-center text-xs font-bold text-green-500",
)} )}
> >
<ShieldUser size={15} className="mr-[0.2rem]" /> <ShieldUser className="mr-[0.2rem]" size={15} />
<span> <span>
{user.nome} {user.nome}
{idx < onlineStatus.length - 1 && ","} {idx < onlineStatus.length - 1 && ","}
@ -159,8 +159,8 @@ const UserStatus = ({
</TooltipProvider> </TooltipProvider>
) : ( ) : (
<span <span
key={idx}
className={cn("flex flex-row text-xs font-bold text-green-500")} className={cn("flex flex-row text-xs font-bold text-green-500")}
key={idx}
> >
<span> <span>
{user.nome} {user.nome}
@ -185,12 +185,12 @@ const AdminPopover = ({
<Popover> <Popover>
<PopoverTrigger <PopoverTrigger
className={cn( className={cn(
buttonVariants({ variant: "outline", size: "icon" }), buttonVariants({ size: "icon", variant: "outline" }),
"size-10", "size-10",
"dark:bg-muted dark:text-muted-foreground dark:hover:bg-muted dark:hover:text-white", "dark:bg-muted dark:text-muted-foreground dark:hover:bg-muted dark:hover:text-white",
)} )}
> >
<Info size={24} className="text-muted-foreground" /> <Info className="text-muted-foreground" size={24} />
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="mr-4 w-fit px-4"> <PopoverContent className="mr-4 w-fit px-4">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
@ -231,8 +231,8 @@ const AdminPopover = ({
buttonVariants({ variant: "outline" }), buttonVariants({ variant: "outline" }),
"justify-start gap-2", "justify-start gap-2",
)} )}
type="button"
onClick={handleSwapUser} onClick={handleSwapUser}
type="button"
> >
<User /> Login <User /> Login
</button> </button>

View file

@ -21,24 +21,24 @@ const Chat = memo(
<div className="shrink-0"> <div className="shrink-0">
<ChatTopbar <ChatTopbar
chatId={chatId} chatId={chatId}
userData={userData}
chatUserData={userinfo} chatUserData={userinfo}
onlineStatus={liveChat.onlineUsers} onlineStatus={liveChat.onlineUsers}
userData={userData}
/> />
</div> </div>
<ChatList <ChatList
chatId={chatId} chatId={chatId}
userData={userData}
messages={liveChat.messages} messages={liveChat.messages}
query={liveChat.query} query={liveChat.query}
typingStatus={liveChat.typingStatus} typingStatus={liveChat.typingStatus}
userData={userData}
/> />
<div className="shrink-0 border-t"> <div className="shrink-0 border-t">
<ChatBottombar <ChatBottombar
chatId={chatId} chatId={chatId}
userData={userData}
chatUserData={userinfo} chatUserData={userinfo}
userData={userData}
/> />
</div> </div>
</div> </div>

View file

@ -2,43 +2,43 @@
export default function MessageLoading() { export default function MessageLoading() {
return ( return (
<svg <svg
width="24" className="text-foreground"
height="24" height="24"
viewBox="0 0 24 24" viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
className="text-foreground"
> >
<title>Loading</title> <title>Loading</title>
<circle cx="4" cy="12" r="2" fill="currentColor"> <circle cx="4" cy="12" fill="currentColor" r="2">
<animate <animate
id="spinner_qFRN" attributeName="cy"
begin="0;spinner_OcgL.end+0.25s" begin="0;spinner_OcgL.end+0.25s"
attributeName="cy"
calcMode="spline" calcMode="spline"
dur="0.6s" dur="0.6s"
values="12;6;12" id="spinner_qFRN"
keySplines=".33,.66,.66,1;.33,0,.66,.33" keySplines=".33,.66,.66,1;.33,0,.66,.33"
values="12;6;12"
/> />
</circle> </circle>
<circle cx="12" cy="12" r="2" fill="currentColor"> <circle cx="12" cy="12" fill="currentColor" r="2">
<animate <animate
attributeName="cy"
begin="spinner_qFRN.begin+0.1s" begin="spinner_qFRN.begin+0.1s"
attributeName="cy"
calcMode="spline" calcMode="spline"
dur="0.6s" dur="0.6s"
values="12;6;12"
keySplines=".33,.66,.66,1;.33,0,.66,.33" keySplines=".33,.66,.66,1;.33,0,.66,.33"
values="12;6;12"
/> />
</circle> </circle>
<circle cx="20" cy="12" r="2" fill="currentColor"> <circle cx="20" cy="12" fill="currentColor" r="2">
<animate <animate
id="spinner_OcgL"
begin="spinner_qFRN.begin+0.2s"
attributeName="cy" attributeName="cy"
begin="spinner_qFRN.begin+0.2s"
calcMode="spline" calcMode="spline"
dur="0.6s" dur="0.6s"
values="12;6;12" id="spinner_OcgL"
keySplines=".33,.66,.66,1;.33,0,.66,.33" keySplines=".33,.66,.66,1;.33,0,.66,.33"
values="12;6;12"
/> />
</circle> </circle>
</svg> </svg>

View file

@ -26,13 +26,13 @@ export const CodiceBox = ({
return ( return (
<AutocompleteSearchBox <AutocompleteSearchBox
key={locale}
options={codici || []}
loading={loading}
className={className} className={className}
optionBoxClassName={optionBoxClassName}
optionsClassName={optionsClassName}
containerClassName={containerClassName} containerClassName={containerClassName}
key={locale}
loading={loading}
optionBoxClassName={optionBoxClassName}
options={codici || []}
optionsClassName={optionsClassName}
/> />
); );
}; };
@ -77,7 +77,7 @@ const AutocompleteSearchBox = ({
}, [searchValue, options]); }, [searchValue, options]);
return ( return (
<div ref={wrapperRef} className="z-[5000] w-full"> <div className="z-[5000] w-full" ref={wrapperRef}>
<div <div
className={cn( className={cn(
"relative flex w-full items-center justify-center rounded-md bg-white", "relative flex w-full items-center justify-center rounded-md bg-white",
@ -85,42 +85,42 @@ const AutocompleteSearchBox = ({
)} )}
id="searchContainer" id="searchContainer"
> >
<label htmlFor="search" className="sr-only"> <label className="sr-only" htmlFor="search">
Search Box Search Box
</label> </label>
<Input <Input
className={cn(
"placeholder:text-primary search-cancel:brightness-75 search-cancel:grayscale z-10 cursor-pointer rounded-md bg-transparent text-xl placeholder:font-bold",
className,
)}
id="search" id="search"
type="search" onBlur={() => {
value={searchValue} setIsFocused(false);
}}
onChange={(e) => setSearchValue(e.target.value)} onChange={(e) => setSearchValue(e.target.value)}
onFocus={() => { onFocus={() => {
setOpen(true); setOpen(true);
setIsFocused(true); setIsFocused(true);
}} }}
onBlur={() => {
setIsFocused(false);
}}
onKeyDown={async (e) => { onKeyDown={async (e) => {
if (e.key === "Enter" && results[0] && results.length > 0) { if (e.key === "Enter" && results[0] && results.length > 0) {
setNavigationEnter(true); setNavigationEnter(true);
await router.push(`/annuncio/${results[0].trim()}`); await router.push(`/annuncio/${results[0].trim()}`);
} }
}} }}
className={cn( type="search"
"placeholder:text-primary search-cancel:brightness-75 search-cancel:grayscale z-10 cursor-pointer rounded-md bg-transparent text-xl placeholder:font-bold", value={searchValue}
className,
)}
/> />
<MovingText hidden={isFocused || searchValue !== ""} /> <MovingText hidden={isFocused || searchValue !== ""} />
</div> </div>
{open && ( {open && (
<div <div
ref={listRef}
className={cn( className={cn(
"border-foreground ring-foreground absolute z-50 mt-1 max-h-64 w-full overflow-hidden rounded-md border bg-white shadow-xs ring-1", "border-foreground ring-foreground absolute z-50 mt-1 max-h-64 w-full overflow-hidden rounded-md border bg-white shadow-xs ring-1",
optionBoxClassName, optionBoxClassName,
)} )}
id="searchOptionsBox" id="searchOptionsBox"
ref={listRef}
> >
<ul <ul
className={cn( className={cn(
@ -150,11 +150,11 @@ const AutocompleteSearchBox = ({
{results.map((item) => ( {results.map((item) => (
<li key={item}> <li key={item}>
<Link <Link
href={`/annuncio/${item}`}
className={cn( className={cn(
"flex w-full cursor-pointer items-center justify-between border-b border-neutral-200 bg-white px-4 py-2 text-xl text-neutral-500 duration-150 hover:bg-neutral-100 hover:text-neutral-600 hover:underline hover:underline-offset-2", "flex w-full cursor-pointer items-center justify-between border-b border-neutral-200 bg-white px-4 py-2 text-xl text-neutral-500 duration-150 hover:bg-neutral-100 hover:text-neutral-600 hover:underline hover:underline-offset-2",
optionsClassName, optionsClassName,
)} )}
href={`/annuncio/${item}`}
> >
{item} {item}
</Link> </Link>
@ -189,10 +189,10 @@ const MovingText = ({ hidden }: { hidden: boolean }) => {
return ( return (
<StaggeredFade <StaggeredFade
key={randomFrase}
text={randomFrase}
className="absolute text-2xl font-bold dark:text-neutral-800" className="absolute text-2xl font-bold dark:text-neutral-800"
hide={hidden} hide={hidden}
key={randomFrase}
text={randomFrase}
/> />
); );
}; };

View file

@ -7,34 +7,34 @@ export const ComeFunziona = () => {
const features = [ const features = [
{ {
image: "/come_funziona_ricerca.jpg",
icon: <Search className="size-8" />,
title: t.come_funziona.features.ricerca.title,
desc: t.come_funziona.features.ricerca.desc, desc: t.come_funziona.features.ricerca.desc,
icon: <Search className="size-8" />,
image: "/come_funziona_ricerca.jpg",
title: t.come_funziona.features.ricerca.title,
}, },
{ {
image: "/come_funziona_aggiungi.jpg",
icon: <HeartPlus className="size-8" />,
title: t.come_funziona.features.aggiungi.title,
desc: t.come_funziona.features.aggiungi.desc, desc: t.come_funziona.features.aggiungi.desc,
icon: <HeartPlus className="size-8" />,
image: "/come_funziona_aggiungi.jpg",
title: t.come_funziona.features.aggiungi.title,
}, },
{ {
image: "/come_funziona_meet.jpg",
icon: <Handshake className="size-8" />,
title: t.come_funziona.features.contatta.title,
desc: t.come_funziona.features.contatta.desc, desc: t.come_funziona.features.contatta.desc,
icon: <Handshake className="size-8" />,
image: "/come_funziona_meet.jpg",
title: t.come_funziona.features.contatta.title,
}, },
{ {
image: "/come_funziona_confirm.jpg",
icon: <Home className="size-8" />,
title: t.come_funziona.features.conferma.title,
desc: t.come_funziona.features.conferma.desc, desc: t.come_funziona.features.conferma.desc,
icon: <Home className="size-8" />,
image: "/come_funziona_confirm.jpg",
title: t.come_funziona.features.conferma.title,
}, },
{ {
image: "/come_funziona_contratto.jpg",
icon: <ScrollText className="size-8" />,
title: t.come_funziona.features.firma.title,
desc: t.come_funziona.features.firma.desc, desc: t.come_funziona.features.firma.desc,
icon: <ScrollText className="size-8" />,
image: "/come_funziona_contratto.jpg",
title: t.come_funziona.features.firma.title,
}, },
]; ];
return ( return (
@ -45,16 +45,16 @@ export const ComeFunziona = () => {
<div className="mx-auto mt-8 w-full space-y-12 md:mt-16"> <div className="mx-auto mt-8 w-full space-y-12 md:mt-16">
{features.map((feature, idx) => ( {features.map((feature, idx) => (
<div <div
key={idx}
className="flex flex-col items-center gap-x-8 gap-y-6 lg:flex-row lg:odd:flex-row-reverse" className="flex flex-col items-center gap-x-8 gap-y-6 lg:flex-row lg:odd:flex-row-reverse"
key={idx}
> >
<div className="flex shrink-0 basis-1/2 justify-center"> <div className="flex shrink-0 basis-1/2 justify-center">
<Image <Image
alt={`img${idx}`}
className="border-border/50 aspect-[6/4] rounded-xl border" className="border-border/50 aspect-[6/4] rounded-xl border"
height={200}
src={feature.image} src={feature.image}
width={400} width={400}
height={200}
alt={`img${idx}`}
/> />
</div> </div>
<div className="flex basis-1/2 flex-col"> <div className="flex basis-1/2 flex-col">

View file

@ -37,7 +37,7 @@ export const Confirm = ({
confirmText?: string; confirmText?: string;
}) => { }) => {
return ( return (
<AlertDialog open={open} onOpenChange={setOpen}> <AlertDialog onOpenChange={setOpen} open={open}>
{onlyHedless ? null : ( {onlyHedless ? null : (
<AlertDialogTrigger asChild>{children}</AlertDialogTrigger> <AlertDialogTrigger asChild>{children}</AlertDialogTrigger>
)} )}

View file

@ -20,24 +20,24 @@ export const Counter = ({
}; };
return ( return (
<div className="w-[9.5rem]"> <div className="w-[9.5rem]">
<label htmlFor={name} className="sr-only"> <label className="sr-only" htmlFor={name}>
{name} {name}
</label> </label>
<div className="flex items-center rounded-md border px-1"> <div className="flex items-center rounded-md border px-1">
<button <button
type="button"
onClick={() => handleChange(-1)}
className="flex size-10 items-center justify-center leading-10 transition hover:opacity-75 disabled:opacity-50" className="flex size-10 items-center justify-center leading-10 transition hover:opacity-75 disabled:opacity-50"
disabled={disabled} disabled={disabled}
onClick={() => handleChange(-1)}
type="button"
> >
<Minus /> <Minus />
</button> </button>
<Input <Input
type="number" className="no-spinner h-10 w-16 rounded-md border-none text-center shadow-none disabled:cursor-default sm:text-sm"
disabled={disabled}
id={name} id={name}
value={value}
onChange={(e) => { onChange={(e) => {
if (!Number.isNaN(Number.parseInt(e.target.value))) { if (!Number.isNaN(Number.parseInt(e.target.value))) {
setValue(Number.parseInt(e.target.value)); setValue(Number.parseInt(e.target.value));
@ -45,15 +45,15 @@ export const Counter = ({
e.target.value = String(value); e.target.value = String(value);
} }
}} }}
disabled={disabled} type="number"
className="no-spinner h-10 w-16 rounded-md border-none text-center shadow-none disabled:cursor-default sm:text-sm" value={value}
/> />
<button <button
type="button"
onClick={() => handleChange(1)}
className="flex size-10 items-center justify-center leading-10 transition hover:opacity-75 disabled:opacity-50" className="flex size-10 items-center justify-center leading-10 transition hover:opacity-75 disabled:opacity-50"
disabled={disabled} disabled={disabled}
onClick={() => handleChange(1)}
type="button"
> >
<Plus /> <Plus />
</button> </button>

View file

@ -46,15 +46,15 @@ export const AnimatedButton = forwardRef<
const sparklesAnimation: AnimationSequence = sparkles.map((_, index) => [ const sparklesAnimation: AnimationSequence = sparkles.map((_, index) => [
`.sparkle-${index}`, `.sparkle-${index}`,
{ {
opacity: 1,
scale: randomNumberBetween(sparklesScale[0], sparklesScale[1]),
x: randomNumberBetween(-sparklesRadius, sparklesRadius), x: randomNumberBetween(-sparklesRadius, sparklesRadius),
y: randomNumberBetween(-sparklesRadius, sparklesRadius), y: randomNumberBetween(-sparklesRadius, sparklesRadius),
scale: randomNumberBetween(sparklesScale[0], sparklesScale[1]),
opacity: 1,
}, },
{ {
duration: 0.4,
at: "<", at: "<",
delay: index * 0.01, delay: index * 0.01,
duration: 0.4,
}, },
]); ]);
@ -65,8 +65,8 @@ export const AnimatedButton = forwardRef<
scale: 0, scale: 0,
}, },
{ {
duration: 0.3,
at: "<", at: "<",
duration: 0.3,
}, },
]); ]);
@ -83,7 +83,7 @@ export const AnimatedButton = forwardRef<
animate([ animate([
...sparklesReset, ...sparklesReset,
["button", { scale: 0.8 }, { duration: 0.1, at: "<" }], ["button", { scale: 0.8 }, { at: "<", duration: 0.1 }],
["button", { scale: 1 }, { duration: 0.1 }], ["button", { scale: 1 }, { duration: 0.1 }],
...sparklesAnimation, ...sparklesAnimation,
["button", { duration: 0.000001 }], ["button", { duration: 0.000001 }],
@ -96,11 +96,11 @@ export const AnimatedButton = forwardRef<
})); }));
return ( return (
<div ref={scope} className="w-full"> <div className="w-full" ref={scope}>
<Button <Button
{...props} {...props}
className={cn("relative", props.className)}
aria-label="Animated Button" aria-label="Animated Button"
className={cn("relative", props.className)}
> >
{props.children} {props.children}
<span <span
@ -109,12 +109,12 @@ export const AnimatedButton = forwardRef<
> >
{Array.from({ length: sparklesNumber }).map((_, index) => ( {Array.from({ length: sparklesNumber }).map((_, index) => (
<IconMatrix <IconMatrix
type={sparklesIcon}
className={cn( className={cn(
`absolute top-1/2 left-1/2 opacity-0 sparkle-${index}`, `absolute top-1/2 left-1/2 opacity-0 sparkle-${index}`,
sparklesClassName, sparklesClassName,
)} )}
key={index} key={index}
type={sparklesIcon}
/> />
))} ))}
</span> </span>

View file

@ -32,9 +32,9 @@ export function AnimatedNumber({
onAnimationComplete, onAnimationComplete,
}: AnimatedNumberProps) { }: AnimatedNumberProps) {
const spring = useSpring(value, { const spring = useSpring(value, {
damping,
mass, mass,
stiffness, stiffness,
damping,
}); });
const display: MotionValue<string> = useTransform(spring, (current) => const display: MotionValue<string> = useTransform(spring, (current) =>

View file

@ -77,18 +77,18 @@ export const AutosizeTextarea = forwardRef<
const [triggerAutoSize, setTriggerAutoSize] = useState(""); const [triggerAutoSize, setTriggerAutoSize] = useState("");
useAutosizeTextArea({ useAutosizeTextArea({
textAreaRef: textAreaRef.current,
triggerAutoSize: triggerAutoSize,
maxHeight, maxHeight,
minHeight, minHeight,
textAreaRef: textAreaRef.current,
triggerAutoSize: triggerAutoSize,
}); });
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
// biome-ignore lint/style/noNonNullAssertion: <it exists>
textArea: textAreaRef.current!,
focus: () => textAreaRef.current?.focus(), focus: () => textAreaRef.current?.focus(),
maxHeight, maxHeight,
minHeight, minHeight,
// biome-ignore lint/style/noNonNullAssertion: <it exists>
textArea: textAreaRef.current!,
})); }));
useEffect(() => { useEffect(() => {
@ -100,8 +100,6 @@ export const AutosizeTextarea = forwardRef<
return ( return (
<textarea <textarea
{...props} {...props}
value={value}
ref={textAreaRef}
className={cn( className={cn(
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50", "border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className, className,
@ -110,6 +108,8 @@ export const AutosizeTextarea = forwardRef<
setTriggerAutoSize(e.target.value); setTriggerAutoSize(e.target.value);
onChange?.(e); onChange?.(e);
}} }}
ref={textAreaRef}
value={value}
/> />
); );
}, },

View file

@ -83,23 +83,23 @@ export function DataTable<TData, TValue>({
}, [rowSelection, columnFilters, columnVisibility, sorting, onStateChange]); }, [rowSelection, columnFilters, columnVisibility, sorting, onStateChange]);
const table = useReactTable({ const table = useReactTable({
data,
columns, columns,
onRowSelectionChange: setRowSelection, data,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(), getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(), getSortedRowModel: getSortedRowModel(),
onColumnFiltersChange: setColumnFilters, onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility, onColumnVisibilityChange: setColumnVisibility,
onGlobalFilterChange: setGlobalFilter, onGlobalFilterChange: setGlobalFilter,
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
state: { state: {
globalFilter,
sorting,
columnFilters, columnFilters,
columnVisibility, columnVisibility,
globalFilter,
rowSelection, rowSelection,
sorting,
}, },
}); });
@ -107,8 +107,8 @@ export function DataTable<TData, TValue>({
const { rows } = table.getRowModel(); const { rows } = table.getRowModel();
const virtualizer = useVirtualizer({ const virtualizer = useVirtualizer({
count: rows.length, count: rows.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 34, estimateSize: () => 34,
getScrollElement: () => parentRef.current,
overscan: 20, overscan: 20,
}); });
@ -121,11 +121,11 @@ export function DataTable<TData, TValue>({
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<DataTableToolbar <DataTableToolbar
table={table}
pinnedColumns={pinnedFiltri}
searchColumn={searchColumn}
columns_titles={columns_titles} columns_titles={columns_titles}
globalFilter={globalFilter} globalFilter={globalFilter}
pinnedColumns={pinnedFiltri}
searchColumn={searchColumn}
table={table}
/> />
<div className="rounded-md border" ref={parentRef}> <div className="rounded-md border" ref={parentRef}>
<Table> <Table>
@ -154,25 +154,25 @@ export function DataTable<TData, TValue>({
const row = rows[virtualRow.index]!; const row = rows[virtualRow.index]!;
return ( return (
<TableRow <TableRow
className={cn(
rowOnClick && "cursor-pointer",
highlightedRow === row.id && "bg-neutral-100/50",
"group/row",
)}
data-row-id={row.id}
data-state={row.getIsSelected() && "selected"}
key={row.id}
onClick={ onClick={
rowOnClick rowOnClick
? () => rowOnClick(row.original, row.id) ? () => rowOnClick(row.original, row.id)
: undefined : undefined
} }
key={row.id}
data-state={row.getIsSelected() && "selected"}
data-row-id={row.id}
style={{ style={{
height: `${virtualRow.size}px`, height: `${virtualRow.size}px`,
transform: `translateY(${ transform: `translateY(${
virtualRow.start - index * virtualRow.size virtualRow.start - index * virtualRow.size
}px)`, }px)`,
}} }}
className={cn(
rowOnClick && "cursor-pointer",
highlightedRow === row.id && "bg-neutral-100/50",
"group/row",
)}
> >
{row.getVisibleCells().map((cell) => { {row.getVisibleCells().map((cell) => {
return ( return (
@ -190,8 +190,8 @@ export function DataTable<TData, TValue>({
) : ( ) : (
<TableRow> <TableRow>
<TableCell <TableCell
colSpan={columns.length}
className="h-24 text-center" className="h-24 text-center"
colSpan={columns.length}
> >
No results. No results.
</TableCell> </TableCell>
@ -201,7 +201,7 @@ export function DataTable<TData, TValue>({
</Table> </Table>
</div> </div>
{pagination ? ( {pagination ? (
<DataTablePagination table={table} hasSelectRow={hasSelectRow} /> <DataTablePagination hasSelectRow={hasSelectRow} table={table} />
) : null} ) : null}
</div> </div>
); );

View file

@ -41,23 +41,23 @@ export function DataTableFacetedFilter<TData, TValue>({
return ( return (
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button variant="outline" size="sm" className="h-8 border-dashed"> <Button className="h-8 border-dashed" size="sm" variant="outline">
<CirclePlus className="mr-2 size-4" /> <CirclePlus className="mr-2 size-4" />
{title} {title}
{selectedValues?.size > 0 && ( {selectedValues?.size > 0 && (
<> <>
<Separator orientation="vertical" className="mx-2 h-4" /> <Separator className="mx-2 h-4" orientation="vertical" />
<Badge <Badge
variant="secondary"
className="rounded-xs px-1 font-normal lg:hidden" className="rounded-xs px-1 font-normal lg:hidden"
variant="secondary"
> >
{selectedValues.size} {selectedValues.size}
</Badge> </Badge>
<div className="hidden space-x-1 lg:flex"> <div className="hidden space-x-1 lg:flex">
{selectedValues.size > 2 ? ( {selectedValues.size > 2 ? (
<Badge <Badge
variant="secondary"
className="rounded-xs px-1 font-normal" className="rounded-xs px-1 font-normal"
variant="secondary"
> >
{selectedValues.size} selected {selectedValues.size} selected
</Badge> </Badge>
@ -66,9 +66,9 @@ export function DataTableFacetedFilter<TData, TValue>({
.filter((option) => selectedValues.has(option.value)) .filter((option) => selectedValues.has(option.value))
.map((option) => ( .map((option) => (
<Badge <Badge
variant="secondary"
key={option.value}
className="rounded-xs px-1 font-normal" className="rounded-xs px-1 font-normal"
key={option.value}
variant="secondary"
> >
{option.label} {option.label}
</Badge> </Badge>
@ -79,7 +79,7 @@ export function DataTableFacetedFilter<TData, TValue>({
)} )}
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-[200px] p-0" align="start"> <PopoverContent align="start" className="w-[200px] p-0">
<Command> <Command>
<CommandInput placeholder={title} /> <CommandInput placeholder={title} />
<CommandList> <CommandList>
@ -130,8 +130,8 @@ export function DataTableFacetedFilter<TData, TValue>({
<CommandSeparator /> <CommandSeparator />
<CommandGroup> <CommandGroup>
<CommandItem <CommandItem
onSelect={() => column?.setFilterValue(undefined)}
className="justify-center text-center" className="justify-center text-center"
onSelect={() => column?.setFilterValue(undefined)}
> >
Reset filtri Reset filtri
</CommandItem> </CommandItem>

View file

@ -30,9 +30,9 @@ export function DataTableViewOptions<TData>({
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
variant="outline"
size="sm"
className="ml-auto hidden h-8 lg:flex" className="ml-auto hidden h-8 lg:flex"
size="sm"
variant="outline"
> >
<Filter className="mr-2 size-4" /> <Filter className="mr-2 size-4" />
Filtro Filtro
@ -44,9 +44,9 @@ export function DataTableViewOptions<TData>({
{hidableColumns.map((column) => { {hidableColumns.map((column) => {
return ( return (
<DropdownMenuCheckboxItem <DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()} checked={column.getIsVisible()}
className="capitalize"
key={column.id}
onCheckedChange={(value) => column.toggleVisibility(!!value)} onCheckedChange={(value) => column.toggleVisibility(!!value)}
> >
{columns_titles[column.id]} {columns_titles[column.id]}

View file

@ -36,9 +36,9 @@ export function DataTableColumnHeader<TData, TValue>({
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
variant="ghost"
size="sm"
className="data-[state=open]:bg-accent -ml-3 h-8" className="data-[state=open]:bg-accent -ml-3 h-8"
size="sm"
variant="ghost"
> >
<span>{title}</span> <span>{title}</span>
{column.getIsSorted() === "desc" ? ( {column.getIsSorted() === "desc" ? (

View file

@ -37,10 +37,10 @@ export function DataTablePagination<TData>({
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<p className="text-sm font-medium">Righe per pagina</p> <p className="text-sm font-medium">Righe per pagina</p>
<Select <Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => { onValueChange={(value) => {
table.setPageSize(Number(value)); table.setPageSize(Number(value));
}} }}
value={`${table.getState().pagination.pageSize}`}
> >
<SelectTrigger className="h-8 w-[70px]"> <SelectTrigger className="h-8 w-[70px]">
<SelectValue placeholder={table.getState().pagination.pageSize} /> <SelectValue placeholder={table.getState().pagination.pageSize} />
@ -61,37 +61,37 @@ export function DataTablePagination<TData>({
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Button <Button
variant="outline"
className="hidden size-8 p-0 lg:flex" className="hidden size-8 p-0 lg:flex"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()} disabled={!table.getCanPreviousPage()}
onClick={() => table.setPageIndex(0)}
variant="outline"
> >
<span className="sr-only">Vai alla prima pagina</span> <span className="sr-only">Vai alla prima pagina</span>
<ChevronFirst className="size-4" /> <ChevronFirst className="size-4" />
</Button> </Button>
<Button <Button
variant="outline"
className="size-8 p-0" className="size-8 p-0"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()} disabled={!table.getCanPreviousPage()}
onClick={() => table.previousPage()}
variant="outline"
> >
<span className="sr-only">Vai alla pagina precedente</span> <span className="sr-only">Vai alla pagina precedente</span>
<ChevronLeftIcon className="size-4" /> <ChevronLeftIcon className="size-4" />
</Button> </Button>
<Button <Button
variant="outline"
className="size-8 p-0" className="size-8 p-0"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()} disabled={!table.getCanNextPage()}
onClick={() => table.nextPage()}
variant="outline"
> >
<span className="sr-only">Vai alla pagina successiva</span> <span className="sr-only">Vai alla pagina successiva</span>
<ChevronRightIcon className="size-4" /> <ChevronRightIcon className="size-4" />
</Button> </Button>
<Button <Button
variant="outline"
className="hidden size-8 p-0 lg:flex" className="hidden size-8 p-0 lg:flex"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()} disabled={!table.getCanNextPage()}
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
variant="outline"
> >
<span className="sr-only">Vai alla pagina finale</span> <span className="sr-only">Vai alla pagina finale</span>
<ChevronLast className="size-4" /> <ChevronLast className="size-4" />

View file

@ -36,53 +36,53 @@ export function DataTableToolbar<TData>(props: {
{searchColumn && {searchColumn &&
(searchColumn.columnName ? ( (searchColumn.columnName ? (
<Input <Input
className="mt-0 h-8 w-[150px]"
onChange={(event) =>
table
.getColumn(searchColumn.columnName)
?.setFilterValue(event.target.value)
}
placeholder={searchColumn.placeholder} placeholder={searchColumn.placeholder}
value={ value={
(table (table
.getColumn(searchColumn.columnName) .getColumn(searchColumn.columnName)
?.getFilterValue() as string) ?? "" ?.getFilterValue() as string) ?? ""
} }
onChange={(event) =>
table
.getColumn(searchColumn.columnName)
?.setFilterValue(event.target.value)
}
className="mt-0 h-8 w-[150px]"
/> />
) : ( ) : (
<Input <Input
placeholder={searchColumn.placeholder} className="mt-0 h-8 w-[150px]"
value={globalFilter ?? ""}
onChange={(event) => onChange={(event) =>
table.setGlobalFilter(String(event.target.value)) table.setGlobalFilter(String(event.target.value))
} }
className="mt-0 h-8 w-[150px]" placeholder={searchColumn.placeholder}
value={globalFilter ?? ""}
/> />
))} ))}
{pinnedColumns?.map( {pinnedColumns?.map(
(column) => (column) =>
table.getColumn(column.columnName) && ( table.getColumn(column.columnName) && (
<DataTableFacetedFilter <DataTableFacetedFilter
key={column.columnName}
column={table.getColumn(column.columnName)} column={table.getColumn(column.columnName)}
title={column.title} key={column.columnName}
options={column.options} options={column.options}
title={column.title}
/> />
), ),
)} )}
{isFiltered && ( {isFiltered && (
<Button <Button
variant="ghost"
onClick={() => table.resetColumnFilters()}
className="h-8 px-2 lg:px-3" className="h-8 px-2 lg:px-3"
onClick={() => table.resetColumnFilters()}
variant="ghost"
> >
Reset Reset
<RotateCcw className="ml-2 size-4" /> <RotateCcw className="ml-2 size-4" />
</Button> </Button>
)} )}
</div> </div>
<DataTableViewOptions table={table} columns_titles={columns_titles} /> <DataTableViewOptions columns_titles={columns_titles} table={table} />
</div> </div>
); );
} }

View file

@ -29,11 +29,11 @@ function EmojiPicker({
}: React.ComponentProps<typeof EmojiPickerPrimitive.Root>) { }: React.ComponentProps<typeof EmojiPickerPrimitive.Root>) {
return ( return (
<EmojiPickerPrimitive.Root <EmojiPickerPrimitive.Root
data-slot="emoji-picker"
className={cn( className={cn(
"bg-popover text-popover-foreground isolate flex h-full w-fit flex-col overflow-hidden rounded-md", "bg-popover text-popover-foreground isolate flex h-full w-fit flex-col overflow-hidden rounded-md",
className, className,
)} )}
data-slot="emoji-picker"
{...props} {...props}
/> />
); );
@ -45,13 +45,13 @@ function EmojiPickerSearch({
}: React.ComponentProps<typeof EmojiPickerPrimitive.Search>) { }: React.ComponentProps<typeof EmojiPickerPrimitive.Search>) {
return ( return (
<div <div
data-slot="emoji-picker-search-wrapper"
className={cn("flex h-10 items-center gap-2 border-b px-3", className)} className={cn("flex h-10 items-center gap-2 border-b px-3", className)}
data-slot="emoji-picker-search-wrapper"
> >
<SearchIcon className="size-4 shrink-0 opacity-50" /> <SearchIcon className="size-4 shrink-0 opacity-50" />
<EmojiPickerPrimitive.Search <EmojiPickerPrimitive.Search
data-slot="emoji-picker-search"
className="placeholder:text-muted-foreground focus:border-accent-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden focus:ring-0 disabled:cursor-not-allowed disabled:opacity-50" className="placeholder:text-muted-foreground focus:border-accent-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden focus:ring-0 disabled:cursor-not-allowed disabled:opacity-50"
data-slot="emoji-picker-search"
{...props} {...props}
/> />
</div> </div>
@ -60,7 +60,7 @@ function EmojiPickerSearch({
function EmojiPickerRow({ children, ...props }: EmojiPickerListRowProps) { function EmojiPickerRow({ children, ...props }: EmojiPickerListRowProps) {
return ( return (
<div data-slot="emoji-picker-row" className="scroll-my-1 px-1" {...props}> <div className="scroll-my-1 px-1" data-slot="emoji-picker-row" {...props}>
{children} {children}
</div> </div>
); );
@ -73,11 +73,11 @@ function EmojiPickerEmoji({
}: EmojiPickerListEmojiProps) { }: EmojiPickerListEmojiProps) {
return ( return (
<button <button
data-slot="emoji-picker-emoji"
className={cn( className={cn(
"data-[active]:bg-accent flex size-7 items-center justify-center rounded-xs text-base", "data-[active]:bg-accent flex size-7 items-center justify-center rounded-xs text-base",
className, className,
)} )}
data-slot="emoji-picker-emoji"
{...props} {...props}
> >
{emoji.emoji} {emoji.emoji}
@ -91,8 +91,8 @@ function EmojiPickerCategoryHeader({
}: EmojiPickerListCategoryHeaderProps) { }: EmojiPickerListCategoryHeaderProps) {
return ( return (
<div <div
data-slot="emoji-picker-category-header"
className="bg-popover text-muted-foreground px-3 pt-3.5 pb-2 text-xs leading-none" className="bg-popover text-muted-foreground px-3 pt-3.5 pb-2 text-xs leading-none"
data-slot="emoji-picker-category-header"
{...props} {...props}
> >
{category.label} {category.label}
@ -106,30 +106,30 @@ function EmojiPickerContent({
}: React.ComponentProps<typeof EmojiPickerPrimitive.Viewport>) { }: React.ComponentProps<typeof EmojiPickerPrimitive.Viewport>) {
return ( return (
<EmojiPickerPrimitive.Viewport <EmojiPickerPrimitive.Viewport
data-slot="emoji-picker-viewport"
className={cn("relative flex-1 outline-hidden", className)} className={cn("relative flex-1 outline-hidden", className)}
data-slot="emoji-picker-viewport"
{...props} {...props}
> >
<EmojiPickerPrimitive.Loading <EmojiPickerPrimitive.Loading
data-slot="emoji-picker-loading"
className="text-muted-foreground absolute inset-0 flex items-center justify-center" className="text-muted-foreground absolute inset-0 flex items-center justify-center"
data-slot="emoji-picker-loading"
> >
<LoaderIcon className="size-4 animate-spin" /> <LoaderIcon className="size-4 animate-spin" />
</EmojiPickerPrimitive.Loading> </EmojiPickerPrimitive.Loading>
<EmojiPickerPrimitive.Empty <EmojiPickerPrimitive.Empty
data-slot="emoji-picker-empty"
className="text-muted-foreground absolute inset-0 flex items-center justify-center text-sm" className="text-muted-foreground absolute inset-0 flex items-center justify-center text-sm"
data-slot="emoji-picker-empty"
> >
No emoji found. No emoji found.
</EmojiPickerPrimitive.Empty> </EmojiPickerPrimitive.Empty>
<EmojiPickerPrimitive.List <EmojiPickerPrimitive.List
data-slot="emoji-picker-list"
className="pb-1 select-none" className="pb-1 select-none"
components={{ components={{
Row: EmojiPickerRow,
Emoji: EmojiPickerEmoji,
CategoryHeader: EmojiPickerCategoryHeader, CategoryHeader: EmojiPickerCategoryHeader,
Emoji: EmojiPickerEmoji,
Row: EmojiPickerRow,
}} }}
data-slot="emoji-picker-list"
/> />
</EmojiPickerPrimitive.Viewport> </EmojiPickerPrimitive.Viewport>
); );

View file

@ -114,8 +114,8 @@ function createStore(
invalid?: boolean, invalid?: boolean,
) { ) {
const initialState: StoreState = { const initialState: StoreState = {
files,
dragOver: false, dragOver: false,
files,
invalid: invalid ?? false, invalid: invalid ?? false,
}; };
@ -247,7 +247,7 @@ function createStore(
return () => listeners.delete(listener); return () => listeners.delete(listener);
} }
return { getState, dispatch, subscribe }; return { dispatch, getState, subscribe };
} }
const StoreContext = createContext<ReturnType<typeof createStore> | null>(null); const StoreContext = createContext<ReturnType<typeof createStore> | null>(null);
@ -277,7 +277,7 @@ function useStore<T>(selector: (state: StoreState) => T): T {
} }
const nextValue = selector(state); const nextValue = selector(state);
lastValueRef.current = { value: nextValue, state }; lastValueRef.current = { state, value: nextValue };
return nextValue; return nextValue;
}, [store, selector, lastValueRef]); }, [store, selector, lastValueRef]);
@ -380,26 +380,26 @@ const FileUploadRoot = forwardRef<HTMLDivElement, FileUploadRootProps>(
const contextValue = useMemo<FileUploadContextValue>( const contextValue = useMemo<FileUploadContextValue>(
() => ({ () => ({
dropzoneId,
inputId,
listId,
labelId,
dir, dir,
disabled, disabled,
dropzoneId,
inputId,
inputRef, inputRef,
labelId,
listId,
}), }),
[dropzoneId, inputId, listId, labelId, dir, disabled], [dropzoneId, inputId, listId, labelId, dir, disabled],
); );
useEffect(() => { useEffect(() => {
if (isControlled) { if (isControlled) {
store.dispatch({ variant: "SET_FILES", files: value }); store.dispatch({ files: value, variant: "SET_FILES" });
} else if ( } else if (
defaultValue && defaultValue &&
defaultValue.length > 0 && defaultValue.length > 0 &&
!store.getState().files.size !store.getState().files.size
) { ) {
store.dispatch({ variant: "SET_FILES", files: defaultValue }); store.dispatch({ files: defaultValue, variant: "SET_FILES" });
} }
}, [value, defaultValue, isControlled, store]); }, [value, defaultValue, isControlled, store]);
@ -497,14 +497,14 @@ const FileUploadRoot = forwardRef<HTMLDivElement, FileUploadRootProps>(
} }
if (invalid) { if (invalid) {
store.dispatch({ variant: "SET_INVALID", invalid }); store.dispatch({ invalid, variant: "SET_INVALID" });
setTimeout(() => { setTimeout(() => {
store.dispatch({ variant: "SET_INVALID", invalid: false }); store.dispatch({ invalid: false, variant: "SET_INVALID" });
}, 2000); }, 2000);
} }
if (acceptedFiles.length > 0) { if (acceptedFiles.length > 0) {
store.dispatch({ variant: "ADD_FILES", files: acceptedFiles }); store.dispatch({ files: acceptedFiles, variant: "ADD_FILES" });
if (isControlled && propsRef.current.onValueChange) { if (isControlled && propsRef.current.onValueChange) {
const currentFiles = Array.from( const currentFiles = Array.from(
@ -537,32 +537,32 @@ const FileUploadRoot = forwardRef<HTMLDivElement, FileUploadRootProps>(
async (files: File[]) => { async (files: File[]) => {
try { try {
for (const file of files) { for (const file of files) {
store.dispatch({ variant: "SET_PROGRESS", file, progress: 0 }); store.dispatch({ file, progress: 0, variant: "SET_PROGRESS" });
} }
if (propsRef.current.onUpload) { if (propsRef.current.onUpload) {
await propsRef.current.onUpload(files, { await propsRef.current.onUpload(files, {
onError: (file, error) => {
store.dispatch({
error: error.message ?? "Upload failed",
file,
variant: "SET_ERROR",
});
},
onProgress: (file, progress) => { onProgress: (file, progress) => {
store.dispatch({ store.dispatch({
variant: "SET_PROGRESS",
file, file,
progress: Math.min(Math.max(0, progress), 100), progress: Math.min(Math.max(0, progress), 100),
variant: "SET_PROGRESS",
}); });
}, },
onSuccess: (file) => { onSuccess: (file) => {
store.dispatch({ variant: "SET_SUCCESS", file }); store.dispatch({ file, variant: "SET_SUCCESS" });
},
onError: (file, error) => {
store.dispatch({
variant: "SET_ERROR",
file,
error: error.message ?? "Upload failed",
});
}, },
}); });
} else { } else {
for (const file of files) { for (const file of files) {
store.dispatch({ variant: "SET_SUCCESS", file }); store.dispatch({ file, variant: "SET_SUCCESS" });
} }
} }
} catch (error) { } catch (error) {
@ -570,9 +570,9 @@ const FileUploadRoot = forwardRef<HTMLDivElement, FileUploadRootProps>(
error instanceof Error ? error.message : "Upload failed"; error instanceof Error ? error.message : "Upload failed";
for (const file of files) { for (const file of files) {
store.dispatch({ store.dispatch({
variant: "SET_ERROR",
file,
error: errorMessage, error: errorMessage,
file,
variant: "SET_ERROR",
}); });
} }
} }
@ -600,26 +600,26 @@ const FileUploadRoot = forwardRef<HTMLDivElement, FileUploadRootProps>(
data-slot="file-upload" data-slot="file-upload"
dir={dir} dir={dir}
{...rootProps} {...rootProps}
ref={forwardedRef}
className={cn("relative flex flex-col gap-2", className)} className={cn("relative flex flex-col gap-2", className)}
ref={forwardedRef}
> >
{children} {children}
<input <input
type="file"
id={inputId}
aria-labelledby={labelId}
aria-describedby={dropzoneId}
ref={inputRef}
tabIndex={-1}
accept={accept} accept={accept}
name={name} aria-describedby={dropzoneId}
disabled={disabled} aria-labelledby={labelId}
multiple={multiple}
required={required}
className="sr-only" className="sr-only"
disabled={disabled}
id={inputId}
multiple={multiple}
name={name}
onChange={onInputChange} onChange={onInputChange}
ref={inputRef}
required={required}
tabIndex={-1}
type="file"
/> />
<span id={labelId} className="sr-only"> <span className="sr-only" id={labelId}>
{label ?? "File upload"} {label ?? "File upload"}
</span> </span>
</RootPrimitive> </RootPrimitive>
@ -671,7 +671,7 @@ const FileUploadDropzone = forwardRef<HTMLDivElement, FileUploadDropzoneProps>(
if (event.defaultPrevented) return; if (event.defaultPrevented) return;
event.preventDefault(); event.preventDefault();
store.dispatch({ variant: "SET_DRAG_OVER", dragOver: true }); store.dispatch({ dragOver: true, variant: "SET_DRAG_OVER" });
}, },
[store, propsRef.current.onDragOver], [store, propsRef.current.onDragOver],
); );
@ -683,7 +683,7 @@ const FileUploadDropzone = forwardRef<HTMLDivElement, FileUploadDropzoneProps>(
if (event.defaultPrevented) return; if (event.defaultPrevented) return;
event.preventDefault(); event.preventDefault();
store.dispatch({ variant: "SET_DRAG_OVER", dragOver: true }); store.dispatch({ dragOver: true, variant: "SET_DRAG_OVER" });
}, },
[store, propsRef.current.onDragEnter], [store, propsRef.current.onDragEnter],
); );
@ -695,7 +695,7 @@ const FileUploadDropzone = forwardRef<HTMLDivElement, FileUploadDropzoneProps>(
if (event.defaultPrevented) return; if (event.defaultPrevented) return;
event.preventDefault(); event.preventDefault();
store.dispatch({ variant: "SET_DRAG_OVER", dragOver: false }); store.dispatch({ dragOver: false, variant: "SET_DRAG_OVER" });
}, },
[store, propsRef.current.onDragLeave], [store, propsRef.current.onDragLeave],
); );
@ -707,7 +707,7 @@ const FileUploadDropzone = forwardRef<HTMLDivElement, FileUploadDropzoneProps>(
if (event.defaultPrevented) return; if (event.defaultPrevented) return;
event.preventDefault(); event.preventDefault();
store.dispatch({ variant: "SET_DRAG_OVER", dragOver: false }); store.dispatch({ dragOver: false, variant: "SET_DRAG_OVER" });
const files = Array.from(event.dataTransfer.files); const files = Array.from(event.dataTransfer.files);
const inputElement = context.inputRef.current; const inputElement = context.inputRef.current;
@ -743,19 +743,17 @@ const FileUploadDropzone = forwardRef<HTMLDivElement, FileUploadDropzoneProps>(
return ( return (
<DropzonePrimitive <DropzonePrimitive
role="region"
id={context.dropzoneId}
aria-controls={`${context.inputId} ${context.listId}`} aria-controls={`${context.inputId} ${context.listId}`}
//aria-disabled={context.disabled}
//aria-invalid={invalid}
data-disabled={context.disabled ? "" : undefined} data-disabled={context.disabled ? "" : undefined}
data-dragging={dragOver ? "" : undefined} data-dragging={dragOver ? "" : undefined}
//aria-disabled={context.disabled}
//aria-invalid={invalid}
data-invalid={invalid ? "" : undefined} data-invalid={invalid ? "" : undefined}
data-slot="file-upload-dropzone" data-slot="file-upload-dropzone"
dir={context.dir} dir={context.dir}
id={context.dropzoneId}
role="region"
{...dropzoneProps} {...dropzoneProps}
ref={forwardedRef}
tabIndex={context.disabled ? undefined : 0}
className={cn( className={cn(
"data-[dragging]:border-primary data-[invalid]:border-destructive data-[invalid]:ring-destructive/20 hover:bg-accent/30 focus-visible:border-ring/50 relative flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed p-6 outline-hidden transition-colors select-none data-[disabled]:pointer-events-none", "data-[dragging]:border-primary data-[invalid]:border-destructive data-[invalid]:ring-destructive/20 hover:bg-accent/30 focus-visible:border-ring/50 relative flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed p-6 outline-hidden transition-colors select-none data-[disabled]:pointer-events-none",
className, className,
@ -766,6 +764,8 @@ const FileUploadDropzone = forwardRef<HTMLDivElement, FileUploadDropzoneProps>(
onDragOver={onDragOver} onDragOver={onDragOver}
onDrop={onDrop} onDrop={onDrop}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
ref={forwardedRef}
tabIndex={context.disabled ? undefined : 0}
/> />
); );
}, },
@ -797,14 +797,14 @@ const FileUploadTrigger = forwardRef<HTMLButtonElement, FileUploadTriggerProps>(
return ( return (
<TriggerPrimitive <TriggerPrimitive
type="button"
aria-controls={context.inputId} aria-controls={context.inputId}
data-disabled={context.disabled ? "" : undefined} data-disabled={context.disabled ? "" : undefined}
data-slot="file-upload-trigger" data-slot="file-upload-trigger"
type="button"
{...triggerProps} {...triggerProps}
ref={forwardedRef}
disabled={context.disabled} disabled={context.disabled}
onClick={onClick} onClick={onClick}
ref={forwardedRef}
/> />
); );
}, },
@ -838,20 +838,20 @@ const FileUploadList = forwardRef<HTMLDivElement, FileUploadListProps>(
return ( return (
<ListPrimitive <ListPrimitive
role="list"
id={context.listId}
//aria-orientation={orientation}
data-orientation={orientation} data-orientation={orientation}
data-slot="file-upload-list" data-slot="file-upload-list"
//aria-orientation={orientation}
data-state={shouldRender ? "active" : "inactive"} data-state={shouldRender ? "active" : "inactive"}
dir={context.dir} dir={context.dir}
id={context.listId}
role="list"
{...listProps} {...listProps}
ref={forwardedRef}
className={cn( className={cn(
"data-[state=active]:animate-in data-[state=inactive]:animate-out data-[state=active]:fade-in-0 data-[state=inactive]:fade-out-0 data-[state=active]:slide-in-from-top-2 data-[state=inactive]:slide-out-to-top-2 flex flex-col gap-2", "data-[state=active]:animate-in data-[state=inactive]:animate-out data-[state=active]:fade-in-0 data-[state=inactive]:fade-out-0 data-[state=active]:slide-in-from-top-2 data-[state=inactive]:slide-out-to-top-2 flex flex-col gap-2",
orientation === "horizontal" && "flex-row overflow-x-auto p-1.5", orientation === "horizontal" && "flex-row overflow-x-auto p-1.5",
className, className,
)} )}
ref={forwardedRef}
/> />
); );
}, },
@ -904,12 +904,12 @@ const FileUploadItem = forwardRef<HTMLDivElement, FileUploadItemProps>(
const itemContext = useMemo( const itemContext = useMemo(
() => ({ () => ({
id,
fileState, fileState,
id,
messageId,
nameId, nameId,
sizeId, sizeId,
statusId, statusId,
messageId,
}), }),
[id, fileState, statusId, nameId, sizeId, messageId], [id, fileState, statusId, nameId, sizeId, messageId],
); );
@ -929,25 +929,25 @@ const FileUploadItem = forwardRef<HTMLDivElement, FileUploadItemProps>(
return ( return (
<FileUploadItemContext.Provider value={itemContext}> <FileUploadItemContext.Provider value={itemContext}>
<ItemPrimitive <ItemPrimitive
role="listitem"
id={id}
aria-setsize={fileCount}
aria-posinset={fileIndex}
aria-describedby={`${nameId} ${sizeId} ${statusId} ${ aria-describedby={`${nameId} ${sizeId} ${statusId} ${
fileState.error ? messageId : "" fileState.error ? messageId : ""
}`} }`}
aria-labelledby={nameId} aria-labelledby={nameId}
aria-posinset={fileIndex}
aria-setsize={fileCount}
data-slot="file-upload-item" data-slot="file-upload-item"
dir={context.dir} dir={context.dir}
id={id}
role="listitem"
{...itemProps} {...itemProps}
ref={forwardedRef}
className={cn( className={cn(
"relative flex items-center gap-2.5 rounded-md border p-3 has-[_[data-slot=file-upload-progress]]:flex-col has-[_[data-slot=file-upload-progress]]:items-start", "relative flex items-center gap-2.5 rounded-md border p-3 has-[_[data-slot=file-upload-progress]]:flex-col has-[_[data-slot=file-upload-progress]]:items-start",
className, className,
)} )}
ref={forwardedRef}
> >
{props.children} {props.children}
<span id={statusId} className="sr-only"> <span className="sr-only" id={statusId}>
{statusText} {statusText}
</span> </span>
</ItemPrimitive> </ItemPrimitive>
@ -1042,13 +1042,13 @@ const FileUploadItemPreview = forwardRef<
return ( return (
// biome-ignore lint/a11y/noNoninteractiveElementInteractions: <intended> // biome-ignore lint/a11y/noNoninteractiveElementInteractions: <intended>
<img <img
src={URL.createObjectURL(file)}
alt={file.name} alt={file.name}
className="size-full rounded-sm object-cover" className="size-full rounded-sm object-cover"
onLoad={(event) => { onLoad={(event) => {
if (!(event.target instanceof HTMLImageElement)) return; if (!(event.target instanceof HTMLImageElement)) return;
URL.revokeObjectURL(event.target.src); URL.revokeObjectURL(event.target.src);
}} }}
src={URL.createObjectURL(file)}
/> />
); );
} }
@ -1067,12 +1067,12 @@ const FileUploadItemPreview = forwardRef<
aria-labelledby={itemContext.nameId} aria-labelledby={itemContext.nameId}
data-slot="file-upload-preview" data-slot="file-upload-preview"
{...previewProps} {...previewProps}
ref={forwardedRef}
className={cn( className={cn(
"relative flex size-10 shrink-0 items-center justify-center rounded-md", "relative flex size-10 shrink-0 items-center justify-center rounded-md",
isImage ? "object-cover" : "bg-accent/50 [&>svg]:size-7", isImage ? "object-cover" : "bg-accent/50 [&>svg]:size-7",
className, className,
)} )}
ref={forwardedRef}
> >
{onPreviewRender(itemContext.fileState.file)} {onPreviewRender(itemContext.fileState.file)}
{children} {children}
@ -1103,27 +1103,27 @@ const FileUploadItemMetadata = forwardRef<
data-slot="file-upload-metadata" data-slot="file-upload-metadata"
dir={context.dir} dir={context.dir}
{...metadataProps} {...metadataProps}
ref={forwardedRef}
className={cn("flex min-w-0 flex-1 flex-col", className)} className={cn("flex min-w-0 flex-1 flex-col", className)}
ref={forwardedRef}
> >
{children ?? ( {children ?? (
<> <>
<span <span
id={itemContext.nameId}
className="truncate text-sm font-medium" className="truncate text-sm font-medium"
id={itemContext.nameId}
> >
{itemContext.fileState.file.name} {itemContext.fileState.file.name}
</span> </span>
<span <span
id={itemContext.sizeId}
className="text-muted-foreground text-xs" className="text-muted-foreground text-xs"
id={itemContext.sizeId}
> >
{formatBytes(itemContext.fileState.file.size)} {formatBytes(itemContext.fileState.file.size)}
</span> </span>
{itemContext.fileState.error && ( {itemContext.fileState.error && (
<span <span
id={itemContext.messageId}
className="text-destructive text-xs" className="text-destructive text-xs"
id={itemContext.messageId}
> >
{itemContext.fileState.error} {itemContext.fileState.error}
</span> </span>
@ -1162,45 +1162,45 @@ const FileUploadItemProgress = forwardRef<
return ( return (
<ItemProgressPrimitive <ItemProgressPrimitive
role="progressbar" aria-labelledby={itemContext.nameId}
aria-valuemin={0}
aria-valuemax={100} aria-valuemax={100}
aria-valuemin={0}
aria-valuenow={itemContext.fileState.progress} aria-valuenow={itemContext.fileState.progress}
aria-valuetext={`${itemContext.fileState.progress}%`} aria-valuetext={`${itemContext.fileState.progress}%`}
aria-labelledby={itemContext.nameId}
data-slot="file-upload-progress" data-slot="file-upload-progress"
role="progressbar"
{...progressProps} {...progressProps}
ref={forwardedRef}
className={cn( className={cn(
"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2", "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2",
className, className,
)} )}
ref={forwardedRef}
> >
<svg <svg
className="rotate-[-90deg] transform" className="rotate-[-90deg] transform"
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
fill="none" fill="none"
height={size}
stroke="currentColor" stroke="currentColor"
viewBox={`0 0 ${size} ${size}`}
width={size}
> >
<title>Progress</title> <title>Progress</title>
<circle <circle
className="text-primary/20" className="text-primary/20"
strokeWidth="2"
cx={size / 2} cx={size / 2}
cy={size / 2} cy={size / 2}
r={(size - 4) / 2} r={(size - 4) / 2}
strokeWidth="2"
/> />
<circle <circle
className="text-primary transition-all" className="text-primary transition-all"
strokeWidth="2"
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
cx={size / 2} cx={size / 2}
cy={size / 2} cy={size / 2}
r={(size - 4) / 2} r={(size - 4) / 2}
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
strokeLinecap="round"
strokeWidth="2"
/> />
</svg> </svg>
</ItemProgressPrimitive> </ItemProgressPrimitive>
@ -1209,19 +1209,19 @@ const FileUploadItemProgress = forwardRef<
return ( return (
<ItemProgressPrimitive <ItemProgressPrimitive
role="progressbar" aria-labelledby={itemContext.nameId}
aria-valuemin={0}
aria-valuemax={100} aria-valuemax={100}
aria-valuemin={0}
aria-valuenow={itemContext.fileState.progress} aria-valuenow={itemContext.fileState.progress}
aria-valuetext={`${itemContext.fileState.progress}%`} aria-valuetext={`${itemContext.fileState.progress}%`}
aria-labelledby={itemContext.nameId}
data-slot="file-upload-progress" data-slot="file-upload-progress"
role="progressbar"
{...progressProps} {...progressProps}
ref={forwardedRef}
className={cn( className={cn(
"bg-primary/20 relative h-1.5 w-full overflow-hidden rounded-full", "bg-primary/20 relative h-1.5 w-full overflow-hidden rounded-full",
className, className,
)} )}
ref={forwardedRef}
> >
<div <div
className="bg-primary h-full w-full flex-1 transition-all" className="bg-primary h-full w-full flex-1 transition-all"
@ -1255,8 +1255,8 @@ const FileUploadItemDelete = forwardRef<
if (!itemContext.fileState || event.defaultPrevented) return; if (!itemContext.fileState || event.defaultPrevented) return;
store.dispatch({ store.dispatch({
variant: "REMOVE_FILE",
file: itemContext.fileState.file, file: itemContext.fileState.file,
variant: "REMOVE_FILE",
}); });
}, },
[store, itemContext.fileState, propsRef.current?.onClick], [store, itemContext.fileState, propsRef.current?.onClick],
@ -1268,13 +1268,13 @@ const FileUploadItemDelete = forwardRef<
return ( return (
<ItemDeletePrimitive <ItemDeletePrimitive
type="button"
aria-controls={itemContext.id} aria-controls={itemContext.id}
aria-describedby={itemContext.nameId} aria-describedby={itemContext.nameId}
data-slot="file-upload-item-delete" data-slot="file-upload-item-delete"
type="button"
{...deleteProps} {...deleteProps}
ref={forwardedRef}
onClick={onClick} onClick={onClick}
ref={forwardedRef}
/> />
); );
}); });
@ -1315,14 +1315,14 @@ const FileUploadClear = forwardRef<HTMLButtonElement, FileUploadClearProps>(
return ( return (
<ClearPrimitive <ClearPrimitive
type="button"
aria-controls={context.listId} aria-controls={context.listId}
data-slot="file-upload-clear"
data-disabled={isDisabled ? "" : undefined} data-disabled={isDisabled ? "" : undefined}
data-slot="file-upload-clear"
type="button"
{...clearProps} {...clearProps}
ref={forwardedRef}
disabled={isDisabled} disabled={isDisabled}
onClick={onClick} onClick={onClick}
ref={forwardedRef}
/> />
); );
}, },

View file

@ -53,11 +53,11 @@ const useFormField = () => {
const { id } = itemContext; const { id } = itemContext;
return { return {
formDescriptionId: `${id}-form-item-description`,
formItemId: `${id}-form-item`,
formMessageId: `${id}-form-item-message`,
id, id,
name: fieldContext.name, name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState, ...fieldState,
}; };
}; };
@ -78,7 +78,7 @@ const FormItem = React.forwardRef<
return ( return (
<FormItemContext.Provider value={{ id }}> <FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} /> <div className={cn("space-y-2", className)} ref={ref} {...props} />
</FormItemContext.Provider> </FormItemContext.Provider>
); );
}); });
@ -92,9 +92,9 @@ const FormLabel = React.forwardRef<
return ( return (
<Label <Label
ref={ref}
className={cn(error && "text-red-500 dark:text-red-600", className)} className={cn(error && "text-red-500 dark:text-red-600", className)}
htmlFor={formItemId} htmlFor={formItemId}
ref={ref}
{...props} {...props}
/> />
); );
@ -110,14 +110,14 @@ const FormControl = React.forwardRef<
return ( return (
<Slot <Slot
ref={ref}
id={formItemId}
aria-describedby={ aria-describedby={
!error !error
? `${formDescriptionId}` ? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}` : `${formDescriptionId} ${formMessageId}`
} }
aria-invalid={!!error} aria-invalid={!!error}
id={formItemId}
ref={ref}
{...props} {...props}
/> />
); );
@ -132,12 +132,12 @@ const FormDescription = React.forwardRef<
return ( return (
<p <p
ref={ref}
id={formDescriptionId}
className={cn( className={cn(
"text-sm text-neutral-500 dark:text-neutral-400", "text-sm text-neutral-500 dark:text-neutral-400",
className, className,
)} )}
id={formDescriptionId}
ref={ref}
{...props} {...props}
/> />
); );
@ -157,12 +157,12 @@ const FormMessage = React.forwardRef<
return ( return (
<p <p
ref={ref}
id={formMessageId}
className={cn( className={cn(
"text-sm font-medium text-red-500 dark:text-red-600", "text-sm font-medium text-red-500 dark:text-red-600",
className, className,
)} )}
id={formMessageId}
ref={ref}
{...props} {...props}
> >
{body} {body}

View file

@ -7,8 +7,8 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => { ({ className, type, ...props }, ref) => {
return ( return (
<input <input
type={type}
ref={ref} ref={ref}
type={type}
{...props} {...props}
className={cn( className={cn(
`border-border bg-background text-primary placeholder:text-muted-foreground hover:border-muted-foreground focus:border-foreground focus:ring-foreground w-full rounded-lg border px-3 py-2 font-medium shadow-xs outline-hidden transition-all duration-200 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50`, `border-border bg-background text-primary placeholder:text-muted-foreground hover:border-muted-foreground focus:border-foreground focus:ring-foreground w-full rounded-lg border px-3 py-2 font-medium shadow-xs outline-hidden transition-all duration-200 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50`,

View file

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

View file

@ -81,15 +81,15 @@ export function MultiSelect({
return ( return (
<MultiSelectContext <MultiSelectContext
value={{ value={{
open,
setOpen,
selectedValues: values ? new Set(values) : selectedValues,
toggleValue,
items, items,
onItemAdded, onItemAdded,
open,
selectedValues: values ? new Set(values) : selectedValues,
setOpen,
toggleValue,
}} }}
> >
<Popover open={open} onOpenChange={setOpen}> <Popover onOpenChange={setOpen} open={open}>
{children} {children}
</Popover> </Popover>
</MultiSelectContext> </MultiSelectContext>
@ -110,13 +110,13 @@ export function MultiSelectTrigger({
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
{...props} {...props}
variant={props.variant ?? "outline"}
role={props.role ?? "combobox"}
aria-expanded={props["aria-expanded"] ?? open} aria-expanded={props["aria-expanded"] ?? open}
className={cn( className={cn(
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 border-input data-[placeholder]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 dark:bg-input/30 dark:hover:bg-input/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex h-auto min-h-9 w-fit items-center justify-between gap-2 overflow-hidden rounded-md border bg-transparent px-3 py-1.5 text-sm whitespace-nowrap shadow-xs outline-hidden transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 border-input data-[placeholder]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 dark:bg-input/30 dark:hover:bg-input/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex h-auto min-h-9 w-fit items-center justify-between gap-2 overflow-hidden rounded-md border bg-transparent px-3 py-1.5 text-sm whitespace-nowrap shadow-xs outline-hidden transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className, className,
)} )}
role={props.role ?? "combobox"}
variant={props.variant ?? "outline"}
> >
{children} {children}
<ChevronsUpDownIcon className="size-4 shrink-0 opacity-50" /> <ChevronsUpDownIcon className="size-4 shrink-0 opacity-50" />
@ -197,26 +197,17 @@ export function MultiSelectValue({
return ( return (
<div <div
{...props} {...props}
ref={valueRef}
className={cn( className={cn(
"flex w-full gap-1.5 overflow-hidden", "flex w-full gap-1.5 overflow-hidden",
shouldWrap && "h-full flex-wrap", shouldWrap && "h-full flex-wrap",
className, className,
)} )}
ref={valueRef}
> >
{[...selectedValues] {[...selectedValues]
.filter((value) => items.has(value)) .filter((value) => items.has(value))
.map((value) => ( .map((value) => (
<Badge <Badge
ref={(el) => {
if (el == null) return;
itemsRef.current.add(el);
return () => {
itemsRef.current.delete(el);
};
}}
variant="outline"
className="group flex items-center gap-1" className="group flex items-center gap-1"
key={value} key={value}
onClick={ onClick={
@ -227,6 +218,15 @@ export function MultiSelectValue({
} }
: undefined : undefined
} }
ref={(el) => {
if (el == null) return;
itemsRef.current.add(el);
return () => {
itemsRef.current.delete(el);
};
}}
variant="outline"
> >
{items.get(value)} {items.get(value)}
{clickToRemove && ( {clickToRemove && (
@ -235,11 +235,11 @@ export function MultiSelectValue({
</Badge> </Badge>
))} ))}
<Badge <Badge
ref={overflowRef}
style={{ style={{
display: overflowAmount > 0 && !shouldWrap ? "block" : "none", display: overflowAmount > 0 && !shouldWrap ? "block" : "none",
}} }}
variant="outline" variant="outline"
ref={overflowRef}
> >
+{overflowAmount} +{overflowAmount}
</Badge> </Badge>
@ -275,8 +275,8 @@ export function MultiSelectContent({
) : ( ) : (
// biome-ignore lint/a11y/noAriaHiddenOnFocusable: <intended> // biome-ignore lint/a11y/noAriaHiddenOnFocusable: <intended>
<button <button
autoFocus
aria-hidden="true" aria-hidden="true"
autoFocus
className="sr-only" className="sr-only"
type="button" type="button"
/> />
@ -315,11 +315,11 @@ export function MultiSelectItem({
return ( return (
<CommandItem <CommandItem
{...props} {...props}
value={value}
onSelect={(v) => { onSelect={(v) => {
toggleValue(v); toggleValue(v);
onSelect?.(v); onSelect?.(v);
}} }}
value={value}
> >
<CheckIcon <CheckIcon
className={cn("mr-2 size-4", isSelected ? "opacity-100" : "opacity-0")} className={cn("mr-2 size-4", isSelected ? "opacity-100" : "opacity-0")}

View file

@ -45,45 +45,45 @@ const VirtualizedMenuList = (
const virtualizer = useVirtualizer({ const virtualizer = useVirtualizer({
count, count,
getScrollElement: () => parentRef.current,
estimateSize: () => 45, estimateSize: () => 45,
getScrollElement: () => parentRef.current,
}); });
const items = virtualizer.getVirtualItems(); const items = virtualizer.getVirtualItems();
return ( return (
<div <div
ref={parentRef}
className="List" className="List"
ref={parentRef}
style={{ style={{
contain: "strict",
height: `${virtualizer.getTotalSize()}px`, height: `${virtualizer.getTotalSize()}px`,
maxHeight: "300px", maxHeight: "300px",
width: "auto",
overflowY: "auto", overflowY: "auto",
contain: "strict", width: "auto",
}} }}
> >
<div <div
style={{ style={{
height: virtualizer.getTotalSize(), height: virtualizer.getTotalSize(),
width: "100%",
position: "relative", position: "relative",
width: "100%",
}} }}
> >
<div <div
style={{ style={{
left: 0,
position: "absolute", position: "absolute",
top: 0, top: 0,
left: 0,
width: "100%",
transform: `translateY(${items[0]?.start ?? 0}px)`, transform: `translateY(${items[0]?.start ?? 0}px)`,
width: "100%",
}} }}
> >
{items.map((virtualRow) => ( {items.map((virtualRow) => (
<div <div
key={virtualRow.key}
data-index={virtualRow.index}
ref={virtualizer.measureElement}
className={virtualRow.index % 2 ? "ListItemOdd" : "ListItemEven"} className={virtualRow.index % 2 ? "ListItemOdd" : "ListItemEven"}
data-index={virtualRow.index}
key={virtualRow.key}
ref={virtualizer.measureElement}
> >
{rows[virtualRow.index]} {rows[virtualRow.index]}
</div> </div>
@ -129,14 +129,45 @@ export const MultiSelect = ({
return ( return (
<ReactSelect <ReactSelect
menuPlacement={menuPlacement} classNamePrefix="select"
classNames={{
control: () =>
cn(
"mt-2 w-full rounded-lg h-min-[42px] dark:bg-primary bg-white shadow-xs outline-hidden text-foreground dark:text-white",
),
input: () => cn("*:ring-0 focus:*:ring-0"),
menu: () =>
cn(
`dark:bg-black text-foreground bg-white dark:text-white border shadow-xs outline-hidden
dark:border-foreground border-neutral-300 overflow-hidden`,
menuOpen
? "animate-in fade-in-0 zoom-in-90"
: "animate-out fade-out-0 zoom-out-90",
),
multiValue: () =>
cn(
"overflow-hidden dark:bg-black rounded-lg text-primary bg-white dark:text-white border border-neutral-300 dark:border-foreground shadow-xs outline-hidden",
),
multiValueLabel: () =>
cn(
"dark:bg-black rounded-lg text-primary bg-white dark:text-white outline-hidden",
),
multiValueRemove: () => cn("rounded-s-none rounded-e-md"),
option: () =>
cn(
"dark:bg-black text-foreground bg-white dark:text-white border-b border-neutral-300 dark:border-foreground shadow-xs outline-hidden hover:text-primary hover:bg-neutral-200 dark:hover:bg-neutral-700",
),
singleValue: () => cn("text-primary font-medium"),
valueContainer: () => cn("min-h-[38px]"),
}}
components={{ MenuList: VirtualizedMenuList }} components={{ MenuList: VirtualizedMenuList }}
defaultValue={defaultValue}
inputId={elementId} inputId={elementId}
isMulti={isMulti}
isDisabled={disabled}
instanceId={elementId} instanceId={elementId}
isDisabled={disabled}
isMulti={isMulti}
menuPlacement={menuPlacement}
name={elementName} name={elementName}
options={options}
onChange={(values) => { onChange={(values) => {
if (isMulti) { if (isMulti) {
const valore = values as ListOption[]; const valore = values as ListOption[];
@ -146,40 +177,34 @@ export const MultiSelect = ({
onValueChange(valore); onValueChange(valore);
} }
}} }}
defaultValue={defaultValue}
placeholder={placeholder}
onMenuOpen={() => setMenuOpen(true)}
onMenuClose={() => setMenuOpen(false)} onMenuClose={() => setMenuOpen(false)}
onMenuOpen={() => setMenuOpen(true)}
options={options}
placeholder={placeholder}
styles={{ styles={{
container: (base, state) => container: (base, state) =>
({ ({
...base, ...base,
":hover .select__dropdown-indicator": {
color: state.isFocused ? "rgb(161,161,170)" : "rgb(161,161,170)",
},
":hover .select__indicator-separator": { ":hover .select__indicator-separator": {
backgroundColor: state.isFocused backgroundColor: state.isFocused
? "rgb(161,161,170)" ? "rgb(161,161,170)"
: "rgb(161,161,170)", : "rgb(161,161,170)",
}, },
":hover .select__dropdown-indicator": {
color: state.isFocused ? "rgb(161,161,170)" : "rgb(161,161,170)",
},
}) as CSSObjectWithLabel, }) as CSSObjectWithLabel,
control: (base, state) => control: (base, state) =>
({ ({
...base, ...base,
minHeight: "42px",
borderRadius: "0.5rem",
boxShadow: undefined,
border: state.isFocused "& .select__dropdown-indicator": {
? "1px solid rgb(161,161,170)" color: state.isFocused
: "1px solid rgb(212 ,212 ,216)",
"&:hover": {
borderColor: state.isFocused
? "rgb(161,161,170)" ? "rgb(161,161,170)"
: "rgb(161 ,161 ,170)", : "rgb(212 ,212 ,216)",
transition: "none",
}, },
"& .select__indicator-separator": { "& .select__indicator-separator": {
@ -188,12 +213,23 @@ export const MultiSelect = ({
: "rgb(212 ,212 ,216)", : "rgb(212 ,212 ,216)",
}, },
"& .select__dropdown-indicator": { "&:hover": {
color: state.isFocused borderColor: state.isFocused
? "rgb(161,161,170)" ? "rgb(161,161,170)"
: "rgb(212 ,212 ,216)", : "rgb(161 ,161 ,170)",
transition: "none",
}, },
border: state.isFocused
? "1px solid rgb(161,161,170)"
: "1px solid rgb(212 ,212 ,216)",
borderRadius: "0.5rem",
boxShadow: undefined,
minHeight: "42px",
}) as CSSObjectWithLabel,
menu: (base) =>
({
...base,
borderRadius: "0.5rem",
}) as CSSObjectWithLabel, }) as CSSObjectWithLabel,
multiValue: (base) => multiValue: (base) =>
({ ({
@ -206,21 +242,9 @@ export const MultiSelect = ({
...base, ...base,
borderRadius: "0.5rem", borderRadius: "0.5rem",
}) as CSSObjectWithLabel, }) as CSSObjectWithLabel,
menu: (base) =>
({
...base,
borderRadius: "0.5rem",
}) as CSSObjectWithLabel,
option: (base, state) => option: (base, state) =>
({ ({
...base, ...base,
backgroundColor: undefined,
color: state.isSelected ? undefined : undefined,
"&:hover": {
backgroundColor: undefined,
color: state.isSelected ? undefined : undefined,
},
"&:active": { "&:active": {
backgroundColor: "rgb(161, 161, 170)", backgroundColor: "rgb(161, 161, 170)",
@ -230,39 +254,15 @@ export const MultiSelect = ({
"&:focus": { "&:focus": {
backgroundColor: undefined, backgroundColor: undefined,
}, },
"&:hover": {
backgroundColor: undefined,
color: state.isSelected ? undefined : undefined,
},
backgroundColor: undefined,
color: state.isSelected ? undefined : undefined,
}) as CSSObjectWithLabel, }) as CSSObjectWithLabel,
}} }}
classNames={{
valueContainer: () => cn("min-h-[38px]"),
control: () =>
cn(
"mt-2 w-full rounded-lg h-min-[42px] dark:bg-primary bg-white shadow-xs outline-hidden text-foreground dark:text-white",
),
option: () =>
cn(
"dark:bg-black text-foreground bg-white dark:text-white border-b border-neutral-300 dark:border-foreground shadow-xs outline-hidden hover:text-primary hover:bg-neutral-200 dark:hover:bg-neutral-700",
),
menu: () =>
cn(
`dark:bg-black text-foreground bg-white dark:text-white border shadow-xs outline-hidden
dark:border-foreground border-neutral-300 overflow-hidden`,
menuOpen
? "animate-in fade-in-0 zoom-in-90"
: "animate-out fade-out-0 zoom-out-90",
),
multiValueLabel: () =>
cn(
"dark:bg-black rounded-lg text-primary bg-white dark:text-white outline-hidden",
),
multiValue: () =>
cn(
"overflow-hidden dark:bg-black rounded-lg text-primary bg-white dark:text-white border border-neutral-300 dark:border-foreground shadow-xs outline-hidden",
),
multiValueRemove: () => cn("rounded-s-none rounded-e-md"),
input: () => cn("*:ring-0 focus:*:ring-0"),
singleValue: () => cn("text-primary font-medium"),
}}
classNamePrefix="select"
/> />
); );
}; };
@ -271,7 +271,7 @@ type UnpackOptions = {
options: string[]; options: string[];
}; };
export const UnpackOptions = ({ options }: UnpackOptions) => { export const UnpackOptions = ({ options }: UnpackOptions) => {
return options.map((v, idx) => ({ value: idx.toString(), label: v })); return options.map((v, idx) => ({ label: v, value: idx.toString() }));
}; };
type RepackValues = { type RepackValues = {

View file

@ -32,7 +32,7 @@ function Dialog({ children }: { children: ReactNode }) {
return ( return (
<DialogContext.Provider value={{ innerOpen, setInnerOpen }}> <DialogContext.Provider value={{ innerOpen, setInnerOpen }}>
<DialogPrimitive.Root open={outerOpen} onOpenChange={setOuterOpen}> <DialogPrimitive.Root onOpenChange={setOuterOpen} open={outerOpen}>
{children} {children}
</DialogPrimitive.Root> </DialogPrimitive.Root>
</DialogContext.Provider> </DialogContext.Provider>
@ -48,11 +48,11 @@ const DialogOverlay = forwardRef<
ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
ref={ref}
className={cn( className={cn(
"bg-background/40 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 backdrop-blur-sm", "bg-background/40 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 backdrop-blur-sm",
className, className,
)} )}
ref={ref}
{...props} {...props}
/> />
)); ));
@ -69,12 +69,12 @@ const DialogContent = forwardRef<
<DialogPortal> <DialogPortal>
<DialogOverlay /> <DialogOverlay />
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref}
className={cn( className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:rounded-lg", "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:rounded-lg",
context.innerOpen && "translate-y-[-55%] scale-[0.97]", context.innerOpen && "translate-y-[-55%] scale-[0.97]",
className, className,
)} )}
ref={ref}
{...props} {...props}
> >
{children} {children}
@ -108,8 +108,8 @@ function InnerDialog({ children }: { children: ReactNode }) {
return ( return (
<DialogPrimitive.Root <DialogPrimitive.Root
open={context.innerOpen}
onOpenChange={context.setInnerOpen} onOpenChange={context.setInnerOpen}
open={context.innerOpen}
> >
{children} {children}
</DialogPrimitive.Root> </DialogPrimitive.Root>
@ -177,14 +177,6 @@ const InnerDialogContent = forwardRef<
return ( return (
<DialogPortal> <DialogPortal>
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
style={{
transform: `translate(-50%, calc(-50% + ${currentY}px))`,
transition: isDragging ? "none" : "transform 0.3s ease-out",
}}
className={cn( className={cn(
"bg-background fixed top-[50%] left-[50%] z-[60] grid w-full max-w-lg translate-x-[-50%] translate-y-[-45%] gap-4 rounded-lg border p-6 shadow-lg duration-200", "bg-background fixed top-[50%] left-[50%] z-[60] grid w-full max-w-lg translate-x-[-50%] translate-y-[-45%] gap-4 rounded-lg border p-6 shadow-lg duration-200",
isClosingByDrag isClosingByDrag
@ -203,6 +195,14 @@ const InnerDialogContent = forwardRef<
draggable && "", draggable && "",
className, className,
)} )}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
ref={ref}
style={{
transform: `translate(-50%, calc(-50% + ${currentY}px))`,
transition: isDragging ? "none" : "transform 0.3s ease-out",
}}
{...props} {...props}
> >
<div ref={contentRef}>{children}</div> <div ref={contentRef}>{children}</div>
@ -247,11 +247,11 @@ const InnerDialogTitle = forwardRef<
ComponentPropsWithoutRef<typeof DialogPrimitive.Title> ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Title <DialogPrimitive.Title
ref={ref}
className={cn( className={cn(
"text-lg leading-none font-semibold tracking-tight", "text-lg leading-none font-semibold tracking-tight",
className, className,
)} )}
ref={ref}
{...props} {...props}
/> />
)); ));
@ -262,8 +262,8 @@ const InnerDialogDescription = forwardRef<
ComponentPropsWithoutRef<typeof DialogPrimitive.Description> ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Description <DialogPrimitive.Description
ref={ref}
className={cn("text-muted-foreground text-sm", className)} className={cn("text-muted-foreground text-sm", className)}
ref={ref}
{...props} {...props}
/> />
)); ));
@ -299,11 +299,11 @@ const DialogTitle = forwardRef<
ComponentPropsWithoutRef<typeof DialogPrimitive.Title> ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Title <DialogPrimitive.Title
ref={ref}
className={cn( className={cn(
"text-lg leading-none font-semibold tracking-tight", "text-lg leading-none font-semibold tracking-tight",
className, className,
)} )}
ref={ref}
{...props} {...props}
/> />
)); ));
@ -314,8 +314,8 @@ const DialogDescription = forwardRef<
ComponentPropsWithoutRef<typeof DialogPrimitive.Description> ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Description <DialogPrimitive.Description
ref={ref}
className={cn("text-muted-foreground text-sm", className)} className={cn("text-muted-foreground text-sm", className)}
ref={ref}
{...props} {...props}
/> />
)); ));

View file

@ -9,6 +9,21 @@ type TextStaggeredFadeProps = {
hide?: boolean; hide?: boolean;
}; };
// Helper function to properly split text including emojis
const splitTextWithEmojis = (text: string): string[] => {
// Use Intl.Segmenter if available (modern browsers)
if (typeof Intl !== "undefined" && Intl.Segmenter) {
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
return Array.from(segmenter.segment(text), (segment) => segment.segment);
}
return Array.from(
text.matchAll(
/\p{Extended_Pictographic}|\p{Emoji_Component}|\p{Emoji_Modifier}|\p{Emoji_Modifier_Base}|\p{Emoji_Presentation}|./gu,
),
).map((match) => match[0]);
};
export const StaggeredFade: React.FC<TextStaggeredFadeProps> = ({ export const StaggeredFade: React.FC<TextStaggeredFadeProps> = ({
text, text,
className = "", className = "",
@ -17,30 +32,30 @@ export const StaggeredFade: React.FC<TextStaggeredFadeProps> = ({
const variants = { const variants = {
hidden: { opacity: 0 }, hidden: { opacity: 0 },
show: (i: number) => ({ show: (i: number) => ({
y: 0,
opacity: 1, opacity: 1,
transition: { delay: i * 0.07 }, transition: { delay: i * 0.07 },
y: 0,
}), }),
}; };
const letters = text.split(""); const letters = splitTextWithEmojis(text);
const ref = React.useRef(null); const ref = React.useRef(null);
const isInView = useInView(ref, { once: true }); const isInView = useInView(ref, { once: true });
return ( return (
<motion.h2 <motion.h2
ref={ref}
initial="hidden"
animate={isInView ? (!hide ? "show" : "hidden") : ""} animate={isInView ? (!hide ? "show" : "hidden") : ""}
variants={variants}
viewport={{ once: true }}
className={cn( className={cn(
"text-center text-xl font-bold tracking-tighter md:leading-[4rem]", "text-center text-xl font-bold tracking-tighter md:leading-[4rem]",
className, className,
)} )}
initial="hidden"
ref={ref}
variants={variants}
viewport={{ once: true }}
> >
{letters.map((word, i) => ( {letters.map((word, i) => (
<motion.span key={`${word}-${i}`} variants={variants} custom={i}> <motion.span custom={i} key={`${word}-${i}`} variants={variants}>
{word} {word}
</motion.span> </motion.span>
))} ))}

View file

@ -43,23 +43,23 @@ export const EtichetteDisplayer = ({
<div className={cn("flex flex-wrap gap-2", className)}> <div className={cn("flex flex-wrap gap-2", className)}>
{data.map((etichetta) => ( {data.map((etichetta) => (
<Etichetta <Etichetta
className="text-xs"
color_hex={etichetta.color_hex}
key={etichetta.id_etichetta} key={etichetta.id_etichetta}
title={etichetta.title} title={etichetta.title}
color_hex={etichetta.color_hex}
className="text-xs"
/> />
))} ))}
<button <button
type="button"
onClick={() => setOpenEdit(true)}
className="flex items-center justify-center rounded-lg border p-1" className="flex items-center justify-center rounded-lg border p-1"
onClick={() => setOpenEdit(true)}
type="button"
> >
<Plus className="size-4" /> <Plus className="size-4" />
</button> </button>
<EtichetteModal <EtichetteModal
chatId={chatId} chatId={chatId}
open={openEdit}
onOpenChange={handleOpen} onOpenChange={handleOpen}
open={openEdit}
/> />
</div> </div>
); );
@ -93,7 +93,7 @@ export const EtichetteModal = ({
}); });
if (!data) return null; if (!data) return null;
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog onOpenChange={onOpenChange} open={open}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Etichette Attive</DialogTitle> <DialogTitle>Etichette Attive</DialogTitle>
@ -102,10 +102,10 @@ export const EtichetteModal = ({
<div className="space-y-5"> <div className="space-y-5">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{data.map((etichetta, idx) => ( {data.map((etichetta, idx) => (
<div key={idx} className="flex gap-2"> <div className="flex gap-2" key={idx}>
<Etichetta <Etichetta
title={etichetta.title}
color_hex={etichetta.color_hex} color_hex={etichetta.color_hex}
title={etichetta.title}
/> />
<Button <Button
onClick={() => { onClick={() => {
@ -179,9 +179,9 @@ export const Etichetta = ({
function hexToRgb(hex: string): { r: number; g: number; b: number } { function hexToRgb(hex: string): { r: number; g: number; b: number } {
const bigint = parseInt(hex.slice(1), 16); const bigint = parseInt(hex.slice(1), 16);
return { return {
r: (bigint >> 16) & 255,
g: (bigint >> 8) & 255,
b: bigint & 255, b: bigint & 255,
g: (bigint >> 8) & 255,
r: (bigint >> 16) & 255,
}; };
} }

View file

@ -17,17 +17,17 @@ export default function FailedAnnuncioLoading() {
<p className="text-neutral-600">{t.annuncio_load_fail.sottotitolo}</p> <p className="text-neutral-600">{t.annuncio_load_fail.sottotitolo}</p>
<div className="flex flex-wrap items-center justify-center gap-3"> <div className="flex flex-wrap items-center justify-center gap-3">
<button <button
type="button" className="block rounded-lg bg-rose-600 px-4 py-2 font-medium text-white duration-150 hover:bg-rose-500 active:bg-rose-700"
onClick={() => { onClick={() => {
router.back(); router.back();
}} }}
className="block rounded-lg bg-rose-600 px-4 py-2 font-medium text-white duration-150 hover:bg-rose-500 active:bg-rose-700" type="button"
> >
{t.annuncio_load_fail.CTA} {t.annuncio_load_fail.CTA}
</button> </button>
<Link <Link
href="/annunci"
className="block rounded-lg border px-4 py-2 font-medium text-neutral-700 duration-150 hover:bg-neutral-50 active:bg-neutral-100" className="block rounded-lg border px-4 py-2 font-medium text-neutral-700 duration-150 hover:bg-neutral-50 active:bg-neutral-100"
href="/annunci"
> >
{t.annuncio_load_fail.home} {t.annuncio_load_fail.home}
</Link> </Link>

View file

@ -10,9 +10,9 @@ export const Footer = () => {
<div className="mx-auto max-w-6xl px-4 py-6 sm:px-6 lg:px-8"> <div className="mx-auto max-w-6xl px-4 py-6 sm:px-6 lg:px-8">
<div className="text-center"> <div className="text-center">
<Link <Link
href="/"
aria-label="footerLogo" aria-label="footerLogo"
className="flex items-center justify-center text-3xl font-bold tracking-wide antialiased" className="flex items-center justify-center text-3xl font-bold tracking-wide antialiased"
href="/"
> >
<LogoSvg className="h-12 w-auto [&>g]:fill-red-500 dark:[&>g]:fill-neutral-100 [&>text]:fill-neutral-600 [&>text]:stroke-neutral-600 dark:[&>text]:fill-neutral-100 dark:[&>text]:stroke-neutral-100" /> <LogoSvg className="h-12 w-auto [&>g]:fill-red-500 dark:[&>g]:fill-neutral-100 [&>text]:fill-neutral-600 [&>text]:stroke-neutral-600 dark:[&>text]:fill-neutral-100 dark:[&>text]:stroke-neutral-100" />
</Link> </Link>
@ -22,9 +22,9 @@ export const Footer = () => {
<ul className="flex flex-wrap justify-center gap-6 md:gap-8 lg:gap-12"> <ul className="flex flex-wrap justify-center gap-6 md:gap-8 lg:gap-12">
<li> <li>
<Link <Link
aria-label="footerGuida"
className="text-sm" className="text-sm"
href="/guida" href="/guida"
aria-label="footerGuida"
> >
{t.footer.Guida} {t.footer.Guida}
</Link> </Link>
@ -32,9 +32,9 @@ export const Footer = () => {
<li> <li>
<Link <Link
aria-label="footerChiSiamo"
className="text-sm" className="text-sm"
href="/chi-siamo" href="/chi-siamo"
aria-label="footerChiSiamo"
> >
{t.footer["Chi Siamo"]} {t.footer["Chi Siamo"]}
</Link> </Link>
@ -42,18 +42,18 @@ export const Footer = () => {
<li> <li>
<Link <Link
aria-label="footerTerminiCondizioni"
className="text-sm" className="text-sm"
href="/termini-condizioni" href="/termini-condizioni"
aria-label="footerTerminiCondizioni"
> >
{t.termini_condizioni_title} {t.termini_condizioni_title}
</Link> </Link>
</li> </li>
<li> <li>
<Link <Link
aria-label="footerPrivacyPolicy"
className="text-sm" className="text-sm"
href="/privacy-policy" href="/privacy-policy"
aria-label="footerPrivacyPolicy"
> >
{t.privacy_policy_title} {t.privacy_policy_title}
</Link> </Link>
@ -82,8 +82,8 @@ export const MiniFooter = () => {
<div className="text-center"> <div className="text-center">
<Link <Link
aria-label="footerLogo" aria-label="footerLogo"
href="/"
className="text-2xl font-bold tracking-wide antialiased" className="text-2xl font-bold tracking-wide antialiased"
href="/"
> >
Infoalloggi.it Infoalloggi.it
</Link> </Link>

View file

@ -52,9 +52,9 @@ type MapProps = {
} & (GroupProps | SingleProps); } & (GroupProps | SingleProps);
const defaults = { const defaults = {
zoom: 15,
width: "500px",
height: "500px", height: "500px",
width: "500px",
zoom: 15,
}; };
const MedianPoint = (pos: posData[]) => { const MedianPoint = (pos: posData[]) => {
@ -92,20 +92,20 @@ const MapComp = ({
case "price": case "price":
return ( return (
<CustomMarker <CustomMarker
position={pos.pos}
eventHandlers={pos.onClick && { click: pos.onClick }} eventHandlers={pos.onClick && { click: pos.onClick }}
title={pos.markerTxt}
icon={ icon={
<MapMarker <MapMarker
color="primary"
label={ label={
<span className="inline-block whitespace-nowrap"> <span className="inline-block whitespace-nowrap">
{pos.markerTxt} {pos.markerTxt}
</span> </span>
} }
color="primary"
size="sm" size="sm"
/> />
} }
position={pos.pos}
title={pos.markerTxt}
> >
{pos.popupTxt && <Popup>{pos.popupTxt}</Popup>} {pos.popupTxt && <Popup>{pos.popupTxt}</Popup>}
</CustomMarker> </CustomMarker>
@ -114,8 +114,8 @@ const MapComp = ({
case "precise": case "precise":
return ( return (
<Marker <Marker
position={pos.pos}
eventHandlers={pos.onClick && { click: pos.onClick }} eventHandlers={pos.onClick && { click: pos.onClick }}
position={pos.pos}
title={pos.markerTxt} title={pos.markerTxt}
> >
{pos.popupTxt && <Popup>{pos.popupTxt}</Popup>} {pos.popupTxt && <Popup>{pos.popupTxt}</Popup>}
@ -125,9 +125,9 @@ const MapComp = ({
return ( return (
<Circle <Circle
center={pos.pos} center={pos.pos}
eventHandlers={pos.onClick && { click: pos.onClick }}
pathOptions={{ color: "red" }} pathOptions={{ color: "red" }}
radius={markerRadius || 100} radius={markerRadius || 100}
eventHandlers={pos.onClick && { click: pos.onClick }}
> >
{pos.popupTxt && <Popup>{pos.popupTxt}</Popup>} {pos.popupTxt && <Popup>{pos.popupTxt}</Popup>}
</Circle> </Circle>
@ -137,22 +137,22 @@ const MapComp = ({
<> <>
<Circle <Circle
center={pos.pos} center={pos.pos}
pathOptions={{ color: "#ec4899" }}
fillOpacity={1}
radius={markerRadius || 100}
eventHandlers={pos.onClick && { click: pos.onClick }} eventHandlers={pos.onClick && { click: pos.onClick }}
fillOpacity={1}
pathOptions={{ color: "#ec4899" }}
radius={markerRadius || 100}
> >
{pos.popupTxt && <Popup>{pos.popupTxt}</Popup>} {pos.popupTxt && <Popup>{pos.popupTxt}</Popup>}
</Circle> </Circle>
<CustomMarker <CustomMarker
position={pos.pos}
eventHandlers={pos.onClick && { click: pos.onClick }} eventHandlers={pos.onClick && { click: pos.onClick }}
title={pos.markerTxt}
icon={ icon={
<div className="relative flex size-10 items-center justify-center rounded-full bg-pink-500 p-2"> <div className="relative flex size-10 items-center justify-center rounded-full bg-pink-500 p-2">
<Home className="absolute size-3/4 stroke-white" /> <Home className="absolute size-3/4 stroke-white" />
</div> </div>
} }
position={pos.pos}
title={pos.markerTxt}
> >
{pos.popupTxt && <Popup>{pos.popupTxt}</Popup>} {pos.popupTxt && <Popup>{pos.popupTxt}</Popup>}
</CustomMarker> </CustomMarker>
@ -167,24 +167,24 @@ const MapComp = ({
return ( return (
<MapContainer <MapContainer
key={hashKey}
center={ center={
autocenter ? MedianPoint(isGroup ? groupData : [posix]) : posix.pos autocenter ? MedianPoint(isGroup ? groupData : [posix]) : posix.pos
} }
zoom={zoom} className={cn("")}
key={hashKey}
scrollWheelZoom={scrollWheelZoom} scrollWheelZoom={scrollWheelZoom}
style={{ style={{
borderRadius: "8px",
height: height, height: height,
width: width, width: width,
borderRadius: "8px",
}} }}
className={cn("")} zoom={zoom}
> >
<APIs /> <APIs />
<TileLayer <TileLayer
url="/api/tiles/{s}/{z}/{x}/{y}"
//url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>' attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
//url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
url="/api/tiles/{s}/{z}/{x}/{y}"
/> />
{isGroup ? ( {isGroup ? (

View file

@ -248,9 +248,9 @@ const ComponentMarker = forwardRef<LeafletMarker, BaseMarkerProps>(
...(iconAnchor ? { iconAnchor } : []), ...(iconAnchor ? { iconAnchor } : []),
...(popupAnchor ? { popupAnchor } : []), ...(popupAnchor ? { popupAnchor } : []),
...(tooltipAnchor ? { tooltipAnchor } : []), ...(tooltipAnchor ? { tooltipAnchor } : []),
pane,
attribution, attribution,
className, className,
pane,
}); });
}, iconDeps); }, iconDeps);
@ -370,9 +370,9 @@ export const Marker = forwardRef<LeafletMarker, MarkerProps>(
if (validElement) { if (validElement) {
return ( return (
<ComponentMarker <ComponentMarker
ref={ref}
icon={Icon}
componentIconOpts={componentIconOpts} componentIconOpts={componentIconOpts}
icon={Icon}
ref={ref}
{...otherProps} {...otherProps}
/> />
); );
@ -381,9 +381,9 @@ export const Marker = forwardRef<LeafletMarker, MarkerProps>(
if (validComponent) { if (validComponent) {
return ( return (
<ComponentMarker <ComponentMarker
ref={ref}
icon={<Icon />}
componentIconOpts={componentIconOpts} componentIconOpts={componentIconOpts}
icon={<Icon />}
ref={ref}
{...otherProps} {...otherProps}
/> />
); );
@ -391,8 +391,8 @@ export const Marker = forwardRef<LeafletMarker, MarkerProps>(
return ( return (
<ReactLeafletMarker <ReactLeafletMarker
ref={ref}
icon={Icon as ReactLeafletMarkerProps["icon"]} icon={Icon as ReactLeafletMarkerProps["icon"]}
ref={ref}
{...otherProps} {...otherProps}
/> />
); );

View file

@ -16,54 +16,54 @@ export default function MapMarker({
size = "md", size = "md",
}: MapMarkerProps) { }: MapMarkerProps) {
const colorClasses = { const colorClasses = {
primary: {
rectangle: "bg-[#242428] text-white",
triangle: "border-t-[#242428]",
},
red: {
rectangle: "bg-red-500 text-white",
triangle: "border-t-red-500",
},
blue: { blue: {
rectangle: "bg-blue-500 text-white", rectangle: "bg-blue-500 text-white",
triangle: "border-t-blue-500", triangle: "border-t-blue-500",
}, },
green: {
rectangle: "bg-green-500 text-white",
triangle: "border-t-green-500",
},
yellow: {
rectangle: "bg-yellow-500 text-black",
triangle: "border-t-yellow-500",
},
purple: {
rectangle: "bg-purple-500 text-white",
triangle: "border-t-purple-500",
},
gray: { gray: {
rectangle: "bg-gray-500 text-white", rectangle: "bg-gray-500 text-white",
triangle: "border-t-gray-500", triangle: "border-t-gray-500",
}, },
green: {
rectangle: "bg-green-500 text-white",
triangle: "border-t-green-500",
},
primary: {
rectangle: "bg-[#242428] text-white",
triangle: "border-t-[#242428]",
},
purple: {
rectangle: "bg-purple-500 text-white",
triangle: "border-t-purple-500",
},
red: {
rectangle: "bg-red-500 text-white",
triangle: "border-t-red-500",
},
yellow: {
rectangle: "bg-yellow-500 text-black",
triangle: "border-t-yellow-500",
},
}; };
const sizeClasses = { const sizeClasses = {
sm: { lg: {
container: "h-8", container: "h-16",
rectangle: "px-2 py-1 rounded-md", rectangle: "px-4 py-3 rounded-xl",
triangle: "border-l-[8px] border-r-[8px] border-t-[8px]", text: "text-base",
text: "text-xs", triangle: "border-l-[12px] border-r-[12px] border-t-[12px]",
}, },
md: { md: {
container: "h-12", container: "h-12",
rectangle: "px-3 py-2 rounded-lg", rectangle: "px-3 py-2 rounded-lg",
triangle: "border-l-[10px] border-r-[10px] border-t-[10px]",
text: "text-sm", text: "text-sm",
triangle: "border-l-[10px] border-r-[10px] border-t-[10px]",
}, },
lg: { sm: {
container: "h-16", container: "h-8",
rectangle: "px-4 py-3 rounded-xl", rectangle: "px-2 py-1 rounded-md",
triangle: "border-l-[12px] border-r-[12px] border-t-[12px]", text: "text-xs",
text: "text-base", triangle: "border-l-[8px] border-r-[8px] border-t-[8px]",
}, },
}; };

View file

@ -25,10 +25,10 @@ export const LangToggle = () => {
return ( return (
<LoadingButton <LoadingButton
aria-label="Change Language" aria-label="Change Language"
loading={busy}
variant="outline"
className="text-primary flex w-fit items-center gap-2" className="text-primary flex w-fit items-center gap-2"
loading={busy}
onClick={() => handleLangChange(locale === "it" ? "en" : "it")} onClick={() => handleLangChange(locale === "it" ? "en" : "it")}
variant="outline"
> >
{locale === "it" ? ( {locale === "it" ? (
<> <>

View file

@ -64,7 +64,7 @@ export const UserHeaderSection = ({
{!session || !session.user ? ( {!session || !session.user ? (
<Button <Button
aria-label="Login Button" aria-label="Login Button"
variant="ghost" className="flex h-[46px] w-full items-center justify-center gap-x-1 rounded-lg p-2"
onClick={async () => { onClick={async () => {
await router.push({ await router.push({
pathname: "/login", pathname: "/login",
@ -72,24 +72,24 @@ export const UserHeaderSection = ({
}); });
setSidebar(false); setSidebar(false);
}} }}
className="flex h-[46px] w-full items-center justify-center gap-x-1 rounded-lg p-2" variant="ghost"
> >
{inSidebar && t.auth.login.titolo} {inSidebar && t.auth.login.titolo}
<LogIn className="size-5" /> <LogIn className="size-5" />
</Button> </Button>
) : ( ) : (
<DropdownMenu open={open} onOpenChange={setOpen}> <DropdownMenu onOpenChange={setOpen} open={open}>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
aria-label="User Profile Button" aria-label="User Profile Button"
variant="ghost"
className="flex h-[46px] w-full items-center justify-center gap-x-1 rounded-full p-2 font-medium" className="flex h-[46px] w-full items-center justify-center gap-x-1 rounded-full p-2 font-medium"
variant="ghost"
> >
<UserAvatar <UserAvatar
username={session.user.username}
userId={session.user.id}
className="size-10" className="size-10"
userId={session.user.id}
username={session.user.username}
/> />
{inSidebar ? ( {inSidebar ? (
<span>{session.user.username}</span> <span>{session.user.username}</span>
@ -100,7 +100,7 @@ export const UserHeaderSection = ({
)} )}
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end" forceMount> <DropdownMenuContent align="end" className="w-56" forceMount>
{session.user.isAdmin ? ( {session.user.isAdmin ? (
<> <>
<DropdownMenuLabel className="font-normal"> <DropdownMenuLabel className="font-normal">
@ -120,8 +120,8 @@ export const UserHeaderSection = ({
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem
className="flex cursor-pointer items-center gap-3 px-2 py-1.5"
aria-label="Logout Button" aria-label="Logout Button"
className="flex cursor-pointer items-center gap-3 px-2 py-1.5"
onClick={() => { onClick={() => {
logout(); logout();
}} }}
@ -179,16 +179,16 @@ const Items = ({
return ( return (
<DropdownMenuGroup> <DropdownMenuGroup>
{items?.map((item, idx) => ( {items?.map((item, idx) => (
<DropdownMenuItem key={idx} className="p-0"> <DropdownMenuItem className="p-0" key={idx}>
<Link <Link
aria-label={`Link to ${item.title}`} aria-label={`Link to ${item.title}`}
onClick={() => setOpen(false)}
href={item.href}
className="flex h-full w-full items-center gap-3 px-2 py-1.5" className="flex h-full w-full items-center gap-3 px-2 py-1.5"
href={item.href}
onClick={() => setOpen(false)}
> >
<IconMatrix <IconMatrix
type={item.icon.icon}
className={cn("size-6", item.icon.className)} className={cn("size-6", item.icon.className)}
type={item.icon.icon}
/> />
{item.title} {item.title}

View file

@ -14,8 +14,8 @@ export function MainNav({ pathname }: MainNavProps) {
<div className="mr-2 hidden gap-6 md:flex"> <div className="mr-2 hidden gap-6 md:flex">
<Link <Link
aria-label="mainNavLink" aria-label="mainNavLink"
href="/"
className="flex shrink-0 items-center font-medium md:mb-0" className="flex shrink-0 items-center font-medium md:mb-0"
href="/"
> >
<LogoSvg className="[&>text]:fill-primary [&>text]:stroke-primary h-8 w-auto [&>g]:fill-red-500" /> <LogoSvg className="[&>text]:fill-primary [&>text]:stroke-primary h-8 w-auto [&>g]:fill-red-500" />
</Link> </Link>
@ -24,12 +24,12 @@ export function MainNav({ pathname }: MainNavProps) {
{t.nav.mainNav.map((item) => ( {t.nav.mainNav.map((item) => (
<Link <Link
aria-label={item.title} aria-label={item.title}
key={item.href}
href={item.href}
className={cn( className={cn(
"hover:text-primary/80 transition-colors", "hover:text-primary/80 transition-colors",
pathname === item.href ? "text-primary" : "text-primary/70", pathname === item.href ? "text-primary" : "text-primary/70",
)} )}
href={item.href}
key={item.href}
> >
{item.title} {item.title}
</Link> </Link>

View file

@ -41,19 +41,19 @@ export function MobileNav({
const verySmallScreen = useMediaQuery("(max-width: 320px)"); const verySmallScreen = useMediaQuery("(max-width: 320px)");
return ( return (
<> <>
<Sheet open={open} onOpenChange={setOpen}> <Sheet onOpenChange={setOpen} open={open}>
<SheetTrigger asChild> <SheetTrigger asChild>
<Button <Button
aria-label="Toggle Mobile Menu" aria-label="Toggle Mobile Menu"
variant="ghost"
className="mr-2 size-8 p-0 text-base hover:bg-transparent focus-visible:bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 md:hidden [&_svg]:size-6" className="mr-2 size-8 p-0 text-base hover:bg-transparent focus-visible:bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 md:hidden [&_svg]:size-6"
variant="ghost"
> >
<AlignLeft /> <AlignLeft />
<span className="sr-only">Toggle Menu</span> <span className="sr-only">Toggle Menu</span>
</Button> </Button>
</SheetTrigger> </SheetTrigger>
<SheetContent side="left" className="z-500 p-0"> <SheetContent className="z-500 p-0" side="left">
<SheetHeader> <SheetHeader>
<SheetTitle className="sr-only">Mobile Menu</SheetTitle> <SheetTitle className="sr-only">Mobile Menu</SheetTitle>
<SheetDescription className="sr-only"> <SheetDescription className="sr-only">
@ -62,9 +62,9 @@ export function MobileNav({
</SheetHeader> </SheetHeader>
<div className="flex h-full flex-col space-y-2 overflow-y-auto px-3 pb-4"> <div className="flex h-full flex-col space-y-2 overflow-y-auto px-3 pb-4">
<MobileLink <MobileLink
href="/"
aria-label="mainNavLinkHome" aria-label="mainNavLinkHome"
className="ml-2 flex items-center gap-3" className="ml-2 flex items-center gap-3"
href="/"
onOpenChange={setOpen} onOpenChange={setOpen}
> >
<Home className="size-6" />{" "} <Home className="size-6" />{" "}
@ -88,18 +88,18 @@ export function MobileNav({
<div className="text-primary flex h-full grow flex-col gap-3"> <div className="text-primary flex h-full grow flex-col gap-3">
{t.nav.mainNav.map((item) => ( {t.nav.mainNav.map((item) => (
<MobileLink <MobileLink
key={item.href}
href={item.href}
onOpenChange={setOpen}
className={cn( className={cn(
"flex items-center gap-3 rounded-lg px-2 py-1 font-medium", "flex items-center gap-3 rounded-lg px-2 py-1 font-medium",
item.href === pathname && "bg-muted", item.href === pathname && "bg-muted",
)} )}
href={item.href}
key={item.href}
onOpenChange={setOpen}
> >
<IconMatrix <IconMatrix
className="size-5"
key={item.title} key={item.title}
type={item.icon} type={item.icon}
className="size-5"
/>{" "} />{" "}
{item.title} {item.title}
</MobileLink> </MobileLink>
@ -109,10 +109,10 @@ export function MobileNav({
<div className="flex h-fit flex-col gap-4 pb-2"> <div className="flex h-fit flex-col gap-4 pb-2">
<UserHeaderSection <UserHeaderSection
inSidebar
open={userOpen} open={userOpen}
setOpen={setUserOpen} setOpen={setUserOpen}
setSidebar={setOpen} setSidebar={setOpen}
inSidebar
/> />
<div className="flex items-center justify-center gap-6"> <div className="flex items-center justify-center gap-6">
@ -125,9 +125,9 @@ export function MobileNav({
</SheetContent> </SheetContent>
</Sheet> </Sheet>
<Link <Link
href="/"
className="ml-3 flex shrink-0 items-center md:hidden"
aria-label="mainNavLink" aria-label="mainNavLink"
className="ml-3 flex shrink-0 items-center md:hidden"
href="/"
> >
{verySmallScreen ? ( {verySmallScreen ? (
<ASvg className="h-8 w-auto" /> <ASvg className="h-8 w-auto" />
@ -158,6 +158,7 @@ function MobileLink({
return ( return (
<Link <Link
aria-label="mobileLink" aria-label="mobileLink"
className={cn(className)}
href={href} href={href}
onClick={(e) => { onClick={(e) => {
if (disabled) { if (disabled) {
@ -165,13 +166,10 @@ function MobileLink({
e.stopPropagation(); e.stopPropagation();
onOpenChange?.(false); onOpenChange?.(false);
} else { } else {
router.push( router.push(href.toString());
href.toString(),
);
onOpenChange?.(false); onOpenChange?.(false);
} }
}} }}
className={cn(className)}
{...props} {...props}
> >
{children} {children}
@ -203,7 +201,7 @@ const AreaRiservataLinks = ({
item.href === pathname && "bg-muted", item.href === pathname && "bg-muted",
)} )}
> >
<IconMatrix type={item.icon} className="size-5" /> <IconMatrix className="size-5" type={item.icon} />
{item.title} {item.title}
</div> </div>
@ -212,14 +210,14 @@ const AreaRiservataLinks = ({
{item.items.map((item) => { {item.items.map((item) => {
return ( return (
<MobileLink <MobileLink
key={item.title}
href={item.href}
onOpenChange={setOpen}
className={cn( className={cn(
`hover:text-accent-foreground mx-4 block w-full border-l px-6 py-2 duration-150 hover:border-neutral-800`, `hover:text-accent-foreground mx-4 block w-full border-l px-6 py-2 duration-150 hover:border-neutral-800`,
item.href === pathname && item.href === pathname &&
"border-neutral-900 text-neutral-900", "border-neutral-900 text-neutral-900",
)} )}
href={item.href}
key={item.title}
onOpenChange={setOpen}
> >
{item.title} {item.title}
</MobileLink> </MobileLink>
@ -232,30 +230,30 @@ const AreaRiservataLinks = ({
} }
return ( return (
<MobileLink <MobileLink
key={item.title}
href={item.href}
onOpenChange={setOpen}
className={cn( className={cn(
"flex items-center gap-3 rounded-lg px-2 py-1 font-medium", "flex items-center gap-3 rounded-lg px-2 py-1 font-medium",
item.href === pathname && "bg-muted", item.href === pathname && "bg-muted",
)} )}
href={item.href}
key={item.title}
onOpenChange={setOpen}
> >
<IconMatrix type={item.icon} className="size-5" /> <IconMatrix className="size-5" type={item.icon} />
{item.title} {item.title}
</MobileLink> </MobileLink>
); );
}) })
: t.nav.areaRiservataUserNav.map((item) => ( : t.nav.areaRiservataUserNav.map((item) => (
<MobileLink <MobileLink
key={item.title}
href={item.href}
onOpenChange={setOpen}
className={cn( className={cn(
"flex items-center gap-3 rounded-lg px-2 py-1 font-medium", "flex items-center gap-3 rounded-lg px-2 py-1 font-medium",
item.href === pathname && "bg-muted", item.href === pathname && "bg-muted",
)} )}
href={item.href}
key={item.title}
onOpenChange={setOpen}
> >
<IconMatrix type={item.icon} className="size-5" /> <IconMatrix className="size-5" type={item.icon} />
{item.title} {item.title}
</MobileLink> </MobileLink>
))} ))}
@ -294,7 +292,7 @@ export const ARMinimizableLinks = ({
minimized && "justify-center", minimized && "justify-center",
)} )}
> >
<IconMatrix type={item.icon} className="size-5" /> <IconMatrix className="size-5" type={item.icon} />
{!minimized && item.title} {!minimized && item.title}
</div> </div>
@ -303,9 +301,6 @@ export const ARMinimizableLinks = ({
{item.items.map((item) => { {item.items.map((item) => {
return ( return (
<MobileLink <MobileLink
key={item.title}
href={item.href}
onOpenChange={setOpen}
className={cn( className={cn(
`hover:text-accent-foreground mx-4 block w-full border-l px-6 py-2 duration-150 hover:border-neutral-800`, `hover:text-accent-foreground mx-4 block w-full border-l px-6 py-2 duration-150 hover:border-neutral-800`,
item.href === pathname && item.href === pathname &&
@ -313,6 +308,9 @@ export const ARMinimizableLinks = ({
minimized && "hidden", minimized && "hidden",
)} )}
disabled={minimized} disabled={minimized}
href={item.href}
key={item.title}
onOpenChange={setOpen}
> >
{item.title} {item.title}
</MobileLink> </MobileLink>
@ -325,34 +323,34 @@ export const ARMinimizableLinks = ({
} }
return ( return (
<MobileLink <MobileLink
key={item.title}
href={item.href}
onOpenChange={setOpen}
className={cn( className={cn(
"flex items-center gap-3 rounded-lg px-2 py-1 font-medium", "flex items-center gap-3 rounded-lg px-2 py-1 font-medium",
item.href === pathname && "bg-muted", item.href === pathname && "bg-muted",
minimized && "justify-center", minimized && "justify-center",
)} )}
disabled={minimized} disabled={minimized}
href={item.href}
key={item.title}
onOpenChange={setOpen}
> >
<IconMatrix type={item.icon} className="size-5" /> <IconMatrix className="size-5" type={item.icon} />
{!minimized && item.title} {!minimized && item.title}
</MobileLink> </MobileLink>
); );
}) })
: t.nav.areaRiservataUserNav.map((item) => ( : t.nav.areaRiservataUserNav.map((item) => (
<MobileLink <MobileLink
key={item.title}
href={item.href}
onOpenChange={setOpen}
className={cn( className={cn(
"flex items-center gap-3 rounded-lg px-2 py-1 font-medium", "flex items-center gap-3 rounded-lg px-2 py-1 font-medium",
item.href === pathname && "bg-muted", item.href === pathname && "bg-muted",
minimized && "justify-center", minimized && "justify-center",
)} )}
disabled={minimized} disabled={minimized}
href={item.href}
key={item.title}
onOpenChange={setOpen}
> >
<IconMatrix type={item.icon} className="size-5" /> <IconMatrix className="size-5" type={item.icon} />
{!minimized && item.title} {!minimized && item.title}
</MobileLink> </MobileLink>
))} ))}

View file

@ -24,9 +24,9 @@ export function RicercaCommand() {
}, []); }, []);
return ( return (
<> <>
<Dialog open={open} onOpenChange={setOpen}> <Dialog onOpenChange={setOpen} open={open}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="ghost" className="p-2" aria-label="ricercaBtn"> <Button aria-label="ricercaBtn" className="p-2" variant="ghost">
<Search className="size-5" /> <Search className="size-5" />
</Button> </Button>
</DialogTrigger> </DialogTrigger>
@ -37,8 +37,8 @@ export function RicercaCommand() {
</DialogHeader> </DialogHeader>
<div className="relative mx-auto flex w-full gap-2"> <div className="relative mx-auto flex w-full gap-2">
<CodiceBox <CodiceBox
key={"codiceBox"}
className="outline-foreground focus:ring-foreground rounded-none rounded-t-md outline placeholder:text-center focus:border-none focus:outline-hidden" className="outline-foreground focus:ring-foreground rounded-none rounded-t-md outline placeholder:text-center focus:border-none focus:outline-hidden"
key={"codiceBox"}
optionBoxClassName="rounded-none rounded-b-md mt-0 border-b-0 border-x-0" optionBoxClassName="rounded-none rounded-b-md mt-0 border-b-0 border-x-0"
/> />
</div> </div>

View file

@ -12,7 +12,7 @@ export const SettingsPopover = () => {
return ( return (
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button variant="ghost" className="p-2" aria-label="Settings"> <Button aria-label="Settings" className="p-2" variant="ghost">
<Settings className="size-5" /> <Settings className="size-5" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>

View file

@ -25,11 +25,11 @@ export function SiteHeader() {
<div className="mx-auto flex h-14 w-full items-center pr-3 pl-4 md:pr-4 md:pl-8"> <div className="mx-auto flex h-14 w-full items-center pr-3 pl-4 md:pr-4 md:pl-8">
<MainNav pathname={pathname} /> <MainNav pathname={pathname} />
<MobileNav <MobileNav
pathname={pathname}
open={openSidebar} open={openSidebar}
pathname={pathname}
setOpen={setOpenSidebar} setOpen={setOpenSidebar}
userOpen={openUserMenuSidebar}
setUserOpen={setOpenUserMenuSidebar} setUserOpen={setOpenUserMenuSidebar}
userOpen={openUserMenuSidebar}
/> />
<div className="float-end flex flex-1 items-center justify-end gap-0.5"> <div className="float-end flex flex-1 items-center justify-end gap-0.5">

View file

@ -7,20 +7,20 @@ export const ThemeSwitch = () => {
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme();
const spring: Transition = { const spring: Transition = {
type: "spring",
stiffness: 700,
damping: 30, damping: 30,
stiffness: 700,
type: "spring",
}; };
return ( return (
<button <button
aria-label="Toggle Theme" aria-label="Toggle Theme"
type="button"
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
className={cn( className={cn(
`flex h-10 w-20 rounded-full bg-neutral-200 p-[5px] shadow-inner hover:cursor-pointer dark:bg-neutral-700`, `flex h-10 w-20 rounded-full bg-neutral-200 p-[5px] shadow-inner hover:cursor-pointer dark:bg-neutral-700`,
theme === "light" && "place-content-end", theme === "light" && "place-content-end",
)} )}
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
type="button"
> >
<motion.div <motion.div
className={cn( className={cn(

View file

@ -14,23 +14,23 @@ export default function PaymentStatus({
title, title,
}: PaymentStatusProps) { }: PaymentStatusProps) {
const statusConfig = { const statusConfig = {
success: {
icon: CheckCircle,
color: "text-green-500",
bgColor: "bg-green-50",
borderColor: "border-green-200",
},
processing: {
icon: Loader2,
color: "text-blue-500",
bgColor: "bg-blue-50",
borderColor: "border-blue-200",
},
error: { error: {
icon: AlertCircle,
color: "text-red-500",
bgColor: "bg-red-50", bgColor: "bg-red-50",
borderColor: "border-red-200", borderColor: "border-red-200",
color: "text-red-500",
icon: AlertCircle,
},
processing: {
bgColor: "bg-blue-50",
borderColor: "border-blue-200",
color: "text-blue-500",
icon: Loader2,
},
success: {
bgColor: "bg-green-50",
borderColor: "border-green-200",
color: "text-green-500",
icon: CheckCircle,
}, },
}; };

View file

@ -37,19 +37,19 @@ const PhoneInput: React.ForwardRefExoticComponent<PhoneInputProps> = React.memo(
>(({ className, onChange, ...props }, ref) => { >(({ className, onChange, ...props }, ref) => {
return ( return (
<RPNInput.default <RPNInput.default
ref={ref}
className={cn( className={cn(
"[&_input]:ph-input-fix flex h-auto w-full flex-1 [&_*]:!h-auto", "[&_input]:ph-input-fix flex h-auto w-full flex-1 [&_*]:!h-auto",
className, className,
)} )}
flagComponent={FlagComponent}
countrySelectComponent={CountrySelect} countrySelectComponent={CountrySelect}
inputComponent={InputComponent}
smartCaret={false}
defaultCountry="IT" defaultCountry="IT"
flagComponent={FlagComponent}
focusInputOnCountrySelection
inputComponent={InputComponent}
international international
labels={EN} labels={EN}
focusInputOnCountrySelection onChange={(value) => onChange?.(value || ("" as RPNInput.Value))}
ref={ref}
/** /**
* Handles the onChange event. * Handles the onChange event.
* *
@ -59,7 +59,7 @@ const PhoneInput: React.ForwardRefExoticComponent<PhoneInputProps> = React.memo(
* *
* @param {E164Number | undefined} value - The entered value * @param {E164Number | undefined} value - The entered value
*/ */
onChange={(value) => onChange?.(value || ("" as RPNInput.Value))} smartCaret={false}
{...props} {...props}
/> />
); );
@ -111,8 +111,8 @@ const CountrySelect = React.memo(
const virtualizer = useVirtualizer({ const virtualizer = useVirtualizer({
count: filteredCountries.length, count: filteredCountries.length,
getScrollElement: () => parentNode,
estimateSize: () => 35, estimateSize: () => 35,
getScrollElement: () => parentNode,
overscan: 5, overscan: 5,
}); });
@ -129,13 +129,13 @@ const CountrySelect = React.memo(
}; };
return ( return (
<Popover open={open} onOpenChange={setOpen}> <Popover onOpenChange={setOpen} open={open}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
role="combobox"
variant="outline"
aria-expanded={open} aria-expanded={open}
className="flex gap-1 rounded-md rounded-e-none border-r-0 px-3 py-2 focus:z-10" className="flex gap-1 rounded-md rounded-e-none border-r-0 px-3 py-2 focus:z-10"
role="combobox"
variant="outline"
> >
<FlagComponent <FlagComponent
country={selectedCountry} country={selectedCountry}
@ -155,9 +155,9 @@ const CountrySelect = React.memo(
<PopoverContent className="w-[300px] p-0"> <PopoverContent className="w-[300px] p-0">
<Command shouldFilter={false}> <Command shouldFilter={false}>
<CommandInput <CommandInput
placeholder="Search country..."
onValueChange={handleSearch}
className="border-none ring-0 focus:ring-0 focus-visible:ring-0" className="border-none ring-0 focus:ring-0 focus-visible:ring-0"
onValueChange={handleSearch}
placeholder="Search country..."
/> />
<ScrollArea className="h-[400px] overflow-auto"> <ScrollArea className="h-[400px] overflow-auto">
@ -166,8 +166,8 @@ const CountrySelect = React.memo(
<div <div
style={{ style={{
height: `${virtualizer.getTotalSize()}px`, height: `${virtualizer.getTotalSize()}px`,
width: "100%",
position: "relative", position: "relative",
width: "100%",
}} }}
> >
{virtualizer {virtualizer
@ -178,16 +178,16 @@ const CountrySelect = React.memo(
return ( return (
<CommandItem <CommandItem
key={country.value} key={country.value}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: `${size}px`,
transform: `translateY(${start}px)`,
}}
// biome-ignore lint/style/noNonNullAssertion: <exists> // biome-ignore lint/style/noNonNullAssertion: <exists>
onSelect={() => onChange(country.value!)} onSelect={() => onChange(country.value!)}
style={{
height: `${size}px`,
left: 0,
position: "absolute",
top: 0,
transform: `translateY(${start}px)`,
width: "100%",
}}
value={country.value} value={country.value}
> >
<FlagComponent <FlagComponent

View file

@ -37,26 +37,25 @@ export const PricingChoice = () => {
return ( return (
<div className="text-primary flex flex-col items-center justify-center gap-4"> <div className="text-primary flex flex-col items-center justify-center gap-4">
<h2 className="text-3xl font-semibold">Scegli il tuo servizio</h2> <h2 className="text-3xl font-semibold">Scegli il tuo servizio</h2>
<Tabs defaultValue="transitorio" className="mb-2 w-full max-w-xl"> <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"> <TabsList className="flex h-auto w-full items-center justify-between gap-1 p-1">
<TabsTrigger <TabsTrigger
value="transitorio"
className="bg-transitorio [&[data-state=active]]:bg-transitorio w-full cursor-pointer px-2 text-xl sm:px-3" className="bg-transitorio [&[data-state=active]]:bg-transitorio w-full cursor-pointer px-2 text-xl sm:px-3"
value="transitorio"
> >
Transitorio Transitorio
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="stabile"
className="bg-stabile [&[data-state=active]]:bg-stabile w-full cursor-pointer px-2 text-xl sm:px-3" className="bg-stabile [&[data-state=active]]:bg-stabile w-full cursor-pointer px-2 text-xl sm:px-3"
value="stabile"
> >
Stabile Stabile
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="transitorio" className="w-full"> <TabsContent className="w-full" value="transitorio">
<PricingComponent <PricingComponent
title={t.prezzi.titolo_transitori} className="border-transitorio bg-transitorio"
cta={t.pricing_cmp.cta_breve} cta={t.pricing_cmp.cta_breve}
pricingData={pricingBreve}
opzioni={[ opzioni={[
t.pricing_cmp.breve_option1, t.pricing_cmp.breve_option1,
t.pricing_cmp.breve_option2, t.pricing_cmp.breve_option2,
@ -64,21 +63,22 @@ export const PricingChoice = () => {
t.pricing_cmp.breve_option4, t.pricing_cmp.breve_option4,
t.pricing_cmp.breve_option5, t.pricing_cmp.breve_option5,
]} ]}
className="border-transitorio bg-transitorio" pricingData={pricingBreve}
tipo={TipologiaPosizioneEnum.Transitorio} tipo={TipologiaPosizioneEnum.Transitorio}
title={t.prezzi.titolo_transitori}
/> />
</TabsContent> </TabsContent>
<TabsContent value="stabile"> <TabsContent value="stabile">
<PricingComponent <PricingComponent
title={t.prezzi.titolo_stabile} className="border-stabile bg-stabile"
cta={t.pricing_cmp.cta_stabile} cta={t.pricing_cmp.cta_stabile}
pricingData={pricingStabile}
opzioni={[ opzioni={[
t.pricing_cmp.stabile_option1, t.pricing_cmp.stabile_option1,
t.pricing_cmp.stabile_option2, t.pricing_cmp.stabile_option2,
]} ]}
className="border-stabile bg-stabile" pricingData={pricingStabile}
tipo={TipologiaPosizioneEnum.Stabile} tipo={TipologiaPosizioneEnum.Stabile}
title={t.prezzi.titolo_stabile}
/> />
</TabsContent> </TabsContent>
</Tabs> </Tabs>
@ -173,9 +173,9 @@ export const PricingComponent = ({
data-role="amount" data-role="amount"
> >
<AnimatedNumber <AnimatedNumber
value={secondPayment}
format={customFormat} format={customFormat}
precision={secondPayment % 1 === 0 ? 0 : 2} precision={secondPayment % 1 === 0 ? 0 : 2}
value={secondPayment}
/> />
</p> </p>
</div> </div>
@ -196,8 +196,8 @@ export const PricingComponent = ({
</div> </div>
) : ( ) : (
<Link <Link
href={"/prezzi#contratti"}
className="flex flex-col justify-center leading-none sm:h-16" className="flex flex-col justify-center leading-none sm:h-16"
href={"/prezzi#contratti"}
> >
<Button className="flex items-center gap-2"> <Button className="flex items-center gap-2">
Più info <ExternalLink className="size-4" /> Più info <ExternalLink className="size-4" />
@ -209,17 +209,17 @@ export const PricingComponent = ({
<div className="flex w-full flex-wrap items-center justify-center gap-2"> <div className="flex w-full flex-wrap items-center justify-center gap-2">
{opzioni.map((opzione, idx) => ( {opzioni.map((opzione, idx) => (
<Button <Button
key={idx}
size="default"
className={cn( className={cn(
"xs:h-10 xs:px-4 xs:py-2 h-9 rounded-md border-none px-3", "xs:h-10 xs:px-4 xs:py-2 h-9 rounded-md border-none px-3",
idx === Value ? "animate-in fade-in-0" : "", idx === Value ? "animate-in fade-in-0" : "",
)} )}
variant={idx === Value ? "default" : "outline"} key={idx}
onClick={() => { onClick={() => {
ClearCarousel(); ClearCarousel();
setValue(idx); setValue(idx);
}} }}
size="default"
variant={idx === Value ? "default" : "outline"}
> >
{opzione} {opzione}
</Button> </Button>
@ -282,24 +282,24 @@ export const PricingComponentConsulenza = ({
<div className="xs:gap-4 flex w-full items-center justify-center gap-0"> <div className="xs:gap-4 flex w-full items-center justify-center gap-0">
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
<p className="xs:text-6xl text-3xl font-semibold"> <p className="xs:text-6xl text-3xl font-semibold">
<AnimatedNumber value={p || 0} format={customFormat} /> <AnimatedNumber format={customFormat} value={p || 0} />
</p> </p>
</div> </div>
</div> </div>
<div className="flex w-full flex-wrap items-center justify-center gap-2"> <div className="flex w-full flex-wrap items-center justify-center gap-2">
{opzioni.map((opzione, idx) => ( {opzioni.map((opzione, idx) => (
<Button <Button
key={idx}
size="default"
className={cn( className={cn(
"xs:h-10 xs:px-4 xs:py-2 h-9 rounded-md border-none px-3", "xs:h-10 xs:px-4 xs:py-2 h-9 rounded-md border-none px-3",
idx === Value ? "animate-in fade-in-0" : "", idx === Value ? "animate-in fade-in-0" : "",
)} )}
variant={idx === Value ? "default" : "outline"} key={idx}
onClick={() => { onClick={() => {
ClearCarousel(); ClearCarousel();
setValue(idx); setValue(idx);
}} }}
size="default"
variant={idx === Value ? "default" : "outline"}
> >
{opzione} {opzione}
</Button> </Button>
@ -340,20 +340,19 @@ export const PrezziShow = () => {
principali: Affitto Transitorio e Affitto Stabile. principali: Affitto Transitorio e Affitto Stabile.
</p> </p>
</div> </div>
<Tabs defaultValue="transitorio" className="w-full sm:w-3/5"> <Tabs className="w-full sm:w-3/5" defaultValue="transitorio">
<TabsList className="flex h-auto w-full items-center justify-between"> <TabsList className="flex h-auto w-full items-center justify-between">
<TabsTrigger value="transitorio" className="w-full text-xl"> <TabsTrigger className="w-full text-xl" value="transitorio">
Affitto Transitorio Affitto Transitorio
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="stabile" className="w-full text-xl"> <TabsTrigger className="w-full text-xl" value="stabile">
Affitto Stabile Affitto Stabile
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="transitorio"> <TabsContent value="transitorio">
<PricingComponent <PricingComponent
title={t.prezzi.titolo_transitori} className="border-transitorio bg-transitorio"
cta={t.pricing_cmp.cta_breve} cta={t.pricing_cmp.cta_breve}
pricingData={pricingBreve}
opzioni={[ opzioni={[
t.pricing_cmp.breve_option1, t.pricing_cmp.breve_option1,
t.pricing_cmp.breve_option2, t.pricing_cmp.breve_option2,
@ -361,21 +360,22 @@ export const PrezziShow = () => {
t.pricing_cmp.breve_option4, t.pricing_cmp.breve_option4,
t.pricing_cmp.breve_option5, t.pricing_cmp.breve_option5,
]} ]}
pricingData={pricingBreve}
tipo={TipologiaPosizioneEnum.Transitorio} tipo={TipologiaPosizioneEnum.Transitorio}
className="border-transitorio bg-transitorio" title={t.prezzi.titolo_transitori}
/> />
</TabsContent> </TabsContent>
<TabsContent value="stabile"> <TabsContent value="stabile">
<PricingComponent <PricingComponent
title={t.prezzi.titolo_stabile} className="border-stabile bg-stabile"
cta={t.pricing_cmp.cta_stabile} cta={t.pricing_cmp.cta_stabile}
pricingData={pricingStabile}
opzioni={[ opzioni={[
t.pricing_cmp.stabile_option1, t.pricing_cmp.stabile_option1,
t.pricing_cmp.stabile_option2, t.pricing_cmp.stabile_option2,
]} ]}
className="border-stabile bg-stabile" pricingData={pricingStabile}
tipo={TipologiaPosizioneEnum.Stabile} tipo={TipologiaPosizioneEnum.Stabile}
title={t.prezzi.titolo_stabile}
/> />
</TabsContent> </TabsContent>
</Tabs> </Tabs>

View file

@ -44,15 +44,15 @@ export const ProgressRedirect = ({ href }: { href: string }) => {
}, [progress]); }, [progress]);
return ( return (
<div className="flex flex-col items-center justify-center gap-4"> <div className="flex flex-col items-center justify-center gap-4">
<Progress value={progress} className="h-3" /> <Progress className="h-3" value={progress} />
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Stai venendo reindirizzato... oppure Stai venendo reindirizzato... oppure
</p> </p>
<Link <Link
href={href}
className="font-semibold underline underline-offset-1" className="font-semibold underline underline-offset-1"
href={href}
> >
Clicca qui Clicca qui
</Link> </Link>

View file

@ -76,6 +76,10 @@ export const AnnuncioActions = () => {
const utils = api.useUtils(); const utils = api.useUtils();
const { mutate } = api.servizio.removeAnnuncioServizio.useMutation({ const { mutate } = api.servizio.removeAnnuncioServizio.useMutation({
onError: (error) => {
console.error(error);
toast.error("Errore durante la rimozione dell'annuncio");
},
onSuccess: async () => { onSuccess: async () => {
await utils.servizio.getAllServizioAnnunci.invalidate({ userId }); await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
await utils.servizio.getCompatibileAnnunci.invalidate({ await utils.servizio.getCompatibileAnnunci.invalidate({
@ -83,10 +87,6 @@ export const AnnuncioActions = () => {
}); });
toast.success("Annuncio rimosso"); toast.success("Annuncio rimosso");
}, },
onError: (error) => {
console.error(error);
toast.error("Errore durante la rimozione dell'annuncio");
},
}); });
if ((!isAdmin && data.open_contatti_at == null) || isAdmin) { if ((!isAdmin && data.open_contatti_at == null) || isAdmin) {
@ -117,29 +117,29 @@ export const AnnuncioActions = () => {
> >
<Button <Button
aria-label="Edit Annuncio" aria-label="Edit Annuncio"
variant="secondary"
className="flex w-full items-center gap-2" className="flex w-full items-center gap-2"
size="sm" size="sm"
variant="secondary"
> >
<Wrench className="size-6" /> <Wrench className="size-6" />
</Button> </Button>
</Link> </Link>
<Confirm <Confirm
title="Elimina annuncio"
description="Sei sicuro di voler eliminare l'annuncio?" description="Sei sicuro di voler eliminare l'annuncio?"
onConfirm={() => { onConfirm={() => {
mutate({ mutate({
servizioId,
annuncioId, annuncioId,
servizioId,
}); });
}} }}
title="Elimina annuncio"
> >
<Button <Button
aria-label="Delete Annuncio" aria-label="Delete Annuncio"
variant="destructive"
className="flex items-center gap-2" className="flex items-center gap-2"
size="sm" size="sm"
variant="destructive"
> >
<Trash2 className="size-6" /> <Trash2 className="size-6" />
</Button> </Button>
@ -153,7 +153,6 @@ export const AnnuncioActions = () => {
}; };
const ContrattoImportSchema = z.object({ const ContrattoImportSchema = z.object({
gestionale_id: z.number().nullable(),
contratto_decorrenza: z.string().nullable(), contratto_decorrenza: z.string().nullable(),
contratto_scadenza: z.string().nullable(), contratto_scadenza: z.string().nullable(),
contratto_tipo: z contratto_tipo: z
@ -167,6 +166,7 @@ const ContrattoImportSchema = z.object({
"Commerciale", "Commerciale",
]) ])
.nullable(), .nullable(),
gestionale_id: z.number().nullable(),
}); });
interface ContrattoImportObj interface ContrattoImportObj
@ -212,7 +212,6 @@ const ContrattoImport = ({
}) => { }) => {
const { data } = useServizioAnnuncio(); const { data } = useServizioAnnuncio();
const [contratto, setContratto] = useState<ContrattoImportObj | undefined>({ const [contratto, setContratto] = useState<ContrattoImportObj | undefined>({
gestionale_id: data.gestionale_id,
contratto_decorrenza: data.contratto_decorrenza contratto_decorrenza: data.contratto_decorrenza
? new Date(data.contratto_decorrenza) ? new Date(data.contratto_decorrenza)
: null, : null,
@ -220,6 +219,7 @@ const ContrattoImport = ({
? new Date(data.contratto_scadenza) ? new Date(data.contratto_scadenza)
: null, : null,
contratto_tipo: data.contratto_tipo as ContrattoImportObj["contratto_tipo"], contratto_tipo: data.contratto_tipo as ContrattoImportObj["contratto_tipo"],
gestionale_id: data.gestionale_id,
}); });
const handleSave = () => { const handleSave = () => {
handleUpdate({ handleUpdate({
@ -236,12 +236,12 @@ const ContrattoImport = ({
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Button <Button
aria-label="Import Contratto" aria-label="Import Contratto"
className="flex items-center gap-2"
onClick={async () => { onClick={async () => {
const clipboardText = await navigator.clipboard.readText(); const clipboardText = await navigator.clipboard.readText();
setContratto(parseContrattoImport(clipboardText)); setContratto(parseContrattoImport(clipboardText));
}} }}
className="flex items-center gap-2"
variant="info" variant="info"
> >
<ClipboardPaste className="size-4" /> <ClipboardPaste className="size-4" />
@ -249,15 +249,15 @@ const ContrattoImport = ({
</Button> </Button>
<Button <Button
aria-label="Reset Contratto" aria-label="Reset Contratto"
className="flex items-center gap-2"
onClick={async () => { onClick={async () => {
setContratto({ setContratto({
gestionale_id: null,
contratto_decorrenza: null, contratto_decorrenza: null,
contratto_scadenza: null, contratto_scadenza: null,
contratto_tipo: null, contratto_tipo: null,
gestionale_id: null,
}); });
}} }}
className="flex items-center gap-2"
variant="destructive" variant="destructive"
> >
<Trash2 className="size-4" /> <Trash2 className="size-4" />
@ -271,6 +271,7 @@ const ContrattoImport = ({
{contratto.gestionale_id}{" "} {contratto.gestionale_id}{" "}
<Button <Button
aria-label="Edit Gestionale ID" aria-label="Edit Gestionale ID"
className="h-6 px-1.5 [&_svg]:size-4"
onClick={() => { onClick={() => {
const newValue = prompt( const newValue = prompt(
"Modifica l'ID gestionale del contratto:", "Modifica l'ID gestionale del contratto:",
@ -284,7 +285,6 @@ const ContrattoImport = ({
); );
} }
}} }}
className="h-6 px-1.5 [&_svg]:size-4"
size="sm" size="sm"
> >
<Pen /> <Pen />
@ -298,6 +298,7 @@ const ContrattoImport = ({
)}{" "} )}{" "}
<Button <Button
aria-label="Edit Decorrenza" aria-label="Edit Decorrenza"
className="h-6 px-1.5 [&_svg]:size-4"
onClick={() => { onClick={() => {
const newValue = prompt( const newValue = prompt(
"Modifica la decorrenza del contratto (DD/MM/YYYY):", "Modifica la decorrenza del contratto (DD/MM/YYYY):",
@ -318,7 +319,6 @@ const ContrattoImport = ({
); );
} }
}} }}
className="h-6 px-1.5 [&_svg]:size-4"
size="sm" size="sm"
> >
<Pen /> <Pen />
@ -332,6 +332,7 @@ const ContrattoImport = ({
)}{" "} )}{" "}
<Button <Button
aria-label="Edit Scadenza" aria-label="Edit Scadenza"
className="h-6 px-1.5 [&_svg]:size-4"
onClick={() => { onClick={() => {
const newValue = prompt( const newValue = prompt(
"Modifica la scadenza del contratto (DD/MM/YYYY):", "Modifica la scadenza del contratto (DD/MM/YYYY):",
@ -352,7 +353,6 @@ const ContrattoImport = ({
); );
} }
}} }}
className="h-6 px-1.5 [&_svg]:size-4"
size="sm" size="sm"
> >
<Pen /> <Pen />
@ -363,6 +363,7 @@ const ContrattoImport = ({
{contratto.contratto_tipo}{" "} {contratto.contratto_tipo}{" "}
<Button <Button
aria-label="Edit Tipo di contratto" aria-label="Edit Tipo di contratto"
className="h-6 px-1.5 [&_svg]:size-4"
onClick={() => { onClick={() => {
const newValue = prompt( const newValue = prompt(
"Modifica il tipo di contratto:", "Modifica il tipo di contratto:",
@ -385,7 +386,6 @@ const ContrattoImport = ({
); );
} }
}} }}
className="h-6 px-1.5 [&_svg]:size-4"
size="sm" size="sm"
> >
<Pen /> <Pen />
@ -421,6 +421,9 @@ const AdminContratto = () => {
const { mutate: updateServizio } = const { mutate: updateServizio } =
api.servizio.updateServizioAnnunci.useMutation({ api.servizio.updateServizioAnnunci.useMutation({
onError: (error) => {
toast.error(error.message);
},
onSuccess: async () => { onSuccess: async () => {
await utils.servizio.getAllServizioAnnunci.invalidate({ userId }); await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
await utils.servizio.getServizio.invalidate({ await utils.servizio.getServizio.invalidate({
@ -429,28 +432,25 @@ const AdminContratto = () => {
toast.success("Dati salvati con successo"); toast.success("Dati salvati con successo");
}, },
onError: (error) => {
toast.error(error.message);
},
}); });
const handleUpdate = (upd: ServizioAnnunciUpdate) => { const handleUpdate = (upd: ServizioAnnunciUpdate) => {
updateServizio({ updateServizio({
servizioId,
annuncioId, annuncioId,
data: { data: {
...upd, ...upd,
}, },
servizioId,
}); });
}; };
return ( return (
<Credenza open={open} onOpenChange={setOpen}> <Credenza onOpenChange={setOpen} open={open}>
<CredenzaTrigger asChild> <CredenzaTrigger asChild>
<Button <Button
variant={data.doc_contratto_added ? "success" : "secondary"}
className="flex w-fit items-center gap-2"
aria-label="Open Contratto Section" aria-label="Open Contratto Section"
className="flex w-fit items-center gap-2"
variant={data.doc_contratto_added ? "success" : "secondary"}
> >
{data.doc_contratto_added ? ( {data.doc_contratto_added ? (
<CircleCheck className="size-6" /> <CircleCheck className="size-6" />
@ -479,7 +479,7 @@ const AdminContratto = () => {
<CredenzaFooter> <CredenzaFooter>
<CredenzaClose asChild> <CredenzaClose asChild>
<Button variant="outline" aria-label="Close Credenza"> <Button aria-label="Close Credenza" variant="outline">
Chiudi Chiudi
</Button> </Button>
</CredenzaClose> </CredenzaClose>
@ -515,13 +515,13 @@ const ContrattoSection = ({
const handleSave = () => { const handleSave = () => {
handleUpdate({ handleUpdate({
doc_contratto_ref: selectedDoc,
doc_contratto_added: selectedDoc != null, doc_contratto_added: selectedDoc != null,
doc_contratto_ref: selectedDoc,
}); });
if (selectedDoc) { if (selectedDoc) {
setUserStorage({ setUserStorage({
userId,
fileId: selectedDoc, fileId: selectedDoc,
userId,
}); });
} }
}; };
@ -542,15 +542,15 @@ const ContrattoSection = ({
<DocInfo storageId={selectedDoc}> <DocInfo storageId={selectedDoc}>
<Button <Button
aria-label="Reset Contratto" aria-label="Reset Contratto"
variant="destructive"
className="flex items-center gap-3" className="flex items-center gap-3"
onClick={() => { onClick={() => {
handleUpdate({ handleUpdate({
doc_contratto_ref: null,
doc_contratto_added: false, doc_contratto_added: false,
doc_contratto_ref: null,
}); });
setSelectedDoc(null); setSelectedDoc(null);
}} }}
variant="destructive"
> >
<Trash2 className="size-6" /> <Trash2 className="size-6" />
</Button> </Button>
@ -558,11 +558,11 @@ const ContrattoSection = ({
</div> </div>
)} )}
<DocCombo <DocCombo
current={selectedDoc}
files={files} files={files}
onSelect={(f) => { onSelect={(f) => {
setSelectedDoc(f.id); setSelectedDoc(f.id);
}} }}
current={selectedDoc}
/> />
<UploadModal isAdmin userId={userId} /> <UploadModal isAdmin userId={userId} />
</CardContent> </CardContent>
@ -607,13 +607,13 @@ const RicevutaSection = ({
const handleSave = () => { const handleSave = () => {
handleUpdate({ handleUpdate({
doc_registrazione_ref: selectedDoc,
doc_registrazione_added: selectedDoc != null, doc_registrazione_added: selectedDoc != null,
doc_registrazione_ref: selectedDoc,
}); });
if (selectedDoc) { if (selectedDoc) {
setUserStorage({ setUserStorage({
userId,
fileId: selectedDoc, fileId: selectedDoc,
userId,
}); });
} }
}; };
@ -634,15 +634,15 @@ const RicevutaSection = ({
<DocInfo storageId={selectedDoc}> <DocInfo storageId={selectedDoc}>
<Button <Button
aria-label="Reset Ricevuta" aria-label="Reset Ricevuta"
variant="destructive"
className="flex items-center gap-3" className="flex items-center gap-3"
onClick={() => { onClick={() => {
handleUpdate({ handleUpdate({
doc_registrazione_ref: null,
doc_registrazione_added: false, doc_registrazione_added: false,
doc_registrazione_ref: null,
}); });
setSelectedDoc(null); setSelectedDoc(null);
}} }}
variant="destructive"
> >
<Trash2 className="size-6" /> <Trash2 className="size-6" />
</Button> </Button>
@ -650,11 +650,11 @@ const RicevutaSection = ({
</div> </div>
)} )}
<DocCombo <DocCombo
current={selectedDoc}
files={files} files={files}
onSelect={(f) => { onSelect={(f) => {
setSelectedDoc(f.id); setSelectedDoc(f.id);
}} }}
current={selectedDoc}
/> />
<UploadModal isAdmin userId={userId} /> <UploadModal isAdmin userId={userId} />
</CardContent> </CardContent>
@ -683,6 +683,9 @@ const AdminLavoraConferma = () => {
const { mutate: updateServizio } = const { mutate: updateServizio } =
api.servizio.updateServizioAnnunci.useMutation({ api.servizio.updateServizioAnnunci.useMutation({
onError: (error) => {
toast.error(error.message);
},
onSuccess: async () => { onSuccess: async () => {
await utils.servizio.getAllServizioAnnunci.invalidate({ userId }); await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
await utils.servizio.getServizio.invalidate({ await utils.servizio.getServizio.invalidate({
@ -691,28 +694,25 @@ const AdminLavoraConferma = () => {
toast.success("Dati salvati con successo"); toast.success("Dati salvati con successo");
}, },
onError: (error) => {
toast.error(error.message);
},
}); });
const handleUpdate = (upd: ServizioAnnunciUpdate) => { const handleUpdate = (upd: ServizioAnnunciUpdate) => {
updateServizio({ updateServizio({
servizioId,
annuncioId, annuncioId,
data: { data: {
...upd, ...upd,
}, },
servizioId,
}); });
}; };
return ( return (
<Credenza open={open} onOpenChange={setOpen}> <Credenza onOpenChange={setOpen} open={open}>
<CredenzaTrigger asChild> <CredenzaTrigger asChild>
<Button <Button
variant={data.hasConfermaAdmin ? "success" : "secondary"}
className="flex w-fit items-center gap-2"
aria-label="Open Lavora Conferma Section" aria-label="Open Lavora Conferma Section"
className="flex w-fit items-center gap-2"
variant={data.hasConfermaAdmin ? "success" : "secondary"}
> >
{data.hasConfermaAdmin ? ( {data.hasConfermaAdmin ? (
<CircleCheck className="size-6" /> <CircleCheck className="size-6" />
@ -741,7 +741,7 @@ const AdminLavoraConferma = () => {
<CredenzaFooter> <CredenzaFooter>
<CredenzaClose asChild> <CredenzaClose asChild>
<Button variant="outline" aria-label="Close Credenza"> <Button aria-label="Close Credenza" variant="outline">
Chiudi Chiudi
</Button> </Button>
</CredenzaClose> </CredenzaClose>
@ -790,14 +790,14 @@ const DocSection = ({
<DocInfo storageId={data.doc_conferma_ref}> <DocInfo storageId={data.doc_conferma_ref}>
<Button <Button
aria-label="Reset Conferma" aria-label="Reset Conferma"
variant="destructive"
className="flex items-center gap-3" className="flex items-center gap-3"
onClick={() => { onClick={() => {
handleUpdate({ handleUpdate({
doc_conferma_ref: null,
doc_conferma_added: false, doc_conferma_added: false,
doc_conferma_ref: null,
}); });
}} }}
variant="destructive"
> >
<Trash2 /> <Trash2 />
</Button> </Button>
@ -806,19 +806,19 @@ const DocSection = ({
)} )}
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
<DocCombo <DocCombo
current={data.doc_conferma_ref}
files={files} files={files}
onSelect={(f) => { onSelect={(f) => {
handleUpdate({ handleUpdate({
doc_conferma_ref: f.id,
doc_conferma_added: true, doc_conferma_added: true,
doc_conferma_ref: f.id,
}); });
setUserStorage({ setUserStorage({
userId,
fileId: f.id, fileId: f.id,
userId,
}); });
}} }}
current={data.doc_conferma_ref}
/> />
<UploadModal isAdmin userId={userId} /> <UploadModal isAdmin userId={userId} />
</div> </div>
@ -845,15 +845,15 @@ const DocInfo = ({
<div className="flex w-full flex-1"> <div className="flex w-full flex-1">
<Link <Link
aria-label="Open Selected File" aria-label="Open Selected File"
href={`/area-riservata/allegato-view/${storageId}`}
target="_blank"
className={cn( className={cn(
buttonVariants({ buttonVariants({
variant: "outline", variant: "outline",
}), }),
"flex w-fit grow items-center gap-2", "flex w-fit grow items-center gap-2",
)} )}
href={`/area-riservata/allegato-view/${storageId}`}
id="selected-file" id="selected-file"
target="_blank"
> >
{file_info.ext && <ExtIcon ext={file_info.ext} />} {file_info.ext && <ExtIcon ext={file_info.ext} />}
<span className="max-w-40 truncate sm:max-w-80"> <span className="max-w-40 truncate sm:max-w-80">
@ -880,14 +880,14 @@ const DocCombo = ({
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
return ( return (
<Popover open={open} onOpenChange={setOpen}> <Popover onOpenChange={setOpen} open={open}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
aria-label="Select Existing File"
variant="outline"
role="combobox"
aria-expanded={open} aria-expanded={open}
aria-label="Select Existing File"
className="w-auto justify-between" className="w-auto justify-between"
role="combobox"
variant="outline"
> >
Seleziona file esistente... Seleziona file esistente...
<ChevronsUpDown className="ml-2 size-4 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 size-4 shrink-0 opacity-50" />
@ -902,7 +902,6 @@ const DocCombo = ({
{files.map((file) => ( {files.map((file) => (
<CommandItem <CommandItem
key={file.id} key={file.id}
value={file.id}
onSelect={(currentValue) => { onSelect={(currentValue) => {
if (currentValue !== current) { if (currentValue !== current) {
// biome-ignore lint/style/noNonNullAssertion: <exists> // biome-ignore lint/style/noNonNullAssertion: <exists>
@ -910,6 +909,7 @@ const DocCombo = ({
} }
setOpen(false); setOpen(false);
}} }}
value={file.id}
> >
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
{file.ext && <ExtIcon ext={file.ext} />} {file.ext && <ExtIcon ext={file.ext} />}
@ -985,12 +985,12 @@ const Caparra = ({
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
<Button <Button
aria-label="Import Caparra" aria-label="Import Caparra"
className="flex items-center gap-2"
onClick={async () => { onClick={async () => {
const clipboardText = await navigator.clipboard.readText(); const clipboardText = await navigator.clipboard.readText();
setCaparra(parseCaparra(clipboardText)); setCaparra(parseCaparra(clipboardText));
}} }}
className="flex items-center gap-2"
variant="info" variant="info"
> >
<ClipboardPaste className="size-4" /> <ClipboardPaste className="size-4" />
@ -998,6 +998,7 @@ const Caparra = ({
</Button> </Button>
<Button <Button
aria-label="Reset Caparra" aria-label="Reset Caparra"
className="flex items-center gap-2"
onClick={async () => { onClick={async () => {
setCaparra({ setCaparra({
caparra_causale: null, caparra_causale: null,
@ -1006,7 +1007,6 @@ const Caparra = ({
caparra_intestazione: null, caparra_intestazione: null,
}); });
}} }}
className="flex items-center gap-2"
variant="destructive" variant="destructive"
> >
<Trash2 className="size-4" /> <Trash2 className="size-4" />
@ -1019,6 +1019,7 @@ const Caparra = ({
{caparra.caparra_intestazione}{" "} {caparra.caparra_intestazione}{" "}
<Button <Button
aria-label="Edit Intestazione" aria-label="Edit Intestazione"
className="h-6 px-1.5 [&_svg]:size-4"
onClick={() => { onClick={() => {
const newValue = prompt( const newValue = prompt(
"Modifica l'intestazione della caparra:", "Modifica l'intestazione della caparra:",
@ -1032,7 +1033,6 @@ const Caparra = ({
); );
} }
}} }}
className="h-6 px-1.5 [&_svg]:size-4"
size="sm" size="sm"
> >
<Pen /> <Pen />
@ -1043,6 +1043,7 @@ const Caparra = ({
{caparra.caparra_iban}{" "} {caparra.caparra_iban}{" "}
<Button <Button
aria-label="Edit IBAN" aria-label="Edit IBAN"
className="h-6 px-1.5 [&_svg]:size-4"
onClick={() => { onClick={() => {
const newValue = prompt( const newValue = prompt(
"Modifica l'IBAN della caparra:", "Modifica l'IBAN della caparra:",
@ -1054,7 +1055,6 @@ const Caparra = ({
); );
} }
}} }}
className="h-6 px-1.5 [&_svg]:size-4"
size="sm" size="sm"
> >
<Pen /> <Pen />
@ -1065,6 +1065,7 @@ const Caparra = ({
{caparra.caparra_importo}{" "} {caparra.caparra_importo}{" "}
<Button <Button
aria-label="Edit Importo" aria-label="Edit Importo"
className="h-6 px-1.5 [&_svg]:size-4"
onClick={() => { onClick={() => {
const newValue = prompt( const newValue = prompt(
"Modifica l'importo della caparra:", "Modifica l'importo della caparra:",
@ -1076,7 +1077,6 @@ const Caparra = ({
); );
} }
}} }}
className="h-6 px-1.5 [&_svg]:size-4"
size="sm" size="sm"
> >
<Pen /> <Pen />
@ -1087,6 +1087,7 @@ const Caparra = ({
{caparra.caparra_causale}{" "} {caparra.caparra_causale}{" "}
<Button <Button
aria-label="Edit Causale" aria-label="Edit Causale"
className="h-6 px-1.5 [&_svg]:size-4"
onClick={() => { onClick={() => {
const newValue = prompt( const newValue = prompt(
"Modifica la causale della caparra:", "Modifica la causale della caparra:",
@ -1098,7 +1099,6 @@ const Caparra = ({
); );
} }
}} }}
className="h-6 px-1.5 [&_svg]:size-4"
size="sm" size="sm"
> >
<Pen /> <Pen />
@ -1183,8 +1183,8 @@ const SendConferma = ({
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Switch <Switch
aria-label="Conferma Lavorata" aria-label="Conferma Lavorata"
id="conferma"
checked={check} checked={check}
id="conferma"
onCheckedChange={(c) => { onCheckedChange={(c) => {
setCheck(c); setCheck(c);
}} }}
@ -1213,6 +1213,10 @@ export const AddButton = ({ annuncioId }: { annuncioId: AnnunciId }) => {
const { servizioId, userId } = useServizio(); const { servizioId, userId } = useServizio();
const utils = api.useUtils(); const utils = api.useUtils();
const { mutate } = api.servizio.addServizioAnnunci.useMutation({ const { mutate } = api.servizio.addServizioAnnunci.useMutation({
onError: (error) => {
console.error(error);
toast.error("Errore durante l'aggiunta dell'annuncio");
},
onSuccess: async () => { onSuccess: async () => {
await utils.servizio.getAllServizioAnnunci.invalidate({ userId }); await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
await utils.servizio.getAnnunciAvailableToAdd.invalidate({ await utils.servizio.getAnnunciAvailableToAdd.invalidate({
@ -1227,29 +1231,25 @@ export const AddButton = ({ annuncioId }: { annuncioId: AnnunciId }) => {
toast.success("Annuncio aggiunto alla ricerca"); toast.success("Annuncio aggiunto alla ricerca");
}, },
onError: (error) => {
console.error(error);
toast.error("Errore durante l'aggiunta dell'annuncio");
},
}); });
const buttonRef = useRef<AnimatedButtonRef>(null); const buttonRef = useRef<AnimatedButtonRef>(null);
return ( return (
<AnimatedButton <AnimatedButton
ref={buttonRef} className="group flex w-full gap-2"
sparklesIcon="thumbs-up"
sparklesNumber={15}
sparklesRadius={60}
sparklesClassName="fill-yellow-500 stroke-transparent"
sparklesScale={[1, 1.5]}
onClick={async () => { onClick={async () => {
buttonRef.current?.sparkle(); buttonRef.current?.sparkle();
//wait for the animation to finish //wait for the animation to finish
await new Promise((resolve) => setTimeout(resolve, 1000)); await new Promise((resolve) => setTimeout(resolve, 1000));
mutate({ servizioId, annunci: [annuncioId] }); 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" variant="info"
className="group flex w-full gap-2"
> >
<Plus className="size-5" /> <Plus className="size-5" />
<span>Aggiungi agli annunci da contattare</span> <span>Aggiungi agli annunci da contattare</span>

View file

@ -34,9 +34,9 @@ export const AnnuncioCard = ({ className }: { className?: string }) => {
return ( return (
<BasicAnnuncioCard <BasicAnnuncioCard
data={data}
className={className}
actions={<AnnuncioActions />} actions={<AnnuncioActions />}
className={className}
data={data}
interactions={<Interactions />} interactions={<Interactions />}
/> />
); );
@ -77,16 +77,16 @@ export const BasicAnnuncioCard = ({
<CardContent className="px-4"> <CardContent className="px-4">
<div className="flex w-full flex-col gap-4 md:flex-row md:gap-10"> <div className="flex w-full flex-col gap-4 md:flex-row md:gap-10">
<ImageFlbk <ImageFlbk
alt={data.codice}
className="h-auto w-full rounded-md object-contain sm:max-w-xs"
height={1080}
priority
src={ src={
(data.url_immagini && (data.url_immagini &&
`/go-api/images/get/${data.url_immagini[0]}`) || `/go-api/images/get/${data.url_immagini[0]}`) ||
"/fallback-image.png" "/fallback-image.png"
} }
priority
width={1920} width={1920}
height={1080}
className="h-auto w-full rounded-md object-contain sm:max-w-xs"
alt={data.codice}
/> />
<div className="flex w-full flex-col gap-5"> <div className="flex w-full flex-col gap-5">
{interactions} {interactions}
@ -95,11 +95,11 @@ export const BasicAnnuncioCard = ({
<TipoBadge tipo={data.tipo} /> <TipoBadge tipo={data.tipo} />
<span> <span>
{handleConsegna({ {handleConsegna({
aggiornamento: t.card.in_aggiornamento,
consegna: data.consegna, consegna: data.consegna,
consegna_da: t.card.consegna_da, consegna_da: t.card.consegna_da,
mesi: t.preferenze.mesi, mesi: t.preferenze.mesi,
subito: t.card.consegna_subito, subito: t.card.consegna_subito,
aggiornamento: t.card.in_aggiornamento,
})} })}
</span> </span>
<span>{formatCurrency(data.prezzo / 1e2)}</span> <span>{formatCurrency(data.prezzo / 1e2)}</span>
@ -110,12 +110,12 @@ export const BasicAnnuncioCard = ({
<AnnuncioDettaglio data={data} /> <AnnuncioDettaglio data={data} />
{data.stato !== "Sospeso" && data.web && ( {data.stato !== "Sospeso" && data.web && (
<Link <Link
href={`/annuncio/${data.codice}`}
aria-label="Pagina Annuncio" aria-label="Pagina Annuncio"
href={`/annuncio/${data.codice}`}
> >
<Button <Button
variant="outline"
className="flex items-center gap-2" className="flex items-center gap-2"
variant="outline"
> >
<ExternalLink className="size-6" /> <ExternalLink className="size-6" />
</Button> </Button>
@ -173,9 +173,9 @@ const AnnuncioDettaglio = ({
<Credenza> <Credenza>
<CredenzaTrigger asChild> <CredenzaTrigger asChild>
<Button <Button
variant="outline"
className="flex items-center gap-2"
aria-label="Dettagli Annuncio" aria-label="Dettagli Annuncio"
className="flex items-center gap-2"
variant="outline"
> >
<UnfoldVertical className="size-6" /> Dettagli annuncio <UnfoldVertical className="size-6" /> Dettagli annuncio
</Button> </Button>
@ -197,21 +197,21 @@ const AnnuncioDettaglio = ({
<CarouselItem key={img}> <CarouselItem key={img}>
<ImageFlbk <ImageFlbk
alt={`${data.codice} - ${idx}`} alt={`${data.codice} - ${idx}`}
width={400} className={"h-80 w-full object-contain sm:h-[35rem]"}
height={200} height={200}
priority={idx === 0} priority={idx === 0}
src={`/go-api/images/get/${img}`} src={`/go-api/images/get/${img}`}
className={"h-80 w-full object-contain sm:h-[35rem]"} width={400}
/> />
</CarouselItem> </CarouselItem>
))} ))}
</CarouselContent> </CarouselContent>
<div <div
className="block opacity-0 transition-all duration-200 group-hover:opacity-60 disabled:group-hover:opacity-0"
onClick={(e) => e.preventDefault()}
onKeyDown={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()}
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={(e) => e.preventDefault()}
className="block opacity-0 transition-all duration-200 group-hover:opacity-60 disabled:group-hover:opacity-0"
> >
<CarouselPrevious <CarouselPrevious
className="left-2 cursor-pointer" className="left-2 cursor-pointer"

View file

@ -22,6 +22,7 @@ export const FileSection = ({ storageId }: { storageId: StorageindexId }) => {
/> />
<Button <Button
className="underline underline-offset-1 after:content-['_↗']"
onClick={() => onClick={() =>
handleDownload({ handleDownload({
file: fileInfos, file: fileInfos,
@ -29,7 +30,6 @@ export const FileSection = ({ storageId }: { storageId: StorageindexId }) => {
}) })
} }
variant="link" variant="link"
className="underline underline-offset-1 after:content-['_↗']"
> >
{t.file_section.download} {t.file_section.download}
</Button> </Button>

View file

@ -63,8 +63,8 @@ export const Interactions = () => {
href={`/servizio/conferma-immobile/${servizioId}/${annuncioId}`} href={`/servizio/conferma-immobile/${servizioId}/${annuncioId}`}
> >
<Button <Button
variant="success"
className="flex w-fit items-center gap-2" className="flex w-fit items-center gap-2"
variant="success"
> >
<Pointer className="size-6" /> <Pointer className="size-6" />
Confermato - Procedi ora Confermato - Procedi ora
@ -75,8 +75,8 @@ export const Interactions = () => {
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
variant="success"
className="flex w-fit cursor-not-allowed items-center gap-2 opacity-50" className="flex w-fit cursor-not-allowed items-center gap-2 opacity-50"
variant="success"
> >
<Pointer className="size-6" /> <Pointer className="size-6" />
Confermato - Procedi ora Confermato - Procedi ora
@ -96,7 +96,7 @@ export const Interactions = () => {
aria-label="Conferma immobile" aria-label="Conferma immobile"
href={`/servizio/riapri-conferma/${servizioId}/${annuncioId}`} href={`/servizio/riapri-conferma/${servizioId}/${annuncioId}`}
> >
<Button variant="success" className="flex w-fit items-center gap-2"> <Button className="flex w-fit items-center gap-2" variant="success">
<Pointer className="size-6" /> <Pointer className="size-6" />
Rivedi la conferma Rivedi la conferma
</Button> </Button>
@ -234,14 +234,14 @@ export const SaldoButton = ({
return ( return (
<Button <Button
aria-label="Procedi al saldo" aria-label="Procedi al saldo"
variant={variant}
className={cn("flex w-fit items-center gap-2", className)} className={cn("flex w-fit items-center gap-2", className)}
onClick={() => { onClick={() => {
mutate({ mutate({
servizioId,
annuncioId, annuncioId,
servizioId,
}); });
}} }}
variant={variant}
> >
<PackageCheck className="size-6" /> <PackageCheck className="size-6" />
Procedi al saldo Procedi al saldo
@ -275,14 +275,14 @@ const SaldoConsulenzaButton = ({
return ( return (
<Button <Button
aria-label="Procedi al saldo consulenza" aria-label="Procedi al saldo consulenza"
variant={variant}
className={cn("flex w-fit items-center gap-2", className)} className={cn("flex w-fit items-center gap-2", className)}
onClick={() => { onClick={() => {
mutate({ mutate({
servizioId,
annuncioId, annuncioId,
servizioId,
}); });
}} }}
variant={variant}
> >
<PackageCheck className="size-6" /> <PackageCheck className="size-6" />
Procedi al saldo della consulenza Procedi al saldo della consulenza
@ -297,12 +297,12 @@ const AnnuncioConfirm = () => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const utils = api.useUtils(); const utils = api.useUtils();
const { mutate, isPending } = api.servizio.userConfirmImmobile.useMutation({ const { mutate, isPending } = api.servizio.userConfirmImmobile.useMutation({
onMutate: () => {
toast.loading("Conferma in corso", { id: "updateRichiestaToast" });
},
onError: (err) => { onError: (err) => {
toast.error(err.message, { id: "updateRichiestaToast" }); toast.error(err.message, { id: "updateRichiestaToast" });
}, },
onMutate: () => {
toast.loading("Conferma in corso", { id: "updateRichiestaToast" });
},
onSuccess: async () => { onSuccess: async () => {
toast.success("Richiesta confermata con successo", { toast.success("Richiesta confermata con successo", {
id: "updateRichiestaToast", id: "updateRichiestaToast",
@ -313,11 +313,11 @@ const AnnuncioConfirm = () => {
}, },
}); });
return ( return (
<Credenza open={open} onOpenChange={setOpen}> <Credenza onOpenChange={setOpen} open={open}>
<CredenzaTrigger asChild> <CredenzaTrigger asChild>
<Button <Button
className="flex w-fit items-center gap-2"
aria-label="Conferma immobile" aria-label="Conferma immobile"
className="flex w-fit items-center gap-2"
> >
<Handshake className="size-6" /> Conferma immobile <Handshake className="size-6" /> Conferma immobile
</Button> </Button>
@ -336,13 +336,13 @@ const AnnuncioConfirm = () => {
<LoadingButton <LoadingButton
aria-label="Conferma immobile" aria-label="Conferma immobile"
loading={isPending} loading={isPending}
variant="success"
onClick={() => { onClick={() => {
mutate({ mutate({
annuncioId, annuncioId,
servizioId, servizioId,
}); });
}} }}
variant="success"
> >
{t.conferma} {t.conferma}
</LoadingButton> </LoadingButton>
@ -361,8 +361,8 @@ const ConfermaLavorazioneModal = () => {
<Credenza> <Credenza>
<CredenzaTrigger asChild> <CredenzaTrigger asChild>
<Button <Button
className="flex w-fit items-center gap-2"
aria-label="Conferma in lavorazione" aria-label="Conferma in lavorazione"
className="flex w-fit items-center gap-2"
> >
<CircleCheck className="size-7" /> <CircleCheck className="size-7" />
Conferma in lavorazione Conferma in lavorazione
@ -381,10 +381,10 @@ const ConfermaLavorazioneModal = () => {
{data.user_confirmed_at && {data.user_confirmed_at &&
new Date(data.user_confirmed_at).toLocaleString("it", { new Date(data.user_confirmed_at).toLocaleString("it", {
day: "2-digit", day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit", hour: "2-digit",
minute: "2-digit", minute: "2-digit",
month: "2-digit",
year: "numeric",
})} })}
</p> </p>
<p> <p>
@ -396,7 +396,7 @@ const ConfermaLavorazioneModal = () => {
<CredenzaFooter> <CredenzaFooter>
<CredenzaClose asChild> <CredenzaClose asChild>
<Button variant="outline" aria-label="Chiudi modal credenza"> <Button aria-label="Chiudi modal credenza" variant="outline">
{t.chiudi} {t.chiudi}
</Button> </Button>
</CredenzaClose> </CredenzaClose>
@ -416,6 +416,11 @@ const ContattiProprietarioModal = () => {
const utils = api.useUtils(); const utils = api.useUtils();
const { mutate } = api.servizio.unlockServizioContatti.useMutation({ const { mutate } = api.servizio.unlockServizioContatti.useMutation({
onError: (err) => {
toast.error(err.message, {
id: "unlock-contacts",
});
},
onMutate: () => { onMutate: () => {
toast.loading("Caricamento ...", { toast.loading("Caricamento ...", {
id: "unlock-contacts", id: "unlock-contacts",
@ -436,11 +441,6 @@ const ContattiProprietarioModal = () => {
}); });
setShowContacts(true); setShowContacts(true);
}, },
onError: (err) => {
toast.error(err.message, {
id: "unlock-contacts",
});
},
}); });
const d = new Date(); const d = new Date();
@ -452,18 +452,18 @@ const ContattiProprietarioModal = () => {
<CredenzaTrigger asChild> <CredenzaTrigger asChild>
{showContacts ? ( {showContacts ? (
<Button <Button
variant="success"
className="flex w-fit items-center gap-2"
aria-label="Vedi contatti proprietario" aria-label="Vedi contatti proprietario"
className="flex w-fit items-center gap-2"
variant="success"
> >
<CircleUser className="size-7" /> <CircleUser className="size-7" />
Vedi i contatti Vedi i contatti
</Button> </Button>
) : ( ) : (
<Button <Button
variant="warning"
className="flex w-fit items-center gap-2"
aria-label="Contatta proprietario" aria-label="Contatta proprietario"
className="flex w-fit items-center gap-2"
variant="warning"
> >
<CircleUser className="size-7" /> <CircleUser className="size-7" />
Ottieni i contatti Ottieni i contatti
@ -504,19 +504,19 @@ const ContattiProprietarioModal = () => {
{!showContacts && ( {!showContacts && (
<Button <Button
aria-label="Contatta proprietario" aria-label="Contatta proprietario"
variant="success"
onClick={() => onClick={() =>
mutate({ mutate({
servizioId,
annuncioId, annuncioId,
servizioId,
}) })
} }
variant="success"
> >
{t.accetta} {t.accetta}
</Button> </Button>
)} )}
<CredenzaClose asChild> <CredenzaClose asChild>
<Button variant="outline" aria-label="Chiudi modal credenza"> <Button aria-label="Chiudi modal credenza" variant="outline">
{t.chiudi} {t.chiudi}
</Button> </Button>
</CredenzaClose> </CredenzaClose>
@ -575,8 +575,8 @@ const ContattiProprietarioData = () => {
{t.contatto.nome}: {data.propData.locatore} {t.contatto.nome}: {data.propData.locatore}
</p> </p>
<a <a
href={`tel:${data.propData.numero}`}
className="flex items-center gap-2" className="flex items-center gap-2"
href={`tel:${data.propData.numero}`}
> >
<Phone />: <Phone />:
<span className="text-blue-600 underline">{data.propData.numero}</span> <span className="text-blue-600 underline">{data.propData.numero}</span>
@ -589,10 +589,10 @@ const ContattiProprietarioData = () => {
) )
</p> </p>
<a <a
target="_blank"
rel="noreferrer"
href={navigationUrl}
className="border-border flex w-fit items-center gap-2 rounded-md border px-3 py-2" className="border-border flex w-fit items-center gap-2 rounded-md border px-3 py-2"
href={navigationUrl}
rel="noreferrer"
target="_blank"
> >
<Navigation /> <span>{t.contatto.apri_nav}</span> <Navigation /> <span>{t.contatto.apri_nav}</span>
</a> </a>
@ -601,10 +601,10 @@ const ContattiProprietarioData = () => {
{t.contatto.sbloccato_il}:{" "} {t.contatto.sbloccato_il}:{" "}
{data.opened_at.toLocaleString("it", { {data.opened_at.toLocaleString("it", {
day: "2-digit", day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit", hour: "2-digit",
minute: "2-digit", minute: "2-digit",
month: "2-digit",
year: "numeric",
})} })}
</p> </p>
<div className="flex items-center gap-2 rounded-md bg-sky-200 p-2"> <div className="flex items-center gap-2 rounded-md bg-sky-200 p-2">

View file

@ -46,9 +46,9 @@ export const ServizioContainer = ({
return ( return (
<ServizioProvider <ServizioProvider
isAdmin={isAdmin} isAdmin={isAdmin}
userId={userId}
servizioId={servizio.servizio_id}
servizio={servizio} servizio={servizio}
servizioId={servizio.servizio_id}
userId={userId}
> >
<ServizioMain /> <ServizioMain />
</ServizioProvider> </ServizioProvider>
@ -91,13 +91,13 @@ export const ServizioContent = () => {
const { servizio, isAdmin, userId } = useServizio(); const { servizio, isAdmin, userId } = useServizio();
const utils = api.useUtils(); const utils = api.useUtils();
const { mutate: updateServizio } = api.servizio.updateServizio.useMutation({ const { mutate: updateServizio } = api.servizio.updateServizio.useMutation({
onError: (error) => {
toast.error(error.message);
},
onSuccess: async () => { onSuccess: async () => {
toast.success("Servizio modificato con successo"); toast.success("Servizio modificato con successo");
await utils.servizio.getAllServizioAnnunci.invalidate({ userId }); await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
}, },
onError: (error) => {
toast.error(error.message);
},
}); });
const router = useRouter(); const router = useRouter();
@ -114,6 +114,15 @@ export const ServizioContent = () => {
return ( return (
<ServizioCard <ServizioCard
cardClassName="bg-neutral-100 border-neutral-300" cardClassName="bg-neutral-100 border-neutral-300"
content={
<>
<p className="text-lg font-medium">Servizio interrotto</p>
<p>
Non è possibile riutilizzare questo servizio, poichè è stato
interrotto.
</p>
</>
}
header={ header={
<> <>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
@ -126,24 +135,15 @@ export const ServizioContent = () => {
<p> <p>
aggiunto il{" "} aggiunto il{" "}
{servizio.created_at.toLocaleDateString("it", { {servizio.created_at.toLocaleDateString("it", {
year: "numeric",
month: "2-digit",
day: "2-digit", day: "2-digit",
month: "2-digit",
year: "numeric",
})} })}
</p> </p>
</CardDescription> </CardDescription>
</div> </div>
</> </>
} }
content={
<>
<p className="text-lg font-medium">Servizio interrotto</p>
<p>
Non è possibile riutilizzare questo servizio, poichè è stato
interrotto.
</p>
</>
}
/> />
); );
} }
@ -151,6 +151,40 @@ export const ServizioContent = () => {
if (!servizio.isOkAcconto || servizio.decorrenza === null) { if (!servizio.isOkAcconto || servizio.decorrenza === null) {
return ( return (
<ServizioCard <ServizioCard
content={
<div className="flex flex-col gap-4">
<div className="flex flex-wrap gap-3">
<Link
aria-label="Attiva Servizio"
href={`/servizio/onboard/${servizio.servizio_id}`}
>
<Button>Procedi all&apos;attivazione</Button>
</Link>
{isAdmin && (
<Button
onClick={() =>
updateServizio({
data: {
decorrenza: new Date(),
isOkAcconto: true,
},
servizioId: servizio.servizio_id,
})
}
variant="success"
>
Attiva Manualmente (Azione Admin)
</Button>
)}
</div>
{isAdmin && (
<AnnunciSelezionati
annunci={servizio.annunci}
isConfirming={false}
/>
)}
</div>
}
header={ header={
<div className="flex w-full flex-col gap-2"> <div className="flex w-full flex-col gap-2">
{/* <div className="flex w-full flex-col gap-4"> {/* <div className="flex w-full flex-col gap-4">
@ -168,40 +202,6 @@ export const ServizioContent = () => {
<ServizioActions2 /> <ServizioActions2 />
</div> </div>
} }
content={
<div className="flex flex-col gap-4">
<div className="flex flex-wrap gap-3">
<Link
href={`/servizio/onboard/${servizio.servizio_id}`}
aria-label="Attiva Servizio"
>
<Button>Procedi all&apos;attivazione</Button>
</Link>
{isAdmin && (
<Button
onClick={() =>
updateServizio({
servizioId: servizio.servizio_id,
data: {
isOkAcconto: true,
decorrenza: new Date(),
},
})
}
variant="success"
>
Attiva Manualmente (Azione Admin)
</Button>
)}
</div>
{isAdmin && (
<AnnunciSelezionati
isConfirming={false}
annunci={servizio.annunci}
/>
)}
</div>
}
/> />
); );
} }
@ -212,6 +212,17 @@ export const ServizioContent = () => {
if (servizio.isOkSaldo) { if (servizio.isOkSaldo) {
return ( return (
<ServizioCard <ServizioCard
content={
<>
{annuncioConfermato.map((data) => {
return (
<ServizioAnnuncioProvider data={data} key={data.id}>
<AnnuncioCard />
</ServizioAnnuncioProvider>
);
})}
</>
}
header={ header={
<> <>
{/* <div className="flex w-full flex-wrap items-center gap-2"> {/* <div className="flex w-full flex-wrap items-center gap-2">
@ -235,17 +246,6 @@ export const ServizioContent = () => {
<ServizioActions2 /> <ServizioActions2 />
</> </>
} }
content={
<>
{annuncioConfermato.map((data) => {
return (
<ServizioAnnuncioProvider data={data} key={data.id}>
<AnnuncioCard />
</ServizioAnnuncioProvider>
);
})}
</>
}
/> />
); );
} }
@ -263,6 +263,68 @@ export const ServizioContent = () => {
return ( return (
<ServizioCard <ServizioCard
content={
<>
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<UserCircle className="text-primary size-6" />
<span className="font-medium">Contatti sbloccati</span>
</div>
<span className="text-sm">{openContattiCount} / 10</span>
</div>
<Progress className="h-2" value={openContattiCount * 10} />
</div>
<ServizioDuration decorrenza={servizio.decorrenza} />
{annuncioConfermato.length > 0 && (
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<PackageCheck className="text-primary size-6" />
<span className="font-medium">Saldi in attesa:</span>
</div>
{servizio.annunci
.filter((a) => a.accettato_conferma_at !== null)
.map((a) => (
<div
className="flex items-center gap-2 rounded-md bg-sky-400 p-2"
key={a.annunci_id}
>
<h3>
{a.codice} - confermato il:{" "}
{/** biome-ignore lint/style/noNonNullAssertion: <already checked above> */}
{new Date(a.accettato_conferma_at!).toLocaleDateString(
"it",
{
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
month: "2-digit",
year: "numeric",
},
)}
</h3>
<SaldoButton
annuncioId={a.annunci_id}
className="border-none"
variant={"outline"}
/>
</div>
))}
</div>
)}
<div className="space-y-2">
<AnnunciInConferma annunci={annunciInConferma} />
<AnnunciSelezionati
annunci={annunciNotInConferma}
isConfirming={annunciInConferma.length > 0}
/>
<AnnunciCompatibili />
</div>
</>
}
header={ header={
<> <>
{/* <div className="flex w-full flex-col gap-2"> {/* <div className="flex w-full flex-col gap-2">
@ -286,68 +348,6 @@ export const ServizioContent = () => {
<ServizioActions2 /> <ServizioActions2 />
</> </>
} }
content={
<>
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<UserCircle className="text-primary size-6" />
<span className="font-medium">Contatti sbloccati</span>
</div>
<span className="text-sm">{openContattiCount} / 10</span>
</div>
<Progress value={openContattiCount * 10} className="h-2" />
</div>
<ServizioDuration decorrenza={servizio.decorrenza} />
{annuncioConfermato.length > 0 && (
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<PackageCheck className="text-primary size-6" />
<span className="font-medium">Saldi in attesa:</span>
</div>
{servizio.annunci
.filter((a) => a.accettato_conferma_at !== null)
.map((a) => (
<div
key={a.annunci_id}
className="flex items-center gap-2 rounded-md bg-sky-400 p-2"
>
<h3>
{a.codice} - confermato il:{" "}
{/** biome-ignore lint/style/noNonNullAssertion: <already checked above> */}
{new Date(a.accettato_conferma_at!).toLocaleDateString(
"it",
{
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
},
)}
</h3>
<SaldoButton
variant={"outline"}
className="border-none"
annuncioId={a.annunci_id}
/>
</div>
))}
</div>
)}
<div className="space-y-2">
<AnnunciInConferma annunci={annunciInConferma} />
<AnnunciSelezionati
isConfirming={annunciInConferma.length > 0}
annunci={annunciNotInConferma}
/>
<AnnunciCompatibili />
</div>
</>
}
/> />
); );
}; };
@ -356,13 +356,13 @@ const ServizioMain = () => {
const { servizio, isAdmin, userId } = useServizio(); const { servizio, isAdmin, userId } = useServizio();
const utils = api.useUtils(); const utils = api.useUtils();
const { mutate: updateServizio } = api.servizio.updateServizio.useMutation({ const { mutate: updateServizio } = api.servizio.updateServizio.useMutation({
onError: (error) => {
toast.error(error.message);
},
onSuccess: async () => { onSuccess: async () => {
toast.success("Servizio modificato con successo"); toast.success("Servizio modificato con successo");
await utils.servizio.getAllServizioAnnunci.invalidate({ userId }); await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
}, },
onError: (error) => {
toast.error(error.message);
},
}); });
const router = useRouter(); const router = useRouter();
@ -379,6 +379,15 @@ const ServizioMain = () => {
return ( return (
<ServizioCard <ServizioCard
cardClassName="bg-neutral-100 border-neutral-300" cardClassName="bg-neutral-100 border-neutral-300"
content={
<>
<p className="text-lg font-medium">Servizio interrotto</p>
<p>
Non è possibile riutilizzare questo servizio, poichè è stato
interrotto.
</p>
</>
}
header={ header={
<> <>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
@ -391,24 +400,15 @@ const ServizioMain = () => {
<p> <p>
aggiunto il{" "} aggiunto il{" "}
{servizio.created_at.toLocaleDateString("it", { {servizio.created_at.toLocaleDateString("it", {
year: "numeric",
month: "2-digit",
day: "2-digit", day: "2-digit",
month: "2-digit",
year: "numeric",
})} })}
</p> </p>
</CardDescription> </CardDescription>
</div> </div>
</> </>
} }
content={
<>
<p className="text-lg font-medium">Servizio interrotto</p>
<p>
Non è possibile riutilizzare questo servizio, poichè è stato
interrotto.
</p>
</>
}
/> />
); );
} }
@ -416,6 +416,40 @@ const ServizioMain = () => {
if (!servizio.isOkAcconto || servizio.decorrenza === null) { if (!servizio.isOkAcconto || servizio.decorrenza === null) {
return ( return (
<ServizioCard <ServizioCard
content={
<div className="flex flex-col gap-4">
<div className="flex flex-wrap gap-3">
<Link
aria-label="Attiva Servizio"
href={`/servizio/onboard/${servizio.servizio_id}`}
>
<Button>Procedi all&apos;attivazione</Button>
</Link>
{isAdmin && (
<Button
onClick={() =>
updateServizio({
data: {
decorrenza: new Date(),
isOkAcconto: true,
},
servizioId: servizio.servizio_id,
})
}
variant="success"
>
Attiva Manualmente (Azione Admin)
</Button>
)}
</div>
{isAdmin && (
<AnnunciSelezionati
annunci={servizio.annunci}
isConfirming={false}
/>
)}
</div>
}
header={ header={
<> <>
<div className="flex w-full flex-col gap-4"> <div className="flex w-full flex-col gap-4">
@ -430,40 +464,6 @@ const ServizioMain = () => {
<ServizioActions /> <ServizioActions />
</> </>
} }
content={
<div className="flex flex-col gap-4">
<div className="flex flex-wrap gap-3">
<Link
href={`/servizio/onboard/${servizio.servizio_id}`}
aria-label="Attiva Servizio"
>
<Button>Procedi all&apos;attivazione</Button>
</Link>
{isAdmin && (
<Button
onClick={() =>
updateServizio({
servizioId: servizio.servizio_id,
data: {
isOkAcconto: true,
decorrenza: new Date(),
},
})
}
variant="success"
>
Attiva Manualmente (Azione Admin)
</Button>
)}
</div>
{isAdmin && (
<AnnunciSelezionati
isConfirming={false}
annunci={servizio.annunci}
/>
)}
</div>
}
/> />
); );
} }
@ -474,6 +474,17 @@ const ServizioMain = () => {
if (servizio.isOkSaldo) { if (servizio.isOkSaldo) {
return ( return (
<ServizioCard <ServizioCard
content={
<>
{annuncioConfermato.map((data) => {
return (
<ServizioAnnuncioProvider data={data} key={data.id}>
<AnnuncioCard />
</ServizioAnnuncioProvider>
);
})}
</>
}
header={ header={
<> <>
<div className="flex w-full flex-wrap items-center gap-2"> <div className="flex w-full flex-wrap items-center gap-2">
@ -486,9 +497,9 @@ const ServizioMain = () => {
<p> <p>
aggiunto il{" "} aggiunto il{" "}
{servizio.created_at.toLocaleDateString("it", { {servizio.created_at.toLocaleDateString("it", {
year: "numeric",
month: "2-digit",
day: "2-digit", day: "2-digit",
month: "2-digit",
year: "numeric",
})} })}
</p> </p>
</CardDescription> </CardDescription>
@ -497,17 +508,6 @@ const ServizioMain = () => {
<ServizioActions /> <ServizioActions />
</> </>
} }
content={
<>
{annuncioConfermato.map((data) => {
return (
<ServizioAnnuncioProvider data={data} key={data.id}>
<AnnuncioCard />
</ServizioAnnuncioProvider>
);
})}
</>
}
/> />
); );
} }
@ -525,29 +525,6 @@ const ServizioMain = () => {
return ( return (
<ServizioCard <ServizioCard
header={
<>
<div className="flex w-full flex-col gap-2">
<CardTitle className="flex items-center gap-2 text-xl sm:text-2xl">
<Package className="text-primary size-6" />
Cerco Affitto {servizio.tipologia}
</CardTitle>
<CardDescription className="text-foreground/70 space-y-2 pl-2">
<p>
aggiunto il{" "}
{servizio.created_at.toLocaleDateString("it", {
year: "numeric",
month: "2-digit",
day: "2-digit",
})}
</p>
</CardDescription>
</div>
<ServizioActions />
</>
}
content={ content={
<> <>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
@ -558,7 +535,7 @@ const ServizioMain = () => {
</div> </div>
<span className="text-sm">{openContattiCount} / 10</span> <span className="text-sm">{openContattiCount} / 10</span>
</div> </div>
<Progress value={openContattiCount * 10} className="h-2" /> <Progress className="h-2" value={openContattiCount * 10} />
</div> </div>
<ServizioDuration decorrenza={servizio.decorrenza} /> <ServizioDuration decorrenza={servizio.decorrenza} />
@ -573,8 +550,8 @@ const ServizioMain = () => {
.filter((a) => a.accettato_conferma_at !== null) .filter((a) => a.accettato_conferma_at !== null)
.map((a) => ( .map((a) => (
<div <div
key={a.annunci_id}
className="flex items-center gap-2 rounded-md bg-sky-400 p-2" className="flex items-center gap-2 rounded-md bg-sky-400 p-2"
key={a.annunci_id}
> >
<h3> <h3>
{a.codice} - confermato il:{" "} {a.codice} - confermato il:{" "}
@ -582,18 +559,18 @@ const ServizioMain = () => {
{new Date(a.accettato_conferma_at!).toLocaleDateString( {new Date(a.accettato_conferma_at!).toLocaleDateString(
"it", "it",
{ {
year: "numeric",
month: "2-digit",
day: "2-digit", day: "2-digit",
hour: "2-digit", hour: "2-digit",
minute: "2-digit", minute: "2-digit",
month: "2-digit",
year: "numeric",
}, },
)} )}
</h3> </h3>
<SaldoButton <SaldoButton
variant={"outline"}
className="border-none"
annuncioId={a.annunci_id} annuncioId={a.annunci_id}
className="border-none"
variant={"outline"}
/> />
</div> </div>
))} ))}
@ -603,13 +580,36 @@ const ServizioMain = () => {
<div className="space-y-2"> <div className="space-y-2">
<AnnunciInConferma annunci={annunciInConferma} /> <AnnunciInConferma annunci={annunciInConferma} />
<AnnunciSelezionati <AnnunciSelezionati
isConfirming={annunciInConferma.length > 0}
annunci={annunciNotInConferma} annunci={annunciNotInConferma}
isConfirming={annunciInConferma.length > 0}
/> />
<AnnunciCompatibili /> <AnnunciCompatibili />
</div> </div>
</> </>
} }
header={
<>
<div className="flex w-full flex-col gap-2">
<CardTitle className="flex items-center gap-2 text-xl sm:text-2xl">
<Package className="text-primary size-6" />
Cerco Affitto {servizio.tipologia}
</CardTitle>
<CardDescription className="text-foreground/70 space-y-2 pl-2">
<p>
aggiunto il{" "}
{servizio.created_at.toLocaleDateString("it", {
day: "2-digit",
month: "2-digit",
year: "numeric",
})}
</p>
</CardDescription>
</div>
<ServizioActions />
</>
}
/> />
); );
}; };

View file

@ -21,9 +21,9 @@ export const ServizioDuration = ({
} }
return ( return (
<Accordion <Accordion
type="single"
collapsible collapsible
defaultValue={isDesktop ? "validity" : undefined} defaultValue={isDesktop ? "validity" : undefined}
type="single"
> >
<AccordionItem value="validity"> <AccordionItem value="validity">
<AccordionTrigger> <AccordionTrigger>
@ -35,8 +35,8 @@ export const ServizioDuration = ({
<AccordionContent> <AccordionContent>
<ServiceDurationDisplay <ServiceDurationDisplay
decorrenza={decorrenza} decorrenza={decorrenza}
scadenza={add(decorrenza, { days: 60 })}
recessoDays={14} recessoDays={14}
scadenza={add(decorrenza, { days: 60 })}
/> />
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
@ -70,7 +70,7 @@ function ServiceDurationDisplay({
<CardContent className="flex flex-col gap-5 px-1"> <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 w-full flex-col justify-between gap-4 sm:flex-row sm:items-center">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<CalendarDays className="text-primary h-6 w-6" aria-hidden="true" /> <CalendarDays aria-hidden="true" className="text-primary h-6 w-6" />
<div> <div>
<div className="text-muted-foreground text-sm">Decorrenza</div> <div className="text-muted-foreground text-sm">Decorrenza</div>
<div className="text-foreground text-lg font-semibold"> <div className="text-foreground text-lg font-semibold">
@ -84,8 +84,8 @@ function ServiceDurationDisplay({
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<CalendarClock <CalendarClock
className="text-primary h-6 w-6"
aria-hidden="true" aria-hidden="true"
className="text-primary h-6 w-6"
/> />
<div> <div>
<div className="text-muted-foreground text-sm">Scadenza</div> <div className="text-muted-foreground text-sm">Scadenza</div>
@ -99,7 +99,7 @@ function ServiceDurationDisplay({
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Hourglass className="text-primary h-6 w-6" aria-hidden="true" /> <Hourglass aria-hidden="true" className="text-primary h-6 w-6" />
<div> <div>
<div className="text-muted-foreground text-sm"> <div className="text-muted-foreground text-sm">
Periodo di recesso Periodo di recesso
@ -114,9 +114,9 @@ function ServiceDurationDisplay({
{/* Progress Bar */} {/* Progress Bar */}
<div className="pt-2"> <div className="pt-2">
<Progress <Progress
value={progressPercentage}
className="h-2"
aria-label="Service duration progress" aria-label="Service duration progress"
className="h-2"
value={progressPercentage}
/> />
<div className="text-muted-foreground mt-1 text-right text-sm"> <div className="text-muted-foreground mt-1 text-right text-sm">
{remainingDaysText} {remainingDaysText}

View file

@ -128,6 +128,10 @@ export const AddAnnuncio = ({
const utils = api.useUtils(); const utils = api.useUtils();
const { mutate } = api.servizio.addServizioAnnunci.useMutation({ const { mutate } = api.servizio.addServizioAnnunci.useMutation({
onError: (error) => {
console.error(error);
toast.error("Errore durante l'aggiunta dell'annuncio");
},
onSuccess: async () => { onSuccess: async () => {
await utils.servizio.getAllServizioAnnunci.invalidate({ userId }); await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
await utils.servizio.getAnnunciAvailableToAdd.invalidate({ await utils.servizio.getAnnunciAvailableToAdd.invalidate({
@ -141,22 +145,18 @@ export const AddAnnuncio = ({
setOpenModal(false); setOpenModal(false);
setSelectedAnnunci([]); setSelectedAnnunci([]);
}, },
onError: (error) => {
console.error(error);
toast.error("Errore durante l'aggiunta dell'annuncio");
},
}); });
return ( return (
<Dialog open={openModal} onOpenChange={setOpenModal}> <Dialog onOpenChange={setOpenModal} open={openModal}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button <Button
aria-label="Add Annuncio" aria-label="Add Annuncio"
variant="outline" className="items-center gap-2"
onClick={() => { onClick={() => {
setSelectedAnnunci([]); setSelectedAnnunci([]);
}} }}
className="items-center gap-2" variant="outline"
> >
<Plus /> <Plus />
Annuncio Annuncio
@ -181,40 +181,40 @@ export const AddAnnuncio = ({
<LoadingPage /> <LoadingPage />
) : ( ) : (
<MultiSelect <MultiSelect
defaultValue={undefined}
elementId="select-annuncio" elementId="select-annuncio"
elementName="annuncio" elementName="annuncio"
isMulti={true} isMulti={true}
placeholder="" onValueChange={(value) => {
defaultValue={undefined} setSelectedAnnunci(
value.map((v) => ({
label: v.label,
value: parseInt(v.value) as AnnunciId,
})),
);
}}
options={ options={
codici?.map((cod) => ({ codici?.map((cod) => ({
label: cod.codice, label: cod.codice,
value: cod.id.toString(), value: cod.id.toString(),
})) || [] })) || []
} }
onValueChange={(value) => { placeholder=""
setSelectedAnnunci(
value.map((v) => ({
value: parseInt(v.value) as AnnunciId,
label: v.label,
})),
);
}}
/> />
)} )}
</div> </div>
<DialogFooter> <DialogFooter>
<Button <Button
aria-label="Cancel Add Annuncio" aria-label="Cancel Add Annuncio"
type="button" disabled={!selectedAnnunci.length}
onClick={() => { onClick={() => {
if (!selectedAnnunci.length) return; if (!selectedAnnunci.length) return;
mutate({ mutate({
servizioId,
annunci: selectedAnnunci.map((annuncio) => annuncio.value), annunci: selectedAnnunci.map((annuncio) => annuncio.value),
servizioId,
}); });
}} }}
disabled={!selectedAnnunci.length} type="button"
> >
Aggiungi Aggiungi
</Button> </Button>
@ -236,14 +236,14 @@ const InterruzioneServizio = () => {
const utils = api.useUtils(); const utils = api.useUtils();
const { mutate } = api.servizio.interruzioneServizio.useMutation({ const { mutate } = api.servizio.interruzioneServizio.useMutation({
onSuccess: async () => {
await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
toast.success("Interruzione richiesta");
},
onError: (error) => { onError: (error) => {
console.error(error); console.error(error);
toast.error("Errore durante la richiesta di interruzione"); toast.error("Errore durante la richiesta di interruzione");
}, },
onSuccess: async () => {
await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
toast.success("Interruzione richiesta");
},
}); });
return ( return (
@ -251,9 +251,9 @@ const InterruzioneServizio = () => {
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<Button <Button
aria-label="Request Interruzione" aria-label="Request Interruzione"
variant="outline"
className="items-center gap-2" className="items-center gap-2"
disabled={!beforeInterruzioneLimit} disabled={!beforeInterruzioneLimit}
variant="outline"
> >
<ClockAlert /> <span>Interruzione</span> <ClockAlert /> <span>Interruzione</span>
</Button> </Button>
@ -291,6 +291,9 @@ const EditPreferenze = () => {
const utils = api.useUtils(); const utils = api.useUtils();
const { mutate: update } = api.servizio.updateServizio.useMutation({ const { mutate: update } = api.servizio.updateServizio.useMutation({
onError: (error) => {
toast.error(error.message);
},
onSuccess: async () => { onSuccess: async () => {
toast.success("Servizio modificato con successo"); toast.success("Servizio modificato con successo");
await utils.servizio.getAllServizioAnnunci.invalidate({ userId }); await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
@ -300,22 +303,19 @@ const EditPreferenze = () => {
}); });
setOpenEditPreferenze(false); setOpenEditPreferenze(false);
}, },
onError: (error) => {
toast.error(error.message);
},
}); });
function onSubmit(fields: FormValues) { function onSubmit(fields: FormValues) {
update({ update({
servizioId,
data: { data: {
...fields, ...fields,
}, },
servizioId,
}); });
} }
return ( return (
<Credenza open={openEditPreferenze} onOpenChange={setOpenEditPreferenze}> <Credenza onOpenChange={setOpenEditPreferenze} open={openEditPreferenze}>
<CredenzaTrigger asChild> <CredenzaTrigger asChild>
<Button <Button
aria-label="Preferenze di ricerca" aria-label="Preferenze di ricerca"
@ -338,8 +338,8 @@ const EditPreferenze = () => {
) : ( ) : (
<FormNewServizio <FormNewServizio
initialData={data} initialData={data}
onSubmit={onSubmit}
isLimited={!isAdmin} isLimited={!isAdmin}
onSubmit={onSubmit}
/> />
)} )}
</CredenzaBody> </CredenzaBody>
@ -355,6 +355,9 @@ const EditServizioAdmin = () => {
const utils = api.useUtils(); const utils = api.useUtils();
const { mutate: update } = api.servizio.updateServizio.useMutation({ const { mutate: update } = api.servizio.updateServizio.useMutation({
onError: (error) => {
toast.error(error.message);
},
onSuccess: async () => { onSuccess: async () => {
toast.success("Servizio modificato con successo"); toast.success("Servizio modificato con successo");
await utils.servizio.getAllServizioAnnunci.invalidate({ userId }); await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
@ -364,22 +367,19 @@ const EditServizioAdmin = () => {
}); });
setOpen(false); setOpen(false);
}, },
onError: (error) => {
toast.error(error.message);
},
}); });
function onSubmit(fields: EditFormValues) { function onSubmit(fields: EditFormValues) {
update({ update({
servizioId,
data: { data: {
...fields, ...fields,
}, },
servizioId,
}); });
} }
return ( return (
<Credenza open={open} onOpenChange={setOpen}> <Credenza onOpenChange={setOpen} open={open}>
<CredenzaTrigger asChild> <CredenzaTrigger asChild>
<Button <Button
aria-label="Modifica Servizio Admin" aria-label="Modifica Servizio Admin"
@ -404,7 +404,7 @@ const EditServizioAdmin = () => {
)} )}
<Collapsible> <Collapsible>
<CollapsibleTrigger asChild> <CollapsibleTrigger asChild>
<Button variant="warning" className="items-center gap-2"> <Button className="items-center gap-2" variant="warning">
<Bug /> Debug Info Servizio <Bug /> Debug Info Servizio
</Button> </Button>
</CollapsibleTrigger> </CollapsibleTrigger>

View file

@ -32,8 +32,8 @@ export const AnnunciInConferma = ({
} }
return ( return (
<Accordion type="single" collapsible defaultValue="in-conferma"> <Accordion collapsible defaultValue="in-conferma" type="single">
<AccordionItem value="in-conferma" className="space-y-4"> <AccordionItem className="space-y-4" value="in-conferma">
<AccordionTrigger className="rounded-md border-2 border-green-500 bg-white p-2"> <AccordionTrigger className="rounded-md border-2 border-green-500 bg-white p-2">
Annunci in conferma: Annunci in conferma:
</AccordionTrigger> </AccordionTrigger>
@ -62,11 +62,11 @@ export const AnnunciSelezionati = ({
}) => { }) => {
return ( return (
<Accordion <Accordion
type="single"
collapsible collapsible
defaultValue={!isConfirming ? "not-in-conferma" : undefined} defaultValue={!isConfirming ? "not-in-conferma" : undefined}
type="single"
> >
<AccordionItem value="not-in-conferma" className="space-y-4"> <AccordionItem className="space-y-4" value="not-in-conferma">
<AccordionTrigger className="cursor-pointer rounded-md border-2 border-sky-500 bg-white p-2"> <AccordionTrigger className="cursor-pointer rounded-md border-2 border-sky-500 bg-white p-2">
Annunci Selezionati: Annunci Selezionati:
</AccordionTrigger> </AccordionTrigger>
@ -86,8 +86,8 @@ export const AnnunciSelezionati = ({
Nessun annuncio selezionato, fai una ricerca nella pagina{" "} Nessun annuncio selezionato, fai una ricerca nella pagina{" "}
<Link <Link
aria-label="Annunci Compatibili" aria-label="Annunci Compatibili"
href="/annunci"
className="inline-flex items-center gap-1 text-blue-500 underline" className="inline-flex items-center gap-1 text-blue-500 underline"
href="/annunci"
> >
Annunci Annunci
<ExternalLink className="size-4" /> <ExternalLink className="size-4" />
@ -116,11 +116,11 @@ export const AnnunciCompatibili = () => {
return ( return (
<Accordion <Accordion
type="single"
collapsible collapsible
defaultValue={!data || data.length === 0 ? "compatibili" : undefined} defaultValue={!data || data.length === 0 ? "compatibili" : undefined}
type="single"
> >
<AccordionItem value="compatibili" className="space-y-4"> <AccordionItem className="space-y-4" value="compatibili">
<AccordionTrigger className="cursor-pointer rounded-md border-2 border-neutral-500 bg-white p-2"> <AccordionTrigger className="cursor-pointer rounded-md border-2 border-neutral-500 bg-white p-2">
Annunci Compatibili: Annunci Compatibili:
</AccordionTrigger> </AccordionTrigger>
@ -137,9 +137,8 @@ export const AnnunciCompatibili = () => {
data.map((a) => { data.map((a) => {
return ( return (
<BasicAnnuncioCard <BasicAnnuncioCard
key={a.id}
data={a}
className="border-2 border-neutral-500" className="border-2 border-neutral-500"
data={a}
interactions={ interactions={
<> <>
{isAdmin ? ( {isAdmin ? (
@ -152,6 +151,7 @@ export const AnnunciCompatibili = () => {
)} )}
</> </>
} }
key={a.id}
/> />
); );
}) })
@ -180,8 +180,8 @@ export const AnnunciRichiesti = ({ userId }: { userId: UsersId }) => {
if (!data || data.length === 0) return null; if (!data || data.length === 0) return null;
return ( return (
<Accordion type="single" collapsible> <Accordion collapsible type="single">
<AccordionItem value="compatibili" className="space-y-4"> <AccordionItem className="space-y-4" value="compatibili">
<AccordionTrigger className="cursor-pointer rounded-md border-2 border-pink-500 bg-white p-2"> <AccordionTrigger className="cursor-pointer rounded-md border-2 border-pink-500 bg-white p-2">
Annunci Richiesti: Annunci Richiesti:
</AccordionTrigger> </AccordionTrigger>
@ -200,19 +200,19 @@ export const AnnunciRichiesti = ({ userId }: { userId: UsersId }) => {
<BasicAnnuncioCard <BasicAnnuncioCard
className="w-full border-2 border-pink-500" className="w-full border-2 border-pink-500"
data={a} data={a}
key={a.id}
interactions={ interactions={
<Button <Button
className="flex items-center gap-2"
onClick={() => { onClick={() => {
deleteAnnuncio({ annuncioId: a.id, userId }); deleteAnnuncio({ annuncioId: a.id, userId });
}} }}
className="flex items-center gap-2"
variant="destructive" variant="destructive"
> >
Rimuovi Rimuovi
<Trash2 className="size-4" /> <Trash2 className="size-4" />
</Button> </Button>
} }
key={a.id}
/> />
); );
}) })

View file

@ -16,17 +16,17 @@ export const Status500 = () => {
<p className="text-neutral-600">{t[500].sottotitolo}</p> <p className="text-neutral-600">{t[500].sottotitolo}</p>
<div className="flex flex-wrap items-center justify-center gap-3"> <div className="flex flex-wrap items-center justify-center gap-3">
<button <button
type="button" className="block rounded-lg bg-red-600 px-4 py-2 font-medium text-white duration-150 hover:bg-red-500 active:bg-red-700"
onClick={() => { onClick={() => {
router.back(); router.back();
}} }}
className="block rounded-lg bg-red-600 px-4 py-2 font-medium text-white duration-150 hover:bg-red-500 active:bg-red-700" type="button"
> >
{t[500].CTA} {t[500].CTA}
</button> </button>
<Link <Link
href="/"
className="block rounded-lg border px-4 py-2 font-medium text-neutral-700 duration-150 hover:bg-neutral-50 active:bg-neutral-100" className="block rounded-lg border px-4 py-2 font-medium text-neutral-700 duration-150 hover:bg-neutral-50 active:bg-neutral-100"
href="/"
> >
{t[500].home} {t[500].home}
</Link> </Link>

View file

@ -45,11 +45,11 @@ export function SubscriptionStatus({
<div> <div>
Error - <em>{errorMsg}</em> Error - <em>{errorMsg}</em>
<button <button
type="button" className="hover:underline"
onClick={() => { onClick={() => {
reset(); reset();
}} }}
className="hover:underline" type="button"
> >
Try Again Try Again
</button> </button>

View file

@ -38,48 +38,48 @@ export const HeroSvg = (props: SVGProps<SVGSVGElement>) => (
ry={13.1} ry={13.1}
/> />
<path <path
id="_\u041A\u043E\u043D\u0442\u0443\u0440_27"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
className="fill-[#3a2c6d]" className="fill-[#3a2c6d]"
d="M454.85 237.69c.91-1.46 7.86-2.42 10.15 3.47.86 2.23.73 7.94 1.46 11.52s2.43 5.62 3 8.53-.38 7.78-7.39 8.65-12.4-1.6-14.06-3.84-1.69-7 0-9.53 3-4.63 3-8Z" d="M454.85 237.69c.91-1.46 7.86-2.42 10.15 3.47.86 2.23.73 7.94 1.46 11.52s2.43 5.62 3 8.53-.38 7.78-7.39 8.65-12.4-1.6-14.06-3.84-1.69-7 0-9.53 3-4.63 3-8Z"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
id="_\u041A\u043E\u043D\u0442\u0443\u0440_27"
/> />
<path <path
className="fill-[#ffcea9]" className="fill-[#ffcea9]"
d="M446.53 262.33S444.88 274 448 282.21s2.25-2.13 2.25-2.13l-.75-2.46s-.62-9.5-.36-11.83.2-4.33.36-4.52-.61-.56-.94-.61-2.03 1.67-2.03 1.67Z" d="M446.53 262.33S444.88 274 448 282.21s2.25-2.13 2.25-2.13l-.75-2.46s-.62-9.5-.36-11.83.2-4.33.36-4.52-.61-.56-.94-.61-2.03 1.67-2.03 1.67Z"
/> />
<path <path
id="_\u041A\u043E\u043D\u0442\u0443\u0440_28"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
className="fill-[#3a2c6d]" className="fill-[#3a2c6d]"
d="M459.44 236.68s-5.23-1.51-8.4 1.32a7.77 7.77 0 0 0-2.24 7.75c.42 1.65 1.27 3.91 2.58 4s8.06-13.07 8.06-13.07Z" d="M459.44 236.68s-5.23-1.51-8.4 1.32a7.77 7.77 0 0 0-2.24 7.75c.42 1.65 1.27 3.91 2.58 4s8.06-13.07 8.06-13.07Z"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
id="_\u041A\u043E\u043D\u0442\u0443\u0440_28"
/> />
<path <path
id="_\u041A\u043E\u043D\u0442\u0443\u0440_29"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
className="fill-[#ffcea9]" className="fill-[#ffcea9]"
d="M461.44 251s-.34 5.83-.23 6.26 2.28.84 2.9 1.35-3.17 6.42-5.1 7.3-7.88-1.36-8.29-4.33c-.38-2.72 1.7-4.84 2.51-5.58a12.41 12.41 0 0 1 1.64 0l.17-2.82c2.61-.93 6.4-2.18 6.4-2.18Z" d="M461.44 251s-.34 5.83-.23 6.26 2.28.84 2.9 1.35-3.17 6.42-5.1 7.3-7.88-1.36-8.29-4.33c-.38-2.72 1.7-4.84 2.51-5.58a12.41 12.41 0 0 1 1.64 0l.17-2.82c2.61-.93 6.4-2.18 6.4-2.18Z"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
id="_\u041A\u043E\u043D\u0442\u0443\u0440_29"
/> />
<path <path
id="_\u041A\u043E\u043D\u0442\u0443\u0440_30"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
className="fill-[#e4e4e7]" className="fill-[#e4e4e7]"
d="M462.24 257.77c1.64.26 3.51 1.13 3.77 2a19.33 19.33 0 0 1-.47 9.73c-1.42 3.75-2.37 6.14-3.19 7.76 0 0-7.09 3-12.8-.51 0 0-.4-4-.42-6.17-5.28-4.6-.77-10.28 3.7-14.69a10.85 10.85 0 0 1 1.61 0s-4 4.72-1.21 7.4c5.93-1.79 7.11-4.45 9.01-5.52Z" d="M462.24 257.77c1.64.26 3.51 1.13 3.77 2a19.33 19.33 0 0 1-.47 9.73c-1.42 3.75-2.37 6.14-3.19 7.76 0 0-7.09 3-12.8-.51 0 0-.4-4-.42-6.17-5.28-4.6-.77-10.28 3.7-14.69a10.85 10.85 0 0 1 1.61 0s-4 4.72-1.21 7.4c5.93-1.79 7.11-4.45 9.01-5.52Z"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
id="_\u041A\u043E\u043D\u0442\u0443\u0440_30"
/> />
<path <path
id="_\u041A\u043E\u043D\u0442\u0443\u0440_31"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
className="fill-[#ffcea9]" className="fill-[#ffcea9]"
d="M450.55 243.72c.59-3.75 3.29-6.58 7.16-6.35a7.43 7.43 0 0 1 3.73 13.63 7.54 7.54 0 0 1-.59 1.63c-1.37 1.6-5 2.61-6.21 2.62-1 0-1.93-.95-2.9-2.56-2.03-3.4-1.51-7.12-1.19-8.97Z" d="M450.55 243.72c.59-3.75 3.29-6.58 7.16-6.35a7.43 7.43 0 0 1 3.73 13.63 7.54 7.54 0 0 1-.59 1.63c-1.37 1.6-5 2.61-6.21 2.62-1 0-1.93-.95-2.9-2.56-2.03-3.4-1.51-7.12-1.19-8.97Z"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
id="_\u041A\u043E\u043D\u0442\u0443\u0440_31"
/> />
<path <path
className="fill-[#3a2c6d]" className="fill-[#3a2c6d]"
d="M458.9 237.44c2.71.53 4.94 1.29 5.68 3.95.5 1.84 1.16 5.33.53 6.8l-.38 1.07-3.29 1.74s-1.53-1.28-1.05-5a2.8 2.8 0 0 0-.46-1.42 4.12 4.12 0 0 1-.46-1.13 4.25 4.25 0 0 0-2.12-2.3c-2.45-1.37-6-.19-6.52.31a7.11 7.11 0 0 1 8.07-4.02Z" d="M458.9 237.44c2.71.53 4.94 1.29 5.68 3.95.5 1.84 1.16 5.33.53 6.8l-.38 1.07-3.29 1.74s-1.53-1.28-1.05-5a2.8 2.8 0 0 0-.46-1.42 4.12 4.12 0 0 1-.46-1.13 4.25 4.25 0 0 0-2.12-2.3c-2.45-1.37-6-.19-6.52.31a7.11 7.11 0 0 1 8.07-4.02Z"
/> />
<path <path
id="_\u041A\u043E\u043D\u0442\u0443\u0440_32"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
className="fill-[#ffcea9]" className="fill-[#ffcea9]"
d="M466.2 285c.61-3.8 1.75-6.26 1.22-8.35a75.85 75.85 0 0 0-4.62-12.3c-1.2-2.36-.83-4 .41-4.94 1.4-1 2.73-.59 4 2 1.59 3.29 3.37 6.67 4.24 11.57a25.78 25.78 0 0 1-.12 9.57c-.54 3.41-1.62 5.33-2.91 8.53-.36.92-2.74-2.84-2.22-6.08Z" d="M466.2 285c.61-3.8 1.75-6.26 1.22-8.35a75.85 75.85 0 0 0-4.62-12.3c-1.2-2.36-.83-4 .41-4.94 1.4-1 2.73-.59 4 2 1.59 3.29 3.37 6.67 4.24 11.57a25.78 25.78 0 0 1-.12 9.57c-.54 3.41-1.62 5.33-2.91 8.53-.36.92-2.74-2.84-2.22-6.08Z"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
id="_\u041A\u043E\u043D\u0442\u0443\u0440_32"
/> />
<path <path
className="fill-[#38226d]" className="fill-[#38226d]"
@ -119,10 +119,10 @@ export const HeroSvg = (props: SVGProps<SVGSVGElement>) => (
ry={13.1} ry={13.1}
/> />
<path <path
id="_\u041A\u043E\u043D\u0442\u0443\u0440_33"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
className="fill-[#ffcea9]" className="fill-[#ffcea9]"
d="M98.48 386.23c2 .57 3.12 2.76 4.86 7.45a39.25 39.25 0 0 1 2.09 13.22c-.56 4.42-2.39 7.86-6.81 11.44l-2.11-3.87s4.33-3.82 4.77-8.24c.25-2.48-2.66-10.84-2.66-11.07s-.14-8.93-.14-8.93Z" d="M98.48 386.23c2 .57 3.12 2.76 4.86 7.45a39.25 39.25 0 0 1 2.09 13.22c-.56 4.42-2.39 7.86-6.81 11.44l-2.11-3.87s4.33-3.82 4.77-8.24c.25-2.48-2.66-10.84-2.66-11.07s-.14-8.93-.14-8.93Z"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
id="_\u041A\u043E\u043D\u0442\u0443\u0440_33"
/> />
<path <path
className="fill-[#38226d]" className="fill-[#38226d]"
@ -141,36 +141,36 @@ export const HeroSvg = (props: SVGProps<SVGSVGElement>) => (
d="M94.7 484.44c-1.15-.25-3.26-1.67-6.12-2.36-1.09-.27-3-1.47-3.17-2.9-.25 1.64 2 3.1 3.17 3.39 2.86.69 5 2.12 6.12 2.36a2.9 2.9 0 0 0 2.64-.37 1.67 1.67 0 0 0 .37-.69 3.4 3.4 0 0 1-3.01.57Z" d="M94.7 484.44c-1.15-.25-3.26-1.67-6.12-2.36-1.09-.27-3-1.47-3.17-2.9-.25 1.64 2 3.1 3.17 3.39 2.86.69 5 2.12 6.12 2.36a2.9 2.9 0 0 0 2.64-.37 1.67 1.67 0 0 0 .37-.69 3.4 3.4 0 0 1-3.01.57Z"
/> />
<path <path
id="_\u041A\u043E\u043D\u0442\u0443\u0440_34"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
className="fill-[#ffcea9]" className="fill-[#ffcea9]"
d="M99.42 463.6a21.09 21.09 0 0 0-1.24-7.15s.82-9.79 1.45-16.22c.92-9.48 2.38-10.42 2-16.18-.38-5.34-3.42-8.8-4.21-12.42l-15.47 1.22s-1.35 4.39-3.09 13.13-1.86 19.13-1.72 32.89c0 3.55-.6 6.63-.4 15.47.14 5.94.48 13.22.48 13.22 2.26 1.94 3.83.51 3.83.51s4-17.6 4.55-21.64a12.73 12.73 0 0 0-.54-6.68s1.16-5.53 2-9.88c1-5.31 3.65-16.77 3.65-16.77s-.45 17.61.32 21.08.9 6.49 1.56 11.21c.79 5.7 1.15 14.05 1.15 14.05 2.36 1.5 3.41.35 3.41.35s2.27-11.2 2.27-16.19Z" d="M99.42 463.6a21.09 21.09 0 0 0-1.24-7.15s.82-9.79 1.45-16.22c.92-9.48 2.38-10.42 2-16.18-.38-5.34-3.42-8.8-4.21-12.42l-15.47 1.22s-1.35 4.39-3.09 13.13-1.86 19.13-1.72 32.89c0 3.55-.6 6.63-.4 15.47.14 5.94.48 13.22.48 13.22 2.26 1.94 3.83.51 3.83.51s4-17.6 4.55-21.64a12.73 12.73 0 0 0-.54-6.68s1.16-5.53 2-9.88c1-5.31 3.65-16.77 3.65-16.77s-.45 17.61.32 21.08.9 6.49 1.56 11.21c.79 5.7 1.15 14.05 1.15 14.05 2.36 1.5 3.41.35 3.41.35s2.27-11.2 2.27-16.19Z"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
id="_\u041A\u043E\u043D\u0442\u0443\u0440_34"
/> />
<path <path
className="fill-[#8c50ff]" className="fill-[#8c50ff]"
d="M97.38 411.63c1.62 1.66 4.3 6.71 4.3 14.35 0 8.6-1.25 6.86-1.85 16.33l-.6 9.47s-12.93 6.17-22.15.45c-.82-1.79.31-21.71 1.93-28.79s2.73-10.55 2.73-10.55 11.89 3.21 15.64-1.26Z" d="M97.38 411.63c1.62 1.66 4.3 6.71 4.3 14.35 0 8.6-1.25 6.86-1.85 16.33l-.6 9.47s-12.93 6.17-22.15.45c-.82-1.79.31-21.71 1.93-28.79s2.73-10.55 2.73-10.55 11.89 3.21 15.64-1.26Z"
/> />
<path <path
id="_\u041A\u043E\u043D\u0442\u0443\u0440_35"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
className="fill-[#ffcea9]" className="fill-[#ffcea9]"
d="M97.57 386.14c-3.3-.2-3.62.35-3.88-.82-.09-.42-.28-3.32-.28-3.32a10.61 10.61 0 0 0 .83-1.22 8.37 8.37 0 0 0 4-6.29 8.47 8.47 0 0 0-7.54-9.3c-4.37-.45-7.59 2.62-8.45 6.84-.46 2.08-.94 6.33.36 9.74.73 1.93 1.41 3.28 3.15 3.21a10.47 10.47 0 0 0 1.72-.3 16.18 16.18 0 0 1 .1 2.41c0 .85.1 1.27-1.56 2.29s2.5 3.1 5.21 2.9 5.75-1.84 6.59-3.59c.92-1.96 1.3-2.46-.25-2.55Z" d="M97.57 386.14c-3.3-.2-3.62.35-3.88-.82-.09-.42-.28-3.32-.28-3.32a10.61 10.61 0 0 0 .83-1.22 8.37 8.37 0 0 0 4-6.29 8.47 8.47 0 0 0-7.54-9.3c-4.37-.45-7.59 2.62-8.45 6.84-.46 2.08-.94 6.33.36 9.74.73 1.93 1.41 3.28 3.15 3.21a10.47 10.47 0 0 0 1.72-.3 16.18 16.18 0 0 1 .1 2.41c0 .85.1 1.27-1.56 2.29s2.5 3.1 5.21 2.9 5.75-1.84 6.59-3.59c.92-1.96 1.3-2.46-.25-2.55Z"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
id="_\u041A\u043E\u043D\u0442\u0443\u0440_35"
/> />
<path <path
className="fill-[#e4e4e7]" className="fill-[#e4e4e7]"
d="M100 400.75s3.73-.84 5.44-2.5c0 0-2.22-7-3.64-8.74S99 399 99 399Z" d="M100 400.75s3.73-.84 5.44-2.5c0 0-2.22-7-3.64-8.74S99 399 99 399Z"
/> />
<path <path
id="_\u041A\u043E\u043D\u0442\u0443\u0440_36"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
className="fill-[#e4e4e7]" className="fill-[#e4e4e7]"
d="M94.29 386.07c.61.76.68 1.71-1.67 2.31a7.44 7.44 0 0 1-5.15-.4 34.52 34.52 0 0 0-5.32 2.82c-2 6.6-.39 7.45-1.23 12.18.33 2.38.72 8.52.82 9.91 4.49 3.14 13.6 2.07 15.64-1.26a27.55 27.55 0 0 1 .31-3.31c2.14-7.94 3.81-10 3.22-16-.34-3.42-.61-5.14-2.43-6.06a16.1 16.1 0 0 0-4.19-.19Z" d="M94.29 386.07c.61.76.68 1.71-1.67 2.31a7.44 7.44 0 0 1-5.15-.4 34.52 34.52 0 0 0-5.32 2.82c-2 6.6-.39 7.45-1.23 12.18.33 2.38.72 8.52.82 9.91 4.49 3.14 13.6 2.07 15.64-1.26a27.55 27.55 0 0 1 .31-3.31c2.14-7.94 3.81-10 3.22-16-.34-3.42-.61-5.14-2.43-6.06a16.1 16.1 0 0 0-4.19-.19Z"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
id="_\u041A\u043E\u043D\u0442\u0443\u0440_36"
/> />
<path <path
id="_\u041A\u043E\u043D\u0442\u0443\u0440_37"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
className="fill-[#ffcea9]" className="fill-[#ffcea9]"
d="M53.07 411.89a23.07 23.07 0 0 1 3 .86 10 10 0 0 0 2.93 1c1.1.17 2.82-.4 5.79-1.8a32 32 0 0 0 8.7-6.54 75.44 75.44 0 0 0 7.6-10.85c1.29-2.35 2.92-2.94 4.34-2.48 1.7.56 1.91 2.25 0 5.41a60.5 60.5 0 0 1-8.7 11.39 32.63 32.63 0 0 1-9.07 6.4 27.87 27.87 0 0 1-7.63 2.65c-.93.2-1.56.59-3.28 1.19a11.36 11.36 0 0 1-5.35.59c-1.49-.29-2-.63-2.06-1s.24-.63.94-.58a9.18 9.18 0 0 0 3.32-.08s-1.54-.05-2.45-.16a8.86 8.86 0 0 1-2.38-.6c-.73-.31-1-1.46-.26-1.42s1.24.26 2.6.4a9 9 0 0 0 2.16 0 16.91 16.91 0 0 1-2.65-.66 3.76 3.76 0 0 1-1.85-1.14c-.25-.29-.18-1 .69-.82a13.89 13.89 0 0 0 3.24.71c1.11 0 1.9-.09 1.76-.26s-.8-.15-1.64-.62-1.34-1.38-1-1.82.34-.11 1.25.23Z" d="M53.07 411.89a23.07 23.07 0 0 1 3 .86 10 10 0 0 0 2.93 1c1.1.17 2.82-.4 5.79-1.8a32 32 0 0 0 8.7-6.54 75.44 75.44 0 0 0 7.6-10.85c1.29-2.35 2.92-2.94 4.34-2.48 1.7.56 1.91 2.25 0 5.41a60.5 60.5 0 0 1-8.7 11.39 32.63 32.63 0 0 1-9.07 6.4 27.87 27.87 0 0 1-7.63 2.65c-.93.2-1.56.59-3.28 1.19a11.36 11.36 0 0 1-5.35.59c-1.49-.29-2-.63-2.06-1s.24-.63.94-.58a9.18 9.18 0 0 0 3.32-.08s-1.54-.05-2.45-.16a8.86 8.86 0 0 1-2.38-.6c-.73-.31-1-1.46-.26-1.42s1.24.26 2.6.4a9 9 0 0 0 2.16 0 16.91 16.91 0 0 1-2.65-.66 3.76 3.76 0 0 1-1.85-1.14c-.25-.29-.18-1 .69-.82a13.89 13.89 0 0 0 3.24.71c1.11 0 1.9-.09 1.76-.26s-.8-.15-1.64-.62-1.34-1.38-1-1.82.34-.11 1.25.23Z"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
id="_\u041A\u043E\u043D\u0442\u0443\u0440_37"
/> />
<path <path
className="fill-[#e4e4e7]" className="fill-[#e4e4e7]"
@ -206,32 +206,32 @@ export const HeroSvg = (props: SVGProps<SVGSVGElement>) => (
d="M24.23 449.73c.21 1.28 1.24 1.64 2.29 2.29 1.52.94 2.85 2.3 4.42 3.1s5.08 1.46 6.1-.4c-.55 2.51-4.65 1.73-6.21.94s-2.85-2.12-4.34-3.05c-1.07-.66-2.11-1-2.28-2.34a3.6 3.6 0 0 1 .02-.54Z" d="M24.23 449.73c.21 1.28 1.24 1.64 2.29 2.29 1.52.94 2.85 2.3 4.42 3.1s5.08 1.46 6.1-.4c-.55 2.51-4.65 1.73-6.21.94s-2.85-2.12-4.34-3.05c-1.07-.66-2.11-1-2.28-2.34a3.6 3.6 0 0 1 .02-.54Z"
/> />
<path <path
id="_\u041A\u043E\u043D\u0442\u0443\u0440_38"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
className="fill-[#e26161]" className="fill-[#e26161]"
d="M11.87 394.54c.08 2.34.8 12.5 1.13 17.87s.4 12 .4 12-.93 3.91-1.53 8.52 0 19.13 0 19.13a3.67 3.67 0 0 0 4 0s2.48-11.63 3.62-16.87 1.35-8.26 1.94-11.5c.72-3.93 2.27-22.26 2.27-22.26h.62l1.66 20.69a23.16 23.16 0 0 0-1.05 4.7c-.45 3.37.48 20.33.48 20.33a4.1 4.1 0 0 0 4.28-.18s4.13-22.46 4.13-25.87c0-2.27-.38-32.07-.38-32.07Z" d="M11.87 394.54c.08 2.34.8 12.5 1.13 17.87s.4 12 .4 12-.93 3.91-1.53 8.52 0 19.13 0 19.13a3.67 3.67 0 0 0 4 0s2.48-11.63 3.62-16.87 1.35-8.26 1.94-11.5c.72-3.93 2.27-22.26 2.27-22.26h.62l1.66 20.69a23.16 23.16 0 0 0-1.05 4.7c-.45 3.37.48 20.33.48 20.33a4.1 4.1 0 0 0 4.28-.18s4.13-22.46 4.13-25.87c0-2.27-.38-32.07-.38-32.07Z"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
id="_\u041A\u043E\u043D\u0442\u0443\u0440_38"
/> />
<path <path
id="_\u041A\u043E\u043D\u0442\u0443\u0440_39"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
className="fill-[#ffcea9]" className="fill-[#ffcea9]"
d="M17.48 349.35s.26 5.1.15 5.55-2.34 1.94-3 2.48 3.53 4.62 5.56 5.62 8.46-1.52 8.57-4.68-1.1-5.81-2.08-5.95-9.2-3.02-9.2-3.02Z" d="M17.48 349.35s.26 5.1.15 5.55-2.34 1.94-3 2.48 3.53 4.62 5.56 5.62 8.46-1.52 8.57-4.68-1.1-5.81-2.08-5.95-9.2-3.02-9.2-3.02Z"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
id="_\u041A\u043E\u043D\u0442\u0443\u0440_39"
/> />
<path <path
className="fill-[#ffcea9]" className="fill-[#ffcea9]"
d="M31.44 358.81c.3-2.25-1.18-4.39.25-6.18 5 4.94 4.4 18 5.54 26.35 3.74 3.9 11.25 7.75 13.36 8.7.38.18.72.37.64.78a9.26 9.26 0 0 1-1.75 3.82c-5-2.14-8.55-3.37-14.76-9.27a13.59 13.59 0 0 1-1.57-2.07c-1.77-5.32-2.22-18.25-1.71-22.13Z" d="M31.44 358.81c.3-2.25-1.18-4.39.25-6.18 5 4.94 4.4 18 5.54 26.35 3.74 3.9 11.25 7.75 13.36 8.7.38.18.72.37.64.78a9.26 9.26 0 0 1-1.75 3.82c-5-2.14-8.55-3.37-14.76-9.27a13.59 13.59 0 0 1-1.57-2.07c-1.77-5.32-2.22-18.25-1.71-22.13Z"
/> />
<path <path
id="_\u041A\u043E\u043D\u0442\u0443\u0440_40"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
className="fill-[#e4e4e7]" className="fill-[#e4e4e7]"
d="M16.91 355.78s3.7 5.51 8.86 6c1.63-1.6 1.77-7.31-.48-9.14 0 0 2.07-1.67 3.49-1.35a7.35 7.35 0 0 1 3.22 4.1 33.52 33.52 0 0 1 1.82 11.8c-.09 5.41 0 21.94 0 21.94s-.72 3.49-5.39 6.09-8.14 2.86-11.31 2.12c-2.61-.61-4.77-1.53-5.67-3.42.24-3.8 1.93-13.69 1.3-18.46s-1.64-8.32-1.9-12.18.5-3.78 2.44-5.15a29.62 29.62 0 0 1 3.62-2.35Z" d="M16.91 355.78s3.7 5.51 8.86 6c1.63-1.6 1.77-7.31-.48-9.14 0 0 2.07-1.67 3.49-1.35a7.35 7.35 0 0 1 3.22 4.1 33.52 33.52 0 0 1 1.82 11.8c-.09 5.41 0 21.94 0 21.94s-.72 3.49-5.39 6.09-8.14 2.86-11.31 2.12c-2.61-.61-4.77-1.53-5.67-3.42.24-3.8 1.93-13.69 1.3-18.46s-1.64-8.32-1.9-12.18.5-3.78 2.44-5.15a29.62 29.62 0 0 1 3.62-2.35Z"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
id="_\u041A\u043E\u043D\u0442\u0443\u0440_40"
/> />
<path <path
id="_\u041A\u043E\u043D\u0442\u0443\u0440_41"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
className="fill-[#ffcea9]" className="fill-[#ffcea9]"
d="M29 341.75c-.61-4-3.46-7-7.55-6.73a7.85 7.85 0 0 0-4 14.33 7.82 7.82 0 0 0 .63 1.73c1.44 1.69 5.81 2.73 7.06 2.55a3.74 3.74 0 0 0 3.28-2.63c1.36-3.14.93-7.3.58-9.25Z" d="M29 341.75c-.61-4-3.46-7-7.55-6.73a7.85 7.85 0 0 0-4 14.33 7.82 7.82 0 0 0 .63 1.73c1.44 1.69 5.81 2.73 7.06 2.55a3.74 3.74 0 0 0 3.28-2.63c1.36-3.14.93-7.3.58-9.25Z"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
id="_\u041A\u043E\u043D\u0442\u0443\u0440_41"
/> />
<path <path
className="fill-[#2b2a30]" className="fill-[#2b2a30]"
@ -292,10 +292,10 @@ export const HeroSvg = (props: SVGProps<SVGSVGElement>) => (
cy={438.88} cy={438.88}
rx={3.9} rx={3.9}
ry={6.75} ry={6.75}
transform="rotate(-30 319.598 438.896)"
style={{ style={{
opacity: 0.1, opacity: 0.1,
}} }}
transform="rotate(-30 319.598 438.896)"
/> />
<path <path
className="fill-[#b1b4c4]" className="fill-[#b1b4c4]"
@ -327,16 +327,16 @@ export const HeroSvg = (props: SVGProps<SVGSVGElement>) => (
ry={13.1} ry={13.1}
/> />
<path <path
id="_\u041A\u043E\u043D\u0442\u0443\u0440_54"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
className="fill-[#ffcea9]" className="fill-[#ffcea9]"
d="M439.85 392.73c.24 2.11 2.76 11.21 2.22 14.32s-9.14 8.33-9.14 8.33l-1.19-5.33 4.76-4.67-1.29-8.83Z" d="M439.85 392.73c.24 2.11 2.76 11.21 2.22 14.32s-9.14 8.33-9.14 8.33l-1.19-5.33 4.76-4.67-1.29-8.83Z"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
id="_\u041A\u043E\u043D\u0442\u0443\u0440_54"
/> />
<path <path
id="_\u041A\u043E\u043D\u0442\u0443\u0440_55"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
className="fill-[#e4e4e7]" className="fill-[#e4e4e7]"
d="M433.2 383.76a3.77 3.77 0 0 1 4.79 1.45c1.26 2.09 2.57 9.28 2.69 10.5 0 0-1.77 2.46-4.43 2.05Z" d="M433.2 383.76a3.77 3.77 0 0 1 4.79 1.45c1.26 2.09 2.57 9.28 2.69 10.5 0 0-1.77 2.46-4.43 2.05Z"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
id="_\u041A\u043E\u043D\u0442\u0443\u0440_55"
/> />
<path <path
className="fill-[#2b2a30]" className="fill-[#2b2a30]"
@ -355,34 +355,34 @@ export const HeroSvg = (props: SVGProps<SVGSVGElement>) => (
d="M431.85 486c-1.48-.31-4.2-2.14-7.87-3-1.41-.34-3.84-1.88-4.09-3.72-.32 2.11 2.52 4 4.09 4.36 3.67.89 6.39 2.72 7.87 3 1.1.23 2.52.38 3.4-.48a2.22 2.22 0 0 0 .48-.88 4.38 4.38 0 0 1-3.88.72Z" d="M431.85 486c-1.48-.31-4.2-2.14-7.87-3-1.41-.34-3.84-1.88-4.09-3.72-.32 2.11 2.52 4 4.09 4.36 3.67.89 6.39 2.72 7.87 3 1.1.23 2.52.38 3.4-.48a2.22 2.22 0 0 0 .48-.88 4.38 4.38 0 0 1-3.88.72Z"
/> />
<path <path
id="_\u041A\u043E\u043D\u0442\u0443\u0440_56"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
className="fill-[#e26161]" className="fill-[#e26161]"
d="M435.32 460.15a44.45 44.45 0 0 0-1.67-8s.5-6.88.75-13.4c.28-7.4 2.85-13.91.82-19.81l-22.82 5.07s1.38 29.5 1.68 33.07a66.94 66.94 0 0 0 1 8.28c1 5.73 2.73 19.35 2.73 19.35a8.52 8.52 0 0 0 5.58.62s-.17-17.22-.33-21.35c-.19-5-.34-4.65-.34-4.65l.76-12.55.49-6.24s.63 4.58 1 8.67c.36 3.57 1.22 6.8 2.18 13.44.84 5.76 2.19 17.34 2.19 17.34 2.42 1.4 5.69.24 5.69.24s.97-15.08.29-20.08Z" d="M435.32 460.15a44.45 44.45 0 0 0-1.67-8s.5-6.88.75-13.4c.28-7.4 2.85-13.91.82-19.81l-22.82 5.07s1.38 29.5 1.68 33.07a66.94 66.94 0 0 0 1 8.28c1 5.73 2.73 19.35 2.73 19.35a8.52 8.52 0 0 0 5.58.62s-.17-17.22-.33-21.35c-.19-5-.34-4.65-.34-4.65l.76-12.55.49-6.24s.63 4.58 1 8.67c.36 3.57 1.22 6.8 2.18 13.44.84 5.76 2.19 17.34 2.19 17.34 2.42 1.4 5.69.24 5.69.24s.97-15.08.29-20.08Z"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
id="_\u041A\u043E\u043D\u0442\u0443\u0440_56"
/> />
<path <path
id="_\u041A\u043E\u043D\u0442\u0443\u0440_57"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
className="fill-[#ffcea9]" className="fill-[#ffcea9]"
d="M432 383.88c-1.74.09-3 .54-3.25 0a24.73 24.73 0 0 1-.4-2.71c.2-.51.38-1.05.38-1.05 2.18-1.34 2.87-3.7 3.16-6.45.49-4.7-2.05-8.81-6.75-9.3-4.41-.46-7.66 2.65-8.53 6.91-.46 2.1-1.39 6.07-.14 9.53.71 1.94 1.61 3.47 2.52 3.77a20.09 20.09 0 0 0 2.61-.23s.23 1.3.39 2.14.1 1.28-1.57 2.3 2.52 3.14 5.26 2.93 5.81-1.86 6.66-3.62c.93-1.95.84-4.27-.34-4.22Z" d="M432 383.88c-1.74.09-3 .54-3.25 0a24.73 24.73 0 0 1-.4-2.71c.2-.51.38-1.05.38-1.05 2.18-1.34 2.87-3.7 3.16-6.45.49-4.7-2.05-8.81-6.75-9.3-4.41-.46-7.66 2.65-8.53 6.91-.46 2.1-1.39 6.07-.14 9.53.71 1.94 1.61 3.47 2.52 3.77a20.09 20.09 0 0 0 2.61-.23s.23 1.3.39 2.14.1 1.28-1.57 2.3 2.52 3.14 5.26 2.93 5.81-1.86 6.66-3.62c.93-1.95.84-4.27-.34-4.22Z"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
id="_\u041A\u043E\u043D\u0442\u0443\u0440_57"
/> />
<path <path
id="_\u041A\u043E\u043D\u0442\u0443\u0440_58"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
className="fill-[#e4e4e7]" className="fill-[#e4e4e7]"
d="M429.48 384.14c.48 1.2-.75 2.67-4 3.7s-3.56-.3-3.56-.3a66.49 66.49 0 0 0-6.35 3.27c-2 1.37-2.82 5.34-3.09 12-.3 7.74-.33 19.23-.07 21.21 0 0 3.66 3.65 8.35 3.27s12.57-4.53 14.48-7.74c-.05-6.79-.85-7.89.25-11.8 2.52-9 3.84-12.62 2.62-18.57-1-4.91-2.42-5.52-4.36-5.47a41.07 41.07 0 0 0-4.27.43Z" d="M429.48 384.14c.48 1.2-.75 2.67-4 3.7s-3.56-.3-3.56-.3a66.49 66.49 0 0 0-6.35 3.27c-2 1.37-2.82 5.34-3.09 12-.3 7.74-.33 19.23-.07 21.21 0 0 3.66 3.65 8.35 3.27s12.57-4.53 14.48-7.74c-.05-6.79-.85-7.89.25-11.8 2.52-9 3.84-12.62 2.62-18.57-1-4.91-2.42-5.52-4.36-5.47a41.07 41.07 0 0 0-4.27.43Z"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
id="_\u041A\u043E\u043D\u0442\u0443\u0440_58"
/> />
<path <path
id="_\u041A\u043E\u043D\u0442\u0443\u0440_59"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
className="fill-[#ffcea9]" className="fill-[#ffcea9]"
d="M405.1 409.24c3.85 1 4.75-1.45 5.26-3.46 1.22-4.86 1.88-9.25 2.79-12 1.08-3.26 2.2-3.78 3.79-4.41 1.85-.75 3.45.82 2.83 5a82.82 82.82 0 0 1-3.52 14.13c-.48 1.52-1.55 4.21-3.16 5.51-2 1.6-5 1.53-9.5.41-1.92-.48-4-1.63-7.69-3.33-1-.46-1.74-.76-3.62-1.64a12.94 12.94 0 0 1-4.82-3.76c-1-1.43-1.11-2.1-.91-2.42s.7-.32 1.22.28a10.72 10.72 0 0 0 2.77 2.56s-1.22-1.26-1.87-2.07a10.55 10.55 0 0 1-1.46-2.38c-.35-.83.37-2 .91-1.37s.81 1.2 1.8 2.39a10.35 10.35 0 0 0 1.78 1.7 19.53 19.53 0 0 1-1.64-2.64 4.45 4.45 0 0 1-.6-2.39c0-.43.65-.95 1.22-.12a16.2 16.2 0 0 0 2.07 3.15c.88.91 1.62 1.44 1.64 1.18s-.53-.75-.84-1.81 0-2.18.59-2.31.47-.09.94.91a28.49 28.49 0 0 1 1.75 3.17 6.56 6.56 0 0 0 1.58 2.73c.95.96 3.04 2.07 6.69 2.99Z" d="M405.1 409.24c3.85 1 4.75-1.45 5.26-3.46 1.22-4.86 1.88-9.25 2.79-12 1.08-3.26 2.2-3.78 3.79-4.41 1.85-.75 3.45.82 2.83 5a82.82 82.82 0 0 1-3.52 14.13c-.48 1.52-1.55 4.21-3.16 5.51-2 1.6-5 1.53-9.5.41-1.92-.48-4-1.63-7.69-3.33-1-.46-1.74-.76-3.62-1.64a12.94 12.94 0 0 1-4.82-3.76c-1-1.43-1.11-2.1-.91-2.42s.7-.32 1.22.28a10.72 10.72 0 0 0 2.77 2.56s-1.22-1.26-1.87-2.07a10.55 10.55 0 0 1-1.46-2.38c-.35-.83.37-2 .91-1.37s.81 1.2 1.8 2.39a10.35 10.35 0 0 0 1.78 1.7 19.53 19.53 0 0 1-1.64-2.64 4.45 4.45 0 0 1-.6-2.39c0-.43.65-.95 1.22-.12a16.2 16.2 0 0 0 2.07 3.15c.88.91 1.62 1.44 1.64 1.18s-.53-.75-.84-1.81 0-2.18.59-2.31.47-.09.94.91a28.49 28.49 0 0 1 1.75 3.17 6.56 6.56 0 0 0 1.58 2.73c.95.96 3.04 2.07 6.69 2.99Z"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
id="_\u041A\u043E\u043D\u0442\u0443\u0440_59"
/> />
<path <path
id="_\u041A\u043E\u043D\u0442\u0443\u0440_60"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
className="fill-[#e4e4e7]" className="fill-[#e4e4e7]"
d="M418.36 388.69c-2.57-.37-4.38.78-5.52 4.67s-1.92 7.09-1.92 7.09a7 7 0 0 0 4.3 2.86c3.07.74 4.18-.73 4.18-.73s.85-4.08 1.38-6.85.76-6.57-2.42-7.04Z" d="M418.36 388.69c-2.57-.37-4.38.78-5.52 4.67s-1.92 7.09-1.92 7.09a7 7 0 0 0 4.3 2.86c3.07.74 4.18-.73 4.18-.73s.85-4.08 1.38-6.85.76-6.57-2.42-7.04Z"
data-name="&lt;\u041A\u043E\u043D\u0442\u0443\u0440&gt;"
id="_\u041A\u043E\u043D\u0442\u0443\u0440_60"
/> />
<path <path
className="fill-[#2b2a30]" className="fill-[#2b2a30]"
@ -394,10 +394,10 @@ export const HeroSvg = (props: SVGProps<SVGSVGElement>) => (
export const ASvg = (props: SVGProps<SVGSVGElement>) => ( export const ASvg = (props: SVGProps<SVGSVGElement>) => (
<svg <svg
xmlns="http://www.w3.org/2000/svg"
width={80}
height={100} height={100}
viewBox="20 0 10 12" viewBox="20 0 10 12"
width={80}
xmlns="http://www.w3.org/2000/svg"
{...props} {...props}
> >
<title>{"\u0410"}</title> <title>{"\u0410"}</title>
@ -422,30 +422,30 @@ export const ASvg = (props: SVGProps<SVGSVGElement>) => (
export const LogoSvg = (props: SVGProps<SVGSVGElement>) => ( export const LogoSvg = (props: SVGProps<SVGSVGElement>) => (
<svg <svg
xmlns="http://www.w3.org/2000/svg"
width={258.758}
height={49.796} height={49.796}
viewBox="0 0 68.463 13.175" viewBox="0 0 68.463 13.175"
width={258.758}
xmlns="http://www.w3.org/2000/svg"
{...props} {...props}
> >
<title>{"lloggi.it Info"}</title> <title>{"lloggi.it Info"}</title>
<defs> <defs>
<path id="a" d="M69.953 191.276h677.665v225.16H69.953z" /> <path d="M69.953 191.276h677.665v225.16H69.953z" id="a" />
<path id="b" d="M69.953 191.276h677.665v225.16H69.953z" /> <path d="M69.953 191.276h677.665v225.16H69.953z" id="b" />
</defs> </defs>
<text <text
xmlSpace="preserve"
className="font-sans" className="font-sans"
style={{ style={{
fontSize: 40, fontSize: 40,
lineHeight: 5,
textAlign: "center",
letterSpacing: 1, letterSpacing: 1,
wordSpacing: 0, lineHeight: 5,
whiteSpace: "pre",
strokeWidth: 2, strokeWidth: 2,
textAlign: "center",
whiteSpace: "pre",
wordSpacing: 0,
}} }}
transform="matrix(.26458 0 0 .26458 -98.855 -69.754)" transform="matrix(.26458 0 0 .26458 -98.855 -69.754)"
xmlSpace="preserve"
> >
<tspan x={371.603} y={303.296}> <tspan x={371.603} y={303.296}>
<tspan <tspan
@ -458,18 +458,18 @@ export const LogoSvg = (props: SVGProps<SVGSVGElement>) => (
</tspan> </tspan>
</text> </text>
<text <text
xmlSpace="preserve"
className="font-sans" className="font-sans"
style={{ style={{
fontSize: 40, fontSize: 40,
lineHeight: 5,
textAlign: "center",
letterSpacing: 1, letterSpacing: 1,
wordSpacing: 0, lineHeight: 5,
whiteSpace: "pre",
strokeWidth: 2, strokeWidth: 2,
textAlign: "center",
whiteSpace: "pre",
wordSpacing: 0,
}} }}
transform="matrix(.26458 0 0 .26458 -58.739 -69.754)" transform="matrix(.26458 0 0 .26458 -58.739 -69.754)"
xmlSpace="preserve"
> >
<tspan x={336.717} y={303.296}> <tspan x={336.717} y={303.296}>
<tspan <tspan
@ -502,11 +502,11 @@ export const LogoSvg = (props: SVGProps<SVGSVGElement>) => (
export const EngFlag = (props: SVGProps<SVGSVGElement>) => ( export const EngFlag = (props: SVGProps<SVGSVGElement>) => (
<svg <svg
xmlns="http://www.w3.org/2000/svg"
xmlSpace="preserve"
width={800}
height={800} height={800}
viewBox="0 0 512 512" viewBox="0 0 512 512"
width={800}
xmlns="http://www.w3.org/2000/svg"
xmlSpace="preserve"
{...props} {...props}
> >
<title>EnglishFlag</title> <title>EnglishFlag</title>
@ -539,9 +539,9 @@ export const EngFlag = (props: SVGProps<SVGSVGElement>) => (
export const ItaFlag = (props: SVGProps<SVGSVGElement>) => ( export const ItaFlag = (props: SVGProps<SVGSVGElement>) => (
<svg <svg
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlSpace="preserve" xmlSpace="preserve"
viewBox="0 0 512 512"
{...props} {...props}
> >
<title>ItalianFlag</title> <title>ItalianFlag</title>
@ -568,16 +568,16 @@ export const ItaFlag = (props: SVGProps<SVGSVGElement>) => (
export const MissingFile = ({ className }: { className?: string }) => ( export const MissingFile = ({ className }: { className?: string }) => (
<svg <svg
xmlns="http://www.w3.org/2000/svg" className={className}
viewBox="0 0 512 512"
style={{ style={{
clipRule: "evenodd",
fillRule: "evenodd",
imageRendering: "auto",
shapeRendering: "geometricPrecision", shapeRendering: "geometricPrecision",
textRendering: "geometricPrecision", textRendering: "geometricPrecision",
imageRendering: "auto",
fillRule: "evenodd",
clipRule: "evenodd",
}} }}
className={className} viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
> >
<title>MissingFile</title> <title>MissingFile</title>
<path <path
@ -651,11 +651,11 @@ export const MissingFile = ({ className }: { className?: string }) => (
export const WhatsAppIcon = ({ className }: { className?: string }) => ( export const WhatsAppIcon = ({ className }: { className?: string }) => (
<svg <svg
xmlns="http://www.w3.org/2000/svg" className={className}
width={800}
height={800} height={800}
viewBox="0 0 24 24" viewBox="0 0 24 24"
className={className} width={800}
xmlns="http://www.w3.org/2000/svg"
> >
<title>{"WhatsApp icon"}</title> <title>{"WhatsApp icon"}</title>
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51a12.8 12.8 0 0 0-.57-.01c-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 0 1-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 0 1-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 0 1 2.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0 0 12.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 0 0 5.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 0 0-3.48-8.413Z" /> <path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51a12.8 12.8 0 0 0-.57-.01c-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 0 1-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 0 1-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 0 1 2.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0 0 12.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 0 0 5.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 0 0-3.48-8.413Z" />

View file

@ -15,55 +15,53 @@ export const BanlistTable = (props: {
const tabledata = data.map((row) => { const tabledata = data.map((row) => {
return { return {
actions: "Azioni",
id: row.id, id: row.id,
type: row.type, type: row.type,
value: row.value, value: row.value,
actions: "Azioni",
}; };
}); });
const columns_titles = { const columns_titles = {
actions: "Azioni",
id: "Codice", id: "Codice",
type: "Tipo", type: "Tipo",
value: "Valore", value: "Valore",
actions: "Azioni",
}; };
type Column = (typeof tabledata)[number]; type Column = (typeof tabledata)[number];
const columns: ColumnDef<Column>[] = [ const columns: ColumnDef<Column>[] = [
{ {
accessorKey: "id",
header: ({ column }) => ( header: ({ column }) => (
<DataTableColumnHeader column={column} title={columns_titles.id} /> <DataTableColumnHeader column={column} title={columns_titles.id} />
), ),
accessorKey: "id",
}, },
{ {
accessorKey: "type",
header: ({ column }) => ( header: ({ column }) => (
<DataTableColumnHeader column={column} title={columns_titles.type} /> <DataTableColumnHeader column={column} title={columns_titles.type} />
), ),
accessorKey: "type",
}, },
{ {
accessorKey: "value",
header: ({ column }) => ( header: ({ column }) => (
<DataTableColumnHeader column={column} title={columns_titles.value} /> <DataTableColumnHeader column={column} title={columns_titles.value} />
), ),
accessorKey: "value",
}, },
{ {
header: ({ column }) => ( accessorKey: "actions",
<DataTableColumnHeader column={column} title={columns_titles.actions} />
),
cell: ({ row }) => { cell: ({ row }) => {
const data = row.original; const data = row.original;
return ( return (
<Button <Button
aria-label="Edit Banlist Entry" aria-label="Edit Banlist Entry"
variant="outline"
onClick={() => { onClick={() => {
setEditData(data); setEditData(data);
setOpenEdit(true); setOpenEdit(true);
}} }}
variant="outline"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Cog /> Modifica <Cog /> Modifica
@ -71,9 +69,11 @@ export const BanlistTable = (props: {
</Button> </Button>
); );
}, },
accessorKey: "actions",
enableHiding: false, enableHiding: false,
enableSorting: false, enableSorting: false,
header: ({ column }) => (
<DataTableColumnHeader column={column} title={columns_titles.actions} />
),
}, },
]; ];
@ -85,11 +85,11 @@ export const BanlistTable = (props: {
<div className="mx-auto w-full px-2 py-10"> <div className="mx-auto w-full px-2 py-10">
<DataTable <DataTable
columns={columns} columns={columns}
columns_titles={columns_titles}
data={tabledata} data={tabledata}
defaultSort={[{ desc: true, id: "id" }]}
pinnedFiltri={undefined} pinnedFiltri={undefined}
searchColumn={searchFiltro} searchColumn={searchFiltro}
columns_titles={columns_titles}
defaultSort={[{ desc: true, id: "id" }]}
/> />
</div> </div>
); );

View file

@ -14,183 +14,180 @@ export const BannersTable = (props: {
const tabledata = data.map((row) => { const tabledata = data.map((row) => {
return { return {
idbanner: row.idbanner, actions: "Azioni",
titolo: row.titolo, color: row.color,
testo: row.testo,
is_unskippable: row.is_unskippable,
has_cta: row.has_cta,
cta_href: row.cta_href, cta_href: row.cta_href,
cta_icon: row.cta_icon, cta_icon: row.cta_icon,
cta_text: row.cta_text, cta_text: row.cta_text,
color: row.color, has_cta: row.has_cta,
is_active: row.is_active,
show_public: row.show_public,
show_private: row.show_private,
hide_duration: row.hide_duration, hide_duration: row.hide_duration,
idbanner: row.idbanner,
actions: "Azioni", is_active: row.is_active,
is_unskippable: row.is_unskippable,
show_private: row.show_private,
show_public: row.show_public,
testo: row.testo,
titolo: row.titolo,
}; };
}); });
const columns_titles = { const columns_titles = {
idbanner: "Codice", actions: "Azioni",
titolo: "Titolo", color: "Colore",
testo: "Testo",
is_unskippable: "Non saltabile",
has_cta: "Ha link",
hide_duration: "Durata",
cta_href: "Indirizzo", cta_href: "Indirizzo",
cta_icon: "Icona", cta_icon: "Icona",
cta_text: "Testo", cta_text: "Testo",
color: "Colore", has_cta: "Ha link",
hide_duration: "Durata",
idbanner: "Codice",
is_active: "Attivo", is_active: "Attivo",
actions: "Azioni", is_unskippable: "Non saltabile",
show_public: "Pubblico",
show_private: "A. Riservata", show_private: "A. Riservata",
show_public: "Pubblico",
testo: "Testo",
titolo: "Titolo",
}; };
type Column = (typeof tabledata)[number]; type Column = (typeof tabledata)[number];
const columns: ColumnDef<Column>[] = [ const columns: ColumnDef<Column>[] = [
{ {
accessorKey: "idbanner",
header: ({ column }) => ( header: ({ column }) => (
<DataTableColumnHeader <DataTableColumnHeader
column={column} column={column}
title={columns_titles.idbanner} title={columns_titles.idbanner}
/> />
), ),
accessorKey: "idbanner",
}, },
{ {
accessorKey: "titolo",
header: ({ column }) => ( header: ({ column }) => (
<DataTableColumnHeader column={column} title={columns_titles.titolo} /> <DataTableColumnHeader column={column} title={columns_titles.titolo} />
), ),
accessorKey: "titolo",
}, },
{ {
accessorKey: "testo",
header: ({ column }) => ( header: ({ column }) => (
<DataTableColumnHeader column={column} title={columns_titles.testo} /> <DataTableColumnHeader column={column} title={columns_titles.testo} />
), ),
accessorKey: "testo",
}, },
{ {
accessorKey: "is_unskippable",
cell: ({ row }) => {
const data = row.original;
return data.is_unskippable ? "Vero" : "Falso";
},
header: ({ column }) => ( header: ({ column }) => (
<DataTableColumnHeader <DataTableColumnHeader
column={column} column={column}
title={columns_titles.is_unskippable} title={columns_titles.is_unskippable}
/> />
), ),
accessorKey: "is_unskippable",
cell: ({ row }) => {
const data = row.original;
return data.is_unskippable ? "Vero" : "Falso";
},
}, },
{ {
header: ({ column }) => (
<DataTableColumnHeader column={column} title={columns_titles.has_cta} />
),
accessorKey: "has_cta", accessorKey: "has_cta",
cell: ({ row }) => { cell: ({ row }) => {
const data = row.original; const data = row.original;
return data.has_cta ? "Vero" : "Falso"; return data.has_cta ? "Vero" : "Falso";
}, },
header: ({ column }) => (
<DataTableColumnHeader column={column} title={columns_titles.has_cta} />
),
}, },
{ {
accessorKey: "hide_duration",
header: ({ column }) => ( header: ({ column }) => (
<DataTableColumnHeader <DataTableColumnHeader
column={column} column={column}
title={columns_titles.hide_duration} title={columns_titles.hide_duration}
/> />
), ),
accessorKey: "hide_duration",
}, },
{ {
accessorKey: "cta_href",
header: ({ column }) => ( header: ({ column }) => (
<DataTableColumnHeader <DataTableColumnHeader
column={column} column={column}
title={columns_titles.cta_href} title={columns_titles.cta_href}
/> />
), ),
accessorKey: "cta_href",
}, },
{ {
accessorKey: "cta_icon",
header: ({ column }) => ( header: ({ column }) => (
<DataTableColumnHeader <DataTableColumnHeader
column={column} column={column}
title={columns_titles.cta_icon} title={columns_titles.cta_icon}
/> />
), ),
accessorKey: "cta_icon",
}, },
{ {
accessorKey: "cta_text",
header: ({ column }) => ( header: ({ column }) => (
<DataTableColumnHeader <DataTableColumnHeader
column={column} column={column}
title={columns_titles.cta_text} title={columns_titles.cta_text}
/> />
), ),
accessorKey: "cta_text",
}, },
{ {
accessorKey: "color",
header: ({ column }) => ( header: ({ column }) => (
<DataTableColumnHeader column={column} title={columns_titles.color} /> <DataTableColumnHeader column={column} title={columns_titles.color} />
), ),
accessorKey: "color",
}, },
{ {
accessorKey: "is_active",
cell: ({ row }) => {
const data = row.original;
return data.is_active ? "Vero" : "Falso";
},
header: ({ column }) => ( header: ({ column }) => (
<DataTableColumnHeader <DataTableColumnHeader
column={column} column={column}
title={columns_titles.is_active} title={columns_titles.is_active}
/> />
), ),
accessorKey: "is_active",
cell: ({ row }) => {
const data = row.original;
return data.is_active ? "Vero" : "Falso";
},
}, },
{ {
accessorKey: "show_public",
cell: ({ row }) => {
const data = row.original;
return data.show_public ? "Vero" : "Falso";
},
header: ({ column }) => ( header: ({ column }) => (
<DataTableColumnHeader <DataTableColumnHeader
column={column} column={column}
title={columns_titles.show_public} title={columns_titles.show_public}
/> />
), ),
accessorKey: "show_public",
cell: ({ row }) => {
const data = row.original;
return data.show_public ? "Vero" : "Falso";
},
}, },
{ {
accessorKey: "show_private",
cell: ({ row }) => {
const data = row.original;
return data.show_private ? "Vero" : "Falso";
},
header: ({ column }) => ( header: ({ column }) => (
<DataTableColumnHeader <DataTableColumnHeader
column={column} column={column}
title={columns_titles.show_private} title={columns_titles.show_private}
/> />
), ),
accessorKey: "show_private",
cell: ({ row }) => {
const data = row.original;
return data.show_private ? "Vero" : "Falso";
},
}, },
{ {
header: ({ column }) => ( accessorKey: "actions",
<DataTableColumnHeader column={column} title={columns_titles.actions} />
),
cell: ({ row }) => { cell: ({ row }) => {
const data = row.original; const data = row.original;
return ( return (
<Button <Button
aria-label="Edit Banner" aria-label="Edit Banner"
variant="outline"
onClick={() => { onClick={() => {
setEditData(data); setEditData(data);
setOpenEdit(true); setOpenEdit(true);
}} }}
variant="outline"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Cog /> Modifica <Cog /> Modifica
@ -198,9 +195,11 @@ export const BannersTable = (props: {
</Button> </Button>
); );
}, },
accessorKey: "actions",
enableHiding: false, enableHiding: false,
enableSorting: false, enableSorting: false,
header: ({ column }) => (
<DataTableColumnHeader column={column} title={columns_titles.actions} />
),
}, },
]; ];
@ -212,28 +211,28 @@ export const BannersTable = (props: {
<div className="mx-auto w-full px-2 py-10"> <div className="mx-auto w-full px-2 py-10">
<DataTable <DataTable
columns={columns} columns={columns}
data={tabledata}
pinnedFiltri={undefined}
searchColumn={searchFiltro}
columns_titles={columns_titles} columns_titles={columns_titles}
defaultSort={[{ desc: true, id: "idbanner" }]} data={tabledata}
defaultColumnVisibility={{ defaultColumnVisibility={{
idbanner: true, actions: true,
color: false,
titolo: true,
testo: true,
is_unskippable: true,
has_cta: true,
hide_duration: false,
cta_href: false, cta_href: false,
cta_icon: false, cta_icon: false,
cta_text: false, cta_text: false,
color: false, has_cta: true,
hide_duration: false,
idbanner: true,
is_active: true, is_active: true,
actions: true, is_unskippable: true,
show_public: true,
show_private: true, show_private: true,
show_public: true,
testo: true,
titolo: true,
}} }}
defaultSort={[{ desc: true, id: "idbanner" }]}
pinnedFiltri={undefined}
searchColumn={searchFiltro}
/> />
</div> </div>
); );

Some files were not shown because too many files have changed in this diff Show more