eslint migration biome
This commit is contained in:
parent
9185d8ddec
commit
3f82763168
348 changed files with 64918 additions and 67612 deletions
|
|
@ -17,6 +17,4 @@ knip.json
|
||||||
check-env-vars.js
|
check-env-vars.js
|
||||||
headers
|
headers
|
||||||
.kanelrc.js
|
.kanelrc.js
|
||||||
eslint.config.mjs
|
|
||||||
prettier.config.cjs
|
|
||||||
kyselyRules
|
kyselyRules
|
||||||
33
apps/infoalloggi/.vscode/settings.json
vendored
33
apps/infoalloggi/.vscode/settings.json
vendored
|
|
@ -1,21 +1,16 @@
|
||||||
{
|
{
|
||||||
"typescript.tsdk": "node_modules\\typescript\\lib",
|
"typescript.tsdk": "node_modules\\typescript\\lib",
|
||||||
"sqltools.connections": [
|
"sqltools.connections": [
|
||||||
{
|
{
|
||||||
"previewLimit": 50,
|
"previewLimit": 50,
|
||||||
"server": "localhost",
|
"server": "localhost",
|
||||||
"port": 5432,
|
"port": 5432,
|
||||||
"driver": "PostgreSQL",
|
"driver": "PostgreSQL",
|
||||||
"name": "DB",
|
"name": "DB",
|
||||||
"database": "postgres",
|
"database": "postgres",
|
||||||
"username": "postgres",
|
"username": "postgres",
|
||||||
"password": "rootpost"
|
"password": "rootpost"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"source.fixAll.eslint": "explicit",
|
|
||||||
"source.fixAll.biome": "explicit",
|
|
||||||
"source.organizeImports.biome": "explicit"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -44,7 +44,11 @@
|
||||||
"!*compose.yml",
|
"!*compose.yml",
|
||||||
"!.kanelrc.js",
|
"!.kanelrc.js",
|
||||||
"!TODO",
|
"!TODO",
|
||||||
"!*.d.ts"
|
"!*.d.ts",
|
||||||
|
"!src/i18n/comuni.ts",
|
||||||
|
"!src/i18n/nazioni.ts",
|
||||||
|
"!src/i18n/provincie.ts",
|
||||||
|
"!src/styles/globals.css"
|
||||||
],
|
],
|
||||||
"ignoreUnknown": true
|
"ignoreUnknown": true
|
||||||
},
|
},
|
||||||
|
|
@ -54,6 +58,7 @@
|
||||||
},
|
},
|
||||||
"linter": {
|
"linter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
|
||||||
"domains": {
|
"domains": {
|
||||||
"next": "recommended",
|
"next": "recommended",
|
||||||
"react": "recommended"
|
"react": "recommended"
|
||||||
|
|
@ -61,14 +66,29 @@
|
||||||
"rules": {
|
"rules": {
|
||||||
"recommended": true,
|
"recommended": true,
|
||||||
"a11y": {
|
"a11y": {
|
||||||
"noNoninteractiveElementInteractions": "error"
|
"noNoninteractiveElementInteractions": "error",
|
||||||
|
"useSemanticElements": "off",
|
||||||
|
"noAutofocus": "off"
|
||||||
},
|
},
|
||||||
"suspicious": {
|
"suspicious": {
|
||||||
"noArrayIndexKey": "off"
|
"useIterableCallbackReturn": "off",
|
||||||
|
"noArrayIndexKey": "off",
|
||||||
|
"noUnknownAtRules": "off"
|
||||||
|
},
|
||||||
|
"complexity": {
|
||||||
|
"noUselessFragments": "off"
|
||||||
},
|
},
|
||||||
|
|
||||||
"correctness": {
|
"correctness": {
|
||||||
"useExhaustiveDependencies": "off"
|
"useHookAtTopLevel": "off",
|
||||||
|
"useExhaustiveDependencies": "off",
|
||||||
|
"useParseIntRadix": "off"
|
||||||
|
},
|
||||||
|
"performance": {
|
||||||
|
"noImgElement": "off"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"noDangerouslySetInnerHtml": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,79 +1,79 @@
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Container,
|
Container,
|
||||||
Head,
|
Head,
|
||||||
Html,
|
Hr,
|
||||||
Preview,
|
Html,
|
||||||
Section,
|
Img,
|
||||||
Text,
|
Preview,
|
||||||
Tailwind,
|
Section,
|
||||||
Img,
|
Tailwind,
|
||||||
Hr,
|
Text,
|
||||||
} from "@react-email/components";
|
} from "@react-email/components";
|
||||||
import type { JSX } from "react";
|
import type { JSX } from "react";
|
||||||
|
|
||||||
const Base = ({
|
const Base = ({
|
||||||
children,
|
children,
|
||||||
preview,
|
preview,
|
||||||
noreply,
|
noreply,
|
||||||
}: {
|
}: {
|
||||||
children: JSX.Element;
|
children: JSX.Element;
|
||||||
preview: string;
|
preview: string;
|
||||||
noreply?: boolean;
|
noreply?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Tailwind
|
<Tailwind
|
||||||
config={{
|
config={{
|
||||||
theme: {
|
theme: {
|
||||||
container: {
|
container: {
|
||||||
center: true,
|
center: true,
|
||||||
padding: "2rem",
|
padding: "2rem",
|
||||||
screens: {
|
screens: {
|
||||||
"2xl": "1400px",
|
"2xl": "1400px",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Html>
|
<Html>
|
||||||
<Head />
|
<Head />
|
||||||
|
|
||||||
<Preview>{preview}</Preview>
|
<Preview>{preview}</Preview>
|
||||||
<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"
|
referrerPolicy="no-referrer"
|
||||||
width={230.5}
|
width={230.5}
|
||||||
height={44.25}
|
height={44.25}
|
||||||
// src="https://lh3.google.com/u/0/d/1nEurjVWPiz0F8QRLhKcT4ZSujeFRChBs"
|
// src="https://lh3.google.com/u/0/d/1nEurjVWPiz0F8QRLhKcT4ZSujeFRChBs"
|
||||||
src="cid:logo"
|
src="cid:logo"
|
||||||
alt="Infoalloggi.it"
|
alt="Infoalloggi.it"
|
||||||
className="mx-auto my-2 scale-80"
|
className="mx-auto my-2 scale-80"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Section className="max-w-lg px-8">
|
<Section className="max-w-lg px-8">
|
||||||
<Hr />
|
<Hr />
|
||||||
<Section className="max-w-md">{children}</Section>
|
<Section className="max-w-md">{children}</Section>
|
||||||
|
|
||||||
<Section className="mt-4">
|
<Section className="mt-4">
|
||||||
{noreply && (
|
{noreply && (
|
||||||
<Text className="text-xs text-neutral-500">
|
<Text className="text-xs text-neutral-500">
|
||||||
Questo è un messaggio automatico, per favore non rispondere
|
Questo è un messaggio automatico, per favore non rispondere
|
||||||
a questa email.
|
a questa email.
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Section>
|
</Section>
|
||||||
<Section className="mt-3">
|
<Section className="mt-3">
|
||||||
<Text className="text-left text-sm">
|
<Text className="text-left text-sm">
|
||||||
Arcenia S.r.l. Sede legale: Via Beata Giovanna 1, Bassano del
|
Arcenia S.r.l. Sede legale: Via Beata Giovanna 1, Bassano del
|
||||||
Grappa (VI)
|
Grappa (VI)
|
||||||
</Text>
|
</Text>
|
||||||
</Section>
|
</Section>
|
||||||
</Section>
|
</Section>
|
||||||
</Container>
|
</Container>
|
||||||
</Body>
|
</Body>
|
||||||
</Html>
|
</Html>
|
||||||
</Tailwind>
|
</Tailwind>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default Base;
|
export default Base;
|
||||||
|
|
|
||||||
|
|
@ -2,43 +2,43 @@ import { Heading, Row, Section, Text } from "@react-email/components";
|
||||||
import Base from "./base";
|
import Base from "./base";
|
||||||
|
|
||||||
export type ContattoAdmin_NewMail = {
|
export type ContattoAdmin_NewMail = {
|
||||||
mailType: "contattoAdminEmail";
|
mailType: "contattoAdminEmail";
|
||||||
props: ContattoAdminEmailProps;
|
props: ContattoAdminEmailProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ContattoAdminEmailProps = {
|
type ContattoAdminEmailProps = {
|
||||||
nome: string;
|
nome: string;
|
||||||
cognome: string;
|
cognome: string;
|
||||||
email: string;
|
email: string;
|
||||||
telefono: string;
|
telefono: string;
|
||||||
messaggio: string;
|
messaggio: string;
|
||||||
};
|
};
|
||||||
const ContattoAdminEmail = ({
|
const ContattoAdminEmail = ({
|
||||||
nome,
|
nome,
|
||||||
cognome,
|
cognome,
|
||||||
email,
|
email,
|
||||||
telefono,
|
telefono,
|
||||||
messaggio,
|
messaggio,
|
||||||
}: ContattoAdminEmailProps) => {
|
}: ContattoAdminEmailProps) => {
|
||||||
return (
|
return (
|
||||||
<Base preview="Contatto Email">
|
<Base preview="Contatto Email">
|
||||||
<>
|
<>
|
||||||
<Heading className="text-center text-3xl leading-8">
|
<Heading className="text-center text-3xl leading-8">
|
||||||
Richiesta di contatto
|
Richiesta di contatto
|
||||||
</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>Data: {new Date().toLocaleString()}</Text>
|
<Text>Data: {new Date().toLocaleString()}</Text>
|
||||||
<Text>Nome: {nome}</Text>
|
<Text>Nome: {nome}</Text>
|
||||||
<Text>Cognome: {cognome}</Text>
|
<Text>Cognome: {cognome}</Text>
|
||||||
<Text>Email: {email}</Text>
|
<Text>Email: {email}</Text>
|
||||||
<Text>Telefono: {telefono}</Text>
|
<Text>Telefono: {telefono}</Text>
|
||||||
<Text>Messaggio: {messaggio}</Text>
|
<Text>Messaggio: {messaggio}</Text>
|
||||||
</Row>
|
</Row>
|
||||||
</Section>
|
</Section>
|
||||||
</>
|
</>
|
||||||
</Base>
|
</Base>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default ContattoAdminEmail;
|
export default ContattoAdminEmail;
|
||||||
|
|
|
||||||
|
|
@ -2,51 +2,51 @@ import { Button, Heading, Row, Section, Text } from "@react-email/components";
|
||||||
import Base from "./base";
|
import Base from "./base";
|
||||||
|
|
||||||
export type ContattoAnnuncio_NewMail = {
|
export type ContattoAnnuncio_NewMail = {
|
||||||
mailType: "contattoAnnuncioEmail";
|
mailType: "contattoAnnuncioEmail";
|
||||||
props: ContattoAnnuncioEmailProps;
|
props: ContattoAnnuncioEmailProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ContattoAnnuncioEmailProps = {
|
type ContattoAnnuncioEmailProps = {
|
||||||
nome: string;
|
nome: string;
|
||||||
codice: string;
|
codice: string;
|
||||||
email: string;
|
email: string;
|
||||||
telefono: string;
|
telefono: string;
|
||||||
messaggio: string;
|
messaggio: string;
|
||||||
};
|
};
|
||||||
const ContattoAnnuncioEmail = ({
|
const ContattoAnnuncioEmail = ({
|
||||||
nome,
|
nome,
|
||||||
codice,
|
codice,
|
||||||
email,
|
email,
|
||||||
telefono,
|
telefono,
|
||||||
messaggio,
|
messaggio,
|
||||||
}: ContattoAnnuncioEmailProps) => {
|
}: ContattoAnnuncioEmailProps) => {
|
||||||
return (
|
return (
|
||||||
<Base preview="Contatto Email">
|
<Base preview="Contatto Email">
|
||||||
<>
|
<>
|
||||||
<Heading className="text-center text-3xl leading-8">
|
<Heading className="text-center text-3xl leading-8">
|
||||||
Richiesta di contatto
|
Richiesta di contatto
|
||||||
</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>Data: {new Date().toLocaleString()}</Text>
|
<Text>Data: {new Date().toLocaleString()}</Text>
|
||||||
<Text>Nome: {nome}</Text>
|
<Text>Nome: {nome}</Text>
|
||||||
<Text>Annuncio: {codice}</Text>
|
<Text>Annuncio: {codice}</Text>
|
||||||
<Text>Telefono: {telefono}</Text>
|
<Text>Telefono: {telefono}</Text>
|
||||||
<Text>Email: {email}</Text>
|
<Text>Email: {email}</Text>
|
||||||
<Text>Messaggio: {messaggio}</Text>
|
<Text>Messaggio: {messaggio}</Text>
|
||||||
</Row>
|
</Row>
|
||||||
</Section>
|
</Section>
|
||||||
<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}/annuncio/${codice}`}
|
href={`${process.env.NEXT_PUBLIC_BASE_URL}/annuncio/${codice}`}
|
||||||
>
|
>
|
||||||
Vai all'annuncio
|
Vai all'annuncio
|
||||||
</Button>
|
</Button>
|
||||||
</Section>
|
</Section>
|
||||||
</>
|
</>
|
||||||
</Base>
|
</Base>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default ContattoAnnuncioEmail;
|
export default ContattoAnnuncioEmail;
|
||||||
|
|
|
||||||
|
|
@ -1,71 +1,71 @@
|
||||||
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 ContattoInteressato_NewMail = {
|
export type ContattoInteressato_NewMail = {
|
||||||
mailType: "emailContattoInteressato";
|
mailType: "emailContattoInteressato";
|
||||||
props: EmailContattoInteressatoProps;
|
props: EmailContattoInteressatoProps;
|
||||||
};
|
};
|
||||||
type EmailContattoInteressatoProps = {
|
type EmailContattoInteressatoProps = {
|
||||||
nome_cognome: string;
|
nome_cognome: string;
|
||||||
nome_cognome_utente: string;
|
nome_cognome_utente: string;
|
||||||
telefono: string;
|
telefono: string;
|
||||||
indirizzo: string | null;
|
indirizzo: string | null;
|
||||||
sesso: string | null;
|
sesso: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const EmailContattoInteressato = ({
|
const EmailContattoInteressato = ({
|
||||||
nome_cognome,
|
nome_cognome,
|
||||||
nome_cognome_utente,
|
nome_cognome_utente,
|
||||||
telefono,
|
telefono,
|
||||||
indirizzo,
|
indirizzo,
|
||||||
sesso,
|
sesso,
|
||||||
}: EmailContattoInteressatoProps) => {
|
}: EmailContattoInteressatoProps) => {
|
||||||
const sig = sesso
|
const sig = sesso
|
||||||
? sesso == "M"
|
? sesso === "M"
|
||||||
? "Il Sig."
|
? "Il Sig."
|
||||||
: "La Sig.ra"
|
: "La Sig.ra"
|
||||||
: "Il/La Sig./Sig.ra";
|
: "Il/La Sig./Sig.ra";
|
||||||
return (
|
return (
|
||||||
<Base preview="Contatto Email">
|
<Base preview="Contatto Email">
|
||||||
<>
|
<>
|
||||||
<Heading className="text-center text-3xl leading-8">
|
<Heading className="text-center text-3xl leading-8">
|
||||||
Richiesta di contatto
|
Richiesta di contatto
|
||||||
</Heading>
|
</Heading>
|
||||||
<Section className="px-5 text-left md:px-12">
|
<Section className="px-5 text-left md:px-12">
|
||||||
<Row>
|
<Row>
|
||||||
<Text className="text-base">
|
<Text className="text-base">
|
||||||
Gentile proprietario {nome_cognome}, le comunichiamo che a breve
|
Gentile proprietario {nome_cognome}, le comunichiamo che a breve
|
||||||
la contatterà {sig} {nome_cognome_utente} (tel: {telefono}),
|
la contatterà {sig} {nome_cognome_utente} (tel: {telefono}),
|
||||||
cliente Arca abbonato tramite il nostro servizio online
|
cliente Arca abbonato tramite il nostro servizio online
|
||||||
Infoalloggi.it interessato a vedere il Suo immobile{" "}
|
Infoalloggi.it interessato a vedere il Suo immobile{" "}
|
||||||
{indirizzo ? "di " + indirizzo : ""}.
|
{indirizzo ? `di ${indirizzo}` : ""}.
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-base">
|
<Text className="text-base">
|
||||||
Per ulteriori informazioni e chiarimenti non esiti a contattarci
|
Per ulteriori informazioni e chiarimenti non esiti a contattarci
|
||||||
all'indirizzo{" "}
|
all'indirizzo{" "}
|
||||||
<a href="mailto:web@infoalloggi.it" rel="noopener noreferrer">
|
<a href="mailto:web@infoalloggi.it" rel="noopener noreferrer">
|
||||||
web@infoalloggi.it
|
web@infoalloggi.it
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
oppure tramite tel. 04241760915.
|
oppure tramite tel. 04241760915.
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-base">Cordiali saluti</Text>
|
<Text className="text-base">Cordiali saluti</Text>
|
||||||
<br />
|
<br />
|
||||||
<Text className="text-xs">
|
<Text className="text-xs">
|
||||||
{" "}
|
{" "}
|
||||||
Le informazioni contenute in questo messaggio di posta elettronica
|
Le informazioni contenute in questo messaggio di posta elettronica
|
||||||
sono riservate confidenziali e ne è vietata la diffusione in
|
sono riservate confidenziali e ne è vietata la diffusione in
|
||||||
qualunque modo eseguita. Qualora Lei non fosse la persona a cui il
|
qualunque modo eseguita. Qualora Lei non fosse la persona a cui il
|
||||||
presente messaggio è destinato, La invitiamo gentilmente ad
|
presente messaggio è destinato, La invitiamo gentilmente ad
|
||||||
eliminarlo dopo averne dato tempestiva comunicazione al mittente e
|
eliminarlo dopo averne dato tempestiva comunicazione al mittente e
|
||||||
a non utilizzare in alcun caso il suo contenuto. Qualsiasi
|
a non utilizzare in alcun caso il suo contenuto. Qualsiasi
|
||||||
utilizzo non autorizzato di questo messaggio e dei suoi eventuali
|
utilizzo non autorizzato di questo messaggio e dei suoi eventuali
|
||||||
allegati espone il responsabile alle relative conseguenze civili e
|
allegati espone il responsabile alle relative conseguenze civili e
|
||||||
penali.
|
penali.
|
||||||
</Text>
|
</Text>
|
||||||
</Row>
|
</Row>
|
||||||
</Section>
|
</Section>
|
||||||
</>
|
</>
|
||||||
</Base>
|
</Base>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EmailContattoInteressato;
|
export default EmailContattoInteressato;
|
||||||
|
|
|
||||||
|
|
@ -1,102 +1,102 @@
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Heading,
|
Heading,
|
||||||
Img,
|
Img,
|
||||||
Link,
|
Link,
|
||||||
Row,
|
Row,
|
||||||
Section,
|
Section,
|
||||||
Text,
|
Text,
|
||||||
} from "@react-email/components";
|
} from "@react-email/components";
|
||||||
import Base from "./base";
|
import Base from "./base";
|
||||||
export type OnboardingServizio_NewMail = {
|
export type OnboardingServizio_NewMail = {
|
||||||
mailType: "onboardingServizio";
|
mailType: "onboardingServizio";
|
||||||
props: OnboardingServizioProps;
|
props: OnboardingServizioProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
type OnboardingServizioProps = {
|
type OnboardingServizioProps = {
|
||||||
token: string;
|
token: string;
|
||||||
nome: string;
|
nome: string;
|
||||||
annunci?: {
|
annunci?: {
|
||||||
titolo: string | null;
|
titolo: string | null;
|
||||||
immagine: string | null;
|
immagine: string | null;
|
||||||
codice: string;
|
codice: string;
|
||||||
prezzo: number;
|
prezzo: number;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const OnboardingServizio = ({
|
const OnboardingServizio = ({
|
||||||
token,
|
token,
|
||||||
nome,
|
nome,
|
||||||
annunci,
|
annunci,
|
||||||
}: OnboardingServizioProps) => {
|
}: OnboardingServizioProps) => {
|
||||||
return (
|
return (
|
||||||
<Base preview="Procedi ora su Infoalloggi.it">
|
<Base preview="Procedi ora su Infoalloggi.it">
|
||||||
<>
|
<>
|
||||||
<Heading className="text-center text-3xl leading-8">
|
<Heading className="text-center text-3xl leading-8">
|
||||||
Procedi ora su Infoalloggi.it
|
Procedi ora su Infoalloggi.it
|
||||||
</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>
|
||||||
Ciao {nome},<br />
|
Ciao {nome},<br />
|
||||||
Accedi ora al servizio di ricerca Infoalloggi, dove potrai andare
|
Accedi ora al servizio di ricerca Infoalloggi, dove potrai andare
|
||||||
a contattare direttamente i proprietari degli immobili
|
a contattare direttamente i proprietari degli immobili
|
||||||
disponibili.
|
disponibili.
|
||||||
</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}/servizio/pre-onboard/${token}`}
|
href={`${process.env.NEXT_PUBLIC_BASE_URL}/servizio/pre-onboard/${token}`}
|
||||||
>
|
>
|
||||||
Vai al servizio
|
Vai al servizio
|
||||||
</Button>
|
</Button>
|
||||||
</Section>
|
</Section>
|
||||||
<Section>
|
<Section>
|
||||||
{annunci?.map((annuncio, index) => (
|
{annunci?.map((annuncio, index) => (
|
||||||
<>
|
<>
|
||||||
<Text className="font-semibold">
|
<Text className="font-semibold">
|
||||||
Annunci selezionati per te:
|
Annunci selezionati per te:
|
||||||
</Text>
|
</Text>
|
||||||
<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}`}
|
src={`${process.env.NEXT_PUBLIC_BASE_URL}/go-api/images/get/${annuncio.immagine}`}
|
||||||
width="100%"
|
width="100%"
|
||||||
height="140px"
|
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"
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
<Row className="block w-full sm:hidden">
|
<Row className="block w-full sm:hidden">
|
||||||
<div className="mb-[5px] flex h-[24px] w-fit items-center justify-center gap-1 rounded-full bg-indigo-600 px-2 text-[12px] leading-none font-semibold text-white">
|
<div className="mb-[5px] flex h-[24px] w-fit items-center justify-center gap-1 rounded-full bg-indigo-600 px-2 text-[12px] leading-none font-semibold text-white">
|
||||||
<strong>Codice: </strong> {annuncio.codice}
|
<strong>Codice: </strong> {annuncio.codice}
|
||||||
</div>
|
</div>
|
||||||
<Heading
|
<Heading
|
||||||
as="h2"
|
as="h2"
|
||||||
className="mt-0 mb-[6px] text-[16px] leading-none font-bold"
|
className="mt-0 mb-[6px] text-[16px] leading-none font-bold"
|
||||||
>
|
>
|
||||||
{annuncio.titolo || "N/A"}
|
{annuncio.titolo || "N/A"}
|
||||||
</Heading>
|
</Heading>
|
||||||
<Text className="m-0 text-[14px] leading-[24px] text-gray-500">
|
<Text className="m-0 text-[14px] leading-[24px] text-gray-500">
|
||||||
<strong>Prezzo:</strong> €
|
<strong>Prezzo:</strong> €
|
||||||
{(annuncio.prezzo / 1e2).toFixed(2)}
|
{(annuncio.prezzo / 1e2).toFixed(2)}
|
||||||
</Text>
|
</Text>
|
||||||
<Link
|
<Link
|
||||||
href={`${process.env.NEXT_PUBLIC_BASE_URL}/annuncio/${annuncio.codice}`}
|
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"
|
||||||
>
|
>
|
||||||
Scheda dell'annuncio →
|
Scheda dell'annuncio →
|
||||||
</Link>
|
</Link>
|
||||||
</Row>
|
</Row>
|
||||||
</Section>
|
</Section>
|
||||||
</>
|
</>
|
||||||
))}
|
))}
|
||||||
</Section>
|
</Section>
|
||||||
</Section>
|
</Section>
|
||||||
</>
|
</>
|
||||||
</Base>
|
</Base>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default OnboardingServizio;
|
export default OnboardingServizio;
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,42 @@
|
||||||
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 PagamentoConferma_NewMail = {
|
export type PagamentoConferma_NewMail = {
|
||||||
mailType: "pagamentoConferma";
|
mailType: "pagamentoConferma";
|
||||||
props: PagamentoConfermaProps;
|
props: PagamentoConfermaProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PagamentoConfermaProps = {
|
type PagamentoConfermaProps = {
|
||||||
receiptHref: string;
|
receiptHref: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PagamentoConferma = ({ receiptHref }: PagamentoConfermaProps) => {
|
const PagamentoConferma = ({ receiptHref }: PagamentoConfermaProps) => {
|
||||||
return (
|
return (
|
||||||
<Base preview="Conferma Pagamento">
|
<Base preview="Conferma Pagamento">
|
||||||
<>
|
<>
|
||||||
<Heading className="text-center text-3xl leading-8">
|
<Heading className="text-center text-3xl leading-8">
|
||||||
Conferma Pagamento
|
Conferma 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 è stato confermato
|
Gentile Cliente, la informiamo che il pagamento è stato confermato
|
||||||
con successo. Procederemo ad attivare i servizi acquistati se non
|
con successo. Procederemo ad attivare i servizi acquistati se non
|
||||||
lo fossero già. Può controllare lo stato del suo account nella
|
lo fossero già. Può controllare lo stato del suo account nella
|
||||||
sezione dedicata.
|
sezione dedicata.
|
||||||
</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={receiptHref}
|
href={receiptHref}
|
||||||
>
|
>
|
||||||
Vai al tuo account
|
Vai al tuo account
|
||||||
</Button>
|
</Button>
|
||||||
</Section>
|
</Section>
|
||||||
</Section>
|
</Section>
|
||||||
</>
|
</>
|
||||||
</Base>
|
</Base>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PagamentoConferma;
|
export default PagamentoConferma;
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,39 @@
|
||||||
import { Heading, Row, Section, Text, Button } from "@react-email/components";
|
import { Button, Heading, Row, Section, Text } from "@react-email/components";
|
||||||
import Base from "./base";
|
import Base from "./base";
|
||||||
export type PwResetLink_NewMail = {
|
export type PwResetLink_NewMail = {
|
||||||
mailType: "pwResetLink";
|
mailType: "pwResetLink";
|
||||||
props: PwResetLinkProps;
|
props: PwResetLinkProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PwResetLinkProps = {
|
type PwResetLinkProps = {
|
||||||
resetlink: string;
|
resetlink: string;
|
||||||
};
|
};
|
||||||
const PwResetLink = ({ resetlink }: PwResetLinkProps) => {
|
const PwResetLink = ({ resetlink }: PwResetLinkProps) => {
|
||||||
return (
|
return (
|
||||||
<Base preview="Link Reset Password">
|
<Base preview="Link Reset Password">
|
||||||
<>
|
<>
|
||||||
<Heading className="text-center text-3xl leading-8">
|
<Heading className="text-center text-3xl leading-8">
|
||||||
Link Reset Password
|
Link Reset Password
|
||||||
</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>
|
||||||
Abbiamo ricevuto la tua richiesta di reset password, clicca sul
|
Abbiamo ricevuto la tua richiesta di reset password, clicca sul
|
||||||
link sottostante per procedere.
|
link sottostante per procedere.
|
||||||
</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={resetlink}
|
href={resetlink}
|
||||||
>
|
>
|
||||||
Clicca qui per il reset della password
|
Clicca qui per il reset della password
|
||||||
</Button>
|
</Button>
|
||||||
</Section>
|
</Section>
|
||||||
</Section>
|
</Section>
|
||||||
</>
|
</>
|
||||||
</Base>
|
</Base>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PwResetLink;
|
export default PwResetLink;
|
||||||
|
|
|
||||||
|
|
@ -1,152 +0,0 @@
|
||||||
import react from "eslint-plugin-react";
|
|
||||||
import typescriptEslint from "@typescript-eslint/eslint-plugin";
|
|
||||||
import pluginQuery from "@tanstack/eslint-plugin-query";
|
|
||||||
import kyselyRules from "./kyselyRules/plugin.js";
|
|
||||||
import tsParser from "@typescript-eslint/parser";
|
|
||||||
import path from "node:path";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
import js from "@eslint/js";
|
|
||||||
import noRelativeImportPaths from "eslint-plugin-no-relative-import-paths";
|
|
||||||
import { FlatCompat } from "@eslint/eslintrc";
|
|
||||||
import jsxA11y from "eslint-plugin-jsx-a11y";
|
|
||||||
import eslintPluginBetterTailwindcss from "eslint-plugin-better-tailwindcss";
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = path.dirname(__filename);
|
|
||||||
const compat = new FlatCompat({
|
|
||||||
baseDirectory: __dirname,
|
|
||||||
recommendedConfig: js.configs.recommended,
|
|
||||||
allConfig: js.configs.all,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default [
|
|
||||||
...pluginQuery.configs["flat/recommended"],
|
|
||||||
|
|
||||||
{
|
|
||||||
ignores: [
|
|
||||||
"**/.next",
|
|
||||||
"**/out",
|
|
||||||
"**/public",
|
|
||||||
"**/node_modules/",
|
|
||||||
"**/check-env-vars.js",
|
|
||||||
"**/headers/",
|
|
||||||
"**/schemas/",
|
|
||||||
"**/kyselyRules/",
|
|
||||||
"prettier.config.cjs",
|
|
||||||
"postcss.config.cjs",
|
|
||||||
".kanelrc.js",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
...compat.extends(
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:react/recommended",
|
|
||||||
"plugin:@next/next/recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended-type-checked",
|
|
||||||
"plugin:@typescript-eslint/stylistic-type-checked",
|
|
||||||
"plugin:jsx-a11y/recommended",
|
|
||||||
),
|
|
||||||
{
|
|
||||||
plugins: {
|
|
||||||
react,
|
|
||||||
"@typescript-eslint": typescriptEslint,
|
|
||||||
"better-tailwindcss": eslintPluginBetterTailwindcss,
|
|
||||||
"jsx-a11y": jsxA11y,
|
|
||||||
kyselyRules: kyselyRules,
|
|
||||||
//"no-relative-import-paths": noRelativeImportPaths,
|
|
||||||
},
|
|
||||||
|
|
||||||
languageOptions: {
|
|
||||||
parser: tsParser,
|
|
||||||
ecmaVersion: 5,
|
|
||||||
sourceType: "script",
|
|
||||||
|
|
||||||
parserOptions: {
|
|
||||||
project: true,
|
|
||||||
tsconfigRootDir: "./",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
linterOptions: {
|
|
||||||
reportUnusedDisableDirectives: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
settings: {
|
|
||||||
react: {
|
|
||||||
version: "detect",
|
|
||||||
},
|
|
||||||
|
|
||||||
"import/resolver": {
|
|
||||||
typescript: {
|
|
||||||
alwaysTryTypes: true,
|
|
||||||
project: "./tsconfig.json",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"better-tailwindcss": {
|
|
||||||
entryPoint: "src/styles/globals.css",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
rules: {
|
|
||||||
"react/react-in-jsx-scope": "off",
|
|
||||||
"react-hooks/exhaustive-deps": "off",
|
|
||||||
"react/prop-types": "off",
|
|
||||||
"@typescript-eslint/ban-types": "off",
|
|
||||||
"@typescript-eslint/ban-ts-comment": "off",
|
|
||||||
"@typescript-eslint/array-type": "off",
|
|
||||||
"@typescript-eslint/no-redundant-type-constituents": "warn",
|
|
||||||
"@typescript-eslint/consistent-type-definitions": "off",
|
|
||||||
"@typescript-eslint/prefer-optional-chain": "off",
|
|
||||||
"@typescript-eslint/prefer-nullish-coalescing": "off",
|
|
||||||
"@typescript-eslint/no-empty-interface": "off",
|
|
||||||
"@typescript-eslint/unbound-method": "off",
|
|
||||||
"@typescript-eslint/consistent-type-imports": [
|
|
||||||
"warn",
|
|
||||||
{
|
|
||||||
prefer: "type-imports",
|
|
||||||
fixStyle: "inline-type-imports",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
"@typescript-eslint/no-unused-vars": [
|
|
||||||
"warn",
|
|
||||||
{
|
|
||||||
argsIgnorePattern: "^_",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
"@typescript-eslint/require-await": "off",
|
|
||||||
"@typescript-eslint/switch-exhaustiveness-check": "warn",
|
|
||||||
|
|
||||||
"@typescript-eslint/no-misused-promises": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
checksVoidReturn: {
|
|
||||||
attributes: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
"@next/next/no-img-element": "warn",
|
|
||||||
"kyselyRules/enforce-select": "error",
|
|
||||||
"kyselyRules/enforce-where": "error",
|
|
||||||
"kyselyRules/enforce-null": "error",
|
|
||||||
...eslintPluginBetterTailwindcss.configs["recommended-warn"]?.rules,
|
|
||||||
...eslintPluginBetterTailwindcss.configs["recommended-error"]?.rules,
|
|
||||||
"better-tailwindcss/enforce-consistent-line-wrapping": "off",
|
|
||||||
"better-tailwindcss/enforce-consistent-class-order": "off",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
//ignores: ["**/emails/**/*.tsx", "next.config.js"],
|
|
||||||
files: ["src/**/*.tsx", "src/**/*.ts"],
|
|
||||||
plugins: {
|
|
||||||
"no-relative-import-paths": noRelativeImportPaths,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
"no-relative-import-paths/no-relative-import-paths": [
|
|
||||||
"warn",
|
|
||||||
{ rootDir: "src", prefix: "~" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
@ -1,57 +1,40 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://unpkg.com/knip@5/schema.json",
|
"$schema": "https://unpkg.com/knip@5/schema.json",
|
||||||
"project": [
|
"project": ["src/**", "**", "!src/schemas/public/*.ts", "!emails/**"],
|
||||||
"src/**",
|
"ignore": [
|
||||||
"**",
|
".kanelrc.js",
|
||||||
"!src/schemas/public/*.ts",
|
"TypeHelpers.type.ts",
|
||||||
"!emails/**"
|
"src/utils/api.ts",
|
||||||
],
|
"src/server/api/trpc.ts",
|
||||||
"ignore": [
|
"check-env-vars.js",
|
||||||
".kanelrc.js",
|
"src/components/ui/*",
|
||||||
".eslintrc.json",
|
"src/hooks/coordDistance.ts",
|
||||||
"TypeHelpers.type.ts",
|
"src/hooks/useMobile.tsx",
|
||||||
"src/utils/api.ts",
|
"src/lib/nossr.tsx",
|
||||||
"src/server/api/trpc.ts",
|
"src/lib/useIsTyping.ts",
|
||||||
"check-env-vars.js",
|
"src/middlewares/apis_middleware.ts",
|
||||||
"src/components/ui/*",
|
"src/server/auth.ts",
|
||||||
"src/hooks/coordDistance.ts",
|
"src/utils/get-media-url.ts",
|
||||||
"src/hooks/useMobile.tsx",
|
"src/utils/utils.ts"
|
||||||
"src/lib/nossr.tsx",
|
],
|
||||||
"src/lib/useIsTyping.ts",
|
"ignoreBinaries": ["stripe"],
|
||||||
"src/middlewares/apis_middleware.ts",
|
"ignoreDependencies": [
|
||||||
"src/server/auth.ts",
|
"kysely-plugin-serialize",
|
||||||
"src/utils/get-media-url.ts",
|
"sharp",
|
||||||
"src/utils/utils.ts"
|
"uuid",
|
||||||
],
|
"@react-email/components",
|
||||||
"ignoreBinaries": [
|
"@types/uuid",
|
||||||
"stripe"
|
"kanel-kysely",
|
||||||
],
|
"npm-run-all",
|
||||||
"ignoreDependencies": [
|
"@react-email/preview-server",
|
||||||
"kysely-plugin-serialize",
|
"@tailwindcss/forms",
|
||||||
"sharp",
|
"@tailwindcss/typography",
|
||||||
"uuid",
|
"tailwind-scrollbar",
|
||||||
"@react-email/components",
|
"tw-animate-css",
|
||||||
"@eslint/compat",
|
"@radix-ui/react-dialog",
|
||||||
"@next/eslint-plugin-next",
|
"@radix-ui/react-label",
|
||||||
"@prettier/sync",
|
"@radix-ui/react-slot",
|
||||||
"@types/eslint",
|
"@radix-ui/react-tooltip",
|
||||||
"@types/uuid",
|
"@radix-ui/react-popover"
|
||||||
"eslint-config-next",
|
]
|
||||||
"eslint-import-resolver-typescript",
|
}
|
||||||
"eslint-plugin-import",
|
|
||||||
"eslint-plugin-react-hooks",
|
|
||||||
"kanel-kysely",
|
|
||||||
"npm-run-all",
|
|
||||||
"typescript-eslint",
|
|
||||||
"@react-email/preview-server",
|
|
||||||
"@tailwindcss/forms",
|
|
||||||
"@tailwindcss/typography",
|
|
||||||
"tailwind-scrollbar",
|
|
||||||
"tw-animate-css",
|
|
||||||
"@radix-ui/react-dialog",
|
|
||||||
"@radix-ui/react-label",
|
|
||||||
"@radix-ui/react-slot",
|
|
||||||
"@radix-ui/react-tooltip",
|
|
||||||
"@radix-ui/react-popover"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -7,88 +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
|
output: env.NODE_ENV === "production" ? "standalone" : undefined, // This is for docker builds
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
compiler: {
|
compiler: {
|
||||||
removeConsole: false,
|
removeConsole: false,
|
||||||
},
|
},
|
||||||
i18n: {
|
i18n: {
|
||||||
locales: ["it", "en"],
|
locales: ["it", "en"],
|
||||||
defaultLocale: "it",
|
defaultLocale: "it",
|
||||||
},
|
},
|
||||||
images: {
|
images: {
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{
|
{
|
||||||
protocol: "https",
|
protocol: "https",
|
||||||
hostname: "*.googleusercontent.com",
|
hostname: "*.googleusercontent.com",
|
||||||
port: "",
|
port: "",
|
||||||
pathname: "**",
|
pathname: "**",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
hostname: env.BACKENDSERVER_URL || "localhost:1323",
|
hostname: env.BACKENDSERVER_URL || "localhost:1323",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
minimumCacheTTL: 86400, // 1 day in seconds
|
minimumCacheTTL: 86400, // 1 day in seconds
|
||||||
},
|
},
|
||||||
|
|
||||||
typescript: {
|
typescript: {
|
||||||
ignoreBuildErrors: true,
|
ignoreBuildErrors: true,
|
||||||
},
|
},
|
||||||
eslint: {
|
|
||||||
ignoreDuringBuilds: true,
|
devIndicators: false,
|
||||||
},
|
rewrites: async () => {
|
||||||
devIndicators: false,
|
return [
|
||||||
rewrites: async () => {
|
{
|
||||||
return [
|
source: "/api/panel",
|
||||||
{
|
destination: env.NODE_ENV === "production" ? "/404" : "/api/panel",
|
||||||
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/images/get/:slug*",
|
},
|
||||||
destination: env.BACKENDSERVER_URL + "/images/get/:slug*",
|
{
|
||||||
},
|
source: "/go-api/storage/get/:slug*",
|
||||||
{
|
destination: `${env.BACKENDSERVER_URL}/storage/get/:slug*`,
|
||||||
source: "/go-api/storage/get/:slug*",
|
has: [
|
||||||
destination: env.BACKENDSERVER_URL + "/storage/get/:slug*",
|
{ type: "cookie", key: "access_token" },
|
||||||
has: [
|
{
|
||||||
{ type: "cookie", key: "access_token" },
|
type: "query",
|
||||||
{
|
key: "token",
|
||||||
type: "query",
|
value:
|
||||||
key: "token",
|
"(?<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}$)",
|
||||||
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*`,
|
||||||
source: "/go-api/storage/upload:slug*",
|
has: [
|
||||||
destination: env.BACKENDSERVER_URL + "/storage/upload:slug*",
|
{ type: "cookie", key: "access_token" },
|
||||||
has: [
|
{
|
||||||
{ type: "cookie", key: "access_token" },
|
type: "query",
|
||||||
{
|
key: "token",
|
||||||
type: "query",
|
value:
|
||||||
key: "token",
|
"(?<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}$)",
|
||||||
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 () => {
|
||||||
},
|
return [
|
||||||
headers: async () => {
|
{
|
||||||
return [
|
source: "/:path*",
|
||||||
{
|
headers: [
|
||||||
source: "/:path*",
|
{
|
||||||
headers: [
|
key: "X-Frame-Options",
|
||||||
{
|
value: "",
|
||||||
key: "X-Frame-Options",
|
},
|
||||||
value: "",
|
],
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
},
|
},
|
||||||
];
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|
|
||||||
40195
apps/infoalloggi/package-lock.json
generated
40195
apps/infoalloggi/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -101,17 +101,9 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.2.2",
|
"@biomejs/biome": "2.2.2",
|
||||||
"@eslint/compat": "^1.3.1",
|
|
||||||
"@eslint/eslintrc": "^3.3.0",
|
|
||||||
"@eslint/js": "^9.32.0",
|
|
||||||
"@hookform/devtools": "^4.4.0",
|
"@hookform/devtools": "^4.4.0",
|
||||||
"@next/eslint-plugin-next": "^15.4.5",
|
|
||||||
"@prettier/sync": "^0.6.1",
|
|
||||||
"@react-email/preview-server": "^4.2.8",
|
"@react-email/preview-server": "^4.2.8",
|
||||||
"@tanstack/eslint-plugin-query": "^5.83.1",
|
|
||||||
"@total-typescript/ts-reset": "^0.6.1",
|
"@total-typescript/ts-reset": "^0.6.1",
|
||||||
"@types/eslint": "^9.6.1",
|
|
||||||
"@types/eslint-plugin-jsx-a11y": "^6.10.0",
|
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/leaflet": "^1.9.20",
|
"@types/leaflet": "^1.9.20",
|
||||||
"@types/node": "^24.2.0",
|
"@types/node": "^24.2.0",
|
||||||
|
|
@ -122,27 +114,13 @@
|
||||||
"@types/react-dom": "^19.1.7",
|
"@types/react-dom": "^19.1.7",
|
||||||
"@types/react-is": "^19.0.0",
|
"@types/react-is": "^19.0.0",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.39.0",
|
|
||||||
"@typescript-eslint/parser": "^8.39.0",
|
|
||||||
"cross-env": "^10.0.0",
|
"cross-env": "^10.0.0",
|
||||||
"eslint": "^9.32.0",
|
|
||||||
"eslint-config-next": "^15.4.5",
|
|
||||||
"eslint-import-resolver-typescript": "^4.4.4",
|
|
||||||
"eslint-plugin-better-tailwindcss": "^3.7.4",
|
|
||||||
"eslint-plugin-import": "^2.32.0",
|
|
||||||
"eslint-plugin-no-relative-import-paths": "^1.6.1",
|
|
||||||
"eslint-plugin-react": "^7.37.5",
|
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
|
||||||
"jiti": "^2.5.1",
|
"jiti": "^2.5.1",
|
||||||
"kanel": "^3.14.2",
|
"kanel": "^3.14.2",
|
||||||
"kanel-kysely": "^0.7.1",
|
"kanel-kysely": "^0.7.1",
|
||||||
"knip": "^5.62.0",
|
"knip": "^5.62.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^3.6.2",
|
"typescript": "^5.9.2"
|
||||||
"prettier-plugin-classnames": "^0.8.2",
|
|
||||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
|
||||||
"typescript": "^5.9.2",
|
|
||||||
"typescript-eslint": "^8.39.0"
|
|
||||||
},
|
},
|
||||||
"ct3aMetadata": {
|
"ct3aMetadata": {
|
||||||
"initVersion": "7.9.0"
|
"initVersion": "7.9.0"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* eslint-env node */
|
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
plugins: {
|
plugins: {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
/* eslint-env node */
|
|
||||||
/** @type {import("prettier").Config & import('prettier-plugin-tailwindcss').PluginOptions} */
|
|
||||||
const config = {
|
|
||||||
printWidth: 80,
|
|
||||||
plugins: ["prettier-plugin-classnames", "prettier-plugin-tailwindcss"],
|
|
||||||
customAttributes: ["className"],
|
|
||||||
customFunctions: ["classNames"],
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = config;
|
|
||||||
|
|
@ -1,22 +1,22 @@
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import Annunci from "~/_app/ui/annunci";
|
|
||||||
import { getDataFromDB } from "~/_app/lib/annunci";
|
import { getDataFromDB } from "~/_app/lib/annunci";
|
||||||
|
import AnnunciList from "~/_app/ui/annunci";
|
||||||
import { LoadingPage } from "~/components/loading";
|
import { LoadingPage } from "~/components/loading";
|
||||||
|
|
||||||
export default function TestAppPage() {
|
export default function TestAppPage() {
|
||||||
const annunci = getDataFromDB();
|
const annunci = getDataFromDB();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8">
|
<div className="p-8">
|
||||||
<h1 className="mb-4 text-2xl font-bold">Test App Page</h1>
|
<h1 className="mb-4 text-2xl font-bold">Test App Page</h1>
|
||||||
|
|
||||||
<div className="rounded-lg border bg-gray-50 p-4">
|
<div className="rounded-lg border bg-gray-50 p-4">
|
||||||
<h2 className="mb-2 text-lg font-semibold">Data from Database:</h2>
|
<h2 className="mb-2 text-lg font-semibold">Data from Database:</h2>
|
||||||
|
|
||||||
<Suspense fallback={<LoadingPage />}>
|
<Suspense fallback={<LoadingPage />}>
|
||||||
<Annunci annunci={annunci} />
|
<AnnunciList annunci={annunci} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,17 @@ import { use } from "react";
|
||||||
import type { Annunci } from "~/schemas/public/Annunci";
|
import type { Annunci } from "~/schemas/public/Annunci";
|
||||||
|
|
||||||
type AnnunciProps = {
|
type AnnunciProps = {
|
||||||
annunci: Promise<Pick<Annunci, "codice">[]>;
|
annunci: Promise<Pick<Annunci, "codice">[]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Annunci({ annunci }: AnnunciProps) {
|
export default function AnnunciList({ annunci }: AnnunciProps) {
|
||||||
const allAnnunci = use(annunci);
|
const allAnnunci = use(annunci);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul>
|
<ul>
|
||||||
{allAnnunci.map((ann, i) => (
|
{allAnnunci.map((ann, i) => (
|
||||||
<li key={i}>{ann.codice}</li>
|
<li key={i}>{ann.codice}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,238 +1,238 @@
|
||||||
import {
|
import {
|
||||||
Accessibility,
|
Accessibility,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
Baby,
|
Baby,
|
||||||
BellRing,
|
BellRing,
|
||||||
Building,
|
Building,
|
||||||
Building2,
|
Building2,
|
||||||
CircleDot,
|
CircleDot,
|
||||||
CircleParking,
|
CircleParking,
|
||||||
CirclePlus,
|
CirclePlus,
|
||||||
CircleX,
|
CircleX,
|
||||||
Cog,
|
Cog,
|
||||||
Contact,
|
Contact,
|
||||||
Cross,
|
Cross,
|
||||||
Dot,
|
Dot,
|
||||||
Euro,
|
Euro,
|
||||||
FerrisWheel,
|
FerrisWheel,
|
||||||
Film,
|
Film,
|
||||||
GraduationCap,
|
GraduationCap,
|
||||||
Heart,
|
Heart,
|
||||||
Heater,
|
Heater,
|
||||||
House,
|
House,
|
||||||
Info,
|
Info,
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
Mail,
|
type LucideProps,
|
||||||
MessageSquareText,
|
Mail,
|
||||||
MessagesSquare,
|
MessageSquareText,
|
||||||
Milestone,
|
MessagesSquare,
|
||||||
Paperclip,
|
Milestone,
|
||||||
Pointer,
|
Paperclip,
|
||||||
Search,
|
Pointer,
|
||||||
ShoppingCart,
|
Search,
|
||||||
SlidersHorizontal,
|
ShoppingCart,
|
||||||
Sofa,
|
SlidersHorizontal,
|
||||||
Star,
|
Sofa,
|
||||||
Target,
|
Star,
|
||||||
ThermometerSun,
|
Target,
|
||||||
ThumbsUp,
|
ThermometerSun,
|
||||||
Trees,
|
ThumbsUp,
|
||||||
TriangleAlert,
|
Trees,
|
||||||
User,
|
TriangleAlert,
|
||||||
Users,
|
User,
|
||||||
Wifi,
|
Users,
|
||||||
type LucideProps,
|
Wifi,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "~/components/ui/tooltip";
|
|
||||||
import toast from "react-hot-toast";
|
|
||||||
import type {
|
import type {
|
||||||
ComponentPropsWithoutRef,
|
ComponentPropsWithoutRef,
|
||||||
ForwardRefExoticComponent,
|
ForwardRefExoticComponent,
|
||||||
RefAttributes,
|
RefAttributes,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "~/components/ui/tooltip";
|
||||||
|
|
||||||
const iconOptions = [
|
const iconOptions = [
|
||||||
"heater",
|
"heater",
|
||||||
"error",
|
"error",
|
||||||
"warning",
|
"warning",
|
||||||
"sofa",
|
"sofa",
|
||||||
"building",
|
"building",
|
||||||
"building2",
|
"building2",
|
||||||
"wifi",
|
"wifi",
|
||||||
"accessibility",
|
"accessibility",
|
||||||
"baby",
|
"baby",
|
||||||
"thermometer-sun",
|
"thermometer-sun",
|
||||||
"circle-plus",
|
"circle-plus",
|
||||||
"circle-dot",
|
"circle-dot",
|
||||||
"dot",
|
"dot",
|
||||||
"cog",
|
"cog",
|
||||||
"check",
|
"check",
|
||||||
"search",
|
"search",
|
||||||
"contact",
|
"contact",
|
||||||
"euro",
|
"euro",
|
||||||
"dashboard",
|
"dashboard",
|
||||||
"settings",
|
"settings",
|
||||||
"user",
|
"user",
|
||||||
"users",
|
"users",
|
||||||
"messages",
|
"messages",
|
||||||
"message",
|
"message",
|
||||||
"arrowR",
|
"arrowR",
|
||||||
"arrowL",
|
"arrowL",
|
||||||
"attachment",
|
"attachment",
|
||||||
"cart",
|
"cart",
|
||||||
"star",
|
"star",
|
||||||
"house",
|
"house",
|
||||||
"heart",
|
"heart",
|
||||||
"info",
|
"info",
|
||||||
"mail",
|
"mail",
|
||||||
"school",
|
"school",
|
||||||
"cross",
|
"cross",
|
||||||
"park",
|
"park",
|
||||||
"parking",
|
"parking",
|
||||||
"film",
|
"film",
|
||||||
"milestone",
|
"milestone",
|
||||||
"entertainment",
|
"entertainment",
|
||||||
"circle-help",
|
"circle-help",
|
||||||
"bell",
|
"bell",
|
||||||
"target",
|
"target",
|
||||||
"thumbs-up",
|
"thumbs-up",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type IconType = (typeof iconOptions)[number];
|
export type IconType = (typeof iconOptions)[number];
|
||||||
type LucidePropsType = ForwardRefExoticComponent<
|
type LucidePropsType = ForwardRefExoticComponent<
|
||||||
Omit<LucideProps, "ref"> & RefAttributes<SVGSVGElement>
|
Omit<LucideProps, "ref"> & RefAttributes<SVGSVGElement>
|
||||||
>;
|
>;
|
||||||
interface IconMatrixProps extends ComponentPropsWithoutRef<LucidePropsType> {
|
interface IconMatrixProps extends ComponentPropsWithoutRef<LucidePropsType> {
|
||||||
type: IconType;
|
type: IconType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IconMatrix = ({ type, ...rest }: IconMatrixProps) => {
|
export const IconMatrix = ({ type, ...rest }: IconMatrixProps) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "heater":
|
case "heater":
|
||||||
return <Heater {...rest} />;
|
return <Heater {...rest} />;
|
||||||
case "error":
|
case "error":
|
||||||
return <CircleX {...rest} />;
|
return <CircleX {...rest} />;
|
||||||
case "warning":
|
case "warning":
|
||||||
return <TriangleAlert {...rest} />;
|
return <TriangleAlert {...rest} />;
|
||||||
case "sofa":
|
case "sofa":
|
||||||
return <Sofa {...rest} />;
|
return <Sofa {...rest} />;
|
||||||
case "building":
|
case "building":
|
||||||
return <Building {...rest} />;
|
return <Building {...rest} />;
|
||||||
case "building2":
|
case "building2":
|
||||||
return <Building2 {...rest} />;
|
return <Building2 {...rest} />;
|
||||||
case "wifi":
|
case "wifi":
|
||||||
return <Wifi {...rest} />;
|
return <Wifi {...rest} />;
|
||||||
case "accessibility":
|
case "accessibility":
|
||||||
return <Accessibility {...rest} />;
|
return <Accessibility {...rest} />;
|
||||||
case "baby":
|
case "baby":
|
||||||
return <Baby {...rest} />;
|
return <Baby {...rest} />;
|
||||||
case "thermometer-sun":
|
case "thermometer-sun":
|
||||||
return <ThermometerSun {...rest} />;
|
return <ThermometerSun {...rest} />;
|
||||||
case "circle-plus":
|
case "circle-plus":
|
||||||
return <CirclePlus {...rest} />;
|
return <CirclePlus {...rest} />;
|
||||||
case "circle-dot":
|
case "circle-dot":
|
||||||
return <CircleDot {...rest} />;
|
return <CircleDot {...rest} />;
|
||||||
case "dot":
|
case "dot":
|
||||||
return <Dot {...rest} />;
|
return <Dot {...rest} />;
|
||||||
case "cog":
|
case "cog":
|
||||||
return <Cog {...rest} />;
|
return <Cog {...rest} />;
|
||||||
case "check":
|
case "check":
|
||||||
return <CheckCircleIcon {...rest} />;
|
return <CheckCircleIcon {...rest} />;
|
||||||
case "search":
|
case "search":
|
||||||
return <Search {...rest} />;
|
return <Search {...rest} />;
|
||||||
case "contact":
|
case "contact":
|
||||||
return <Contact {...rest} />;
|
return <Contact {...rest} />;
|
||||||
case "euro":
|
case "euro":
|
||||||
return <Euro {...rest} />;
|
return <Euro {...rest} />;
|
||||||
case "dashboard":
|
case "dashboard":
|
||||||
return <LayoutDashboard {...rest} />;
|
return <LayoutDashboard {...rest} />;
|
||||||
case "settings":
|
case "settings":
|
||||||
return <SlidersHorizontal {...rest} />;
|
return <SlidersHorizontal {...rest} />;
|
||||||
case "user":
|
case "user":
|
||||||
return <User {...rest} />;
|
return <User {...rest} />;
|
||||||
case "users":
|
case "users":
|
||||||
return <Users {...rest} />;
|
return <Users {...rest} />;
|
||||||
case "messages":
|
case "messages":
|
||||||
return <MessagesSquare {...rest} />;
|
return <MessagesSquare {...rest} />;
|
||||||
case "message":
|
case "message":
|
||||||
return <MessageSquareText {...rest} />;
|
return <MessageSquareText {...rest} />;
|
||||||
case "arrowR":
|
case "arrowR":
|
||||||
return <ArrowRight {...rest} />;
|
return <ArrowRight {...rest} />;
|
||||||
case "arrowL":
|
case "arrowL":
|
||||||
return <ArrowLeft {...rest} />;
|
return <ArrowLeft {...rest} />;
|
||||||
case "attachment":
|
case "attachment":
|
||||||
return <Paperclip {...rest} />;
|
return <Paperclip {...rest} />;
|
||||||
case "cart":
|
case "cart":
|
||||||
return <ShoppingCart {...rest} />;
|
return <ShoppingCart {...rest} />;
|
||||||
case "star":
|
case "star":
|
||||||
return <Star {...rest} />;
|
return <Star {...rest} />;
|
||||||
case "house":
|
case "house":
|
||||||
return <House {...rest} />;
|
return <House {...rest} />;
|
||||||
case "heart":
|
case "heart":
|
||||||
return <Heart {...rest} />;
|
return <Heart {...rest} />;
|
||||||
case "info":
|
case "info":
|
||||||
return <Info {...rest} />;
|
return <Info {...rest} />;
|
||||||
case "mail":
|
case "mail":
|
||||||
return <Mail {...rest} />;
|
return <Mail {...rest} />;
|
||||||
case "school":
|
case "school":
|
||||||
return <GraduationCap {...rest} />;
|
return <GraduationCap {...rest} />;
|
||||||
case "cross":
|
case "cross":
|
||||||
return <Cross {...rest} />;
|
return <Cross {...rest} />;
|
||||||
case "park":
|
case "park":
|
||||||
return <Trees {...rest} />;
|
return <Trees {...rest} />;
|
||||||
case "parking":
|
case "parking":
|
||||||
return <CircleParking {...rest} />;
|
return <CircleParking {...rest} />;
|
||||||
case "film":
|
case "film":
|
||||||
return <Film {...rest} />;
|
return <Film {...rest} />;
|
||||||
case "milestone":
|
case "milestone":
|
||||||
return <Milestone {...rest} />;
|
return <Milestone {...rest} />;
|
||||||
case "entertainment":
|
case "entertainment":
|
||||||
return <FerrisWheel {...rest} />;
|
return <FerrisWheel {...rest} />;
|
||||||
case "circle-help":
|
case "circle-help":
|
||||||
return <QuestionMarkCircleIcon {...rest} />;
|
return <QuestionMarkCircleIcon {...rest} />;
|
||||||
case "bell":
|
case "bell":
|
||||||
return <BellRing {...rest} />;
|
return <BellRing {...rest} />;
|
||||||
case "target":
|
case "target":
|
||||||
return <Target {...rest} />;
|
return <Target {...rest} />;
|
||||||
case "thumbs-up":
|
case "thumbs-up":
|
||||||
return <ThumbsUp {...rest} />;
|
return <ThumbsUp {...rest} />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IconMatrixTooltip = () => {
|
export const IconMatrixTooltip = () => {
|
||||||
return (
|
return (
|
||||||
<TooltipProvider delayDuration={300}>
|
<TooltipProvider delayDuration={300}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger className="flex flex-row gap-1" type="button">
|
<TooltipTrigger className="flex flex-row gap-1" type="button">
|
||||||
Lista Icone <Pointer className="size-4" />
|
Lista Icone <Pointer className="size-4" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<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"
|
type="button"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await navigator.clipboard.writeText(icon);
|
await navigator.clipboard.writeText(icon);
|
||||||
toast("Copiato", { icon: "📋" });
|
toast("Copiato", { icon: "📋" });
|
||||||
}}
|
}}
|
||||||
key={icon}
|
key={icon}
|
||||||
className="bg-muted flex flex-col items-center rounded-xl p-2 hover:opacity-75"
|
className="bg-muted flex flex-col items-center rounded-xl p-2 hover:opacity-75"
|
||||||
>
|
>
|
||||||
<IconMatrix type={icon} className="size-6" />
|
<IconMatrix type={icon} className="size-6" />
|
||||||
<span className="text-sm">{icon}</span>
|
<span className="text-sm">{icon}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/*
|
/*
|
||||||
type IRP_readOnly = {
|
type IRP_readOnly = {
|
||||||
|
|
@ -305,70 +305,74 @@ export const IconRatingComonent = ({
|
||||||
};
|
};
|
||||||
*/
|
*/
|
||||||
export const ShieldExclamationIcon = ({
|
export const ShieldExclamationIcon = ({
|
||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
}) => (
|
}) => (
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
className={className}
|
className={className}
|
||||||
>
|
>
|
||||||
<path
|
<title>Shield</title>
|
||||||
fillRule="evenodd"
|
<path
|
||||||
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"
|
||||||
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"
|
||||||
/>
|
clipRule="evenodd"
|
||||||
</svg>
|
/>
|
||||||
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const QuestionMarkCircleIcon = ({
|
export const QuestionMarkCircleIcon = ({
|
||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
}) => (
|
}) => (
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
className={className}
|
className={className}
|
||||||
>
|
>
|
||||||
<path
|
<title>QuestionMark</title>
|
||||||
fillRule="evenodd"
|
<path
|
||||||
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"
|
||||||
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"
|
||||||
/>
|
clipRule="evenodd"
|
||||||
</svg>
|
/>
|
||||||
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const HomeIcon = ({ className }: { className?: string }) => (
|
export const HomeIcon = ({ className }: { className?: string }) => (
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
strokeWidth={1.5}
|
strokeWidth={1.5}
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
className={className}
|
className={className}
|
||||||
>
|
>
|
||||||
<path
|
<title>Home</title>
|
||||||
strokeLinecap="round"
|
<path
|
||||||
strokeLinejoin="round"
|
strokeLinecap="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"
|
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"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
className={className}
|
className={className}
|
||||||
>
|
>
|
||||||
<path
|
<title>CheckCircle</title>
|
||||||
fillRule="evenodd"
|
<path
|
||||||
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"
|
||||||
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"
|
||||||
/>
|
clipRule="evenodd"
|
||||||
</svg>
|
/>
|
||||||
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { type ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
|
||||||
const fallbackImage = "/fallback-image.png";
|
const fallbackImage = "/fallback-image.png";
|
||||||
export const ImageFlbk = (props: ComponentProps<typeof Image>) => {
|
export const ImageFlbk = (props: ComponentProps<typeof Image>) => {
|
||||||
return (
|
return (
|
||||||
<Image
|
<Image
|
||||||
{...props}
|
{...props}
|
||||||
onLoad={(e) => {
|
onLoad={(e) => {
|
||||||
if (e.currentTarget.naturalWidth === 0) {
|
if (e.currentTarget.naturalWidth === 0) {
|
||||||
e.currentTarget.src = fallbackImage;
|
e.currentTarget.src = fallbackImage;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
e.currentTarget.src = fallbackImage;
|
e.currentTarget.src = fallbackImage;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
import {
|
|
||||||
HybridTooltip,
|
|
||||||
HybridTooltipContent,
|
|
||||||
HybridTooltipTrigger,
|
|
||||||
} from "~/components/custom_ui/HybridTooltip";
|
|
||||||
import { TooltipProvider } from "~/components/ui/tooltip";
|
|
||||||
import { IconMatrix, type IconType } from "~/components/IconComponents";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { ExternalLink } from "lucide-react";
|
import { ExternalLink } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
import {
|
||||||
|
HybridTooltip,
|
||||||
|
HybridTooltipContent,
|
||||||
|
HybridTooltipTrigger,
|
||||||
|
} from "~/components/custom_ui/HybridTooltip";
|
||||||
|
import { IconMatrix, type IconType } from "~/components/IconComponents";
|
||||||
|
import { TooltipProvider } from "~/components/ui/tooltip";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
<TouchProvider>
|
<TouchProvider>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|
@ -19,70 +20,70 @@ import type { ReactNode } from "react";
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type ChildrenBubbleProps = {
|
type ChildrenBubbleProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
href?: undefined;
|
href?: undefined;
|
||||||
txt?: undefined;
|
txt?: undefined;
|
||||||
target?: undefined;
|
target?: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
type LinkBubbleProps = {
|
type LinkBubbleProps = {
|
||||||
children?: undefined;
|
children?: undefined;
|
||||||
href: string;
|
href: string;
|
||||||
txt: string;
|
txt: string;
|
||||||
target?: "_self" | "_blank" | "_parent" | "_top";
|
target?: "_self" | "_blank" | "_parent" | "_top";
|
||||||
};
|
};
|
||||||
type InformationBubbleProps = {
|
type InformationBubbleProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
iconClassName?: string;
|
iconClassName?: string;
|
||||||
icon?: IconType;
|
icon?: IconType;
|
||||||
side?: "top" | "bottom" | "left" | "right";
|
side?: "top" | "bottom" | "left" | "right";
|
||||||
} & (ChildrenBubbleProps | LinkBubbleProps);
|
} & (ChildrenBubbleProps | LinkBubbleProps);
|
||||||
|
|
||||||
const InformationBubble = ({
|
const InformationBubble = ({
|
||||||
className,
|
className,
|
||||||
icon = "circle-help",
|
icon = "circle-help",
|
||||||
iconClassName,
|
iconClassName,
|
||||||
side,
|
side,
|
||||||
children,
|
children,
|
||||||
href,
|
href,
|
||||||
txt,
|
txt,
|
||||||
target,
|
target,
|
||||||
}: InformationBubbleProps) => {
|
}: InformationBubbleProps) => {
|
||||||
return (
|
return (
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<HybridTooltip>
|
<HybridTooltip>
|
||||||
<HybridTooltipTrigger
|
<HybridTooltipTrigger
|
||||||
className={cn(
|
className={cn(
|
||||||
`absolute -top-2 -right-2 flex size-6 items-center justify-center rounded-full bg-white`,
|
`absolute -top-2 -right-2 flex size-6 items-center justify-center rounded-full bg-white`,
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<IconMatrix
|
<IconMatrix
|
||||||
type={icon}
|
type={icon}
|
||||||
className={cn("size-6 text-indigo-500", iconClassName)}
|
className={cn("size-6 text-indigo-500", iconClassName)}
|
||||||
/>
|
/>
|
||||||
</HybridTooltipTrigger>
|
</HybridTooltipTrigger>
|
||||||
|
|
||||||
<HybridTooltipContent
|
<HybridTooltipContent
|
||||||
side={side}
|
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"
|
||||||
>
|
>
|
||||||
{href ? (
|
{href ? (
|
||||||
<Link
|
<Link
|
||||||
href={href}
|
href={href}
|
||||||
target={target}
|
target={target}
|
||||||
className="flex items-center justify-center text-sm text-blue-500 underline-offset-2 hover:underline"
|
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>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<>{children}</>
|
<>{children}</>
|
||||||
)}
|
)}
|
||||||
</HybridTooltipContent>
|
</HybridTooltipContent>
|
||||||
</HybridTooltip>
|
</HybridTooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default InformationBubble;
|
export default InformationBubble;
|
||||||
|
|
|
||||||
|
|
@ -1,297 +1,297 @@
|
||||||
import { useContext, type ReactNode } from "react";
|
import {
|
||||||
import { BannerFactory } from "~/components/banners";
|
ExternalLink,
|
||||||
import { Footer, MiniFooter } from "~/components/footer";
|
Mail,
|
||||||
import { SiteHeader } from "~/components/navbar/site-header";
|
MessagesSquare,
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
Paperclip,
|
||||||
import { UserViewContext, UserViewProvider } from "~/lib/userViewContext";
|
Search,
|
||||||
|
ShoppingBag,
|
||||||
|
Tickets,
|
||||||
|
UserCog,
|
||||||
|
} from "lucide-react";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {
|
import { usePathname } from "next/navigation";
|
||||||
ExternalLink,
|
import { useRouter } from "next/router";
|
||||||
Mail,
|
import { type ReactNode, useContext } from "react";
|
||||||
MessagesSquare,
|
import toast from "react-hot-toast";
|
||||||
Paperclip,
|
import { Sidebar } from "~/components/area-riservata/sidebar";
|
||||||
Search,
|
import { BannerFactory } from "~/components/banners";
|
||||||
ShoppingBag,
|
import { Footer, MiniFooter } from "~/components/footer";
|
||||||
Tickets,
|
import { LoadingPage } from "~/components/loading";
|
||||||
UserCog,
|
import { SiteHeader } from "~/components/navbar/site-header";
|
||||||
} from "lucide-react";
|
import { WhatsAppIcon } from "~/components/svgs";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Separator } from "~/components/ui/separator";
|
import { Separator } from "~/components/ui/separator";
|
||||||
import { usePathname } from "next/navigation";
|
import { UserViewContext, UserViewProvider } from "~/lib/userViewContext";
|
||||||
import { api } from "~/utils/api";
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
import { Sidebar } from "~/components/area-riservata/sidebar";
|
|
||||||
import type { UsersId } from "~/schemas/public/Users";
|
|
||||||
import toast from "react-hot-toast";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import {
|
import {
|
||||||
useSession,
|
EnforcedSessionContext,
|
||||||
EnforcedSessionContext,
|
useSession,
|
||||||
} from "~/providers/SessionProvider";
|
} from "~/providers/SessionProvider";
|
||||||
import { WhatsAppIcon } from "~/components/svgs";
|
import type { UsersId } from "~/schemas/public/Users";
|
||||||
import { LoadingPage } from "~/components/loading";
|
|
||||||
import type { ValidSession } from "~/server/api/trpc";
|
import type { ValidSession } from "~/server/api/trpc";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
noFooter?: boolean;
|
noFooter?: boolean;
|
||||||
}
|
}
|
||||||
export const Layout = ({ children, noFooter }: Props) => {
|
export const Layout = ({ children, noFooter }: Props) => {
|
||||||
const { data: bannerData } = api.settings.getBannerData.useQuery({
|
const { data: bannerData } = api.settings.getBannerData.useQuery({
|
||||||
area_riservata: false,
|
area_riservata: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen w-full flex-col">
|
<div className="flex min-h-screen w-full flex-col">
|
||||||
<SiteHeader />
|
<SiteHeader />
|
||||||
{bannerData?.map((banner) => (
|
{bannerData?.map((banner) => (
|
||||||
<div key={`banner-elem-${banner.idbanner}`}>
|
<div key={`banner-elem-${banner.idbanner}`}>
|
||||||
{BannerFactory(banner)}
|
{BannerFactory(banner)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<main className="h-full flex-1 grow">{children}</main>
|
<main className="h-full flex-1 grow">{children}</main>
|
||||||
|
|
||||||
{!noFooter && <Footer />}
|
{!noFooter && <Footer />}
|
||||||
{/* <CookieConsent /> */}
|
{/* <CookieConsent /> */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AreaRiservataLayout = ({
|
export const AreaRiservataLayout = ({
|
||||||
children,
|
children,
|
||||||
noSidebar,
|
noSidebar,
|
||||||
noFooter,
|
noFooter,
|
||||||
ignoreSessionCheck,
|
ignoreSessionCheck,
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
noSidebar?: boolean;
|
noSidebar?: boolean;
|
||||||
noFooter?: boolean;
|
noFooter?: boolean;
|
||||||
ignoreSessionCheck?: boolean;
|
ignoreSessionCheck?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { data: bannerData } = api.settings.getBannerData.useQuery({
|
const { data: bannerData } = api.settings.getBannerData.useQuery({
|
||||||
area_riservata: true,
|
area_riservata: true,
|
||||||
});
|
});
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
if (!ignoreSessionCheck) {
|
if (!ignoreSessionCheck) {
|
||||||
if (session.status === "pending")
|
if (session.status === "pending")
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-screen flex-col text-clip">
|
<div className="flex h-full min-h-screen flex-col text-clip">
|
||||||
<SiteHeader />
|
<SiteHeader />
|
||||||
|
|
||||||
<main className="flex h-full flex-1 overflow-auto">
|
<main className="flex h-full flex-1 overflow-auto">
|
||||||
<div className="flex h-auto w-full flex-col md:flex-row">
|
<div className="flex h-auto w-full flex-col md:flex-row">
|
||||||
<LoadingPage />;
|
<LoadingPage />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
{!noFooter && <MiniFooter />}
|
{!noFooter && <MiniFooter />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
if (session.status !== "success" || !session.user) {
|
if (session.status !== "success" || !session.user) {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-screen flex-col text-clip">
|
<div className="flex h-full min-h-screen flex-col text-clip">
|
||||||
<SiteHeader />
|
<SiteHeader />
|
||||||
|
|
||||||
<main className="flex h-full flex-1 overflow-auto">
|
<main className="flex h-full flex-1 overflow-auto">
|
||||||
<div className="flex h-auto w-full flex-col md:flex-row">
|
<div className="flex h-auto w-full flex-col md:flex-row">
|
||||||
<LoadingPage />;
|
<LoadingPage />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
{!noFooter && <MiniFooter />}
|
{!noFooter && <MiniFooter />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-screen flex-col text-clip">
|
<div className="flex h-full min-h-screen flex-col text-clip">
|
||||||
<SiteHeader />
|
<SiteHeader />
|
||||||
{bannerData?.map((banner) => (
|
{bannerData?.map((banner) => (
|
||||||
<div key={`banner-elem-${banner.idbanner}`}>
|
<div key={`banner-elem-${banner.idbanner}`}>
|
||||||
{BannerFactory(banner)}
|
{BannerFactory(banner)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<main className="flex h-full flex-1 overflow-auto">
|
<main className="flex h-full flex-1 overflow-auto">
|
||||||
<div className="flex h-auto w-full flex-col md:flex-row">
|
<div className="flex h-auto w-full flex-col md:flex-row">
|
||||||
{noSidebar ? null : (
|
{noSidebar ? null : (
|
||||||
<Sidebar isAdmin={session.user?.isAdmin || false} />
|
<Sidebar isAdmin={session.user?.isAdmin || false} />
|
||||||
)}
|
)}
|
||||||
<EnforcedSessionContext.Provider value={session as ValidSession}>
|
<EnforcedSessionContext.Provider value={session as ValidSession}>
|
||||||
{children}
|
{children}
|
||||||
</EnforcedSessionContext.Provider>
|
</EnforcedSessionContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
{!noFooter && <MiniFooter />}
|
{!noFooter && <MiniFooter />}
|
||||||
|
|
||||||
{/* <CookieConsent /> */}
|
{/* <CookieConsent /> */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AreaRiservataLayoutUserView = ({
|
export const AreaRiservataLayoutUserView = ({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode & { props: { userId: UsersId } };
|
children: ReactNode & { props: { userId: UsersId } };
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const props = children.props as { userId: UsersId };
|
const props = children.props as { userId: UsersId };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-screen flex-col bg-white">
|
<div className="flex h-full min-h-screen flex-col bg-white">
|
||||||
<SiteHeader />
|
<SiteHeader />
|
||||||
<main className="flex h-full flex-1 overflow-auto">
|
<main className="flex h-full flex-1 overflow-auto">
|
||||||
<UserViewProvider userId={props.userId}>
|
<UserViewProvider userId={props.userId}>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{t.heads.area_riservata_titolo}</title>
|
<title>{t.heads.area_riservata_titolo}</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content={t.heads.area_riservata_description}
|
content={t.heads.area_riservata_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">
|
||||||
<Sidebar isAdmin />
|
<Sidebar isAdmin />
|
||||||
|
|
||||||
<div className="mx-4 my-3 flex-1 overflow-auto">
|
<div className="mx-4 my-3 flex-1 overflow-auto">
|
||||||
<UserViewHeader />
|
<UserViewHeader />
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</UserViewProvider>
|
</UserViewProvider>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<MiniFooter />
|
<MiniFooter />
|
||||||
|
|
||||||
{/* <CookieConsent /> */}
|
{/* <CookieConsent /> */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const UserViewHeader = () => {
|
const UserViewHeader = () => {
|
||||||
const data = useContext(UserViewContext);
|
const data = useContext(UserViewContext);
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { mutate: swap } = api.auth.swapUser.useMutation({
|
const { mutate: swap } = api.auth.swapUser.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
toast.success("Utente cambiato con successo");
|
toast.success("Utente cambiato con successo");
|
||||||
await router.push("/area-riservata/dashboard");
|
await router.push("/area-riservata/dashboard");
|
||||||
router.reload();
|
router.reload();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!data) return <div>Errore</div>;
|
if (!data) return <div>Errore</div>;
|
||||||
return (
|
return (
|
||||||
<div className="mb-5 flex flex-col space-y-5">
|
<div className="mb-5 flex flex-col space-y-5">
|
||||||
<div className="flex flex-row flex-wrap items-baseline gap-3">
|
<div className="flex flex-row flex-wrap items-baseline gap-3">
|
||||||
<div className="text-3xl">{data.username}</div>
|
<div className="text-3xl">{data.username}</div>
|
||||||
<div className="text-muted-foreground text-sm">
|
<div className="text-muted-foreground text-sm">
|
||||||
{data.email} | {data.telefono}
|
{data.email} | {data.telefono}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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"
|
variant="success"
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<WhatsAppIcon className="size-4 fill-white" />
|
<WhatsAppIcon className="size-4 fill-white" />
|
||||||
WhatsApp
|
WhatsApp
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row flex-wrap gap-2">
|
<div className="flex flex-row flex-wrap gap-2">
|
||||||
<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"}
|
variant={pathname.includes("edit-user") ? "secondary" : "outline"}
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<UserCog /> Profilo
|
<UserCog /> Profilo
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<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"}
|
variant={pathname.includes("servizio") ? "secondary" : "outline"}
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<Tickets /> Servizi
|
<Tickets /> Servizi
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<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"}
|
variant={pathname.includes("ricerca") ? "secondary" : "outline"}
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<Search /> Ricerca
|
<Search /> Ricerca
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<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"
|
||||||
variant={
|
variant={
|
||||||
pathname.includes("comunicazioni") ? "secondary" : "outline"
|
pathname.includes("comunicazioni") ? "secondary" : "outline"
|
||||||
}
|
}
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<Mail /> Comunicazioni
|
<Mail /> Comunicazioni
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<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"}
|
variant={pathname.includes("ordini") ? "secondary" : "outline"}
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<ShoppingBag /> Ordini
|
<ShoppingBag /> Ordini
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<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"}
|
variant={pathname.includes("allegati") ? "secondary" : "outline"}
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<Paperclip /> Allegati
|
<Paperclip /> Allegati
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href={
|
href={
|
||||||
data.chatid
|
data.chatid
|
||||||
? {
|
? {
|
||||||
pathname: "/area-riservata/admin/chats",
|
pathname: "/area-riservata/admin/chats",
|
||||||
query: { chatid: data.chatid },
|
query: { chatid: data.chatid },
|
||||||
}
|
}
|
||||||
: "/area-riservata/admin/chats"
|
: "/area-riservata/admin/chats"
|
||||||
}
|
}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
className="flex items-center gap-2 border"
|
className="flex items-center gap-2 border"
|
||||||
variant={!data.chatid ? "ghost" : "outline"}
|
variant={!data.chatid ? "ghost" : "outline"}
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<MessagesSquare /> Chat
|
<MessagesSquare /> Chat
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
className="flex gap-2"
|
className="flex gap-2"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
swap({
|
swap({
|
||||||
id: data.id,
|
id: data.id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ExternalLink /> Login
|
<ExternalLink /> Login
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,65 +1,65 @@
|
||||||
"use client";
|
"use client";
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import { MapIcon } from "lucide-react";
|
import { MapIcon } from "lucide-react";
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
import dynamic from "next/dynamic";
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "~/components/ui/dialog";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "~/components/ui/dialog";
|
||||||
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
|
|
||||||
const Map = dynamic(() => import("~/components/map/Map"), {
|
const MapComp = dynamic(() => import("~/components/map/Map"), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const MappaDialogFullscreen = ({
|
export const MappaDialogFullscreen = ({
|
||||||
lat,
|
lat,
|
||||||
long,
|
long,
|
||||||
}: {
|
}: {
|
||||||
lat: string | null;
|
lat: string | null;
|
||||||
long: string | null;
|
long: string | null;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<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}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="h-full w-screen max-w-[100vw] p-0 sm:max-h-[60vh] sm:max-w-2xl [&_#dialog-close]:bg-white [&_#dialog-close]:opacity-100 [&_#dialog-close>svg]:size-8">
|
<DialogContent className="h-full w-screen max-w-[100vw] p-0 sm:max-h-[60vh] sm:max-w-2xl [&_#dialog-close]:bg-white [&_#dialog-close]:opacity-100 [&_#dialog-close>svg]:size-8">
|
||||||
<DialogHeader className="hidden h-0">
|
<DialogHeader className="hidden h-0">
|
||||||
<DialogTitle className="sr-only">{t.mappa}</DialogTitle>
|
<DialogTitle className="sr-only">{t.mappa}</DialogTitle>
|
||||||
<DialogDescription className="sr-only">{t.mappa}</DialogDescription>
|
<DialogDescription className="sr-only">{t.mappa}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="size-full">
|
<div className="size-full">
|
||||||
{lat && long && (
|
{lat && long && (
|
||||||
<>
|
<>
|
||||||
<Map
|
<MapComp
|
||||||
posix={{
|
posix={{
|
||||||
pos: {
|
pos: {
|
||||||
lat: Number.parseFloat(lat),
|
lat: Number.parseFloat(lat),
|
||||||
lng: Number.parseFloat(long),
|
lng: Number.parseFloat(long),
|
||||||
},
|
},
|
||||||
popupTxt:
|
popupTxt:
|
||||||
"Posizione esatta fornita dopo la richiesta di contatto",
|
"Posizione esatta fornita dopo la richiesta di contatto",
|
||||||
}}
|
}}
|
||||||
markerType="home"
|
markerType="home"
|
||||||
scrollWheelZoom
|
scrollWheelZoom
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
zoom={14}
|
zoom={14}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
import { forwardRef, type InputHTMLAttributes } from "react";
|
import { forwardRef, type InputHTMLAttributes } from "react";
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionContent,
|
AccordionContent,
|
||||||
AccordionItem,
|
AccordionItem,
|
||||||
AccordionTrigger,
|
AccordionTrigger,
|
||||||
} from "~/components/ui/accordion";
|
} from "~/components/ui/accordion";
|
||||||
import type { Faq } from "~/i18n/locales";
|
import type { Faq } from "~/i18n/locales";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
interface AccordionCompProps extends InputHTMLAttributes<HTMLDivElement> {
|
interface AccordionCompProps extends InputHTMLAttributes<HTMLDivElement> {
|
||||||
texts: Faq[];
|
texts: Faq[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -29,30 +29,30 @@ interface AccordionCompProps extends InputHTMLAttributes<HTMLDivElement> {
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export const AccordionComp = forwardRef<HTMLDivElement, AccordionCompProps>(
|
export const AccordionComp = forwardRef<HTMLDivElement, AccordionCompProps>(
|
||||||
({ texts, className }, ref) => {
|
({ texts, className }, ref) => {
|
||||||
if (!texts || texts.length === 0) {
|
if (!texts || texts.length === 0) {
|
||||||
return null; // Return null if no texts are provided
|
return null; // Return null if no texts are provided
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
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 type="single" collapsible className="w-full">
|
||||||
{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">
|
||||||
{text.title}
|
{text.title}
|
||||||
</AccordionTrigger>
|
</AccordionTrigger>
|
||||||
<AccordionContent className="text-base">
|
<AccordionContent className="text-base">
|
||||||
{text.description}
|
{text.description}
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
))}
|
))}
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
AccordionComp.displayName = "AccordionComp";
|
AccordionComp.displayName = "AccordionComp";
|
||||||
|
|
|
||||||
|
|
@ -1,242 +1,241 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import { useState, type FormEvent } from "react";
|
|
||||||
import {
|
import {
|
||||||
Elements,
|
Elements,
|
||||||
PaymentElement,
|
PaymentElement,
|
||||||
useElements,
|
useElements,
|
||||||
useStripe,
|
useStripe,
|
||||||
} from "@stripe/react-stripe-js";
|
} from "@stripe/react-stripe-js";
|
||||||
|
import type { Stripe } from "@stripe/stripe-js";
|
||||||
|
import { ArrowRight, ExternalLink } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { type FormEvent, useState } from "react";
|
||||||
|
import LoadingButton from "~/components/custom_ui/loading-button";
|
||||||
import { LoadingPage } from "~/components/loading";
|
import { LoadingPage } from "~/components/loading";
|
||||||
import { Status500 } from "~/components/status-page";
|
import { Status500 } from "~/components/status-page";
|
||||||
import LoadingButton from "~/components/custom_ui/loading-button";
|
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
|
||||||
import type { Stripe } from "@stripe/stripe-js";
|
|
||||||
import { formatCurrency } from "~/lib/utils";
|
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||||
|
import { env } from "~/env.mjs";
|
||||||
|
import { formatCurrency } from "~/lib/utils";
|
||||||
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
import type { PaymentsId } from "~/schemas/public/Payments";
|
import type { PaymentsId } from "~/schemas/public/Payments";
|
||||||
import type { PrezziarioIdprezziario } from "~/schemas/public/Prezziario";
|
import type { PrezziarioIdprezziario } from "~/schemas/public/Prezziario";
|
||||||
import { ArrowRight, ExternalLink } from "lucide-react";
|
import { api } from "~/utils/api";
|
||||||
import { env } from "~/env.mjs";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
type AcquistoProcessingProps = {
|
type AcquistoProcessingProps = {
|
||||||
paymentId: PaymentsId;
|
paymentId: PaymentsId;
|
||||||
packId: PrezziarioIdprezziario;
|
packId: PrezziarioIdprezziario;
|
||||||
stripePromise: Promise<Stripe | null>;
|
stripePromise: Promise<Stripe | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AcquistoProcessing = ({
|
export const AcquistoProcessing = ({
|
||||||
paymentId,
|
paymentId,
|
||||||
packId,
|
packId,
|
||||||
stripePromise,
|
stripePromise,
|
||||||
}: AcquistoProcessingProps) => {
|
}: AcquistoProcessingProps) => {
|
||||||
const [stripeIntent, setStripeIntent] = useState<{
|
const [stripeIntent, setStripeIntent] = useState<{
|
||||||
clientSecret: string | null;
|
clientSecret: string | null;
|
||||||
titolo: string;
|
titolo: string;
|
||||||
prezzo: number;
|
prezzo: number;
|
||||||
descrizione: string;
|
descrizione: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [generatedIntent, setGeneratedIntent] = useState(false);
|
const [generatedIntent, setGeneratedIntent] = useState(false);
|
||||||
|
|
||||||
const { mutate, isPending } = api.stripe.createIntent.useMutation({
|
const { mutate, isPending } = api.stripe.createIntent.useMutation({
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
setStripeIntent(data);
|
setStripeIntent(data);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (generatedIntent && !isPending && !stripeIntent) {
|
if (generatedIntent && !isPending && !stripeIntent) {
|
||||||
return <Status500 />;
|
return <Status500 />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto my-4 w-full max-w-3xl space-y-8 rounded-md">
|
<div className="mx-auto my-4 w-full max-w-3xl space-y-8 rounded-md">
|
||||||
<h3 className="mt-2 text-center text-2xl font-bold tracking-wide text-neutral-700 uppercase">
|
<h3 className="mt-2 text-center text-2xl font-bold tracking-wide text-neutral-700 uppercase">
|
||||||
{t.acquisto.titolo}
|
{t.acquisto.titolo}
|
||||||
</h3>
|
</h3>
|
||||||
{!generatedIntent ? (
|
{!generatedIntent ? (
|
||||||
<PricePreparation
|
<PricePreparation
|
||||||
packId={packId}
|
packId={packId}
|
||||||
onProcedi={() => {
|
onProcedi={() => {
|
||||||
setGeneratedIntent(true);
|
setGeneratedIntent(true);
|
||||||
mutate({
|
mutate({
|
||||||
paymentId,
|
paymentId,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : isPending ? (
|
) : isPending ? (
|
||||||
<LoadingPage />
|
<LoadingPage />
|
||||||
) : (
|
) : (
|
||||||
stripeIntent &&
|
stripeIntent?.clientSecret && (
|
||||||
stripeIntent.clientSecret && (
|
<>
|
||||||
<>
|
<Elements
|
||||||
<Elements
|
options={{
|
||||||
options={{
|
appearance: {
|
||||||
appearance: {
|
theme: "stripe",
|
||||||
theme: "stripe",
|
variables: {
|
||||||
variables: {
|
colorPrimary: "#18181b",
|
||||||
colorPrimary: "#18181b",
|
colorText: "#18181b",
|
||||||
colorText: "#18181b",
|
},
|
||||||
},
|
},
|
||||||
},
|
clientSecret: stripeIntent.clientSecret,
|
||||||
clientSecret: stripeIntent.clientSecret,
|
}}
|
||||||
}}
|
stripe={stripePromise}
|
||||||
stripe={stripePromise}
|
>
|
||||||
>
|
<CheckoutForm
|
||||||
<CheckoutForm
|
titolo={stripeIntent.titolo}
|
||||||
titolo={stripeIntent.titolo}
|
prezzo={stripeIntent.prezzo}
|
||||||
prezzo={stripeIntent.prezzo}
|
descrizione={stripeIntent.descrizione}
|
||||||
descrizione={stripeIntent.descrizione}
|
/>
|
||||||
/>
|
</Elements>
|
||||||
</Elements>
|
</>
|
||||||
</>
|
)
|
||||||
)
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PricePreparation = ({
|
export const PricePreparation = ({
|
||||||
packId,
|
packId,
|
||||||
onProcedi,
|
onProcedi,
|
||||||
}: {
|
}: {
|
||||||
packId: PrezziarioIdprezziario;
|
packId: PrezziarioIdprezziario;
|
||||||
onProcedi: () => void;
|
onProcedi: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { data: prezziario, isLoading } =
|
const { data: prezziario, isLoading } =
|
||||||
api.prezziario.getPrezzoPerServizio.useQuery({
|
api.prezziario.getPrezzoPerServizio.useQuery({
|
||||||
idprezziario: packId,
|
idprezziario: packId,
|
||||||
});
|
});
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto mt-4 max-w-2xl p-2 sm:mt-10">
|
<div className="mx-auto mt-4 max-w-2xl p-2 sm:mt-10">
|
||||||
{isLoading || !prezziario ? (
|
{isLoading || !prezziario ? (
|
||||||
<>
|
<>
|
||||||
<h3 className="text-2xl font-semibold">{t.acquisto.preparazione}</h3>
|
<h3 className="text-2xl font-semibold">{t.acquisto.preparazione}</h3>
|
||||||
<LoadingPage />
|
<LoadingPage />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<h3 className="text-center text-2xl font-semibold">
|
<h3 className="text-center text-2xl font-semibold">
|
||||||
{t.acquisto.servizi_acquisto}
|
{t.acquisto.servizi_acquisto}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<p className="text-3xl">{prezziario.nome_it}</p>
|
<p className="text-3xl">{prezziario.nome_it}</p>
|
||||||
<p className="text-lg">
|
<p className="text-lg">
|
||||||
Cod: “{prezziario.idprezziario}”
|
Cod: “{prezziario.idprezziario}”
|
||||||
</p>
|
</p>
|
||||||
<p className="text-4xl font-bold">
|
<p className="text-4xl font-bold">
|
||||||
{formatCurrency(prezziario.prezzo_cent / 100)}
|
{formatCurrency(prezziario.prezzo_cent / 100)}
|
||||||
</p>
|
</p>
|
||||||
<p>{prezziario.desc_it}</p>
|
<p>{prezziario.desc_it}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{prezziario.testo_condizioni && (
|
{prezziario.testo_condizioni && (
|
||||||
<Link
|
<Link
|
||||||
aria-label="Leggi le condizioni"
|
aria-label="Leggi le condizioni"
|
||||||
href={`/servizio/condizioni/${prezziario.testo_condizioni}`}
|
href={`/servizio/condizioni/${prezziario.testo_condizioni}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="block w-fit"
|
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>
|
||||||
<ExternalLink />
|
<ExternalLink />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={onProcedi}
|
onClick={onProcedi}
|
||||||
className="flex w-full items-center gap-2 text-lg"
|
className="flex w-full items-center gap-2 text-lg"
|
||||||
variant="info"
|
variant="info"
|
||||||
size="xl"
|
size="xl"
|
||||||
disabled={isLoading || !prezziario}
|
disabled={isLoading || !prezziario}
|
||||||
>
|
>
|
||||||
<span>{t.acquisto.procedi_pagamento}</span> <ArrowRight />
|
<span>{t.acquisto.procedi_pagamento}</span> <ArrowRight />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const CheckoutForm = ({
|
const CheckoutForm = ({
|
||||||
titolo,
|
titolo,
|
||||||
prezzo,
|
prezzo,
|
||||||
descrizione,
|
descrizione,
|
||||||
}: {
|
}: {
|
||||||
titolo: string;
|
titolo: string;
|
||||||
prezzo: number;
|
prezzo: number;
|
||||||
descrizione: string;
|
descrizione: string;
|
||||||
}) => {
|
}) => {
|
||||||
const stripe = useStripe();
|
const stripe = useStripe();
|
||||||
const elements = useElements();
|
const elements = useElements();
|
||||||
|
|
||||||
const [message, setMessage] = useState<string | null>(null);
|
const [message, setMessage] = useState<string | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
console.log("submitting payment");
|
console.log("submitting payment");
|
||||||
if (!stripe || !elements) {
|
if (!stripe || !elements) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const { error } = await stripe.confirmPayment({
|
const { error } = await stripe.confirmPayment({
|
||||||
elements,
|
elements,
|
||||||
confirmParams: {
|
confirmParams: {
|
||||||
return_url: `${env.NEXT_PUBLIC_BASE_URL}/servizio/acquisto-processing`,
|
return_url: `${env.NEXT_PUBLIC_BASE_URL}/servizio/acquisto-processing`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
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.");
|
||||||
} else {
|
} else {
|
||||||
setMessage("An unexpected error occurred.");
|
setMessage("An unexpected error occurred.");
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="flex w-full flex-col justify-between text-left">
|
<Card className="flex w-full flex-col justify-between text-left">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>
|
<CardTitle>
|
||||||
<p className="text-2xl">{titolo}</p>
|
<p className="text-2xl">{titolo}</p>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<p>{descrizione}</p>
|
<p>{descrizione}</p>
|
||||||
<span>
|
<span>
|
||||||
<span className="text-3xl font-bold">
|
<span className="text-3xl font-bold">
|
||||||
{formatCurrency(prezzo / 100)}
|
{formatCurrency(prezzo / 100)}
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
<span className="text-base font-normal sm:text-sm">
|
<span className="text-base font-normal sm:text-sm">
|
||||||
{t.pricing_cmp.risultati_iva}
|
{t.pricing_cmp.risultati_iva}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form id="payment-form" onSubmit={handleSubmit} className="space-y-5">
|
<form id="payment-form" onSubmit={handleSubmit} className="space-y-5">
|
||||||
<PaymentElement
|
<PaymentElement
|
||||||
id="payment-element"
|
id="payment-element"
|
||||||
options={{ layout: "accordion" }}
|
options={{ layout: "accordion" }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
aria-label="Procedi al pagamento"
|
aria-label="Procedi al pagamento"
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
disabled={isLoading || !stripe || !elements}
|
disabled={isLoading || !stripe || !elements}
|
||||||
loading={isLoading || !stripe || !elements}
|
loading={isLoading || !stripe || !elements}
|
||||||
>
|
>
|
||||||
{t.procedi}
|
{t.procedi}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
{message && <div id="payment-message">{message}</div>}
|
{message && <div id="payment-message">{message}</div>}
|
||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,187 +1,187 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import { it } from "date-fns/locale";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useRef } from "react";
|
||||||
|
import { useReactToPrint } from "react-to-print";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Card } from "~/components/ui/card";
|
import { Card } from "~/components/ui/card";
|
||||||
import Image from "next/image";
|
|
||||||
import { format } from "date-fns";
|
|
||||||
import { useReactToPrint } from "react-to-print";
|
|
||||||
import { it } from "date-fns/locale";
|
|
||||||
import { formatCurrency } from "~/lib/utils";
|
|
||||||
import { useRef } from "react";
|
|
||||||
import { PaymentMethodToString, type PaymentType } from "~/i18n/stripe";
|
import { PaymentMethodToString, type PaymentType } from "~/i18n/stripe";
|
||||||
|
import { formatCurrency } from "~/lib/utils";
|
||||||
|
|
||||||
export function ReceiptGenerator({ data }: { data: PurchaseData }) {
|
export function ReceiptGenerator({ data }: { data: PurchaseData }) {
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
const reactToPrintFn = useReactToPrint({
|
const reactToPrintFn = useReactToPrint({
|
||||||
contentRef,
|
contentRef,
|
||||||
//print: async (target) => {console.log("Printing...", target.contentDocument);},
|
//print: async (target) => {console.log("Printing...", target.contentDocument);},
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-4xl">
|
<div className="mx-auto max-w-4xl">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<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 onClick={() => reactToPrintFn()} className="print:hidden">
|
||||||
Stampa
|
Stampa
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div ref={contentRef}>
|
<div ref={contentRef}>
|
||||||
<Receipt data={data} />
|
<Receipt data={data} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Item {
|
interface Item {
|
||||||
description: string;
|
description: string;
|
||||||
price: number;
|
price: number;
|
||||||
discount: number;
|
discount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PurchaseData {
|
export interface PurchaseData {
|
||||||
paymentId: string;
|
paymentId: string;
|
||||||
date: Date;
|
date: Date;
|
||||||
|
|
||||||
clienteNome: string;
|
clienteNome: string;
|
||||||
clienteIndirizzo: string;
|
clienteIndirizzo: string;
|
||||||
paymentMethod: string;
|
paymentMethod: string;
|
||||||
items: Item[];
|
items: Item[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReceiptProps {
|
interface ReceiptProps {
|
||||||
data: PurchaseData;
|
data: PurchaseData;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Receipt({ data }: ReceiptProps) {
|
function Receipt({ data }: ReceiptProps) {
|
||||||
const companyName = "Arcenia S.r.l.";
|
const companyName = "Arcenia S.r.l.";
|
||||||
|
|
||||||
const companyAddress =
|
const companyAddress =
|
||||||
"Via Beata Giovanna 1\nBassano del Grappa (VI) 36061\nItalia";
|
"Via Beata Giovanna 1\nBassano del Grappa (VI) 36061\nItalia";
|
||||||
|
|
||||||
const total = data.items.reduce(
|
const total = data.items.reduce(
|
||||||
(acc, item) => acc + item.price * (1 - item.discount),
|
(acc, item) => acc + item.price * (1 - item.discount),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
const note =
|
const note =
|
||||||
"La presente è non costituisce ricevuta o fattura fiscale.\nRiceverai la fattura elettronica direttamente al tuo indirizzo di posta.";
|
"La presente è non costituisce ricevuta o fattura fiscale.\nRiceverai la fattura elettronica direttamente al tuo indirizzo di posta.";
|
||||||
const footer =
|
const footer =
|
||||||
"Arcenia Srl. | Tel. +39 0424529869 | Email: arca@infoalloggi.it";
|
"Arcenia Srl. | Tel. +39 0424529869 | Email: arca@infoalloggi.it";
|
||||||
return (
|
return (
|
||||||
<Card className="p-4 print:border-none print:shadow-none">
|
<Card className="p-4 print:border-none print:shadow-none">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold">{companyName}</h1>
|
<h1 className="text-2xl font-bold">{companyName}</h1>
|
||||||
<p className="text-sm whitespace-pre-line">{companyAddress}</p>
|
<p className="text-sm whitespace-pre-line">{companyAddress}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative size-24">
|
<div className="relative size-24">
|
||||||
<Image
|
<Image
|
||||||
src={"/favicon/favicon.png"}
|
src={"/favicon/favicon.png"}
|
||||||
alt={`${companyName} logo`}
|
alt={`${companyName} logo`}
|
||||||
fill
|
fill
|
||||||
className="object-contain"
|
className="object-contain"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-b pb-4">
|
<div className="border-b pb-4">
|
||||||
<h2 className="text-center text-xl font-semibold">
|
<h2 className="text-center text-xl font-semibold">
|
||||||
Ricevuta di cortesia
|
Ricevuta di cortesia
|
||||||
</h2>
|
</h2>
|
||||||
<div className="mt-2 flex justify-between">
|
<div className="mt-2 flex justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground text-sm">ID Pagamento</p>
|
<p className="text-muted-foreground text-sm">ID Pagamento</p>
|
||||||
<p className="text-sm">{data.paymentId}</p>
|
<p className="text-sm">{data.paymentId}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<p className="text-muted-foreground text-sm">Data</p>
|
<p className="text-muted-foreground text-sm">Data</p>
|
||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
{format(data.date, "PPP", {
|
{format(data.date, "PPP", {
|
||||||
locale: it,
|
locale: it,
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
{format(data.date, "p", {
|
{format(data.date, "p", {
|
||||||
locale: it,
|
locale: it,
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-6">
|
<div className="grid grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="mb-1 font-semibold">Intestatario:</h3>
|
<h3 className="mb-1 font-semibold">Intestatario:</h3>
|
||||||
<p className="font-medium">{data.clienteNome}</p>
|
<p className="font-medium">{data.clienteNome}</p>
|
||||||
<p className="text-sm whitespace-pre-line">
|
<p className="text-sm whitespace-pre-line">
|
||||||
{data.clienteIndirizzo}
|
{data.clienteIndirizzo}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b">
|
<tr className="border-b">
|
||||||
<th className="py-2 text-left">Descrizione</th>
|
<th className="py-2 text-left">Descrizione</th>
|
||||||
<th className="py-2 text-right">Prezzo</th>
|
<th className="py-2 text-right">Prezzo</th>
|
||||||
<th className="py-2 text-right">Sconto</th>
|
<th className="py-2 text-right">Sconto</th>
|
||||||
<th className="py-2 text-right">Importo</th>
|
<th className="py-2 text-right">Importo</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{data.items.map((item, idx) => (
|
{data.items.map((item, idx) => (
|
||||||
<tr key={idx} className="border-b">
|
<tr key={idx} className="border-b">
|
||||||
<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)}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 text-right">
|
<td className="py-2 text-right">
|
||||||
{item.discount > 0
|
{item.discount > 0
|
||||||
? `-${(item.discount * 100).toFixed(0)}%`
|
? `-${(item.discount * 100).toFixed(0)}%`
|
||||||
: ""}
|
: ""}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 text-right">
|
<td className="py-2 text-right">
|
||||||
{formatCurrency(item.price * (1 - item.discount))}
|
{formatCurrency(item.price * (1 - item.discount))}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<div className="w-64">
|
<div className="w-64">
|
||||||
<div className="mt-1 flex justify-between pt-2 font-bold">
|
<div className="mt-1 flex justify-between pt-2 font-bold">
|
||||||
<span>Totale:</span>
|
<span>Totale:</span>
|
||||||
<span>{formatCurrency(total)}</span>
|
<span>{formatCurrency(total)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between py-1">
|
<div className="flex justify-between py-1">
|
||||||
<span>di cui IVA (22%):</span>
|
<span>di cui IVA (22%):</span>
|
||||||
<span>{formatCurrency(total * 0.22)}</span>
|
<span>{formatCurrency(total * 0.22)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t pt-4">
|
<div className="border-t pt-4">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="mb-1 font-semibold">Metodo di pagamento</h3>
|
<h3 className="mb-1 font-semibold">Metodo di pagamento</h3>
|
||||||
<p>{PaymentMethodToString(data.paymentMethod as PaymentType)}</p>
|
<p>{PaymentMethodToString(data.paymentMethod as PaymentType)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<h3 className="mb-1 font-semibold">Status pagamento</h3>
|
<h3 className="mb-1 font-semibold">Status pagamento</h3>
|
||||||
<p className="font-medium text-green-600">Pagato</p>
|
<p className="font-medium text-green-600">Pagato</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t pt-4">
|
<div className="border-t pt-4">
|
||||||
<h3 className="mb-1 font-semibold">Note</h3>
|
<h3 className="mb-1 font-semibold">Note</h3>
|
||||||
<p className="text-sm whitespace-pre-line">{note}</p>
|
<p className="text-sm whitespace-pre-line">{note}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-muted-foreground pt-6 text-center text-sm">
|
<div className="text-muted-foreground pt-6 text-center text-sm">
|
||||||
<p>{footer}</p>
|
<p>{footer}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,56 +1,57 @@
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { LoadingPage } from "~/components/loading";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
import type { TempTokensToken } from "~/schemas/public/TempTokens";
|
import type { TempTokensToken } from "~/schemas/public/TempTokens";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { LoadingPage } from "~/components/loading";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
|
|
||||||
export const AllegatoIframe = ({
|
export const AllegatoIframe = ({
|
||||||
className,
|
className,
|
||||||
allegato,
|
allegato,
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
allegato: string;
|
allegato: string;
|
||||||
}) => {
|
}) => {
|
||||||
const if_ref = useRef<HTMLIFrameElement>(null);
|
const if_ref = useRef<HTMLIFrameElement>(null);
|
||||||
const { mutateAsync: getToken } = api.storage.getStorageToken.useMutation();
|
const { mutateAsync: getToken } = api.storage.getStorageToken.useMutation();
|
||||||
const [token, setToken] = useState<TempTokensToken | null>(null);
|
const [token, setToken] = useState<TempTokensToken | null>(null);
|
||||||
const handleIframeLoad = () => {
|
const handleIframeLoad = () => {
|
||||||
if (if_ref.current && if_ref.current.contentWindow) {
|
if (if_ref.current?.contentWindow) {
|
||||||
const img = if_ref.current.contentWindow.document.querySelector("img");
|
const img = if_ref.current.contentWindow.document.querySelector("img");
|
||||||
if (img) {
|
if (img) {
|
||||||
img.style.width = "100%";
|
img.style.width = "100%";
|
||||||
img.style.height = "100%";
|
img.style.height = "100%";
|
||||||
img.style.objectFit = "contain";
|
img.style.objectFit = "contain";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function set() {
|
async function set() {
|
||||||
const token = await getToken();
|
const token = await getToken();
|
||||||
setToken(token);
|
setToken(token);
|
||||||
}
|
}
|
||||||
set().catch((e) => console.error(e));
|
set().catch((e) => console.error(e));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!token) return <LoadingPage />;
|
if (!token) return <LoadingPage />;
|
||||||
|
|
||||||
const requestUrl = `/go-api/storage/get/${allegato}?token=${String(token)}`;
|
const requestUrl = `/go-api/storage/get/${allegato}?token=${String(token)}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-full w-full items-center justify-center object-contain",
|
"flex h-full w-full items-center justify-center object-contain",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<iframe
|
{/** biome-ignore lint/a11y/noNoninteractiveElementInteractions: <need interaction> */}
|
||||||
title="Allegato"
|
<iframe
|
||||||
ref={if_ref}
|
title="Allegato"
|
||||||
src={requestUrl}
|
ref={if_ref}
|
||||||
className="h-full w-full"
|
src={requestUrl}
|
||||||
onLoad={handleIframeLoad}
|
className="h-full w-full"
|
||||||
allowFullScreen
|
onLoad={handleIframeLoad}
|
||||||
/>
|
allowFullScreen
|
||||||
</div>
|
/>
|
||||||
);
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,34 @@
|
||||||
import { getTitoloTranslation } from "~/lib/annuncio_details";
|
|
||||||
import { CardAnnuncio } from "~/components/annuncio_card";
|
import { CardAnnuncio } from "~/components/annuncio_card";
|
||||||
|
import { getTitoloTranslation } from "~/lib/annuncio_details";
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
import type { AnnuncioRicerca } from "~/server/controllers/annunci.controller";
|
import type { AnnuncioRicerca } from "~/server/controllers/annunci.controller";
|
||||||
|
|
||||||
export const AnnunciGrid = ({ pagedata }: { pagedata: AnnuncioRicerca[] }) => {
|
export const AnnunciGrid = ({ pagedata }: { pagedata: AnnuncioRicerca[] }) => {
|
||||||
const { locale } = useTranslation();
|
const { locale } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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}
|
id={annuncio.id}
|
||||||
key={annuncio.codice}
|
key={annuncio.codice}
|
||||||
codice={annuncio.codice}
|
codice={annuncio.codice}
|
||||||
prezzo={annuncio.prezzo}
|
prezzo={annuncio.prezzo}
|
||||||
titolo={getTitoloTranslation({
|
titolo={getTitoloTranslation({
|
||||||
locale: locale,
|
locale: locale,
|
||||||
titolo_it: annuncio.titolo_it,
|
titolo_it: annuncio.titolo_it,
|
||||||
titolo_en: annuncio.titolo_en,
|
titolo_en: annuncio.titolo_en,
|
||||||
})}
|
})}
|
||||||
mq={annuncio.mq}
|
mq={annuncio.mq}
|
||||||
comune={annuncio.comune}
|
comune={annuncio.comune}
|
||||||
provincia={annuncio.provincia}
|
provincia={annuncio.provincia}
|
||||||
consegna={annuncio.consegna}
|
consegna={annuncio.consegna}
|
||||||
camere={annuncio.numero_camere}
|
camere={annuncio.numero_camere}
|
||||||
immagini={annuncio.url_immagini || undefined}
|
immagini={annuncio.url_immagini || undefined}
|
||||||
tipo={annuncio.tipo}
|
tipo={annuncio.tipo}
|
||||||
stato={annuncio.stato}
|
stato={annuncio.stato}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,161 +1,161 @@
|
||||||
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import type { AnnuncioRicercaWPosition } from "~/server/controllers/annunci.controller";
|
import Image from "next/image";
|
||||||
import { api } from "~/utils/api";
|
import {
|
||||||
|
type Dispatch,
|
||||||
|
Fragment,
|
||||||
|
memo,
|
||||||
|
type SetStateAction,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import { LoadingPage } from "~/components/loading";
|
import { LoadingPage } from "~/components/loading";
|
||||||
import { Status500 } from "~/components/status-page";
|
import { Status500 } from "~/components/status-page";
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
|
||||||
import Image from "next/image";
|
|
||||||
import { formatCurrency } from "~/lib/utils";
|
|
||||||
import { handleConsegna } from "~/lib/annuncio_details";
|
import { handleConsegna } from "~/lib/annuncio_details";
|
||||||
import {
|
import { formatCurrency } from "~/lib/utils";
|
||||||
Fragment,
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
memo,
|
|
||||||
useState,
|
|
||||||
type Dispatch,
|
|
||||||
type SetStateAction,
|
|
||||||
} from "react";
|
|
||||||
import { useRicerca } from "~/providers/RicercaProvider";
|
import { useRicerca } from "~/providers/RicercaProvider";
|
||||||
|
import type { AnnuncioRicercaWPosition } from "~/server/controllers/annunci.controller";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
const Map = dynamic(() => import("~/components/map/Map"), {
|
const MapComp = dynamic(() => import("~/components/map/Map"), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const MapSection = () => {
|
export const MapSection = () => {
|
||||||
const { comune, consegna, tipo } = useRicerca();
|
const { comune, consegna, tipo } = useRicerca();
|
||||||
const [selected, setSelected] = useState<AnnuncioRicercaWPosition | null>(
|
const [selected, setSelected] = useState<AnnuncioRicercaWPosition | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
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}
|
selectTipo={tipo}
|
||||||
selectComune={comune}
|
selectComune={comune}
|
||||||
selectConsegna={consegna}
|
selectConsegna={consegna}
|
||||||
setSelected={setSelected}
|
setSelected={setSelected}
|
||||||
/>
|
/>
|
||||||
<SelectedComp selected={selected} />
|
<SelectedComp selected={selected} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const MapDisplay = memo(
|
const MapDisplay = memo(
|
||||||
({
|
({
|
||||||
selectTipo,
|
selectTipo,
|
||||||
selectComune,
|
selectComune,
|
||||||
selectConsegna,
|
selectConsegna,
|
||||||
setSelected,
|
setSelected,
|
||||||
}: {
|
}: {
|
||||||
selectTipo: string;
|
selectTipo: string;
|
||||||
selectComune: string;
|
selectComune: string;
|
||||||
selectConsegna: number | null;
|
selectConsegna: number | null;
|
||||||
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,
|
tipologia: selectTipo,
|
||||||
comune: selectComune,
|
comune: selectComune,
|
||||||
consegna: selectConsegna || undefined,
|
consegna: selectConsegna || undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isLoading) return <LoadingPage />;
|
if (isLoading) return <LoadingPage />;
|
||||||
if (!data) return <Status500 />;
|
if (!data) return <Status500 />;
|
||||||
|
|
||||||
const posizioni = data
|
const posizioni = data
|
||||||
.map((a) => {
|
.map((a) => {
|
||||||
if (a.lat_secondario && a.lon_secondario) {
|
if (a.lat_secondario && a.lon_secondario) {
|
||||||
return {
|
return {
|
||||||
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),
|
onClick: () => setSelected(a),
|
||||||
markerTxt: `€ ${a.prezzo / 1e2}`,
|
markerTxt: `€ ${a.prezzo / 1e2}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
})
|
})
|
||||||
.filter((a) => a !== undefined);
|
.filter((a) => a !== undefined);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Map
|
<MapComp
|
||||||
isGroup
|
isGroup
|
||||||
groupData={posizioni}
|
groupData={posizioni}
|
||||||
posix={{ pos: { lat: 45.764718, lng: 11.73067 } }}
|
posix={{ pos: { lat: 45.764718, lng: 11.73067 } }}
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
zoom={13}
|
zoom={13}
|
||||||
scrollWheelZoom
|
scrollWheelZoom
|
||||||
markerType="price"
|
markerType="price"
|
||||||
onMapClick={() => setSelected(null)}
|
onMapClick={() => setSelected(null)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
MapDisplay.displayName = "MapDisplay";
|
MapDisplay.displayName = "MapDisplay";
|
||||||
|
|
||||||
const SelectedComp = memo(
|
const SelectedComp = memo(
|
||||||
({ selected }: { selected: AnnuncioRicercaWPosition | null }) => {
|
({ selected }: { selected: AnnuncioRicercaWPosition | null }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<AnimatePresence initial={false}>
|
<AnimatePresence initial={false}>
|
||||||
{selected && (
|
{selected && (
|
||||||
<motion.a
|
<motion.a
|
||||||
aria-label="Visualizza Dettagli Annuncio"
|
aria-label="Visualizza Dettagli Annuncio"
|
||||||
href={`/annuncio/${selected.codice}`}
|
href={`/annuncio/${selected.codice}`}
|
||||||
target="_blank"
|
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"
|
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 }}
|
initial={{ opacity: 0, scale: 0 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
exit={{ opacity: 0, scale: 0 }}
|
exit={{ opacity: 0, scale: 0 }}
|
||||||
transition={{
|
transition={{
|
||||||
duration: 0.1,
|
duration: 0.1,
|
||||||
scale: { type: "spring", visualDuration: 0.1, bounce: 0.2 },
|
scale: { type: "spring", visualDuration: 0.1, bounce: 0.2 },
|
||||||
}}
|
}}
|
||||||
key={selected.id}
|
key={selected.id}
|
||||||
>
|
>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{selected.url_immagini && selected.url_immagini[0] ? (
|
{selected.url_immagini?.[0] ? (
|
||||||
<Image
|
<Image
|
||||||
src={selected.url_immagini[0]}
|
src={selected.url_immagini[0]}
|
||||||
width={500}
|
width={500}
|
||||||
height={500}
|
height={500}
|
||||||
className="size-24 rounded-md object-cover sm:size-40"
|
className="size-24 rounded-md object-cover sm:size-40"
|
||||||
alt="a"
|
alt="a"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="size-20 bg-gray-200 sm:size-40" />
|
<div className="size-20 bg-gray-200 sm:size-40" />
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col sm:gap-2 sm:text-lg">
|
<div className="flex flex-col sm:gap-2 sm:text-lg">
|
||||||
<h6 className="font-semibold underline-offset-2 group-hover:underline">
|
<h6 className="font-semibold underline-offset-2 group-hover:underline">
|
||||||
{selected.codice}
|
{selected.codice}
|
||||||
</h6>
|
</h6>
|
||||||
<h3 className="text-sm sm:text-base">{selected.titolo_it}</h3>
|
<h3 className="text-sm sm:text-base">{selected.titolo_it}</h3>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<h3 className="font-semibold">
|
<h3 className="font-semibold">
|
||||||
{formatCurrency(selected.prezzo / 1e2)}
|
{formatCurrency(selected.prezzo / 1e2)}
|
||||||
</h3>
|
</h3>
|
||||||
<h3>{selected.mq}mq</h3>
|
<h3>{selected.mq}mq</h3>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
{handleConsegna({
|
{handleConsegna({
|
||||||
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,
|
aggiornamento: t.card.in_aggiornamento,
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.a>
|
</motion.a>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
SelectedComp.displayName = "SelectedComp";
|
SelectedComp.displayName = "SelectedComp";
|
||||||
|
|
|
||||||
|
|
@ -1,118 +1,117 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
import { DialogTrigger } from "@radix-ui/react-dialog";
|
||||||
import { getCookie, setCookie } from "cookies-next/client";
|
import { getCookie, setCookie } from "cookies-next/client";
|
||||||
import { add } from "date-fns";
|
import { add } from "date-fns";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "~/components/ui/dialog";
|
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { ArrowRight, CircleHelp } from "lucide-react";
|
import { ArrowRight, CircleHelp } from "lucide-react";
|
||||||
import { DialogTrigger } from "@radix-ui/react-dialog";
|
import { useRouter } from "next/router";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "~/components/ui/dialog";
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
|
|
||||||
const COOKIE_KEY = "tipologia_tutorial_shown";
|
const COOKIE_KEY = "tipologia_tutorial_shown";
|
||||||
|
|
||||||
export const CTA_TipologiaModal = () => {
|
export const CTA_TipologiaModal = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const path = useRouter().asPath;
|
const path = useRouter().asPath;
|
||||||
const hasCookie = getCookie(COOKIE_KEY) === "true";
|
const hasCookie = getCookie(COOKIE_KEY) === "true";
|
||||||
|
|
||||||
const update = async (v: "Transitorio" | "Stabile" | null) => {
|
const update = async (v: "Transitorio" | "Stabile" | null) => {
|
||||||
setCookie(COOKIE_KEY, "true", {
|
setCookie(COOKIE_KEY, "true", {
|
||||||
expires: add(new Date(), { days: 30 }), // Set cookie to expire in 30 days
|
expires: add(new Date(), { days: 30 }), // Set cookie to expire in 30 days
|
||||||
});
|
});
|
||||||
if (v !== null) {
|
if (v !== null) {
|
||||||
window.location.assign("/annunci?tipo=" + v);
|
window.location.assign(`/annunci?tipo=${v}`);
|
||||||
}
|
}
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (path == "/annunci" && !hasCookie) {
|
if (path === "/annunci" && !hasCookie) {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
}, 1000); // 1 second delay
|
}, 1000); // 1 second delay
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}
|
}
|
||||||
}, [path, hasCookie]);
|
}, [path, hasCookie]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={async (v) => {
|
onOpenChange={async (v) => {
|
||||||
setOpen(v);
|
setOpen(v);
|
||||||
if (!v) {
|
if (!v) {
|
||||||
await update(null);
|
await update(null);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="info" className="flex items-center gap-2">
|
<Button variant="info" className="flex items-center gap-2">
|
||||||
<CircleHelp />{" "}
|
<CircleHelp />{" "}
|
||||||
<span className="hidden sm:block">
|
<span className="hidden sm:block">
|
||||||
{t.tipologia_tutorial.btn_title}
|
{t.tipologia_tutorial.btn_title}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-2xl">
|
<DialogTitle className="text-2xl">
|
||||||
{t.tipologia_tutorial.title}
|
{t.tipologia_tutorial.title}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="sr-only">
|
<DialogDescription className="sr-only">
|
||||||
tipologia hint
|
tipologia hint
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
<div className="border-transitorio space-y-2 rounded-md border p-2">
|
<div className="border-transitorio space-y-2 rounded-md border p-2">
|
||||||
<h3 className="text-transitorio text-lg font-semibold">
|
<h3 className="text-transitorio text-lg font-semibold">
|
||||||
{t.tipologia_tutorial.transitorio}
|
{t.tipologia_tutorial.transitorio}
|
||||||
</h3>
|
</h3>
|
||||||
<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")}
|
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"
|
||||||
>
|
>
|
||||||
{t.tipologia_tutorial.transitorio_cta}
|
{t.tipologia_tutorial.transitorio_cta}
|
||||||
|
|
||||||
<ArrowRight className="size-4" />
|
<ArrowRight className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-stabile space-y-2 rounded-md border p-2">
|
<div className="border-stabile space-y-2 rounded-md border p-2">
|
||||||
<h3 className="text-stabile text-lg font-semibold">
|
<h3 className="text-stabile text-lg font-semibold">
|
||||||
{t.tipologia_tutorial.stabile}
|
{t.tipologia_tutorial.stabile}
|
||||||
</h3>
|
</h3>
|
||||||
<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")}
|
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"
|
||||||
>
|
>
|
||||||
{t.tipologia_tutorial.stabile_cta}
|
{t.tipologia_tutorial.stabile_cta}
|
||||||
|
|
||||||
<ArrowRight className="size-4" />
|
<ArrowRight className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
aria-label={t.tipologia_tutorial.alone}
|
aria-label={t.tipologia_tutorial.alone}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={async () => await update(null)}
|
onClick={async () => await update(null)}
|
||||||
className="flex w-full items-center gap-2"
|
className="flex w-full items-center gap-2"
|
||||||
>
|
>
|
||||||
{t.tipologia_tutorial.alone}
|
{t.tipologia_tutorial.alone}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,208 +1,208 @@
|
||||||
import { ArrowRight, BadgePlus, LogIn } from "lucide-react";
|
import { ArrowRight, BadgePlus, LogIn } from "lucide-react";
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
import { Button, buttonVariants } from "~/components/ui/button";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { ContattoAnnuncio } from "~/components/annuncio-interactions/contatto_modal";
|
||||||
|
import LoadingButton from "~/components/custom_ui/loading-button";
|
||||||
|
import { Button, buttonVariants } from "~/components/ui/button";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
import { useAnnuncio } from "~/providers/AnnuncioProvider";
|
import { useAnnuncio } from "~/providers/AnnuncioProvider";
|
||||||
import type { SessionContextType } from "~/providers/SessionProvider";
|
import type { SessionContextType } from "~/providers/SessionProvider";
|
||||||
import LoadingButton from "~/components/custom_ui/loading-button";
|
|
||||||
import type { UsersId } from "~/schemas/public/Users";
|
|
||||||
import type { AnnunciId } from "~/schemas/public/Annunci";
|
import type { AnnunciId } from "~/schemas/public/Annunci";
|
||||||
import { ContattoAnnuncio } from "~/components/annuncio-interactions/contatto_modal";
|
import type { UsersId } from "~/schemas/public/Users";
|
||||||
import toast from "react-hot-toast";
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
export const AnnuncioInteractions = ({
|
export const AnnuncioInteractions = ({
|
||||||
session,
|
session,
|
||||||
disabled,
|
disabled,
|
||||||
}: {
|
}: {
|
||||||
session: SessionContextType;
|
session: SessionContextType;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { id, tipo } = useAnnuncio();
|
const { id, tipo } = useAnnuncio();
|
||||||
|
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full rounded-md bg-red-500 p-2 text-center text-base font-semibold text-white">
|
<div className="w-full rounded-md bg-red-500 p-2 text-center text-base font-semibold text-white">
|
||||||
Richieste Temporaneamente disattivate
|
Richieste Temporaneamente disattivate
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col justify-around gap-3 md:gap-5">
|
<div className="flex w-full flex-col justify-around gap-3 md:gap-5">
|
||||||
{session.user ? (
|
{session.user ? (
|
||||||
tipo ? (
|
tipo ? (
|
||||||
<ServizioInteraction
|
<ServizioInteraction
|
||||||
userId={session.user.id}
|
userId={session.user.id}
|
||||||
tipologia={tipo}
|
tipologia={tipo}
|
||||||
annuncioId={id}
|
annuncioId={id}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span>Errore</span>
|
<span>Errore</span>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<div className="flex w-full flex-col gap-2">
|
<div className="flex w-full flex-col gap-2">
|
||||||
<Link
|
<Link
|
||||||
aria-label="Login"
|
aria-label="Login"
|
||||||
className={cn(buttonVariants({ variant: "default" }))}
|
className={cn(buttonVariants({ variant: "default" }))}
|
||||||
href={{
|
href={{
|
||||||
pathname: "/login",
|
pathname: "/login",
|
||||||
query: { redirect: router.asPath },
|
query: { redirect: router.asPath },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LogIn className="mr-1 size-7" /> Accedi se hai un account
|
<LogIn className="mr-1 size-7" /> Accedi se hai un account
|
||||||
</Link>
|
</Link>
|
||||||
<span className="w-full text-center text-sm">oppure</span>
|
<span className="w-full text-center text-sm">oppure</span>
|
||||||
<ContattoAnnuncio />
|
<ContattoAnnuncio />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ServizioInteraction = ({
|
const ServizioInteraction = ({
|
||||||
userId,
|
userId,
|
||||||
tipologia,
|
tipologia,
|
||||||
annuncioId,
|
annuncioId,
|
||||||
}: {
|
}: {
|
||||||
userId: UsersId;
|
userId: UsersId;
|
||||||
tipologia: string;
|
tipologia: string;
|
||||||
annuncioId: AnnunciId;
|
annuncioId: AnnunciId;
|
||||||
}) => {
|
}) => {
|
||||||
const { data: servizio, isLoading } =
|
const { data: servizio, isLoading } =
|
||||||
api.servizio.AnnuncioServizioTipologiaMatch.useQuery({
|
api.servizio.AnnuncioServizioTipologiaMatch.useQuery({
|
||||||
userId,
|
userId,
|
||||||
tipologia,
|
tipologia,
|
||||||
annuncioId,
|
annuncioId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isLoading) return <LoadingButton loading className="w-full" />;
|
if (isLoading) return <LoadingButton loading className="w-full" />;
|
||||||
if (
|
if (
|
||||||
servizio == undefined ||
|
servizio === undefined ||
|
||||||
servizio.status == "invalid" ||
|
servizio.status === "invalid" ||
|
||||||
servizio.status == "not_found"
|
servizio.status === "not_found"
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p>Non hai un servizio attivo</p>
|
<p>Non hai un servizio attivo</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (servizio.status == "already_saved") {
|
if (servizio.status === "already_saved") {
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-2">
|
<div className="flex w-full flex-col gap-2">
|
||||||
<div className="w-full text-center font-semibold text-green-500">
|
<div className="w-full text-center font-semibold text-green-500">
|
||||||
Annuncio già salvato!
|
Annuncio già salvato!
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href={`/area-riservata/dashboard`}
|
href={`/area-riservata/dashboard`}
|
||||||
aria-label="Vai alla tua area riservata"
|
aria-label="Vai alla tua area riservata"
|
||||||
>
|
>
|
||||||
<Button variant="success" className="flex w-full items-center gap-2">
|
<Button variant="success" className="flex w-full items-center gap-2">
|
||||||
Vai alla tua area riservata <ArrowRight />
|
Vai alla tua area riservata <ArrowRight />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <InteressatoButton annuncioId={annuncioId} />;
|
return <InteressatoButton annuncioId={annuncioId} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InteressatoButton = ({
|
export const InteressatoButton = ({
|
||||||
annuncioId,
|
annuncioId,
|
||||||
}: {
|
}: {
|
||||||
annuncioId: AnnunciId;
|
annuncioId: AnnunciId;
|
||||||
}) => {
|
}) => {
|
||||||
const { data: hasInterest, isLoading } =
|
const { data: hasInterest, isLoading } =
|
||||||
api.intrests.hasUserInterest.useQuery({
|
api.intrests.hasUserInterest.useQuery({
|
||||||
annuncioId,
|
annuncioId,
|
||||||
});
|
});
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { mutate: add } = api.intrests.addIntrest.useMutation({
|
const { mutate: add } = api.intrests.addIntrest.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await utils.intrests.hasUserInterest.invalidate({
|
await utils.intrests.hasUserInterest.invalidate({
|
||||||
annuncioId,
|
annuncioId,
|
||||||
});
|
});
|
||||||
toast.success("Richiesta di interesse inviata con successo!");
|
toast.success("Richiesta di interesse inviata con successo!");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isLoading || hasInterest === undefined) {
|
if (isLoading || hasInterest === undefined) {
|
||||||
return (
|
return (
|
||||||
<Button className="flex w-full items-center gap-2" disabled>
|
<Button className="flex w-full items-center gap-2" disabled>
|
||||||
<BadgePlus className="size-6" />
|
<BadgePlus className="size-6" />
|
||||||
Caricamento...
|
Caricamento...
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (hasInterest) {
|
if (hasInterest) {
|
||||||
return (
|
return (
|
||||||
<span className="flex w-full items-center justify-center gap-2 rounded-md bg-green-500 p-2 text-sm font-semibold text-white">
|
<span className="flex w-full items-center justify-center gap-2 rounded-md bg-green-500 p-2 text-sm font-semibold text-white">
|
||||||
<BadgePlus className="inline size-5" />
|
<BadgePlus className="inline size-5" />
|
||||||
Hai già mandato una richiesta
|
Hai già mandato una richiesta
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className="flex w-full items-center gap-2"
|
className="flex w-full items-center gap-2"
|
||||||
variant="info"
|
variant="info"
|
||||||
//size="xl"
|
//size="xl"
|
||||||
aria-label="Sono interessato"
|
aria-label="Sono interessato"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
add({
|
add({
|
||||||
annuncioId,
|
annuncioId,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BadgePlus className="size-6" />
|
<BadgePlus className="size-6" />
|
||||||
Sono interessato
|
Sono interessato
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InteressatoButtonBatchV = ({
|
export const InteressatoButtonBatchV = ({
|
||||||
annuncioId,
|
annuncioId,
|
||||||
hasInterest,
|
hasInterest,
|
||||||
}: {
|
}: {
|
||||||
annuncioId: AnnunciId;
|
annuncioId: AnnunciId;
|
||||||
hasInterest: boolean;
|
hasInterest: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { mutate: add } = api.intrests.addIntrest.useMutation({
|
const { mutate: add } = api.intrests.addIntrest.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await utils.intrests.getUserInterests.invalidate();
|
await utils.intrests.getUserInterests.invalidate();
|
||||||
toast.success("Richiesta di interesse inviata con successo!");
|
toast.success("Richiesta di interesse inviata con successo!");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (hasInterest) {
|
if (hasInterest) {
|
||||||
return (
|
return (
|
||||||
<span className="flex w-full items-center justify-center gap-2 rounded-md bg-green-500 p-2 text-sm font-semibold text-white">
|
<span className="flex w-full items-center justify-center gap-2 rounded-md bg-green-500 p-2 text-sm font-semibold text-white">
|
||||||
<BadgePlus className="inline size-5" />
|
<BadgePlus className="inline size-5" />
|
||||||
Hai già mandato una richiesta
|
Hai già mandato una richiesta
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className="flex w-full items-center gap-2"
|
className="flex w-full items-center gap-2"
|
||||||
variant="info"
|
variant="info"
|
||||||
//size="xl"
|
//size="xl"
|
||||||
aria-label="Sono interessato"
|
aria-label="Sono interessato"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
add({
|
add({
|
||||||
annuncioId,
|
annuncioId,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BadgePlus className="size-6" />
|
<BadgePlus className="size-6" />
|
||||||
Sono interessato
|
Sono interessato
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,41 @@
|
||||||
import { useAnnuncio } from "~/providers/AnnuncioProvider";
|
|
||||||
import {
|
|
||||||
Credenza,
|
|
||||||
CredenzaBody,
|
|
||||||
CredenzaContent,
|
|
||||||
CredenzaDescription,
|
|
||||||
CredenzaHeader,
|
|
||||||
CredenzaTitle,
|
|
||||||
CredenzaTrigger,
|
|
||||||
} from "~/components/custom_ui/credenza";
|
|
||||||
import { FormContattoAnnucio } from "~/forms/FormContattoAnnuncio";
|
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import { MessageCircleQuestion } from "lucide-react";
|
import { MessageCircleQuestion } from "lucide-react";
|
||||||
|
import {
|
||||||
|
Credenza,
|
||||||
|
CredenzaBody,
|
||||||
|
CredenzaContent,
|
||||||
|
CredenzaDescription,
|
||||||
|
CredenzaHeader,
|
||||||
|
CredenzaTitle,
|
||||||
|
CredenzaTrigger,
|
||||||
|
} from "~/components/custom_ui/credenza";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { FormContattoAnnucio } from "~/forms/FormContattoAnnuncio";
|
||||||
|
import { useAnnuncio } from "~/providers/AnnuncioProvider";
|
||||||
|
|
||||||
export const ContattoAnnuncio = () => {
|
export const ContattoAnnuncio = () => {
|
||||||
const { codice } = useAnnuncio();
|
const { codice } = useAnnuncio();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Credenza>
|
<Credenza>
|
||||||
<CredenzaTrigger asChild>
|
<CredenzaTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
className="flex w-full items-center gap-2"
|
className="flex w-full items-center gap-2"
|
||||||
>
|
>
|
||||||
<MessageCircleQuestion className="size-6" /> Contattaci
|
<MessageCircleQuestion className="size-6" /> Contattaci
|
||||||
</Button>
|
</Button>
|
||||||
</CredenzaTrigger>
|
</CredenzaTrigger>
|
||||||
<CredenzaContent className="max-h-[90vh]">
|
<CredenzaContent className="max-h-[90vh]">
|
||||||
<CredenzaHeader>
|
<CredenzaHeader>
|
||||||
<CredenzaTitle>Contattaci per avere informazioni</CredenzaTitle>
|
<CredenzaTitle>Contattaci per avere informazioni</CredenzaTitle>
|
||||||
<CredenzaDescription>
|
<CredenzaDescription>
|
||||||
Ti contatteremo il prima possibile per darti più informazioni
|
Ti contatteremo il prima possibile per darti più informazioni
|
||||||
</CredenzaDescription>
|
</CredenzaDescription>
|
||||||
</CredenzaHeader>
|
</CredenzaHeader>
|
||||||
<CredenzaBody className="max-h-[80vh] overflow-auto pb-5">
|
<CredenzaBody className="max-h-[80vh] overflow-auto pb-5">
|
||||||
<FormContattoAnnucio codice={codice} />
|
<FormContattoAnnucio codice={codice} />
|
||||||
</CredenzaBody>
|
</CredenzaBody>
|
||||||
</CredenzaContent>
|
</CredenzaContent>
|
||||||
</Credenza>
|
</Credenza>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,307 +1,304 @@
|
||||||
import Link from "next/link";
|
|
||||||
import {
|
|
||||||
Carousel,
|
|
||||||
CarouselContent,
|
|
||||||
CarouselItem,
|
|
||||||
CarouselNext,
|
|
||||||
CarouselPrevious,
|
|
||||||
} from "~/components/ui/carousel";
|
|
||||||
import { cn, formatCurrency } from "~/lib/utils";
|
|
||||||
import { BedDouble, CalendarClock, Maximize2, Ruler } from "lucide-react";
|
import { BedDouble, CalendarClock, Maximize2, Ruler } from "lucide-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { ImageFlbk } from "~/components/ImageWithFallback";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Carousel,
|
||||||
DialogContent,
|
CarouselContent,
|
||||||
DialogDescription,
|
CarouselItem,
|
||||||
DialogHeader,
|
CarouselNext,
|
||||||
DialogTitle,
|
CarouselPrevious,
|
||||||
|
} from "~/components/ui/carousel";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
} from "~/components/ui/dialog";
|
} from "~/components/ui/dialog";
|
||||||
import { camereTesti, handleConsegna } from "~/lib/annuncio_details";
|
import { camereTesti, handleConsegna } from "~/lib/annuncio_details";
|
||||||
import { ImageFlbk } from "~/components/ImageWithFallback";
|
import { cn, formatCurrency } from "~/lib/utils";
|
||||||
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
|
|
||||||
type CardAnnuncioProps = {
|
type CardAnnuncioProps = {
|
||||||
id: number;
|
id: number;
|
||||||
codice: string;
|
codice: string;
|
||||||
prezzo: number;
|
prezzo: number;
|
||||||
titolo: string | null;
|
titolo: string | null;
|
||||||
mq: string | null;
|
mq: string | null;
|
||||||
comune: string | null;
|
comune: string | null;
|
||||||
provincia: string | null;
|
provincia: string | null;
|
||||||
consegna: number | null;
|
consegna: number | null;
|
||||||
camere: number | null;
|
camere: number | null;
|
||||||
immagini: string[] | undefined;
|
immagini: string[] | undefined;
|
||||||
tipo: string | null;
|
tipo: string | null;
|
||||||
stato: string | null;
|
stato: string | null;
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CardAnnuncio = ({
|
export const CardAnnuncio = ({
|
||||||
id,
|
id,
|
||||||
codice,
|
codice,
|
||||||
prezzo,
|
prezzo,
|
||||||
titolo,
|
titolo,
|
||||||
mq,
|
mq,
|
||||||
comune,
|
comune,
|
||||||
provincia,
|
provincia,
|
||||||
consegna,
|
consegna,
|
||||||
camere,
|
camere,
|
||||||
immagini,
|
immagini,
|
||||||
tipo,
|
tipo,
|
||||||
stato,
|
stato,
|
||||||
className,
|
className,
|
||||||
}: CardAnnuncioProps) => {
|
}: CardAnnuncioProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-[464px] rounded-xl bg-white shadow-sm shadow-neutral-100 outline outline-neutral-200 hover:shadow-md hover:outline-neutral-300",
|
"h-[464px] rounded-xl bg-white shadow-sm shadow-neutral-100 outline outline-neutral-200 hover:shadow-md hover:outline-neutral-300",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
id={`card-annuncio-${id}`}
|
id={`card-annuncio-${id}`}
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
href={`/annuncio/${codice}`}
|
href={`/annuncio/${codice}`}
|
||||||
//target="_blank"
|
//target="_blank"
|
||||||
className="block p-2" //duration-700 ease-in-out animate-in fade-in
|
className="block p-2" //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" && (
|
||||||
<div>
|
<div>
|
||||||
<div className="absolute z-20 h-56 w-full">
|
<div className="absolute z-20 h-56 w-full">
|
||||||
<div className="absolute bottom-0 w-full touch-none bg-violet-500 text-center text-lg text-white select-none">
|
<div className="absolute bottom-0 w-full touch-none bg-violet-500 text-center text-lg text-white select-none">
|
||||||
Annuncio in Trattativa
|
Annuncio in Trattativa
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Carousel opts={{ loop: true }}>
|
<Carousel opts={{ loop: true }}>
|
||||||
<CarouselContent>
|
<CarouselContent>
|
||||||
{immagini &&
|
{immagini?.map((img, idx) => (
|
||||||
immagini.map((img, idx) => (
|
<CarouselItem key={`${img}-${idx}`}>
|
||||||
<CarouselItem key={`${img}-${idx}`}>
|
<ImageFlbk
|
||||||
<ImageFlbk
|
alt={t.card.alt_immagine}
|
||||||
alt={t.card.alt_immagine}
|
width={800}
|
||||||
width={800}
|
height={400}
|
||||||
height={400}
|
src={img}
|
||||||
src={img}
|
priority={idx === 0}
|
||||||
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"
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</CarouselItem>
|
||||||
</CarouselItem>
|
))}
|
||||||
))}
|
</CarouselContent>
|
||||||
</CarouselContent>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
onKeyDown={(e) => e.stopPropagation()}
|
onKeyDown={(e) => e.stopPropagation()}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={(e) => e.preventDefault()}
|
onClick={(e) => e.preventDefault()}
|
||||||
className="block opacity-0 transition-all duration-200 group-hover:opacity-60 disabled:group-hover:opacity-0"
|
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"
|
||||||
disabled={!immagini || immagini.length === 1}
|
disabled={!immagini || immagini.length === 1}
|
||||||
/>
|
/>
|
||||||
<CarouselNext
|
<CarouselNext
|
||||||
className="right-2 cursor-pointer"
|
className="right-2 cursor-pointer"
|
||||||
disabled={!immagini || immagini.length === 1}
|
disabled={!immagini || immagini.length === 1}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Carousel>
|
</Carousel>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"mt-2 flex w-full items-center justify-center rounded-md",
|
"mt-2 flex w-full items-center justify-center rounded-md",
|
||||||
tipo === "Transitorio" && "bg-transitorio",
|
tipo === "Transitorio" && "bg-transitorio",
|
||||||
tipo === "Stabile" && "bg-stabile",
|
tipo === "Stabile" && "bg-stabile",
|
||||||
tipo === "Cessione" && "bg-blue-400",
|
tipo === "Cessione" && "bg-blue-400",
|
||||||
tipo === "Vendita" && "bg-green-400",
|
tipo === "Vendita" && "bg-green-400",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="font-semibold text-white">{tipo}</div>
|
<div className="font-semibold text-white">{tipo}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 space-y-2">
|
<div className="mt-2 space-y-2">
|
||||||
<div className="flex flex-row justify-around">
|
<div className="flex flex-row justify-around">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<span className="sr-only">{t.card.codice}</span>
|
<span className="sr-only">{t.card.codice}</span>
|
||||||
|
|
||||||
<span className="text-xl font-semibold text-red-600">
|
<span className="text-xl font-semibold text-red-600">
|
||||||
Cod: {codice}
|
Cod: {codice}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<span className="sr-only">{t.card.prezzo}</span>
|
<span className="sr-only">{t.card.prezzo}</span>
|
||||||
|
|
||||||
<span className="text-xl font-semibold">
|
<span className="text-xl font-semibold">
|
||||||
{formatCurrency(prezzo / 1e2)}
|
{formatCurrency(prezzo / 1e2)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-muted-foreground mb-3 text-center">
|
<div className="text-muted-foreground mb-3 text-center">
|
||||||
<span className="sr-only">{t.card.titolo}</span>
|
<span className="sr-only">{t.card.titolo}</span>
|
||||||
|
|
||||||
<span className="line-clamp-2 min-h-[3.5rem] text-base font-medium md:text-lg">
|
<span className="line-clamp-2 min-h-[3.5rem] text-base font-medium md:text-lg">
|
||||||
{titolo}
|
{titolo}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground text-center">
|
<div className="text-muted-foreground text-center">
|
||||||
<span className="sr-only">{t.card.indirizzo}</span>
|
<span className="sr-only">{t.card.indirizzo}</span>
|
||||||
|
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{comune && provincia && `${comune} (${provincia})`}
|
{comune && provincia && `${comune} (${provincia})`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-muted-foreground mt-4 flex items-center justify-around gap-2 text-xs">
|
<div className="text-muted-foreground mt-4 flex items-center justify-around gap-2 text-xs">
|
||||||
<div className="flex w-full items-center justify-center gap-1 rounded-md bg-neutral-100 py-3">
|
<div className="flex w-full items-center justify-center gap-1 rounded-md bg-neutral-100 py-3">
|
||||||
<Ruler className="size-4 text-indigo-700" />
|
<Ruler className="size-4 text-indigo-700" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
{mq ? Number.parseFloat(mq) : 0}{" "}
|
{mq ? Number.parseFloat(mq) : 0}{" "}
|
||||||
<span>
|
<span>
|
||||||
m<sup>2</sup>
|
m<sup>2</sup>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full items-center justify-center gap-1 rounded-md bg-neutral-100 py-3">
|
<div className="flex w-full items-center justify-center gap-1 rounded-md bg-neutral-100 py-3">
|
||||||
<BedDouble className="size-4 text-indigo-700" />
|
<BedDouble className="size-4 text-indigo-700" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
{camereTesti({ camere: camere, testi: t.card })}
|
{camereTesti({ camere: camere, testi: t.card })}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full items-center justify-center gap-1 rounded-md bg-neutral-100 py-3">
|
<div className="flex w-full items-center justify-center gap-1 rounded-md bg-neutral-100 py-3">
|
||||||
<CalendarClock className="size-4 text-indigo-700" />
|
<CalendarClock className="size-4 text-indigo-700" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="truncate font-medium">
|
<p className="truncate font-medium">
|
||||||
{handleConsegna({
|
{handleConsegna({
|
||||||
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,
|
aggiornamento: t.card.in_aggiornamento,
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CarouselAnnuncio = ({
|
export const CarouselAnnuncio = ({
|
||||||
single,
|
single,
|
||||||
immagini,
|
immagini,
|
||||||
}: {
|
}: {
|
||||||
single?: boolean;
|
single?: boolean;
|
||||||
immagini: string[] | null;
|
immagini: string[] | null;
|
||||||
}) => {
|
}) => {
|
||||||
const [openModal, setOpenModal] = useState(false);
|
const [openModal, setOpenModal] = useState(false);
|
||||||
const [idxModal, setIdxModal] = useState(0);
|
const [idxModal, setIdxModal] = useState(0);
|
||||||
function handleOpenModal(position: number) {
|
function handleOpenModal(position: number) {
|
||||||
setIdxModal(position);
|
setIdxModal(position);
|
||||||
setOpenModal(true);
|
setOpenModal(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<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={{ loop: true, align: "start" }}>
|
||||||
<CarouselContent>
|
<CarouselContent>
|
||||||
{immagini &&
|
{immagini?.map((img, idx) => (
|
||||||
immagini.map((img, idx) => (
|
<CarouselItem
|
||||||
<CarouselItem
|
key={`${img}-${idx}-cF`}
|
||||||
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",
|
)}
|
||||||
)}
|
>
|
||||||
>
|
<ImageFlbk
|
||||||
<ImageFlbk
|
priority={idx === 0}
|
||||||
priority={idx === 0}
|
alt={img}
|
||||||
alt={img}
|
width={800}
|
||||||
width={800}
|
height={400}
|
||||||
height={400}
|
onClick={() => handleOpenModal(idx)}
|
||||||
onClick={() => handleOpenModal(idx)}
|
src={img}
|
||||||
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",
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
<Maximize2
|
||||||
<Maximize2
|
onClick={() => handleOpenModal(idx)}
|
||||||
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"
|
/>
|
||||||
/>
|
</CarouselItem>
|
||||||
</CarouselItem>
|
))}
|
||||||
))}
|
</CarouselContent>
|
||||||
</CarouselContent>
|
|
||||||
|
|
||||||
<CarouselPrevious
|
<CarouselPrevious
|
||||||
type="button"
|
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}
|
||||||
/>
|
/>
|
||||||
<CarouselNext
|
<CarouselNext
|
||||||
type="button"
|
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}
|
||||||
/>
|
/>
|
||||||
</Carousel>
|
</Carousel>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Dialog open={openModal} onOpenChange={setOpenModal}>
|
<Dialog open={openModal} onOpenChange={setOpenModal}>
|
||||||
<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>
|
||||||
<DialogDescription className="sr-only">Immagine</DialogDescription>
|
<DialogDescription className="sr-only">Immagine</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<Carousel opts={{ loop: true, startIndex: idxModal }}>
|
<Carousel opts={{ loop: true, startIndex: idxModal }}>
|
||||||
<CarouselContent>
|
<CarouselContent>
|
||||||
{immagini &&
|
{immagini?.map((img, idx) => (
|
||||||
immagini.map((img, idx) => (
|
<CarouselItem key={`${img}-${idx}-cD`}>
|
||||||
<CarouselItem key={`${img}-${idx}-cD`}>
|
<Image
|
||||||
<Image
|
alt={`carousel-img-${idx}`}
|
||||||
alt={`carousel-img-${idx}`}
|
width={1920}
|
||||||
width={1920}
|
height={1080}
|
||||||
height={1080}
|
src={img}
|
||||||
src={img}
|
className="mx-auto h-[90vh] w-full rounded-md object-contain sm:w-[80vw]"
|
||||||
className="mx-auto h-[90vh] w-full rounded-md object-contain sm:w-[80vw]"
|
/>
|
||||||
/>
|
</CarouselItem>
|
||||||
</CarouselItem>
|
))}
|
||||||
))}
|
</CarouselContent>
|
||||||
</CarouselContent>
|
|
||||||
|
|
||||||
<CarouselPrevious
|
<CarouselPrevious
|
||||||
type="button"
|
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}
|
||||||
/>
|
/>
|
||||||
<CarouselNext
|
<CarouselNext
|
||||||
type="button"
|
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}
|
||||||
/>
|
/>
|
||||||
</Carousel>
|
</Carousel>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,383 +1,383 @@
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import {
|
||||||
|
Download,
|
||||||
|
File,
|
||||||
|
FileArchive,
|
||||||
|
FileAudio,
|
||||||
|
FileCode,
|
||||||
|
FileSpreadsheet,
|
||||||
|
FileText,
|
||||||
|
FileVideo,
|
||||||
|
ImageIcon,
|
||||||
|
MoreVertical,
|
||||||
|
Pencil,
|
||||||
|
Trash2,
|
||||||
|
TriangleAlert,
|
||||||
|
} from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { type FormEvent, useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
import Input from "~/components/custom_ui/input";
|
||||||
|
import { LoadingPage } from "~/components/loading";
|
||||||
|
import { Status500 } from "~/components/status-page";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "~/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "~/components/ui/dropdown-menu";
|
||||||
|
import { Separator } from "~/components/ui/separator";
|
||||||
|
import { UploadModal } from "~/components/upload_modal";
|
||||||
|
import { handleDownload } from "~/hooks/filesHooks";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
import type {
|
import type {
|
||||||
Storageindex,
|
Storageindex,
|
||||||
StorageindexId,
|
StorageindexId,
|
||||||
} from "~/schemas/public/Storageindex";
|
} from "~/schemas/public/Storageindex";
|
||||||
import type { UsersId } from "~/schemas/public/Users";
|
import type { UsersId } from "~/schemas/public/Users";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { LoadingPage } from "~/components/loading";
|
|
||||||
import { Status500 } from "~/components/status-page";
|
|
||||||
import { Separator } from "~/components/ui/separator";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import {
|
|
||||||
Download,
|
|
||||||
File,
|
|
||||||
FileArchive,
|
|
||||||
FileAudio,
|
|
||||||
FileCode,
|
|
||||||
FileSpreadsheet,
|
|
||||||
FileText,
|
|
||||||
FileVideo,
|
|
||||||
ImageIcon,
|
|
||||||
MoreVertical,
|
|
||||||
Pencil,
|
|
||||||
Trash2,
|
|
||||||
TriangleAlert,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useState, type FormEvent } from "react";
|
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
|
||||||
import Input from "~/components/custom_ui/input";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "~/components/ui/dialog";
|
|
||||||
import { handleDownload } from "~/hooks/filesHooks";
|
|
||||||
import { UploadModal } from "~/components/upload_modal";
|
|
||||||
import { format } from "date-fns";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "~/components/ui/dropdown-menu";
|
|
||||||
|
|
||||||
export const AllegatiComp = ({
|
export const AllegatiComp = ({
|
||||||
userId,
|
userId,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
}: {
|
}: {
|
||||||
userId: UsersId;
|
userId: UsersId;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data: allegati, isLoading: isLoadingAllegati } =
|
const { data: allegati, isLoading: isLoadingAllegati } =
|
||||||
api.storage.getUserStorage.useQuery({ userId: userId });
|
api.storage.getUserStorage.useQuery({ userId: userId });
|
||||||
|
|
||||||
const { mutateAsync: getToken } = api.storage.getStorageToken.useMutation();
|
const { mutateAsync: getToken } = api.storage.getStorageToken.useMutation();
|
||||||
|
|
||||||
const { mutate: deleteFile } = api.storage.deleteUserStorage.useMutation({
|
const { mutate: deleteFile } = api.storage.deleteUserStorage.useMutation({
|
||||||
onMutate: () => {
|
onMutate: () => {
|
||||||
const toastId = toast.loading(t.caricamento, {
|
const toastId = toast.loading(t.caricamento, {
|
||||||
icon: "🪛",
|
icon: "🪛",
|
||||||
});
|
});
|
||||||
return { toastId };
|
return { toastId };
|
||||||
},
|
},
|
||||||
onSuccess: async (_error, _variables, context) => {
|
onSuccess: async (_error, _variables, context) => {
|
||||||
await utils.storage.getUserStorage.invalidate({
|
await utils.storage.getUserStorage.invalidate({
|
||||||
userId: userId,
|
userId: userId,
|
||||||
});
|
});
|
||||||
toast.success(t.allegati.allegato_rimosso, {
|
toast.success(t.allegati.allegato_rimosso, {
|
||||||
icon: "🗑️",
|
icon: "🗑️",
|
||||||
id: context?.toastId,
|
id: context?.toastId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutate: renameFile } = api.storage.renameFile.useMutation({
|
const { mutate: renameFile } = api.storage.renameFile.useMutation({
|
||||||
onMutate: () => {
|
onMutate: () => {
|
||||||
setEditOpen(false);
|
setEditOpen(false);
|
||||||
const toastId = toast.loading(t.caricamento, {
|
const toastId = toast.loading(t.caricamento, {
|
||||||
icon: "🪛",
|
icon: "🪛",
|
||||||
});
|
});
|
||||||
return { toastId };
|
return { toastId };
|
||||||
},
|
},
|
||||||
onSuccess: async (_error, _variables, context) => {
|
onSuccess: async (_error, _variables, context) => {
|
||||||
await utils.storage.getUserStorage.invalidate({
|
await utils.storage.getUserStorage.invalidate({
|
||||||
userId: userId,
|
userId: userId,
|
||||||
});
|
});
|
||||||
toast.success(t.successo, {
|
toast.success(t.successo, {
|
||||||
icon: "👍",
|
icon: "👍",
|
||||||
id: context?.toastId,
|
id: context?.toastId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const [editData, setEditData] = useState<{
|
const [editData, setEditData] = useState<{
|
||||||
id: StorageindexId;
|
id: StorageindexId;
|
||||||
filename: string;
|
filename: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [editOpen, setEditOpen] = useState(false);
|
const [editOpen, setEditOpen] = useState(false);
|
||||||
|
|
||||||
const handleEditSubmit = (e: FormEvent<HTMLFormElement>) => {
|
const handleEditSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const form = e.currentTarget;
|
const form = e.currentTarget;
|
||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
if (!formData.get("id") || !formData.get("filename")) {
|
if (!formData.get("id") || !formData.get("filename")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
renameFile({
|
renameFile({
|
||||||
fileId: formData.get("id") as StorageindexId,
|
fileId: formData.get("id") as StorageindexId,
|
||||||
name: formData.get("filename") as string,
|
name: formData.get("filename") as string,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
if (isLoadingAllegati) return <LoadingPage />;
|
if (isLoadingAllegati) return <LoadingPage />;
|
||||||
if (!allegati) return <Status500 />;
|
if (!allegati) return <Status500 />;
|
||||||
|
|
||||||
const from_admin = allegati.filter((a) => a.from_admin);
|
const from_admin = allegati.filter((a) => a.from_admin);
|
||||||
const not_from_admin = allegati.filter((a) => !a.from_admin);
|
const not_from_admin = allegati.filter((a) => !a.from_admin);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col space-y-5">
|
<div className="flex flex-col space-y-5">
|
||||||
<h1 className="text-primary text-2xl font-bold">{t.allegati.title}</h1>
|
<h1 className="text-primary text-2xl font-bold">{t.allegati.title}</h1>
|
||||||
<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}
|
files={from_admin}
|
||||||
isAdmin={isAdmin}
|
isAdmin={isAdmin}
|
||||||
selectHandler={(id, filename) => {
|
selectHandler={(id, filename) => {
|
||||||
setEditData({ id, filename });
|
setEditData({ id, filename });
|
||||||
setEditOpen(true);
|
setEditOpen(true);
|
||||||
}}
|
}}
|
||||||
delete_onClick={(id) => deleteFile({ userId, fileId: id })}
|
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(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Separator />
|
<Separator />
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<UploadModal userId={userId} isAdmin={isAdmin} />{" "}
|
<UploadModal userId={userId} isAdmin={isAdmin} />{" "}
|
||||||
<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">
|
||||||
<div className="flex items-center gap-2 text-orange-500">
|
<div className="flex items-center gap-2 text-orange-500">
|
||||||
<TriangleAlert className="size-5" />
|
<TriangleAlert className="size-5" />
|
||||||
{t.importante}:
|
{t.importante}:
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm">{t.allegati.disclamer}</span>
|
<span className="text-sm">{t.allegati.disclamer}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{not_from_admin && (
|
{not_from_admin && (
|
||||||
<FileList
|
<FileList
|
||||||
files={not_from_admin}
|
files={not_from_admin}
|
||||||
isAdmin={isAdmin}
|
isAdmin={isAdmin}
|
||||||
selectHandler={(id, filename) => {
|
selectHandler={(id, filename) => {
|
||||||
setEditData({ id, filename });
|
setEditData({ id, filename });
|
||||||
setEditOpen(true);
|
setEditOpen(true);
|
||||||
}}
|
}}
|
||||||
delete_onClick={(id) => deleteFile({ userId, fileId: id })}
|
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(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<EditFileDialog
|
<EditFileDialog
|
||||||
open={editOpen}
|
open={editOpen}
|
||||||
onOpenChange={setEditOpen}
|
onOpenChange={setEditOpen}
|
||||||
data={editData}
|
data={editData}
|
||||||
handleEditSubmit={handleEditSubmit}
|
handleEditSubmit={handleEditSubmit}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ExtIcon = ({
|
export const ExtIcon = ({
|
||||||
ext,
|
ext,
|
||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
ext: string | null | undefined;
|
ext: string | null | undefined;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) => {
|
}) => {
|
||||||
if (!ext) return <File className={cn("size-5 text-gray-500", className)} />;
|
if (!ext) return <File className={cn("size-5 text-gray-500", className)} />;
|
||||||
if (ext.includes("pdf")) {
|
if (ext.includes("pdf")) {
|
||||||
return <FileText className={cn("size-5 text-red-500", className)} />;
|
return <FileText className={cn("size-5 text-red-500", className)} />;
|
||||||
} else if (
|
} else if (
|
||||||
ext.includes("image") ||
|
ext.includes("image") ||
|
||||||
["jpg", "jpeg", "png", "gif", "svg", "webp"].some((ext) =>
|
["jpg", "jpeg", "png", "gif", "svg", "webp"].some((ext) =>
|
||||||
ext.includes(ext),
|
ext.includes(ext),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return <ImageIcon className={cn("size-5 text-blue-500", className)} />;
|
return <ImageIcon className={cn("size-5 text-blue-500", className)} />;
|
||||||
} else if (
|
} else if (
|
||||||
["html", "css", "js", "jsx", "ts", "tsx", "json", "xml"].some((ext) =>
|
["html", "css", "js", "jsx", "ts", "tsx", "json", "xml"].some((ext) =>
|
||||||
ext.includes(ext),
|
ext.includes(ext),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return <FileCode className={cn("size-5 text-emerald-500", className)} />;
|
return <FileCode className={cn("size-5 text-emerald-500", className)} />;
|
||||||
} else if (["xls", "xlsx", "csv"].some((ext) => ext.includes(ext))) {
|
} else if (["xls", "xlsx", "csv"].some((ext) => ext.includes(ext))) {
|
||||||
return (
|
return (
|
||||||
<FileSpreadsheet className={cn("size-5 text-green-600", className)} />
|
<FileSpreadsheet className={cn("size-5 text-green-600", className)} />
|
||||||
);
|
);
|
||||||
} else if (
|
} else if (
|
||||||
["zip", "rar", "7z", "tar", "gz"].some((ext) => ext.includes(ext))
|
["zip", "rar", "7z", "tar", "gz"].some((ext) => ext.includes(ext))
|
||||||
) {
|
) {
|
||||||
return <FileArchive className={cn("size-5 text-amber-600", className)} />;
|
return <FileArchive className={cn("size-5 text-amber-600", className)} />;
|
||||||
} else if (["mp3", "wav", "ogg", "flac"].some((ext) => ext.includes(ext))) {
|
} else if (["mp3", "wav", "ogg", "flac"].some((ext) => ext.includes(ext))) {
|
||||||
return <FileAudio className={cn("size-5 text-purple-500", className)} />;
|
return <FileAudio className={cn("size-5 text-purple-500", className)} />;
|
||||||
} else if (
|
} else if (
|
||||||
["mp4", "avi", "mov", "wmv", "webm"].some((ext) => ext.includes(ext))
|
["mp4", "avi", "mov", "wmv", "webm"].some((ext) => ext.includes(ext))
|
||||||
) {
|
) {
|
||||||
return <FileVideo className={cn("size-5 text-pink-500", className)} />;
|
return <FileVideo className={cn("size-5 text-pink-500", className)} />;
|
||||||
} else {
|
} else {
|
||||||
return <File className={cn("size-5 text-gray-500", className)} />;
|
return <File className={cn("size-5 text-gray-500", className)} />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const EditFileDialog = ({
|
const EditFileDialog = ({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
data,
|
data,
|
||||||
handleEditSubmit,
|
handleEditSubmit,
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (status: boolean) => void;
|
onOpenChange: (status: boolean) => void;
|
||||||
data: {
|
data: {
|
||||||
id: StorageindexId;
|
id: StorageindexId;
|
||||||
filename: string;
|
filename: string;
|
||||||
} | null;
|
} | null;
|
||||||
handleEditSubmit: (e: FormEvent<HTMLFormElement>) => void;
|
handleEditSubmit: (e: FormEvent<HTMLFormElement>) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<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 onSubmit={handleEditSubmit} method="post">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<label htmlFor="id" className="sr-only">
|
<label htmlFor="id" className="sr-only">
|
||||||
ID
|
ID
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
className="hidden"
|
className="hidden"
|
||||||
name="id"
|
name="id"
|
||||||
value={data.id}
|
value={data.id}
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
<label htmlFor="filename" className="sr-only">
|
<label htmlFor="filename" className="sr-only">
|
||||||
filename
|
filename
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Modifica messaggio"
|
placeholder="Modifica messaggio"
|
||||||
className="mt-0"
|
className="mt-0"
|
||||||
id="filename"
|
id="filename"
|
||||||
name="filename"
|
name="filename"
|
||||||
defaultValue={data.filename || undefined}
|
defaultValue={data.filename || undefined}
|
||||||
/>
|
/>
|
||||||
<Button type="submit">{t.salva}</Button>
|
<Button type="submit">{t.salva}</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface FileListProps {
|
interface FileListProps {
|
||||||
files: Storageindex[];
|
files: Storageindex[];
|
||||||
delete_onClick: (id: StorageindexId) => void;
|
delete_onClick: (id: StorageindexId) => void;
|
||||||
download_onClick: (file: Storageindex) => void;
|
download_onClick: (file: Storageindex) => void;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
selectHandler: (id: StorageindexId, filename: string) => void;
|
selectHandler: (id: StorageindexId, filename: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function FileList({
|
function FileList({
|
||||||
files,
|
files,
|
||||||
delete_onClick,
|
delete_onClick,
|
||||||
download_onClick,
|
download_onClick,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
selectHandler,
|
selectHandler,
|
||||||
}: FileListProps) {
|
}: FileListProps) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="rounded-md border">
|
<div className="rounded-md border">
|
||||||
<div className="bg-muted/50 text-muted-foreground grid grid-cols-12 gap-2 p-4 text-sm font-medium">
|
<div className="bg-muted/50 text-muted-foreground grid grid-cols-12 gap-2 p-4 text-sm font-medium">
|
||||||
<div className="col-span-6 md:col-span-8">File</div>
|
<div className="col-span-6 md:col-span-8">File</div>
|
||||||
<div className="col-span-4 md:col-span-3">Data</div>
|
<div className="col-span-4 md:col-span-3">Data</div>
|
||||||
<div className="col-span-2 text-right md:col-span-1">Azioni</div>
|
<div className="col-span-2 text-right md:col-span-1">Azioni</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="divide-y">
|
<div className="divide-y">
|
||||||
{files.map((file) => (
|
{files.map((file) => (
|
||||||
<div
|
<div
|
||||||
key={file.id}
|
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"
|
||||||
>
|
>
|
||||||
<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">
|
||||||
<ExtIcon ext={file.ext} />
|
<ExtIcon ext={file.ext} />
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
aria-label="Allegato"
|
aria-label="Allegato"
|
||||||
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"
|
className="text-foreground truncate font-medium hover:underline"
|
||||||
>
|
>
|
||||||
{file.filename}
|
{file.filename}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-muted-foreground col-span-4 md:col-span-3">
|
<div className="text-muted-foreground col-span-4 md:col-span-3">
|
||||||
{format(file.created_at, "d MMM yyyy")}
|
{format(file.created_at, "d MMM yyyy")}
|
||||||
</div>
|
</div>
|
||||||
<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"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
asChild
|
asChild
|
||||||
className="size-8"
|
className="size-8"
|
||||||
title="Download"
|
title="Download"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="flex gap-1 text-sm"
|
className="flex gap-1 text-sm"
|
||||||
onClick={() => download_onClick(file)}
|
onClick={() => download_onClick(file)}
|
||||||
>
|
>
|
||||||
<Download className="size-4" />
|
<Download className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" className="size-8">
|
<Button variant="ghost" size="icon" className="size-8">
|
||||||
<MoreVertical className="size-4" />
|
<MoreVertical className="size-4" />
|
||||||
<span className="sr-only">More options</span>
|
<span className="sr-only">More options</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
aria-label="Rinomina"
|
aria-label="Rinomina"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
selectHandler(file.id, file.filename || "")
|
selectHandler(file.id, file.filename || "")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Pencil className="mr-2 size-4" />
|
<Pencil className="mr-2 size-4" />
|
||||||
Rinomina
|
Rinomina
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
aria-label="Elimina"
|
aria-label="Elimina"
|
||||||
onClick={() => delete_onClick(file.id)}
|
onClick={() => delete_onClick(file.id)}
|
||||||
className="text-destructive focus:text-destructive"
|
className="text-destructive focus:text-destructive"
|
||||||
>
|
>
|
||||||
<Trash2 className="mr-2 size-4" />
|
<Trash2 className="mr-2 size-4" />
|
||||||
Elimina
|
Elimina
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,63 +1,63 @@
|
||||||
import type { UsersId } from "~/schemas/public/Users";
|
import { useState } from "react";
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionContent,
|
AccordionContent,
|
||||||
AccordionItem,
|
AccordionItem,
|
||||||
AccordionTrigger,
|
AccordionTrigger,
|
||||||
} from "~/components/ui/accordion";
|
} from "~/components/ui/accordion";
|
||||||
import { useMediaQuery } from "~/hooks/use-media-query";
|
import { useMediaQuery } from "~/hooks/use-media-query";
|
||||||
import { useState } from "react";
|
import type { UsersId } from "~/schemas/public/Users";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
export const EmailAccordion = ({ userId }: { userId: UsersId }) => {
|
export const EmailAccordion = ({ userId }: { userId: UsersId }) => {
|
||||||
const { data } = api.comunicazioni.getEmails.useQuery({
|
const { data } = api.comunicazioni.getEmails.useQuery({
|
||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Track which accordion item is open
|
// Track which accordion item is open
|
||||||
const [openIndex, setOpenIndex] = useState<string | undefined>(undefined);
|
const [openIndex, setOpenIndex] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
const mobile = useMediaQuery("(max-width: 768px)");
|
const mobile = useMediaQuery("(max-width: 768px)");
|
||||||
if (!data || data.length === 0) {
|
if (!data || data.length === 0) {
|
||||||
return <div className="text-center">Nessuna comunicazione trovata</div>;
|
return <div className="text-center">Nessuna comunicazione trovata</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto py-4">
|
<div className="mx-auto py-4">
|
||||||
<Accordion
|
<Accordion
|
||||||
type="single"
|
type="single"
|
||||||
collapsible
|
collapsible
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
value={openIndex}
|
value={openIndex}
|
||||||
onValueChange={setOpenIndex}
|
onValueChange={setOpenIndex}
|
||||||
>
|
>
|
||||||
{data.map((email, index) => (
|
{data.map((email, index) => (
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
value={String(index)}
|
value={String(index)}
|
||||||
key={index}
|
key={index}
|
||||||
className="space-y-2 border-none"
|
className="space-y-2 border-none"
|
||||||
>
|
>
|
||||||
<AccordionTrigger className="rounded-md bg-muted px-4 py-2">
|
<AccordionTrigger className="rounded-md bg-muted px-4 py-2">
|
||||||
Email “{email.title}” del{" "}
|
Email “{email.title}” del{" "}
|
||||||
{new Date(email.created_at).toLocaleDateString("it-IT", {
|
{new Date(email.created_at).toLocaleDateString("it-IT", {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "2-digit",
|
month: "2-digit",
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
})}
|
})}
|
||||||
</AccordionTrigger>
|
</AccordionTrigger>
|
||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
{openIndex === String(index) && (
|
{openIndex === String(index) && (
|
||||||
<iframe
|
<iframe
|
||||||
title="Email Content"
|
title="Email Content"
|
||||||
srcDoc={email.html}
|
srcDoc={email.html}
|
||||||
height={mobile ? "600px" : "500px"}
|
height={mobile ? "600px" : "500px"}
|
||||||
width="100%"
|
width="100%"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
))}
|
))}
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,86 +1,86 @@
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import { LoadingPage } from "~/components/loading";
|
|
||||||
import type { UsersId } from "~/schemas/public/Users";
|
|
||||||
import { AccordionComp } from "~/components/accordionComp";
|
import { AccordionComp } from "~/components/accordionComp";
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
import { LoadingPage } from "~/components/loading";
|
||||||
import TimeserieBarChart from "~/components/timeserieBarChart";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "~/components/ui/card";
|
|
||||||
import TimeserieBarChartStacked from "~/components/timeserieBarChartStacked";
|
|
||||||
import { TabRicerca } from "~/components/tables/ricerca-table";
|
import { TabRicerca } from "~/components/tables/ricerca-table";
|
||||||
|
import TimeserieBarChart from "~/components/timeserieBarChart";
|
||||||
|
import TimeserieBarChartStacked from "~/components/timeserieBarChartStacked";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "~/components/ui/card";
|
||||||
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
|
import type { UsersId } from "~/schemas/public/Users";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
export const AdminDashboard = () => {
|
export const AdminDashboard = () => {
|
||||||
const { data: stats, isLoading } = api.stats.adminDashStats.useQuery();
|
const { data: stats, isLoading } = api.stats.adminDashStats.useQuery();
|
||||||
|
|
||||||
if (isLoading) return <LoadingPage />;
|
if (isLoading) return <LoadingPage />;
|
||||||
return (
|
return (
|
||||||
<div className="mx-1 flex-1 overflow-auto">
|
<div className="mx-1 flex-1 overflow-auto">
|
||||||
<div className="mx-auto pt-4 pb-8">
|
<div className="mx-auto pt-4 pb-8">
|
||||||
<div className="mx-3 items-start justify-between pb-3 md:flex">
|
<div className="mx-3 items-start justify-between pb-3 md:flex">
|
||||||
<div className="flex max-w-lg items-center gap-3">
|
<div className="flex max-w-lg items-center gap-3">
|
||||||
<h3 className="text-2xl font-bold">Admin Dashboard</h3>
|
<h3 className="text-2xl font-bold">Admin Dashboard</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!stats ? (
|
{!stats ? (
|
||||||
<div className="text-red-500">Errore caricamento statistiche</div>
|
<div className="text-red-500">Errore caricamento statistiche</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 gap-4 px-2 pt-4 sm:grid-cols-2 sm:px-4">
|
<div className="grid grid-cols-1 gap-4 px-2 pt-4 sm:grid-cols-2 sm:px-4">
|
||||||
<Card className="">
|
<Card className="">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Utenti</CardTitle>
|
<CardTitle>Utenti</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Numero totale di utenti registrati
|
Numero totale di utenti registrati
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="flex h-full items-center justify-center text-6xl font-bold">
|
<CardContent className="flex h-full items-center justify-center text-6xl font-bold">
|
||||||
{stats.userCount}
|
{stats.userCount}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<TimeserieBarChart
|
<TimeserieBarChart
|
||||||
title="Utenti registrati"
|
title="Utenti registrati"
|
||||||
timeserie={stats.userTimeserie}
|
timeserie={stats.userTimeserie}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TimeserieBarChartStacked
|
<TimeserieBarChartStacked
|
||||||
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}
|
timeserie={stats.serviziTimeserie.data}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TimeserieBarChart
|
<TimeserieBarChart
|
||||||
title="Contatti generati"
|
title="Contatti generati"
|
||||||
timeserie={stats.contattiTimeserie}
|
timeserie={stats.contattiTimeserie}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserDashboard = ({ userId }: { userId: UsersId }) => {
|
export const UserDashboard = ({ userId }: { userId: UsersId }) => {
|
||||||
// const { data: servizi, isLoading: loadingServizi } =
|
// const { data: servizi, isLoading: loadingServizi } =
|
||||||
// api.servizio.getAllServizioAnnunci.useQuery({ userId });
|
// api.servizio.getAllServizioAnnunci.useQuery({ userId });
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div className="mx-1 flex-1 overflow-auto">
|
<div className="mx-1 flex-1 overflow-auto">
|
||||||
<div className="mx-auto pt-4">
|
<div className="mx-auto pt-4">
|
||||||
<div className="mx-3 items-start justify-between md:flex">
|
<div className="mx-3 items-start justify-between md:flex">
|
||||||
<div className="flex max-w-lg items-center gap-3">
|
<div className="flex max-w-lg items-center gap-3">
|
||||||
<h3 className="text-2xl font-bold">Le tue ricerche</h3>
|
<h3 className="text-2xl font-bold">Le tue ricerche</h3>
|
||||||
</div>
|
</div>
|
||||||
</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 userId={userId} isAdmin={false} />
|
||||||
{/* {loadingServizi ? (
|
{/* {loadingServizi ? (
|
||||||
<LoadingPage />
|
<LoadingPage />
|
||||||
) : (
|
) : (
|
||||||
servizi?.map((servizio, idx) => (
|
servizi?.map((servizio, idx) => (
|
||||||
|
|
@ -92,12 +92,12 @@ export const UserDashboard = ({ userId }: { userId: UsersId }) => {
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
)} */}
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
<AccordionComp
|
<AccordionComp
|
||||||
texts={t.area_riservata.dash_accordion_minifaq}
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,214 +1,214 @@
|
||||||
|
import { CircleCheck, CircleSlash, ReceiptText, Trash2 } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { Confirm } from "~/components/confirm";
|
||||||
import {
|
import {
|
||||||
Credenza,
|
Credenza,
|
||||||
CredenzaContent,
|
CredenzaBody,
|
||||||
CredenzaHeader,
|
CredenzaClose,
|
||||||
CredenzaTitle,
|
CredenzaContent,
|
||||||
CredenzaDescription,
|
CredenzaDescription,
|
||||||
CredenzaBody,
|
CredenzaFooter,
|
||||||
CredenzaFooter,
|
CredenzaHeader,
|
||||||
CredenzaClose,
|
CredenzaTitle,
|
||||||
} from "~/components/custom_ui/credenza";
|
} from "~/components/custom_ui/credenza";
|
||||||
import { PaymentsTable } from "~/components/tables/payments-table";
|
import { PaymentsTable } from "~/components/tables/payments-table";
|
||||||
import type { OrdiniOrdineId } from "~/schemas/public/Ordini";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import { Confirm } from "~/components/confirm";
|
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { CircleCheck, CircleSlash, ReceiptText, Trash2 } from "lucide-react";
|
|
||||||
import toast from "react-hot-toast";
|
|
||||||
import type { UsersId } from "~/schemas/public/Users";
|
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuRadioGroup,
|
DropdownMenuRadioGroup,
|
||||||
DropdownMenuRadioItem,
|
DropdownMenuRadioItem,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "~/components/ui/dropdown-menu";
|
} from "~/components/ui/dropdown-menu";
|
||||||
import PaymentStatusEnum from "~/schemas/public/PaymentStatusEnum";
|
import { cn } from "~/lib/utils";
|
||||||
import Link from "next/link";
|
|
||||||
import OrderTypeEnum from "~/schemas/public/OrderTypeEnum";
|
import OrderTypeEnum from "~/schemas/public/OrderTypeEnum";
|
||||||
|
import type { OrdiniOrdineId } from "~/schemas/public/Ordini";
|
||||||
|
import PaymentStatusEnum from "~/schemas/public/PaymentStatusEnum";
|
||||||
|
import type { UsersId } from "~/schemas/public/Users";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
export const OrdiniModal = ({
|
export const OrdiniModal = ({
|
||||||
isAdmin,
|
isAdmin,
|
||||||
open,
|
open,
|
||||||
setModalOpen,
|
setModalOpen,
|
||||||
ordineId,
|
ordineId,
|
||||||
userId,
|
userId,
|
||||||
}: {
|
}: {
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setModalOpen: (open: boolean) => void;
|
setModalOpen: (open: boolean) => void;
|
||||||
ordineId: OrdiniOrdineId;
|
ordineId: OrdiniOrdineId;
|
||||||
userId: UsersId | undefined;
|
userId: UsersId | undefined;
|
||||||
}) => {
|
}) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { mutate: remove } = api.servizio.removeOrder.useMutation({
|
const { mutate: remove } = api.servizio.removeOrder.useMutation({
|
||||||
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({
|
||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
setModalOpen(false);
|
setModalOpen(false);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(`Errore durante la rimozione ordine: ${error.message}`);
|
toast.error(`Errore durante la rimozione ordine: ${error.message}`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { mutate: update } = api.servizio.updateOrder.useMutation({
|
const { mutate: update } = api.servizio.updateOrder.useMutation({
|
||||||
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,
|
servizioId: data.servizio_id,
|
||||||
data: {
|
data: {
|
||||||
isOkAcconto: true,
|
isOkAcconto: true,
|
||||||
decorrenza: new Date(),
|
decorrenza: new Date(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
toast.success("Ordine aggiornato con successo!");
|
toast.success("Ordine aggiornato con successo!");
|
||||||
await utils.servizio.getOrdini.invalidate({
|
await utils.servizio.getOrdini.invalidate({
|
||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
await utils.servizio.getOrdineById.invalidate({
|
await utils.servizio.getOrdineById.invalidate({
|
||||||
ordineId,
|
ordineId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(`Errore durante la modifica: ${error.message}`);
|
toast.error(`Errore durante la modifica: ${error.message}`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutate: updateServizio } = api.servizio.updateServizio.useMutation({
|
const { mutate: updateServizio } = api.servizio.updateServizio.useMutation({
|
||||||
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) => {
|
onError: (error) => {
|
||||||
toast.error(error.message);
|
toast.error(error.message);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data } = api.servizio.getOrdineById.useQuery({
|
const { data } = api.servizio.getOrdineById.useQuery({
|
||||||
ordineId,
|
ordineId,
|
||||||
});
|
});
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Credenza
|
<Credenza
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={(b) => {
|
onOpenChange={(b) => {
|
||||||
setModalOpen(b);
|
setModalOpen(b);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CredenzaContent className="max-h-[90vh] max-w-2xl">
|
<CredenzaContent className="max-h-[90vh] max-w-2xl">
|
||||||
<CredenzaHeader>
|
<CredenzaHeader>
|
||||||
<CredenzaTitle>Dettagli Ordine</CredenzaTitle>
|
<CredenzaTitle>Dettagli Ordine</CredenzaTitle>
|
||||||
<CredenzaDescription>
|
<CredenzaDescription>
|
||||||
Visualizza i dettagli e i pagamenti associati.
|
Visualizza i dettagli e i pagamenti associati.
|
||||||
</CredenzaDescription>
|
</CredenzaDescription>
|
||||||
</CredenzaHeader>
|
</CredenzaHeader>
|
||||||
<CredenzaBody className="max-h-[80vh] space-y-2 overflow-auto pb-5">
|
<CredenzaBody className="max-h-[80vh] space-y-2 overflow-auto pb-5">
|
||||||
<p>Id Ordine: {data.ordine_id}</p>
|
<p>Id Ordine: {data.ordine_id}</p>
|
||||||
<p>
|
<p>
|
||||||
Data Ordine:
|
Data Ordine:
|
||||||
{new Date(data.created_at).toLocaleDateString("it-IT", {
|
{new Date(data.created_at).toLocaleDateString("it-IT", {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "2-digit",
|
month: "2-digit",
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
<p>Tipologia: {data.type.toString()}</p>
|
<p>Tipologia: {data.type.toString()}</p>
|
||||||
<p>Pack ID: {data.packid}</p>
|
<p>Pack ID: {data.packid}</p>
|
||||||
<p className="flex items-center gap-2">
|
<p className="flex items-center gap-2">
|
||||||
Stato:{" "}
|
Stato:{" "}
|
||||||
{data.isActive ? (
|
{data.isActive ? (
|
||||||
<span className="inline-flex items-center gap-1 text-green-500">
|
<span className="inline-flex items-center gap-1 text-green-500">
|
||||||
<CircleCheck className="size-5" />
|
<CircleCheck className="size-5" />
|
||||||
Attivo
|
Attivo
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="inline-flex items-center gap-1">
|
<span className="inline-flex items-center gap-1">
|
||||||
<CircleSlash className="size-5" />
|
<CircleSlash className="size-5" />
|
||||||
Non Attivo
|
Non Attivo
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{data.isActive &&
|
{data.isActive &&
|
||||||
data.pagamenti.find(
|
data.pagamenti.find(
|
||||||
(p) => p.paymentstatus === PaymentStatusEnum.success,
|
(p) => p.paymentstatus === PaymentStatusEnum.success,
|
||||||
) && (
|
) && (
|
||||||
<div>
|
<div>
|
||||||
<Link
|
<Link
|
||||||
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 variant="outline" className="flex items-center gap-2">
|
||||||
Ricevuta di cortesia <ReceiptText />
|
Ricevuta di cortesia <ReceiptText />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<PaymentsTable data={data.pagamenti} isAdmin={isAdmin} isUserView />
|
<PaymentsTable data={data.pagamenti} isAdmin={isAdmin} isUserView />
|
||||||
</CredenzaBody>
|
</CredenzaBody>
|
||||||
<CredenzaFooter
|
<CredenzaFooter
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full",
|
"flex w-full",
|
||||||
isAdmin ? "sm:justify-between" : "sm:justify-end",
|
isAdmin ? "sm:justify-between" : "sm:justify-end",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<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 variant="outline" aria-label="Modifica Status">
|
||||||
Modifica Status
|
Modifica Status
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<DropdownMenuLabel>Status</DropdownMenuLabel>
|
<DropdownMenuLabel>Status</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuRadioGroup
|
<DropdownMenuRadioGroup
|
||||||
value={data.isActive ? "true" : "false"}
|
value={data.isActive ? "true" : "false"}
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
update({
|
update({
|
||||||
ordineId,
|
ordineId,
|
||||||
data: { isActive: v === "true" },
|
data: { isActive: v === "true" },
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenuRadioItem value="true">
|
<DropdownMenuRadioItem value="true">
|
||||||
Attivo
|
Attivo
|
||||||
</DropdownMenuRadioItem>
|
</DropdownMenuRadioItem>
|
||||||
<DropdownMenuRadioItem value="false">
|
<DropdownMenuRadioItem value="false">
|
||||||
Non Attivo
|
Non Attivo
|
||||||
</DropdownMenuRadioItem>
|
</DropdownMenuRadioItem>
|
||||||
</DropdownMenuRadioGroup>
|
</DropdownMenuRadioGroup>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
<Confirm
|
<Confirm
|
||||||
title="Elimina ordine"
|
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 });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button variant="destructive" aria-label="Elimina Ordine">
|
<Button variant="destructive" aria-label="Elimina Ordine">
|
||||||
<Trash2 className="size-6" />
|
<Trash2 className="size-6" />
|
||||||
Elimina Ordine
|
Elimina Ordine
|
||||||
</Button>
|
</Button>
|
||||||
</Confirm>
|
</Confirm>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<CredenzaClose asChild>
|
<CredenzaClose asChild>
|
||||||
<Button variant="ghost">Chiudi</Button>
|
<Button variant="ghost">Chiudi</Button>
|
||||||
</CredenzaClose>
|
</CredenzaClose>
|
||||||
</CredenzaFooter>
|
</CredenzaFooter>
|
||||||
</CredenzaContent>
|
</CredenzaContent>
|
||||||
</Credenza>
|
</Credenza>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,86 +1,86 @@
|
||||||
import { usePathname } from "next/navigation";
|
|
||||||
import { ARMinimizableLinks } from "~/components/navbar/mobile-nav";
|
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import { Expand, Minimize2 } from "lucide-react";
|
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import { getCookie, setCookie } from "cookies-next/client";
|
import { getCookie, setCookie } from "cookies-next/client";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { add } from "date-fns";
|
import { add } from "date-fns";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { Expand, Minimize2 } from "lucide-react";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { ARMinimizableLinks } from "~/components/navbar/mobile-nav";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
export const Sidebar = ({
|
export const Sidebar = ({
|
||||||
isAdmin,
|
isAdmin,
|
||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
const [minimized, setMinimized] = useState(false);
|
const [minimized, setMinimized] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const defaultOpen = getCookie("sidebar_minimized") === "true";
|
const defaultOpen = getCookie("sidebar_minimized") === "true";
|
||||||
setMinimized(defaultOpen);
|
setMinimized(defaultOpen);
|
||||||
}, []);
|
}, []);
|
||||||
const toggleSidebar = (v: boolean) => {
|
const toggleSidebar = (v: boolean) => {
|
||||||
setMinimized(v);
|
setMinimized(v);
|
||||||
setCookie("sidebar_minimized", v.toString(), {
|
setCookie("sidebar_minimized", v.toString(), {
|
||||||
expires: add(new Date(), { days: 30 }),
|
expires: add(new Date(), { days: 30 }),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.nav
|
<motion.nav
|
||||||
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 }}
|
animate={{ width: minimized ? 65 : 192 }}
|
||||||
onClick={
|
onClick={
|
||||||
minimized
|
minimized
|
||||||
? (e) => {
|
? (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
toggleSidebar(false);
|
toggleSidebar(false);
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
aria-label="Sidebar Menu"
|
aria-label="Sidebar Menu"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center justify-between px-7",
|
"flex items-center justify-between px-7",
|
||||||
minimized && "justify-center px-0",
|
minimized && "justify-center px-0",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!minimized && (
|
{!minimized && (
|
||||||
<h4 className={cn("w-full text-xl font-semibold")}>Menu</h4>
|
<h4 className={cn("w-full text-xl font-semibold")}>Menu</h4>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
aria-label="Toggle Sidebar"
|
aria-label="Toggle Sidebar"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => toggleSidebar(!minimized)}
|
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"
|
||||||
>
|
>
|
||||||
{minimized ? (
|
{minimized ? (
|
||||||
<Expand className="size-6" />
|
<Expand className="size-6" />
|
||||||
) : (
|
) : (
|
||||||
<Minimize2 className="size-6" />
|
<Minimize2 className="size-6" />
|
||||||
)}
|
)}
|
||||||
<span className="sr-only">Toggle Menu</span>
|
<span className="sr-only">Toggle Menu</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<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}
|
setOpen={toggleSidebar}
|
||||||
pathname={pathname}
|
pathname={pathname}
|
||||||
minimized={minimized}
|
minimized={minimized}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</motion.nav>
|
</motion.nav>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,115 +1,114 @@
|
||||||
import Link from "next/link";
|
|
||||||
import { useState } from "react";
|
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { useEffect, type MouseEvent } from "react";
|
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import type { Banners } from "~/schemas/public/Banners";
|
import Link from "next/link";
|
||||||
import { cn } from "~/lib/utils";
|
import { type MouseEvent, useEffect, useState } from "react";
|
||||||
import { IconMatrix, type IconType } from "~/components/IconComponents";
|
import { IconMatrix, type IconType } from "~/components/IconComponents";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
import type { Banners } from "~/schemas/public/Banners";
|
||||||
|
|
||||||
export const BannerFactory = (bannerData: Banners) => {
|
export const BannerFactory = (bannerData: Banners) => {
|
||||||
const NEW_BANNER_KEY = `bannercookie_${bannerData.idbanner}`;
|
const NEW_BANNER_KEY = `bannercookie_${bannerData.idbanner}`;
|
||||||
|
|
||||||
let bgColor = "";
|
let bgColor = "";
|
||||||
let accentColor = "";
|
let accentColor = "";
|
||||||
let closeHoverColor = "";
|
let closeHoverColor = "";
|
||||||
|
|
||||||
switch (bannerData.color) {
|
switch (bannerData.color) {
|
||||||
case "blu":
|
case "blu":
|
||||||
bgColor = "bg-blue-600";
|
bgColor = "bg-blue-600";
|
||||||
accentColor = "bg-blue-800";
|
accentColor = "bg-blue-800";
|
||||||
closeHoverColor = "hover:bg-blue-500";
|
closeHoverColor = "hover:bg-blue-500";
|
||||||
break;
|
break;
|
||||||
case "rosso":
|
case "rosso":
|
||||||
bgColor = "bg-red-600";
|
bgColor = "bg-red-600";
|
||||||
accentColor = "bg-red-800";
|
accentColor = "bg-red-800";
|
||||||
closeHoverColor = "hover:bg-red-500";
|
closeHoverColor = "hover:bg-red-500";
|
||||||
break;
|
break;
|
||||||
case "verde":
|
case "verde":
|
||||||
bgColor = "bg-green-600";
|
bgColor = "bg-green-600";
|
||||||
accentColor = "bg-green-800";
|
accentColor = "bg-green-800";
|
||||||
closeHoverColor = "hover:bg-green-500";
|
closeHoverColor = "hover:bg-green-500";
|
||||||
break;
|
break;
|
||||||
case "indaco":
|
case "indaco":
|
||||||
bgColor = "bg-indigo-600";
|
bgColor = "bg-indigo-600";
|
||||||
accentColor = "bg-indigo-800";
|
accentColor = "bg-indigo-800";
|
||||||
closeHoverColor = "hover:bg-indigo-500";
|
closeHoverColor = "hover:bg-indigo-500";
|
||||||
break;
|
break;
|
||||||
case null:
|
default:
|
||||||
default:
|
bgColor = "bg-indigo-600";
|
||||||
bgColor = "bg-indigo-600";
|
accentColor = "bg-indigo-800";
|
||||||
accentColor = "bg-indigo-800";
|
closeHoverColor = "hover:bg-indigo-500";
|
||||||
closeHoverColor = "hover:bg-indigo-500";
|
break;
|
||||||
break;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const NewBanner = () => {
|
const NewBanner = () => {
|
||||||
const [isBannerToShow, setIsBannerToShow] = useState(false);
|
const [isBannerToShow, setIsBannerToShow] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (bannerData.is_unskippable === false) {
|
if (bannerData.is_unskippable === false) {
|
||||||
const cookievalue = Cookies.get(NEW_BANNER_KEY) !== "true";
|
const cookievalue = Cookies.get(NEW_BANNER_KEY) !== "true";
|
||||||
setIsBannerToShow(cookievalue);
|
setIsBannerToShow(cookievalue);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onClick = (e: MouseEvent<HTMLButtonElement>) => {
|
const onClick = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
Cookies.set(NEW_BANNER_KEY, "true", {
|
Cookies.set(NEW_BANNER_KEY, "true", {
|
||||||
expires: bannerData.hide_duration || 365,
|
expires: bannerData.hide_duration || 365,
|
||||||
});
|
});
|
||||||
setIsBannerToShow(false);
|
setIsBannerToShow(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isBannerToShow && bannerData.is_unskippable === false) {
|
if (!isBannerToShow && bannerData.is_unskippable === false) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className={cn("bg-indigo-600", bgColor)}>
|
<div className={cn("bg-indigo-600", bgColor)}>
|
||||||
<div className="mx-auto flex max-w-screen-xl items-start justify-between px-4 py-3 text-white sm:items-center md:px-8">
|
<div className="mx-auto flex max-w-screen-xl items-start justify-between px-4 py-3 text-white sm:items-center md:px-8">
|
||||||
<div className="flex flex-1 items-start justify-center gap-x-4 sm:items-center">
|
<div className="flex flex-1 items-start justify-center gap-x-4 sm:items-center">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-none items-center justify-center rounded-full bg-indigo-800 p-1.5 px-4 text-sm font-medium",
|
"flex flex-none items-center justify-center rounded-full bg-indigo-800 p-1.5 px-4 text-sm font-medium",
|
||||||
accentColor,
|
accentColor,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{bannerData.titolo}{" "}
|
{bannerData.titolo}{" "}
|
||||||
</div>
|
</div>
|
||||||
<p className="flex gap-2 p-2 font-medium">
|
<p className="flex gap-2 p-2 font-medium">
|
||||||
<span> {bannerData.testo}</span>
|
<span> {bannerData.testo}</span>
|
||||||
|
|
||||||
{bannerData.has_cta &&
|
{bannerData.has_cta &&
|
||||||
bannerData.cta_href &&
|
bannerData.cta_href &&
|
||||||
bannerData.cta_icon && (
|
bannerData.cta_icon && (
|
||||||
<Link
|
<Link
|
||||||
href={bannerData.cta_href}
|
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"
|
||||||
>
|
>
|
||||||
{bannerData.cta_text}
|
{bannerData.cta_text}
|
||||||
<IconMatrix
|
<IconMatrix
|
||||||
type={bannerData.cta_icon as IconType}
|
type={bannerData.cta_icon as IconType}
|
||||||
className="size-4"
|
className="size-4"
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{bannerData.is_unskippable === false && (
|
{bannerData.is_unskippable === false && (
|
||||||
<button
|
<button
|
||||||
className={cn(
|
type="button"
|
||||||
"rounded-lg p-2 ring-offset-2 duration-150 hover:bg-indigo-500 focus:ring",
|
className={cn(
|
||||||
closeHoverColor,
|
"rounded-lg p-2 ring-offset-2 duration-150 hover:bg-indigo-500 focus:ring",
|
||||||
)}
|
closeHoverColor,
|
||||||
onClick={onClick}
|
)}
|
||||||
>
|
onClick={onClick}
|
||||||
<X className="size-5" />
|
>
|
||||||
</button>
|
<X className="size-5" />
|
||||||
)}
|
</button>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
};
|
);
|
||||||
return <NewBanner />;
|
};
|
||||||
|
return <NewBanner />;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
export default function BlurryDivider() {
|
export default function BlurryDivider() {
|
||||||
return (
|
return (
|
||||||
<hr className="h-px border-t-0 bg-transparent bg-gradient-to-r from-transparent via-neutral-500 to-transparent opacity-80 dark:via-neutral-50 dark:opacity-100" />
|
<hr className="h-px border-t-0 bg-transparent bg-gradient-to-r from-transparent via-neutral-500 to-transparent opacity-80 dark:via-neutral-50 dark:opacity-100" />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,191 +1,191 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
import { differenceInDays, differenceInMinutes, format } from "date-fns";
|
||||||
import { Paperclip } from "lucide-react";
|
import { Paperclip } from "lucide-react";
|
||||||
import {
|
|
||||||
Credenza,
|
|
||||||
CredenzaTrigger,
|
|
||||||
CredenzaContent,
|
|
||||||
CredenzaBody,
|
|
||||||
CredenzaHeader,
|
|
||||||
CredenzaDescription,
|
|
||||||
CredenzaTitle,
|
|
||||||
CredenzaFooter,
|
|
||||||
CredenzaClose,
|
|
||||||
} from "~/components/custom_ui/credenza";
|
|
||||||
import { Button, buttonVariants } from "~/components/ui/button";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import { LoadingPage } from "~/components/loading";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
import { ExtIcon } from "~/components/area-riservata/allegati";
|
import { ExtIcon } from "~/components/area-riservata/allegati";
|
||||||
|
import {
|
||||||
|
Credenza,
|
||||||
|
CredenzaBody,
|
||||||
|
CredenzaClose,
|
||||||
|
CredenzaContent,
|
||||||
|
CredenzaDescription,
|
||||||
|
CredenzaFooter,
|
||||||
|
CredenzaHeader,
|
||||||
|
CredenzaTitle,
|
||||||
|
CredenzaTrigger,
|
||||||
|
} from "~/components/custom_ui/credenza";
|
||||||
|
import { LoadingPage } from "~/components/loading";
|
||||||
|
import { Button, buttonVariants } from "~/components/ui/button";
|
||||||
|
import { UploadComponent } from "~/components/upload_modal";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
import type { Session } from "~/server/api/trpc";
|
import type { Session } from "~/server/api/trpc";
|
||||||
import type { ChatUserInfo } from "~/server/services/chat.service";
|
import type { ChatUserInfo } from "~/server/services/chat.service";
|
||||||
import { UploadComponent } from "~/components/upload_modal";
|
import { api } from "~/utils/api";
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
|
||||||
import { differenceInDays, differenceInMinutes, format } from "date-fns";
|
|
||||||
|
|
||||||
type ChatAttachmentsProps = {
|
type ChatAttachmentsProps = {
|
||||||
userData: Session;
|
userData: Session;
|
||||||
chatUserData: ChatUserInfo;
|
chatUserData: ChatUserInfo;
|
||||||
};
|
};
|
||||||
export const ChatAttachments = ({
|
export const ChatAttachments = ({
|
||||||
userData,
|
userData,
|
||||||
chatUserData,
|
chatUserData,
|
||||||
}: ChatAttachmentsProps) => {
|
}: ChatAttachmentsProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const {
|
const {
|
||||||
data: attachments,
|
data: attachments,
|
||||||
isLoading: attachmentsLoading,
|
isLoading: attachmentsLoading,
|
||||||
refetch,
|
refetch,
|
||||||
} = api.storage.getUserStorage.useQuery(
|
} = api.storage.getUserStorage.useQuery(
|
||||||
{ userId: chatUserData.id },
|
{ userId: chatUserData.id },
|
||||||
{ refetchOnMount: true },
|
{ refetchOnMount: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Credenza onOpenChange={(o) => o && refetch()}>
|
<Credenza onOpenChange={(o) => o && refetch()}>
|
||||||
<CredenzaTrigger asChild>
|
<CredenzaTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="relative size-9"
|
className="relative size-9"
|
||||||
aria-label="Allegati Chat"
|
aria-label="Allegati Chat"
|
||||||
>
|
>
|
||||||
{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 size={20} className="text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</CredenzaTrigger>
|
</CredenzaTrigger>
|
||||||
<CredenzaContent className="max-h-[90vh]">
|
<CredenzaContent className="max-h-[90vh]">
|
||||||
<CredenzaHeader>
|
<CredenzaHeader>
|
||||||
<CredenzaTitle className="sr-only">Allegati</CredenzaTitle>
|
<CredenzaTitle className="sr-only">Allegati</CredenzaTitle>
|
||||||
<CredenzaDescription className="sr-only">
|
<CredenzaDescription className="sr-only">
|
||||||
Chat Attachments
|
Chat Attachments
|
||||||
</CredenzaDescription>
|
</CredenzaDescription>
|
||||||
</CredenzaHeader>
|
</CredenzaHeader>
|
||||||
<CredenzaBody className="max-h-[80vh] space-y-3 overflow-auto pb-5">
|
<CredenzaBody className="max-h-[80vh] space-y-3 overflow-auto pb-5">
|
||||||
<UploadComponent
|
<UploadComponent
|
||||||
isAdmin={userData.isAdmin}
|
isAdmin={userData.isAdmin}
|
||||||
userId={chatUserData.id}
|
userId={chatUserData.id}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="h-auto max-h-[40rem] max-w-lg overflow-auto rounded-lg border-2 border-gray-300 py-4">
|
<div className="h-auto max-h-[40rem] max-w-lg overflow-auto rounded-lg border-2 border-gray-300 py-4">
|
||||||
{attachmentsLoading ? (
|
{attachmentsLoading ? (
|
||||||
<LoadingPage />
|
<LoadingPage />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{!attachments || attachments.length == 0 ? (
|
{!attachments || attachments.length === 0 ? (
|
||||||
<div className="flex h-full items-center justify-center">
|
<div className="flex h-full items-center justify-center">
|
||||||
<p className="text-center text-lg font-semibold text-gray-500">
|
<p className="text-center text-lg font-semibold text-gray-500">
|
||||||
{t.allegati.nessun_allegato}
|
{t.allegati.nessun_allegato}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ul className="px-2">
|
<ul className="px-2">
|
||||||
<h3 className="px-3 text-lg font-semibold text-gray-700">
|
<h3 className="px-3 text-lg font-semibold text-gray-700">
|
||||||
{t.allegati.allegati_recenti}
|
{t.allegati.allegati_recenti}
|
||||||
</h3>
|
</h3>
|
||||||
{attachments?.map((file, idx) => {
|
{attachments?.map((file, idx) => {
|
||||||
if (idx >= 3) return null;
|
if (idx >= 3) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={`${file.filename}-${idx}`}
|
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"
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
aria-label="Visualizza Allegato"
|
aria-label="Visualizza Allegato"
|
||||||
href={`/area-riservata/allegato-view/${file.id}`}
|
href={`/area-riservata/allegato-view/${file.id}`}
|
||||||
target="_blank"
|
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",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{file.ext && <ExtIcon ext={file.ext} />}
|
{file.ext && <ExtIcon ext={file.ext} />}
|
||||||
<span className="truncate">{file.filename}</span>
|
<span className="truncate">{file.filename}</span>
|
||||||
</Link>
|
</Link>
|
||||||
<span
|
<span
|
||||||
className="text-muted-foreground text-xs"
|
className="text-muted-foreground text-xs"
|
||||||
key={new Date().getTime()}
|
key={new Date().toString()}
|
||||||
>
|
>
|
||||||
{TimeSince(file.created_at)}
|
{TimeSince(file.created_at)}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CredenzaBody>
|
</CredenzaBody>
|
||||||
<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"
|
href="/area-riservata/allegati"
|
||||||
className={cn(
|
className={cn(
|
||||||
buttonVariants({
|
buttonVariants({
|
||||||
variant: "outline",
|
variant: "outline",
|
||||||
}),
|
}),
|
||||||
"relative w-full",
|
"relative w-full",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Paperclip /> {t.allegati.i_tuoi_allegati}
|
<Paperclip /> {t.allegati.i_tuoi_allegati}
|
||||||
{attachments && attachments.length > 0 && (
|
{attachments && attachments.length > 0 && (
|
||||||
<span className="absolute -top-1 -right-1 flex size-5 items-center justify-center rounded-full bg-red-600 text-xs text-white">
|
<span className="absolute -top-1 -right-1 flex size-5 items-center justify-center rounded-full bg-red-600 text-xs text-white">
|
||||||
{attachments.length}
|
{attachments.length}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
<CredenzaClose asChild>
|
<CredenzaClose asChild>
|
||||||
<Button className="w-full">{t.chiudi}</Button>
|
<Button className="w-full">{t.chiudi}</Button>
|
||||||
</CredenzaClose>
|
</CredenzaClose>
|
||||||
</CredenzaFooter>
|
</CredenzaFooter>
|
||||||
</CredenzaContent>
|
</CredenzaContent>
|
||||||
</Credenza>
|
</Credenza>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const TimeSince = (date: Date) => {
|
const TimeSince = (date: Date) => {
|
||||||
const minsSince = Math.abs(differenceInMinutes(date, new Date()));
|
const minsSince = Math.abs(differenceInMinutes(date, new Date()));
|
||||||
const daysSince = Math.abs(differenceInDays(date, new Date()));
|
const daysSince = Math.abs(differenceInDays(date, new Date()));
|
||||||
|
|
||||||
const relevantMinsIncrements = [1, 3, 5, 10, 15, 30, 60, 120];
|
const relevantMinsIncrements = [1, 3, 5, 10, 15, 30, 60, 120];
|
||||||
|
|
||||||
if (daysSince === 0) {
|
if (daysSince === 0) {
|
||||||
if (minsSince < 1) {
|
if (minsSince < 1) {
|
||||||
return "ora";
|
return "ora";
|
||||||
}
|
}
|
||||||
|
|
||||||
const closestToNow = relevantMinsIncrements.reduce((prev, curr) => {
|
const closestToNow = relevantMinsIncrements.reduce((prev, curr) => {
|
||||||
return Math.abs(curr - minsSince) < Math.abs(prev - minsSince)
|
return Math.abs(curr - minsSince) < Math.abs(prev - minsSince)
|
||||||
? curr
|
? curr
|
||||||
: prev;
|
: prev;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (closestToNow === 60) {
|
if (closestToNow === 60) {
|
||||||
return "~1 ora fa";
|
return "~1 ora fa";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (closestToNow === 120 && minsSince < 121) {
|
if (closestToNow === 120 && minsSince < 121) {
|
||||||
return "~2 ore fa";
|
return "~2 ore fa";
|
||||||
}
|
}
|
||||||
|
|
||||||
return `~${closestToNow} min fa`;
|
return `~${closestToNow} min fa`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (daysSince === 1) {
|
if (daysSince === 1) {
|
||||||
return "ieri";
|
return "ieri";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (daysSince < 30) {
|
if (daysSince < 30) {
|
||||||
return `${daysSince} giorni fa`;
|
return `${daysSince} giorni fa`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return format(date, "dd/MM/yyyy");
|
return format(date, "dd/MM/yyyy");
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,170 +2,170 @@
|
||||||
|
|
||||||
import { SendHorizontal, SmilePlusIcon, ThumbsUp } from "lucide-react";
|
import { SendHorizontal, SmilePlusIcon, ThumbsUp } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
useCallback,
|
type ChangeEvent,
|
||||||
useEffect,
|
useCallback,
|
||||||
useRef,
|
useEffect,
|
||||||
useState,
|
useRef,
|
||||||
type ChangeEvent,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import { ChatAttachments } from "~/components/chat/chat-attachments";
|
||||||
|
import {
|
||||||
|
type AutosizeTextAreaRef,
|
||||||
|
AutosizeTextarea,
|
||||||
|
} from "~/components/custom_ui/autoResizeTextArea";
|
||||||
|
import {
|
||||||
|
EmojiPicker,
|
||||||
|
EmojiPickerContent,
|
||||||
|
EmojiPickerSearch,
|
||||||
|
} from "~/components/custom_ui/emoji-picker";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import {
|
import {
|
||||||
AutosizeTextarea,
|
Popover,
|
||||||
type AutosizeTextAreaRef,
|
PopoverContent,
|
||||||
} from "~/components/custom_ui/autoResizeTextArea";
|
PopoverTrigger,
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import { ChatAttachments } from "~/components/chat/chat-attachments";
|
|
||||||
import { useMediaQuery } from "~/hooks/use-media-query";
|
|
||||||
import { useThrottledIsTypingMutation } from "~/hooks/chatHooks";
|
|
||||||
import type { Session } from "~/server/api/trpc";
|
|
||||||
import type { ChatsChatid } from "~/schemas/public/Chats";
|
|
||||||
import type { ChatUserInfo } from "~/server/services/chat.service";
|
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
} from "~/components/ui/popover";
|
} from "~/components/ui/popover";
|
||||||
import {
|
import { useThrottledIsTypingMutation } from "~/hooks/chatHooks";
|
||||||
EmojiPickerContent,
|
import { useMediaQuery } from "~/hooks/use-media-query";
|
||||||
EmojiPickerSearch,
|
|
||||||
EmojiPicker,
|
|
||||||
} from "~/components/custom_ui/emoji-picker";
|
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
|
import type { ChatsChatid } from "~/schemas/public/Chats";
|
||||||
|
import type { Session } from "~/server/api/trpc";
|
||||||
|
import type { ChatUserInfo } from "~/server/services/chat.service";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
type ChatBottombarProps = {
|
type ChatBottombarProps = {
|
||||||
chatId: ChatsChatid;
|
chatId: ChatsChatid;
|
||||||
userData: Session;
|
userData: Session;
|
||||||
chatUserData: ChatUserInfo;
|
chatUserData: ChatUserInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ChatBottombar({
|
export default function ChatBottombar({
|
||||||
chatId,
|
chatId,
|
||||||
userData,
|
userData,
|
||||||
chatUserData,
|
chatUserData,
|
||||||
}: ChatBottombarProps) {
|
}: ChatBottombarProps) {
|
||||||
const { locale } = useTranslation();
|
const { locale } = useTranslation();
|
||||||
const [message, setMessage] = useState("");
|
const [message, setMessage] = useState("");
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
const inputRef = useRef<AutosizeTextAreaRef>(null);
|
const inputRef = useRef<AutosizeTextAreaRef>(null);
|
||||||
|
|
||||||
const { mutate: addMutation } = api.messagesSSE.add.useMutation({});
|
const { mutate: addMutation } = api.messagesSSE.add.useMutation({});
|
||||||
|
|
||||||
const handleThumbsUp = useCallback(() => {
|
const handleThumbsUp = useCallback(() => {
|
||||||
addMutation({
|
addMutation({
|
||||||
chatId,
|
chatId,
|
||||||
message: "👍",
|
message: "👍",
|
||||||
sender: userData.id,
|
sender: userData.id,
|
||||||
});
|
});
|
||||||
setMessage("");
|
setMessage("");
|
||||||
}, [chatId, addMutation, userData]);
|
}, [chatId, addMutation, userData]);
|
||||||
|
|
||||||
const handleSend = () => {
|
const handleSend = () => {
|
||||||
if (message.length !== 0) {
|
if (message.length !== 0) {
|
||||||
addMutation({
|
addMutation({
|
||||||
chatId,
|
chatId,
|
||||||
message: message.trim(),
|
message: message.trim(),
|
||||||
sender: userData.id,
|
sender: userData.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
setMessage("");
|
setMessage("");
|
||||||
|
|
||||||
if (inputRef.current) {
|
if (inputRef.current) {
|
||||||
inputRef.current.textArea.focus();
|
inputRef.current.textArea.focus();
|
||||||
inputRef.current.textArea.style.height = "auto";
|
inputRef.current.textArea.style.height = "auto";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const isTypingMutation = useThrottledIsTypingMutation({
|
const isTypingMutation = useThrottledIsTypingMutation({
|
||||||
chatId,
|
chatId,
|
||||||
username: userData.username,
|
username: userData.username,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// update isTyping state
|
// update isTyping state
|
||||||
isTypingMutation(isFocused && message.trim().length > 0);
|
isTypingMutation(isFocused && message.trim().length > 0);
|
||||||
}, [isFocused, message, isTypingMutation]);
|
}, [isFocused, message, isTypingMutation]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (inputRef.current) {
|
if (inputRef.current) {
|
||||||
inputRef.current.textArea.focus();
|
inputRef.current.textArea.focus();
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
const [openEmoji, setOpenEmoji] = useState(false);
|
const [openEmoji, setOpenEmoji] = useState(false);
|
||||||
|
|
||||||
const isMobile = useMediaQuery("(min-width: 426px)");
|
const isMobile = useMediaQuery("(min-width: 426px)");
|
||||||
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 userData={userData} chatUserData={chatUserData} />
|
||||||
</div>
|
</div>
|
||||||
<div className="relative w-full">
|
<div className="relative w-full">
|
||||||
<AutosizeTextarea
|
<AutosizeTextarea
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
value={message}
|
value={message}
|
||||||
minHeight={56}
|
minHeight={56}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
setMessage((prev) => `${prev}\n`);
|
setMessage((prev) => `${prev}\n`);
|
||||||
} else {
|
} else {
|
||||||
handleSend();
|
handleSend();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onFocus={() => setIsFocused(true)}
|
onFocus={() => setIsFocused(true)}
|
||||||
onBlur={() => setIsFocused(false)}
|
onBlur={() => setIsFocused(false)}
|
||||||
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => {
|
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
setMessage(e.target.value);
|
setMessage(e.target.value);
|
||||||
}}
|
}}
|
||||||
name="message"
|
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"
|
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"
|
||||||
/>
|
/>
|
||||||
{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 open={openEmoji} onOpenChange={setOpenEmoji}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
<SmilePlusIcon className="stroke-muted-foreground" />
|
<SmilePlusIcon className="stroke-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</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"}
|
locale={locale as "it" | "en"}
|
||||||
className="h-84"
|
className="h-84"
|
||||||
onEmojiSelect={({ emoji }) => {
|
onEmojiSelect={({ emoji }) => {
|
||||||
setMessage(message + emoji);
|
setMessage(message + emoji);
|
||||||
// if (inputRef.current) {
|
// if (inputRef.current) {
|
||||||
// inputRef.current.textArea.focus();
|
// inputRef.current.textArea.focus();
|
||||||
// }
|
// }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<EmojiPickerSearch
|
<EmojiPickerSearch
|
||||||
placeholder={locale == "en" ? "Search..." : "Cerca..."}
|
placeholder={locale === "en" ? "Search..." : "Cerca..."}
|
||||||
/>
|
/>
|
||||||
<EmojiPickerContent />
|
<EmojiPickerContent />
|
||||||
</EmojiPicker>
|
</EmojiPicker>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
aria-label="Send Message"
|
aria-label="Send Message"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="shrink-0"
|
className="shrink-0"
|
||||||
onClick={
|
onClick={
|
||||||
message.length != 0 ? () => handleSend() : () => handleThumbsUp()
|
message.length !== 0 ? () => handleSend() : () => handleThumbsUp()
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{message.length != 0 ? (
|
{message.length !== 0 ? (
|
||||||
<SendHorizontal size={20} className="text-muted-foreground" />
|
<SendHorizontal size={20} className="text-muted-foreground" />
|
||||||
) : (
|
) : (
|
||||||
<ThumbsUp size={20} className="text-muted-foreground" />
|
<ThumbsUp size={20} className="text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,202 +1,199 @@
|
||||||
import * as React from "react";
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
import MessageLoading from "~/components/chat/message-loading";
|
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
import { Button, type ButtonProps } from "~/components/ui/button";
|
|
||||||
import { Check, CheckCheck } from "lucide-react";
|
import { Check, CheckCheck } from "lucide-react";
|
||||||
|
import * as React from "react";
|
||||||
|
import MessageLoading from "~/components/chat/message-loading";
|
||||||
|
import { Button, type ButtonProps } from "~/components/ui/button";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
// ChatBubble
|
// ChatBubble
|
||||||
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",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
received: "self-start",
|
received: "self-start",
|
||||||
sent: "self-end flex-row-reverse",
|
sent: "self-end flex-row-reverse",
|
||||||
},
|
},
|
||||||
layout: {
|
layout: {
|
||||||
default: "",
|
default: "",
|
||||||
ai: "max-w-full w-full items-center",
|
ai: "max-w-full w-full items-center",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "received",
|
variant: "received",
|
||||||
layout: "default",
|
layout: "default",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
interface ChatBubbleProps
|
interface ChatBubbleProps
|
||||||
extends React.HTMLAttributes<HTMLDivElement>,
|
extends React.HTMLAttributes<HTMLDivElement>,
|
||||||
VariantProps<typeof chatBubbleVariant> {}
|
VariantProps<typeof chatBubbleVariant> {}
|
||||||
|
|
||||||
const ChatBubble = React.forwardRef<HTMLDivElement, ChatBubbleProps>(
|
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({ variant, layout, className }),
|
||||||
"group relative",
|
"group relative",
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{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,
|
variant,
|
||||||
layout,
|
layout,
|
||||||
} as Partial<React.ComponentProps<typeof child.type>>)
|
} as Partial<React.ComponentProps<typeof child.type>>)
|
||||||
: child,
|
: child,
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
ChatBubble.displayName = "ChatBubble";
|
ChatBubble.displayName = "ChatBubble";
|
||||||
|
|
||||||
type ChatBubbleReadStatusProps = {
|
type ChatBubbleReadStatusProps = {
|
||||||
isread: boolean;
|
isread: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ChatBubbleReadStatus = ({ isread }: ChatBubbleReadStatusProps) => {
|
export const ChatBubbleReadStatus = ({ isread }: ChatBubbleReadStatusProps) => {
|
||||||
if (isread) {
|
if (isread) {
|
||||||
return <CheckCheck className="mt-2 size-4 text-green-500" />;
|
return <CheckCheck className="mt-2 size-4 text-green-500" />;
|
||||||
} else {
|
} else {
|
||||||
return <Check className="text-muted-foreground mt-2 size-4" />;
|
return <Check className="text-muted-foreground mt-2 size-4" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ChatBubbleMessage
|
// ChatBubbleMessage
|
||||||
const chatBubbleMessageVariants = cva("p-2", {
|
const chatBubbleMessageVariants = cva("p-2", {
|
||||||
variants: {
|
variants: {
|
||||||
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: {
|
layout: {
|
||||||
default: "",
|
default: "",
|
||||||
ai: "border-t w-full rounded-none bg-transparent",
|
ai: "border-t w-full rounded-none bg-transparent",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "received",
|
variant: "received",
|
||||||
layout: "default",
|
layout: "default",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
interface ChatBubbleMessageProps
|
interface ChatBubbleMessageProps
|
||||||
extends React.HTMLAttributes<HTMLDivElement>,
|
extends React.HTMLAttributes<HTMLDivElement>,
|
||||||
VariantProps<typeof chatBubbleMessageVariants> {
|
VariantProps<typeof chatBubbleMessageVariants> {
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChatBubbleMessage = React.forwardRef<
|
const ChatBubbleMessage = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
ChatBubbleMessageProps
|
ChatBubbleMessageProps
|
||||||
>(
|
>(
|
||||||
(
|
(
|
||||||
{ className, variant, layout, isLoading = false, children, ...props },
|
{ className, variant, layout, isLoading = false, children, ...props },
|
||||||
ref,
|
ref,
|
||||||
) => (
|
) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
chatBubbleMessageVariants({ variant, layout, className }),
|
chatBubbleMessageVariants({ variant, layout, className }),
|
||||||
"max-w-full break-words whitespace-pre-wrap",
|
"max-w-full break-words whitespace-pre-wrap",
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<MessageLoading />
|
<MessageLoading />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
children
|
children
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
ChatBubbleMessage.displayName = "ChatBubbleMessage";
|
ChatBubbleMessage.displayName = "ChatBubbleMessage";
|
||||||
|
|
||||||
// ChatBubbleTimestamp
|
// ChatBubbleTimestamp
|
||||||
interface ChatBubbleTimestampProps
|
interface ChatBubbleTimestampProps
|
||||||
extends React.HTMLAttributes<HTMLDivElement> {
|
extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChatBubbleTimestamp: React.FC<ChatBubbleTimestampProps> = ({
|
const ChatBubbleTimestamp: React.FC<ChatBubbleTimestampProps> = ({
|
||||||
timestamp,
|
timestamp,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}) => (
|
}) => (
|
||||||
<div className={cn("mt-2 text-right text-xs", className)} {...props}>
|
<div className={cn("mt-2 text-right text-xs", className)} {...props}>
|
||||||
{timestamp}
|
{timestamp}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// ChatBubbleAction
|
// ChatBubbleAction
|
||||||
type ChatBubbleActionProps = ButtonProps & {
|
type ChatBubbleActionProps = ButtonProps & {
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ChatBubbleAction: React.FC<ChatBubbleActionProps> = ({
|
const ChatBubbleAction: React.FC<ChatBubbleActionProps> = ({
|
||||||
icon,
|
icon,
|
||||||
onClick,
|
onClick,
|
||||||
className,
|
className,
|
||||||
variant = "ghost",
|
variant = "ghost",
|
||||||
size = "icon",
|
size = "icon",
|
||||||
...props
|
...props
|
||||||
}) => (
|
}) => (
|
||||||
<Button
|
<Button
|
||||||
aria-label="Chat Bubble Action"
|
aria-label="Chat Bubble Action"
|
||||||
variant={variant}
|
variant={variant}
|
||||||
size={size}
|
size={size}
|
||||||
className={className}
|
className={className}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
interface ChatBubbleActionWrapperProps
|
interface ChatBubbleActionWrapperProps
|
||||||
extends React.HTMLAttributes<HTMLDivElement> {
|
extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
variant?: "sent" | "received";
|
variant?: "sent" | "received";
|
||||||
className?: string;
|
className?: string;
|
||||||
show: boolean;
|
show: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChatBubbleActionWrapper = React.forwardRef<
|
const ChatBubbleActionWrapper = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
ChatBubbleActionWrapperProps
|
ChatBubbleActionWrapperProps
|
||||||
>(({ variant, className, show, children, ...props }, ref) => (
|
>(({ variant, className, show, children, ...props }, ref) => (
|
||||||
<>
|
<>
|
||||||
{show && (
|
{show && (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
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"
|
||||||
? "-left-1 -translate-x-full flex-row-reverse"
|
? "-left-1 -translate-x-full flex-row-reverse"
|
||||||
: "-right-1 translate-x-full",
|
: "-right-1 translate-x-full",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
));
|
));
|
||||||
ChatBubbleActionWrapper.displayName = "ChatBubbleActionWrapper";
|
ChatBubbleActionWrapper.displayName = "ChatBubbleActionWrapper";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ChatBubble,
|
ChatBubble,
|
||||||
ChatBubbleMessage,
|
ChatBubbleMessage,
|
||||||
ChatBubbleTimestamp,
|
ChatBubbleTimestamp,
|
||||||
ChatBubbleAction,
|
ChatBubbleAction,
|
||||||
ChatBubbleActionWrapper,
|
ChatBubbleActionWrapper,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,439 +1,440 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
import {
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
type FormEvent,
|
|
||||||
} from "react";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import toast from "react-hot-toast";
|
|
||||||
import Input from "~/components/custom_ui/input";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "~/components/ui/dialog";
|
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import { Pencil, Trash2 } from "lucide-react";
|
|
||||||
import type { MessagesMessageid } from "~/schemas/public/Messages";
|
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
import { Pencil, Trash2 } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
ChatBubble,
|
type FormEvent,
|
||||||
ChatBubbleAction,
|
useCallback,
|
||||||
ChatBubbleActionWrapper,
|
useEffect,
|
||||||
ChatBubbleMessage,
|
useRef,
|
||||||
ChatBubbleReadStatus,
|
useState,
|
||||||
ChatBubbleTimestamp,
|
} from "react";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import {
|
||||||
|
ChatBubble,
|
||||||
|
ChatBubbleAction,
|
||||||
|
ChatBubbleActionWrapper,
|
||||||
|
ChatBubbleMessage,
|
||||||
|
ChatBubbleReadStatus,
|
||||||
|
ChatBubbleTimestamp,
|
||||||
} from "~/components/chat/chat-bubble";
|
} from "~/components/chat/chat-bubble";
|
||||||
import { ChatMessageList } from "~/components/chat/chat-message-list";
|
import { ChatMessageList } from "~/components/chat/chat-message-list";
|
||||||
import {
|
import Input from "~/components/custom_ui/input";
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
} from "~/components/ui/alert-dialog";
|
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
|
||||||
import type { ChatMessage } from "~/server/services/messages.service";
|
|
||||||
import type { UsersId } from "~/schemas/public/Users";
|
|
||||||
import type { WhoIsTyping } from "~/server/sse";
|
|
||||||
import type { Session } from "~/server/api/trpc";
|
|
||||||
import type { ChatsChatid } from "~/schemas/public/Chats";
|
|
||||||
import type { LiveChat } from "~/hooks/chatHooks";
|
|
||||||
import LoadingButton from "~/components/custom_ui/loading-button";
|
import LoadingButton from "~/components/custom_ui/loading-button";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from "~/components/ui/alert-dialog";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "~/components/ui/dialog";
|
||||||
import { getUserColor, UserAvatar } from "~/components/user_avatar";
|
import { getUserColor, UserAvatar } from "~/components/user_avatar";
|
||||||
|
import type { LiveChat } from "~/hooks/chatHooks";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
|
import type { ChatsChatid } from "~/schemas/public/Chats";
|
||||||
|
import type { MessagesMessageid } from "~/schemas/public/Messages";
|
||||||
|
import type { UsersId } from "~/schemas/public/Users";
|
||||||
|
import type { Session } from "~/server/api/trpc";
|
||||||
|
import type { ChatMessage } from "~/server/services/messages.service";
|
||||||
|
import type { WhoIsTyping } from "~/server/sse";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
type ChatListProps = {
|
type ChatListProps = {
|
||||||
chatId: ChatsChatid;
|
chatId: ChatsChatid;
|
||||||
messages: ChatMessage[];
|
messages: ChatMessage[];
|
||||||
userData: Session;
|
userData: Session;
|
||||||
query: LiveChat["query"];
|
query: LiveChat["query"];
|
||||||
typingStatus: WhoIsTyping[];
|
typingStatus: WhoIsTyping[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ChatList = ({
|
export const ChatList = ({
|
||||||
chatId,
|
chatId,
|
||||||
messages,
|
messages,
|
||||||
userData,
|
userData,
|
||||||
query,
|
query,
|
||||||
typingStatus,
|
typingStatus,
|
||||||
}: ChatListProps) => {
|
}: ChatListProps) => {
|
||||||
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||||
const [msgSelected, setMsgSelected] = useState<ChatMessage | null>(null);
|
const [msgSelected, setMsgSelected] = useState<ChatMessage | null>(null);
|
||||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||||||
|
|
||||||
const handleEditOpen = (status: boolean) => {
|
const handleEditOpen = (status: boolean) => {
|
||||||
setEditModalOpen(status);
|
setEditModalOpen(status);
|
||||||
};
|
};
|
||||||
const handleDeleteOpen = (status: boolean) => {
|
const handleDeleteOpen = (status: boolean) => {
|
||||||
setDeleteModalOpen(status);
|
setDeleteModalOpen(status);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { mutate: deleteMutation } = api.messagesSSE.delete.useMutation({
|
const { mutate: deleteMutation } = api.messagesSSE.delete.useMutation({
|
||||||
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: () => {
|
onError: () => {
|
||||||
toast.error("Error deleting message", { id: "deleteMessage" });
|
toast.error("Error deleting message", { id: "deleteMessage" });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { mutate: editMutation } = api.messagesSSE.edit.useMutation({
|
const { mutate: editMutation } = api.messagesSSE.edit.useMutation({
|
||||||
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: () => {
|
onError: () => {
|
||||||
toast.error("Error editing message", { id: "editMessage" });
|
toast.error("Error editing message", { id: "editMessage" });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleEditSubmit = useCallback(
|
const handleEditSubmit = useCallback(
|
||||||
(e: FormEvent<HTMLFormElement>) => {
|
(e: FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const form = e.currentTarget;
|
const form = e.currentTarget;
|
||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
if (!formData.get("message-edit") || !formData.get("message-id")) {
|
if (!formData.get("message-edit") || !formData.get("message-id")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
editMutation({
|
editMutation({
|
||||||
chatId,
|
chatId,
|
||||||
messageId: formData.get("message-id") as MessagesMessageid,
|
messageId: formData.get("message-id") as MessagesMessageid,
|
||||||
message: formData.get("message-edit") as string,
|
message: formData.get("message-edit") as string,
|
||||||
});
|
});
|
||||||
setEditModalOpen(false);
|
setEditModalOpen(false);
|
||||||
},
|
},
|
||||||
[editMutation],
|
[editMutation],
|
||||||
);
|
);
|
||||||
const [locked, setLocked] = useState(false);
|
const [locked, setLocked] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
messagesContainerRef.current &&
|
messagesContainerRef.current &&
|
||||||
!locked &&
|
!locked &&
|
||||||
!query.isLoading &&
|
!query.isLoading &&
|
||||||
!query.isFetching
|
!query.isFetching
|
||||||
) {
|
) {
|
||||||
messagesContainerRef.current.scrollTop =
|
messagesContainerRef.current.scrollTop =
|
||||||
messagesContainerRef.current.scrollHeight;
|
messagesContainerRef.current.scrollHeight;
|
||||||
}
|
}
|
||||||
setLocked(false);
|
setLocked(false);
|
||||||
}, [messages, typingStatus]);
|
}, [messages, typingStatus]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col overflow-y-auto">
|
<div className="flex h-full w-full flex-col overflow-y-auto">
|
||||||
<ChatMessageList ref={messagesContainerRef}>
|
<ChatMessageList ref={messagesContainerRef}>
|
||||||
<AnimatePresence key="chat-messages">
|
<AnimatePresence key="chat-messages">
|
||||||
{query.hasNextPage && (
|
{query.hasNextPage && (
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
aria-label="Load More Messages"
|
aria-label="Load More Messages"
|
||||||
loading={query.isFetchingNextPage}
|
loading={query.isFetchingNextPage}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
key="load-more"
|
key="load-more"
|
||||||
disabled={!query.hasNextPage || query.isFetchingNextPage}
|
disabled={!query.hasNextPage || query.isFetchingNextPage}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setLocked(true);
|
setLocked(true);
|
||||||
await query.fetchNextPage();
|
await query.fetchNextPage();
|
||||||
}}
|
}}
|
||||||
className="mb-3"
|
className="mb-3"
|
||||||
>
|
>
|
||||||
{query.isFetchingNextPage
|
{query.isFetchingNextPage
|
||||||
? "Caricamento..."
|
? "Caricamento..."
|
||||||
: "Carica messaggi precedenti"}
|
: "Carica messaggi precedenti"}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{messages.map((message, index) => {
|
{messages.map((message, index) => {
|
||||||
const variant = getMessageVariant(message.sender, userData.id);
|
const variant = getMessageVariant(message.sender, userData.id);
|
||||||
|
|
||||||
const isNextSameSender =
|
const isNextSameSender =
|
||||||
index + 1 < messages.length &&
|
index + 1 < messages.length &&
|
||||||
messages[index + 1]?.sender == message.sender;
|
messages[index + 1]?.sender === message.sender;
|
||||||
|
|
||||||
const isPreviousSameSender =
|
const isPreviousSameSender =
|
||||||
index - 1 >= 0 && messages[index - 1]?.sender == message.sender;
|
index - 1 >= 0 && messages[index - 1]?.sender === message.sender;
|
||||||
|
|
||||||
const isPrevSameDay =
|
const isPrevSameDay =
|
||||||
index - 1 >= 0 &&
|
index - 1 >= 0 &&
|
||||||
new Date(messages[index - 1]!.time).toDateString() ===
|
// biome-ignore lint/style/noNonNullAssertion: <non lo so>
|
||||||
new Date(message.time).toDateString();
|
new Date(messages[index - 1]!.time).toDateString() ===
|
||||||
|
new Date(message.time).toDateString();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={message.messageid}
|
key={message.messageid}
|
||||||
layout
|
layout
|
||||||
initial={{ opacity: 0, scale: 1, y: 50, x: 0 }}
|
initial={{ opacity: 0, scale: 1, y: 50, x: 0 }}
|
||||||
animate={{ opacity: 1, scale: 1, y: 0, x: 0 }}
|
animate={{ opacity: 1, scale: 1, y: 0, x: 0 }}
|
||||||
exit={{ opacity: 0, scale: 1, y: 1, x: 0 }}
|
exit={{ opacity: 0, scale: 1, y: 1, x: 0 }}
|
||||||
transition={{
|
transition={{
|
||||||
opacity: { duration: 0.1 },
|
opacity: { duration: 0.1 },
|
||||||
layout: {
|
layout: {
|
||||||
type: "spring",
|
type: "spring",
|
||||||
bounce: 0.3,
|
bounce: 0.3,
|
||||||
duration: 0.3,
|
duration: 0.3,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
style={{ originX: 0.5, originY: 0.5 }}
|
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",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!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">
|
||||||
{new Date(message.time).toLocaleDateString("it", {
|
{new Date(message.time).toLocaleDateString("it", {
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
month: "2-digit",
|
month: "2-digit",
|
||||||
year: "2-digit",
|
year: "2-digit",
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<ChatBubble variant={variant}>
|
<ChatBubble variant={variant}>
|
||||||
{isNextSameSender ? (
|
{isNextSameSender ? (
|
||||||
<div className="size-6"></div>
|
<div className="size-6"></div>
|
||||||
) : (
|
) : (
|
||||||
<UserAvatar userId={message.sender} className="size-6" />
|
<UserAvatar userId={message.sender} className="size-6" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
{!isPreviousSameSender && (
|
{!isPreviousSameSender && (
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-sm font-semibold",
|
"text-sm font-semibold",
|
||||||
variant === "sent" && "text-right",
|
variant === "sent" && "text-right",
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
color: getUserColor(message.sender),
|
color: getUserColor(message.sender),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{message.sender_nome}
|
{message.sender_nome}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<ChatBubbleMessage
|
<ChatBubbleMessage
|
||||||
isLoading={false}
|
isLoading={false}
|
||||||
className={cn(isNextSameSender && "rounded-lg")}
|
className={cn(isNextSameSender && "rounded-lg")}
|
||||||
>
|
>
|
||||||
{message.message}
|
{message.message}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{userData.id == message.sender && (
|
{userData.id === message.sender && (
|
||||||
<ChatBubbleReadStatus isread={message.isread} />
|
<ChatBubbleReadStatus isread={message.isread} />
|
||||||
)}
|
)}
|
||||||
{message.time && (
|
{message.time && (
|
||||||
<ChatBubbleTimestamp
|
<ChatBubbleTimestamp
|
||||||
timestamp={new Date(message.time).toLocaleString(
|
timestamp={new Date(message.time).toLocaleString(
|
||||||
"it",
|
"it",
|
||||||
{
|
{
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
month: "short",
|
month: "short",
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ChatBubbleMessage>
|
</ChatBubbleMessage>
|
||||||
</div>
|
</div>
|
||||||
<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"}
|
key={"delete"}
|
||||||
icon={<Trash2 className="size-4" />}
|
icon={<Trash2 className="size-4" />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMsgSelected(message);
|
setMsgSelected(message);
|
||||||
handleDeleteOpen(true);
|
handleDeleteOpen(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<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"}
|
key={"edit"}
|
||||||
icon={<Pencil className="size-4" />}
|
icon={<Pencil className="size-4" />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMsgSelected(message);
|
setMsgSelected(message);
|
||||||
handleEditOpen(true);
|
handleEditOpen(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ChatBubbleActionWrapper>
|
</ChatBubbleActionWrapper>
|
||||||
</ChatBubble>
|
</ChatBubble>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<Typers
|
<Typers
|
||||||
currentTypers={typingStatus}
|
currentTypers={typingStatus}
|
||||||
messageLenght={messages.length}
|
messageLenght={messages.length}
|
||||||
userId={userData.id}
|
userId={userData.id}
|
||||||
/>
|
/>
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</ChatMessageList>
|
</ChatMessageList>
|
||||||
<EditMessageDialog
|
<EditMessageDialog
|
||||||
open={editModalOpen}
|
open={editModalOpen}
|
||||||
onOpenChange={handleEditOpen}
|
onOpenChange={handleEditOpen}
|
||||||
msgEdit={msgSelected}
|
msgEdit={msgSelected}
|
||||||
handleEditSubmit={handleEditSubmit}
|
handleEditSubmit={handleEditSubmit}
|
||||||
/>
|
/>
|
||||||
<DeleteMessageDialog
|
<DeleteMessageDialog
|
||||||
open={deleteModalOpen}
|
open={deleteModalOpen}
|
||||||
onOpenChange={handleDeleteOpen}
|
onOpenChange={handleDeleteOpen}
|
||||||
msgDelete={msgSelected}
|
msgDelete={msgSelected}
|
||||||
handleDeleteSubmit={(messageId) => {
|
handleDeleteSubmit={(messageId) => {
|
||||||
deleteMutation({ chatId, messageId });
|
deleteMutation({ chatId, messageId });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Typers = ({
|
const Typers = ({
|
||||||
currentTypers,
|
currentTypers,
|
||||||
messageLenght,
|
messageLenght,
|
||||||
userId,
|
userId,
|
||||||
}: {
|
}: {
|
||||||
currentTypers: WhoIsTyping[];
|
currentTypers: WhoIsTyping[];
|
||||||
messageLenght: number;
|
messageLenght: number;
|
||||||
userId: UsersId;
|
userId: UsersId;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{currentTypers.map((typer, index) => {
|
{currentTypers.map((typer, index) => {
|
||||||
if (typer.userId === userId) return null;
|
if (typer.userId === userId) return null;
|
||||||
const variant = "received";
|
const variant = "received";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={"typers" + messageLenght + index}
|
key={`typers${messageLenght}${index}`}
|
||||||
layout
|
layout
|
||||||
initial={{ opacity: 0, scale: 1, y: 50, x: 0 }}
|
initial={{ opacity: 0, scale: 1, y: 50, x: 0 }}
|
||||||
animate={{ opacity: 1, scale: 1, y: 0, x: 0 }}
|
animate={{ opacity: 1, scale: 1, y: 0, x: 0 }}
|
||||||
exit={{ opacity: 0, scale: 1, y: 1, x: 0 }}
|
exit={{ opacity: 0, scale: 1, y: 1, x: 0 }}
|
||||||
transition={{
|
transition={{
|
||||||
opacity: { duration: 0.1 },
|
opacity: { duration: 0.1 },
|
||||||
layout: {
|
layout: {
|
||||||
type: "spring",
|
type: "spring",
|
||||||
bounce: 0.3,
|
bounce: 0.3,
|
||||||
duration: 0.3,
|
duration: 0.3,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
style={{ originX: 0.5, originY: 0.5 }}
|
style={{ originX: 0.5, originY: 0.5 }}
|
||||||
className="flex flex-col gap-2 py-2"
|
className="flex flex-col gap-2 py-2"
|
||||||
>
|
>
|
||||||
<ChatBubble variant={variant}>
|
<ChatBubble variant={variant}>
|
||||||
<UserAvatar userId={typer.userId} className="size-6" />
|
<UserAvatar userId={typer.userId} className="size-6" />
|
||||||
|
|
||||||
<ChatBubbleMessage isLoading={true}></ChatBubbleMessage>
|
<ChatBubbleMessage isLoading={true}></ChatBubbleMessage>
|
||||||
</ChatBubble>
|
</ChatBubble>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const EditMessageDialog = ({
|
const EditMessageDialog = ({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
msgEdit,
|
msgEdit,
|
||||||
handleEditSubmit,
|
handleEditSubmit,
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (status: boolean) => void;
|
onOpenChange: (status: boolean) => void;
|
||||||
msgEdit: ChatMessage | null;
|
msgEdit: ChatMessage | null;
|
||||||
handleEditSubmit: (e: FormEvent<HTMLFormElement>) => void;
|
handleEditSubmit: (e: FormEvent<HTMLFormElement>) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<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 onSubmit={handleEditSubmit} method="post">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<label htmlFor="id" className="sr-only">
|
<label htmlFor="id" className="sr-only">
|
||||||
ID
|
ID
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
id="id"
|
id="id"
|
||||||
type="text"
|
type="text"
|
||||||
className="hidden"
|
className="hidden"
|
||||||
name="message-id"
|
name="message-id"
|
||||||
value={msgEdit.messageid}
|
value={msgEdit.messageid}
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
<label htmlFor="message-edit" className="sr-only">
|
<label htmlFor="message-edit" className="sr-only">
|
||||||
Modifica messaggio
|
Modifica messaggio
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
id="message-edit"
|
id="message-edit"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Modifica messaggio"
|
placeholder="Modifica messaggio"
|
||||||
className="mt-0"
|
className="mt-0"
|
||||||
name="message-edit"
|
name="message-edit"
|
||||||
defaultValue={msgEdit.message || undefined}
|
defaultValue={msgEdit.message || undefined}
|
||||||
/>
|
/>
|
||||||
<Button type="submit">{t.salva}</Button>
|
<Button type="submit">{t.salva}</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const DeleteMessageDialog = ({
|
const DeleteMessageDialog = ({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
msgDelete,
|
msgDelete,
|
||||||
handleDeleteSubmit,
|
handleDeleteSubmit,
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (status: boolean) => void;
|
onOpenChange: (status: boolean) => void;
|
||||||
msgDelete: ChatMessage | null;
|
msgDelete: ChatMessage | null;
|
||||||
handleDeleteSubmit: (msgId: MessagesMessageid) => void;
|
handleDeleteSubmit: (msgId: MessagesMessageid) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{msgDelete && (
|
{msgDelete && (
|
||||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Vuoi eliminare il messaggio?</AlertDialogTitle>
|
<AlertDialogTitle>Vuoi eliminare il messaggio?</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
Questa azione non può essere annullata.
|
Questa azione non può essere annullata.
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>{t.annulla}</AlertDialogCancel>
|
<AlertDialogCancel>{t.annulla}</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
aria-label="Delete Message"
|
aria-label="Delete Message"
|
||||||
onClick={() => handleDeleteSubmit(msgDelete.messageid)}
|
onClick={() => handleDeleteSubmit(msgDelete.messageid)}
|
||||||
>
|
>
|
||||||
{t.procedi}
|
{t.procedi}
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMessageVariant = (messageName: string, selectedUserName: string) =>
|
const getMessageVariant = (messageName: string, selectedUserName: string) =>
|
||||||
messageName !== selectedUserName ? "received" : "sent";
|
messageName !== selectedUserName ? "received" : "sent";
|
||||||
|
|
|
||||||
|
|
@ -1,447 +1,447 @@
|
||||||
import { Search, SquarePen } from "lucide-react";
|
import { Search, SquarePen } from "lucide-react";
|
||||||
import { cn } from "~/lib/utils";
|
import Link from "next/link";
|
||||||
import { Button, buttonVariants } from "~/components/ui/button";
|
import { useRouter } from "next/router";
|
||||||
import { useMediaQuery } from "~/hooks/use-media-query";
|
|
||||||
|
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import Input from "~/components/custom_ui/input";
|
import Input from "~/components/custom_ui/input";
|
||||||
import {
|
|
||||||
CommandDialog,
|
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
|
||||||
CommandInput,
|
|
||||||
CommandItem,
|
|
||||||
CommandList,
|
|
||||||
} from "~/components/ui/command";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import type { ChatsChatid } from "~/schemas/public/Chats";
|
|
||||||
import {
|
|
||||||
ContextMenu,
|
|
||||||
ContextMenuContent,
|
|
||||||
ContextMenuItem,
|
|
||||||
ContextMenuSeparator,
|
|
||||||
ContextMenuSub,
|
|
||||||
ContextMenuSubContent,
|
|
||||||
ContextMenuSubTrigger,
|
|
||||||
ContextMenuTrigger,
|
|
||||||
} from "~/components/ui/context-menu";
|
|
||||||
import Link from "next/link";
|
|
||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
} from "~/components/ui/alert-dialog";
|
|
||||||
import { Etichetta, EtichetteModal } from "~/components/etichette";
|
import { Etichetta, EtichetteModal } from "~/components/etichette";
|
||||||
import type { NonNullableUser } from "~/server/controllers/user.controller";
|
|
||||||
import { LoadingPage } from "~/components/loading";
|
import { LoadingPage } from "~/components/loading";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from "~/components/ui/alert-dialog";
|
||||||
|
import { Button, buttonVariants } from "~/components/ui/button";
|
||||||
|
import {
|
||||||
|
CommandDialog,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
} from "~/components/ui/command";
|
||||||
|
import {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuContent,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuSeparator,
|
||||||
|
ContextMenuSub,
|
||||||
|
ContextMenuSubContent,
|
||||||
|
ContextMenuSubTrigger,
|
||||||
|
ContextMenuTrigger,
|
||||||
|
} from "~/components/ui/context-menu";
|
||||||
import { UserAvatar } from "~/components/user_avatar";
|
import { UserAvatar } from "~/components/user_avatar";
|
||||||
|
import { useMediaQuery } from "~/hooks/use-media-query";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
import type { ChatsChatid } from "~/schemas/public/Chats";
|
||||||
import type { ActiveChatsType } from "~/server/controllers/chat.controller";
|
import type { ActiveChatsType } from "~/server/controllers/chat.controller";
|
||||||
|
import type { NonNullableUser } from "~/server/controllers/user.controller";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
type LastMessage = {
|
type LastMessage = {
|
||||||
message: string | null;
|
message: string | null;
|
||||||
time: Date;
|
time: Date;
|
||||||
isLastSender: boolean;
|
isLastSender: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ChatSidebar = ({ chatId }: { chatId: ChatsChatid | null }) => {
|
export const ChatSidebar = ({ chatId }: { chatId: ChatsChatid | null }) => {
|
||||||
const { data: activeChats, isLoading: loadingActiveChats } =
|
const { data: activeChats, isLoading: loadingActiveChats } =
|
||||||
api.chat.getActiveChats.useQuery(undefined, {
|
api.chat.getActiveChats.useQuery(undefined, {
|
||||||
refetchInterval: 5000, // Refetch every 5 seconds, Good enough for a chat sidebar
|
refetchInterval: 5000, // Refetch every 5 seconds, Good enough for a chat sidebar
|
||||||
refetchOnWindowFocus: true,
|
refetchOnWindowFocus: true,
|
||||||
});
|
});
|
||||||
const { data: inactiveUsers, isLoading: loadingInactiveUsers } =
|
const { data: inactiveUsers, isLoading: loadingInactiveUsers } =
|
||||||
api.users.getActiveUsersWithoutChat.useQuery();
|
api.users.getActiveUsersWithoutChat.useQuery();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isDesktop = useMediaQuery("(min-width: 768px)");
|
const isDesktop = useMediaQuery("(min-width: 768px)");
|
||||||
const [selectedChat, setSelectedChat] = useState<ChatsChatid | null>(null);
|
const [selectedChat, setSelectedChat] = useState<ChatsChatid | null>(null);
|
||||||
const [addDialogOpen, setAddDialogOpen] = useState(false);
|
const [addDialogOpen, setAddDialogOpen] = useState(false);
|
||||||
const [searchDialogOpen, setSearchDialogOpen] = useState(false);
|
const [searchDialogOpen, setSearchDialogOpen] = useState(false);
|
||||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||||||
const [editEtichettaModalOpen, setEditEtichettaModalOpen] = useState(false);
|
const [editEtichettaModalOpen, setEditEtichettaModalOpen] = useState(false);
|
||||||
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { mutate: deletechat } = api.chat.deleteChat.useMutation({
|
const { mutate: deletechat } = api.chat.deleteChat.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await utils.chat.getActiveChats.invalidate();
|
await utils.chat.getActiveChats.invalidate();
|
||||||
await utils.users.getActiveUsersWithoutChat.invalidate();
|
await utils.users.getActiveUsersWithoutChat.invalidate();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const getVariant = (lastmessage: LastMessage | null) => {
|
const getVariant = (lastmessage: LastMessage | null) => {
|
||||||
if (lastmessage) {
|
if (lastmessage) {
|
||||||
return lastmessage.isLastSender ? "secondary" : "grey";
|
return lastmessage.isLastSender ? "secondary" : "grey";
|
||||||
}
|
}
|
||||||
return "grey";
|
return "grey";
|
||||||
};
|
};
|
||||||
const getLastMessage = (lastmessage: LastMessage | null) => {
|
const getLastMessage = (lastmessage: LastMessage | null) => {
|
||||||
if (lastmessage) {
|
if (lastmessage) {
|
||||||
return `${lastmessage.isLastSender ? "Tu: " : ""}${lastmessage.message}`;
|
return `${lastmessage.isLastSender ? "Tu: " : ""}${lastmessage.message}`;
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChatSelection = useCallback(
|
const handleChatSelection = useCallback(
|
||||||
async (new_chatid: ChatsChatid) => {
|
async (new_chatid: ChatsChatid) => {
|
||||||
if (chatId === new_chatid) return;
|
if (chatId === new_chatid) return;
|
||||||
await router.push(`/area-riservata/admin/chats/${new_chatid}`);
|
await router.push(`/area-riservata/admin/chats/${new_chatid}`);
|
||||||
setSearchDialogOpen(false);
|
setSearchDialogOpen(false);
|
||||||
},
|
},
|
||||||
[chatId, router],
|
[chatId, router],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (loadingActiveChats || loadingInactiveUsers) return <LoadingPage />;
|
if (loadingActiveChats || loadingInactiveUsers) return <LoadingPage />;
|
||||||
if (!activeChats || !inactiveUsers) return <div>Errore caricamento</div>;
|
if (!activeChats || !inactiveUsers) return <div>Errore caricamento</div>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group relative flex h-full flex-col gap-4 py-2 data-[collapsed=true]:p-2">
|
<div className="group relative flex h-full flex-col gap-4 py-2 data-[collapsed=true]:p-2">
|
||||||
<div className="mx-2 flex flex-col items-center justify-between gap-2 sm:flex-row sm:gap-0">
|
<div className="mx-2 flex flex-col items-center justify-between gap-2 sm:flex-row sm:gap-0">
|
||||||
{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 htmlFor="search" className="sr-only">
|
||||||
Search Chats
|
Search Chats
|
||||||
</label>
|
</label>
|
||||||
<Input placeholder="Search" className="m-0 pl-8" id="search" />
|
<Input placeholder="Search" className="m-0 pl-8" id="search" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
aria-label="Open Search Dialog"
|
aria-label="Open Search Dialog"
|
||||||
onClick={() => setSearchDialogOpen(true)}
|
onClick={() => setSearchDialogOpen(true)}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="size-8"
|
className="size-8"
|
||||||
>
|
>
|
||||||
<Search size={20} />
|
<Search size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SearchUserComponent
|
<SearchUserComponent
|
||||||
activeChats={activeChats}
|
activeChats={activeChats}
|
||||||
isOpen={searchDialogOpen}
|
isOpen={searchDialogOpen}
|
||||||
OpenChange={setSearchDialogOpen}
|
OpenChange={setSearchDialogOpen}
|
||||||
handleChatSelection={handleChatSelection}
|
handleChatSelection={handleChatSelection}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
aria-label="Open Add User Dialog"
|
aria-label="Open Add User Dialog"
|
||||||
onClick={() => setAddDialogOpen(true)}
|
onClick={() => setAddDialogOpen(true)}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="size-8"
|
className="size-8"
|
||||||
>
|
>
|
||||||
<SquarePen size={20} />
|
<SquarePen size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<NewUserComponent
|
<NewUserComponent
|
||||||
inactiveUsers={inactiveUsers}
|
inactiveUsers={inactiveUsers}
|
||||||
isOpen={addDialogOpen}
|
isOpen={addDialogOpen}
|
||||||
OpenChange={setAddDialogOpen}
|
OpenChange={setAddDialogOpen}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="grid gap-1 rounded-lg group-[[data-collapsed=true]]:justify-center group-[[data-collapsed=true]]:px-2">
|
<nav className="grid gap-1 rounded-lg group-[[data-collapsed=true]]:justify-center group-[[data-collapsed=true]]:px-2">
|
||||||
{activeChats.map((chat) => (
|
{activeChats.map((chat) => (
|
||||||
<ContextMenu key={chat.userinfo.username}>
|
<ContextMenu key={chat.userinfo.username}>
|
||||||
<ContextMenuTrigger asChild>
|
<ContextMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
aria-label="Select Chat"
|
aria-label="Select Chat"
|
||||||
onClick={() => handleChatSelection(chat.id)}
|
onClick={() => handleChatSelection(chat.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
buttonVariants({
|
buttonVariants({
|
||||||
variant: getVariant(chat.lastmessage),
|
variant: getVariant(chat.lastmessage),
|
||||||
size: !isDesktop ? "icon" : "xl",
|
size: !isDesktop ? "icon" : "xl",
|
||||||
}),
|
}),
|
||||||
|
|
||||||
!isDesktop ? "p-1" : "justify-start gap-4",
|
!isDesktop ? "p-1" : "justify-start gap-4",
|
||||||
"h-fit w-full",
|
"h-fit w-full",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
username={chat.userinfo.username}
|
username={chat.userinfo.username}
|
||||||
userId={chat.userinfo.id}
|
userId={chat.userinfo.id}
|
||||||
className="size-10"
|
className="size-10"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isDesktop && (
|
{isDesktop && (
|
||||||
<div className="flex h-full max-w-28 flex-col text-left align-top">
|
<div className="flex h-full max-w-28 flex-col text-left align-top">
|
||||||
<span>{chat.userinfo.username}</span>
|
<span>{chat.userinfo.username}</span>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{chat.etichette.map((etichetta) => (
|
{chat.etichette.map((etichetta) => (
|
||||||
<Etichetta
|
<Etichetta
|
||||||
key={etichetta.id_etichetta}
|
key={etichetta.id_etichetta}
|
||||||
title={etichetta.title}
|
title={etichetta.title}
|
||||||
color_hex={etichetta.color_hex}
|
color_hex={etichetta.color_hex}
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="text-muted-foreground truncate text-xs">
|
<span className="text-muted-foreground truncate text-xs">
|
||||||
{getLastMessage(chat.lastmessage)}
|
{getLastMessage(chat.lastmessage)}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-muted-foreground truncate text-xs">
|
<span className="text-muted-foreground truncate text-xs">
|
||||||
{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",
|
month: "2-digit",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</ContextMenuTrigger>
|
</ContextMenuTrigger>
|
||||||
<ChatSidebarContextMenu
|
<ChatSidebarContextMenu
|
||||||
chatid={chat.id}
|
chatid={chat.id}
|
||||||
setSelectedChat={setSelectedChat}
|
setSelectedChat={setSelectedChat}
|
||||||
setDeleteModalOpen={setDeleteModalOpen}
|
setDeleteModalOpen={setDeleteModalOpen}
|
||||||
setEditEtichettaModalOpen={setEditEtichettaModalOpen}
|
setEditEtichettaModalOpen={setEditEtichettaModalOpen}
|
||||||
/>
|
/>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
))}
|
))}
|
||||||
{selectedChat && (
|
{selectedChat && (
|
||||||
<>
|
<>
|
||||||
<AlertDialog
|
<AlertDialog
|
||||||
open={deleteModalOpen}
|
open={deleteModalOpen}
|
||||||
onOpenChange={setDeleteModalOpen}
|
onOpenChange={setDeleteModalOpen}
|
||||||
>
|
>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Sei assolutamente sicuro?</AlertDialogTitle>
|
<AlertDialogTitle>Sei assolutamente sicuro?</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
Questa azione non può essere annullata. Questo eliminerà
|
Questa azione non può essere annullata. Questo eliminerà
|
||||||
definitivamente la chat e rimuoverà i dati dai nostri
|
definitivamente la chat e rimuoverà i dati dai nostri
|
||||||
server.
|
server.
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Annulla</AlertDialogCancel>
|
<AlertDialogCancel>Annulla</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
aria-label="Delete Chat"
|
aria-label="Delete Chat"
|
||||||
className={cn(buttonVariants({ variant: "destructive" }))}
|
className={cn(buttonVariants({ variant: "destructive" }))}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (selectedChat) {
|
if (selectedChat) {
|
||||||
deletechat({
|
deletechat({
|
||||||
chatId: selectedChat,
|
chatId: selectedChat,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Continua
|
Continua
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
<EtichetteModal
|
<EtichetteModal
|
||||||
open={editEtichettaModalOpen}
|
open={editEtichettaModalOpen}
|
||||||
onOpenChange={setEditEtichettaModalOpen}
|
onOpenChange={setEditEtichettaModalOpen}
|
||||||
chatId={selectedChat}
|
chatId={selectedChat}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const SearchUserComponent = ({
|
const SearchUserComponent = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
OpenChange,
|
OpenChange,
|
||||||
activeChats,
|
activeChats,
|
||||||
handleChatSelection,
|
handleChatSelection,
|
||||||
}: {
|
}: {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
OpenChange: (status: boolean) => void;
|
OpenChange: (status: boolean) => void;
|
||||||
activeChats: ActiveChatsType[];
|
activeChats: ActiveChatsType[];
|
||||||
handleChatSelection: (chatId: ChatsChatid) => void;
|
handleChatSelection: (chatId: ChatsChatid) => void;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<CommandDialog open={isOpen} onOpenChange={OpenChange}>
|
<CommandDialog open={isOpen} onOpenChange={OpenChange}>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="Digita il nome utente..."
|
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"
|
||||||
/>
|
/>
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>Nessun risultato</CommandEmpty>
|
<CommandEmpty>Nessun risultato</CommandEmpty>
|
||||||
<CommandGroup
|
<CommandGroup
|
||||||
className="pb-2"
|
className="pb-2"
|
||||||
heading={activeChats.length > 0 ? "Suggerimenti" : undefined}
|
heading={activeChats.length > 0 ? "Suggerimenti" : undefined}
|
||||||
>
|
>
|
||||||
{activeChats?.map((chat) => (
|
{activeChats?.map((chat) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
className="rounded-md"
|
className="rounded-md"
|
||||||
key={chat.id}
|
key={chat.id}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
handleChatSelection(chat.id);
|
handleChatSelection(chat.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
username={chat.userinfo.username}
|
username={chat.userinfo.username}
|
||||||
userId={chat.userinfo.id}
|
userId={chat.userinfo.id}
|
||||||
className="size-8"
|
className="size-8"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
<span className="block font-medium">
|
<span className="block font-medium">
|
||||||
{chat.userinfo.username}
|
{chat.userinfo.username}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</CommandDialog>
|
</CommandDialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const NewUserComponent = ({
|
const NewUserComponent = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
OpenChange,
|
OpenChange,
|
||||||
inactiveUsers,
|
inactiveUsers,
|
||||||
}: {
|
}: {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
OpenChange: (status: boolean) => void;
|
OpenChange: (status: boolean) => void;
|
||||||
inactiveUsers: NonNullableUser[];
|
inactiveUsers: NonNullableUser[];
|
||||||
}) => {
|
}) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { mutate: createNewChat } = api.chatSSE.create.useMutation({
|
const { mutate: createNewChat } = api.chatSSE.create.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await utils.chat.getActiveChats.invalidate();
|
await utils.chat.getActiveChats.invalidate();
|
||||||
OpenChange(false);
|
OpenChange(false);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<CommandDialog open={isOpen} onOpenChange={OpenChange}>
|
<CommandDialog open={isOpen} onOpenChange={OpenChange}>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="Digita il nome utente..."
|
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"
|
||||||
/>
|
/>
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>Nessun risultato</CommandEmpty>
|
<CommandEmpty>Nessun risultato</CommandEmpty>
|
||||||
<CommandGroup
|
<CommandGroup
|
||||||
className="pb-2"
|
className="pb-2"
|
||||||
heading={inactiveUsers.length > 0 ? "Suggerimenti" : undefined}
|
heading={inactiveUsers.length > 0 ? "Suggerimenti" : undefined}
|
||||||
>
|
>
|
||||||
{inactiveUsers?.map((user) => (
|
{inactiveUsers?.map((user) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
className="rounded-md"
|
className="rounded-md"
|
||||||
key={user.id}
|
key={user.id}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
createNewChat({
|
createNewChat({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
username={user.username}
|
username={user.username}
|
||||||
userId={user.id}
|
userId={user.id}
|
||||||
className="size-8"
|
className="size-8"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
<span className="block font-medium">{user.username}</span>
|
<span className="block font-medium">{user.username}</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</CommandDialog>
|
</CommandDialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ChatSidebarContextMenu = ({
|
const ChatSidebarContextMenu = ({
|
||||||
chatid,
|
chatid,
|
||||||
setSelectedChat,
|
setSelectedChat,
|
||||||
setDeleteModalOpen,
|
setDeleteModalOpen,
|
||||||
setEditEtichettaModalOpen,
|
setEditEtichettaModalOpen,
|
||||||
}: {
|
}: {
|
||||||
chatid: ChatsChatid;
|
chatid: ChatsChatid;
|
||||||
setSelectedChat: (chat: ChatsChatid | null) => void;
|
setSelectedChat: (chat: ChatsChatid | null) => void;
|
||||||
setDeleteModalOpen: (status: boolean) => void;
|
setDeleteModalOpen: (status: boolean) => void;
|
||||||
setEditEtichettaModalOpen: (status: boolean) => void;
|
setEditEtichettaModalOpen: (status: boolean) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { data: chatData } = api.chat.getChatInfobyId.useQuery({
|
const { data: chatData } = api.chat.getChatInfobyId.useQuery({
|
||||||
chatId: chatid,
|
chatId: chatid,
|
||||||
});
|
});
|
||||||
if (!chatData) return null;
|
if (!chatData) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ContextMenuContent>
|
<ContextMenuContent>
|
||||||
<ContextMenuSub>
|
<ContextMenuSub>
|
||||||
<ContextMenuSubTrigger>Etichette</ContextMenuSubTrigger>
|
<ContextMenuSubTrigger>Etichette</ContextMenuSubTrigger>
|
||||||
<ContextMenuSubContent className="w-48">
|
<ContextMenuSubContent className="w-48">
|
||||||
<ContextMenuItem asChild>
|
<ContextMenuItem asChild>
|
||||||
<button
|
<button
|
||||||
aria-label="Edit Chat Labels"
|
type="button"
|
||||||
onClick={() => {
|
aria-label="Edit Chat Labels"
|
||||||
setSelectedChat(chatid);
|
onClick={() => {
|
||||||
setEditEtichettaModalOpen(true);
|
setSelectedChat(chatid);
|
||||||
}}
|
setEditEtichettaModalOpen(true);
|
||||||
className="w-full cursor-pointer"
|
}}
|
||||||
>
|
className="w-full cursor-pointer"
|
||||||
Modifica
|
>
|
||||||
</button>
|
Modifica
|
||||||
</ContextMenuItem>
|
</button>
|
||||||
</ContextMenuSubContent>
|
</ContextMenuItem>
|
||||||
</ContextMenuSub>
|
</ContextMenuSubContent>
|
||||||
<ContextMenuSeparator />
|
</ContextMenuSub>
|
||||||
<ContextMenuItem>
|
<ContextMenuSeparator />
|
||||||
<Link
|
<ContextMenuItem>
|
||||||
aria-label="View User Profile"
|
<Link
|
||||||
target="_blank"
|
aria-label="View User Profile"
|
||||||
href={`/area-riservata/admin/user-view/edit-user/${chatData.user_ref}`}
|
target="_blank"
|
||||||
>
|
href={`/area-riservata/admin/user-view/edit-user/${chatData.user_ref}`}
|
||||||
Profilo
|
>
|
||||||
</Link>
|
Profilo
|
||||||
</ContextMenuItem>
|
</Link>
|
||||||
|
</ContextMenuItem>
|
||||||
|
|
||||||
<ContextMenuItem>
|
<ContextMenuItem>
|
||||||
<Link
|
<Link
|
||||||
aria-label="View User Orders"
|
aria-label="View User Orders"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href={`/area-riservata/admin/user-view/ordini/${chatData.user_ref}`}
|
href={`/area-riservata/admin/user-view/ordini/${chatData.user_ref}`}
|
||||||
>
|
>
|
||||||
Ordini
|
Ordini
|
||||||
</Link>
|
</Link>
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
<ContextMenuItem>
|
<ContextMenuItem>
|
||||||
<Link
|
<Link
|
||||||
aria-label="View User Attachments"
|
aria-label="View User Attachments"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href={`/area-riservata/admin/user-view/allegati/${chatData.user_ref}`}
|
href={`/area-riservata/admin/user-view/allegati/${chatData.user_ref}`}
|
||||||
>
|
>
|
||||||
Allegati
|
Allegati
|
||||||
</Link>
|
</Link>
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
<ContextMenuSub>
|
<ContextMenuSub>
|
||||||
<ContextMenuSubTrigger>Altro</ContextMenuSubTrigger>
|
<ContextMenuSubTrigger>Altro</ContextMenuSubTrigger>
|
||||||
<ContextMenuSubContent className="w-48">
|
<ContextMenuSubContent className="w-48">
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
aria-label="Edit Chat"
|
aria-label="Edit Chat"
|
||||||
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"
|
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>
|
||||||
</ContextMenuSubContent>
|
</ContextMenuSubContent>
|
||||||
</ContextMenuSub>
|
</ContextMenuSub>
|
||||||
</ContextMenuContent>
|
</ContextMenuContent>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,269 +1,269 @@
|
||||||
"use client";
|
"use client";
|
||||||
import {
|
import {
|
||||||
Info,
|
Info,
|
||||||
Paperclip,
|
Paperclip,
|
||||||
ShieldUser,
|
ShieldUser,
|
||||||
ShoppingBag,
|
ShoppingBag,
|
||||||
User,
|
User,
|
||||||
UserCog,
|
UserCog,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
import { Button, buttonVariants } from "~/components/ui/button";
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import {
|
import Link from "next/link";
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
} from "~/components/ui/popover";
|
|
||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogAction,
|
|
||||||
} from "~/components/ui/alert-dialog";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { useCallback } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { Separator } from "~/components/ui/separator";
|
|
||||||
import { EtichetteDisplayer } from "~/components/etichette";
|
import { EtichetteDisplayer } from "~/components/etichette";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "~/components/ui/alert-dialog";
|
||||||
|
import { Button, buttonVariants } from "~/components/ui/button";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "~/components/ui/popover";
|
||||||
|
import { Separator } from "~/components/ui/separator";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "~/components/ui/tooltip";
|
||||||
|
import { UserAvatar } from "~/components/user_avatar";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
import type { ChatsChatid } from "~/schemas/public/Chats";
|
||||||
import type { Session } from "~/server/api/trpc";
|
import type { Session } from "~/server/api/trpc";
|
||||||
import type { ChatUserInfo } from "~/server/services/chat.service";
|
import type { ChatUserInfo } from "~/server/services/chat.service";
|
||||||
import type { ChatsChatid } from "~/schemas/public/Chats";
|
|
||||||
import type { OnlineStatus } from "~/server/sse";
|
import type { OnlineStatus } from "~/server/sse";
|
||||||
import {
|
import { api } from "~/utils/api";
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "~/components/ui/tooltip";
|
|
||||||
import { useCallback } from "react";
|
|
||||||
import { UserAvatar } from "~/components/user_avatar";
|
|
||||||
|
|
||||||
type ChatTopbarProps = {
|
type ChatTopbarProps = {
|
||||||
chatId: ChatsChatid;
|
chatId: ChatsChatid;
|
||||||
userData: Session;
|
userData: Session;
|
||||||
chatUserData: ChatUserInfo;
|
chatUserData: ChatUserInfo;
|
||||||
onlineStatus: OnlineStatus[];
|
onlineStatus: OnlineStatus[];
|
||||||
};
|
};
|
||||||
export default function ChatTopbar({
|
export default function ChatTopbar({
|
||||||
chatId,
|
chatId,
|
||||||
userData,
|
userData,
|
||||||
chatUserData,
|
chatUserData,
|
||||||
onlineStatus,
|
onlineStatus,
|
||||||
}: ChatTopbarProps) {
|
}: ChatTopbarProps) {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { mutate: deletechat } = api.chat.deleteChat.useMutation({
|
const { mutate: deletechat } = api.chat.deleteChat.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await utils.chat.getActiveChats.invalidate();
|
await utils.chat.getActiveChats.invalidate();
|
||||||
await utils.users.getActiveUsersWithoutChat.invalidate();
|
await utils.users.getActiveUsersWithoutChat.invalidate();
|
||||||
await router.push("/area-riservata/admin/chats/");
|
await router.push("/area-riservata/admin/chats/");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { mutate: swap } = api.auth.swapUser.useMutation({
|
const { mutate: swap } = api.auth.swapUser.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
toast.success("Utente cambiato con successo");
|
toast.success("Utente cambiato con successo");
|
||||||
await router.push("/area-riservata/dashboard");
|
await router.push("/area-riservata/dashboard");
|
||||||
router.reload();
|
router.reload();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleDeleteChat = useCallback(() => {
|
const handleDeleteChat = useCallback(() => {
|
||||||
deletechat({ chatId });
|
deletechat({ chatId });
|
||||||
}, [chatId, deletechat]);
|
}, [chatId, deletechat]);
|
||||||
|
|
||||||
const handleSwapUser = useCallback(() => {
|
const handleSwapUser = useCallback(() => {
|
||||||
swap({ id: chatUserData.id });
|
swap({ id: chatUserData.id });
|
||||||
}, [chatUserData, swap]);
|
}, [chatUserData, swap]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-18 w-full items-center justify-between border-b px-4 py-2">
|
<div className="flex h-18 w-full items-center justify-between border-b px-4 py-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{!userData.isAdmin ? (
|
{!userData.isAdmin ? (
|
||||||
<Image
|
<Image
|
||||||
src={"/favicon/favicon.png"}
|
src={"/favicon/favicon.png"}
|
||||||
alt="Infoalloggi logo"
|
alt="Infoalloggi logo"
|
||||||
width={40}
|
width={40}
|
||||||
height={40}
|
height={40}
|
||||||
className="size-10 rounded-full"
|
className="size-10 rounded-full"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
className="size-10"
|
className="size-10"
|
||||||
username={chatUserData.username}
|
username={chatUserData.username}
|
||||||
userId={chatUserData.id}
|
userId={chatUserData.id}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{!userData.isAdmin ? "Chat Infoalloggi" : chatUserData.username}
|
{!userData.isAdmin ? "Chat Infoalloggi" : chatUserData.username}
|
||||||
</span>
|
</span>
|
||||||
{userData.isAdmin && <EtichetteDisplayer chatId={chatId} />}
|
{userData.isAdmin && <EtichetteDisplayer chatId={chatId} />}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
<UserStatus onlineStatus={onlineStatus} userData={userData} />
|
<UserStatus onlineStatus={onlineStatus} userData={userData} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{userData.isAdmin && (
|
{userData.isAdmin && (
|
||||||
<AdminPopover
|
<AdminPopover
|
||||||
chatUserData={chatUserData}
|
chatUserData={chatUserData}
|
||||||
handleSwapUser={handleSwapUser}
|
handleSwapUser={handleSwapUser}
|
||||||
handleDeleteChat={handleDeleteChat}
|
handleDeleteChat={handleDeleteChat}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserStatus = ({
|
const UserStatus = ({
|
||||||
onlineStatus,
|
onlineStatus,
|
||||||
userData,
|
userData,
|
||||||
}: {
|
}: {
|
||||||
onlineStatus: OnlineStatus[];
|
onlineStatus: OnlineStatus[];
|
||||||
userData: Session;
|
userData: Session;
|
||||||
}) => (
|
}) => (
|
||||||
<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 key={idx} delayDuration={0}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"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 size={15} className="mr-[0.2rem]" />
|
||||||
<span>
|
<span>
|
||||||
{user.nome}
|
{user.nome}
|
||||||
{idx < onlineStatus.length - 1 && ","}
|
{idx < onlineStatus.length - 1 && ","}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Admin Infoalloggi</p>
|
<p>Admin Infoalloggi</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
) : (
|
) : (
|
||||||
<span
|
<span
|
||||||
key={idx}
|
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")}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
{user.nome}
|
{user.nome}
|
||||||
{!userData.isAdmin && " (Tu)"}
|
{!userData.isAdmin && " (Tu)"}
|
||||||
{idx < onlineStatus.length - 1 && ", "}
|
{idx < onlineStatus.length - 1 && ", "}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const AdminPopover = ({
|
const AdminPopover = ({
|
||||||
chatUserData,
|
chatUserData,
|
||||||
handleSwapUser,
|
handleSwapUser,
|
||||||
handleDeleteChat,
|
handleDeleteChat,
|
||||||
}: {
|
}: {
|
||||||
chatUserData: ChatUserInfo;
|
chatUserData: ChatUserInfo;
|
||||||
handleSwapUser: () => void;
|
handleSwapUser: () => void;
|
||||||
handleDeleteChat: () => void;
|
handleDeleteChat: () => void;
|
||||||
}) => (
|
}) => (
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger
|
<PopoverTrigger
|
||||||
className={cn(
|
className={cn(
|
||||||
buttonVariants({ variant: "outline", size: "icon" }),
|
buttonVariants({ variant: "outline", size: "icon" }),
|
||||||
"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 size={24} className="text-muted-foreground" />
|
||||||
</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">
|
||||||
<Link
|
<Link
|
||||||
aria-label="Visualizza Profilo Utente"
|
aria-label="Visualizza Profilo Utente"
|
||||||
className={cn(
|
className={cn(
|
||||||
buttonVariants({ variant: "outline" }),
|
buttonVariants({ variant: "outline" }),
|
||||||
"justify-start gap-2",
|
"justify-start gap-2",
|
||||||
)}
|
)}
|
||||||
href={`/area-riservata/admin/user-view/edit-user/${chatUserData.id}`}
|
href={`/area-riservata/admin/user-view/edit-user/${chatUserData.id}`}
|
||||||
>
|
>
|
||||||
<UserCog /> Profilo
|
<UserCog /> Profilo
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
aria-label="Visualizza Ordini Utente"
|
aria-label="Visualizza Ordini Utente"
|
||||||
className={cn(
|
className={cn(
|
||||||
buttonVariants({ variant: "outline" }),
|
buttonVariants({ variant: "outline" }),
|
||||||
"justify-start gap-2",
|
"justify-start gap-2",
|
||||||
)}
|
)}
|
||||||
href={`/area-riservata/admin/user-view/ordini/${chatUserData.id}`}
|
href={`/area-riservata/admin/user-view/ordini/${chatUserData.id}`}
|
||||||
>
|
>
|
||||||
<ShoppingBag /> Ordini
|
<ShoppingBag /> Ordini
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
aria-label="Visualizza Allegati Utente"
|
aria-label="Visualizza Allegati Utente"
|
||||||
className={cn(
|
className={cn(
|
||||||
buttonVariants({ variant: "outline" }),
|
buttonVariants({ variant: "outline" }),
|
||||||
"justify-start gap-2",
|
"justify-start gap-2",
|
||||||
)}
|
)}
|
||||||
href={`/area-riservata/admin/user-view/allegati/${chatUserData.id}`}
|
href={`/area-riservata/admin/user-view/allegati/${chatUserData.id}`}
|
||||||
>
|
>
|
||||||
<Paperclip /> Allegati
|
<Paperclip /> Allegati
|
||||||
</Link>
|
</Link>
|
||||||
<button
|
<button
|
||||||
aria-label="Cambia Utente"
|
aria-label="Cambia Utente"
|
||||||
className={cn(
|
className={cn(
|
||||||
buttonVariants({ variant: "outline" }),
|
buttonVariants({ variant: "outline" }),
|
||||||
"justify-start gap-2",
|
"justify-start gap-2",
|
||||||
)}
|
)}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleSwapUser}
|
onClick={handleSwapUser}
|
||||||
>
|
>
|
||||||
<User /> Login
|
<User /> Login
|
||||||
</button>
|
</button>
|
||||||
<Separator />
|
<Separator />
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button size="sm" variant="destructive">
|
<Button size="sm" variant="destructive">
|
||||||
Cancella Chat
|
Cancella Chat
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Sei assolutamente sicuro?</AlertDialogTitle>
|
<AlertDialogTitle>Sei assolutamente sicuro?</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
Questa azione non può essere annullata. Questo eliminerà
|
Questa azione non può essere annullata. Questo eliminerà
|
||||||
definitivamente la chat e rimuoverà i dati dai nostri server.
|
definitivamente la chat e rimuoverà i dati dai nostri server.
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Annulla</AlertDialogCancel>
|
<AlertDialogCancel>Annulla</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
aria-label="Continua Eliminazione Chat"
|
aria-label="Continua Eliminazione Chat"
|
||||||
className={cn(buttonVariants({ variant: "destructive" }))}
|
className={cn(buttonVariants({ variant: "destructive" }))}
|
||||||
onClick={handleDeleteChat}
|
onClick={handleDeleteChat}
|
||||||
>
|
>
|
||||||
Continua
|
Continua
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
</div>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,52 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { ChatsChatid } from "~/schemas/public/Chats";
|
import { memo } from "react";
|
||||||
import ChatBottombar from "~/components/chat/chat-bottombar";
|
import ChatBottombar from "~/components/chat/chat-bottombar";
|
||||||
import { ChatList } from "~/components/chat/chat-list";
|
import { ChatList } from "~/components/chat/chat-list";
|
||||||
import ChatTopbar from "~/components/chat/chat-topbar";
|
import ChatTopbar from "~/components/chat/chat-topbar";
|
||||||
import { useLiveChat } from "~/hooks/chatHooks";
|
import { useLiveChat } from "~/hooks/chatHooks";
|
||||||
import { api } from "~/utils/api";
|
import type { ChatsChatid } from "~/schemas/public/Chats";
|
||||||
import type { Session } from "~/server/api/trpc";
|
import type { Session } from "~/server/api/trpc";
|
||||||
import { memo } from "react";
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
const Chat = memo(
|
const Chat = memo(
|
||||||
({ chatId, userData }: { chatId: ChatsChatid; userData: Session }) => {
|
({ chatId, userData }: { chatId: ChatsChatid; userData: Session }) => {
|
||||||
const liveChat = useLiveChat(chatId, userData);
|
const liveChat = useLiveChat(chatId, userData);
|
||||||
const [userinfo] = api.chat.getUserInfosByChat.useSuspenseQuery({
|
const [userinfo] = api.chat.getUserInfosByChat.useSuspenseQuery({
|
||||||
chatId,
|
chatId,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col justify-between">
|
<div className="flex h-full w-full flex-col justify-between">
|
||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
<ChatTopbar
|
<ChatTopbar
|
||||||
chatId={chatId}
|
chatId={chatId}
|
||||||
userData={userData}
|
userData={userData}
|
||||||
chatUserData={userinfo}
|
chatUserData={userinfo}
|
||||||
onlineStatus={liveChat.onlineUsers}
|
onlineStatus={liveChat.onlineUsers}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ChatList
|
<ChatList
|
||||||
chatId={chatId}
|
chatId={chatId}
|
||||||
userData={userData}
|
userData={userData}
|
||||||
messages={liveChat.messages}
|
messages={liveChat.messages}
|
||||||
query={liveChat.query}
|
query={liveChat.query}
|
||||||
typingStatus={liveChat.typingStatus}
|
typingStatus={liveChat.typingStatus}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="shrink-0 border-t">
|
<div className="shrink-0 border-t">
|
||||||
<ChatBottombar
|
<ChatBottombar
|
||||||
chatId={chatId}
|
chatId={chatId}
|
||||||
userData={userData}
|
userData={userData}
|
||||||
chatUserData={userinfo}
|
chatUserData={userinfo}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
(prevProps, nextProps) =>
|
(prevProps, nextProps) =>
|
||||||
prevProps.chatId === nextProps.chatId &&
|
prevProps.chatId === nextProps.chatId &&
|
||||||
prevProps.userData.id === nextProps.userData.id,
|
prevProps.userData.id === nextProps.userData.id,
|
||||||
);
|
);
|
||||||
Chat.displayName = "Chat";
|
Chat.displayName = "Chat";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,46 @@
|
||||||
// @hidden
|
// @hidden
|
||||||
export default function MessageLoading() {
|
export default function MessageLoading() {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
width="24"
|
width="24"
|
||||||
height="24"
|
height="24"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
className="text-foreground"
|
className="text-foreground"
|
||||||
>
|
>
|
||||||
<circle cx="4" cy="12" r="2" fill="currentColor">
|
<title>Loading</title>
|
||||||
<animate
|
<circle cx="4" cy="12" r="2" fill="currentColor">
|
||||||
id="spinner_qFRN"
|
<animate
|
||||||
begin="0;spinner_OcgL.end+0.25s"
|
id="spinner_qFRN"
|
||||||
attributeName="cy"
|
begin="0;spinner_OcgL.end+0.25s"
|
||||||
calcMode="spline"
|
attributeName="cy"
|
||||||
dur="0.6s"
|
calcMode="spline"
|
||||||
values="12;6;12"
|
dur="0.6s"
|
||||||
keySplines=".33,.66,.66,1;.33,0,.66,.33"
|
values="12;6;12"
|
||||||
/>
|
keySplines=".33,.66,.66,1;.33,0,.66,.33"
|
||||||
</circle>
|
/>
|
||||||
<circle cx="12" cy="12" r="2" fill="currentColor">
|
</circle>
|
||||||
<animate
|
<circle cx="12" cy="12" r="2" fill="currentColor">
|
||||||
begin="spinner_qFRN.begin+0.1s"
|
<animate
|
||||||
attributeName="cy"
|
begin="spinner_qFRN.begin+0.1s"
|
||||||
calcMode="spline"
|
attributeName="cy"
|
||||||
dur="0.6s"
|
calcMode="spline"
|
||||||
values="12;6;12"
|
dur="0.6s"
|
||||||
keySplines=".33,.66,.66,1;.33,0,.66,.33"
|
values="12;6;12"
|
||||||
/>
|
keySplines=".33,.66,.66,1;.33,0,.66,.33"
|
||||||
</circle>
|
/>
|
||||||
<circle cx="20" cy="12" r="2" fill="currentColor">
|
</circle>
|
||||||
<animate
|
<circle cx="20" cy="12" r="2" fill="currentColor">
|
||||||
id="spinner_OcgL"
|
<animate
|
||||||
begin="spinner_qFRN.begin+0.2s"
|
id="spinner_OcgL"
|
||||||
attributeName="cy"
|
begin="spinner_qFRN.begin+0.2s"
|
||||||
calcMode="spline"
|
attributeName="cy"
|
||||||
dur="0.6s"
|
calcMode="spline"
|
||||||
values="12;6;12"
|
dur="0.6s"
|
||||||
keySplines=".33,.66,.66,1;.33,0,.66,.33"
|
values="12;6;12"
|
||||||
/>
|
keySplines=".33,.66,.66,1;.33,0,.66,.33"
|
||||||
</circle>
|
/>
|
||||||
</svg>
|
</circle>
|
||||||
);
|
</svg>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,184 +1,187 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { useRouter } from "next/router";
|
import { CircleOff } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { api } from "~/utils/api";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import Input from "~/components/custom_ui/input";
|
import Input from "~/components/custom_ui/input";
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
import { StaggeredFade } from "~/components/custom_ui/staggered-fade";
|
import { StaggeredFade } from "~/components/custom_ui/staggered-fade";
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
|
||||||
import { useClickOutside } from "~/hooks/useClickOutside";
|
import { useClickOutside } from "~/hooks/useClickOutside";
|
||||||
import { CircleOff } from "lucide-react";
|
import { cn } from "~/lib/utils";
|
||||||
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
export const CodiceBox = ({
|
export const CodiceBox = ({
|
||||||
className,
|
className,
|
||||||
optionBoxClassName,
|
optionBoxClassName,
|
||||||
optionsClassName,
|
optionsClassName,
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
optionBoxClassName?: string;
|
optionBoxClassName?: string;
|
||||||
optionsClassName?: string;
|
optionsClassName?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { data: codici, isLoading: loading } = api.annunci.getCodici.useQuery();
|
const { data: codici, isLoading: loading } = api.annunci.getCodici.useQuery();
|
||||||
const { locale } = useTranslation();
|
const { locale } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AutocompleteSearchBox
|
<AutocompleteSearchBox
|
||||||
key={locale}
|
key={locale}
|
||||||
options={codici || []}
|
options={codici || []}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
className={className}
|
className={className}
|
||||||
optionBoxClassName={optionBoxClassName}
|
optionBoxClassName={optionBoxClassName}
|
||||||
optionsClassName={optionsClassName}
|
optionsClassName={optionsClassName}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type SearchBoxProps = {
|
type SearchBoxProps = {
|
||||||
options: string[];
|
options: string[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
optionBoxClassName?: string;
|
optionBoxClassName?: string;
|
||||||
optionsClassName?: string;
|
optionsClassName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AutocompleteSearchBox = ({
|
const AutocompleteSearchBox = ({
|
||||||
options,
|
options,
|
||||||
loading,
|
loading,
|
||||||
className,
|
className,
|
||||||
optionBoxClassName,
|
optionBoxClassName,
|
||||||
optionsClassName,
|
optionsClassName,
|
||||||
}: SearchBoxProps) => {
|
}: SearchBoxProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [open, setOpen] = useState<boolean>(false);
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
const [results, setResults] = useState<string[]>(options);
|
const [results, setResults] = useState<string[]>(options);
|
||||||
const [isFocused, setIsFocused] = useState<boolean>(false);
|
const [isFocused, setIsFocused] = useState<boolean>(false);
|
||||||
const [searchValue, setSearchValue] = useState<string>("");
|
const [searchValue, setSearchValue] = useState<string>("");
|
||||||
const [navigationEnter, setNavigationEnter] = useState<boolean>(false);
|
const [navigationEnter, setNavigationEnter] = useState<boolean>(false);
|
||||||
|
|
||||||
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
||||||
const listRef = useRef<HTMLDivElement | null>(null);
|
const listRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
useClickOutside([wrapperRef, listRef], () => setOpen(false));
|
useClickOutside([wrapperRef, listRef], () => setOpen(false));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchValue.trim() == "" || searchValue.length == 0) {
|
if (searchValue.trim() === "" || searchValue.length === 0) {
|
||||||
setResults(options);
|
setResults(options);
|
||||||
} else {
|
} else {
|
||||||
setResults(
|
setResults(
|
||||||
options.filter((option) => option.includes(searchValue.trim())),
|
options.filter((option) => option.includes(searchValue.trim())),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [searchValue, options]);
|
}, [searchValue, options]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={wrapperRef} className="z-[5000] w-full">
|
<div ref={wrapperRef} className="z-[5000] w-full">
|
||||||
<div className="relative flex w-full items-center justify-center rounded-md bg-white">
|
<div className="relative flex w-full items-center justify-center rounded-md bg-white">
|
||||||
<label htmlFor="search" className="sr-only">
|
<label htmlFor="search" className="sr-only">
|
||||||
Search Box
|
Search Box
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
id="search"
|
id="search"
|
||||||
type="search"
|
type="search"
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
onChange={(e) => setSearchValue(e.target.value)}
|
onChange={(e) => setSearchValue(e.target.value)}
|
||||||
onFocus={() => {
|
onFocus={() => {
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
setIsFocused(true);
|
setIsFocused(true);
|
||||||
}}
|
}}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
setIsFocused(false);
|
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(
|
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",
|
"placeholder:text-primary search-cancel:brightness-75 search-cancel:grayscale z-10 cursor-pointer rounded-md bg-transparent text-xl placeholder:font-bold",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<MovingText hidden={isFocused || searchValue != ""} />
|
<MovingText hidden={isFocused || searchValue !== ""} />
|
||||||
</div>
|
</div>
|
||||||
{open && (
|
{open && (
|
||||||
<div
|
<div
|
||||||
ref={listRef}
|
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,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ul
|
<ul
|
||||||
className={cn(
|
className={cn(
|
||||||
"max-h-64 w-full overflow-y-auto",
|
"max-h-64 w-full overflow-y-auto",
|
||||||
navigationEnter &&
|
navigationEnter &&
|
||||||
"[&>li:first-child>a]:bg-neutral-100 [&>li:first-child>a]:underline [&>li:first-child>a]:underline-offset-2",
|
"[&>li:first-child>a]:bg-neutral-100 [&>li:first-child>a]:underline [&>li:first-child>a]:underline-offset-2",
|
||||||
)}
|
)}
|
||||||
role="listbox"
|
// biome-ignore lint/a11y/noNoninteractiveElementToInteractiveRole: <need list>
|
||||||
>
|
role="listbox"
|
||||||
{loading ? (
|
>
|
||||||
<li className={"px-3 py-2 text-center text-xl text-neutral-600"}>
|
{loading ? (
|
||||||
<span>Caricamento...</span>
|
<li className={"px-3 py-2 text-center text-xl text-neutral-600"}>
|
||||||
</li>
|
<span>Caricamento...</span>
|
||||||
) : (
|
</li>
|
||||||
<>
|
) : (
|
||||||
{results.length === 0 && (
|
<>
|
||||||
<li
|
{results.length === 0 && (
|
||||||
className={"px-3 py-2 text-center text-xl text-neutral-600"}
|
<li
|
||||||
>
|
className={"px-3 py-2 text-center text-xl text-neutral-600"}
|
||||||
<span>
|
>
|
||||||
<CircleOff className="inline size-6" />
|
<span>
|
||||||
</span>
|
<CircleOff className="inline size-6" />
|
||||||
</li>
|
</span>
|
||||||
)}
|
</li>
|
||||||
|
)}
|
||||||
|
|
||||||
{results.map((item) => (
|
{results.map((item) => (
|
||||||
<li key={item}>
|
<li key={item}>
|
||||||
<Link
|
<Link
|
||||||
href={`/annuncio/${item}`}
|
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,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{item}
|
{item}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const MovingText = ({ hidden }: { hidden: boolean }) => {
|
const MovingText = ({ hidden }: { hidden: boolean }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [randomFrase, setRandomFrase] = useState<string>(
|
const [randomFrase, setRandomFrase] = useState<string>(
|
||||||
t.code_search_frases[0]!,
|
// biome-ignore lint/style/noNonNullAssertion: <exists>
|
||||||
);
|
t.code_search_frases[0]!,
|
||||||
useEffect(() => {
|
);
|
||||||
const interval = setInterval(() => {
|
useEffect(() => {
|
||||||
const randomIndex = Math.floor(
|
const interval = setInterval(() => {
|
||||||
Math.random() * t.code_search_frases.length,
|
const randomIndex = Math.floor(
|
||||||
);
|
Math.random() * t.code_search_frases.length,
|
||||||
setRandomFrase(t.code_search_frases[randomIndex]!);
|
);
|
||||||
}, 5000);
|
// biome-ignore lint/style/noNonNullAssertion: <exists>
|
||||||
|
setRandomFrase(t.code_search_frases[randomIndex]!);
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StaggeredFade
|
<StaggeredFade
|
||||||
key={randomFrase}
|
key={randomFrase}
|
||||||
text={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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,70 +1,70 @@
|
||||||
import { forwardRef, useMemo, useState } from "react";
|
import { forwardRef, useMemo, useState } from "react";
|
||||||
import { HexColorPicker } from "react-colorful";
|
import { HexColorPicker } from "react-colorful";
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
import { useForwardedRef } from "~/lib/use_fowarded_ref";
|
|
||||||
import type { ButtonProps } from "~/components/ui/button";
|
import type { ButtonProps } from "~/components/ui/button";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
} from "~/components/ui/popover";
|
|
||||||
import { Input } from "~/components/ui/input";
|
import { Input } from "~/components/ui/input";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "~/components/ui/popover";
|
||||||
|
import { useForwardedRef } from "~/lib/use_fowarded_ref";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
interface ColorPickerProps {
|
interface ColorPickerProps {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
onBlur?: () => void;
|
onBlur?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ColorPicker = forwardRef<
|
const ColorPicker = forwardRef<
|
||||||
HTMLInputElement,
|
HTMLInputElement,
|
||||||
Omit<ButtonProps, "value" | "onChange" | "onBlur"> & ColorPickerProps
|
Omit<ButtonProps, "value" | "onChange" | "onBlur"> & ColorPickerProps
|
||||||
>(
|
>(
|
||||||
(
|
(
|
||||||
{ disabled, value, onChange, onBlur, name, className, ...props },
|
{ disabled, value, onChange, onBlur, name, className, ...props },
|
||||||
forwardedRef,
|
forwardedRef,
|
||||||
) => {
|
) => {
|
||||||
const ref = useForwardedRef(forwardedRef);
|
const ref = useForwardedRef(forwardedRef);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const parsedValue = useMemo(() => {
|
const parsedValue = useMemo(() => {
|
||||||
return value || "#FFFFFF";
|
return value || "#FFFFFF";
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover onOpenChange={setOpen} open={open}>
|
<Popover onOpenChange={setOpen} open={open}>
|
||||||
<PopoverTrigger asChild disabled={disabled} onBlur={onBlur}>
|
<PopoverTrigger asChild disabled={disabled} onBlur={onBlur}>
|
||||||
<Button
|
<Button
|
||||||
{...props}
|
{...props}
|
||||||
className={cn("block", className)}
|
className={cn("block", className)}
|
||||||
name={name}
|
name={name}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
}}
|
}}
|
||||||
size="icon"
|
size="icon"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: parsedValue,
|
backgroundColor: parsedValue,
|
||||||
}}
|
}}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
<div />
|
<div />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-full">
|
<PopoverContent className="w-full">
|
||||||
<HexColorPicker color={parsedValue} onChange={onChange} />
|
<HexColorPicker color={parsedValue} onChange={onChange} />
|
||||||
<Input
|
<Input
|
||||||
maxLength={7}
|
maxLength={7}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
onChange(e?.currentTarget?.value);
|
onChange(e?.currentTarget?.value);
|
||||||
}}
|
}}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
value={parsedValue}
|
value={parsedValue}
|
||||||
/>
|
/>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
ColorPicker.displayName = "ColorPicker";
|
ColorPicker.displayName = "ColorPicker";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,74 +3,74 @@ import Image from "next/image";
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
|
|
||||||
export const ComeFunziona = () => {
|
export const ComeFunziona = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const features = [
|
const features = [
|
||||||
{
|
{
|
||||||
image: "/come_funziona_ricerca.jpg",
|
image: "/come_funziona_ricerca.jpg",
|
||||||
icon: <Search className="size-8" />,
|
icon: <Search className="size-8" />,
|
||||||
title: t.come_funziona.features.ricerca.title,
|
title: t.come_funziona.features.ricerca.title,
|
||||||
desc: t.come_funziona.features.ricerca.desc,
|
desc: t.come_funziona.features.ricerca.desc,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
image: "/come_funziona_aggiungi.jpg",
|
image: "/come_funziona_aggiungi.jpg",
|
||||||
icon: <HeartPlus className="size-8" />,
|
icon: <HeartPlus className="size-8" />,
|
||||||
title: t.come_funziona.features.aggiungi.title,
|
title: t.come_funziona.features.aggiungi.title,
|
||||||
desc: t.come_funziona.features.aggiungi.desc,
|
desc: t.come_funziona.features.aggiungi.desc,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
image: "/come_funziona_meet.jpg",
|
image: "/come_funziona_meet.jpg",
|
||||||
icon: <Handshake className="size-8" />,
|
icon: <Handshake className="size-8" />,
|
||||||
title: t.come_funziona.features.contatta.title,
|
title: t.come_funziona.features.contatta.title,
|
||||||
desc: t.come_funziona.features.contatta.desc,
|
desc: t.come_funziona.features.contatta.desc,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
image: "/come_funziona_confirm.jpg",
|
image: "/come_funziona_confirm.jpg",
|
||||||
icon: <Home className="size-8" />,
|
icon: <Home className="size-8" />,
|
||||||
title: t.come_funziona.features.conferma.title,
|
title: t.come_funziona.features.conferma.title,
|
||||||
desc: t.come_funziona.features.conferma.desc,
|
desc: t.come_funziona.features.conferma.desc,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
image: "/come_funziona_contratto.jpg",
|
image: "/come_funziona_contratto.jpg",
|
||||||
icon: <ScrollText className="size-8" />,
|
icon: <ScrollText className="size-8" />,
|
||||||
title: t.come_funziona.features.firma.title,
|
title: t.come_funziona.features.firma.title,
|
||||||
desc: t.come_funziona.features.firma.desc,
|
desc: t.come_funziona.features.firma.desc,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full max-w-6xl py-2">
|
<div className="mx-auto w-full max-w-6xl py-2">
|
||||||
<h2 className="max-w-xl text-3xl font-semibold tracking-tight md:mx-auto md:text-center md:text-4xl md:leading-[3.5rem]">
|
<h2 className="max-w-xl text-3xl font-semibold tracking-tight md:mx-auto md:text-center md:text-4xl md:leading-[3.5rem]">
|
||||||
{t.come_funziona.title}
|
{t.come_funziona.title}
|
||||||
</h2>
|
</h2>
|
||||||
<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}
|
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"
|
||||||
>
|
>
|
||||||
<div className="flex shrink-0 basis-1/2 justify-center">
|
<div className="flex shrink-0 basis-1/2 justify-center">
|
||||||
<Image
|
<Image
|
||||||
className="border-border/50 aspect-[6/4] rounded-xl border"
|
className="border-border/50 aspect-[6/4] rounded-xl border"
|
||||||
src={feature.image}
|
src={feature.image}
|
||||||
width={400}
|
width={400}
|
||||||
height={200}
|
height={200}
|
||||||
alt={"img" + idx}
|
alt={`img${idx}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex basis-1/2 flex-col">
|
<div className="flex basis-1/2 flex-col">
|
||||||
<div className="flex items-center gap-3 text-blue-600">
|
<div className="flex items-center gap-3 text-blue-600">
|
||||||
{feature.icon}
|
{feature.icon}
|
||||||
<h4 className="my-3 text-3xl font-semibold tracking-tight">
|
<h4 className="my-3 text-3xl font-semibold tracking-tight">
|
||||||
{feature.title}
|
{feature.title}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-muted-foreground text-[17px]">
|
<p className="text-muted-foreground text-[17px]">
|
||||||
{feature.desc}
|
{feature.desc}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,63 +1,63 @@
|
||||||
import { cn } from "~/lib/utils";
|
import type { ReactNode } from "react";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogTrigger,
|
AlertDialogAction,
|
||||||
AlertDialogContent,
|
AlertDialogCancel,
|
||||||
AlertDialogHeader,
|
AlertDialogContent,
|
||||||
AlertDialogTitle,
|
AlertDialogDescription,
|
||||||
AlertDialogDescription,
|
AlertDialogFooter,
|
||||||
AlertDialogFooter,
|
AlertDialogHeader,
|
||||||
AlertDialogCancel,
|
AlertDialogTitle,
|
||||||
AlertDialogAction,
|
AlertDialogTrigger,
|
||||||
} from "~/components/ui/alert-dialog";
|
} from "~/components/ui/alert-dialog";
|
||||||
import { buttonVariants } from "~/components/ui/button";
|
import { buttonVariants } from "~/components/ui/button";
|
||||||
import type { ReactNode } from "react";
|
import { cn } from "~/lib/utils";
|
||||||
export const Confirm = ({
|
export const Confirm = ({
|
||||||
children,
|
children,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
onCancel,
|
onCancel,
|
||||||
open,
|
open,
|
||||||
setOpen,
|
setOpen,
|
||||||
onlyHedless,
|
onlyHedless,
|
||||||
cancelText,
|
cancelText,
|
||||||
confirmText,
|
confirmText,
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
setOpen?: (open: boolean) => void;
|
setOpen?: (open: boolean) => void;
|
||||||
onlyHedless?: boolean;
|
onlyHedless?: boolean;
|
||||||
cancelText?: string;
|
cancelText?: string;
|
||||||
confirmText?: string;
|
confirmText?: string;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<AlertDialog open={open} onOpenChange={setOpen}>
|
<AlertDialog open={open} onOpenChange={setOpen}>
|
||||||
{onlyHedless ? null : (
|
{onlyHedless ? null : (
|
||||||
<AlertDialogTrigger asChild>{children}</AlertDialogTrigger>
|
<AlertDialogTrigger asChild>{children}</AlertDialogTrigger>
|
||||||
)}
|
)}
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>{title}</AlertDialogTitle>
|
<AlertDialogTitle>{title}</AlertDialogTitle>
|
||||||
<AlertDialogDescription>{description}</AlertDialogDescription>
|
<AlertDialogDescription>{description}</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel onClick={() => onCancel && onCancel()}>
|
<AlertDialogCancel onClick={() => onCancel?.()}>
|
||||||
{cancelText || "Annulla"}
|
{cancelText || "Annulla"}
|
||||||
</AlertDialogCancel>
|
</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
className={cn(buttonVariants({ variant: "destructive" }))}
|
className={cn(buttonVariants({ variant: "destructive" }))}
|
||||||
onClick={() => onConfirm()}
|
onClick={() => onConfirm()}
|
||||||
>
|
>
|
||||||
{confirmText || "Conferma"}
|
{confirmText || "Conferma"}
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,63 +1,63 @@
|
||||||
import { Minus, Plus } from "lucide-react";
|
import { Minus, Plus } from "lucide-react";
|
||||||
import Input from "~/components/custom_ui/input";
|
import Input from "~/components/custom_ui/input";
|
||||||
export const Counter = ({
|
export const Counter = ({
|
||||||
value,
|
value,
|
||||||
setValue,
|
setValue,
|
||||||
name,
|
name,
|
||||||
onlyPositive,
|
onlyPositive,
|
||||||
disabled,
|
disabled,
|
||||||
}: {
|
}: {
|
||||||
value: number;
|
value: number;
|
||||||
setValue: (value: number) => void;
|
setValue: (value: number) => void;
|
||||||
name: string;
|
name: string;
|
||||||
onlyPositive?: boolean;
|
onlyPositive?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const handleChange = (change: number) => {
|
const handleChange = (change: number) => {
|
||||||
const newValue = value + change;
|
const newValue = value + change;
|
||||||
if (onlyPositive && newValue < 0) return;
|
if (onlyPositive && newValue < 0) return;
|
||||||
setValue(newValue);
|
setValue(newValue);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="w-[9.5rem]">
|
<div className="w-[9.5rem]">
|
||||||
<label htmlFor={name} className="sr-only">
|
<label htmlFor={name} className="sr-only">
|
||||||
{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"
|
type="button"
|
||||||
onClick={() => handleChange(-1)}
|
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}
|
||||||
>
|
>
|
||||||
<Minus />
|
<Minus />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
id={name}
|
id={name}
|
||||||
value={value}
|
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));
|
||||||
} else {
|
} else {
|
||||||
e.target.value = String(value);
|
e.target.value = String(value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="no-spinner h-10 w-16 rounded-md border-none text-center shadow-none disabled:cursor-default sm:text-sm"
|
className="no-spinner h-10 w-16 rounded-md border-none text-center shadow-none disabled:cursor-default sm:text-sm"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleChange(1)}
|
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}
|
||||||
>
|
>
|
||||||
<Plus />
|
<Plus />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,71 +1,71 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
import type {
|
||||||
|
PopoverContentProps,
|
||||||
|
PopoverProps,
|
||||||
|
PopoverTriggerProps,
|
||||||
|
} from "@radix-ui/react-popover";
|
||||||
|
import type {
|
||||||
|
TooltipContentProps,
|
||||||
|
TooltipProps,
|
||||||
|
TooltipTriggerProps,
|
||||||
|
} from "@radix-ui/react-tooltip";
|
||||||
import {
|
import {
|
||||||
type PropsWithChildren,
|
createContext,
|
||||||
createContext,
|
type PropsWithChildren,
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Popover,
|
||||||
TooltipTrigger,
|
PopoverContent,
|
||||||
TooltipContent,
|
PopoverTrigger,
|
||||||
} from "~/components/ui/tooltip";
|
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverTrigger,
|
|
||||||
PopoverContent,
|
|
||||||
} from "~/components/ui/popover";
|
} from "~/components/ui/popover";
|
||||||
import type {
|
import {
|
||||||
TooltipContentProps,
|
Tooltip,
|
||||||
TooltipProps,
|
TooltipContent,
|
||||||
TooltipTriggerProps,
|
TooltipTrigger,
|
||||||
} from "@radix-ui/react-tooltip";
|
} from "~/components/ui/tooltip";
|
||||||
import type {
|
|
||||||
PopoverContentProps,
|
|
||||||
PopoverProps,
|
|
||||||
PopoverTriggerProps,
|
|
||||||
} from "@radix-ui/react-popover";
|
|
||||||
|
|
||||||
const TouchContext = createContext<boolean | undefined>(undefined);
|
const TouchContext = createContext<boolean | undefined>(undefined);
|
||||||
const useTouch = () => useContext(TouchContext);
|
const useTouch = () => useContext(TouchContext);
|
||||||
|
|
||||||
export const TouchProvider = (props: PropsWithChildren) => {
|
export const TouchProvider = (props: PropsWithChildren) => {
|
||||||
const [isTouch, setTouch] = useState<boolean>();
|
const [isTouch, setTouch] = useState<boolean>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTouch(window.matchMedia("(pointer: coarse)").matches);
|
setTouch(window.matchMedia("(pointer: coarse)").matches);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return <TouchContext.Provider value={isTouch} {...props} />;
|
return <TouchContext.Provider value={isTouch} {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HybridTooltip = (props: TooltipProps & PopoverProps) => {
|
export const HybridTooltip = (props: TooltipProps & PopoverProps) => {
|
||||||
const isTouch = useTouch();
|
const isTouch = useTouch();
|
||||||
|
|
||||||
return isTouch ? <Popover {...props} /> : <Tooltip {...props} />;
|
return isTouch ? <Popover {...props} /> : <Tooltip {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HybridTooltipTrigger = (
|
export const HybridTooltipTrigger = (
|
||||||
props: TooltipTriggerProps & PopoverTriggerProps,
|
props: TooltipTriggerProps & PopoverTriggerProps,
|
||||||
) => {
|
) => {
|
||||||
const isTouch = useTouch();
|
const isTouch = useTouch();
|
||||||
|
|
||||||
return isTouch ? (
|
return isTouch ? (
|
||||||
<PopoverTrigger {...props} />
|
<PopoverTrigger {...props} />
|
||||||
) : (
|
) : (
|
||||||
<TooltipTrigger {...props} />
|
<TooltipTrigger {...props} />
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HybridTooltipContent = (
|
export const HybridTooltipContent = (
|
||||||
props: TooltipContentProps & PopoverContentProps,
|
props: TooltipContentProps & PopoverContentProps,
|
||||||
) => {
|
) => {
|
||||||
const isTouch = useTouch();
|
const isTouch = useTouch();
|
||||||
|
|
||||||
return isTouch ? (
|
return isTouch ? (
|
||||||
<PopoverContent {...props} />
|
<PopoverContent {...props} />
|
||||||
) : (
|
) : (
|
||||||
<TooltipContent {...props} />
|
<TooltipContent {...props} />
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,128 +1,127 @@
|
||||||
import { useAnimate, type AnimationSequence } from "framer-motion";
|
import { type AnimationSequence, useAnimate } from "framer-motion";
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import { IconMatrix, type IconType } from "~/components/IconComponents";
|
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
type ComponentPropsWithoutRef,
|
||||||
useImperativeHandle,
|
forwardRef,
|
||||||
type ComponentPropsWithoutRef,
|
useImperativeHandle,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import { IconMatrix, type IconType } from "~/components/IconComponents";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
const randomNumberBetween = (min: number, max: number) => {
|
const randomNumberBetween = (min: number, max: number) => {
|
||||||
return Math.floor(Math.random() * (max - min + 1) + min);
|
return Math.floor(Math.random() * (max - min + 1) + min);
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface AnimatedButtonRef {
|
export interface AnimatedButtonRef {
|
||||||
sparkle: () => void;
|
sparkle: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AnimatedButtonProps extends ComponentPropsWithoutRef<typeof Button> {
|
interface AnimatedButtonProps extends ComponentPropsWithoutRef<typeof Button> {
|
||||||
sparklesRadius?: number;
|
sparklesRadius?: number;
|
||||||
sparklesNumber?: number;
|
sparklesNumber?: number;
|
||||||
sparklesClassName?: string;
|
sparklesClassName?: string;
|
||||||
sparklesIcon?: IconType;
|
sparklesIcon?: IconType;
|
||||||
sparklesScale?: [number, number];
|
sparklesScale?: [number, number];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AnimatedButton = forwardRef<
|
export const AnimatedButton = forwardRef<
|
||||||
AnimatedButtonRef,
|
AnimatedButtonRef,
|
||||||
AnimatedButtonProps
|
AnimatedButtonProps
|
||||||
>(
|
>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
sparklesRadius = 100,
|
sparklesRadius = 100,
|
||||||
sparklesNumber = 20,
|
sparklesNumber = 20,
|
||||||
sparklesClassName = "fill-blue-500 stroke-transparent",
|
sparklesClassName = "fill-blue-500 stroke-transparent",
|
||||||
sparklesIcon = "star",
|
sparklesIcon = "star",
|
||||||
sparklesScale = [1.5, 2.5],
|
sparklesScale = [1.5, 2.5],
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const [scope, animate] = useAnimate();
|
const [scope, animate] = useAnimate();
|
||||||
|
|
||||||
const Sparkles = () => {
|
const Sparkles = () => {
|
||||||
const sparkles = Array.from({ length: sparklesNumber });
|
const sparkles = Array.from({ length: sparklesNumber });
|
||||||
const sparklesAnimation: AnimationSequence = sparkles.map((_, index) => [
|
const sparklesAnimation: AnimationSequence = sparkles.map((_, index) => [
|
||||||
`.sparkle-${index}`,
|
`.sparkle-${index}`,
|
||||||
{
|
{
|
||||||
x: randomNumberBetween(-sparklesRadius, sparklesRadius),
|
x: randomNumberBetween(-sparklesRadius, sparklesRadius),
|
||||||
y: randomNumberBetween(-sparklesRadius, sparklesRadius),
|
y: randomNumberBetween(-sparklesRadius, sparklesRadius),
|
||||||
scale: randomNumberBetween(sparklesScale[0], sparklesScale[1]),
|
scale: randomNumberBetween(sparklesScale[0], sparklesScale[1]),
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
duration: 0.4,
|
duration: 0.4,
|
||||||
at: "<",
|
at: "<",
|
||||||
delay: index * 0.01,
|
delay: index * 0.01,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const sparklesFadeOut: AnimationSequence = sparkles.map((_, index) => [
|
const sparklesFadeOut: AnimationSequence = sparkles.map((_, index) => [
|
||||||
`.sparkle-${index}`,
|
`.sparkle-${index}`,
|
||||||
{
|
{
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
scale: 0,
|
scale: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
duration: 0.3,
|
duration: 0.3,
|
||||||
at: "<",
|
at: "<",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const sparklesReset: AnimationSequence = sparkles.map((_, index) => [
|
const sparklesReset: AnimationSequence = sparkles.map((_, index) => [
|
||||||
`.sparkle-${index}`,
|
`.sparkle-${index}`,
|
||||||
{
|
{
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
duration: 0.000001,
|
duration: 0.000001,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
animate([
|
animate([
|
||||||
...sparklesReset,
|
...sparklesReset,
|
||||||
["button", { scale: 0.8 }, { duration: 0.1, at: "<" }],
|
["button", { scale: 0.8 }, { duration: 0.1, at: "<" }],
|
||||||
["button", { scale: 1 }, { duration: 0.1 }],
|
["button", { scale: 1 }, { duration: 0.1 }],
|
||||||
...sparklesAnimation,
|
...sparklesAnimation,
|
||||||
["button", { duration: 0.000001 }],
|
["button", { duration: 0.000001 }],
|
||||||
...sparklesFadeOut,
|
...sparklesFadeOut,
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
sparkle: Sparkles,
|
sparkle: Sparkles,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={scope} className="w-full">
|
<div ref={scope} className="w-full">
|
||||||
<Button
|
<Button
|
||||||
{...props}
|
{...props}
|
||||||
className={cn("relative", props.className)}
|
className={cn("relative", props.className)}
|
||||||
aria-label="Animated Button"
|
aria-label="Animated Button"
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
<span
|
<span
|
||||||
aria-hidden
|
aria-hidden
|
||||||
className="pointer-events-none absolute inset-0 z-10 block"
|
className="pointer-events-none absolute inset-0 z-10 block"
|
||||||
>
|
>
|
||||||
{Array.from({ length: sparklesNumber }).map((_, index) => (
|
{Array.from({ length: sparklesNumber }).map((_, index) => (
|
||||||
<IconMatrix
|
<IconMatrix
|
||||||
type={sparklesIcon}
|
type={sparklesIcon}
|
||||||
className={cn(
|
className={cn(
|
||||||
// eslint-disable-next-line better-tailwindcss/no-unregistered-classes
|
`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}
|
/>
|
||||||
/>
|
))}
|
||||||
))}
|
</span>
|
||||||
</span>
|
</Button>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
},
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
AnimatedButton.displayName = "AnimatedButton";
|
AnimatedButton.displayName = "AnimatedButton";
|
||||||
|
|
|
||||||
|
|
@ -1,55 +1,56 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import {
|
import {
|
||||||
type MotionValue,
|
type MotionValue,
|
||||||
motion,
|
motion,
|
||||||
useSpring,
|
useSpring,
|
||||||
useTransform,
|
useTransform,
|
||||||
} from "framer-motion";
|
} from "framer-motion";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
//DOC: https://www.cult-ui.com/docs/components/animated-number
|
//DOC: https://www.cult-ui.com/docs/components/animated-number
|
||||||
|
|
||||||
interface AnimatedNumberProps {
|
interface AnimatedNumberProps {
|
||||||
value: number;
|
value: number;
|
||||||
mass?: number;
|
mass?: number;
|
||||||
stiffness?: number;
|
stiffness?: number;
|
||||||
damping?: number;
|
damping?: number;
|
||||||
precision?: number;
|
precision?: number;
|
||||||
format?: (value: number) => string;
|
format?: (value: number) => string;
|
||||||
onAnimationStart?: () => void;
|
onAnimationStart?: () => void;
|
||||||
onAnimationComplete?: () => void;
|
onAnimationComplete?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AnimatedNumber({
|
export function AnimatedNumber({
|
||||||
value,
|
value,
|
||||||
mass = 0.8,
|
mass = 0.8,
|
||||||
stiffness = 75,
|
stiffness = 75,
|
||||||
damping = 15,
|
damping = 15,
|
||||||
precision = 0,
|
precision = 0,
|
||||||
format,
|
format,
|
||||||
onAnimationStart,
|
onAnimationStart,
|
||||||
onAnimationComplete,
|
onAnimationComplete,
|
||||||
}: AnimatedNumberProps) {
|
}: AnimatedNumberProps) {
|
||||||
const spring = useSpring(value, {
|
const spring = useSpring(value, {
|
||||||
mass,
|
mass,
|
||||||
stiffness,
|
stiffness,
|
||||||
damping,
|
damping,
|
||||||
});
|
});
|
||||||
|
|
||||||
const display: MotionValue<string> = useTransform(spring, (current) =>
|
const display: MotionValue<string> = useTransform(spring, (current) =>
|
||||||
format
|
format
|
||||||
? format(parseFloat(current.toFixed(precision)))
|
? format(parseFloat(current.toFixed(precision)))
|
||||||
: parseFloat(current.toFixed(precision)).toLocaleString(),
|
: parseFloat(current.toFixed(precision)).toLocaleString(),
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
spring.set(value);
|
spring.set(value);
|
||||||
if (onAnimationStart) onAnimationStart();
|
if (onAnimationStart) onAnimationStart();
|
||||||
const unsubscribe = spring.on("change", () => {
|
const unsubscribe = spring.on("change", () => {
|
||||||
if (spring.get() === value && onAnimationComplete) onAnimationComplete();
|
if (spring.get() === value && onAnimationComplete) onAnimationComplete();
|
||||||
});
|
});
|
||||||
return () => unsubscribe();
|
return () => unsubscribe();
|
||||||
}, [spring, value, onAnimationStart, onAnimationComplete]);
|
}, [spring, value, onAnimationStart, onAnimationComplete]);
|
||||||
|
|
||||||
return <motion.span>{display}</motion.span>;
|
return <motion.span>{display}</motion.span>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,116 +1,117 @@
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
useEffect,
|
type Ref,
|
||||||
useImperativeHandle,
|
type TextareaHTMLAttributes,
|
||||||
useRef,
|
useEffect,
|
||||||
useState,
|
useImperativeHandle,
|
||||||
type Ref,
|
useRef,
|
||||||
type TextareaHTMLAttributes,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
interface UseAutosizeTextAreaProps {
|
interface UseAutosizeTextAreaProps {
|
||||||
textAreaRef: HTMLTextAreaElement | null;
|
textAreaRef: HTMLTextAreaElement | null;
|
||||||
minHeight?: number;
|
minHeight?: number;
|
||||||
maxHeight?: number;
|
maxHeight?: number;
|
||||||
triggerAutoSize: string;
|
triggerAutoSize: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useAutosizeTextArea = ({
|
const useAutosizeTextArea = ({
|
||||||
textAreaRef,
|
textAreaRef,
|
||||||
triggerAutoSize,
|
triggerAutoSize,
|
||||||
maxHeight = Number.MAX_SAFE_INTEGER,
|
maxHeight = Number.MAX_SAFE_INTEGER,
|
||||||
minHeight = 0,
|
minHeight = 0,
|
||||||
}: UseAutosizeTextAreaProps) => {
|
}: UseAutosizeTextAreaProps) => {
|
||||||
const [init, setInit] = useState(true);
|
const [init, setInit] = useState(true);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// We need to reset the height momentarily to get the correct scrollHeight for the textarea
|
// We need to reset the height momentarily to get the correct scrollHeight for the textarea
|
||||||
const offsetBorder = 2;
|
const offsetBorder = 2;
|
||||||
if (textAreaRef) {
|
if (textAreaRef) {
|
||||||
if (init) {
|
if (init) {
|
||||||
textAreaRef.style.minHeight = `${minHeight + offsetBorder}px`;
|
textAreaRef.style.minHeight = `${minHeight + offsetBorder}px`;
|
||||||
if (maxHeight > minHeight) {
|
if (maxHeight > minHeight) {
|
||||||
textAreaRef.style.maxHeight = `${maxHeight}px`;
|
textAreaRef.style.maxHeight = `${maxHeight}px`;
|
||||||
}
|
}
|
||||||
setInit(false);
|
setInit(false);
|
||||||
}
|
}
|
||||||
textAreaRef.style.height = `${minHeight + offsetBorder}px`;
|
textAreaRef.style.height = `${minHeight + offsetBorder}px`;
|
||||||
const scrollHeight = textAreaRef.scrollHeight;
|
const scrollHeight = textAreaRef.scrollHeight;
|
||||||
// We then set the height directly, outside of the render loop
|
// We then set the height directly, outside of the render loop
|
||||||
// Trying to set this with state or a ref will product an incorrect value.
|
// Trying to set this with state or a ref will product an incorrect value.
|
||||||
if (scrollHeight > maxHeight) {
|
if (scrollHeight > maxHeight) {
|
||||||
textAreaRef.style.height = `${maxHeight}px`;
|
textAreaRef.style.height = `${maxHeight}px`;
|
||||||
} else {
|
} else {
|
||||||
textAreaRef.style.height = `${scrollHeight + offsetBorder}px`;
|
textAreaRef.style.height = `${scrollHeight + offsetBorder}px`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [textAreaRef, triggerAutoSize]);
|
}, [textAreaRef, triggerAutoSize]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AutosizeTextAreaRef = {
|
export type AutosizeTextAreaRef = {
|
||||||
textArea: HTMLTextAreaElement;
|
textArea: HTMLTextAreaElement;
|
||||||
maxHeight: number;
|
maxHeight: number;
|
||||||
minHeight: number;
|
minHeight: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AutosizeTextAreaProps = {
|
type AutosizeTextAreaProps = {
|
||||||
maxHeight?: number;
|
maxHeight?: number;
|
||||||
minHeight?: number;
|
minHeight?: number;
|
||||||
} & TextareaHTMLAttributes<HTMLTextAreaElement>;
|
} & TextareaHTMLAttributes<HTMLTextAreaElement>;
|
||||||
|
|
||||||
export const AutosizeTextarea = forwardRef<
|
export const AutosizeTextarea = forwardRef<
|
||||||
AutosizeTextAreaRef,
|
AutosizeTextAreaRef,
|
||||||
AutosizeTextAreaProps
|
AutosizeTextAreaProps
|
||||||
>(
|
>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
maxHeight = Number.MAX_SAFE_INTEGER,
|
maxHeight = Number.MAX_SAFE_INTEGER,
|
||||||
minHeight = 56,
|
minHeight = 56,
|
||||||
className,
|
className,
|
||||||
onChange,
|
onChange,
|
||||||
value,
|
value,
|
||||||
...props
|
...props
|
||||||
}: AutosizeTextAreaProps,
|
}: AutosizeTextAreaProps,
|
||||||
ref: Ref<AutosizeTextAreaRef>,
|
ref: Ref<AutosizeTextAreaRef>,
|
||||||
) => {
|
) => {
|
||||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
|
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
const [triggerAutoSize, setTriggerAutoSize] = useState("");
|
const [triggerAutoSize, setTriggerAutoSize] = useState("");
|
||||||
|
|
||||||
useAutosizeTextArea({
|
useAutosizeTextArea({
|
||||||
textAreaRef: textAreaRef.current,
|
textAreaRef: textAreaRef.current,
|
||||||
triggerAutoSize: triggerAutoSize,
|
triggerAutoSize: triggerAutoSize,
|
||||||
maxHeight,
|
maxHeight,
|
||||||
minHeight,
|
minHeight,
|
||||||
});
|
});
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
textArea: textAreaRef.current!,
|
// biome-ignore lint/style/noNonNullAssertion: <it exists>
|
||||||
focus: () => textAreaRef.current?.focus(),
|
textArea: textAreaRef.current!,
|
||||||
maxHeight,
|
focus: () => textAreaRef.current?.focus(),
|
||||||
minHeight,
|
maxHeight,
|
||||||
}));
|
minHeight,
|
||||||
|
}));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (value || props?.defaultValue) {
|
if (value || props?.defaultValue) {
|
||||||
setTriggerAutoSize(value as string);
|
setTriggerAutoSize(value as string);
|
||||||
}
|
}
|
||||||
}, [value || props?.defaultValue]);
|
}, [value || props?.defaultValue]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<textarea
|
<textarea
|
||||||
{...props}
|
{...props}
|
||||||
value={value}
|
value={value}
|
||||||
ref={textAreaRef}
|
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,
|
||||||
)}
|
)}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setTriggerAutoSize(e.target.value);
|
setTriggerAutoSize(e.target.value);
|
||||||
onChange?.(e);
|
onChange?.(e);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
AutosizeTextarea.displayName = "AutosizeTextarea";
|
AutosizeTextarea.displayName = "AutosizeTextarea";
|
||||||
|
|
|
||||||
|
|
@ -1,57 +1,57 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useMediaQuery } from "~/hooks/use-media-query";
|
import { createContext, type ReactNode, useContext } from "react";
|
||||||
|
import type { Drawer as DrawerPrimitive } from "vaul";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogClose,
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "~/components/ui/dialog";
|
} from "~/components/ui/dialog";
|
||||||
import {
|
import {
|
||||||
Drawer,
|
Drawer,
|
||||||
DrawerClose,
|
DrawerClose,
|
||||||
DrawerContent,
|
DrawerContent,
|
||||||
DrawerDescription,
|
DrawerDescription,
|
||||||
DrawerFooter,
|
DrawerFooter,
|
||||||
DrawerHeader,
|
DrawerHeader,
|
||||||
//DrawerNested,
|
//DrawerNested,
|
||||||
DrawerTitle,
|
DrawerTitle,
|
||||||
DrawerTrigger,
|
DrawerTrigger,
|
||||||
} from "~/components/ui/drawer";
|
} from "~/components/ui/drawer";
|
||||||
import { type Drawer as DrawerPrimitive } from "vaul";
|
import { useMediaQuery } from "~/hooks/use-media-query";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
import { createContext, useContext, type ReactNode } from "react";
|
|
||||||
|
|
||||||
interface BaseProps {
|
interface BaseProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RootCredenzaProps extends BaseProps {
|
interface RootCredenzaProps extends BaseProps {
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
onOpenChange?: (open: boolean) => void;
|
onOpenChange?: (open: boolean) => void;
|
||||||
drawerProps?: React.ComponentProps<typeof DrawerPrimitive.Root>;
|
drawerProps?: React.ComponentProps<typeof DrawerPrimitive.Root>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CredenzaProps extends BaseProps {
|
interface CredenzaProps extends BaseProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
asChild?: true;
|
asChild?: true;
|
||||||
}
|
}
|
||||||
const CredenzaContext = createContext<{ isDesktop: boolean }>({
|
const CredenzaContext = createContext<{ isDesktop: boolean }>({
|
||||||
isDesktop: false,
|
isDesktop: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const useCredenzaContext = () => {
|
const useCredenzaContext = () => {
|
||||||
const context = useContext(CredenzaContext);
|
const context = useContext(CredenzaContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Credenza components cannot be rendered outside the Credenza Context",
|
"Credenza components cannot be rendered outside the Credenza Context",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Drawer-Dialog component
|
* Drawer-Dialog component
|
||||||
|
|
@ -91,19 +91,19 @@ const useCredenzaContext = () => {
|
||||||
* </Credenza>
|
* </Credenza>
|
||||||
*/
|
*/
|
||||||
const Credenza = ({ children, drawerProps, ...props }: RootCredenzaProps) => {
|
const Credenza = ({ children, drawerProps, ...props }: RootCredenzaProps) => {
|
||||||
const isDesktop = useMediaQuery("(min-width: 768px)");
|
const isDesktop = useMediaQuery("(min-width: 768px)");
|
||||||
const Credenza = isDesktop ? Dialog : Drawer;
|
const Credenza = isDesktop ? Dialog : Drawer;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CredenzaContext.Provider value={{ isDesktop }}>
|
<CredenzaContext.Provider value={{ isDesktop }}>
|
||||||
<Credenza
|
<Credenza
|
||||||
{...props}
|
{...props}
|
||||||
{...(!isDesktop && { ...drawerProps, autoFocus: true })}
|
{...(!isDesktop && { ...drawerProps, autoFocus: true })}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Credenza>
|
</Credenza>
|
||||||
</CredenzaContext.Provider>
|
</CredenzaContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/*const CredenzaNested = ({
|
/*const CredenzaNested = ({
|
||||||
|
|
@ -127,103 +127,103 @@ const Credenza = ({ children, drawerProps, ...props }: RootCredenzaProps) => {
|
||||||
};*/
|
};*/
|
||||||
|
|
||||||
const CredenzaTrigger = ({ className, children, ...props }: CredenzaProps) => {
|
const CredenzaTrigger = ({ className, children, ...props }: CredenzaProps) => {
|
||||||
const { isDesktop } = useCredenzaContext();
|
const { isDesktop } = useCredenzaContext();
|
||||||
const CredenzaTrigger = isDesktop ? DialogTrigger : DrawerTrigger;
|
const CredenzaTrigger = isDesktop ? DialogTrigger : DrawerTrigger;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CredenzaTrigger className={className} {...props}>
|
<CredenzaTrigger className={className} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</CredenzaTrigger>
|
</CredenzaTrigger>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const CredenzaClose = ({ className, children, ...props }: CredenzaProps) => {
|
const CredenzaClose = ({ className, children, ...props }: CredenzaProps) => {
|
||||||
const { isDesktop } = useCredenzaContext();
|
const { isDesktop } = useCredenzaContext();
|
||||||
const CredenzaClose = isDesktop ? DialogClose : DrawerClose;
|
const CredenzaClose = isDesktop ? DialogClose : DrawerClose;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CredenzaClose className={className} {...props}>
|
<CredenzaClose className={className} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</CredenzaClose>
|
</CredenzaClose>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const CredenzaContent = ({ className, children, ...props }: CredenzaProps) => {
|
const CredenzaContent = ({ className, children, ...props }: CredenzaProps) => {
|
||||||
const { isDesktop } = useCredenzaContext();
|
const { isDesktop } = useCredenzaContext();
|
||||||
const CredenzaContent = isDesktop ? DialogContent : DrawerContent;
|
const CredenzaContent = isDesktop ? DialogContent : DrawerContent;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CredenzaContent className={className} {...props}>
|
<CredenzaContent className={className} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</CredenzaContent>
|
</CredenzaContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const CredenzaDescription = ({
|
const CredenzaDescription = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: CredenzaProps) => {
|
}: CredenzaProps) => {
|
||||||
const { isDesktop } = useCredenzaContext();
|
const { isDesktop } = useCredenzaContext();
|
||||||
const CredenzaDescription = isDesktop ? DialogDescription : DrawerDescription;
|
const CredenzaDescription = isDesktop ? DialogDescription : DrawerDescription;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CredenzaDescription className={className} {...props}>
|
<CredenzaDescription className={className} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</CredenzaDescription>
|
</CredenzaDescription>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const CredenzaHeader = ({ className, children, ...props }: CredenzaProps) => {
|
const CredenzaHeader = ({ className, children, ...props }: CredenzaProps) => {
|
||||||
const { isDesktop } = useCredenzaContext();
|
const { isDesktop } = useCredenzaContext();
|
||||||
const CredenzaHeader = isDesktop ? DialogHeader : DrawerHeader;
|
const CredenzaHeader = isDesktop ? DialogHeader : DrawerHeader;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CredenzaHeader className={className} {...props}>
|
<CredenzaHeader className={className} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</CredenzaHeader>
|
</CredenzaHeader>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const CredenzaTitle = ({ className, children, ...props }: CredenzaProps) => {
|
const CredenzaTitle = ({ className, children, ...props }: CredenzaProps) => {
|
||||||
const { isDesktop } = useCredenzaContext();
|
const { isDesktop } = useCredenzaContext();
|
||||||
const CredenzaTitle = isDesktop ? DialogTitle : DrawerTitle;
|
const CredenzaTitle = isDesktop ? DialogTitle : DrawerTitle;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CredenzaTitle className={className} {...props}>
|
<CredenzaTitle className={className} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</CredenzaTitle>
|
</CredenzaTitle>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const CredenzaBody = ({ className, children, ...props }: CredenzaProps) => {
|
const CredenzaBody = ({ className, children, ...props }: CredenzaProps) => {
|
||||||
return (
|
return (
|
||||||
<div className={cn("px-4 md:px-0", className)} {...props}>
|
<div className={cn("px-4 md:px-0", className)} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const CredenzaFooter = ({ className, children, ...props }: CredenzaProps) => {
|
const CredenzaFooter = ({ className, children, ...props }: CredenzaProps) => {
|
||||||
const { isDesktop } = useCredenzaContext();
|
const { isDesktop } = useCredenzaContext();
|
||||||
const CredenzaFooter = isDesktop ? DialogFooter : DrawerFooter;
|
const CredenzaFooter = isDesktop ? DialogFooter : DrawerFooter;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CredenzaFooter className={className} {...props}>
|
<CredenzaFooter className={className} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</CredenzaFooter>
|
</CredenzaFooter>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Credenza,
|
Credenza,
|
||||||
//CredenzaNested,
|
//CredenzaNested,
|
||||||
CredenzaTrigger,
|
CredenzaTrigger,
|
||||||
CredenzaClose,
|
CredenzaClose,
|
||||||
CredenzaContent,
|
CredenzaContent,
|
||||||
CredenzaDescription,
|
CredenzaDescription,
|
||||||
CredenzaHeader,
|
CredenzaHeader,
|
||||||
CredenzaTitle,
|
CredenzaTitle,
|
||||||
CredenzaBody,
|
CredenzaBody,
|
||||||
CredenzaFooter,
|
CredenzaFooter,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,207 +1,208 @@
|
||||||
import {
|
import {
|
||||||
type ColumnDef,
|
type ColumnDef,
|
||||||
useReactTable,
|
type ColumnFiltersState,
|
||||||
getCoreRowModel,
|
type ColumnSort,
|
||||||
flexRender,
|
flexRender,
|
||||||
getSortedRowModel,
|
getCoreRowModel,
|
||||||
type VisibilityState,
|
getFilteredRowModel,
|
||||||
type ColumnFiltersState,
|
getPaginationRowModel,
|
||||||
getFilteredRowModel,
|
getSortedRowModel,
|
||||||
type SortingState,
|
type RowSelectionState,
|
||||||
getPaginationRowModel,
|
type SortingState,
|
||||||
type ColumnSort,
|
type Table as TableType,
|
||||||
type RowSelectionState,
|
useReactTable,
|
||||||
type Table as TableType,
|
type VisibilityState,
|
||||||
} from "@tanstack/react-table";
|
} from "@tanstack/react-table";
|
||||||
import {
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||||
Table,
|
import { useEffect, useRef, useState } from "react";
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "~/components/ui/table";
|
|
||||||
import { DataTablePagination } from "~/components/custom_ui/dataTable-pagination";
|
import { DataTablePagination } from "~/components/custom_ui/dataTable-pagination";
|
||||||
import {
|
import {
|
||||||
DataTableToolbar,
|
DataTableToolbar,
|
||||||
type SearchFiltro,
|
type PinnedFiltro,
|
||||||
type PinnedFiltro,
|
type SearchFiltro,
|
||||||
} from "~/components/custom_ui/dataTable-toolbar";
|
} from "~/components/custom_ui/dataTable-toolbar";
|
||||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "~/components/ui/table";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
|
|
||||||
interface DataTableProps<TData, TValue> {
|
interface DataTableProps<TData, TValue> {
|
||||||
columns: ColumnDef<TData, TValue>[];
|
columns: ColumnDef<TData, TValue>[];
|
||||||
data: TData[];
|
data: TData[];
|
||||||
pinnedFiltri?: PinnedFiltro[];
|
pinnedFiltri?: PinnedFiltro[];
|
||||||
hasSelectRow?: boolean;
|
hasSelectRow?: boolean;
|
||||||
searchColumn?: SearchFiltro;
|
searchColumn?: SearchFiltro;
|
||||||
columns_titles: Record<string, string>;
|
columns_titles: Record<string, string>;
|
||||||
defaultSort?: ColumnSort[];
|
defaultSort?: ColumnSort[];
|
||||||
rowOnClick?: (data: TData, rowId: string) => void;
|
rowOnClick?: (data: TData, rowId: string) => void;
|
||||||
highlightedRow?: string;
|
highlightedRow?: string;
|
||||||
defaultColumnVisibility?: VisibilityState;
|
defaultColumnVisibility?: VisibilityState;
|
||||||
tabClassName?: string;
|
tabClassName?: string;
|
||||||
pagination?: boolean;
|
pagination?: boolean;
|
||||||
onStateChange?: () => void;
|
onStateChange?: () => void;
|
||||||
defaultFilterState?: ColumnFiltersState;
|
defaultFilterState?: ColumnFiltersState;
|
||||||
onTableInit?: (table: TableType<TData>) => void;
|
onTableInit?: (table: TableType<TData>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataTable<TData, TValue>({
|
export function DataTable<TData, TValue>({
|
||||||
columns,
|
columns,
|
||||||
data,
|
data,
|
||||||
pinnedFiltri,
|
pinnedFiltri,
|
||||||
hasSelectRow,
|
hasSelectRow,
|
||||||
searchColumn,
|
searchColumn,
|
||||||
columns_titles,
|
columns_titles,
|
||||||
defaultSort,
|
defaultSort,
|
||||||
rowOnClick,
|
rowOnClick,
|
||||||
highlightedRow,
|
highlightedRow,
|
||||||
defaultColumnVisibility,
|
defaultColumnVisibility,
|
||||||
tabClassName,
|
tabClassName,
|
||||||
pagination = true,
|
pagination = true,
|
||||||
onStateChange,
|
onStateChange,
|
||||||
defaultFilterState,
|
defaultFilterState,
|
||||||
onTableInit,
|
onTableInit,
|
||||||
}: DataTableProps<TData, TValue>) {
|
}: DataTableProps<TData, TValue>) {
|
||||||
const [sorting, setSorting] = useState<SortingState>(defaultSort || []);
|
const [sorting, setSorting] = useState<SortingState>(defaultSort || []);
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||||
|
|
||||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>(
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>(
|
||||||
defaultFilterState || [],
|
defaultFilterState || [],
|
||||||
);
|
);
|
||||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
|
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
|
||||||
defaultColumnVisibility || {},
|
defaultColumnVisibility || {},
|
||||||
);
|
);
|
||||||
|
|
||||||
const [globalFilter, setGlobalFilter] = useState<string>("");
|
const [globalFilter, setGlobalFilter] = useState<string>("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onStateChange?.();
|
onStateChange?.();
|
||||||
}, [rowSelection, columnFilters, columnVisibility, sorting, onStateChange]);
|
}, [rowSelection, columnFilters, columnVisibility, sorting, onStateChange]);
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data,
|
data,
|
||||||
columns,
|
columns,
|
||||||
onRowSelectionChange: setRowSelection,
|
onRowSelectionChange: setRowSelection,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
onSortingChange: setSorting,
|
onSortingChange: setSorting,
|
||||||
getSortedRowModel: getSortedRowModel(),
|
getSortedRowModel: getSortedRowModel(),
|
||||||
onColumnFiltersChange: setColumnFilters,
|
onColumnFiltersChange: setColumnFilters,
|
||||||
getFilteredRowModel: getFilteredRowModel(),
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
onColumnVisibilityChange: setColumnVisibility,
|
onColumnVisibilityChange: setColumnVisibility,
|
||||||
onGlobalFilterChange: setGlobalFilter,
|
onGlobalFilterChange: setGlobalFilter,
|
||||||
state: {
|
state: {
|
||||||
globalFilter,
|
globalFilter,
|
||||||
sorting,
|
sorting,
|
||||||
columnFilters,
|
columnFilters,
|
||||||
columnVisibility,
|
columnVisibility,
|
||||||
rowSelection,
|
rowSelection,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const parentRef = useRef<HTMLDivElement>(null);
|
const parentRef = useRef<HTMLDivElement>(null);
|
||||||
const { rows } = table.getRowModel();
|
const { rows } = table.getRowModel();
|
||||||
const virtualizer = useVirtualizer({
|
const virtualizer = useVirtualizer({
|
||||||
count: rows.length,
|
count: rows.length,
|
||||||
getScrollElement: () => parentRef.current,
|
getScrollElement: () => parentRef.current,
|
||||||
estimateSize: () => 34,
|
estimateSize: () => 34,
|
||||||
overscan: 20,
|
overscan: 20,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (table && onTableInit) {
|
if (table && onTableInit) {
|
||||||
onTableInit(table);
|
onTableInit(table);
|
||||||
}
|
}
|
||||||
}, [table, onTableInit]);
|
}, [table, onTableInit]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<DataTableToolbar
|
<DataTableToolbar
|
||||||
table={table}
|
table={table}
|
||||||
pinnedColumns={pinnedFiltri}
|
pinnedColumns={pinnedFiltri}
|
||||||
searchColumn={searchColumn}
|
searchColumn={searchColumn}
|
||||||
columns_titles={columns_titles}
|
columns_titles={columns_titles}
|
||||||
globalFilter={globalFilter}
|
globalFilter={globalFilter}
|
||||||
/>
|
/>
|
||||||
<div className="rounded-md border" ref={parentRef}>
|
<div className="rounded-md border" ref={parentRef}>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
<TableRow key={headerGroup.id}>
|
<TableRow key={headerGroup.id}>
|
||||||
{headerGroup.headers.map((header) => {
|
{headerGroup.headers.map((header) => {
|
||||||
return (
|
return (
|
||||||
<TableHead key={header.id}>
|
<TableHead key={header.id}>
|
||||||
{header.isPlaceholder
|
{header.isPlaceholder
|
||||||
? null
|
? null
|
||||||
: flexRender(
|
: flexRender(
|
||||||
header.column.columnDef.header,
|
header.column.columnDef.header,
|
||||||
header.getContext(),
|
header.getContext(),
|
||||||
)}
|
)}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody className={tabClassName}>
|
<TableBody className={tabClassName}>
|
||||||
{virtualizer.getVirtualItems().length ? (
|
{virtualizer.getVirtualItems().length ? (
|
||||||
virtualizer.getVirtualItems().map((virtualRow, index) => {
|
virtualizer.getVirtualItems().map((virtualRow, index) => {
|
||||||
const row = rows[virtualRow.index]!;
|
// biome-ignore lint/style/noNonNullAssertion: <always exists>
|
||||||
return (
|
const row = rows[virtualRow.index]!;
|
||||||
<TableRow
|
return (
|
||||||
onClick={
|
<TableRow
|
||||||
rowOnClick
|
onClick={
|
||||||
? () => rowOnClick(row.original, row.id)
|
rowOnClick
|
||||||
: undefined
|
? () => rowOnClick(row.original, row.id)
|
||||||
}
|
: undefined
|
||||||
key={row.id}
|
}
|
||||||
data-state={row.getIsSelected() && "selected"}
|
key={row.id}
|
||||||
data-row-id={row.id}
|
data-state={row.getIsSelected() && "selected"}
|
||||||
style={{
|
data-row-id={row.id}
|
||||||
height: `${virtualRow.size}px`,
|
style={{
|
||||||
transform: `translateY(${
|
height: `${virtualRow.size}px`,
|
||||||
virtualRow.start - index * virtualRow.size
|
transform: `translateY(${
|
||||||
}px)`,
|
virtualRow.start - index * virtualRow.size
|
||||||
}}
|
}px)`,
|
||||||
className={cn(
|
}}
|
||||||
rowOnClick && "cursor-pointer",
|
className={cn(
|
||||||
highlightedRow === row.id && "bg-neutral-100/50",
|
rowOnClick && "cursor-pointer",
|
||||||
"group/row",
|
highlightedRow === row.id && "bg-neutral-100/50",
|
||||||
)}
|
"group/row",
|
||||||
>
|
)}
|
||||||
{row.getVisibleCells().map((cell) => {
|
>
|
||||||
return (
|
{row.getVisibleCells().map((cell) => {
|
||||||
<TableCell key={cell.id}>
|
return (
|
||||||
{flexRender(
|
<TableCell key={cell.id}>
|
||||||
cell.column.columnDef.cell,
|
{flexRender(
|
||||||
cell.getContext(),
|
cell.column.columnDef.cell,
|
||||||
)}
|
cell.getContext(),
|
||||||
</TableCell>
|
)}
|
||||||
);
|
</TableCell>
|
||||||
})}
|
);
|
||||||
</TableRow>
|
})}
|
||||||
);
|
</TableRow>
|
||||||
})
|
);
|
||||||
) : (
|
})
|
||||||
<TableRow>
|
) : (
|
||||||
<TableCell
|
<TableRow>
|
||||||
colSpan={columns.length}
|
<TableCell
|
||||||
className="h-24 text-center"
|
colSpan={columns.length}
|
||||||
>
|
className="h-24 text-center"
|
||||||
No results.
|
>
|
||||||
</TableCell>
|
No results.
|
||||||
</TableRow>
|
</TableCell>
|
||||||
)}
|
</TableRow>
|
||||||
</TableBody>
|
)}
|
||||||
</Table>
|
</TableBody>
|
||||||
</div>
|
</Table>
|
||||||
{pagination ? (
|
</div>
|
||||||
<DataTablePagination table={table} hasSelectRow={hasSelectRow} />
|
{pagination ? (
|
||||||
) : null}
|
<DataTablePagination table={table} hasSelectRow={hasSelectRow} />
|
||||||
</div>
|
) : null}
|
||||||
);
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,146 +1,146 @@
|
||||||
import type { Column } from "@tanstack/react-table";
|
import type { Column } from "@tanstack/react-table";
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
} from "~/components/ui/popover";
|
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import { CheckIcon, CirclePlus } from "lucide-react";
|
import { CheckIcon, CirclePlus } from "lucide-react";
|
||||||
import { Separator } from "~/components/ui/separator";
|
import type { ComponentType } from "react";
|
||||||
import { Badge } from "~/components/ui/badge";
|
import { Badge } from "~/components/ui/badge";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
CommandGroup,
|
CommandGroup,
|
||||||
CommandInput,
|
CommandInput,
|
||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList,
|
CommandList,
|
||||||
CommandSeparator,
|
CommandSeparator,
|
||||||
} from "~/components/ui/command";
|
} from "~/components/ui/command";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "~/components/ui/popover";
|
||||||
|
import { Separator } from "~/components/ui/separator";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
import { type ComponentType } from "react";
|
|
||||||
|
|
||||||
interface DataTableFacetedFilterProps<TData, TValue> {
|
interface DataTableFacetedFilterProps<TData, TValue> {
|
||||||
column?: Column<TData, TValue>;
|
column?: Column<TData, TValue>;
|
||||||
title?: string;
|
title?: string;
|
||||||
options: {
|
options: {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
icon?: ComponentType<{ className?: string }>;
|
icon?: ComponentType<{ className?: string }>;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataTableFacetedFilter<TData, TValue>({
|
export function DataTableFacetedFilter<TData, TValue>({
|
||||||
column,
|
column,
|
||||||
title,
|
title,
|
||||||
options,
|
options,
|
||||||
}: DataTableFacetedFilterProps<TData, TValue>) {
|
}: DataTableFacetedFilterProps<TData, TValue>) {
|
||||||
const facets = column?.getFacetedUniqueValues();
|
const facets = column?.getFacetedUniqueValues();
|
||||||
const selectedValues = new Set(column?.getFilterValue() as string[]);
|
const selectedValues = new Set(column?.getFilterValue() as string[]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button variant="outline" size="sm" className="h-8 border-dashed">
|
<Button variant="outline" size="sm" className="h-8 border-dashed">
|
||||||
<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 orientation="vertical" className="mx-2 h-4" />
|
||||||
<Badge
|
<Badge
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="rounded-xs px-1 font-normal lg:hidden"
|
className="rounded-xs px-1 font-normal lg:hidden"
|
||||||
>
|
>
|
||||||
{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"
|
variant="secondary"
|
||||||
className="rounded-xs px-1 font-normal"
|
className="rounded-xs px-1 font-normal"
|
||||||
>
|
>
|
||||||
{selectedValues.size} selected
|
{selectedValues.size} selected
|
||||||
</Badge>
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
options
|
options
|
||||||
.filter((option) => selectedValues.has(option.value))
|
.filter((option) => selectedValues.has(option.value))
|
||||||
.map((option) => (
|
.map((option) => (
|
||||||
<Badge
|
<Badge
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
key={option.value}
|
key={option.value}
|
||||||
className="rounded-xs px-1 font-normal"
|
className="rounded-xs px-1 font-normal"
|
||||||
>
|
>
|
||||||
{option.label}
|
{option.label}
|
||||||
</Badge>
|
</Badge>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-[200px] p-0" align="start">
|
<PopoverContent className="w-[200px] p-0" align="start">
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput placeholder={title} />
|
<CommandInput placeholder={title} />
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>Nessun risultato.</CommandEmpty>
|
<CommandEmpty>Nessun risultato.</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{options.map((option) => {
|
{options.map((option) => {
|
||||||
const isSelected = selectedValues.has(option.value);
|
const isSelected = selectedValues.has(option.value);
|
||||||
return (
|
return (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={option.value}
|
key={option.value}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
selectedValues.delete(option.value);
|
selectedValues.delete(option.value);
|
||||||
} else {
|
} else {
|
||||||
selectedValues.add(option.value);
|
selectedValues.add(option.value);
|
||||||
}
|
}
|
||||||
const filterValues = Array.from(selectedValues);
|
const filterValues = Array.from(selectedValues);
|
||||||
column?.setFilterValue(
|
column?.setFilterValue(
|
||||||
filterValues.length ? filterValues : undefined,
|
filterValues.length ? filterValues : undefined,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-primary mr-2 flex size-4 items-center justify-center rounded-xs border",
|
"border-primary mr-2 flex size-4 items-center justify-center rounded-xs border",
|
||||||
isSelected
|
isSelected
|
||||||
? "bg-primary text-primary-foreground"
|
? "bg-primary text-primary-foreground"
|
||||||
: "opacity-50 [&_svg]:invisible",
|
: "opacity-50 [&_svg]:invisible",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<CheckIcon className={cn("size-4")} />
|
<CheckIcon className={cn("size-4")} />
|
||||||
</div>
|
</div>
|
||||||
{option.icon && (
|
{option.icon && (
|
||||||
<option.icon className="text-muted-foreground mr-2 size-4" />
|
<option.icon className="text-muted-foreground mr-2 size-4" />
|
||||||
)}
|
)}
|
||||||
<span>{option.label}</span>
|
<span>{option.label}</span>
|
||||||
{facets?.get(option.value) && (
|
{facets?.get(option.value) && (
|
||||||
<span className="ml-auto flex size-4 items-center justify-center font-mono text-xs">
|
<span className="ml-auto flex size-4 items-center justify-center font-mono text-xs">
|
||||||
{facets.get(option.value)}
|
{facets.get(option.value)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
{selectedValues.size > 0 && (
|
{selectedValues.size > 0 && (
|
||||||
<>
|
<>
|
||||||
<CommandSeparator />
|
<CommandSeparator />
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
<CommandItem
|
<CommandItem
|
||||||
onSelect={() => column?.setFilterValue(undefined)}
|
onSelect={() => column?.setFilterValue(undefined)}
|
||||||
className="justify-center text-center"
|
className="justify-center text-center"
|
||||||
>
|
>
|
||||||
Reset filtri
|
Reset filtri
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</Command>
|
</Command>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,59 +1,60 @@
|
||||||
import type { Table } from "@tanstack/react-table";
|
import type { Table } from "@tanstack/react-table";
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuCheckboxItem,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "~/components/ui/dropdown-menu";
|
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import { Filter } from "lucide-react";
|
import { Filter } from "lucide-react";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "~/components/ui/dropdown-menu";
|
||||||
|
|
||||||
interface DataTableViewOptionsProps<TData> {
|
interface DataTableViewOptionsProps<TData> {
|
||||||
table: Table<TData>;
|
table: Table<TData>;
|
||||||
columns_titles: Record<string, string>;
|
columns_titles: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataTableViewOptions<TData>({
|
export function DataTableViewOptions<TData>({
|
||||||
table,
|
table,
|
||||||
columns_titles,
|
columns_titles,
|
||||||
}: DataTableViewOptionsProps<TData>) {
|
}: DataTableViewOptionsProps<TData>) {
|
||||||
const hidableColumns = table
|
const hidableColumns = table
|
||||||
.getAllColumns()
|
.getAllColumns()
|
||||||
.filter(
|
.filter(
|
||||||
(column) =>
|
(column) =>
|
||||||
typeof column.accessorFn !== "undefined" && column.getCanHide(),
|
typeof column.accessorFn !== "undefined" && column.getCanHide(),
|
||||||
);
|
);
|
||||||
if (hidableColumns.length > 0)
|
if (hidableColumns.length > 0)
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="ml-auto hidden h-8 lg:flex"
|
className="ml-auto hidden h-8 lg:flex"
|
||||||
>
|
>
|
||||||
<Filter className="mr-2 size-4" />
|
<Filter className="mr-2 size-4" />
|
||||||
Filtro
|
Filtro
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="w-[150px]">
|
<DropdownMenuContent align="end" className="w-[150px]">
|
||||||
<DropdownMenuLabel>Filtra colonne</DropdownMenuLabel>
|
<DropdownMenuLabel>Filtra colonne</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
{hidableColumns.map((column) => {
|
{hidableColumns.map((column) => {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuCheckboxItem
|
<DropdownMenuCheckboxItem
|
||||||
key={column.id}
|
key={column.id}
|
||||||
className="capitalize"
|
className="capitalize"
|
||||||
checked={column.getIsVisible()}
|
checked={column.getIsVisible()}
|
||||||
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
||||||
>
|
>
|
||||||
{columns_titles[column.id]}
|
{columns_titles[column.id]}
|
||||||
</DropdownMenuCheckboxItem>
|
</DropdownMenuCheckboxItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,75 +1,75 @@
|
||||||
import type { Column } from "@tanstack/react-table";
|
import type { Column } from "@tanstack/react-table";
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
ArrowDownIcon,
|
||||||
DropdownMenuContent,
|
ArrowUpIcon,
|
||||||
DropdownMenuItem,
|
ChevronsUpDown,
|
||||||
DropdownMenuSeparator,
|
EyeOff,
|
||||||
DropdownMenuTrigger,
|
} from "lucide-react";
|
||||||
} from "~/components/ui/dropdown-menu";
|
import type { HTMLAttributes } from "react";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import {
|
import {
|
||||||
ArrowDownIcon,
|
DropdownMenu,
|
||||||
ArrowUpIcon,
|
DropdownMenuContent,
|
||||||
ChevronsUpDown,
|
DropdownMenuItem,
|
||||||
EyeOff,
|
DropdownMenuSeparator,
|
||||||
} from "lucide-react";
|
DropdownMenuTrigger,
|
||||||
import { type HTMLAttributes } from "react";
|
} from "~/components/ui/dropdown-menu";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
interface DataTableColumnHeaderProps<TData, TValue>
|
interface DataTableColumnHeaderProps<TData, TValue>
|
||||||
extends HTMLAttributes<HTMLDivElement> {
|
extends HTMLAttributes<HTMLDivElement> {
|
||||||
column: Column<TData, TValue>;
|
column: Column<TData, TValue>;
|
||||||
title: string;
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataTableColumnHeader<TData, TValue>({
|
export function DataTableColumnHeader<TData, TValue>({
|
||||||
column,
|
column,
|
||||||
title,
|
title,
|
||||||
className,
|
className,
|
||||||
}: DataTableColumnHeaderProps<TData, TValue>) {
|
}: DataTableColumnHeaderProps<TData, TValue>) {
|
||||||
if (!column.getCanSort()) {
|
if (!column.getCanSort()) {
|
||||||
return <div className={cn(className)}>{title}</div>;
|
return <div className={cn(className)}>{title}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex items-center space-x-2", className)}>
|
<div className={cn("flex items-center space-x-2", className)}>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="data-[state=open]:bg-accent -ml-3 h-8"
|
className="data-[state=open]:bg-accent -ml-3 h-8"
|
||||||
>
|
>
|
||||||
<span>{title}</span>
|
<span>{title}</span>
|
||||||
{column.getIsSorted() === "desc" ? (
|
{column.getIsSorted() === "desc" ? (
|
||||||
<ArrowDownIcon className="ml-2 size-4" />
|
<ArrowDownIcon className="ml-2 size-4" />
|
||||||
) : column.getIsSorted() === "asc" ? (
|
) : column.getIsSorted() === "asc" ? (
|
||||||
<ArrowUpIcon className="ml-2 size-4" />
|
<ArrowUpIcon className="ml-2 size-4" />
|
||||||
) : (
|
) : (
|
||||||
<ChevronsUpDown className="ml-2 size-4" />
|
<ChevronsUpDown className="ml-2 size-4" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start">
|
<DropdownMenuContent align="start">
|
||||||
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
|
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
|
||||||
<ArrowUpIcon className="text-muted-foreground/70 mr-2 h-3.5 w-3.5" />
|
<ArrowUpIcon className="text-muted-foreground/70 mr-2 h-3.5 w-3.5" />
|
||||||
Asc
|
Asc
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
|
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
|
||||||
<ArrowDownIcon className="text-muted-foreground/70 mr-2 h-3.5 w-3.5" />
|
<ArrowDownIcon className="text-muted-foreground/70 mr-2 h-3.5 w-3.5" />
|
||||||
Desc
|
Desc
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{column.getCanHide() && (
|
{column.getCanHide() && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
|
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
|
||||||
<EyeOff className="text-muted-foreground/70 mr-2 h-3.5 w-3.5" />
|
<EyeOff className="text-muted-foreground/70 mr-2 h-3.5 w-3.5" />
|
||||||
Nascondi
|
Nascondi
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,103 +1,104 @@
|
||||||
import type { Table } from "@tanstack/react-table";
|
import type { Table } from "@tanstack/react-table";
|
||||||
import {
|
import {
|
||||||
Select,
|
ChevronFirst,
|
||||||
SelectContent,
|
ChevronLast,
|
||||||
SelectItem,
|
ChevronLeftIcon,
|
||||||
SelectTrigger,
|
ChevronRightIcon,
|
||||||
SelectValue,
|
} from "lucide-react";
|
||||||
} from "~/components/ui/select";
|
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import {
|
import {
|
||||||
ChevronFirst,
|
Select,
|
||||||
ChevronLast,
|
SelectContent,
|
||||||
ChevronLeftIcon,
|
SelectItem,
|
||||||
ChevronRightIcon,
|
SelectTrigger,
|
||||||
} from "lucide-react";
|
SelectValue,
|
||||||
|
} from "~/components/ui/select";
|
||||||
|
|
||||||
interface DataTablePaginationProps<TData> {
|
interface DataTablePaginationProps<TData> {
|
||||||
table: Table<TData>;
|
table: Table<TData>;
|
||||||
hasSelectRow?: boolean;
|
hasSelectRow?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataTablePagination<TData>({
|
export function DataTablePagination<TData>({
|
||||||
table,
|
table,
|
||||||
hasSelectRow,
|
hasSelectRow,
|
||||||
}: DataTablePaginationProps<TData>) {
|
}: DataTablePaginationProps<TData>) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between px-2">
|
<div className="flex items-center justify-between px-2">
|
||||||
<div className="text-muted-foreground flex-1 text-sm">
|
<div className="text-muted-foreground flex-1 text-sm">
|
||||||
{hasSelectRow && (
|
{hasSelectRow && (
|
||||||
<>
|
<>
|
||||||
{table.getFilteredSelectedRowModel().rows.length} di{" "}
|
{table.getFilteredSelectedRowModel().rows.length} di{" "}
|
||||||
{table.getFilteredRowModel().rows.length} righe selezionate.
|
{table.getFilteredRowModel().rows.length} righe selezionate.
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center gap-x-6 gap-y-2 lg:gap-x-8">
|
<div className="flex flex-wrap items-center gap-x-6 gap-y-2 lg:gap-x-8">
|
||||||
<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}`}
|
value={`${table.getState().pagination.pageSize}`}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
table.setPageSize(Number(value));
|
table.setPageSize(Number(value));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 w-[70px]">
|
<SelectTrigger className="h-8 w-[70px]">
|
||||||
<SelectValue placeholder={table.getState().pagination.pageSize} />
|
<SelectValue placeholder={table.getState().pagination.pageSize} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent side="top">
|
<SelectContent side="top">
|
||||||
{[10, 20, 30, 40, 50].map((pageSize) => (
|
{[10, 20, 30, 40, 50].map((pageSize) => (
|
||||||
<SelectItem key={pageSize} value={`${pageSize}`}>
|
<SelectItem key={pageSize} value={`${pageSize}`}>
|
||||||
{pageSize}
|
{pageSize}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-x-6 lg:gap-x-8">
|
<div className="flex items-center gap-x-6 lg:gap-x-8">
|
||||||
<div className="flex items-center justify-between text-sm font-medium">
|
<div className="flex items-center justify-between text-sm font-medium">
|
||||||
Pagina {table.getState().pagination.pageIndex + 1} di{" "}
|
Pagina {table.getState().pagination.pageIndex + 1} di{" "}
|
||||||
{table.getPageCount()}
|
{table.getPageCount()}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="hidden size-8 p-0 lg:flex"
|
className="hidden size-8 p-0 lg:flex"
|
||||||
onClick={() => table.setPageIndex(0)}
|
onClick={() => table.setPageIndex(0)}
|
||||||
disabled={!table.getCanPreviousPage()}
|
disabled={!table.getCanPreviousPage()}
|
||||||
>
|
>
|
||||||
<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"
|
variant="outline"
|
||||||
className="size-8 p-0"
|
className="size-8 p-0"
|
||||||
onClick={() => table.previousPage()}
|
onClick={() => table.previousPage()}
|
||||||
disabled={!table.getCanPreviousPage()}
|
disabled={!table.getCanPreviousPage()}
|
||||||
>
|
>
|
||||||
<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"
|
variant="outline"
|
||||||
className="size-8 p-0"
|
className="size-8 p-0"
|
||||||
onClick={() => table.nextPage()}
|
onClick={() => table.nextPage()}
|
||||||
disabled={!table.getCanNextPage()}
|
disabled={!table.getCanNextPage()}
|
||||||
>
|
>
|
||||||
<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"
|
variant="outline"
|
||||||
className="hidden size-8 p-0 lg:flex"
|
className="hidden size-8 p-0 lg:flex"
|
||||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||||
disabled={!table.getCanNextPage()}
|
disabled={!table.getCanNextPage()}
|
||||||
>
|
>
|
||||||
<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" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,89 +1,88 @@
|
||||||
import type { Table } from "@tanstack/react-table";
|
import type { Table } from "@tanstack/react-table";
|
||||||
import Input from "~/components/custom_ui/input";
|
|
||||||
import { DataTableViewOptions } from "~/components/custom_ui/dataTable-ViewToggle";
|
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import { DataTableFacetedFilter } from "~/components/custom_ui/dataTable-FacetedFilter";
|
|
||||||
import { RotateCcw } from "lucide-react";
|
import { RotateCcw } from "lucide-react";
|
||||||
import type { ComponentType } from "react";
|
import type { ComponentType } from "react";
|
||||||
|
import { DataTableFacetedFilter } from "~/components/custom_ui/dataTable-FacetedFilter";
|
||||||
|
import { DataTableViewOptions } from "~/components/custom_ui/dataTable-ViewToggle";
|
||||||
|
import Input from "~/components/custom_ui/input";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
|
||||||
export type PinnedFiltro = {
|
export type PinnedFiltro = {
|
||||||
title: string;
|
title: string;
|
||||||
columnName: string;
|
columnName: string;
|
||||||
options: {
|
options: {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
icon?: ComponentType<{ className?: string }>;
|
icon?: ComponentType<{ className?: string }>;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
export type SearchFiltro = {
|
export type SearchFiltro = {
|
||||||
columnName: string;
|
columnName: string;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
};
|
};
|
||||||
export function DataTableToolbar<TData>(props: {
|
export function DataTableToolbar<TData>(props: {
|
||||||
table: Table<TData>;
|
table: Table<TData>;
|
||||||
pinnedColumns?: PinnedFiltro[];
|
pinnedColumns?: PinnedFiltro[];
|
||||||
searchColumn?: SearchFiltro;
|
searchColumn?: SearchFiltro;
|
||||||
columns_titles: Record<string, string>;
|
columns_titles: Record<string, string>;
|
||||||
globalFilter: string;
|
globalFilter: string;
|
||||||
}) {
|
}) {
|
||||||
const { table, pinnedColumns, searchColumn, columns_titles, globalFilter } =
|
const { table, pinnedColumns, searchColumn, columns_titles, globalFilter } =
|
||||||
props;
|
props;
|
||||||
const isFiltered = table.getState().columnFilters.length > 0;
|
const isFiltered = table.getState().columnFilters.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex flex-1 flex-wrap items-center gap-2">
|
<div className="flex flex-1 flex-wrap items-center gap-2">
|
||||||
{searchColumn &&
|
{searchColumn &&
|
||||||
(searchColumn.columnName ? (
|
(searchColumn.columnName ? (
|
||||||
<Input
|
<Input
|
||||||
placeholder={searchColumn.placeholder}
|
placeholder={searchColumn.placeholder}
|
||||||
value={
|
value={
|
||||||
(table
|
(table
|
||||||
.getColumn(searchColumn.columnName)
|
.getColumn(searchColumn.columnName)
|
||||||
?.getFilterValue() as string) ?? ""
|
?.getFilterValue() as string) ?? ""
|
||||||
}
|
}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
table
|
table
|
||||||
.getColumn(searchColumn.columnName)
|
.getColumn(searchColumn.columnName)
|
||||||
?.setFilterValue(event.target.value)
|
?.setFilterValue(event.target.value)
|
||||||
}
|
}
|
||||||
className="mt-0 h-8 w-[150px]"
|
className="mt-0 h-8 w-[150px]"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Input
|
<Input
|
||||||
placeholder={searchColumn.placeholder}
|
placeholder={searchColumn.placeholder}
|
||||||
value={globalFilter ?? ""}
|
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]"
|
className="mt-0 h-8 w-[150px]"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{pinnedColumns &&
|
{pinnedColumns?.map(
|
||||||
pinnedColumns.map(
|
(column) =>
|
||||||
(column) =>
|
table.getColumn(column.columnName) && (
|
||||||
table.getColumn(column.columnName) && (
|
<DataTableFacetedFilter
|
||||||
<DataTableFacetedFilter
|
key={column.columnName}
|
||||||
key={column.columnName}
|
column={table.getColumn(column.columnName)}
|
||||||
column={table.getColumn(column.columnName)}
|
title={column.title}
|
||||||
title={column.title}
|
options={column.options}
|
||||||
options={column.options}
|
/>
|
||||||
/>
|
),
|
||||||
),
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{isFiltered && (
|
{isFiltered && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => table.resetColumnFilters()}
|
onClick={() => table.resetColumnFilters()}
|
||||||
className="h-8 px-2 lg:px-3"
|
className="h-8 px-2 lg:px-3"
|
||||||
>
|
>
|
||||||
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 table={table} columns_titles={columns_titles} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,16 @@
|
||||||
|
|
||||||
// import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
|
// import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
|
||||||
import {
|
import {
|
||||||
type EmojiPickerListCategoryHeaderProps,
|
type EmojiPickerListCategoryHeaderProps,
|
||||||
type EmojiPickerListEmojiProps,
|
type EmojiPickerListEmojiProps,
|
||||||
type EmojiPickerListRowProps,
|
type EmojiPickerListRowProps,
|
||||||
EmojiPicker as EmojiPickerPrimitive,
|
EmojiPicker as EmojiPickerPrimitive,
|
||||||
// type SkinTone,
|
// type SkinTone,
|
||||||
// useActiveEmoji,
|
// useActiveEmoji,
|
||||||
// useSkinTone,
|
// useSkinTone,
|
||||||
} from "frimousse";
|
} from "frimousse";
|
||||||
import { LoaderIcon, SearchIcon } from "lucide-react";
|
import { LoaderIcon, SearchIcon } from "lucide-react";
|
||||||
import * as React from "react";
|
import type * as React from "react";
|
||||||
|
|
||||||
// import { Button } from "~/components/ui/button";
|
// import { Button } from "~/components/ui/button";
|
||||||
// import {
|
// import {
|
||||||
|
|
@ -24,115 +24,115 @@ import { cn } from "~/lib/utils";
|
||||||
// from https://ui-x.junwen-k.dev/docs/components/emoji-picker
|
// from https://ui-x.junwen-k.dev/docs/components/emoji-picker
|
||||||
|
|
||||||
function EmojiPicker({
|
function EmojiPicker({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof EmojiPickerPrimitive.Root>) {
|
}: React.ComponentProps<typeof EmojiPickerPrimitive.Root>) {
|
||||||
return (
|
return (
|
||||||
<EmojiPickerPrimitive.Root
|
<EmojiPickerPrimitive.Root
|
||||||
data-slot="emoji-picker"
|
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,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmojiPickerSearch({
|
function EmojiPickerSearch({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof EmojiPickerPrimitive.Search>) {
|
}: React.ComponentProps<typeof EmojiPickerPrimitive.Search>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="emoji-picker-search-wrapper"
|
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)}
|
||||||
>
|
>
|
||||||
<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"
|
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"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 data-slot="emoji-picker-row" className="scroll-my-1 px-1" {...props}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmojiPickerEmoji({
|
function EmojiPickerEmoji({
|
||||||
emoji,
|
emoji,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: EmojiPickerListEmojiProps) {
|
}: EmojiPickerListEmojiProps) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
data-slot="emoji-picker-emoji"
|
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,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{emoji.emoji}
|
{emoji.emoji}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmojiPickerCategoryHeader({
|
function EmojiPickerCategoryHeader({
|
||||||
category,
|
category,
|
||||||
...props
|
...props
|
||||||
}: EmojiPickerListCategoryHeaderProps) {
|
}: EmojiPickerListCategoryHeaderProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="emoji-picker-category-header"
|
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"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{category.label}
|
{category.label}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmojiPickerContent({
|
function EmojiPickerContent({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof EmojiPickerPrimitive.Viewport>) {
|
}: React.ComponentProps<typeof EmojiPickerPrimitive.Viewport>) {
|
||||||
return (
|
return (
|
||||||
<EmojiPickerPrimitive.Viewport
|
<EmojiPickerPrimitive.Viewport
|
||||||
data-slot="emoji-picker-viewport"
|
data-slot="emoji-picker-viewport"
|
||||||
className={cn("relative flex-1 outline-hidden", className)}
|
className={cn("relative flex-1 outline-hidden", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<EmojiPickerPrimitive.Loading
|
<EmojiPickerPrimitive.Loading
|
||||||
data-slot="emoji-picker-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"
|
||||||
>
|
>
|
||||||
<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"
|
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"
|
||||||
>
|
>
|
||||||
No emoji found.
|
No emoji found.
|
||||||
</EmojiPickerPrimitive.Empty>
|
</EmojiPickerPrimitive.Empty>
|
||||||
<EmojiPickerPrimitive.List
|
<EmojiPickerPrimitive.List
|
||||||
data-slot="emoji-picker-list"
|
data-slot="emoji-picker-list"
|
||||||
className="pb-1 select-none"
|
className="pb-1 select-none"
|
||||||
components={{
|
components={{
|
||||||
Row: EmojiPickerRow,
|
Row: EmojiPickerRow,
|
||||||
Emoji: EmojiPickerEmoji,
|
Emoji: EmojiPickerEmoji,
|
||||||
CategoryHeader: EmojiPickerCategoryHeader,
|
CategoryHeader: EmojiPickerCategoryHeader,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</EmojiPickerPrimitive.Viewport>
|
</EmojiPickerPrimitive.Viewport>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*function EmojiPickerFooter({
|
/*function EmojiPickerFooter({
|
||||||
|
|
@ -228,8 +228,8 @@ function EmojiPickerSkinToneSelector() {
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
export {
|
export {
|
||||||
EmojiPicker,
|
EmojiPicker,
|
||||||
EmojiPickerSearch,
|
EmojiPickerSearch,
|
||||||
EmojiPickerContent,
|
EmojiPickerContent,
|
||||||
//EmojiPickerFooter,
|
//EmojiPickerFooter,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,202 +1,201 @@
|
||||||
import * as React from "react";
|
|
||||||
import type * as LabelPrimitive from "@radix-ui/react-label";
|
import type * as LabelPrimitive from "@radix-ui/react-label";
|
||||||
import { Slot } from "@radix-ui/react-slot";
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
|
import * as React from "react";
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
type ControllerProps,
|
type ControllerProps,
|
||||||
type FieldPath,
|
type FieldPath,
|
||||||
type FieldValues,
|
type FieldValues,
|
||||||
FormProvider,
|
FormProvider,
|
||||||
useFormContext,
|
useFormContext,
|
||||||
} from "react-hook-form";
|
} from "react-hook-form";
|
||||||
|
|
||||||
import { cn } from "src/lib/utils";
|
|
||||||
import { Label } from "src/components/ui/label";
|
import { Label } from "src/components/ui/label";
|
||||||
|
import { cn } from "src/lib/utils";
|
||||||
import { env } from "~/env.mjs";
|
import { env } from "~/env.mjs";
|
||||||
|
|
||||||
const Form = FormProvider;
|
const Form = FormProvider;
|
||||||
|
|
||||||
type FormFieldContextValue<
|
type FormFieldContextValue<
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
> = {
|
> = {
|
||||||
name: TName;
|
name: TName;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||||
{} as FormFieldContextValue,
|
{} as FormFieldContextValue,
|
||||||
);
|
);
|
||||||
|
|
||||||
const FormField = <
|
const FormField = <
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
>({
|
>({
|
||||||
...props
|
...props
|
||||||
}: ControllerProps<TFieldValues, TName>) => {
|
}: ControllerProps<TFieldValues, TName>) => {
|
||||||
return (
|
return (
|
||||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||||
<Controller {...props} />
|
<Controller {...props} />
|
||||||
</FormFieldContext.Provider>
|
</FormFieldContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const useFormField = () => {
|
const useFormField = () => {
|
||||||
const fieldContext = React.useContext(FormFieldContext);
|
const fieldContext = React.useContext(FormFieldContext);
|
||||||
const itemContext = React.useContext(FormItemContext);
|
const itemContext = React.useContext(FormItemContext);
|
||||||
const { getFieldState, formState } = useFormContext();
|
const { getFieldState, formState } = useFormContext();
|
||||||
|
|
||||||
const fieldState = getFieldState(fieldContext.name, formState);
|
const fieldState = getFieldState(fieldContext.name, formState);
|
||||||
|
|
||||||
if (!fieldContext) {
|
if (!fieldContext) {
|
||||||
throw new Error("useFormField should be used within <FormField>");
|
throw new Error("useFormField should be used within <FormField>");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id } = itemContext;
|
const { id } = itemContext;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
name: fieldContext.name,
|
name: fieldContext.name,
|
||||||
formItemId: `${id}-form-item`,
|
formItemId: `${id}-form-item`,
|
||||||
formDescriptionId: `${id}-form-item-description`,
|
formDescriptionId: `${id}-form-item-description`,
|
||||||
formMessageId: `${id}-form-item-message`,
|
formMessageId: `${id}-form-item-message`,
|
||||||
...fieldState,
|
...fieldState,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type FormItemContextValue = {
|
type FormItemContextValue = {
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||||
{} as FormItemContextValue,
|
{} as FormItemContextValue,
|
||||||
);
|
);
|
||||||
|
|
||||||
const FormItem = React.forwardRef<
|
const FormItem = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
>(({ className, ...props }, ref) => {
|
>(({ className, ...props }, ref) => {
|
||||||
const id = React.useId();
|
const id = React.useId();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormItemContext.Provider value={{ id }}>
|
<FormItemContext.Provider value={{ id }}>
|
||||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||||
</FormItemContext.Provider>
|
</FormItemContext.Provider>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
FormItem.displayName = "FormItem";
|
FormItem.displayName = "FormItem";
|
||||||
|
|
||||||
const FormLabel = React.forwardRef<
|
const FormLabel = React.forwardRef<
|
||||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||||
>(({ className, ...props }, ref) => {
|
>(({ className, ...props }, ref) => {
|
||||||
const { error, formItemId } = useFormField();
|
const { error, formItemId } = useFormField();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Label
|
<Label
|
||||||
ref={ref}
|
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}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
FormLabel.displayName = "FormLabel";
|
FormLabel.displayName = "FormLabel";
|
||||||
|
|
||||||
const FormControl = React.forwardRef<
|
const FormControl = React.forwardRef<
|
||||||
React.ElementRef<typeof Slot>,
|
React.ElementRef<typeof Slot>,
|
||||||
React.ComponentPropsWithoutRef<typeof Slot>
|
React.ComponentPropsWithoutRef<typeof Slot>
|
||||||
>(({ ...props }, ref) => {
|
>(({ ...props }, ref) => {
|
||||||
const { error, formItemId, formDescriptionId, formMessageId } =
|
const { error, formItemId, formDescriptionId, formMessageId } =
|
||||||
useFormField();
|
useFormField();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Slot
|
<Slot
|
||||||
ref={ref}
|
ref={ref}
|
||||||
id={formItemId}
|
id={formItemId}
|
||||||
aria-describedby={
|
aria-describedby={
|
||||||
!error
|
!error
|
||||||
? `${formDescriptionId}`
|
? `${formDescriptionId}`
|
||||||
: `${formDescriptionId} ${formMessageId}`
|
: `${formDescriptionId} ${formMessageId}`
|
||||||
}
|
}
|
||||||
aria-invalid={!!error}
|
aria-invalid={!!error}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
FormControl.displayName = "FormControl";
|
FormControl.displayName = "FormControl";
|
||||||
|
|
||||||
const FormDescription = React.forwardRef<
|
const FormDescription = React.forwardRef<
|
||||||
HTMLParagraphElement,
|
HTMLParagraphElement,
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
>(({ className, ...props }, ref) => {
|
>(({ className, ...props }, ref) => {
|
||||||
const { formDescriptionId } = useFormField();
|
const { formDescriptionId } = useFormField();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
ref={ref}
|
ref={ref}
|
||||||
id={formDescriptionId}
|
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,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
FormDescription.displayName = "FormDescription";
|
FormDescription.displayName = "FormDescription";
|
||||||
|
|
||||||
const FormMessage = React.forwardRef<
|
const FormMessage = React.forwardRef<
|
||||||
HTMLParagraphElement,
|
HTMLParagraphElement,
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
>(({ className, children, ...props }, ref) => {
|
>(({ className, children, ...props }, ref) => {
|
||||||
const { error, formMessageId } = useFormField();
|
const { error, formMessageId } = useFormField();
|
||||||
const body = error ? String(error?.message) : children;
|
const body = error ? String(error?.message) : children;
|
||||||
|
|
||||||
if (!body) {
|
if (!body) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
ref={ref}
|
ref={ref}
|
||||||
id={formMessageId}
|
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,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{body}
|
{body}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
FormMessage.displayName = "FormMessage";
|
FormMessage.displayName = "FormMessage";
|
||||||
|
|
||||||
const FormDebug = React.forwardRef<
|
const FormDebug = React.forwardRef<
|
||||||
React.ElementType,
|
React.ElementType,
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
>(async () => {
|
>(async () => {
|
||||||
if (env.NODE_ENV !== "development") {
|
if (env.NODE_ENV !== "development") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const { control, formState } = useFormContext();
|
const { control, formState } = useFormContext();
|
||||||
const devtools = await import(/* webpackIgnore: true */ "@hookform/devtools");
|
const devtools = await import(/* webpackIgnore: true */ "@hookform/devtools");
|
||||||
|
|
||||||
if (Object.keys(formState.errors).length > 0) {
|
if (Object.keys(formState.errors).length > 0) {
|
||||||
console.error("errors", formState.errors);
|
console.error("errors", formState.errors);
|
||||||
}
|
}
|
||||||
return <devtools.DevTool control={control} />;
|
return <devtools.DevTool control={control} />;
|
||||||
});
|
});
|
||||||
FormDebug.displayName = "FormDebug";
|
FormDebug.displayName = "FormDebug";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
useFormField,
|
useFormField,
|
||||||
Form,
|
Form,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormDescription,
|
FormDescription,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
FormField,
|
FormField,
|
||||||
FormDebug,
|
FormDebug,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,6 @@ import dynamic from "next/dynamic";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
export const DevT: React.ElementType = dynamic(
|
export const DevT: React.ElementType = dynamic(
|
||||||
() => import("@hookform/devtools").then((mod) => mod.DevTool),
|
() => import("@hookform/devtools").then((mod) => mod.DevTool),
|
||||||
{ ssr: false },
|
{ ssr: false },
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,35 @@
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
type ComponentPropsWithoutRef,
|
||||||
type ComponentPropsWithoutRef,
|
forwardRef,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
} from "react";
|
} from "react";
|
||||||
import Input from "~/components/custom_ui/input";
|
import Input from "~/components/custom_ui/input";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
type InputWIconProps = ComponentPropsWithoutRef<typeof Input> & {
|
type InputWIconProps = ComponentPropsWithoutRef<typeof Input> & {
|
||||||
iconPosition: "start" | "end";
|
iconPosition: "start" | "end";
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
const InputWIcon = forwardRef<HTMLInputElement, InputWIconProps>(
|
const InputWIcon = forwardRef<HTMLInputElement, InputWIconProps>(
|
||||||
({ children, iconPosition, ...props }, ref) => {
|
({ children, iconPosition, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
className={cn("peer", iconPosition == "start" ? "ps-9" : "pe-9")}
|
className={cn("peer", iconPosition === "start" ? "ps-9" : "pe-9")}
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"pointer-events-none absolute inset-y-0 flex items-center justify-center text-muted-foreground/80 peer-disabled:opacity-50",
|
"pointer-events-none absolute inset-y-0 flex items-center justify-center text-muted-foreground/80 peer-disabled:opacity-50",
|
||||||
iconPosition == "start" ? "start-0 ps-3" : "end-0 pe-3",
|
iconPosition === "start" ? "start-0 ps-3" : "end-0 pe-3",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
InputWIcon.displayName = "InputWIcon";
|
InputWIcon.displayName = "InputWIcon";
|
||||||
|
|
|
||||||
|
|
@ -4,19 +4,19 @@ import { cn } from "src/lib/utils";
|
||||||
type InputProps = InputHTMLAttributes<HTMLInputElement>;
|
type InputProps = InputHTMLAttributes<HTMLInputElement>;
|
||||||
|
|
||||||
const Input = forwardRef<HTMLInputElement, InputProps>(
|
const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||||
({ className, type, ...props }, ref) => {
|
({ className, type, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...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`,
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
Input.displayName = "Input";
|
Input.displayName = "Input";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,23 @@
|
||||||
import { type ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
const DualInputLayout = (props: {
|
const DualInputLayout = (props: {
|
||||||
children1: ReactNode;
|
children1: ReactNode;
|
||||||
children2: ReactNode;
|
children2: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { children1, children2, className } = props;
|
const { children1, children2, className } = props;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col items-center gap-x-6 gap-y-5 sm:flex-row [&>*]:w-full",
|
"flex flex-col items-center gap-x-6 gap-y-5 sm:flex-row [&>*]:w-full",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div>{children1}</div>
|
<div>{children1}</div>
|
||||||
<div>{children2}</div>
|
<div>{children2}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { DualInputLayout };
|
export { DualInputLayout };
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,34 @@
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
buttonVariants,
|
|
||||||
type ButtonProps,
|
|
||||||
} from "~/components/ui/button";
|
|
||||||
import { LoaderCircle } from "lucide-react";
|
import { LoaderCircle } from "lucide-react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
type ButtonProps,
|
||||||
|
buttonVariants,
|
||||||
|
} from "~/components/ui/button";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
const LoadingButton = ({
|
const LoadingButton = ({
|
||||||
className,
|
className,
|
||||||
variant,
|
variant,
|
||||||
size,
|
size,
|
||||||
loading,
|
loading,
|
||||||
...props
|
...props
|
||||||
}: ButtonProps & { loading?: boolean }) => {
|
}: ButtonProps & { loading?: boolean }) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{loading && (
|
{loading && (
|
||||||
<LoaderCircle
|
<LoaderCircle
|
||||||
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"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{props.children}
|
{props.children}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LoadingButton;
|
export default LoadingButton;
|
||||||
|
|
|
||||||
|
|
@ -1,347 +1,352 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { CheckIcon, ChevronsUpDownIcon, XIcon } from "lucide-react";
|
import { CheckIcon, ChevronsUpDownIcon, XIcon } from "lucide-react";
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import {
|
import {
|
||||||
Command,
|
type ComponentPropsWithoutRef,
|
||||||
CommandEmpty,
|
createContext,
|
||||||
CommandGroup,
|
type ReactNode,
|
||||||
CommandInput,
|
useCallback,
|
||||||
CommandItem,
|
useContext,
|
||||||
CommandList,
|
useEffect,
|
||||||
CommandSeparator,
|
useLayoutEffect,
|
||||||
} from "~/components/ui/command";
|
useRef,
|
||||||
import {
|
useState,
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
} from "~/components/ui/popover";
|
|
||||||
import {
|
|
||||||
createContext,
|
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useLayoutEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
type ComponentPropsWithoutRef,
|
|
||||||
type ReactNode,
|
|
||||||
} from "react";
|
} from "react";
|
||||||
import { Badge } from "~/components/ui/badge";
|
import { Badge } from "~/components/ui/badge";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
CommandSeparator,
|
||||||
|
} from "~/components/ui/command";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "~/components/ui/popover";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
type MultiSelectContextType = {
|
type MultiSelectContextType = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void;
|
||||||
selectedValues: Set<string>;
|
selectedValues: Set<string>;
|
||||||
toggleValue: (value: string) => void;
|
toggleValue: (value: string) => void;
|
||||||
items: Map<string, ReactNode>;
|
items: Map<string, ReactNode>;
|
||||||
onItemAdded: (value: string, label: ReactNode) => void;
|
onItemAdded: (value: string, label: ReactNode) => void;
|
||||||
};
|
};
|
||||||
const MultiSelectContext = createContext<MultiSelectContextType | null>(null);
|
const MultiSelectContext = createContext<MultiSelectContextType | null>(null);
|
||||||
|
|
||||||
export function MultiSelect({
|
export function MultiSelect({
|
||||||
children,
|
children,
|
||||||
values,
|
values,
|
||||||
defaultValues,
|
defaultValues,
|
||||||
onValuesChange,
|
onValuesChange,
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
values?: string[];
|
values?: string[];
|
||||||
defaultValues?: string[];
|
defaultValues?: string[];
|
||||||
onValuesChange?: (values: string[]) => void;
|
onValuesChange?: (values: string[]) => void;
|
||||||
}) {
|
}) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [selectedValues, setSelectedValues] = useState(
|
const [selectedValues, setSelectedValues] = useState(
|
||||||
new Set<string>(values ?? defaultValues),
|
new Set<string>(values ?? defaultValues),
|
||||||
);
|
);
|
||||||
const [items, setItems] = useState<Map<string, ReactNode>>(new Map());
|
const [items, setItems] = useState<Map<string, ReactNode>>(new Map());
|
||||||
|
|
||||||
function toggleValue(value: string) {
|
function toggleValue(value: string) {
|
||||||
const getNewSet = (prev: Set<string>) => {
|
const getNewSet = (prev: Set<string>) => {
|
||||||
const newSet = new Set(prev);
|
const newSet = new Set(prev);
|
||||||
if (newSet.has(value)) {
|
if (newSet.has(value)) {
|
||||||
newSet.delete(value);
|
newSet.delete(value);
|
||||||
} else {
|
} else {
|
||||||
newSet.add(value);
|
newSet.add(value);
|
||||||
}
|
}
|
||||||
return newSet;
|
return newSet;
|
||||||
};
|
};
|
||||||
setSelectedValues(getNewSet);
|
setSelectedValues(getNewSet);
|
||||||
onValuesChange?.([...getNewSet(selectedValues)]);
|
onValuesChange?.([...getNewSet(selectedValues)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const onItemAdded = useCallback((value: string, label: ReactNode) => {
|
const onItemAdded = useCallback((value: string, label: ReactNode) => {
|
||||||
setItems((prev) => {
|
setItems((prev) => {
|
||||||
if (prev.get(value) === label) return prev;
|
if (prev.get(value) === label) return prev;
|
||||||
return new Map(prev).set(value, label);
|
return new Map(prev).set(value, label);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MultiSelectContext
|
<MultiSelectContext
|
||||||
value={{
|
value={{
|
||||||
open,
|
open,
|
||||||
setOpen,
|
setOpen,
|
||||||
selectedValues: values ? new Set(values) : selectedValues,
|
selectedValues: values ? new Set(values) : selectedValues,
|
||||||
toggleValue,
|
toggleValue,
|
||||||
items,
|
items,
|
||||||
onItemAdded,
|
onItemAdded,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
{children}
|
{children}
|
||||||
</Popover>
|
</Popover>
|
||||||
</MultiSelectContext>
|
</MultiSelectContext>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MultiSelectTrigger({
|
export function MultiSelectTrigger({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
} & ComponentPropsWithoutRef<typeof Button>) {
|
} & ComponentPropsWithoutRef<typeof Button>) {
|
||||||
const { open } = useMultiSelectContext();
|
const { open } = useMultiSelectContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
{...props}
|
{...props}
|
||||||
variant={props.variant ?? "outline"}
|
variant={props.variant ?? "outline"}
|
||||||
role={props.role ?? "combobox"}
|
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,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronsUpDownIcon className="size-4 shrink-0 opacity-50" />
|
<ChevronsUpDownIcon className="size-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MultiSelectValue({
|
export function MultiSelectValue({
|
||||||
placeholder,
|
placeholder,
|
||||||
clickToRemove = true,
|
clickToRemove = true,
|
||||||
className,
|
className,
|
||||||
overflowBehavior = "wrap-when-open",
|
overflowBehavior = "wrap-when-open",
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
clickToRemove?: boolean;
|
clickToRemove?: boolean;
|
||||||
overflowBehavior?: "wrap" | "wrap-when-open" | "cutoff";
|
overflowBehavior?: "wrap" | "wrap-when-open" | "cutoff";
|
||||||
} & Omit<ComponentPropsWithoutRef<"div">, "children">) {
|
} & Omit<ComponentPropsWithoutRef<"div">, "children">) {
|
||||||
const { selectedValues, toggleValue, items, open } = useMultiSelectContext();
|
const { selectedValues, toggleValue, items, open } = useMultiSelectContext();
|
||||||
const [overflowAmount, setOverflowAmount] = useState(0);
|
const [overflowAmount, setOverflowAmount] = useState(0);
|
||||||
const valueRef = useRef<HTMLDivElement>(null);
|
const valueRef = useRef<HTMLDivElement>(null);
|
||||||
const overflowRef = useRef<HTMLDivElement>(null);
|
const overflowRef = useRef<HTMLDivElement>(null);
|
||||||
const itemsRef = useRef<Set<HTMLElement>>(new Set());
|
const itemsRef = useRef<Set<HTMLElement>>(new Set());
|
||||||
|
|
||||||
const shouldWrap =
|
const shouldWrap =
|
||||||
overflowBehavior === "wrap" ||
|
overflowBehavior === "wrap" ||
|
||||||
(overflowBehavior === "wrap-when-open" && open);
|
(overflowBehavior === "wrap-when-open" && open);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!shouldWrap) return;
|
if (!shouldWrap) return;
|
||||||
itemsRef.current.forEach((child) => child.style.removeProperty("display"));
|
itemsRef.current.forEach((child) => child.style.removeProperty("display"));
|
||||||
}, [shouldWrap]);
|
}, [shouldWrap]);
|
||||||
|
|
||||||
const checkOverflow = useCallback(() => {
|
const checkOverflow = useCallback(() => {
|
||||||
if (valueRef.current == null) return;
|
if (valueRef.current == null) return;
|
||||||
|
|
||||||
const containerElement = valueRef.current;
|
const containerElement = valueRef.current;
|
||||||
const overflowElement = overflowRef.current;
|
const overflowElement = overflowRef.current;
|
||||||
|
|
||||||
if (overflowElement != null) overflowElement.style.display = "none";
|
if (overflowElement != null) overflowElement.style.display = "none";
|
||||||
itemsRef.current.forEach((child) => child.style.removeProperty("display"));
|
itemsRef.current.forEach((child) => child.style.removeProperty("display"));
|
||||||
let amount = 0;
|
let amount = 0;
|
||||||
for (let i = itemsRef.current.size - 1; i >= 0; i--) {
|
for (let i = itemsRef.current.size - 1; i >= 0; i--) {
|
||||||
const child = [...itemsRef.current][i];
|
const child = [...itemsRef.current][i];
|
||||||
if (
|
if (
|
||||||
containerElement.scrollWidth <= containerElement.clientWidth ||
|
containerElement.scrollWidth <= containerElement.clientWidth ||
|
||||||
!child
|
!child
|
||||||
) {
|
) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
amount = itemsRef.current.size - i;
|
amount = itemsRef.current.size - i;
|
||||||
child.style.display = "none";
|
child.style.display = "none";
|
||||||
overflowElement?.style.removeProperty("display");
|
overflowElement?.style.removeProperty("display");
|
||||||
}
|
}
|
||||||
setOverflowAmount(amount);
|
setOverflowAmount(amount);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (valueRef.current == null) return;
|
if (valueRef.current == null) return;
|
||||||
|
|
||||||
const observer = new ResizeObserver(checkOverflow);
|
const observer = new ResizeObserver(checkOverflow);
|
||||||
observer.observe(valueRef.current);
|
observer.observe(valueRef.current);
|
||||||
|
|
||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, [checkOverflow]);
|
}, [checkOverflow]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
checkOverflow();
|
checkOverflow();
|
||||||
}, [selectedValues, checkOverflow]);
|
}, [selectedValues, checkOverflow]);
|
||||||
|
|
||||||
if (selectedValues.size === 0 && placeholder) {
|
if (selectedValues.size === 0 && placeholder) {
|
||||||
return (
|
return (
|
||||||
<span className="text-muted-foreground font-normal">{placeholder}</span>
|
<span className="text-muted-foreground font-normal">{placeholder}</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
{...props}
|
{...props}
|
||||||
ref={valueRef}
|
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,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{[...selectedValues]
|
{[...selectedValues]
|
||||||
.filter((value) => items.has(value))
|
.filter((value) => items.has(value))
|
||||||
.map((value) => (
|
.map((value) => (
|
||||||
<Badge
|
<Badge
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
if (el == null) return;
|
if (el == null) return;
|
||||||
|
|
||||||
itemsRef.current.add(el);
|
itemsRef.current.add(el);
|
||||||
return () => {
|
return () => {
|
||||||
itemsRef.current.delete(el);
|
itemsRef.current.delete(el);
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="group flex items-center gap-1"
|
className="group flex items-center gap-1"
|
||||||
key={value}
|
key={value}
|
||||||
onClick={
|
onClick={
|
||||||
clickToRemove
|
clickToRemove
|
||||||
? (e) => {
|
? (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
toggleValue(value);
|
toggleValue(value);
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{items.get(value)}
|
{items.get(value)}
|
||||||
{clickToRemove && (
|
{clickToRemove && (
|
||||||
<XIcon className="text-muted-foreground group-hover:text-destructive size-2" />
|
<XIcon className="text-muted-foreground group-hover:text-destructive size-2" />
|
||||||
)}
|
)}
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
<Badge
|
<Badge
|
||||||
style={{
|
style={{
|
||||||
display: overflowAmount > 0 && !shouldWrap ? "block" : "none",
|
display: overflowAmount > 0 && !shouldWrap ? "block" : "none",
|
||||||
}}
|
}}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
ref={overflowRef}
|
ref={overflowRef}
|
||||||
>
|
>
|
||||||
+{overflowAmount}
|
+{overflowAmount}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MultiSelectContent({
|
export function MultiSelectContent({
|
||||||
search = true,
|
search = true,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
search?: boolean | { placeholder?: string; emptyMessage?: string };
|
search?: boolean | { placeholder?: string; emptyMessage?: string };
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
} & Omit<ComponentPropsWithoutRef<typeof Command>, "children">) {
|
} & Omit<ComponentPropsWithoutRef<typeof Command>, "children">) {
|
||||||
const canSearch = typeof search === "object" ? true : search;
|
const canSearch = typeof search === "object" ? true : search;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={{ display: "none" }}>
|
<div style={{ display: "none" }}>
|
||||||
<Command>
|
<Command>
|
||||||
<CommandList>{children}</CommandList>
|
<CommandList>{children}</CommandList>
|
||||||
</Command>
|
</Command>
|
||||||
</div>
|
</div>
|
||||||
<PopoverContent className="min-w-[var(--radix-popover-trigger-width)] p-0">
|
<PopoverContent className="min-w-[var(--radix-popover-trigger-width)] p-0">
|
||||||
<Command {...props}>
|
<Command {...props}>
|
||||||
{canSearch ? (
|
{canSearch ? (
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder={
|
placeholder={
|
||||||
typeof search === "object" ? search.placeholder : undefined
|
typeof search === "object" ? search.placeholder : undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
// biome-ignore lint/a11y/noAriaHiddenOnFocusable: <intended>
|
||||||
<button autoFocus aria-hidden="true" className="sr-only" />
|
<button
|
||||||
)}
|
autoFocus
|
||||||
<CommandList>
|
aria-hidden="true"
|
||||||
{canSearch && (
|
className="sr-only"
|
||||||
<CommandEmpty>
|
type="button"
|
||||||
{typeof search === "object" ? search.emptyMessage : undefined}
|
/>
|
||||||
</CommandEmpty>
|
)}
|
||||||
)}
|
<CommandList>
|
||||||
{children}
|
{canSearch && (
|
||||||
</CommandList>
|
<CommandEmpty>
|
||||||
</Command>
|
{typeof search === "object" ? search.emptyMessage : undefined}
|
||||||
</PopoverContent>
|
</CommandEmpty>
|
||||||
</>
|
)}
|
||||||
);
|
{children}
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MultiSelectItem({
|
export function MultiSelectItem({
|
||||||
value,
|
value,
|
||||||
children,
|
children,
|
||||||
badgeLabel,
|
badgeLabel,
|
||||||
onSelect,
|
onSelect,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
badgeLabel?: ReactNode;
|
badgeLabel?: ReactNode;
|
||||||
value: string;
|
value: string;
|
||||||
} & Omit<ComponentPropsWithoutRef<typeof CommandItem>, "value">) {
|
} & Omit<ComponentPropsWithoutRef<typeof CommandItem>, "value">) {
|
||||||
const { toggleValue, selectedValues, onItemAdded } = useMultiSelectContext();
|
const { toggleValue, selectedValues, onItemAdded } = useMultiSelectContext();
|
||||||
const isSelected = selectedValues.has(value);
|
const isSelected = selectedValues.has(value);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onItemAdded(value, badgeLabel ?? children);
|
onItemAdded(value, badgeLabel ?? children);
|
||||||
}, [value, children, onItemAdded, badgeLabel]);
|
}, [value, children, onItemAdded, badgeLabel]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
{...props}
|
{...props}
|
||||||
value={value}
|
value={value}
|
||||||
onSelect={(v) => {
|
onSelect={(v) => {
|
||||||
toggleValue(v);
|
toggleValue(v);
|
||||||
onSelect?.(v);
|
onSelect?.(v);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn("mr-2 size-4", isSelected ? "opacity-100" : "opacity-0")}
|
className={cn("mr-2 size-4", isSelected ? "opacity-100" : "opacity-0")}
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MultiSelectGroup(
|
export function MultiSelectGroup(
|
||||||
props: ComponentPropsWithoutRef<typeof CommandGroup>,
|
props: ComponentPropsWithoutRef<typeof CommandGroup>,
|
||||||
) {
|
) {
|
||||||
return <CommandGroup {...props} />;
|
return <CommandGroup {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MultiSelectSeparator(
|
export function MultiSelectSeparator(
|
||||||
props: ComponentPropsWithoutRef<typeof CommandSeparator>,
|
props: ComponentPropsWithoutRef<typeof CommandSeparator>,
|
||||||
) {
|
) {
|
||||||
return <CommandSeparator {...props} />;
|
return <CommandSeparator {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function useMultiSelectContext() {
|
function useMultiSelectContext() {
|
||||||
const context = useContext(MultiSelectContext);
|
const context = useContext(MultiSelectContext);
|
||||||
if (context == null) {
|
if (context == null) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"useMultiSelectContext must be used within a MultiSelectContext",
|
"useMultiSelectContext must be used within a MultiSelectContext",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,98 +1,97 @@
|
||||||
/* eslint-disable better-tailwindcss/no-unregistered-classes */
|
|
||||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||||
import { useRef, useState, type JSX } from "react";
|
import { type JSX, useRef, useState } from "react";
|
||||||
import ReactSelect, {
|
import ReactSelect, {
|
||||||
type MenuListProps,
|
type CSSObjectWithLabel,
|
||||||
type GroupBase,
|
type GroupBase,
|
||||||
type CSSObjectWithLabel,
|
type MenuListProps,
|
||||||
} from "react-select";
|
} from "react-select";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
export type ListOption = {
|
export type ListOption = {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type BaseProps = {
|
type BaseProps = {
|
||||||
elementId: string;
|
elementId: string;
|
||||||
elementName: string;
|
elementName: string;
|
||||||
options: ListOption[];
|
options: ListOption[];
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
menuPlacement?: "auto" | "bottom" | "top";
|
menuPlacement?: "auto" | "bottom" | "top";
|
||||||
defaultValue?: ListOption | ListOption[];
|
defaultValue?: ListOption | ListOption[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type MultiProps = {
|
type MultiProps = {
|
||||||
isMulti: true;
|
isMulti: true;
|
||||||
onValueChange: (value: ListOption[]) => void;
|
onValueChange: (value: ListOption[]) => void;
|
||||||
};
|
};
|
||||||
type SigletProps = {
|
type SigletProps = {
|
||||||
isMulti: false;
|
isMulti: false;
|
||||||
onValueChange: (value: ListOption) => void;
|
onValueChange: (value: ListOption) => void;
|
||||||
};
|
};
|
||||||
type MultiSelectProps = BaseProps & (MultiProps | SigletProps);
|
type MultiSelectProps = BaseProps & (MultiProps | SigletProps);
|
||||||
|
|
||||||
const VirtualizedMenuList = (
|
const VirtualizedMenuList = (
|
||||||
props: MenuListProps<
|
props: MenuListProps<
|
||||||
ListOption,
|
ListOption,
|
||||||
MultiSelectProps["isMulti"],
|
MultiSelectProps["isMulti"],
|
||||||
GroupBase<ListOption>
|
GroupBase<ListOption>
|
||||||
>,
|
>,
|
||||||
) => {
|
) => {
|
||||||
const rows = props.children as JSX.Element[];
|
const rows = props.children as JSX.Element[];
|
||||||
const parentRef = useRef<HTMLDivElement>(null);
|
const parentRef = useRef<HTMLDivElement>(null);
|
||||||
const count = rows.length;
|
const count = rows.length;
|
||||||
|
|
||||||
const virtualizer = useVirtualizer({
|
const virtualizer = useVirtualizer({
|
||||||
count,
|
count,
|
||||||
getScrollElement: () => parentRef.current,
|
getScrollElement: () => parentRef.current,
|
||||||
estimateSize: () => 45,
|
estimateSize: () => 45,
|
||||||
});
|
});
|
||||||
const items = virtualizer.getVirtualItems();
|
const items = virtualizer.getVirtualItems();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={parentRef}
|
ref={parentRef}
|
||||||
className="List"
|
className="List"
|
||||||
style={{
|
style={{
|
||||||
height: `${virtualizer.getTotalSize()}px`,
|
height: `${virtualizer.getTotalSize()}px`,
|
||||||
maxHeight: "300px",
|
maxHeight: "300px",
|
||||||
width: "auto",
|
width: "auto",
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
contain: "strict",
|
contain: "strict",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: virtualizer.getTotalSize(),
|
height: virtualizer.getTotalSize(),
|
||||||
width: "100%",
|
width: "100%",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
transform: `translateY(${items[0]?.start ?? 0}px)`,
|
transform: `translateY(${items[0]?.start ?? 0}px)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{items.map((virtualRow) => (
|
{items.map((virtualRow) => (
|
||||||
<div
|
<div
|
||||||
key={virtualRow.key}
|
key={virtualRow.key}
|
||||||
data-index={virtualRow.index}
|
data-index={virtualRow.index}
|
||||||
ref={virtualizer.measureElement}
|
ref={virtualizer.measureElement}
|
||||||
className={virtualRow.index % 2 ? "ListItemOdd" : "ListItemEven"}
|
className={virtualRow.index % 2 ? "ListItemOdd" : "ListItemEven"}
|
||||||
>
|
>
|
||||||
{rows[virtualRow.index]}
|
{rows[virtualRow.index]}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -116,178 +115,180 @@ const VirtualizedMenuList = (
|
||||||
* />
|
* />
|
||||||
*/
|
*/
|
||||||
export const MultiSelect = ({
|
export const MultiSelect = ({
|
||||||
elementId,
|
elementId,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
elementName,
|
elementName,
|
||||||
isMulti,
|
isMulti,
|
||||||
onValueChange,
|
onValueChange,
|
||||||
options,
|
options,
|
||||||
placeholder,
|
placeholder,
|
||||||
disabled,
|
disabled,
|
||||||
menuPlacement,
|
menuPlacement,
|
||||||
}: MultiSelectProps) => {
|
}: MultiSelectProps) => {
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactSelect
|
<ReactSelect
|
||||||
menuPlacement={menuPlacement}
|
menuPlacement={menuPlacement}
|
||||||
components={{ MenuList: VirtualizedMenuList }}
|
components={{ MenuList: VirtualizedMenuList }}
|
||||||
inputId={elementId}
|
inputId={elementId}
|
||||||
isMulti={isMulti}
|
isMulti={isMulti}
|
||||||
isDisabled={disabled}
|
isDisabled={disabled}
|
||||||
instanceId={elementId}
|
instanceId={elementId}
|
||||||
name={elementName}
|
name={elementName}
|
||||||
options={options}
|
options={options}
|
||||||
onChange={(values) => {
|
onChange={(values) => {
|
||||||
if (isMulti) {
|
if (isMulti) {
|
||||||
const valore = values as ListOption[];
|
const valore = values as ListOption[];
|
||||||
onValueChange(valore);
|
onValueChange(valore);
|
||||||
} else {
|
} else {
|
||||||
const valore = values as ListOption;
|
const valore = values as ListOption;
|
||||||
onValueChange(valore);
|
onValueChange(valore);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
onMenuOpen={() => setMenuOpen(true)}
|
onMenuOpen={() => setMenuOpen(true)}
|
||||||
onMenuClose={() => setMenuOpen(false)}
|
onMenuClose={() => setMenuOpen(false)}
|
||||||
styles={{
|
styles={{
|
||||||
container: (base, state) =>
|
container: (base, state) =>
|
||||||
({
|
({
|
||||||
...base,
|
...base,
|
||||||
|
|
||||||
":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": {
|
":hover .select__dropdown-indicator": {
|
||||||
color: state.isFocused ? "rgb(161,161,170)" : "rgb(161,161,170)",
|
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",
|
minHeight: "42px",
|
||||||
borderRadius: "0.5rem",
|
borderRadius: "0.5rem",
|
||||||
boxShadow: undefined,
|
boxShadow: undefined,
|
||||||
|
|
||||||
border: state.isFocused
|
border: state.isFocused
|
||||||
? "1px solid rgb(161,161,170)"
|
? "1px solid rgb(161,161,170)"
|
||||||
: "1px solid rgb(212 ,212 ,216)",
|
: "1px solid rgb(212 ,212 ,216)",
|
||||||
|
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
borderColor: state.isFocused
|
borderColor: state.isFocused
|
||||||
? "rgb(161,161,170)"
|
? "rgb(161,161,170)"
|
||||||
: "rgb(161 ,161 ,170)",
|
: "rgb(161 ,161 ,170)",
|
||||||
},
|
},
|
||||||
|
|
||||||
"& .select__indicator-separator": {
|
"& .select__indicator-separator": {
|
||||||
backgroundColor: state.isFocused
|
backgroundColor: state.isFocused
|
||||||
? "rgb(161,161,170)"
|
? "rgb(161,161,170)"
|
||||||
: "rgb(212 ,212 ,216)",
|
: "rgb(212 ,212 ,216)",
|
||||||
},
|
},
|
||||||
|
|
||||||
"& .select__dropdown-indicator": {
|
"& .select__dropdown-indicator": {
|
||||||
color: state.isFocused
|
color: state.isFocused
|
||||||
? "rgb(161,161,170)"
|
? "rgb(161,161,170)"
|
||||||
: "rgb(212 ,212 ,216)",
|
: "rgb(212 ,212 ,216)",
|
||||||
transition: "none",
|
transition: "none",
|
||||||
},
|
},
|
||||||
}) as CSSObjectWithLabel,
|
}) as CSSObjectWithLabel,
|
||||||
multiValue: (base) =>
|
multiValue: (base) =>
|
||||||
({
|
({
|
||||||
...base,
|
...base,
|
||||||
backgroundColor: undefined,
|
backgroundColor: undefined,
|
||||||
borderRadius: "0.5rem",
|
borderRadius: "0.5rem",
|
||||||
}) as CSSObjectWithLabel,
|
}) as CSSObjectWithLabel,
|
||||||
multiValueLabel: (base) =>
|
multiValueLabel: (base) =>
|
||||||
({
|
({
|
||||||
...base,
|
...base,
|
||||||
borderRadius: "0.5rem",
|
borderRadius: "0.5rem",
|
||||||
}) as CSSObjectWithLabel,
|
}) as CSSObjectWithLabel,
|
||||||
menu: (base) =>
|
menu: (base) =>
|
||||||
({
|
({
|
||||||
...base,
|
...base,
|
||||||
borderRadius: "0.5rem",
|
borderRadius: "0.5rem",
|
||||||
}) as CSSObjectWithLabel,
|
}) as CSSObjectWithLabel,
|
||||||
option: (base, state) =>
|
option: (base, state) =>
|
||||||
({
|
({
|
||||||
...base,
|
...base,
|
||||||
backgroundColor: undefined,
|
backgroundColor: undefined,
|
||||||
color: state.isSelected ? undefined : undefined,
|
color: state.isSelected ? undefined : undefined,
|
||||||
|
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
backgroundColor: undefined,
|
backgroundColor: undefined,
|
||||||
color: state.isSelected ? undefined : undefined,
|
color: state.isSelected ? undefined : undefined,
|
||||||
},
|
},
|
||||||
|
|
||||||
"&:active": {
|
"&:active": {
|
||||||
backgroundColor: "rgb(161, 161, 170)",
|
backgroundColor: "rgb(161, 161, 170)",
|
||||||
color: "rgb(255, 255, 255)",
|
color: "rgb(255, 255, 255)",
|
||||||
},
|
},
|
||||||
|
|
||||||
"&:focus": {
|
"&:focus": {
|
||||||
backgroundColor: undefined,
|
backgroundColor: undefined,
|
||||||
},
|
},
|
||||||
}) as CSSObjectWithLabel,
|
}) as CSSObjectWithLabel,
|
||||||
}}
|
}}
|
||||||
classNames={{
|
classNames={{
|
||||||
valueContainer: () => cn("min-h-[38px]"),
|
valueContainer: () => cn("min-h-[38px]"),
|
||||||
control: () =>
|
control: () =>
|
||||||
cn(
|
cn(
|
||||||
"mt-2 w-full rounded-lg h-min-[42px] dark:bg-primary bg-white shadow-xs outline-hidden text-foreground dark:text-white",
|
"mt-2 w-full rounded-lg h-min-[42px] dark:bg-primary bg-white shadow-xs outline-hidden text-foreground dark:text-white",
|
||||||
),
|
),
|
||||||
option: () =>
|
option: () =>
|
||||||
cn(
|
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",
|
"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: () =>
|
menu: () =>
|
||||||
cn(
|
cn(
|
||||||
`dark:bg-black text-foreground bg-white dark:text-white border shadow-xs outline-hidden
|
`dark:bg-black text-foreground bg-white dark:text-white border shadow-xs outline-hidden
|
||||||
dark:border-foreground border-neutral-300 overflow-hidden`,
|
dark:border-foreground border-neutral-300 overflow-hidden`,
|
||||||
menuOpen
|
menuOpen
|
||||||
? "animate-in fade-in-0 zoom-in-90"
|
? "animate-in fade-in-0 zoom-in-90"
|
||||||
: "animate-out fade-out-0 zoom-out-90",
|
: "animate-out fade-out-0 zoom-out-90",
|
||||||
),
|
),
|
||||||
multiValueLabel: () =>
|
multiValueLabel: () =>
|
||||||
cn(
|
cn(
|
||||||
"dark:bg-black rounded-lg text-primary bg-white dark:text-white outline-hidden",
|
"dark:bg-black rounded-lg text-primary bg-white dark:text-white outline-hidden",
|
||||||
),
|
),
|
||||||
multiValue: () =>
|
multiValue: () =>
|
||||||
cn(
|
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",
|
"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"),
|
multiValueRemove: () => cn("rounded-s-none rounded-e-md"),
|
||||||
input: () => cn("*:ring-0 focus:*:ring-0"),
|
input: () => cn("*:ring-0 focus:*:ring-0"),
|
||||||
singleValue: () => cn("text-primary font-medium"),
|
singleValue: () => cn("text-primary font-medium"),
|
||||||
}}
|
}}
|
||||||
classNamePrefix="select"
|
classNamePrefix="select"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type UnpackOptions = {
|
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) => ({ value: idx.toString(), label: v }));
|
||||||
};
|
};
|
||||||
|
|
||||||
type RepackValues = {
|
type RepackValues = {
|
||||||
values: string | string[];
|
values: string | string[];
|
||||||
options: string[];
|
options: string[];
|
||||||
};
|
};
|
||||||
export const RepackValues = ({ values, options }: RepackValues) => {
|
export const RepackValues = ({ values, options }: RepackValues) => {
|
||||||
if (Array.isArray(values)) {
|
if (Array.isArray(values)) {
|
||||||
return values.map((v) => ({
|
return values.map((v) => ({
|
||||||
label: options[Number.parseInt(v)]!,
|
// biome-ignore lint/style/noNonNullAssertion: <intended>
|
||||||
value: v,
|
label: options[Number.parseInt(v)]!,
|
||||||
}));
|
value: v,
|
||||||
}
|
}));
|
||||||
return {
|
}
|
||||||
label: options[Number.parseInt(values)]!,
|
return {
|
||||||
value: values,
|
// biome-ignore lint/style/noNonNullAssertion: <intended>
|
||||||
};
|
label: options[Number.parseInt(values)]!,
|
||||||
|
value: values,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,40 +3,40 @@
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
createContext,
|
type ComponentPropsWithoutRef,
|
||||||
forwardRef,
|
createContext,
|
||||||
useContext,
|
type Dispatch,
|
||||||
useEffect,
|
type ElementRef,
|
||||||
useRef,
|
forwardRef,
|
||||||
useState,
|
type HTMLAttributes,
|
||||||
type ComponentPropsWithoutRef,
|
type PointerEvent,
|
||||||
type Dispatch,
|
type ReactNode,
|
||||||
type ElementRef,
|
type SetStateAction,
|
||||||
type HTMLAttributes,
|
useContext,
|
||||||
type PointerEvent,
|
useEffect,
|
||||||
type ReactNode,
|
useRef,
|
||||||
type SetStateAction,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
interface DialogContextValue {
|
interface DialogContextValue {
|
||||||
innerOpen: boolean;
|
innerOpen: boolean;
|
||||||
setInnerOpen: Dispatch<SetStateAction<boolean>>;
|
setInnerOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DialogContext = createContext<DialogContextValue | undefined>(undefined);
|
const DialogContext = createContext<DialogContextValue | undefined>(undefined);
|
||||||
|
|
||||||
function Dialog({ children }: { children: ReactNode }) {
|
function Dialog({ children }: { children: ReactNode }) {
|
||||||
const [outerOpen, setOuterOpen] = useState(false);
|
const [outerOpen, setOuterOpen] = useState(false);
|
||||||
const [innerOpen, setInnerOpen] = useState(false);
|
const [innerOpen, setInnerOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogContext.Provider value={{ innerOpen, setInnerOpen }}>
|
<DialogContext.Provider value={{ innerOpen, setInnerOpen }}>
|
||||||
<DialogPrimitive.Root open={outerOpen} onOpenChange={setOuterOpen}>
|
<DialogPrimitive.Root open={outerOpen} onOpenChange={setOuterOpen}>
|
||||||
{children}
|
{children}
|
||||||
</DialogPrimitive.Root>
|
</DialogPrimitive.Root>
|
||||||
</DialogContext.Provider>
|
</DialogContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const DialogTrigger = DialogPrimitive.Trigger;
|
const DialogTrigger = DialogPrimitive.Trigger;
|
||||||
|
|
@ -44,301 +44,301 @@ const DialogPortal = DialogPrimitive.Portal;
|
||||||
const DialogClose = DialogPrimitive.Close;
|
const DialogClose = DialogPrimitive.Close;
|
||||||
|
|
||||||
const DialogOverlay = forwardRef<
|
const DialogOverlay = forwardRef<
|
||||||
ElementRef<typeof DialogPrimitive.Overlay>,
|
ElementRef<typeof DialogPrimitive.Overlay>,
|
||||||
ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
ref={ref}
|
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,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||||
|
|
||||||
const DialogContent = forwardRef<
|
const DialogContent = forwardRef<
|
||||||
ElementRef<typeof DialogPrimitive.Content>,
|
ElementRef<typeof DialogPrimitive.Content>,
|
||||||
ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||||
>(({ className, children, ...props }, ref) => {
|
>(({ className, children, ...props }, ref) => {
|
||||||
const context = useContext(DialogContext);
|
const context = useContext(DialogContext);
|
||||||
if (!context) throw new Error("DialogContent must be used within a Dialog");
|
if (!context) throw new Error("DialogContent must be used within a Dialog");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogPortal>
|
<DialogPortal>
|
||||||
<DialogOverlay />
|
<DialogOverlay />
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
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,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<DialogClose className="ring-offset-background data-[state=open]:bg-accent data-[state=open]:text-muted-foreground focus:ring-ring absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
<DialogClose className="ring-offset-background data-[state=open]:bg-accent data-[state=open]:text-muted-foreground focus:ring-ring absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||||
<X className="size-4" />
|
<X className="size-4" />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||||
|
|
||||||
function InnerDialog({ children }: { children: ReactNode }) {
|
function InnerDialog({ children }: { children: ReactNode }) {
|
||||||
const context = useContext(DialogContext);
|
const context = useContext(DialogContext);
|
||||||
if (!context) throw new Error("InnerDialog must be used within a Dialog");
|
if (!context) throw new Error("InnerDialog must be used within a Dialog");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleEscapeKeyDown = (event: KeyboardEvent) => {
|
const handleEscapeKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.key === "Escape" && context.innerOpen) {
|
if (event.key === "Escape" && context.innerOpen) {
|
||||||
context.setInnerOpen(false);
|
context.setInnerOpen(false);
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("keydown", handleEscapeKeyDown);
|
document.addEventListener("keydown", handleEscapeKeyDown);
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("keydown", handleEscapeKeyDown);
|
document.removeEventListener("keydown", handleEscapeKeyDown);
|
||||||
};
|
};
|
||||||
}, [context.innerOpen, context.setInnerOpen]);
|
}, [context.innerOpen, context.setInnerOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogPrimitive.Root
|
<DialogPrimitive.Root
|
||||||
open={context.innerOpen}
|
open={context.innerOpen}
|
||||||
onOpenChange={context.setInnerOpen}
|
onOpenChange={context.setInnerOpen}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</DialogPrimitive.Root>
|
</DialogPrimitive.Root>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const InnerDialogTrigger = DialogPrimitive.Trigger;
|
const InnerDialogTrigger = DialogPrimitive.Trigger;
|
||||||
const InnerDialogClose = DialogPrimitive.Close;
|
const InnerDialogClose = DialogPrimitive.Close;
|
||||||
|
|
||||||
interface InnerDialogContentProps
|
interface InnerDialogContentProps
|
||||||
extends ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
|
extends ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
|
||||||
position?: "default" | "bottom" | "top" | "left" | "right";
|
position?: "default" | "bottom" | "top" | "left" | "right";
|
||||||
draggable?: boolean;
|
draggable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const InnerDialogContent = forwardRef<
|
const InnerDialogContent = forwardRef<
|
||||||
ElementRef<typeof DialogPrimitive.Content>,
|
ElementRef<typeof DialogPrimitive.Content>,
|
||||||
InnerDialogContentProps
|
InnerDialogContentProps
|
||||||
>(
|
>(
|
||||||
(
|
(
|
||||||
{ className, children, position = "default", draggable = false, ...props },
|
{ className, children, position = "default", draggable = false, ...props },
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const context = useContext(DialogContext);
|
const context = useContext(DialogContext);
|
||||||
if (!context)
|
if (!context)
|
||||||
throw new Error("InnerDialogContent must be used within a Dialog");
|
throw new Error("InnerDialogContent must be used within a Dialog");
|
||||||
|
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [startY, setStartY] = useState(0);
|
const [startY, setStartY] = useState(0);
|
||||||
const [currentY, setCurrentY] = useState(0);
|
const [currentY, setCurrentY] = useState(0);
|
||||||
const [isClosingByDrag, setIsClosingByDrag] = useState(false);
|
const [isClosingByDrag, setIsClosingByDrag] = useState(false);
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (context.innerOpen) {
|
if (context.innerOpen) {
|
||||||
setCurrentY(0);
|
setCurrentY(0);
|
||||||
setIsClosingByDrag(false);
|
setIsClosingByDrag(false);
|
||||||
}
|
}
|
||||||
}, [context.innerOpen]);
|
}, [context.innerOpen]);
|
||||||
|
|
||||||
const handlePointerDown = (e: PointerEvent<HTMLDivElement>) => {
|
const handlePointerDown = (e: PointerEvent<HTMLDivElement>) => {
|
||||||
if (!draggable) return;
|
if (!draggable) return;
|
||||||
setIsDragging(true);
|
setIsDragging(true);
|
||||||
setStartY(e.clientY - currentY);
|
setStartY(e.clientY - currentY);
|
||||||
e.currentTarget.setPointerCapture(e.pointerId);
|
e.currentTarget.setPointerCapture(e.pointerId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePointerMove = (e: PointerEvent<HTMLDivElement>) => {
|
const handlePointerMove = (e: PointerEvent<HTMLDivElement>) => {
|
||||||
if (!isDragging || !draggable) return;
|
if (!isDragging || !draggable) return;
|
||||||
const newY = e.clientY - startY;
|
const newY = e.clientY - startY;
|
||||||
setCurrentY(newY > 0 ? newY : 0);
|
setCurrentY(newY > 0 ? newY : 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePointerUp = () => {
|
const handlePointerUp = () => {
|
||||||
if (!draggable) return;
|
if (!draggable) return;
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
if (currentY > (contentRef.current?.offsetHeight || 0) / 2) {
|
if (currentY > (contentRef.current?.offsetHeight || 0) / 2) {
|
||||||
setIsClosingByDrag(true);
|
setIsClosingByDrag(true);
|
||||||
context.setInnerOpen(false);
|
context.setInnerOpen(false);
|
||||||
} else {
|
} else {
|
||||||
setCurrentY(0);
|
setCurrentY(0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogPortal>
|
<DialogPortal>
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onPointerDown={handlePointerDown}
|
onPointerDown={handlePointerDown}
|
||||||
onPointerMove={handlePointerMove}
|
onPointerMove={handlePointerMove}
|
||||||
onPointerUp={handlePointerUp}
|
onPointerUp={handlePointerUp}
|
||||||
style={{
|
style={{
|
||||||
transform: `translate(-50%, calc(-50% + ${currentY}px))`,
|
transform: `translate(-50%, calc(-50% + ${currentY}px))`,
|
||||||
transition: isDragging ? "none" : "transform 0.3s ease-out",
|
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
|
||||||
? "data-[state=closed]:fade-out-0 data-[state=closed]:animate-none"
|
? "data-[state=closed]:fade-out-0 data-[state=closed]:animate-none"
|
||||||
: "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%]",
|
: "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%]",
|
||||||
position === "default" &&
|
position === "default" &&
|
||||||
"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%]",
|
"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%]",
|
||||||
position === "bottom" &&
|
position === "bottom" &&
|
||||||
"data-[state=closed]:slide-out-to-bottom-full data-[state=open]:slide-in-from-bottom-full",
|
"data-[state=closed]:slide-out-to-bottom-full data-[state=open]:slide-in-from-bottom-full",
|
||||||
position === "top" &&
|
position === "top" &&
|
||||||
"data-[state=closed]:slide-out-to-top-full data-[state=open]:slide-in-from-top-full",
|
"data-[state=closed]:slide-out-to-top-full data-[state=open]:slide-in-from-top-full",
|
||||||
position === "left" &&
|
position === "left" &&
|
||||||
"data-[state=closed]:slide-out-to-left-full data-[state=open]:slide-in-from-left-full",
|
"data-[state=closed]:slide-out-to-left-full data-[state=open]:slide-in-from-left-full",
|
||||||
position === "right" &&
|
position === "right" &&
|
||||||
"data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-right-full",
|
"data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-right-full",
|
||||||
draggable && "",
|
draggable && "",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div ref={contentRef}>{children}</div>
|
<div ref={contentRef}>{children}</div>
|
||||||
<InnerDialogClose className="ring-offset-background data-[state=open]:bg-accent data-[state=open]:text-muted-foreground focus:ring-ring absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
<InnerDialogClose className="ring-offset-background data-[state=open]:bg-accent data-[state=open]:text-muted-foreground focus:ring-ring absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||||
<X className="size-4" />
|
<X className="size-4" />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</InnerDialogClose>
|
</InnerDialogClose>
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
InnerDialogContent.displayName = "InnerDialogContent";
|
InnerDialogContent.displayName = "InnerDialogContent";
|
||||||
|
|
||||||
const InnerDialogHeader = ({
|
const InnerDialogHeader = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: HTMLAttributes<HTMLDivElement>) => (
|
}: HTMLAttributes<HTMLDivElement>) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
InnerDialogHeader.displayName = "InnerDialogHeader";
|
InnerDialogHeader.displayName = "InnerDialogHeader";
|
||||||
|
|
||||||
const InnerDialogFooter = ({
|
const InnerDialogFooter = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: HTMLAttributes<HTMLDivElement>) => (
|
}: HTMLAttributes<HTMLDivElement>) => (
|
||||||
<div
|
<div
|
||||||
className={cn("flex flex-col-reverse sm:flex-row sm:space-x-2", className)}
|
className={cn("flex flex-col-reverse sm:flex-row sm:space-x-2", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
InnerDialogFooter.displayName = "InnerDialogFooter";
|
InnerDialogFooter.displayName = "InnerDialogFooter";
|
||||||
|
|
||||||
const InnerDialogTitle = forwardRef<
|
const InnerDialogTitle = forwardRef<
|
||||||
ElementRef<typeof DialogPrimitive.Title>,
|
ElementRef<typeof DialogPrimitive.Title>,
|
||||||
ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DialogPrimitive.Title
|
<DialogPrimitive.Title
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-lg leading-none font-semibold tracking-tight",
|
"text-lg leading-none font-semibold tracking-tight",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
InnerDialogTitle.displayName = "InnerDialogTitle";
|
InnerDialogTitle.displayName = "InnerDialogTitle";
|
||||||
|
|
||||||
const InnerDialogDescription = forwardRef<
|
const InnerDialogDescription = forwardRef<
|
||||||
ElementRef<typeof DialogPrimitive.Description>,
|
ElementRef<typeof DialogPrimitive.Description>,
|
||||||
ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DialogPrimitive.Description
|
<DialogPrimitive.Description
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
InnerDialogDescription.displayName = "InnerDialogDescription";
|
InnerDialogDescription.displayName = "InnerDialogDescription";
|
||||||
|
|
||||||
const DialogHeader = ({
|
const DialogHeader = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: HTMLAttributes<HTMLDivElement>) => (
|
}: HTMLAttributes<HTMLDivElement>) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
DialogHeader.displayName = "DialogHeader";
|
DialogHeader.displayName = "DialogHeader";
|
||||||
|
|
||||||
const DialogFooter = ({
|
const DialogFooter = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: HTMLAttributes<HTMLDivElement>) => (
|
}: HTMLAttributes<HTMLDivElement>) => (
|
||||||
<div
|
<div
|
||||||
className={cn("flex flex-col-reverse sm:flex-row sm:space-x-2", className)}
|
className={cn("flex flex-col-reverse sm:flex-row sm:space-x-2", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
DialogFooter.displayName = "DialogFooter";
|
DialogFooter.displayName = "DialogFooter";
|
||||||
|
|
||||||
const DialogTitle = forwardRef<
|
const DialogTitle = forwardRef<
|
||||||
ElementRef<typeof DialogPrimitive.Title>,
|
ElementRef<typeof DialogPrimitive.Title>,
|
||||||
ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DialogPrimitive.Title
|
<DialogPrimitive.Title
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-lg leading-none font-semibold tracking-tight",
|
"text-lg leading-none font-semibold tracking-tight",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||||
|
|
||||||
const DialogDescription = forwardRef<
|
const DialogDescription = forwardRef<
|
||||||
ElementRef<typeof DialogPrimitive.Description>,
|
ElementRef<typeof DialogPrimitive.Description>,
|
||||||
ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DialogPrimitive.Description
|
<DialogPrimitive.Description
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||||
|
|
||||||
export type { InnerDialogContentProps };
|
export type { InnerDialogContentProps };
|
||||||
export {
|
export {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogClose,
|
DialogClose,
|
||||||
InnerDialog,
|
InnerDialog,
|
||||||
InnerDialogTrigger,
|
InnerDialogTrigger,
|
||||||
InnerDialogContent,
|
InnerDialogContent,
|
||||||
InnerDialogHeader,
|
InnerDialogHeader,
|
||||||
InnerDialogFooter,
|
InnerDialogFooter,
|
||||||
InnerDialogTitle,
|
InnerDialogTitle,
|
||||||
InnerDialogDescription,
|
InnerDialogDescription,
|
||||||
InnerDialogClose,
|
InnerDialogClose,
|
||||||
DialogPortal,
|
DialogPortal,
|
||||||
DialogOverlay,
|
DialogOverlay,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,49 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
import { motion, useInView } from "framer-motion";
|
import { motion, useInView } from "framer-motion";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
type TextStaggeredFadeProps = {
|
type TextStaggeredFadeProps = {
|
||||||
text: string;
|
text: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
hide?: boolean;
|
hide?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StaggeredFade: React.FC<TextStaggeredFadeProps> = ({
|
export const StaggeredFade: React.FC<TextStaggeredFadeProps> = ({
|
||||||
text,
|
text,
|
||||||
className = "",
|
className = "",
|
||||||
hide = false,
|
hide = false,
|
||||||
}) => {
|
}) => {
|
||||||
const variants = {
|
const variants = {
|
||||||
hidden: { opacity: 0 },
|
hidden: { opacity: 0 },
|
||||||
show: (i: number) => ({
|
show: (i: number) => ({
|
||||||
y: 0,
|
y: 0,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
transition: { delay: i * 0.07 },
|
transition: { delay: i * 0.07 },
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const letters = text.split("");
|
const letters = text.split("");
|
||||||
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}
|
ref={ref}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
animate={isInView ? (!hide ? "show" : "hidden") : ""}
|
animate={isInView ? (!hide ? "show" : "hidden") : ""}
|
||||||
variants={variants}
|
variants={variants}
|
||||||
viewport={{ once: true }}
|
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,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{letters.map((word, i) => (
|
{letters.map((word, i) => (
|
||||||
<motion.span key={`${word}-${i}`} variants={variants} custom={i}>
|
<motion.span key={`${word}-${i}`} variants={variants} custom={i}>
|
||||||
{word}
|
{word}
|
||||||
</motion.span>
|
</motion.span>
|
||||||
))}
|
))}
|
||||||
</motion.h2>
|
</motion.h2>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,18 @@ import { cn } from "src/lib/utils";
|
||||||
type TextareaProps = TextareaHTMLAttributes<HTMLTextAreaElement>;
|
type TextareaProps = TextareaHTMLAttributes<HTMLTextAreaElement>;
|
||||||
|
|
||||||
const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||||
({ className, ...props }, ref) => {
|
({ className, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<textarea
|
<textarea
|
||||||
className={cn(
|
className={cn(
|
||||||
`border-border bg-background hover:border-muted-foreground focus:border-foreground focus:ring-foreground flex min-h-[80px] w-full rounded-lg border px-3 py-2 shadow-sm outline-hidden transition-all duration-200 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-500 dark:placeholder:text-neutral-400 dark:hover:border-neutral-400 dark:focus:border-transparent`,
|
`border-border bg-background hover:border-muted-foreground focus:border-foreground focus:ring-foreground flex min-h-[80px] w-full rounded-lg border px-3 py-2 shadow-sm outline-hidden transition-all duration-200 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-500 dark:placeholder:text-neutral-400 dark:hover:border-neutral-400 dark:focus:border-transparent`,
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
Textarea.displayName = "Textarea";
|
Textarea.displayName = "Textarea";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,17 @@ import { MissingFile } from "~/components/svgs";
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
|
|
||||||
export const DocNotFoundPage = () => {
|
export const DocNotFoundPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-[60vh] w-full items-center justify-center">
|
<div className="flex min-h-[60vh] w-full items-center justify-center">
|
||||||
<div className="max-w-lg space-y-5 rounded-xl border bg-white p-10 text-center shadow-lg">
|
<div className="max-w-lg space-y-5 rounded-xl border bg-white p-10 text-center shadow-lg">
|
||||||
<MissingFile className="mx-auto mb-4 size-32 fill-indigo-600" />
|
<MissingFile className="mx-auto mb-4 size-32 fill-indigo-600" />
|
||||||
<h1 className="mb-2 text-3xl font-extrabold text-indigo-600">
|
<h1 className="mb-2 text-3xl font-extrabold text-indigo-600">
|
||||||
{t.doc_not_found.titolo}
|
{t.doc_not_found.titolo}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground">{t.doc_not_found.sottotitolo}</p>
|
<p className="text-muted-foreground">{t.doc_not_found.sottotitolo}</p>
|
||||||
<p className="text-muted-foreground">{t.doc_not_found.CTA}</p>
|
<p className="text-muted-foreground">{t.doc_not_found.CTA}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,191 +1,192 @@
|
||||||
|
import { Plus } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "~/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "~/components/ui/select";
|
||||||
|
import { Separator } from "~/components/ui/separator";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
import type { ChatsChatid } from "~/schemas/public/Chats";
|
import type { ChatsChatid } from "~/schemas/public/Chats";
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "~/components/ui/dialog";
|
|
||||||
import type { EtichetteIdEtichetta } from "~/schemas/public/Etichette";
|
import type { EtichetteIdEtichetta } from "~/schemas/public/Etichette";
|
||||||
import { Button } from "~/components/ui/button";
|
import { api } from "~/utils/api";
|
||||||
import { Separator } from "~/components/ui/separator";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "~/components/ui/select";
|
|
||||||
import { Plus } from "lucide-react";
|
|
||||||
|
|
||||||
export const EtichetteDisplayer = ({
|
export const EtichetteDisplayer = ({
|
||||||
chatId,
|
chatId,
|
||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
chatId: ChatsChatid;
|
chatId: ChatsChatid;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { data, isLoading } = api.chat.getChatEtichette.useQuery({
|
const { data, isLoading } = api.chat.getChatEtichette.useQuery({
|
||||||
chatId,
|
chatId,
|
||||||
});
|
});
|
||||||
const [openEdit, setOpenEdit] = useState(false);
|
const [openEdit, setOpenEdit] = useState(false);
|
||||||
|
|
||||||
const handleOpen = (status: boolean) => {
|
const handleOpen = (status: boolean) => {
|
||||||
setOpenEdit(status);
|
setOpenEdit(status);
|
||||||
};
|
};
|
||||||
|
|
||||||
if ((!isLoading && !data) || !data) return null;
|
if ((!isLoading && !data) || !data) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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
|
||||||
key={etichetta.id_etichetta}
|
key={etichetta.id_etichetta}
|
||||||
title={etichetta.title}
|
title={etichetta.title}
|
||||||
color_hex={etichetta.color_hex}
|
color_hex={etichetta.color_hex}
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<button
|
<button
|
||||||
onClick={() => setOpenEdit(true)}
|
type="button"
|
||||||
className="flex items-center justify-center rounded-lg border p-1"
|
onClick={() => setOpenEdit(true)}
|
||||||
>
|
className="flex items-center justify-center rounded-lg border p-1"
|
||||||
<Plus className="size-4" />
|
>
|
||||||
</button>
|
<Plus className="size-4" />
|
||||||
<EtichetteModal
|
</button>
|
||||||
chatId={chatId}
|
<EtichetteModal
|
||||||
open={openEdit}
|
chatId={chatId}
|
||||||
onOpenChange={handleOpen}
|
open={openEdit}
|
||||||
/>
|
onOpenChange={handleOpen}
|
||||||
</div>
|
/>
|
||||||
);
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EtichetteModal = ({
|
export const EtichetteModal = ({
|
||||||
chatId,
|
chatId,
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: {
|
}: {
|
||||||
chatId: ChatsChatid;
|
chatId: ChatsChatid;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (status: boolean) => void;
|
onOpenChange: (status: boolean) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [selected, setSelected] = useState<EtichetteIdEtichetta | null>(null);
|
const [selected, setSelected] = useState<EtichetteIdEtichetta | null>(null);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { data } = api.chat.getChatEtichette.useQuery({ chatId });
|
const { data } = api.chat.getChatEtichette.useQuery({ chatId });
|
||||||
const { data: etichette } = api.settings.getAllEtichette.useQuery();
|
const { data: etichette } = api.settings.getAllEtichette.useQuery();
|
||||||
|
|
||||||
const { mutate: remove } = api.chat.removeEtichettaFromChat.useMutation({
|
const { mutate: remove } = api.chat.removeEtichettaFromChat.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await utils.chat.getChatEtichette.invalidate({ chatId });
|
await utils.chat.getChatEtichette.invalidate({ chatId });
|
||||||
await utils.chat.getActiveChats.invalidate();
|
await utils.chat.getActiveChats.invalidate();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { mutate: add } = api.chat.addEtichettaToChat.useMutation({
|
const { mutate: add } = api.chat.addEtichettaToChat.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await utils.chat.getChatEtichette.invalidate({ chatId });
|
await utils.chat.getChatEtichette.invalidate({ chatId });
|
||||||
await utils.chat.getActiveChats.invalidate();
|
await utils.chat.getActiveChats.invalidate();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Etichette Attive</DialogTitle>
|
<DialogTitle>Etichette Attive</DialogTitle>
|
||||||
<DialogDescription className="sr-only">Attive</DialogDescription>
|
<DialogDescription className="sr-only">Attive</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<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 key={idx} className="flex gap-2">
|
||||||
<Etichetta
|
<Etichetta
|
||||||
title={etichetta.title}
|
title={etichetta.title}
|
||||||
color_hex={etichetta.color_hex}
|
color_hex={etichetta.color_hex}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
remove({ chatId, etichettaId: etichetta.id_etichetta });
|
remove({ chatId, etichettaId: etichetta.id_etichetta });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Rimuovi
|
Rimuovi
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Select
|
<Select
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
setSelected(parseInt(value) as EtichetteIdEtichetta)
|
setSelected(parseInt(value) as EtichetteIdEtichetta)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[180px]">
|
<SelectTrigger className="w-[180px]">
|
||||||
<SelectValue placeholder="Seleziona" />
|
<SelectValue placeholder="Seleziona" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{etichette?.map((etichetta, idx) => (
|
{etichette?.map((etichetta, idx) => (
|
||||||
<SelectItem key={idx} value={String(etichetta.id_etichetta)}>
|
<SelectItem key={idx} value={String(etichetta.id_etichetta)}>
|
||||||
{etichetta.title}
|
{etichetta.title}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
add({ chatId, etichettaId: selected });
|
add({ chatId, etichettaId: selected });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Aggiungi
|
Aggiungi
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Etichetta = ({
|
export const Etichetta = ({
|
||||||
title,
|
title,
|
||||||
color_hex,
|
color_hex,
|
||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string;
|
||||||
color_hex: string;
|
color_hex: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-fit items-center justify-center rounded-md px-1.5 text-sm whitespace-nowrap select-none",
|
"flex w-fit items-center justify-center rounded-md px-1.5 text-sm whitespace-nowrap select-none",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: color_hex,
|
backgroundColor: color_hex,
|
||||||
color: colorIsDarkSimple(color_hex) ? "#FFFFFF" : "#000000",
|
color: colorIsDarkSimple(color_hex) ? "#FFFFFF" : "#000000",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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,
|
r: (bigint >> 16) & 255,
|
||||||
g: (bigint >> 8) & 255,
|
g: (bigint >> 8) & 255,
|
||||||
b: bigint & 255,
|
b: bigint & 255,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function colorIsDarkSimple(color: string) {
|
export function colorIsDarkSimple(color: string) {
|
||||||
if (color == "#FFFFFF") return false;
|
if (color === "#FFFFFF") return false;
|
||||||
const { r, g, b } = hexToRgb(color);
|
const { r, g, b } = hexToRgb(color);
|
||||||
return r * 0.299 + g * 0.587 + b * 0.114 <= 186;
|
return r * 0.299 + g * 0.587 + b * 0.114 <= 186;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,39 @@
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
|
||||||
import { HomeIcon } from "~/components/IconComponents";
|
import { HomeIcon } from "~/components/IconComponents";
|
||||||
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
|
|
||||||
export default function FailedAnnuncioLoading() {
|
export default function FailedAnnuncioLoading() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<div className="mx-auto flex h-screen max-w-screen-xl items-center justify-start px-4 md:px-8">
|
<div className="mx-auto flex h-screen max-w-screen-xl items-center justify-start px-4 md:px-8">
|
||||||
<div className="mx-auto max-w-lg space-y-3 text-center">
|
<div className="mx-auto max-w-lg space-y-3 text-center">
|
||||||
<HomeIcon className="mx-auto size-20 text-rose-700" />
|
<HomeIcon className="mx-auto size-20 text-rose-700" />
|
||||||
<h3 className="text-accent-foreground text-4xl font-semibold sm:text-5xl">
|
<h3 className="text-accent-foreground text-4xl font-semibold sm:text-5xl">
|
||||||
{t.annuncio_load_fail.titolo}
|
{t.annuncio_load_fail.titolo}
|
||||||
</h3>
|
</h3>
|
||||||
<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
|
||||||
onClick={() => {
|
type="button"
|
||||||
router.back();
|
onClick={() => {
|
||||||
}}
|
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"
|
}}
|
||||||
>
|
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"
|
||||||
{t.annuncio_load_fail.CTA}
|
>
|
||||||
</button>
|
{t.annuncio_load_fail.CTA}
|
||||||
<Link
|
</button>
|
||||||
href="/annunci"
|
<Link
|
||||||
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"
|
||||||
>
|
className="block rounded-lg border px-4 py-2 font-medium text-neutral-700 duration-150 hover:bg-neutral-50 active:bg-neutral-100"
|
||||||
{t.annuncio_load_fail.home}
|
>
|
||||||
</Link>
|
{t.annuncio_load_fail.home}
|
||||||
</div>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</div>
|
||||||
);
|
</main>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,104 +3,98 @@ import BlurryDivider from "~/components/blurry_divider";
|
||||||
import { LogoSvg } from "~/components/svgs";
|
import { LogoSvg } from "~/components/svgs";
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
export const Footer = () => {
|
export const Footer = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div className="mt-auto">
|
<div className="mt-auto">
|
||||||
<footer
|
<footer className="bg-muted text-muted-foreground bottom-0 z-40 w-full">
|
||||||
aria-label="Site Footer"
|
<div className="mx-auto max-w-6xl px-4 py-6 sm:px-6 lg:px-8">
|
||||||
className="bg-muted text-muted-foreground bottom-0 z-40 w-full"
|
<div className="text-center">
|
||||||
>
|
<Link
|
||||||
<div className="mx-auto max-w-6xl px-4 py-6 sm:px-6 lg:px-8">
|
href="/"
|
||||||
<div className="text-center">
|
aria-label="footerLogo"
|
||||||
<Link
|
className="flex items-center justify-center text-3xl font-bold tracking-wide antialiased"
|
||||||
href="/"
|
>
|
||||||
aria-label="footerLogo"
|
<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" />
|
||||||
className="flex items-center justify-center text-3xl font-bold tracking-wide antialiased"
|
</Link>
|
||||||
>
|
</div>
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<nav aria-label="Footer Nav" className="mt-6">
|
<nav aria-label="Footer Nav" className="mt-6">
|
||||||
<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
|
||||||
className="text-sm"
|
className="text-sm"
|
||||||
href="/guida"
|
href="/guida"
|
||||||
aria-label="footerGuida"
|
aria-label="footerGuida"
|
||||||
>
|
>
|
||||||
{t.footer.Guida}
|
{t.footer.Guida}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<Link
|
<Link
|
||||||
className="text-sm"
|
className="text-sm"
|
||||||
href="/chi-siamo"
|
href="/chi-siamo"
|
||||||
aria-label="footerChiSiamo"
|
aria-label="footerChiSiamo"
|
||||||
>
|
>
|
||||||
{t.footer["Chi Siamo"]}
|
{t.footer["Chi Siamo"]}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<Link
|
<Link
|
||||||
className="text-sm"
|
className="text-sm"
|
||||||
href="/termini-condizioni"
|
href="/termini-condizioni"
|
||||||
aria-label="footerTerminiCondizioni"
|
aria-label="footerTerminiCondizioni"
|
||||||
>
|
>
|
||||||
{t.termini_condizioni_title}
|
{t.termini_condizioni_title}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link
|
<Link
|
||||||
className="text-sm"
|
className="text-sm"
|
||||||
href="/privacy-policy"
|
href="/privacy-policy"
|
||||||
aria-label="footerPrivacyPolicy"
|
aria-label="footerPrivacyPolicy"
|
||||||
>
|
>
|
||||||
{t.privacy_policy_title}
|
{t.privacy_policy_title}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<div className="py-4">
|
<div className="py-4">
|
||||||
<BlurryDivider />
|
<BlurryDivider />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-center text-xs">
|
<div className="text-center text-xs">
|
||||||
<p>{t.footer.endtxt}</p>
|
<p>{t.footer.endtxt}</p>
|
||||||
<p>{t.footer.endtxt2}</p>
|
<p>{t.footer.endtxt2}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export const MiniFooter = () => {
|
export const MiniFooter = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div className="mt-auto flex w-full">
|
<div className="mt-auto flex w-full">
|
||||||
<footer
|
<footer className="dark:bg-primary z-40 w-full bg-neutral-100 text-neutral-700 dark:text-white">
|
||||||
aria-label="Site Footer"
|
<div className="mx-auto max-w-5xl px-2 py-2 sm:px-6 lg:px-8">
|
||||||
className="dark:bg-primary z-40 w-full bg-neutral-100 text-neutral-700 dark:text-white"
|
<div className="text-center">
|
||||||
>
|
<Link
|
||||||
<div className="mx-auto max-w-5xl px-2 py-2 sm:px-6 lg:px-8">
|
aria-label="footerLogo"
|
||||||
<div className="text-center">
|
href="/"
|
||||||
<Link
|
className="text-2xl font-bold tracking-wide antialiased"
|
||||||
aria-label="footerLogo"
|
>
|
||||||
href="/"
|
Infoalloggi.it
|
||||||
className="text-2xl font-bold tracking-wide antialiased"
|
</Link>
|
||||||
>
|
</div>
|
||||||
Infoalloggi.it
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-2 text-center text-xs">
|
<div className="mt-2 text-center text-xs">
|
||||||
<p>{t.footer.endtxt}</p>
|
<p>{t.footer.endtxt}</p>
|
||||||
<p>{t.footer.endtxt2}</p>
|
<p>{t.footer.endtxt2}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@
|
||||||
import { ASvg } from "~/components/svgs";
|
import { ASvg } from "~/components/svgs";
|
||||||
|
|
||||||
export const LoadingPage = () => {
|
export const LoadingPage = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full max-h-full w-full animate-pulse items-center justify-center py-20">
|
<div className="flex h-full max-h-full w-full animate-pulse items-center justify-center py-20">
|
||||||
<div className="relative flex size-40 animate-spin items-center justify-center rounded-full border-8 border-gray-300 border-t-red-400 text-4xl text-red-400"></div>
|
<div className="relative flex size-40 animate-spin items-center justify-center rounded-full border-8 border-gray-300 border-t-red-400 text-4xl text-red-400"></div>
|
||||||
<ASvg className="absolute size-24 -translate-y-2" />
|
<ASvg className="absolute size-24 -translate-y-2" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,202 +1,202 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MapContainer,
|
Circle,
|
||||||
TileLayer,
|
MapContainer,
|
||||||
Marker,
|
Marker,
|
||||||
Popup,
|
Popup,
|
||||||
Circle,
|
TileLayer,
|
||||||
useMapEvents,
|
useMapEvents,
|
||||||
} from "react-leaflet";
|
} from "react-leaflet";
|
||||||
|
|
||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css";
|
import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css";
|
||||||
import "leaflet-defaulticon-compatibility";
|
import "leaflet-defaulticon-compatibility";
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
import MapMarker from "~/components/map/map_marker";
|
|
||||||
import { Marker as CustomMarker } from "~/components/map/custom_marker";
|
|
||||||
import { Home } from "lucide-react";
|
import { Home } from "lucide-react";
|
||||||
|
import { Marker as CustomMarker } from "~/components/map/custom_marker";
|
||||||
|
import MapMarker from "~/components/map/map_marker";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
type LatLon = {
|
type LatLon = {
|
||||||
lat: number;
|
lat: number;
|
||||||
lng: number;
|
lng: number;
|
||||||
alt?: number;
|
alt?: number;
|
||||||
};
|
};
|
||||||
type posData = {
|
type posData = {
|
||||||
pos: LatLon;
|
pos: LatLon;
|
||||||
popupTxt?: string;
|
popupTxt?: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
markerTxt?: string;
|
markerTxt?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type GroupProps = {
|
type GroupProps = {
|
||||||
isGroup: true;
|
isGroup: true;
|
||||||
groupData: posData[];
|
groupData: posData[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type SingleProps = {
|
type SingleProps = {
|
||||||
isGroup?: false;
|
isGroup?: false;
|
||||||
groupData?: never;
|
groupData?: never;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MapProps = {
|
type MapProps = {
|
||||||
posix: posData;
|
posix: posData;
|
||||||
autocenter?: boolean;
|
autocenter?: boolean;
|
||||||
zoom?: number;
|
zoom?: number;
|
||||||
markerType: "precise" | "circle" | "price" | "home";
|
markerType: "precise" | "circle" | "price" | "home";
|
||||||
markerRadius?: number;
|
markerRadius?: number;
|
||||||
scrollWheelZoom?: boolean;
|
scrollWheelZoom?: boolean;
|
||||||
width?: string;
|
width?: string;
|
||||||
height?: string;
|
height?: string;
|
||||||
onMapClick?: () => void;
|
onMapClick?: () => void;
|
||||||
} & (GroupProps | SingleProps);
|
} & (GroupProps | SingleProps);
|
||||||
|
|
||||||
const defaults = {
|
const defaults = {
|
||||||
zoom: 15,
|
zoom: 15,
|
||||||
width: "500px",
|
width: "500px",
|
||||||
height: "500px",
|
height: "500px",
|
||||||
};
|
};
|
||||||
|
|
||||||
const MedianPoint = (pos: posData[]) => {
|
const MedianPoint = (pos: posData[]) => {
|
||||||
const lat = pos.map((p) => p.pos.lat).reduce((a, b) => a + b) / pos.length;
|
const lat = pos.map((p) => p.pos.lat).reduce((a, b) => a + b) / pos.length;
|
||||||
const lng = pos.map((p) => p.pos.lng).reduce((a, b) => a + b) / pos.length;
|
const lng = pos.map((p) => p.pos.lng).reduce((a, b) => a + b) / pos.length;
|
||||||
return { lat, lng };
|
return { lat, lng };
|
||||||
};
|
};
|
||||||
|
|
||||||
const Map = ({
|
const MapComp = ({
|
||||||
posix,
|
posix,
|
||||||
autocenter,
|
autocenter,
|
||||||
zoom = defaults.zoom,
|
zoom = defaults.zoom,
|
||||||
markerType,
|
markerType,
|
||||||
markerRadius,
|
markerRadius,
|
||||||
scrollWheelZoom = false,
|
scrollWheelZoom = false,
|
||||||
width = defaults.width,
|
width = defaults.width,
|
||||||
height = defaults.height,
|
height = defaults.height,
|
||||||
isGroup,
|
isGroup,
|
||||||
groupData,
|
groupData,
|
||||||
onMapClick,
|
onMapClick,
|
||||||
}: MapProps) => {
|
}: MapProps) => {
|
||||||
const APIs = () => {
|
const APIs = () => {
|
||||||
useMapEvents({
|
useMapEvents({
|
||||||
click: () => {
|
click: () => {
|
||||||
if (onMapClick) {
|
if (onMapClick) {
|
||||||
onMapClick();
|
onMapClick();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MarkerComp = ({ pos }: { pos: posData }) => {
|
const MarkerComp = ({ pos }: { pos: posData }) => {
|
||||||
switch (markerType) {
|
switch (markerType) {
|
||||||
case "price":
|
case "price":
|
||||||
return (
|
return (
|
||||||
<CustomMarker
|
<CustomMarker
|
||||||
position={pos.pos}
|
position={pos.pos}
|
||||||
eventHandlers={pos.onClick && { click: pos.onClick }}
|
eventHandlers={pos.onClick && { click: pos.onClick }}
|
||||||
title={pos.markerTxt}
|
title={pos.markerTxt}
|
||||||
icon={
|
icon={
|
||||||
<MapMarker
|
<MapMarker
|
||||||
label={
|
label={
|
||||||
<span className="inline-block whitespace-nowrap">
|
<span className="inline-block whitespace-nowrap">
|
||||||
{pos.markerTxt}
|
{pos.markerTxt}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
color="primary"
|
color="primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{pos.popupTxt && <Popup>{pos.popupTxt}</Popup>}
|
{pos.popupTxt && <Popup>{pos.popupTxt}</Popup>}
|
||||||
</CustomMarker>
|
</CustomMarker>
|
||||||
);
|
);
|
||||||
|
|
||||||
case "precise":
|
case "precise":
|
||||||
return (
|
return (
|
||||||
<Marker
|
<Marker
|
||||||
position={pos.pos}
|
position={pos.pos}
|
||||||
eventHandlers={pos.onClick && { click: pos.onClick }}
|
eventHandlers={pos.onClick && { click: pos.onClick }}
|
||||||
title={pos.markerTxt}
|
title={pos.markerTxt}
|
||||||
>
|
>
|
||||||
{pos.popupTxt && <Popup>{pos.popupTxt}</Popup>}
|
{pos.popupTxt && <Popup>{pos.popupTxt}</Popup>}
|
||||||
</Marker>
|
</Marker>
|
||||||
);
|
);
|
||||||
case "circle":
|
case "circle":
|
||||||
return (
|
return (
|
||||||
<Circle
|
<Circle
|
||||||
center={pos.pos}
|
center={pos.pos}
|
||||||
pathOptions={{ color: "red" }}
|
pathOptions={{ color: "red" }}
|
||||||
radius={markerRadius || 100}
|
radius={markerRadius || 100}
|
||||||
eventHandlers={pos.onClick && { click: pos.onClick }}
|
eventHandlers={pos.onClick && { click: pos.onClick }}
|
||||||
>
|
>
|
||||||
{pos.popupTxt && <Popup>{pos.popupTxt}</Popup>}
|
{pos.popupTxt && <Popup>{pos.popupTxt}</Popup>}
|
||||||
</Circle>
|
</Circle>
|
||||||
);
|
);
|
||||||
case "home":
|
case "home":
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Circle
|
<Circle
|
||||||
center={pos.pos}
|
center={pos.pos}
|
||||||
pathOptions={{ color: "#ec4899" }}
|
pathOptions={{ color: "#ec4899" }}
|
||||||
fillOpacity={1}
|
fillOpacity={1}
|
||||||
radius={markerRadius || 100}
|
radius={markerRadius || 100}
|
||||||
eventHandlers={pos.onClick && { click: pos.onClick }}
|
eventHandlers={pos.onClick && { click: pos.onClick }}
|
||||||
>
|
>
|
||||||
{pos.popupTxt && <Popup>{pos.popupTxt}</Popup>}
|
{pos.popupTxt && <Popup>{pos.popupTxt}</Popup>}
|
||||||
</Circle>
|
</Circle>
|
||||||
<CustomMarker
|
<CustomMarker
|
||||||
position={pos.pos}
|
position={pos.pos}
|
||||||
eventHandlers={pos.onClick && { click: pos.onClick }}
|
eventHandlers={pos.onClick && { click: pos.onClick }}
|
||||||
title={pos.markerTxt}
|
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>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{pos.popupTxt && <Popup>{pos.popupTxt}</Popup>}
|
{pos.popupTxt && <Popup>{pos.popupTxt}</Popup>}
|
||||||
</CustomMarker>
|
</CustomMarker>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const hashKey = JSON.stringify(
|
const hashKey = JSON.stringify(
|
||||||
groupData ? groupData.map((p) => p.pos) : posix.pos,
|
groupData ? groupData.map((p) => p.pos) : posix.pos,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MapContainer
|
<MapContainer
|
||||||
key={hashKey}
|
key={hashKey}
|
||||||
center={
|
center={
|
||||||
autocenter ? MedianPoint(isGroup ? groupData : [posix]) : posix.pos
|
autocenter ? MedianPoint(isGroup ? groupData : [posix]) : posix.pos
|
||||||
}
|
}
|
||||||
zoom={zoom}
|
zoom={zoom}
|
||||||
scrollWheelZoom={scrollWheelZoom}
|
scrollWheelZoom={scrollWheelZoom}
|
||||||
style={{
|
style={{
|
||||||
height: height,
|
height: height,
|
||||||
width: width,
|
width: width,
|
||||||
borderRadius: "8px",
|
borderRadius: "8px",
|
||||||
}}
|
}}
|
||||||
className={cn("")}
|
className={cn("")}
|
||||||
>
|
>
|
||||||
<APIs />
|
<APIs />
|
||||||
<TileLayer
|
<TileLayer
|
||||||
url="/api/tiles/{s}/{z}/{x}/{y}"
|
url="/api/tiles/{s}/{z}/{x}/{y}"
|
||||||
//url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
//url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isGroup ? (
|
{isGroup ? (
|
||||||
groupData.map((pos, i) => <MarkerComp key={i} pos={pos} />)
|
groupData.map((pos, i) => <MarkerComp key={i} pos={pos} />)
|
||||||
) : (
|
) : (
|
||||||
<MarkerComp pos={posix} />
|
<MarkerComp pos={posix} />
|
||||||
)}
|
)}
|
||||||
</MapContainer>
|
</MapContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Map;
|
export default MapComp;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
const PuntiInteresse: {
|
const PuntiInteresse: {
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,49 @@
|
||||||
import {
|
import {
|
||||||
useState,
|
type DivIconOptions,
|
||||||
useId,
|
DomEvent,
|
||||||
useMemo,
|
divIcon,
|
||||||
type ReactElement,
|
type LeafletEventHandlerFn,
|
||||||
useCallback,
|
type LeafletEventHandlerFnMap,
|
||||||
isValidElement,
|
type Marker as LeafletMarker,
|
||||||
type ComponentType,
|
type PointExpression,
|
||||||
useLayoutEffect,
|
type PointTuple,
|
||||||
forwardRef,
|
} from "leaflet";
|
||||||
useEffect,
|
import {
|
||||||
|
type ComponentType,
|
||||||
|
forwardRef,
|
||||||
|
isValidElement,
|
||||||
|
type ReactElement,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useId,
|
||||||
|
useLayoutEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { isValidElementType } from "react-is";
|
import { isValidElementType } from "react-is";
|
||||||
import {
|
import {
|
||||||
Marker as ReactLeafletMarker,
|
Marker as ReactLeafletMarker,
|
||||||
type MarkerProps as ReactLeafletMarkerProps,
|
type MarkerProps as ReactLeafletMarkerProps,
|
||||||
} from "react-leaflet";
|
} from "react-leaflet";
|
||||||
import {
|
import {
|
||||||
divIcon,
|
createHtmlPortalNode,
|
||||||
DomEvent,
|
InPortal,
|
||||||
type LeafletEventHandlerFn,
|
OutPortal,
|
||||||
type LeafletEventHandlerFnMap,
|
|
||||||
type PointExpression,
|
|
||||||
type DivIconOptions,
|
|
||||||
type Marker as LeafletMarker,
|
|
||||||
type PointTuple,
|
|
||||||
} from "leaflet";
|
|
||||||
import { type SetRequired } from "type-fest";
|
|
||||||
import {
|
|
||||||
createHtmlPortalNode,
|
|
||||||
OutPortal,
|
|
||||||
InPortal,
|
|
||||||
} from "react-reverse-portal";
|
} from "react-reverse-portal";
|
||||||
|
import type { SetRequired } from "type-fest";
|
||||||
|
|
||||||
const getCoordsFromPointExpression = (expression?: PointExpression) => {
|
const getCoordsFromPointExpression = (expression?: PointExpression) => {
|
||||||
if (!expression) return [];
|
if (!expression) return [];
|
||||||
if (Array.isArray(expression)) {
|
if (Array.isArray(expression)) {
|
||||||
return expression;
|
return expression;
|
||||||
} else {
|
} else {
|
||||||
return [expression.x, expression.y] as PointTuple;
|
return [expression.x, expression.y] as PointTuple;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const useCoordsFromPointExpression = (expression?: PointExpression) =>
|
const useCoordsFromPointExpression = (expression?: PointExpression) =>
|
||||||
useMemo(() => getCoordsFromPointExpression(expression), [expression]);
|
useMemo(() => getCoordsFromPointExpression(expression), [expression]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The possible options for the {@link ComponentMarkerOpts.layoutMode | `componentIconOpts.layoutMode`} option.
|
* The possible options for the {@link ComponentMarkerOpts.layoutMode | `componentIconOpts.layoutMode`} option.
|
||||||
|
|
@ -51,272 +51,274 @@ const useCoordsFromPointExpression = (expression?: PointExpression) =>
|
||||||
type ComponentMarkerLayout = "fit-content" | "fit-parent";
|
type ComponentMarkerLayout = "fit-content" | "fit-parent";
|
||||||
|
|
||||||
type ComponentMarkerRootDivIconOpts = Omit<
|
type ComponentMarkerRootDivIconOpts = Omit<
|
||||||
DivIconOptions,
|
DivIconOptions,
|
||||||
| "html"
|
| "html"
|
||||||
| "bgPos"
|
| "bgPos"
|
||||||
| "shadowUrl"
|
| "shadowUrl"
|
||||||
| "shadowSize"
|
| "shadowSize"
|
||||||
| "shadowAnchor"
|
| "shadowAnchor"
|
||||||
| "shadowRetinaUrl"
|
| "shadowRetinaUrl"
|
||||||
| "iconUrl"
|
| "iconUrl"
|
||||||
| "iconRetinaUrl"
|
| "iconRetinaUrl"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
type RootDivOpt<
|
type RootDivOpt<
|
||||||
RequiredKeys extends keyof ComponentMarkerRootDivIconOpts = never,
|
RequiredKeys extends keyof ComponentMarkerRootDivIconOpts = never,
|
||||||
> = {
|
> = {
|
||||||
/**
|
/**
|
||||||
* The {@link DivIconOptions} (except for the `html` property and other properties that are not relevant in the context of a React driven marker) that are to be supplied to the `div` wrapper for the leaflet-managed wrapper of the React icon component.
|
* The {@link DivIconOptions} (except for the `html` property and other properties that are not relevant in the context of a React driven marker) that are to be supplied to the `div` wrapper for the leaflet-managed wrapper of the React icon component.
|
||||||
*
|
*
|
||||||
* When setting {@link ComponentMarkerOpts.layoutMode | `componentIconOpts.layoutMode`} as `'fit-parent'`, it is expected that {@link ComponentMarkerRootDivIconOpts.iconSize | `componentIconOpts.rootDivOpts.iconSize`} is set since this defines the dimensions of the component in this mode.
|
* When setting {@link ComponentMarkerOpts.layoutMode | `componentIconOpts.layoutMode`} as `'fit-parent'`, it is expected that {@link ComponentMarkerRootDivIconOpts.iconSize | `componentIconOpts.rootDivOpts.iconSize`} is set since this defines the dimensions of the component in this mode.
|
||||||
*
|
*
|
||||||
* @see {@link ComponentMarkerRootDivIconOpts}
|
* @see {@link ComponentMarkerRootDivIconOpts}
|
||||||
**/
|
**/
|
||||||
rootDivOpts: SetRequired<ComponentMarkerRootDivIconOpts, RequiredKeys>;
|
rootDivOpts: SetRequired<ComponentMarkerRootDivIconOpts, RequiredKeys>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CoreComponentMarkerOpts = {
|
type CoreComponentMarkerOpts = {
|
||||||
/**
|
/**
|
||||||
* `'fit-content'` disregards the `iconSize` passed to leaflet (defaults to `[0,0]`) and allows the React icon marker to be determined by the size of the provided component itself (which could be dynamic). Automatic alignment compensation is
|
* `'fit-content'` disregards the `iconSize` passed to leaflet (defaults to `[0,0]`) and allows the React icon marker to be determined by the size of the provided component itself (which could be dynamic). Automatic alignment compensation is
|
||||||
* added to ensure the icon component stays centred on the X axis with the marker.
|
* added to ensure the icon component stays centred on the X axis with the marker.
|
||||||
*
|
*
|
||||||
* `'fit-parent'` will set the container of the component to be the same size as the `iconSize`. Typically, this is used alongside a static icon size that is passed via {@link ComponentMarkerRootDivIconOpts.iconSize | `componentIconOpts.rootDivOpts.iconSize`}. This setup may allow for more granular control over positioning and anchor configuration. The user supplied Icon component itself should use a width and height of `100%` to fill the container.
|
* `'fit-parent'` will set the container of the component to be the same size as the `iconSize`. Typically, this is used alongside a static icon size that is passed via {@link ComponentMarkerRootDivIconOpts.iconSize | `componentIconOpts.rootDivOpts.iconSize`}. This setup may allow for more granular control over positioning and anchor configuration. The user supplied Icon component itself should use a width and height of `100%` to fill the container.
|
||||||
*
|
*
|
||||||
* @defaultValue `"fit-content"`
|
* @defaultValue `"fit-content"`
|
||||||
* @see {@link ComponentMarkerLayout}
|
* @see {@link ComponentMarkerLayout}
|
||||||
*/
|
*/
|
||||||
layoutMode?: ComponentMarkerLayout;
|
layoutMode?: ComponentMarkerLayout;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If set to `true`, panning/scrolling the map will not be possible "through" the component marker.
|
* If set to `true`, panning/scrolling the map will not be possible "through" the component marker.
|
||||||
*
|
*
|
||||||
* This applies to the entire component marker.
|
* This applies to the entire component marker.
|
||||||
*
|
*
|
||||||
* @defaultValue `false`
|
* @defaultValue `false`
|
||||||
*/
|
*/
|
||||||
disableScrollPropagation?: boolean;
|
disableScrollPropagation?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If set to `true`, clicking on the component marker will not be captured by the underlying map.
|
* If set to `true`, clicking on the component marker will not be captured by the underlying map.
|
||||||
*
|
*
|
||||||
* This applies to the entire component marker. Note this will also disable the ability to activate native react-leaflet
|
* This applies to the entire component marker. Note this will also disable the ability to activate native react-leaflet
|
||||||
* popups via clicking.
|
* popups via clicking.
|
||||||
*
|
*
|
||||||
* @defaultValue `false`
|
* @defaultValue `false`
|
||||||
*/
|
*/
|
||||||
disableClickPropagation?: boolean;
|
disableClickPropagation?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable or disable the console warning about the case where the {@link BaseMarkerProps.componentIconOpts | `componentIconOpts`} prop was set but the {@link BaseMarkerProps.icon | `icon`} prop
|
* Enable or disable the console warning about the case where the {@link BaseMarkerProps.componentIconOpts | `componentIconOpts`} prop was set but the {@link BaseMarkerProps.icon | `icon`} prop
|
||||||
* is not a component. This would mean those options are unused.
|
* is not a component. This would mean those options are unused.
|
||||||
*
|
*
|
||||||
* @defaultValue `true`
|
* @defaultValue `true`
|
||||||
* @see {@link BaseMarkerProps.componentIconOpts}
|
* @see {@link BaseMarkerProps.componentIconOpts}
|
||||||
*/
|
*/
|
||||||
unusedOptsWarning?: boolean;
|
unusedOptsWarning?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable or disable the console warning about the case where {@link ComponentMarkerOpts.layoutMode | `componentIconOpts.layoutMode`} was set to `fit-parent` but the {@link ComponentMarkerRootDivIconOpts.iconSize | `componentIconOpts.rootDivOpts.iconSize`}
|
* Enable or disable the console warning about the case where {@link ComponentMarkerOpts.layoutMode | `componentIconOpts.layoutMode`} was set to `fit-parent` but the {@link ComponentMarkerRootDivIconOpts.iconSize | `componentIconOpts.rootDivOpts.iconSize`}
|
||||||
* has not been set. This would mean the size of the React component icon would not be visible.
|
* has not been set. This would mean the size of the React component icon would not be visible.
|
||||||
*
|
*
|
||||||
* @defaultValue `true`
|
* @defaultValue `true`
|
||||||
* @see {@link ComponentMarkerOpts.rootDivOpts}
|
* @see {@link ComponentMarkerOpts.rootDivOpts}
|
||||||
* @see {@link ComponentMarkerRootDivIconOpts.iconSize}
|
* @see {@link ComponentMarkerRootDivIconOpts.iconSize}
|
||||||
*/
|
*/
|
||||||
rootSizeWarning?: boolean;
|
rootSizeWarning?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type UnionKeys<T> = T extends T ? keyof T : never;
|
type UnionKeys<T> = T extends T ? keyof T : never;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: <intended>
|
||||||
type StrictUnionHelper<T, TAll> = T extends any
|
type StrictUnionHelper<T, TAll> = T extends any
|
||||||
? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, undefined>>
|
? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, undefined>>
|
||||||
: never;
|
: never;
|
||||||
type StrictUnion<T> = StrictUnionHelper<T, T>;
|
type StrictUnion<T> = StrictUnionHelper<T, T>;
|
||||||
|
|
||||||
type ComponentMarkerOpts = StrictUnion<
|
type ComponentMarkerOpts = StrictUnion<
|
||||||
| (CoreComponentMarkerOpts & {
|
| (CoreComponentMarkerOpts & {
|
||||||
layoutMode?: "fit-content";
|
layoutMode?: "fit-content";
|
||||||
} & Partial<RootDivOpt>)
|
} & Partial<RootDivOpt>)
|
||||||
| (CoreComponentMarkerOpts & {
|
| (CoreComponentMarkerOpts & {
|
||||||
layoutMode: "fit-parent";
|
layoutMode: "fit-parent";
|
||||||
} & RootDivOpt<"iconSize">)
|
} & RootDivOpt<"iconSize">)
|
||||||
>;
|
>;
|
||||||
|
|
||||||
type BaseMarkerProps<AdditionalIconTypes = never> = Omit<
|
type BaseMarkerProps<AdditionalIconTypes = never> = Omit<
|
||||||
ReactLeafletMarkerProps,
|
ReactLeafletMarkerProps,
|
||||||
"icon"
|
"icon"
|
||||||
> & {
|
> & {
|
||||||
/** A {@link ReactElement} representing the Markers icon, or any type from [react-leaflet Marker](https://react-leaflet.js.org/docs/api-components/#marker) component. */
|
/** A {@link ReactElement} representing the Markers icon, or any type from [react-leaflet Marker](https://react-leaflet.js.org/docs/api-components/#marker) component. */
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
icon: ReactElement<any> | AdditionalIconTypes;
|
|
||||||
|
|
||||||
/**
|
// biome-ignore lint/suspicious/noExplicitAny: <intended>
|
||||||
* The {@link ComponentMarkerOpts}. These will not be effective if {@link BaseMarkerProps.icon | `icon`} is not set to a React Element or Component, and a warning will be given in the console.
|
icon: ReactElement<any> | AdditionalIconTypes;
|
||||||
*
|
|
||||||
* @see {@link ComponentMarkerOpts.unusedOptsWarning}
|
/**
|
||||||
*/
|
* The {@link ComponentMarkerOpts}. These will not be effective if {@link BaseMarkerProps.icon | `icon`} is not set to a React Element or Component, and a warning will be given in the console.
|
||||||
componentIconOpts?: ComponentMarkerOpts;
|
*
|
||||||
|
* @see {@link ComponentMarkerOpts.unusedOptsWarning}
|
||||||
|
*/
|
||||||
|
componentIconOpts?: ComponentMarkerOpts;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MarkerProps = BaseMarkerProps<
|
type MarkerProps = BaseMarkerProps<
|
||||||
ReactLeafletMarkerProps["icon"] | ComponentType
|
ReactLeafletMarkerProps["icon"] | ComponentType
|
||||||
>;
|
>;
|
||||||
|
|
||||||
const DEFAULT_ICON_SIZE: PointExpression = [0, 0];
|
const DEFAULT_ICON_SIZE: PointExpression = [0, 0];
|
||||||
const ComponentMarker = forwardRef<LeafletMarker, BaseMarkerProps>(
|
const ComponentMarker = forwardRef<LeafletMarker, BaseMarkerProps>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
eventHandlers: providedEventHandlers,
|
eventHandlers: providedEventHandlers,
|
||||||
icon: providedIcon,
|
icon: providedIcon,
|
||||||
|
|
||||||
componentIconOpts: {
|
componentIconOpts: {
|
||||||
layoutMode = "fit-content",
|
layoutMode = "fit-content",
|
||||||
disableClickPropagation,
|
disableClickPropagation,
|
||||||
disableScrollPropagation,
|
disableScrollPropagation,
|
||||||
rootDivOpts,
|
rootDivOpts,
|
||||||
rootSizeWarning,
|
rootSizeWarning,
|
||||||
} = {},
|
} = {},
|
||||||
...otherProps
|
...otherProps
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const [markerRendered, setMarkerRendered] = useState(false);
|
const [markerRendered, setMarkerRendered] = useState(false);
|
||||||
const [, setChangeCount] = useState(0);
|
const [, setChangeCount] = useState(0);
|
||||||
const id = "marker-" + useId();
|
const id = `marker-${useId()}`;
|
||||||
|
|
||||||
const portalNode = useMemo(
|
const portalNode = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createHtmlPortalNode({
|
createHtmlPortalNode({
|
||||||
attributes: {
|
attributes: {
|
||||||
"data-react-component-marker": "portal-parent",
|
"data-react-component-marker": "portal-parent",
|
||||||
style: "width:100%;height:100%;",
|
style: "width:100%;height:100%;",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
rootSizeWarning !== false &&
|
rootSizeWarning !== false &&
|
||||||
layoutMode === "fit-parent" &&
|
layoutMode === "fit-parent" &&
|
||||||
rootDivOpts?.iconSize === undefined
|
rootDivOpts?.iconSize === undefined
|
||||||
) {
|
) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`UNBOUND_FIT_PARENT
|
`UNBOUND_FIT_PARENT
|
||||||
The 'componentIconOpts.rootDivOpts.iconSize' option was not set but 'componentIconOpts.layoutMode' was set to 'fit-parent'. This means your React component will not be properly bound by the parent.
|
The 'componentIconOpts.rootDivOpts.iconSize' option was not set but 'componentIconOpts.layoutMode' was set to 'fit-parent'. This means your React component will not be properly bound by the parent.
|
||||||
|
|
||||||
To disable this warning set 'componentIconOpts.rootSizeWarning' to false.`,
|
To disable this warning set 'componentIconOpts.rootSizeWarning' to false.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [layoutMode, rootSizeWarning, rootDivOpts?.iconSize]);
|
}, [layoutMode, rootSizeWarning, rootDivOpts?.iconSize]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
attribution,
|
attribution,
|
||||||
className,
|
className,
|
||||||
iconAnchor,
|
iconAnchor,
|
||||||
iconSize = DEFAULT_ICON_SIZE,
|
iconSize = DEFAULT_ICON_SIZE,
|
||||||
pane,
|
pane,
|
||||||
popupAnchor,
|
popupAnchor,
|
||||||
tooltipAnchor,
|
tooltipAnchor,
|
||||||
} = rootDivOpts ?? {};
|
} = rootDivOpts ?? {};
|
||||||
|
|
||||||
const iconDeps = [
|
const iconDeps = [
|
||||||
id,
|
id,
|
||||||
layoutMode,
|
layoutMode,
|
||||||
attribution,
|
attribution,
|
||||||
className,
|
className,
|
||||||
pane,
|
pane,
|
||||||
Boolean(disableClickPropagation),
|
Boolean(disableClickPropagation),
|
||||||
Boolean(disableScrollPropagation),
|
Boolean(disableScrollPropagation),
|
||||||
...useCoordsFromPointExpression(iconSize),
|
...useCoordsFromPointExpression(iconSize),
|
||||||
...useCoordsFromPointExpression(iconAnchor),
|
...useCoordsFromPointExpression(iconAnchor),
|
||||||
...useCoordsFromPointExpression(popupAnchor),
|
...useCoordsFromPointExpression(popupAnchor),
|
||||||
...useCoordsFromPointExpression(tooltipAnchor),
|
...useCoordsFromPointExpression(tooltipAnchor),
|
||||||
];
|
];
|
||||||
|
|
||||||
const icon = useMemo(() => {
|
const icon = useMemo(() => {
|
||||||
const parentStyles =
|
const parentStyles =
|
||||||
layoutMode === "fit-content"
|
layoutMode === "fit-content"
|
||||||
? "width: min-content; transform: translate(-50%, -50%)"
|
? "width: min-content; transform: translate(-50%, -50%)"
|
||||||
: "width: 100%; height: 100%";
|
: "width: 100%; height: 100%";
|
||||||
|
|
||||||
return divIcon({
|
return divIcon({
|
||||||
html: `<div data-react-component-marker="root" style="${parentStyles}" id="${id}"></div>`,
|
html: `<div data-react-component-marker="root" style="${parentStyles}" id="${id}"></div>`,
|
||||||
...(iconSize ? { iconSize } : []),
|
...(iconSize ? { iconSize } : []),
|
||||||
...(iconAnchor ? { iconAnchor } : []),
|
...(iconAnchor ? { iconAnchor } : []),
|
||||||
...(popupAnchor ? { popupAnchor } : []),
|
...(popupAnchor ? { popupAnchor } : []),
|
||||||
...(tooltipAnchor ? { tooltipAnchor } : []),
|
...(tooltipAnchor ? { tooltipAnchor } : []),
|
||||||
pane,
|
pane,
|
||||||
attribution,
|
attribution,
|
||||||
className,
|
className,
|
||||||
});
|
});
|
||||||
}, iconDeps);
|
}, iconDeps);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
setChangeCount((prev) => prev + 1);
|
setChangeCount((prev) => prev + 1);
|
||||||
}, [icon]);
|
}, [icon]);
|
||||||
|
|
||||||
const handleAddEvent = useCallback<LeafletEventHandlerFn>(
|
const handleAddEvent = useCallback<LeafletEventHandlerFn>(
|
||||||
(...args) => {
|
(...args) => {
|
||||||
setMarkerRendered(true);
|
setMarkerRendered(true);
|
||||||
if (providedEventHandlers?.add) providedEventHandlers.add(...args);
|
if (providedEventHandlers?.add) providedEventHandlers.add(...args);
|
||||||
},
|
},
|
||||||
[providedEventHandlers?.add],
|
[providedEventHandlers?.add],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleRemoveEvent = useCallback<LeafletEventHandlerFn>(
|
const handleRemoveEvent = useCallback<LeafletEventHandlerFn>(
|
||||||
(...args) => {
|
(...args) => {
|
||||||
setMarkerRendered(false);
|
setMarkerRendered(false);
|
||||||
if (providedEventHandlers?.remove)
|
if (providedEventHandlers?.remove)
|
||||||
providedEventHandlers.remove(...args);
|
providedEventHandlers.remove(...args);
|
||||||
},
|
},
|
||||||
[providedEventHandlers?.remove],
|
[providedEventHandlers?.remove],
|
||||||
);
|
);
|
||||||
|
|
||||||
const eventHandlers = useMemo<LeafletEventHandlerFnMap>(
|
const eventHandlers = useMemo<LeafletEventHandlerFnMap>(
|
||||||
() => ({
|
() => ({
|
||||||
...providedEventHandlers,
|
...providedEventHandlers,
|
||||||
add: handleAddEvent,
|
add: handleAddEvent,
|
||||||
remove: handleRemoveEvent,
|
remove: handleRemoveEvent,
|
||||||
}),
|
}),
|
||||||
[providedEventHandlers, handleAddEvent, handleRemoveEvent],
|
[providedEventHandlers, handleAddEvent, handleRemoveEvent],
|
||||||
);
|
);
|
||||||
|
|
||||||
let portalTarget: null | HTMLElement = null;
|
let portalTarget: null | HTMLElement = null;
|
||||||
if (markerRendered) {
|
if (markerRendered) {
|
||||||
portalTarget = document.getElementById(id);
|
portalTarget = document.getElementById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!portalTarget) return;
|
if (!portalTarget) return;
|
||||||
|
|
||||||
if (disableClickPropagation) {
|
if (disableClickPropagation) {
|
||||||
DomEvent.disableClickPropagation(portalTarget);
|
DomEvent.disableClickPropagation(portalTarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (disableScrollPropagation) {
|
if (disableScrollPropagation) {
|
||||||
DomEvent.disableScrollPropagation(portalTarget);
|
DomEvent.disableScrollPropagation(portalTarget);
|
||||||
}
|
}
|
||||||
}, [portalTarget, disableClickPropagation, disableScrollPropagation]);
|
}, [portalTarget, disableClickPropagation, disableScrollPropagation]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ReactLeafletMarker
|
<ReactLeafletMarker
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
eventHandlers={eventHandlers}
|
eventHandlers={eventHandlers}
|
||||||
icon={icon}
|
icon={icon}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{markerRendered && portalTarget !== null && (
|
{markerRendered && portalTarget !== null && (
|
||||||
<>
|
<>
|
||||||
<InPortal node={portalNode}>{providedIcon}</InPortal>
|
<InPortal node={portalNode}>{providedIcon}</InPortal>
|
||||||
{createPortal(<OutPortal node={portalNode} />, portalTarget)}
|
{createPortal(<OutPortal node={portalNode} />, portalTarget)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
ComponentMarker.displayName = "ComponentMarker";
|
ComponentMarker.displayName = "ComponentMarker";
|
||||||
|
|
@ -342,59 +344,59 @@ ComponentMarker.displayName = "ComponentMarker";
|
||||||
* ```
|
* ```
|
||||||
**/
|
**/
|
||||||
export const Marker = forwardRef<LeafletMarker, MarkerProps>(
|
export const Marker = forwardRef<LeafletMarker, MarkerProps>(
|
||||||
({ icon: Icon, componentIconOpts, ...otherProps }, ref) => {
|
({ icon: Icon, componentIconOpts, ...otherProps }, ref) => {
|
||||||
const validElement = isValidElement(Icon);
|
const validElement = isValidElement(Icon);
|
||||||
const validComponent = isValidElementType(Icon);
|
const validComponent = isValidElementType(Icon);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
!validElement &&
|
!validElement &&
|
||||||
!validComponent &&
|
!validComponent &&
|
||||||
componentIconOpts !== undefined &&
|
componentIconOpts !== undefined &&
|
||||||
componentIconOpts.unusedOptsWarning !== false
|
componentIconOpts.unusedOptsWarning !== false
|
||||||
) {
|
) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`UNUSED_OPTIONS The 'componentIconOpts' prop was set but the 'icon' prop was not set to a React component or element. These options will be unused.
|
`UNUSED_OPTIONS The 'componentIconOpts' prop was set but the 'icon' prop was not set to a React component or element. These options will be unused.
|
||||||
To disable this warning set 'componentIconOpts.unusedOptsWarning' to false.`,
|
To disable this warning set 'componentIconOpts.unusedOptsWarning' to false.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
componentIconOpts,
|
componentIconOpts,
|
||||||
componentIconOpts?.unusedOptsWarning,
|
componentIconOpts?.unusedOptsWarning,
|
||||||
validElement,
|
validElement,
|
||||||
validComponent,
|
validComponent,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (validElement) {
|
if (validElement) {
|
||||||
return (
|
return (
|
||||||
<ComponentMarker
|
<ComponentMarker
|
||||||
ref={ref}
|
ref={ref}
|
||||||
icon={Icon}
|
icon={Icon}
|
||||||
componentIconOpts={componentIconOpts}
|
componentIconOpts={componentIconOpts}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validComponent) {
|
if (validComponent) {
|
||||||
return (
|
return (
|
||||||
<ComponentMarker
|
<ComponentMarker
|
||||||
ref={ref}
|
ref={ref}
|
||||||
icon={<Icon />}
|
icon={<Icon />}
|
||||||
componentIconOpts={componentIconOpts}
|
componentIconOpts={componentIconOpts}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactLeafletMarker
|
<ReactLeafletMarker
|
||||||
ref={ref}
|
ref={ref}
|
||||||
icon={Icon as ReactLeafletMarkerProps["icon"]}
|
icon={Icon as ReactLeafletMarkerProps["icon"]}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
Marker.displayName = "Marker";
|
Marker.displayName = "Marker";
|
||||||
|
|
|
||||||
|
|
@ -1,99 +1,99 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
interface MapMarkerProps {
|
interface MapMarkerProps {
|
||||||
label: ReactNode;
|
label: ReactNode;
|
||||||
color?: "primary" | "red" | "blue" | "green" | "yellow" | "purple" | "gray";
|
color?: "primary" | "red" | "blue" | "green" | "yellow" | "purple" | "gray";
|
||||||
className?: string;
|
className?: string;
|
||||||
size?: "sm" | "md" | "lg";
|
size?: "sm" | "md" | "lg";
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MapMarker({
|
export default function MapMarker({
|
||||||
label,
|
label,
|
||||||
color = "primary",
|
color = "primary",
|
||||||
className,
|
className,
|
||||||
size = "md",
|
size = "md",
|
||||||
}: MapMarkerProps) {
|
}: MapMarkerProps) {
|
||||||
const colorClasses = {
|
const colorClasses = {
|
||||||
primary: {
|
primary: {
|
||||||
rectangle: "bg-[#242428] text-white",
|
rectangle: "bg-[#242428] text-white",
|
||||||
triangle: "border-t-[#242428]",
|
triangle: "border-t-[#242428]",
|
||||||
},
|
},
|
||||||
red: {
|
red: {
|
||||||
rectangle: "bg-red-500 text-white",
|
rectangle: "bg-red-500 text-white",
|
||||||
triangle: "border-t-red-500",
|
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: {
|
green: {
|
||||||
rectangle: "bg-green-500 text-white",
|
rectangle: "bg-green-500 text-white",
|
||||||
triangle: "border-t-green-500",
|
triangle: "border-t-green-500",
|
||||||
},
|
},
|
||||||
yellow: {
|
yellow: {
|
||||||
rectangle: "bg-yellow-500 text-black",
|
rectangle: "bg-yellow-500 text-black",
|
||||||
triangle: "border-t-yellow-500",
|
triangle: "border-t-yellow-500",
|
||||||
},
|
},
|
||||||
purple: {
|
purple: {
|
||||||
rectangle: "bg-purple-500 text-white",
|
rectangle: "bg-purple-500 text-white",
|
||||||
triangle: "border-t-purple-500",
|
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",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
sm: {
|
sm: {
|
||||||
container: "h-8",
|
container: "h-8",
|
||||||
rectangle: "px-2 py-1 rounded-md",
|
rectangle: "px-2 py-1 rounded-md",
|
||||||
triangle: "border-l-[8px] border-r-[8px] border-t-[8px]",
|
triangle: "border-l-[8px] border-r-[8px] border-t-[8px]",
|
||||||
text: "text-xs",
|
text: "text-xs",
|
||||||
},
|
},
|
||||||
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]",
|
triangle: "border-l-[10px] border-r-[10px] border-t-[10px]",
|
||||||
text: "text-sm",
|
text: "text-sm",
|
||||||
},
|
},
|
||||||
lg: {
|
lg: {
|
||||||
container: "h-16",
|
container: "h-16",
|
||||||
rectangle: "px-4 py-3 rounded-xl",
|
rectangle: "px-4 py-3 rounded-xl",
|
||||||
triangle: "border-l-[12px] border-r-[12px] border-t-[12px]",
|
triangle: "border-l-[12px] border-r-[12px] border-t-[12px]",
|
||||||
text: "text-base",
|
text: "text-base",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex flex-col items-center",
|
"inline-flex flex-col items-center",
|
||||||
sizeClasses[size].container,
|
sizeClasses[size].container,
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Rectangle part */}
|
{/* Rectangle part */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center justify-center font-medium text-primary-foreground shadow-md",
|
"flex items-center justify-center font-medium text-primary-foreground shadow-md",
|
||||||
sizeClasses[size].rectangle,
|
sizeClasses[size].rectangle,
|
||||||
colorClasses[color].rectangle,
|
colorClasses[color].rectangle,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className={sizeClasses[size].text}>{label}</span>
|
<span className={sizeClasses[size].text}>{label}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Triangle part */}
|
{/* Triangle part */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-0 w-0 border-solid border-transparent",
|
"h-0 w-0 border-solid border-transparent",
|
||||||
sizeClasses[size].triangle,
|
sizeClasses[size].triangle,
|
||||||
colorClasses[color].triangle,
|
colorClasses[color].triangle,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,44 @@
|
||||||
import { useState } from "react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { setCookie } from "cookies-next/client";
|
import { setCookie } from "cookies-next/client";
|
||||||
import { EngFlag, ItaFlag } from "~/components/svgs";
|
|
||||||
import LoadingButton from "~/components/custom_ui/loading-button";
|
|
||||||
import { add } from "date-fns";
|
import { add } from "date-fns";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useState } from "react";
|
||||||
|
import LoadingButton from "~/components/custom_ui/loading-button";
|
||||||
|
import { EngFlag, ItaFlag } from "~/components/svgs";
|
||||||
|
|
||||||
export const LangToggle = () => {
|
export const LangToggle = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const locale = router.locale || "it";
|
const locale = router.locale || "it";
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
|
|
||||||
const handleLangChange = async (lang: string) => {
|
const handleLangChange = async (lang: string) => {
|
||||||
if (lang === locale) return;
|
if (lang === locale) return;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setCookie("NEXT_LOCALE", lang, {
|
setCookie("NEXT_LOCALE", lang, {
|
||||||
expires: add(new Date(), {
|
expires: add(new Date(), {
|
||||||
days: 30,
|
days: 30,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
await router.push(router.pathname, router.asPath, { locale: lang });
|
await router.push(router.pathname, router.asPath, { locale: lang });
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
aria-label="Change Language"
|
aria-label="Change Language"
|
||||||
loading={busy}
|
loading={busy}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="text-primary flex w-fit items-center gap-2"
|
className="text-primary flex w-fit items-center gap-2"
|
||||||
onClick={() => handleLangChange(locale === "it" ? "en" : "it")}
|
onClick={() => handleLangChange(locale === "it" ? "en" : "it")}
|
||||||
>
|
>
|
||||||
{locale === "it" ? (
|
{locale === "it" ? (
|
||||||
<>
|
<>
|
||||||
<ItaFlag className="size-5" />
|
<ItaFlag className="size-5" />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<EngFlag className="size-5" />
|
<EngFlag className="size-5" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,200 +1,200 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
import { LogIn, LogOut } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { IconMatrix, type IconType } from "~/components/IconComponents";
|
||||||
|
import { LoadingSpinner } from "~/components/spinner";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "~/components/ui/dropdown-menu";
|
||||||
|
import { UserAvatar } from "~/components/user_avatar";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
import { useSession } from "~/providers/SessionProvider";
|
import { useSession } from "~/providers/SessionProvider";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "~/components/ui/dropdown-menu";
|
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { LogIn, LogOut } from "lucide-react";
|
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
import { IconMatrix, type IconType } from "~/components/IconComponents";
|
|
||||||
import { usePathname } from "next/navigation";
|
|
||||||
import { UserAvatar } from "~/components/user_avatar";
|
|
||||||
import { LoadingSpinner } from "~/components/spinner";
|
|
||||||
|
|
||||||
type LoginButtonProps = {
|
type LoginButtonProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: (status: boolean) => void;
|
setOpen: (status: boolean) => void;
|
||||||
setSidebar: (status: boolean) => void;
|
setSidebar: (status: boolean) => void;
|
||||||
inSidebar?: boolean;
|
inSidebar?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserHeaderSection = ({
|
export const UserHeaderSection = ({
|
||||||
open,
|
open,
|
||||||
setOpen,
|
setOpen,
|
||||||
setSidebar,
|
setSidebar,
|
||||||
inSidebar,
|
inSidebar,
|
||||||
}: LoginButtonProps) => {
|
}: LoginButtonProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const path = usePathname();
|
const path = usePathname();
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { mutate: logout } = api.auth.logoutUser.useMutation({
|
const { mutate: logout } = api.auth.logoutUser.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
if (path.startsWith("/area-riservata")) {
|
if (path.startsWith("/area-riservata")) {
|
||||||
await router.push("/");
|
await router.push("/");
|
||||||
} else {
|
} else {
|
||||||
router.reload();
|
router.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
await utils.auth.getSession.invalidate();
|
await utils.auth.getSession.invalidate();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (session.status === "pending") {
|
if (session.status === "pending") {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center px-3 py-2">
|
<div className="flex items-center px-3 py-2">
|
||||||
<LoadingSpinner className="size-5" />
|
<LoadingSpinner className="size-5" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{!session || !session.user ? (
|
{!session || !session.user ? (
|
||||||
<Button
|
<Button
|
||||||
aria-label="Login Button"
|
aria-label="Login Button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await router.push({
|
await router.push({
|
||||||
pathname: "/login",
|
pathname: "/login",
|
||||||
query: { redirect: router.asPath },
|
query: { redirect: router.asPath },
|
||||||
});
|
});
|
||||||
setSidebar(false);
|
setSidebar(false);
|
||||||
}}
|
}}
|
||||||
className="flex h-[46px] w-full items-center justify-center gap-x-1 rounded-lg p-2"
|
className="flex h-[46px] w-full items-center justify-center gap-x-1 rounded-lg p-2"
|
||||||
>
|
>
|
||||||
{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 open={open} onOpenChange={setOpen}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
aria-label="User Profile Button"
|
aria-label="User Profile Button"
|
||||||
variant="ghost"
|
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"
|
||||||
>
|
>
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
username={session.user.username}
|
username={session.user.username}
|
||||||
userId={session.user.id}
|
userId={session.user.id}
|
||||||
className="size-10"
|
className="size-10"
|
||||||
/>
|
/>
|
||||||
{inSidebar ? (
|
{inSidebar ? (
|
||||||
<span>{session.user.username}</span>
|
<span>{session.user.username}</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="navqry:block hidden">
|
<span className="navqry:block hidden">
|
||||||
{session.user.username}
|
{session.user.username}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-56" align="end" forceMount>
|
<DropdownMenuContent className="w-56" align="end" forceMount>
|
||||||
{session.user.isAdmin ? (
|
{session.user.isAdmin ? (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuLabel className="font-normal">
|
<DropdownMenuLabel className="font-normal">
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-sm leading-none font-medium">
|
<p className="text-sm leading-none font-medium">
|
||||||
{session.user.username}
|
{session.user.username}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-muted-foreground text-xs leading-none">
|
<p className="text-muted-foreground text-xs leading-none">
|
||||||
Amministratore
|
Amministratore
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
<Items items={t.nav.loginMenuAdmin} setOpen={setOpen} />
|
<Items items={t.nav.loginMenuAdmin} setOpen={setOpen} />
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="flex cursor-pointer items-center gap-3 px-2 py-1.5"
|
className="flex cursor-pointer items-center gap-3 px-2 py-1.5"
|
||||||
aria-label="Logout Button"
|
aria-label="Logout Button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
logout();
|
logout();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LogOut className="size-6" />
|
<LogOut className="size-6" />
|
||||||
{t.auth.login.logout}
|
{t.auth.login.logout}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuLabel className="font-normal">
|
<DropdownMenuLabel className="font-normal">
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<p className="text-sm leading-none font-medium">
|
<p className="text-sm leading-none font-medium">
|
||||||
{session.user.username}
|
{session.user.username}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-muted-foreground text-xs leading-none">
|
<p className="text-muted-foreground text-xs leading-none">
|
||||||
{session.user.email}
|
{session.user.email}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<Items items={t.nav.loginMenuUser} setOpen={setOpen} />
|
<Items items={t.nav.loginMenuUser} setOpen={setOpen} />
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
aria-label="Logout Button"
|
aria-label="Logout Button"
|
||||||
className="flex cursor-pointer items-center gap-3 px-2 py-1.5"
|
className="flex cursor-pointer items-center gap-3 px-2 py-1.5"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
logout();
|
logout();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LogOut className="size-6" />
|
<LogOut className="size-6" />
|
||||||
{t.auth.login.logout}
|
{t.auth.login.logout}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Items = ({
|
const Items = ({
|
||||||
items,
|
items,
|
||||||
setOpen,
|
setOpen,
|
||||||
}: {
|
}: {
|
||||||
items: {
|
items: {
|
||||||
title: string;
|
title: string;
|
||||||
icon: { icon: IconType; className: string };
|
icon: { icon: IconType; className: string };
|
||||||
href: string;
|
href: string;
|
||||||
}[];
|
}[];
|
||||||
setOpen: (status: boolean) => void;
|
setOpen: (status: boolean) => void;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
{items?.map((item, idx) => (
|
{items?.map((item, idx) => (
|
||||||
<DropdownMenuItem key={idx} className="p-0">
|
<DropdownMenuItem key={idx} className="p-0">
|
||||||
<Link
|
<Link
|
||||||
aria-label={`Link to ${item.title}`}
|
aria-label={`Link to ${item.title}`}
|
||||||
onClick={() => setOpen(false)}
|
onClick={() => setOpen(false)}
|
||||||
href={item.href}
|
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"
|
||||||
>
|
>
|
||||||
<IconMatrix
|
<IconMatrix
|
||||||
type={item.icon.icon}
|
type={item.icon.icon}
|
||||||
className={cn("size-6", item.icon.className)}
|
className={cn("size-6", item.icon.className)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{item.title}
|
{item.title}
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
))}
|
))}
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,40 @@
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
import { LogoSvg } from "~/components/svgs";
|
import { LogoSvg } from "~/components/svgs";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
|
|
||||||
type MainNavProps = {
|
type MainNavProps = {
|
||||||
pathname: string;
|
pathname: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function MainNav({ pathname }: MainNavProps) {
|
export function MainNav({ pathname }: MainNavProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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="/"
|
href="/"
|
||||||
className="flex shrink-0 items-center font-medium md:mb-0"
|
className="flex shrink-0 items-center font-medium md:mb-0"
|
||||||
>
|
>
|
||||||
<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>
|
||||||
|
|
||||||
<nav className="flex items-center gap-4 text-lg tracking-wide lg:gap-6">
|
<nav className="flex items-center gap-4 text-lg tracking-wide lg:gap-6">
|
||||||
{t.nav.mainNav.map((item) => (
|
{t.nav.mainNav.map((item) => (
|
||||||
<Link
|
<Link
|
||||||
aria-label={item.title}
|
aria-label={item.title}
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={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",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,362 +1,361 @@
|
||||||
import type { ReactNode } from "react";
|
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import Link, { type LinkProps } from "next/link";
|
|
||||||
import {
|
|
||||||
Sheet,
|
|
||||||
SheetContent,
|
|
||||||
SheetDescription,
|
|
||||||
SheetHeader,
|
|
||||||
SheetTitle,
|
|
||||||
SheetTrigger,
|
|
||||||
} from "~/components/ui/sheet";
|
|
||||||
import { AlignLeft, Home } from "lucide-react";
|
import { AlignLeft, Home } from "lucide-react";
|
||||||
|
import Link, { type LinkProps } from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useSession } from "~/providers/SessionProvider";
|
import type { ReactNode } from "react";
|
||||||
import { IconMatrix } from "~/components/IconComponents";
|
import { IconMatrix } from "~/components/IconComponents";
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
import { LangToggle } from "~/components/navbar/lang-toggle";
|
||||||
import { Separator } from "~/components/ui/separator";
|
|
||||||
import { UserHeaderSection } from "~/components/navbar/login-button";
|
import { UserHeaderSection } from "~/components/navbar/login-button";
|
||||||
import { ASvg, LogoSvg } from "~/components/svgs";
|
import { ASvg, LogoSvg } from "~/components/svgs";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { Separator } from "~/components/ui/separator";
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetDescription,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger,
|
||||||
|
} from "~/components/ui/sheet";
|
||||||
import { useMediaQuery } from "~/hooks/use-media-query";
|
import { useMediaQuery } from "~/hooks/use-media-query";
|
||||||
import { LangToggle } from "~/components/navbar/lang-toggle";
|
import { cn } from "~/lib/utils";
|
||||||
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
|
import { useSession } from "~/providers/SessionProvider";
|
||||||
|
|
||||||
type MobileNavProps = {
|
type MobileNavProps = {
|
||||||
pathname: string;
|
pathname: string;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: (status: boolean) => void;
|
setOpen: (status: boolean) => void;
|
||||||
userOpen: boolean;
|
userOpen: boolean;
|
||||||
setUserOpen: (status: boolean) => void;
|
setUserOpen: (status: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function MobileNav({
|
export function MobileNav({
|
||||||
pathname,
|
pathname,
|
||||||
open,
|
open,
|
||||||
setOpen,
|
setOpen,
|
||||||
userOpen,
|
userOpen,
|
||||||
setUserOpen,
|
setUserOpen,
|
||||||
}: MobileNavProps) {
|
}: MobileNavProps) {
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const verySmallScreen = useMediaQuery("(max-width: 320px)");
|
const verySmallScreen = useMediaQuery("(max-width: 320px)");
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Sheet open={open} onOpenChange={setOpen}>
|
<Sheet open={open} onOpenChange={setOpen}>
|
||||||
<SheetTrigger asChild>
|
<SheetTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
aria-label="Toggle Mobile Menu"
|
aria-label="Toggle Mobile Menu"
|
||||||
variant="ghost"
|
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"
|
||||||
>
|
>
|
||||||
<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 side="left" className="z-500 p-0">
|
||||||
<SheetHeader>
|
<SheetHeader>
|
||||||
<SheetTitle className="sr-only">Mobile Menu</SheetTitle>
|
<SheetTitle className="sr-only">Mobile Menu</SheetTitle>
|
||||||
<SheetDescription className="sr-only">
|
<SheetDescription className="sr-only">
|
||||||
This is the mobile menu
|
This is the mobile menu
|
||||||
</SheetDescription>
|
</SheetDescription>
|
||||||
</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="/"
|
href="/"
|
||||||
aria-label="mainNavLinkHome"
|
aria-label="mainNavLinkHome"
|
||||||
className="ml-2 flex items-center gap-3"
|
className="ml-2 flex items-center gap-3"
|
||||||
onOpenChange={setOpen}
|
onOpenChange={setOpen}
|
||||||
>
|
>
|
||||||
<Home className="size-6" />{" "}
|
<Home className="size-6" />{" "}
|
||||||
<span className="text-xl font-medium">Home</span>
|
<span className="text-xl font-medium">Home</span>
|
||||||
</MobileLink>
|
</MobileLink>
|
||||||
|
|
||||||
<div className="flex h-full flex-col space-y-2 pt-4">
|
<div className="flex h-full flex-col space-y-2 pt-4">
|
||||||
{pathname.startsWith("/area-riservata") && (
|
{pathname.startsWith("/area-riservata") && (
|
||||||
<>
|
<>
|
||||||
{session && session.user && (
|
{session?.user && (
|
||||||
<AreaRiservataLinks
|
<AreaRiservataLinks
|
||||||
isAdmin={session.user.isAdmin}
|
isAdmin={session.user.isAdmin}
|
||||||
pathname={pathname}
|
pathname={pathname}
|
||||||
setOpen={setOpen}
|
setOpen={setOpen}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Separator />
|
<Separator />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<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}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
onOpenChange={setOpen}
|
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",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<IconMatrix
|
<IconMatrix
|
||||||
key={item.title}
|
key={item.title}
|
||||||
type={item.icon}
|
type={item.icon}
|
||||||
className="size-5"
|
className="size-5"
|
||||||
/>{" "}
|
/>{" "}
|
||||||
{item.title}
|
{item.title}
|
||||||
</MobileLink>
|
</MobileLink>
|
||||||
))}
|
))}
|
||||||
<Separator />
|
<Separator />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex h-fit flex-col gap-4 pb-2">
|
<div className="flex h-fit flex-col gap-4 pb-2">
|
||||||
<UserHeaderSection
|
<UserHeaderSection
|
||||||
open={userOpen}
|
open={userOpen}
|
||||||
setOpen={setUserOpen}
|
setOpen={setUserOpen}
|
||||||
setSidebar={setOpen}
|
setSidebar={setOpen}
|
||||||
inSidebar
|
inSidebar
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex items-center justify-center gap-6">
|
<div className="flex items-center justify-center gap-6">
|
||||||
<LangToggle />
|
<LangToggle />
|
||||||
{/* <ThemeSwitch /> */}
|
{/* <ThemeSwitch /> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
className="ml-3 flex shrink-0 items-center md:hidden"
|
className="ml-3 flex shrink-0 items-center md:hidden"
|
||||||
aria-label="mainNavLink"
|
aria-label="mainNavLink"
|
||||||
>
|
>
|
||||||
{verySmallScreen ? (
|
{verySmallScreen ? (
|
||||||
<ASvg className="h-8 w-auto" />
|
<ASvg className="h-8 w-auto" />
|
||||||
) : (
|
) : (
|
||||||
<LogoSvg className="xs:h-8 [&>text]:fill-primary [&>text]:stroke-primary h-6 w-auto text-neutral-700 sm:h-10 [&>g]:fill-red-500 dark:[&>g]:fill-neutral-100 dark:[&>text]:fill-neutral-100 dark:[&>text]:stroke-neutral-100" />
|
<LogoSvg className="xs:h-8 [&>text]:fill-primary [&>text]:stroke-primary h-6 w-auto text-neutral-700 sm:h-10 [&>g]:fill-red-500 dark:[&>g]:fill-neutral-100 dark:[&>text]:fill-neutral-100 dark:[&>text]:stroke-neutral-100" />
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MobileLinkProps extends LinkProps {
|
interface MobileLinkProps extends LinkProps {
|
||||||
onOpenChange?: (open: boolean) => void;
|
onOpenChange?: (open: boolean) => void;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MobileLink({
|
function MobileLink({
|
||||||
href,
|
href,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
disabled,
|
disabled,
|
||||||
...props
|
...props
|
||||||
}: MobileLinkProps) {
|
}: MobileLinkProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
aria-label="mobileLink"
|
aria-label="mobileLink"
|
||||||
href={href}
|
href={href}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onOpenChange?.(false);
|
onOpenChange?.(false);
|
||||||
} else {
|
} else {
|
||||||
router.push(
|
router.push(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
href.toString(),
|
||||||
href.toString(),
|
);
|
||||||
);
|
onOpenChange?.(false);
|
||||||
onOpenChange?.(false);
|
}
|
||||||
}
|
}}
|
||||||
}}
|
className={cn(className)}
|
||||||
className={cn(className)}
|
{...props}
|
||||||
{...props}
|
>
|
||||||
>
|
{children}
|
||||||
{children}
|
</Link>
|
||||||
</Link>
|
);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AreaRiservataLinksProps = {
|
type AreaRiservataLinksProps = {
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
setOpen?: (status: boolean) => void;
|
setOpen?: (status: boolean) => void;
|
||||||
pathname: string;
|
pathname: string;
|
||||||
};
|
};
|
||||||
const AreaRiservataLinks = ({
|
const AreaRiservataLinks = ({
|
||||||
isAdmin,
|
isAdmin,
|
||||||
setOpen,
|
setOpen,
|
||||||
pathname,
|
pathname,
|
||||||
}: AreaRiservataLinksProps) => {
|
}: AreaRiservataLinksProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div className="text-primary flex flex-col space-y-4 text-base">
|
<div className="text-primary flex flex-col space-y-4 text-base">
|
||||||
{isAdmin
|
{isAdmin
|
||||||
? t.nav.areaRiservataAdminNav.map((item) => {
|
? t.nav.areaRiservataAdminNav.map((item) => {
|
||||||
if (item.items !== undefined) {
|
if (item.items !== undefined) {
|
||||||
return (
|
return (
|
||||||
<div key={item.title}>
|
<div key={item.title}>
|
||||||
<div
|
<div
|
||||||
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",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<IconMatrix type={item.icon} className="size-5" />
|
<IconMatrix type={item.icon} className="size-5" />
|
||||||
|
|
||||||
{item.title}
|
{item.title}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<ul>
|
<ul>
|
||||||
{item.items.map((item) => {
|
{item.items.map((item) => {
|
||||||
return (
|
return (
|
||||||
<MobileLink
|
<MobileLink
|
||||||
key={item.title}
|
key={item.title}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
onOpenChange={setOpen}
|
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",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</MobileLink>
|
</MobileLink>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<MobileLink
|
<MobileLink
|
||||||
key={item.title}
|
key={item.title}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
onOpenChange={setOpen}
|
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",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<IconMatrix type={item.icon} className="size-5" />
|
<IconMatrix type={item.icon} className="size-5" />
|
||||||
{item.title}
|
{item.title}
|
||||||
</MobileLink>
|
</MobileLink>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
: t.nav.areaRiservataUserNav.map((item) => (
|
: t.nav.areaRiservataUserNav.map((item) => (
|
||||||
<MobileLink
|
<MobileLink
|
||||||
key={item.title}
|
key={item.title}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
onOpenChange={setOpen}
|
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",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<IconMatrix type={item.icon} className="size-5" />
|
<IconMatrix type={item.icon} className="size-5" />
|
||||||
{item.title}
|
{item.title}
|
||||||
</MobileLink>
|
</MobileLink>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ARMinimizableLinks = ({
|
export const ARMinimizableLinks = ({
|
||||||
isAdmin,
|
isAdmin,
|
||||||
setOpen,
|
setOpen,
|
||||||
pathname,
|
pathname,
|
||||||
minimized,
|
minimized,
|
||||||
}: {
|
}: {
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
setOpen?: (status: boolean) => void;
|
setOpen?: (status: boolean) => void;
|
||||||
pathname: string;
|
pathname: string;
|
||||||
minimized: boolean;
|
minimized: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-primary flex w-full flex-col space-y-4 text-base",
|
"text-primary flex w-full flex-col space-y-4 text-base",
|
||||||
minimized && "justify-center",
|
minimized && "justify-center",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isAdmin
|
{isAdmin
|
||||||
? t.nav.areaRiservataAdminNav.map((item) => {
|
? t.nav.areaRiservataAdminNav.map((item) => {
|
||||||
if (item.items !== undefined) {
|
if (item.items !== undefined) {
|
||||||
return (
|
return (
|
||||||
<div key={item.title}>
|
<div key={item.title}>
|
||||||
<div
|
<div
|
||||||
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",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<IconMatrix type={item.icon} className="size-5" />
|
<IconMatrix type={item.icon} className="size-5" />
|
||||||
|
|
||||||
{!minimized && item.title}
|
{!minimized && item.title}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<ul className="truncate">
|
<ul className="truncate">
|
||||||
{item.items.map((item) => {
|
{item.items.map((item) => {
|
||||||
return (
|
return (
|
||||||
<MobileLink
|
<MobileLink
|
||||||
key={item.title}
|
key={item.title}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
onOpenChange={setOpen}
|
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",
|
||||||
minimized && "hidden",
|
minimized && "hidden",
|
||||||
)}
|
)}
|
||||||
disabled={minimized}
|
disabled={minimized}
|
||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</MobileLink>
|
</MobileLink>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<MobileLink
|
<MobileLink
|
||||||
key={item.title}
|
key={item.title}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
onOpenChange={setOpen}
|
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}
|
||||||
>
|
>
|
||||||
<IconMatrix type={item.icon} className="size-5" />
|
<IconMatrix type={item.icon} className="size-5" />
|
||||||
{!minimized && item.title}
|
{!minimized && item.title}
|
||||||
</MobileLink>
|
</MobileLink>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
: t.nav.areaRiservataUserNav.map((item) => (
|
: t.nav.areaRiservataUserNav.map((item) => (
|
||||||
<MobileLink
|
<MobileLink
|
||||||
key={item.title}
|
key={item.title}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
onOpenChange={setOpen}
|
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}
|
||||||
>
|
>
|
||||||
<IconMatrix type={item.icon} className="size-5" />
|
<IconMatrix type={item.icon} className="size-5" />
|
||||||
{!minimized && item.title}
|
{!minimized && item.title}
|
||||||
</MobileLink>
|
</MobileLink>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,49 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Search } from "lucide-react";
|
import { Search } from "lucide-react";
|
||||||
|
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogTrigger,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogDescription,
|
|
||||||
} from "~/components/ui/dialog";
|
|
||||||
import { CodiceBox } from "~/components/codiceRicerca";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { CodiceBox } from "~/components/codiceRicerca";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "~/components/ui/dialog";
|
||||||
|
|
||||||
export function RicercaCommand() {
|
export function RicercaCommand() {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
router.events.on("routeChangeStart", () => {
|
router.events.on("routeChangeStart", () => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost" className="p-2" aria-label="ricercaBtn">
|
<Button variant="ghost" className="p-2" aria-label="ricercaBtn">
|
||||||
<Search className="size-5" />
|
<Search className="size-5" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="border-none bg-transparent sm:max-w-xl [&_[data-slot=dialog-close]]:hidden">
|
<DialogContent className="border-none bg-transparent sm:max-w-xl [&_[data-slot=dialog-close]]:hidden">
|
||||||
<DialogHeader className="sr-only">
|
<DialogHeader className="sr-only">
|
||||||
<DialogTitle>Ricerca</DialogTitle>
|
<DialogTitle>Ricerca</DialogTitle>
|
||||||
<DialogDescription>Effettua una ricerca</DialogDescription>
|
<DialogDescription>Effettua una ricerca</DialogDescription>
|
||||||
</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"}
|
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"
|
||||||
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>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,25 @@
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import { Settings } from "lucide-react";
|
import { Settings } from "lucide-react";
|
||||||
import { LangToggle } from "~/components/navbar/lang-toggle";
|
import { LangToggle } from "~/components/navbar/lang-toggle";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "~/components/ui/popover";
|
} from "~/components/ui/popover";
|
||||||
|
|
||||||
export const SettingsPopover = () => {
|
export const SettingsPopover = () => {
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button variant="ghost" className="p-2" aria-label="Settings">
|
<Button variant="ghost" className="p-2" aria-label="Settings">
|
||||||
<Settings className="size-5" />
|
<Settings className="size-5" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="flex w-fit justify-center gap-4 p-4">
|
<PopoverContent className="flex w-fit justify-center gap-4 p-4">
|
||||||
<LangToggle />
|
<LangToggle />
|
||||||
{/* <ThemeSwitch /> */}
|
{/* <ThemeSwitch /> */}
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,51 +1,51 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { MainNav } from "~/components/navbar/main-nav";
|
|
||||||
import { MobileNav } from "~/components/navbar/mobile-nav";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { UserHeaderSection } from "~/components/navbar/login-button";
|
import { UserHeaderSection } from "~/components/navbar/login-button";
|
||||||
import { SettingsPopover } from "~/components/navbar/settings-modal";
|
import { MainNav } from "~/components/navbar/main-nav";
|
||||||
|
import { MobileNav } from "~/components/navbar/mobile-nav";
|
||||||
import { RicercaCommand } from "~/components/navbar/ricerca_command";
|
import { RicercaCommand } from "~/components/navbar/ricerca_command";
|
||||||
|
import { SettingsPopover } from "~/components/navbar/settings-modal";
|
||||||
|
|
||||||
export function SiteHeader() {
|
export function SiteHeader() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const [openSidebar, setOpenSidebar] = useState(false);
|
const [openSidebar, setOpenSidebar] = useState(false);
|
||||||
const [openUserMenu, setOpenUserMenu] = useState(false);
|
const [openUserMenu, setOpenUserMenu] = useState(false);
|
||||||
const [openUserMenuSidebar, setOpenUserMenuSidebar] = useState(false);
|
const [openUserMenuSidebar, setOpenUserMenuSidebar] = useState(false);
|
||||||
|
|
||||||
const handleOpenMenu = (status: boolean) => {
|
const handleOpenMenu = (status: boolean) => {
|
||||||
setOpenUserMenu(status);
|
setOpenUserMenu(status);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenSidebar = (status: boolean) => {
|
const handleOpenSidebar = (status: boolean) => {
|
||||||
setOpenSidebar(status);
|
setOpenSidebar(status);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<header className="border-border/40 bg-background/95 supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50 w-full border-b backdrop-blur">
|
<header className="border-border/40 bg-background/95 supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50 w-full border-b backdrop-blur">
|
||||||
<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}
|
pathname={pathname}
|
||||||
open={openSidebar}
|
open={openSidebar}
|
||||||
setOpen={setOpenSidebar}
|
setOpen={setOpenSidebar}
|
||||||
userOpen={openUserMenuSidebar}
|
userOpen={openUserMenuSidebar}
|
||||||
setUserOpen={setOpenUserMenuSidebar}
|
setUserOpen={setOpenUserMenuSidebar}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<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">
|
||||||
<RicercaCommand />
|
<RicercaCommand />
|
||||||
|
|
||||||
<div className="hidden md:block">
|
<div className="hidden md:block">
|
||||||
<SettingsPopover />
|
<SettingsPopover />
|
||||||
</div>
|
</div>
|
||||||
<UserHeaderSection
|
<UserHeaderSection
|
||||||
key={pathname}
|
key={pathname}
|
||||||
open={openUserMenu}
|
open={openUserMenu}
|
||||||
setOpen={handleOpenMenu}
|
setOpen={handleOpenMenu}
|
||||||
setSidebar={handleOpenSidebar}
|
setSidebar={handleOpenSidebar}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,43 @@
|
||||||
|
import { motion, type Transition } from "framer-motion";
|
||||||
import { Moon, Sun } from "lucide-react";
|
import { Moon, Sun } from "lucide-react";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { motion, type Transition } from "framer-motion";
|
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
export const ThemeSwitch = () => {
|
export const ThemeSwitch = () => {
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
|
|
||||||
const spring: Transition = {
|
const spring: Transition = {
|
||||||
type: "spring",
|
type: "spring",
|
||||||
stiffness: 700,
|
stiffness: 700,
|
||||||
damping: 30,
|
damping: 30,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
aria-label="Toggle Theme"
|
aria-label="Toggle Theme"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
|
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",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex size-8 items-center justify-center rounded-full bg-black/90",
|
"flex size-8 items-center justify-center rounded-full bg-black/90",
|
||||||
//theme == "light" && "bg-sky-500",
|
//theme == "light" && "bg-sky-500",
|
||||||
)}
|
)}
|
||||||
layout
|
layout
|
||||||
transition={spring}
|
transition={spring}
|
||||||
>
|
>
|
||||||
<motion.div whileTap={{ rotate: 360 }}>
|
<motion.div whileTap={{ rotate: 360 }}>
|
||||||
{theme == "light" ? (
|
{theme === "light" ? (
|
||||||
<Sun className="size-6 stroke-yellow-300" />
|
<Sun className="size-6 stroke-yellow-300" />
|
||||||
) : (
|
) : (
|
||||||
<Moon className="size-6 stroke-slate-200" />
|
<Moon className="size-6 stroke-slate-200" />
|
||||||
)}
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,54 @@
|
||||||
import { CheckCircle, AlertCircle, Loader2 } from "lucide-react";
|
import { AlertCircle, CheckCircle, Loader2 } from "lucide-react";
|
||||||
|
|
||||||
type PaymentStatus = "success" | "processing" | "error";
|
type PaymentStatus = "success" | "processing" | "error";
|
||||||
|
|
||||||
interface PaymentStatusProps {
|
interface PaymentStatusProps {
|
||||||
status: PaymentStatus;
|
status: PaymentStatus;
|
||||||
title: string;
|
title: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PaymentStatus({
|
export default function PaymentStatus({
|
||||||
status,
|
status,
|
||||||
message,
|
message,
|
||||||
title,
|
title,
|
||||||
}: PaymentStatusProps) {
|
}: PaymentStatusProps) {
|
||||||
const statusConfig = {
|
const statusConfig = {
|
||||||
success: {
|
success: {
|
||||||
icon: CheckCircle,
|
icon: CheckCircle,
|
||||||
color: "text-green-500",
|
color: "text-green-500",
|
||||||
bgColor: "bg-green-50",
|
bgColor: "bg-green-50",
|
||||||
borderColor: "border-green-200",
|
borderColor: "border-green-200",
|
||||||
},
|
},
|
||||||
processing: {
|
processing: {
|
||||||
icon: Loader2,
|
icon: Loader2,
|
||||||
color: "text-blue-500",
|
color: "text-blue-500",
|
||||||
bgColor: "bg-blue-50",
|
bgColor: "bg-blue-50",
|
||||||
borderColor: "border-blue-200",
|
borderColor: "border-blue-200",
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
icon: AlertCircle,
|
icon: AlertCircle,
|
||||||
color: "text-red-500",
|
color: "text-red-500",
|
||||||
bgColor: "bg-red-50",
|
bgColor: "bg-red-50",
|
||||||
borderColor: "border-red-200",
|
borderColor: "border-red-200",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const { icon: Icon, color, bgColor, borderColor } = statusConfig[status];
|
const { icon: Icon, color, bgColor, borderColor } = statusConfig[status];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`mb-4 flex items-center rounded-lg border p-4 ${bgColor} ${borderColor}`}
|
className={`mb-4 flex items-center rounded-lg border p-4 ${bgColor} ${borderColor}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`size-8 shrink-0 ${color} ${status === "processing" ? "animate-spin" : ""}`}
|
className={`size-8 shrink-0 ${color} ${status === "processing" ? "animate-spin" : ""}`}
|
||||||
>
|
>
|
||||||
<Icon className="size-8" />
|
<Icon className="size-8" />
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-3">
|
<div className="ml-3">
|
||||||
<h3 className={`text-sm font-medium ${color}`}>{title}</h3>
|
<h3 className={`text-sm font-medium ${color}`}>{title}</h3>
|
||||||
{message && <div className="mt-2 text-sm text-gray-700">{message}</div>}
|
{message && <div className="mt-2 text-sm text-gray-700">{message}</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,227 +7,228 @@ import flags from "react-phone-number-input/flags";
|
||||||
import EN from "react-phone-number-input/locale/en";
|
import EN from "react-phone-number-input/locale/en";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
CommandGroup,
|
CommandGroup,
|
||||||
CommandInput,
|
CommandInput,
|
||||||
CommandItem,
|
CommandItem,
|
||||||
} from "~/components/ui/command";
|
} from "~/components/ui/command";
|
||||||
import { Input } from "~/components/ui/input";
|
import { Input } from "~/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "~/components/ui/popover";
|
} from "~/components/ui/popover";
|
||||||
import { ScrollArea } from "~/components/ui/scroll-area";
|
import { ScrollArea } from "~/components/ui/scroll-area";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
type PhoneInputProps = Omit<
|
type PhoneInputProps = Omit<
|
||||||
React.ComponentProps<"input">,
|
React.ComponentProps<"input">,
|
||||||
"onChange" | "value" | "ref"
|
"onChange" | "value" | "ref"
|
||||||
> &
|
> &
|
||||||
Omit<RPNInput.Props<typeof RPNInput.default>, "onChange"> & {
|
Omit<RPNInput.Props<typeof RPNInput.default>, "onChange"> & {
|
||||||
onChange?: (value: RPNInput.Value) => void;
|
onChange?: (value: RPNInput.Value) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PhoneInput: React.ForwardRefExoticComponent<PhoneInputProps> = React.memo(
|
const PhoneInput: React.ForwardRefExoticComponent<PhoneInputProps> = React.memo(
|
||||||
React.forwardRef<
|
React.forwardRef<
|
||||||
React.ComponentRef<typeof RPNInput.default>,
|
React.ComponentRef<typeof RPNInput.default>,
|
||||||
PhoneInputProps
|
PhoneInputProps
|
||||||
>(({ className, onChange, ...props }, ref) => {
|
>(({ className, onChange, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<RPNInput.default
|
<RPNInput.default
|
||||||
ref={ref}
|
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}
|
flagComponent={FlagComponent}
|
||||||
countrySelectComponent={CountrySelect}
|
countrySelectComponent={CountrySelect}
|
||||||
inputComponent={InputComponent}
|
inputComponent={InputComponent}
|
||||||
smartCaret={false}
|
smartCaret={false}
|
||||||
defaultCountry="IT"
|
defaultCountry="IT"
|
||||||
international
|
international
|
||||||
labels={EN}
|
labels={EN}
|
||||||
focusInputOnCountrySelection
|
focusInputOnCountrySelection
|
||||||
/**
|
/**
|
||||||
* Handles the onChange event.
|
* Handles the onChange event.
|
||||||
*
|
*
|
||||||
* react-phone-number-input might trigger the onChange event as undefined
|
* react-phone-number-input might trigger the onChange event as undefined
|
||||||
* when a valid phone number is not entered. To prevent this,
|
* when a valid phone number is not entered. To prevent this,
|
||||||
* the value is coerced to an empty string.
|
* the value is coerced to an empty string.
|
||||||
*
|
*
|
||||||
* @param {E164Number | undefined} value - The entered value
|
* @param {E164Number | undefined} value - The entered value
|
||||||
*/
|
*/
|
||||||
onChange={(value) => onChange?.(value || ("" as RPNInput.Value))}
|
onChange={(value) => onChange?.(value || ("" as RPNInput.Value))}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
PhoneInput.displayName = "PhoneInput";
|
PhoneInput.displayName = "PhoneInput";
|
||||||
|
|
||||||
const InputComponent = React.forwardRef<
|
const InputComponent = React.forwardRef<
|
||||||
HTMLInputElement,
|
HTMLInputElement,
|
||||||
React.ComponentProps<"input">
|
React.ComponentProps<"input">
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<Input
|
<Input
|
||||||
className={cn("w-full flex-1 rounded-s-none rounded-e-md", className)}
|
className={cn("w-full flex-1 rounded-s-none rounded-e-md", className)}
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
InputComponent.displayName = "InputComponent";
|
InputComponent.displayName = "InputComponent";
|
||||||
|
|
||||||
type CountryEntry = { label: string; value: RPNInput.Country | undefined };
|
type CountryEntry = { label: string; value: RPNInput.Country | undefined };
|
||||||
|
|
||||||
type CountrySelectProps = {
|
type CountrySelectProps = {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
value: RPNInput.Country;
|
value: RPNInput.Country;
|
||||||
options: CountryEntry[];
|
options: CountryEntry[];
|
||||||
onChange: (country: RPNInput.Country) => void;
|
onChange: (country: RPNInput.Country) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CountrySelect = React.memo(
|
const CountrySelect = React.memo(
|
||||||
({
|
({
|
||||||
disabled,
|
disabled,
|
||||||
value: selectedCountry,
|
value: selectedCountry,
|
||||||
options: countryList,
|
options: countryList,
|
||||||
onChange,
|
onChange,
|
||||||
}: CountrySelectProps) => {
|
}: CountrySelectProps) => {
|
||||||
const [filteredCountries, setFilteredCountries] = React.useState<
|
const [filteredCountries, setFilteredCountries] = React.useState<
|
||||||
CountryEntry[]
|
CountryEntry[]
|
||||||
>(countryList || []);
|
>(countryList || []);
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const [parentNode, setParentNode] = React.useState<HTMLDivElement | null>(
|
const [parentNode, setParentNode] = React.useState<HTMLDivElement | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
const refCallback = React.useCallback((node: HTMLDivElement) => {
|
const refCallback = React.useCallback((node: HTMLDivElement) => {
|
||||||
if (node) {
|
if (node) {
|
||||||
setParentNode(node);
|
setParentNode(node);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const virtualizer = useVirtualizer({
|
const virtualizer = useVirtualizer({
|
||||||
count: filteredCountries.length,
|
count: filteredCountries.length,
|
||||||
getScrollElement: () => parentNode,
|
getScrollElement: () => parentNode,
|
||||||
estimateSize: () => 35,
|
estimateSize: () => 35,
|
||||||
overscan: 5,
|
overscan: 5,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSearch = (search: string) => {
|
const handleSearch = (search: string) => {
|
||||||
if (!search.trim()) {
|
if (!search.trim()) {
|
||||||
setFilteredCountries(countryList);
|
setFilteredCountries(countryList);
|
||||||
} else {
|
} else {
|
||||||
setFilteredCountries(
|
setFilteredCountries(
|
||||||
countryList.filter((country) =>
|
countryList.filter((country) =>
|
||||||
country.label.toLowerCase().includes(search.toLowerCase()),
|
country.label.toLowerCase().includes(search.toLowerCase()),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
role="combobox"
|
role="combobox"
|
||||||
variant="outline"
|
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"
|
||||||
>
|
>
|
||||||
<FlagComponent
|
<FlagComponent
|
||||||
country={selectedCountry}
|
country={selectedCountry}
|
||||||
countryName={
|
countryName={
|
||||||
countryList.find((c) => c.value === selectedCountry)?.label ||
|
countryList.find((c) => c.value === selectedCountry)?.label ||
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ChevronsUpDown
|
<ChevronsUpDown
|
||||||
className={cn(
|
className={cn(
|
||||||
"-mr-2 size-4 opacity-50",
|
"-mr-2 size-4 opacity-50",
|
||||||
disabled ? "hidden" : "opacity-100",
|
disabled ? "hidden" : "opacity-100",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-[300px] p-0">
|
<PopoverContent className="w-[300px] p-0">
|
||||||
<Command shouldFilter={false}>
|
<Command shouldFilter={false}>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="Search country..."
|
placeholder="Search country..."
|
||||||
onValueChange={handleSearch}
|
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"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ScrollArea className="h-[400px] overflow-auto">
|
<ScrollArea className="h-[400px] overflow-auto">
|
||||||
<CommandEmpty>No countries found</CommandEmpty>
|
<CommandEmpty>No countries found</CommandEmpty>
|
||||||
<CommandGroup ref={refCallback}>
|
<CommandGroup ref={refCallback}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: `${virtualizer.getTotalSize()}px`,
|
height: `${virtualizer.getTotalSize()}px`,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{virtualizer
|
{virtualizer
|
||||||
.getVirtualItems()
|
.getVirtualItems()
|
||||||
.map(({ index, size, start }) => {
|
.map(({ index, size, start }) => {
|
||||||
const country = filteredCountries[index];
|
const country = filteredCountries[index];
|
||||||
if (!country?.value) return null;
|
if (!country?.value) return null;
|
||||||
return (
|
return (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={country.value}
|
key={country.value}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: `${size}px`,
|
height: `${size}px`,
|
||||||
transform: `translateY(${start}px)`,
|
transform: `translateY(${start}px)`,
|
||||||
}}
|
}}
|
||||||
onSelect={() => onChange(country.value!)}
|
// biome-ignore lint/style/noNonNullAssertion: <exists>
|
||||||
value={country.value}
|
onSelect={() => onChange(country.value!)}
|
||||||
>
|
value={country.value}
|
||||||
<FlagComponent
|
>
|
||||||
country={country.value}
|
<FlagComponent
|
||||||
countryName={country.label}
|
country={country.value}
|
||||||
/>
|
countryName={country.label}
|
||||||
<span className="flex-1 text-sm">
|
/>
|
||||||
{country.label}
|
<span className="flex-1 text-sm">
|
||||||
</span>
|
{country.label}
|
||||||
<span className="text-foreground/50 text-sm">{`+${RPNInput.getCountryCallingCode(
|
</span>
|
||||||
country.value,
|
<span className="text-foreground/50 text-sm">{`+${RPNInput.getCountryCallingCode(
|
||||||
)}`}</span>
|
country.value,
|
||||||
<CheckIcon
|
)}`}</span>
|
||||||
className={`ml-auto size-4 ${
|
<CheckIcon
|
||||||
country.value === selectedCountry
|
className={`ml-auto size-4 ${
|
||||||
? "opacity-100"
|
country.value === selectedCountry
|
||||||
: "opacity-0"
|
? "opacity-100"
|
||||||
}`}
|
: "opacity-0"
|
||||||
/>
|
}`}
|
||||||
</CommandItem>
|
/>
|
||||||
);
|
</CommandItem>
|
||||||
})}
|
);
|
||||||
</div>
|
})}
|
||||||
</CommandGroup>
|
</div>
|
||||||
</ScrollArea>
|
</CommandGroup>
|
||||||
</Command>
|
</ScrollArea>
|
||||||
</PopoverContent>
|
</Command>
|
||||||
</Popover>
|
</PopoverContent>
|
||||||
);
|
</Popover>
|
||||||
},
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
CountrySelect.displayName = "CountrySelect";
|
CountrySelect.displayName = "CountrySelect";
|
||||||
|
|
||||||
const FlagComponent = ({ country, countryName }: RPNInput.FlagProps) => {
|
const FlagComponent = ({ country, countryName }: RPNInput.FlagProps) => {
|
||||||
const Flag = flags[country];
|
const Flag = flags[country];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="flex size-6 items-center justify-center overflow-hidden rounded-sm">
|
<span className="flex size-6 items-center justify-center overflow-hidden rounded-sm">
|
||||||
{Flag ? <Flag title={countryName} /> : <Globe className="size-4" />}
|
{Flag ? <Flag title={countryName} /> : <Globe className="size-4" />}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { PhoneInput };
|
export { PhoneInput };
|
||||||
|
|
|
||||||
|
|
@ -1,384 +1,384 @@
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
import { ExternalLink } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { AnimatedNumber } from "~/components/custom_ui/animated-number";
|
||||||
|
import { LoadingPage } from "~/components/loading";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardHeader,
|
CardContent,
|
||||||
CardTitle,
|
CardDescription,
|
||||||
CardDescription,
|
CardHeader,
|
||||||
CardContent,
|
CardTitle,
|
||||||
} from "~/components/ui/card";
|
} from "~/components/ui/card";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
import { AnimatedNumber } from "~/components/custom_ui/animated-number";
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
import { ExternalLink } from "lucide-react";
|
|
||||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "~/components/ui/tabs";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import { LoadingPage } from "~/components/loading";
|
|
||||||
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
|
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
|
||||||
import { useEffect, useState } from "react";
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
export const PricingChoice = () => {
|
export const PricingChoice = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data: prezziario, isLoading } =
|
const { data: prezziario, isLoading } =
|
||||||
api.prezziario.getPrezziPerTipologiaAll.useQuery();
|
api.prezziario.getPrezziPerTipologiaAll.useQuery();
|
||||||
if (isLoading) return <LoadingPage />;
|
if (isLoading) return <LoadingPage />;
|
||||||
if (!prezziario) return null;
|
if (!prezziario) return null;
|
||||||
|
|
||||||
const pricingBreve = prezziario.transitorio.saldi.map((s) => ({
|
const pricingBreve = prezziario.transitorio.saldi.map((s) => ({
|
||||||
downPayment: prezziario.transitorio.acconto.prezzo_cent / 100,
|
downPayment: prezziario.transitorio.acconto.prezzo_cent / 100,
|
||||||
secondPayment: s.prezzo_cent / 100,
|
secondPayment: s.prezzo_cent / 100,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const pricingStabile = prezziario.stabile.saldi.map((s) => ({
|
const pricingStabile = prezziario.stabile.saldi.map((s) => ({
|
||||||
downPayment: prezziario.stabile.acconto.prezzo_cent / 100,
|
downPayment: prezziario.stabile.acconto.prezzo_cent / 100,
|
||||||
secondPayment: s.prezzo_cent / 100,
|
secondPayment: s.prezzo_cent / 100,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
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 defaultValue="transitorio" className="mb-2 w-full max-w-xl">
|
||||||
<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"
|
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"
|
||||||
>
|
>
|
||||||
Transitorio
|
Transitorio
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="stabile"
|
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"
|
||||||
>
|
>
|
||||||
Stabile
|
Stabile
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="transitorio" className="w-full">
|
<TabsContent value="transitorio" className="w-full">
|
||||||
<PricingComponent
|
<PricingComponent
|
||||||
title={t.prezzi.titolo_transitori}
|
title={t.prezzi.titolo_transitori}
|
||||||
cta={t.pricing_cmp.cta_breve}
|
cta={t.pricing_cmp.cta_breve}
|
||||||
pricingData={pricingBreve}
|
pricingData={pricingBreve}
|
||||||
opzioni={[
|
opzioni={[
|
||||||
t.pricing_cmp.breve_option1,
|
t.pricing_cmp.breve_option1,
|
||||||
t.pricing_cmp.breve_option2,
|
t.pricing_cmp.breve_option2,
|
||||||
t.pricing_cmp.breve_option3,
|
t.pricing_cmp.breve_option3,
|
||||||
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"
|
className="border-transitorio bg-transitorio"
|
||||||
tipo={TipologiaPosizioneEnum.Transitorio}
|
tipo={TipologiaPosizioneEnum.Transitorio}
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="stabile">
|
<TabsContent value="stabile">
|
||||||
<PricingComponent
|
<PricingComponent
|
||||||
title={t.prezzi.titolo_stabile}
|
title={t.prezzi.titolo_stabile}
|
||||||
cta={t.pricing_cmp.cta_stabile}
|
cta={t.pricing_cmp.cta_stabile}
|
||||||
pricingData={pricingStabile}
|
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"
|
className="border-stabile bg-stabile"
|
||||||
tipo={TipologiaPosizioneEnum.Stabile}
|
tipo={TipologiaPosizioneEnum.Stabile}
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<Link aria-label="Link to pricing page" href="/prezzi">
|
<Link aria-label="Link to pricing page" href="/prezzi">
|
||||||
<Button className="flex h-12 flex-row gap-2 px-6 py-4 text-2xl hover:underline">
|
<Button className="flex h-12 flex-row gap-2 px-6 py-4 text-2xl hover:underline">
|
||||||
<span>Scopri di più sui prezzi</span>
|
<span>Scopri di più sui prezzi</span>
|
||||||
<ExternalLink className="size-5" />
|
<ExternalLink className="size-5" />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type PricingData = { downPayment: number; secondPayment: number }[];
|
type PricingData = { downPayment: number; secondPayment: number }[];
|
||||||
|
|
||||||
export const PricingComponent = ({
|
export const PricingComponent = ({
|
||||||
pricingData,
|
pricingData,
|
||||||
opzioni,
|
opzioni,
|
||||||
cta,
|
cta,
|
||||||
statico = false,
|
statico = false,
|
||||||
className,
|
className,
|
||||||
title,
|
title,
|
||||||
tipo,
|
tipo,
|
||||||
}: {
|
}: {
|
||||||
pricingData: PricingData;
|
pricingData: PricingData;
|
||||||
opzioni: string[];
|
opzioni: string[];
|
||||||
title: string;
|
title: string;
|
||||||
cta: string;
|
cta: string;
|
||||||
statico?: boolean;
|
statico?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
tipo: TipologiaPosizioneEnum;
|
tipo: TipologiaPosizioneEnum;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [Value, setValue] = useState<number>(0);
|
const [Value, setValue] = useState<number>(0);
|
||||||
const p = pricingData[Value];
|
const p = pricingData[Value];
|
||||||
const downPayment = p?.downPayment || 0;
|
const downPayment = p?.downPayment || 0;
|
||||||
const secondPayment = p?.secondPayment || 0;
|
const secondPayment = p?.secondPayment || 0;
|
||||||
|
|
||||||
const customFormat = (num: number) => `${num}€`;
|
const customFormat = (num: number) => `${num}€`;
|
||||||
|
|
||||||
const [intervalId, setIntervalId] = useState<NodeJS.Timeout | null>(null);
|
const [intervalId, setIntervalId] = useState<NodeJS.Timeout | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (statico) return;
|
if (statico) return;
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
setValue((prevValue) => (prevValue + 1) % pricingData.length);
|
setValue((prevValue) => (prevValue + 1) % pricingData.length);
|
||||||
}, 2500);
|
}, 2500);
|
||||||
setIntervalId(interval);
|
setIntervalId(interval);
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const ClearCarousel = () => {
|
const ClearCarousel = () => {
|
||||||
if (intervalId) {
|
if (intervalId) {
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={cn("w-full max-w-xl gap-4 py-4", className)}>
|
<Card className={cn("w-full max-w-xl gap-4 py-4", className)}>
|
||||||
<CardHeader className="py-2">
|
<CardHeader className="py-2">
|
||||||
<CardTitle className="pt-1 text-center text-3xl font-semibold">
|
<CardTitle className="pt-1 text-center text-3xl font-semibold">
|
||||||
{title}
|
{title}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="sr-only">
|
<CardDescription className="sr-only">
|
||||||
Prezzo del servizio
|
Prezzo del servizio
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="xs:px-6 flex w-full flex-col items-center justify-center gap-4 p-0 px-2 py-0 sm:gap-6 sm:py-4">
|
<CardContent className="xs:px-6 flex w-full flex-col items-center justify-center gap-4 p-0 px-2 py-0 sm:gap-6 sm:py-4">
|
||||||
<div
|
<div
|
||||||
className="xs:gap-x-4 xs:gap-y-1 flex w-full items-center justify-center gap-0"
|
className="xs:gap-x-4 xs:gap-y-1 flex w-full items-center justify-center gap-0"
|
||||||
data-role="wrapper"
|
data-role="wrapper"
|
||||||
>
|
>
|
||||||
<div className="xs:w-1/3 flex w-2/5 flex-col items-center gap-2">
|
<div className="xs:w-1/3 flex w-2/5 flex-col items-center gap-2">
|
||||||
<h2 className="text-2xl font-semibold" data-role="title">
|
<h2 className="text-2xl font-semibold" data-role="title">
|
||||||
{t.acconto}
|
{t.acconto}
|
||||||
</h2>
|
</h2>
|
||||||
<p
|
<p
|
||||||
className="xs:text-3xl text-2xl font-semibold sm:text-6xl"
|
className="xs:text-3xl text-2xl font-semibold sm:text-6xl"
|
||||||
data-role="amount"
|
data-role="amount"
|
||||||
>
|
>
|
||||||
{downPayment}€
|
{downPayment}€
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="xs:w-1/3 flex w-2/5 flex-col items-center gap-2 text-center">
|
<div className="xs:w-1/3 flex w-2/5 flex-col items-center gap-2 text-center">
|
||||||
<h2 className="text-2xl font-semibold" data-role="title">
|
<h2 className="text-2xl font-semibold" data-role="title">
|
||||||
{t.saldo}
|
{t.saldo}
|
||||||
</h2>
|
</h2>
|
||||||
<p
|
<p
|
||||||
className="xs:text-3xl text-2xl font-semibold sm:text-6xl"
|
className="xs:text-3xl text-2xl font-semibold sm:text-6xl"
|
||||||
data-role="amount"
|
data-role="amount"
|
||||||
>
|
>
|
||||||
<AnimatedNumber
|
<AnimatedNumber
|
||||||
value={secondPayment}
|
value={secondPayment}
|
||||||
format={customFormat}
|
format={customFormat}
|
||||||
precision={secondPayment % 1 === 0 ? 0 : 2}
|
precision={secondPayment % 1 === 0 ? 0 : 2}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="xs:w-1/3 flex w-2/5 flex-col items-center gap-2 text-center">
|
<div className="xs:w-1/3 flex w-2/5 flex-col items-center gap-2 text-center">
|
||||||
<h2 className="text-2xl font-semibold" data-role="title">
|
<h2 className="text-2xl font-semibold" data-role="title">
|
||||||
Contratto
|
Contratto
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{tipo == TipologiaPosizioneEnum.Transitorio ? (
|
{tipo === TipologiaPosizioneEnum.Transitorio ? (
|
||||||
<div className="flex flex-col justify-center leading-none">
|
<div className="flex flex-col justify-center leading-none">
|
||||||
<span
|
<span
|
||||||
className="xs:text-3xl text-2xl font-semibold sm:text-6xl"
|
className="xs:text-3xl text-2xl font-semibold sm:text-6xl"
|
||||||
data-role="amount"
|
data-role="amount"
|
||||||
>
|
>
|
||||||
0 €
|
0 €
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Link
|
<Link
|
||||||
href={"/prezzi#contratti"}
|
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"
|
||||||
>
|
>
|
||||||
<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" />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</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}
|
key={idx}
|
||||||
size="default"
|
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"}
|
variant={idx === Value ? "default" : "outline"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
ClearCarousel();
|
ClearCarousel();
|
||||||
setValue(idx);
|
setValue(idx);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{opzione}
|
{opzione}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-center text-xl font-semibold text-white">{cta}</h2>
|
<h2 className="text-center text-xl font-semibold text-white">{cta}</h2>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PricingComponentConsulenza = ({
|
export const PricingComponentConsulenza = ({
|
||||||
pricingData,
|
pricingData,
|
||||||
opzioni,
|
opzioni,
|
||||||
cta,
|
cta,
|
||||||
statico = false,
|
statico = false,
|
||||||
className,
|
className,
|
||||||
title,
|
title,
|
||||||
}: {
|
}: {
|
||||||
pricingData: number[];
|
pricingData: number[];
|
||||||
opzioni: string[];
|
opzioni: string[];
|
||||||
title: string;
|
title: string;
|
||||||
cta: string;
|
cta: string;
|
||||||
statico?: boolean;
|
statico?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const [Value, setValue] = useState<number>(0);
|
const [Value, setValue] = useState<number>(0);
|
||||||
const p = pricingData[Value];
|
const p = pricingData[Value];
|
||||||
|
|
||||||
const customFormat = (num: number) => `${num}€`;
|
const customFormat = (num: number) => `${num}€`;
|
||||||
|
|
||||||
const [intervalId, setIntervalId] = useState<NodeJS.Timeout | null>(null);
|
const [intervalId, setIntervalId] = useState<NodeJS.Timeout | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (statico) return;
|
if (statico) return;
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
setValue((prevValue) => (prevValue + 1) % pricingData.length);
|
setValue((prevValue) => (prevValue + 1) % pricingData.length);
|
||||||
}, 2500);
|
}, 2500);
|
||||||
setIntervalId(interval);
|
setIntervalId(interval);
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const ClearCarousel = () => {
|
const ClearCarousel = () => {
|
||||||
if (intervalId) {
|
if (intervalId) {
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={cn("w-full max-w-xl", className)}>
|
<Card className={cn("w-full max-w-xl", className)}>
|
||||||
<CardHeader className="p-3 px-5">
|
<CardHeader className="p-3 px-5">
|
||||||
<CardTitle className="pt-1 text-center text-3xl font-semibold">
|
<CardTitle className="pt-1 text-center text-3xl font-semibold">
|
||||||
{title}
|
{title}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="sr-only">
|
<CardDescription className="sr-only">
|
||||||
Prezzo del servizio
|
Prezzo del servizio
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="xs:px-6 flex flex-col items-center justify-center gap-8 p-0 px-2 pt-4 pb-4">
|
<CardContent className="xs:px-6 flex flex-col items-center justify-center gap-8 p-0 px-2 pt-4 pb-4">
|
||||||
<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 value={p || 0} format={customFormat} />
|
||||||
</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}
|
key={idx}
|
||||||
size="default"
|
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"}
|
variant={idx === Value ? "default" : "outline"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
ClearCarousel();
|
ClearCarousel();
|
||||||
setValue(idx);
|
setValue(idx);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{opzione}
|
{opzione}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-center text-xl font-semibold text-white">{cta}</h2>
|
<h2 className="text-center text-xl font-semibold text-white">{cta}</h2>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PrezziShow = () => {
|
export const PrezziShow = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { data: prezziario, isLoading } =
|
const { data: prezziario, isLoading } =
|
||||||
api.prezziario.getPrezziPerTipologiaAll.useQuery();
|
api.prezziario.getPrezziPerTipologiaAll.useQuery();
|
||||||
if (isLoading) return <LoadingPage />;
|
if (isLoading) return <LoadingPage />;
|
||||||
if (!prezziario) return null;
|
if (!prezziario) return null;
|
||||||
|
|
||||||
const pricingBreve = prezziario.transitorio.saldi.map((s) => ({
|
const pricingBreve = prezziario.transitorio.saldi.map((s) => ({
|
||||||
downPayment: prezziario.transitorio.acconto.prezzo_cent / 100,
|
downPayment: prezziario.transitorio.acconto.prezzo_cent / 100,
|
||||||
secondPayment: s.prezzo_cent / 100,
|
secondPayment: s.prezzo_cent / 100,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const pricingStabile = prezziario.stabile.saldi.map((s) => ({
|
const pricingStabile = prezziario.stabile.saldi.map((s) => ({
|
||||||
downPayment: prezziario.stabile.acconto.prezzo_cent / 100,
|
downPayment: prezziario.stabile.acconto.prezzo_cent / 100,
|
||||||
secondPayment: s.prezzo_cent / 100,
|
secondPayment: s.prezzo_cent / 100,
|
||||||
}));
|
}));
|
||||||
return (
|
return (
|
||||||
<div className="text-primary mx-auto flex w-full flex-col items-center justify-center gap-8 py-6 sm:max-w-5xl sm:flex-row">
|
<div className="text-primary mx-auto flex w-full flex-col items-center justify-center gap-8 py-6 sm:max-w-5xl sm:flex-row">
|
||||||
<div className="flex w-full flex-col gap-2 text-start sm:w-2/5 sm:text-left">
|
<div className="flex w-full flex-col gap-2 text-start sm:w-2/5 sm:text-left">
|
||||||
<h1 className="text-primary text-3xl font-bold">
|
<h1 className="text-primary text-3xl font-bold">
|
||||||
Quanto costa il servizio?
|
Quanto costa il servizio?
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground text-lg">
|
<p className="text-muted-foreground text-lg">
|
||||||
Il nostro servizio è pensato per essere accessibile e conveniente, con
|
Il nostro servizio è pensato per essere accessibile e conveniente, con
|
||||||
tariffe chiare e senza sorprese. Basate sul tipo di affitto che
|
tariffe chiare e senza sorprese. Basate sul tipo di affitto che
|
||||||
desideri, le nostre tariffe sono suddivise in due categorie
|
desideri, le nostre tariffe sono suddivise in due categorie
|
||||||
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 defaultValue="transitorio" className="w-full sm:w-3/5">
|
||||||
<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 value="transitorio" className="w-full text-xl">
|
||||||
Affitto Transitorio
|
Affitto Transitorio
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="stabile" className="w-full text-xl">
|
<TabsTrigger value="stabile" className="w-full text-xl">
|
||||||
Affitto Stabile
|
Affitto Stabile
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="transitorio">
|
<TabsContent value="transitorio">
|
||||||
<PricingComponent
|
<PricingComponent
|
||||||
title={t.prezzi.titolo_transitori}
|
title={t.prezzi.titolo_transitori}
|
||||||
cta={t.pricing_cmp.cta_breve}
|
cta={t.pricing_cmp.cta_breve}
|
||||||
pricingData={pricingBreve}
|
pricingData={pricingBreve}
|
||||||
opzioni={[
|
opzioni={[
|
||||||
t.pricing_cmp.breve_option1,
|
t.pricing_cmp.breve_option1,
|
||||||
t.pricing_cmp.breve_option2,
|
t.pricing_cmp.breve_option2,
|
||||||
t.pricing_cmp.breve_option3,
|
t.pricing_cmp.breve_option3,
|
||||||
t.pricing_cmp.breve_option4,
|
t.pricing_cmp.breve_option4,
|
||||||
t.pricing_cmp.breve_option5,
|
t.pricing_cmp.breve_option5,
|
||||||
]}
|
]}
|
||||||
tipo={TipologiaPosizioneEnum.Transitorio}
|
tipo={TipologiaPosizioneEnum.Transitorio}
|
||||||
className="border-transitorio bg-transitorio"
|
className="border-transitorio bg-transitorio"
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="stabile">
|
<TabsContent value="stabile">
|
||||||
<PricingComponent
|
<PricingComponent
|
||||||
title={t.prezzi.titolo_stabile}
|
title={t.prezzi.titolo_stabile}
|
||||||
cta={t.pricing_cmp.cta_stabile}
|
cta={t.pricing_cmp.cta_stabile}
|
||||||
pricingData={pricingStabile}
|
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"
|
className="border-stabile bg-stabile"
|
||||||
tipo={TipologiaPosizioneEnum.Stabile}
|
tipo={TipologiaPosizioneEnum.Stabile}
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,57 +6,57 @@ import { useEffect, useState } from "react";
|
||||||
import { Progress } from "~/components/ui/progress";
|
import { Progress } from "~/components/ui/progress";
|
||||||
|
|
||||||
export const ProgressRedirect = ({ href }: { href: string }) => {
|
export const ProgressRedirect = ({ href }: { href: string }) => {
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
const [intervalId, setIntervalId] = useState<NodeJS.Timeout | null>(null);
|
const [intervalId, setIntervalId] = useState<NodeJS.Timeout | null>(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const handleProgress = () => {
|
const handleProgress = () => {
|
||||||
setProgress((prev) => {
|
setProgress((prev) => {
|
||||||
if (prev >= 100) {
|
if (prev >= 100) {
|
||||||
if (intervalId) {
|
if (intervalId) {
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return 100;
|
return 100;
|
||||||
}
|
}
|
||||||
return prev + 20; // Increment progress by 20% every second
|
return prev + 20; // Increment progress by 20% every second
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = setInterval(handleProgress, 1000); // Update progress every second
|
const id = setInterval(handleProgress, 1000); // Update progress every second
|
||||||
setIntervalId(id);
|
setIntervalId(id);
|
||||||
return () => {
|
return () => {
|
||||||
if (id) {
|
if (id) {
|
||||||
clearInterval(id);
|
clearInterval(id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function redirect() {
|
async function redirect() {
|
||||||
await router.push(href);
|
await router.push(href);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (progress == 100) {
|
if (progress === 100) {
|
||||||
redirect().catch((error) => {
|
redirect().catch((error) => {
|
||||||
console.error("Error during redirect:", error);
|
console.error("Error during redirect:", error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [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 value={progress} className="h-3" />
|
||||||
|
|
||||||
<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}
|
href={href}
|
||||||
className="font-semibold underline underline-offset-1"
|
className="font-semibold underline underline-offset-1"
|
||||||
>
|
>
|
||||||
Clicca qui
|
Clicca qui
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,282 +1,281 @@
|
||||||
import { useServizioAnnuncio } from "~/providers/ServizioProvider";
|
|
||||||
import { Interactions } from "~/components/servizio/interactions";
|
|
||||||
import type { Annunci } from "~/schemas/public/Annunci";
|
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
|
||||||
import { cn, formatCurrency } from "~/lib/utils";
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
|
||||||
import { ImageFlbk } from "~/components/ImageWithFallback";
|
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import { ExternalLink, UnfoldVertical } from "lucide-react";
|
import { ExternalLink, UnfoldVertical } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { handleConsegna } from "~/lib/annuncio_details";
|
|
||||||
import {
|
import {
|
||||||
Credenza,
|
Credenza,
|
||||||
CredenzaBody,
|
CredenzaBody,
|
||||||
CredenzaClose,
|
CredenzaClose,
|
||||||
CredenzaContent,
|
CredenzaContent,
|
||||||
CredenzaDescription,
|
CredenzaDescription,
|
||||||
CredenzaFooter,
|
CredenzaFooter,
|
||||||
CredenzaHeader,
|
CredenzaHeader,
|
||||||
CredenzaTitle,
|
CredenzaTitle,
|
||||||
CredenzaTrigger,
|
CredenzaTrigger,
|
||||||
} from "~/components/custom_ui/credenza";
|
} from "~/components/custom_ui/credenza";
|
||||||
import {
|
import { ImageFlbk } from "~/components/ImageWithFallback";
|
||||||
Carousel,
|
|
||||||
CarouselContent,
|
|
||||||
CarouselItem,
|
|
||||||
CarouselNext,
|
|
||||||
CarouselPrevious,
|
|
||||||
} from "~/components/ui/carousel";
|
|
||||||
import { AnnuncioActions } from "~/components/servizio/annuncio_actions";
|
import { AnnuncioActions } from "~/components/servizio/annuncio_actions";
|
||||||
|
import { Interactions } from "~/components/servizio/interactions";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||||
|
import {
|
||||||
|
Carousel,
|
||||||
|
CarouselContent,
|
||||||
|
CarouselItem,
|
||||||
|
CarouselNext,
|
||||||
|
CarouselPrevious,
|
||||||
|
} from "~/components/ui/carousel";
|
||||||
|
import { handleConsegna } from "~/lib/annuncio_details";
|
||||||
|
import { cn, formatCurrency } from "~/lib/utils";
|
||||||
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
|
import { useServizioAnnuncio } from "~/providers/ServizioProvider";
|
||||||
|
import type { Annunci } from "~/schemas/public/Annunci";
|
||||||
|
|
||||||
export const AnnuncioCard = ({ className }: { className?: string }) => {
|
export const AnnuncioCard = ({ className }: { className?: string }) => {
|
||||||
const { data } = useServizioAnnuncio();
|
const { data } = useServizioAnnuncio();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BasicAnnuncioCard
|
<BasicAnnuncioCard
|
||||||
data={data}
|
data={data}
|
||||||
className={className}
|
className={className}
|
||||||
actions={<AnnuncioActions />}
|
actions={<AnnuncioActions />}
|
||||||
interactions={<Interactions />}
|
interactions={<Interactions />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export const BasicAnnuncioCard = ({
|
export const BasicAnnuncioCard = ({
|
||||||
actions,
|
actions,
|
||||||
interactions,
|
interactions,
|
||||||
className,
|
className,
|
||||||
data,
|
data,
|
||||||
}: {
|
}: {
|
||||||
actions?: React.ReactNode;
|
actions?: React.ReactNode;
|
||||||
interactions?: React.ReactNode;
|
interactions?: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
data: Pick<
|
data: Pick<
|
||||||
Annunci,
|
Annunci,
|
||||||
| "id"
|
| "id"
|
||||||
| "codice"
|
| "codice"
|
||||||
| "prezzo"
|
| "prezzo"
|
||||||
| "desc_en"
|
| "desc_en"
|
||||||
| "desc_it"
|
| "desc_it"
|
||||||
| "url_immagini"
|
| "url_immagini"
|
||||||
| "titolo_en"
|
| "titolo_en"
|
||||||
| "titolo_it"
|
| "titolo_it"
|
||||||
| "tipo"
|
| "tipo"
|
||||||
| "consegna"
|
| "consegna"
|
||||||
| "stato"
|
| "stato"
|
||||||
| "web"
|
| "web"
|
||||||
>;
|
>;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={cn("w-full border-white py-5", className)}>
|
<Card className={cn("w-full border-white py-5", className)}>
|
||||||
<CardHeader className="flex flex-row flex-wrap items-center justify-between space-y-0 pb-0">
|
<CardHeader className="flex flex-row flex-wrap items-center justify-between space-y-0 pb-0">
|
||||||
<CardTitle className="text-2xl">Codice: {data.codice}</CardTitle>
|
<CardTitle className="text-2xl">Codice: {data.codice}</CardTitle>
|
||||||
{actions}
|
{actions}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<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
|
||||||
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
|
priority
|
||||||
width={1920}
|
width={1920}
|
||||||
height={1080}
|
height={1080}
|
||||||
className="h-auto w-full rounded-md object-contain sm:max-w-xs"
|
className="h-auto w-full rounded-md object-contain sm:max-w-xs"
|
||||||
alt={data.codice}
|
alt={data.codice}
|
||||||
/>
|
/>
|
||||||
<div className="flex w-full flex-col gap-5">
|
<div className="flex w-full flex-col gap-5">
|
||||||
{interactions}
|
{interactions}
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<TipoBadge tipo={data.tipo} />
|
<TipoBadge tipo={data.tipo} />
|
||||||
<span>
|
<span>
|
||||||
{handleConsegna({
|
{handleConsegna({
|
||||||
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,
|
aggiornamento: t.card.in_aggiornamento,
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
<span>{formatCurrency(data.prezzo / 1e2)}</span>
|
<span>{formatCurrency(data.prezzo / 1e2)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-wrap items-center gap-5">
|
<div className="flex w-full flex-wrap items-center gap-5">
|
||||||
<span className="text-wrap">{data.titolo_it || data.codice}</span>
|
<span className="text-wrap">{data.titolo_it || data.codice}</span>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<AnnuncioDettaglio data={data} />
|
<AnnuncioDettaglio data={data} />
|
||||||
{data.stato !== "Sospeso" && data.web && (
|
{data.stato !== "Sospeso" && data.web && (
|
||||||
<Link
|
<Link
|
||||||
href={`/annuncio/${data.codice}`}
|
href={`/annuncio/${data.codice}`}
|
||||||
aria-label="Pagina Annuncio"
|
aria-label="Pagina Annuncio"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<ExternalLink className="size-6" />
|
<ExternalLink className="size-6" />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const TipoBadge = ({ tipo }: { tipo: Annunci["tipo"] }) => {
|
const TipoBadge = ({ tipo }: { tipo: Annunci["tipo"] }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-fit items-center justify-center rounded-md px-3",
|
"flex w-fit items-center justify-center rounded-md px-3",
|
||||||
tipo === "Transitorio" && "bg-orange-400",
|
tipo === "Transitorio" && "bg-orange-400",
|
||||||
tipo === "Stabile" && "bg-red-400",
|
tipo === "Stabile" && "bg-red-400",
|
||||||
tipo === "Cessione" && "bg-blue-400",
|
tipo === "Cessione" && "bg-blue-400",
|
||||||
tipo === "Vendita" && "bg-green-400",
|
tipo === "Vendita" && "bg-green-400",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="font-semibold text-white">{tipo}</div>
|
<div className="font-semibold text-white">{tipo}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AnnuncioDettaglio = ({
|
const AnnuncioDettaglio = ({
|
||||||
data,
|
data,
|
||||||
}: {
|
}: {
|
||||||
data: Pick<
|
data: Pick<
|
||||||
Annunci,
|
Annunci,
|
||||||
| "id"
|
| "id"
|
||||||
| "codice"
|
| "codice"
|
||||||
| "prezzo"
|
| "prezzo"
|
||||||
| "desc_en"
|
| "desc_en"
|
||||||
| "desc_it"
|
| "desc_it"
|
||||||
| "url_immagini"
|
| "url_immagini"
|
||||||
| "titolo_en"
|
| "titolo_en"
|
||||||
| "titolo_it"
|
| "titolo_it"
|
||||||
| "tipo"
|
| "tipo"
|
||||||
| "consegna"
|
| "consegna"
|
||||||
| "stato"
|
| "stato"
|
||||||
| "web"
|
| "web"
|
||||||
>;
|
>;
|
||||||
}) => {
|
}) => {
|
||||||
const { locale, t } = useTranslation();
|
const { locale, t } = useTranslation();
|
||||||
function replaceWithBr(text: string) {
|
function replaceWithBr(text: string) {
|
||||||
return text.replace(/\n/g, "<br />");
|
return text.replace(/\n/g, "<br />");
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Credenza>
|
<Credenza>
|
||||||
<CredenzaTrigger asChild>
|
<CredenzaTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
aria-label="Dettagli Annuncio"
|
aria-label="Dettagli Annuncio"
|
||||||
>
|
>
|
||||||
<UnfoldVertical className="size-6" /> Dettagli annuncio
|
<UnfoldVertical className="size-6" /> Dettagli annuncio
|
||||||
</Button>
|
</Button>
|
||||||
</CredenzaTrigger>
|
</CredenzaTrigger>
|
||||||
<CredenzaContent className="max-h-[90vh] md:max-w-6xl">
|
<CredenzaContent className="max-h-[90vh] md:max-w-6xl">
|
||||||
<CredenzaHeader>
|
<CredenzaHeader>
|
||||||
<CredenzaTitle className="text-xl">
|
<CredenzaTitle className="text-xl">
|
||||||
Annuncio {data.codice}
|
Annuncio {data.codice}
|
||||||
</CredenzaTitle>
|
</CredenzaTitle>
|
||||||
<CredenzaDescription className="sr-only">
|
<CredenzaDescription className="sr-only">
|
||||||
Annuncio {data.codice}
|
Annuncio {data.codice}
|
||||||
</CredenzaDescription>
|
</CredenzaDescription>
|
||||||
</CredenzaHeader>
|
</CredenzaHeader>
|
||||||
<CredenzaBody className="scrollbar-default max-h-[80vh] overflow-y-auto pb-5">
|
<CredenzaBody className="scrollbar-default max-h-[80vh] overflow-y-auto pb-5">
|
||||||
<div className="flex flex-col gap-2 px-2">
|
<div className="flex flex-col gap-2 px-2">
|
||||||
<Carousel opts={{ loop: true }}>
|
<Carousel opts={{ loop: true }}>
|
||||||
<CarouselContent>
|
<CarouselContent>
|
||||||
{data.url_immagini &&
|
{data?.url_immagini?.map((img, idx) => (
|
||||||
data.url_immagini.map((img, idx) => (
|
<CarouselItem key={img}>
|
||||||
<CarouselItem key={img}>
|
<ImageFlbk
|
||||||
<ImageFlbk
|
alt={`${data.codice} - ${idx}`}
|
||||||
alt={`${data.codice} - ${idx}`}
|
width={400}
|
||||||
width={400}
|
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]"}
|
||||||
className={"h-80 w-full object-contain sm:h-[35rem]"}
|
/>
|
||||||
/>
|
</CarouselItem>
|
||||||
</CarouselItem>
|
))}
|
||||||
))}
|
</CarouselContent>
|
||||||
</CarouselContent>
|
<div
|
||||||
<div
|
onKeyDown={(e) => e.stopPropagation()}
|
||||||
onKeyDown={(e) => e.stopPropagation()}
|
role="button"
|
||||||
role="button"
|
tabIndex={0}
|
||||||
tabIndex={0}
|
onClick={(e) => e.preventDefault()}
|
||||||
onClick={(e) => e.preventDefault()}
|
className="block opacity-0 transition-all duration-200 group-hover:opacity-60 disabled:group-hover:opacity-0"
|
||||||
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"
|
disabled={
|
||||||
disabled={
|
!data.url_immagini || data.url_immagini.length === 1
|
||||||
!data.url_immagini || data.url_immagini.length === 1
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<CarouselNext
|
||||||
<CarouselNext
|
className="right-2 cursor-pointer"
|
||||||
className="right-2 cursor-pointer"
|
disabled={
|
||||||
disabled={
|
!data.url_immagini || data.url_immagini.length === 1
|
||||||
!data.url_immagini || data.url_immagini.length === 1
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</Carousel>
|
||||||
</Carousel>
|
<div>
|
||||||
<div>
|
<TipoBadge tipo={data.tipo} />
|
||||||
<TipoBadge tipo={data.tipo} />
|
|
||||||
|
|
||||||
<div className="mt-1 space-y-2">
|
<div className="mt-1 space-y-2">
|
||||||
<div className="flex flex-row justify-around">
|
<div className="flex flex-row justify-around">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<dt className="sr-only">{t.card.codice}</dt>
|
<dt className="sr-only">{t.card.codice}</dt>
|
||||||
|
|
||||||
<dd className="text-lg font-semibold">
|
<dd className="text-lg font-semibold">
|
||||||
Cod: {data.codice}
|
Cod: {data.codice}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<dt className="sr-only">{t.card.prezzo}</dt>
|
<dt className="sr-only">{t.card.prezzo}</dt>
|
||||||
|
|
||||||
<dd className="text-lg font-semibold">
|
<dd className="text-lg font-semibold">
|
||||||
{formatCurrency(data.prezzo / 1e2)}
|
{formatCurrency(data.prezzo / 1e2)}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<dt className="sr-only">{t.card.titolo}</dt>
|
<dt className="sr-only">{t.card.titolo}</dt>
|
||||||
|
|
||||||
<dd className="text-base font-semibold md:text-lg">
|
<dd className="text-base font-semibold md:text-lg">
|
||||||
{locale === "it" ? data.titolo_it : data.titolo_en}
|
{locale === "it" ? data.titolo_it : data.titolo_en}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<dd
|
<dd
|
||||||
className="text-base"
|
className="text-base"
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: replaceWithBr(
|
__html: replaceWithBr(
|
||||||
(locale === "it" ? data.desc_it : data.desc_it) || "",
|
(locale === "it" ? data.desc_it : data.desc_it) || "",
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CredenzaBody>
|
</CredenzaBody>
|
||||||
<CredenzaFooter>
|
<CredenzaFooter>
|
||||||
<CredenzaClose asChild>
|
<CredenzaClose asChild>
|
||||||
<Button aria-label="Chiudi Credenza">Chiudi</Button>
|
<Button aria-label="Chiudi Credenza">Chiudi</Button>
|
||||||
</CredenzaClose>
|
</CredenzaClose>
|
||||||
</CredenzaFooter>
|
</CredenzaFooter>
|
||||||
</CredenzaContent>
|
</CredenzaContent>
|
||||||
</Credenza>
|
</Credenza>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue