mirror of
https://github.com/TheRealOwenRees/echecsfrance.git
synced 2026-07-22 20:16:57 +00:00
Factorise Button and Input components
This commit is contained in:
Vendored
+5
-1
@@ -53,5 +53,9 @@
|
|||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
},
|
},
|
||||||
"css.lint.unknownAtRules": "ignore",
|
"css.lint.unknownAtRules": "ignore",
|
||||||
"tailwindCSS.experimental.classRegex": ["[a-zA-Z]*ClassName=\"([^\"]+)\""]
|
"tailwindCSS.experimental.classRegex": [
|
||||||
|
["[a-zA-Z]*ClassName=\"([^\"]+)\""],
|
||||||
|
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
|
||||||
|
["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"@turf/boolean-point-in-polygon": "^7.0.0-alpha.114",
|
"@turf/boolean-point-in-polygon": "^7.0.0-alpha.114",
|
||||||
"@types/node": "^22.7.2",
|
"@types/node": "^22.7.2",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"geojson": "^0.5.0",
|
"geojson": "^0.5.0",
|
||||||
"jotai": "^2.6.1",
|
"jotai": "^2.6.1",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import dynamic from "next/dynamic";
|
|||||||
import { FormProvider, useForm } from "react-hook-form";
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { Button } from "@/components/Button";
|
||||||
import { clearMessage } from "@/components/InfoMessage";
|
import { clearMessage } from "@/components/InfoMessage";
|
||||||
import InfoMessage from "@/components/InfoMessage";
|
import InfoMessage from "@/components/InfoMessage";
|
||||||
import LoadingMap from "@/components/LoadingMap";
|
import LoadingMap from "@/components/LoadingMap";
|
||||||
@@ -174,12 +175,14 @@ const TournamentForm = () => {
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-4 row-span-2 sm:col-span-2">
|
|
||||||
|
<div className="col-span-4 row-span-2 self-stretch sm:col-span-2">
|
||||||
<TextAreaField
|
<TextAreaField
|
||||||
|
className="h-full"
|
||||||
|
childrenWrapperClassName="h-full"
|
||||||
name="message"
|
name="message"
|
||||||
control={form.control}
|
control={form.control}
|
||||||
label={t("messageLabel")}
|
label={t("messageLabel")}
|
||||||
rows={6}
|
|
||||||
placeholder={t("messagePlaceholder")}
|
placeholder={t("messagePlaceholder")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -207,21 +210,16 @@ const TournamentForm = () => {
|
|||||||
<Map />
|
<Map />
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
disabled={form.formState.isSubmitting}
|
|
||||||
type="submit"
|
|
||||||
className="rounded-lg bg-primary-600 px-5 py-3 text-center text-sm font-medium text-white hover:bg-primary-800 focus:outline-none focus:ring-4 focus:ring-primary-300 disabled:opacity-25 dark:text-white dark:hover:bg-primary-700 dark:focus:ring-primary-800 sm:w-fit"
|
|
||||||
>
|
|
||||||
{form.formState.isSubmitting ? t("sending") : t("sendButton")}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => form.reset()}
|
|
||||||
type="button"
|
|
||||||
className="ml-4 rounded-lg bg-primary-600 px-5 py-3 text-center text-sm font-medium text-white hover:bg-primary-800 focus:outline-none focus:ring-4 focus:ring-primary-300 disabled:opacity-25 dark:text-white dark:hover:bg-primary-700 dark:focus:ring-primary-800 sm:w-fit"
|
|
||||||
>
|
|
||||||
{t("clearForm")}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Button disabled={form.formState.isSubmitting} type="submit">
|
||||||
|
{form.formState.isSubmitting ? t("sending") : t("sendButton")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button onClick={() => form.reset()} type="button" intent="secondary">
|
||||||
|
{t("clearForm")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<InfoMessage responseMessage={responseMessage} />
|
<InfoMessage responseMessage={responseMessage} />
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useTranslations } from "next-intl";
|
|||||||
import { FormProvider, useForm } from "react-hook-form";
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { Button } from "@/components/Button";
|
||||||
import InfoMessage from "@/components/InfoMessage";
|
import InfoMessage from "@/components/InfoMessage";
|
||||||
import { clearMessage } from "@/components/InfoMessage";
|
import { clearMessage } from "@/components/InfoMessage";
|
||||||
import { TextAreaField } from "@/components/form/TextAreaField";
|
import { TextAreaField } from "@/components/form/TextAreaField";
|
||||||
@@ -77,13 +78,9 @@ const ContactForm = () => {
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<Button disabled={form.formState.isSubmitting} type="submit">
|
||||||
disabled={form.formState.isSubmitting}
|
|
||||||
type="submit"
|
|
||||||
className="rounded-lg bg-primary-600 px-5 py-3 text-center text-sm font-medium text-white hover:bg-primary-800 focus:outline-none focus:ring-4 focus:ring-primary-300 disabled:opacity-25 dark:text-white dark:hover:bg-primary-700 dark:focus:ring-primary-800 sm:w-fit"
|
|
||||||
>
|
|
||||||
{form.formState.isSubmitting ? t("sending") : t("sendButton")}
|
{form.formState.isSubmitting ? t("sending") : t("sendButton")}
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<InfoMessage responseMessage={responseMessage} />
|
<InfoMessage responseMessage={responseMessage} />
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useState } from "react";
|
|||||||
import { signOut } from "next-auth/react";
|
import { signOut } from "next-auth/react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
import { Button } from "@/components/Button";
|
||||||
import InfoMessage from "@/components/InfoMessage";
|
import InfoMessage from "@/components/InfoMessage";
|
||||||
import { deleteAccount } from "@/server/deleteAccount";
|
import { deleteAccount } from "@/server/deleteAccount";
|
||||||
import { useRouter } from "@/utils/routing";
|
import { useRouter } from "@/utils/routing";
|
||||||
@@ -67,21 +68,21 @@ export default function Contact() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center justify-center space-x-4 text-sm font-bold">
|
<div className="flex items-center justify-center space-x-4 text-sm font-bold">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
className="rounded-lg border border-primary px-5 py-3 text-center text-xs text-primary hover:bg-primary hover:text-white focus:outline-none focus:ring-4 focus:ring-primary-300 disabled:opacity-25 dark:text-white dark:focus:ring-primary-800 sm:w-fit"
|
intent="secondary"
|
||||||
>
|
>
|
||||||
{at("cancelButton")}
|
{at("cancelButton")}
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
onClick={() => onDeleteAccount()}
|
onClick={() => onDeleteAccount()}
|
||||||
disabled={deleting}
|
disabled={deleting}
|
||||||
className="rounded-lg bg-primary-600 px-5 py-3 text-center text-sm font-medium text-white hover:bg-primary-800 focus:outline-none focus:ring-4 focus:ring-primary-300 disabled:opacity-25 dark:text-white dark:hover:bg-primary-700 dark:focus:ring-primary-800 sm:w-fit"
|
intent="primary"
|
||||||
>
|
>
|
||||||
{t("deleteButton")}
|
{t("deleteButton")}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { IoAdd, IoCloseOutline } from "react-icons/io5";
|
|||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { Button } from "@/components/Button";
|
||||||
import { RadioGroupField } from "@/components/form/RadioGroupField";
|
import { RadioGroupField } from "@/components/form/RadioGroupField";
|
||||||
import { SelectField } from "@/components/form/SelectField";
|
import { SelectField } from "@/components/form/SelectField";
|
||||||
import { TextField } from "@/components/form/TextField";
|
import { TextField } from "@/components/form/TextField";
|
||||||
@@ -176,26 +177,31 @@ export const ManualEloForm = () => {
|
|||||||
}))}
|
}))}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{gameFields.length > 1 && (
|
{gameFields.length > 1 && (
|
||||||
<button
|
<Button
|
||||||
className="hidden h-8 w-8 items-center justify-center rounded-md bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-600 dark:hover:bg-neutral-700 sm:flex"
|
intent="tertiary"
|
||||||
|
size="compacted"
|
||||||
|
className="hidden h-8 w-8 sm:flex"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => removeGame(i)}
|
onClick={() => removeGame(i)}
|
||||||
>
|
>
|
||||||
<IoCloseOutline className="h-6 w-6 text-gray-900 transition-all duration-200 hover:text-primary dark:text-neutral-400 hover:dark:text-white" />
|
<IoCloseOutline className="h-6 w-6" />
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{gameFields.length > 1 && (
|
{gameFields.length > 1 && (
|
||||||
<button
|
<Button
|
||||||
className="flex h-8 w-8 items-center justify-center rounded-md bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-600 dark:hover:bg-neutral-700 sm:hidden"
|
intent="tertiary"
|
||||||
|
size="compacted"
|
||||||
|
className="flex h-8 w-8 sm:hidden"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => removeGame(i)}
|
onClick={() => removeGame(i)}
|
||||||
>
|
>
|
||||||
<IoCloseOutline className="h-6 w-6 text-gray-900 transition-all duration-200 hover:text-primary dark:text-neutral-400 hover:dark:text-white" />
|
<IoCloseOutline className="h-6 w-6" />
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -244,26 +250,26 @@ export const ManualEloForm = () => {
|
|||||||
|
|
||||||
<div className="mt-8 flex justify-end gap-4">
|
<div className="mt-8 flex justify-end gap-4">
|
||||||
{!isDefault && (
|
{!isDefault && (
|
||||||
<button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
form.reset();
|
form.reset();
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
className="px-5 py-3 text-center text-sm font-medium text-primary-600 hover:text-primary-800 focus:outline-none focus:ring-4 focus:ring-primary-300 disabled:opacity-25 dark:text-white dark:hover:text-primary-700 dark:focus:ring-primary-800 sm:w-fit"
|
intent="secondary"
|
||||||
>
|
>
|
||||||
{t("clearButton")}
|
{t("clearButton")}
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
appendGame({});
|
appendGame({});
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
className="rounded-lg bg-primary-600 px-5 py-3 text-center text-sm font-medium text-white hover:bg-primary-800 focus:outline-none focus:ring-4 focus:ring-primary-300 disabled:opacity-25 dark:text-white dark:hover:bg-primary-700 dark:focus:ring-primary-800 sm:w-fit"
|
intent="primary"
|
||||||
>
|
>
|
||||||
<IoAdd className="mr-2 inline-block h-5 w-5" />
|
<IoAdd className="mr-2 inline-block h-5 w-5" />
|
||||||
{t("addGameButton")}
|
{t("addGameButton")}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|||||||
import { FormProvider, useForm } from "react-hook-form";
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { Button } from "@/components/Button";
|
||||||
import { ErrorBox } from "@/components/ErrorBox";
|
import { ErrorBox } from "@/components/ErrorBox";
|
||||||
import { Spinner } from "@/components/Spinner";
|
import { Spinner } from "@/components/Spinner";
|
||||||
import { TranslatedError } from "@/components/TranslatedError";
|
import { TranslatedError } from "@/components/TranslatedError";
|
||||||
@@ -176,7 +177,7 @@ export default function Elo() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="grid place-items-center bg-white pb-20 dark:bg-gray-800">
|
<section className="grid place-items-center bg-white pb-20 dark:bg-gray-800">
|
||||||
<div className="max-w-xl px-4 py-8 lg:py-16">
|
<div className="max-w-xxl px-4 py-8 lg:py-16">
|
||||||
<h2
|
<h2
|
||||||
className="mb-10 text-center text-4xl font-extrabold tracking-tight text-gray-900 dark:text-white"
|
className="mb-10 text-center text-4xl font-extrabold tracking-tight text-gray-900 dark:text-white"
|
||||||
data-test="header2"
|
data-test="header2"
|
||||||
@@ -189,13 +190,14 @@ export default function Elo() {
|
|||||||
|
|
||||||
{hasTournamentId && !isFetching && !error && (
|
{hasTournamentId && !isFetching && !error && (
|
||||||
<div className="mx-auto mb-8 flex justify-center">
|
<div className="mx-auto mb-8 flex justify-center">
|
||||||
<button
|
<Button
|
||||||
|
intent="secondary"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={clearForm}
|
onClick={clearForm}
|
||||||
className="rounded-lg border border-primary px-3 py-2 text-center text-sm text-primary hover:bg-primary hover:text-white focus:outline-none focus:ring-4 focus:ring-primary-300 disabled:opacity-25 dark:text-white dark:focus:ring-primary-800 sm:w-fit"
|
className="rounded-lg border border-primary px-3 py-2 text-center text-sm text-primary hover:bg-primary hover:text-white focus:outline-none focus:ring-4 focus:ring-primary-300 disabled:opacity-25 dark:text-white dark:focus:ring-primary-800 sm:w-fit"
|
||||||
>
|
>
|
||||||
{t("enterManualResultsButton")}
|
{t("enterManualResultsButton")}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ const TournamentTable = () => {
|
|||||||
classNameOverrides={{
|
classNameOverrides={{
|
||||||
container: () => "flex flex-1",
|
container: () => "flex flex-1",
|
||||||
}}
|
}}
|
||||||
|
size="small"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useTranslations } from "next-intl";
|
|||||||
import { FormProvider, useForm } from "react-hook-form";
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { Button } from "@/components/Button";
|
||||||
import { InlineSwitchField } from "@/components/form/InlineSwitchField";
|
import { InlineSwitchField } from "@/components/form/InlineSwitchField";
|
||||||
import { Label } from "@/components/form/Label";
|
import { Label } from "@/components/form/Label";
|
||||||
import { TextField } from "@/components/form/TextField";
|
import { TextField } from "@/components/form/TextField";
|
||||||
@@ -110,20 +111,13 @@ export const ZoneForm = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end gap-4">
|
<div className="flex justify-end gap-4">
|
||||||
<button
|
<Button intent="secondary" onClick={onCancel}>
|
||||||
onClick={onCancel}
|
|
||||||
className="rounded-lg border border-primary px-3 py-2 text-center text-xs text-primary hover:bg-primary hover:text-white focus:outline-none focus:ring-4 focus:ring-primary-300 disabled:opacity-25 dark:text-white dark:focus:ring-primary-800 sm:w-fit"
|
|
||||||
>
|
|
||||||
{cancelTitle ?? at("cancelButton")}
|
{cancelTitle ?? at("cancelButton")}
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button
|
<Button disabled={form.formState.isSubmitting} type="submit">
|
||||||
disabled={form.formState.isSubmitting}
|
|
||||||
type="submit"
|
|
||||||
className="rounded-lg bg-primary-600 px-5 py-3 text-center text-sm font-medium text-white hover:bg-primary-800 focus:outline-none focus:ring-4 focus:ring-primary-300 disabled:opacity-25 dark:text-white dark:hover:bg-primary-700 dark:focus:ring-primary-800 sm:w-fit"
|
|
||||||
>
|
|
||||||
{submitTitle ?? at("saveButton")}
|
{submitTitle ?? at("saveButton")}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useFormatter, useTranslations } from "next-intl";
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { IoAdd } from "react-icons/io5";
|
import { IoAdd } from "react-icons/io5";
|
||||||
|
|
||||||
|
import { Button, buttonVariants } from "@/components/Button";
|
||||||
import InfoMessage, { clearMessage } from "@/components/InfoMessage";
|
import InfoMessage, { clearMessage } from "@/components/InfoMessage";
|
||||||
import { Modal } from "@/components/Modal";
|
import { Modal } from "@/components/Modal";
|
||||||
import { Spinner } from "@/components/Spinner";
|
import { Spinner } from "@/components/Spinner";
|
||||||
@@ -116,17 +117,18 @@ const Zones = () => {
|
|||||||
pathname: "/zones/edit/[id]",
|
pathname: "/zones/edit/[id]",
|
||||||
params: { id: zone.id },
|
params: { id: zone.id },
|
||||||
}}
|
}}
|
||||||
className="rounded-lg border border-primary px-3 py-2 text-center text-xs text-primary hover:bg-primary hover:text-white focus:outline-none focus:ring-4 focus:ring-primary-300 disabled:opacity-25 dark:text-white dark:focus:ring-primary-800 sm:w-fit"
|
className={buttonVariants({ intent: "secondary" })}
|
||||||
>
|
>
|
||||||
{at("editButton")}
|
{at("editButton")}
|
||||||
</Link>
|
</Link>
|
||||||
<button
|
|
||||||
|
<Button
|
||||||
|
intent="secondary"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setDeletingZoneId(zone.id)}
|
onClick={() => setDeletingZoneId(zone.id)}
|
||||||
className="rounded-lg border border-primary px-3 py-2 text-center text-xs text-primary hover:bg-primary hover:text-white focus:outline-none focus:ring-4 focus:ring-primary-300 disabled:opacity-25 dark:text-white dark:focus:ring-primary-800 sm:w-fit"
|
|
||||||
>
|
>
|
||||||
{at("deleteButton")}
|
{at("deleteButton")}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -155,19 +157,15 @@ const Zones = () => {
|
|||||||
<InfoMessage responseMessage={responseMessage} />
|
<InfoMessage responseMessage={responseMessage} />
|
||||||
|
|
||||||
<div className="flex items-center justify-between space-x-4 text-sm font-bold">
|
<div className="flex items-center justify-between space-x-4 text-sm font-bold">
|
||||||
<button
|
<Button
|
||||||
|
intent="secondary"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setDeletingZoneId(null)}
|
onClick={() => setDeletingZoneId(null)}
|
||||||
className="rounded-lg border border-primary px-3 py-2 text-center text-xs text-primary hover:bg-primary hover:text-white focus:outline-none focus:ring-4 focus:ring-primary-300 disabled:opacity-25 dark:text-white dark:focus:ring-primary-800 sm:w-fit"
|
|
||||||
>
|
>
|
||||||
{at("cancelButton")}
|
{at("cancelButton")}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
|
||||||
onClick={() => onDelete()}
|
<Button onClick={() => onDelete()}>{at("deleteButton")}</Button>
|
||||||
className="rounded-lg bg-primary-600 px-5 py-3 text-center text-sm font-medium text-white hover:bg-primary-800 focus:outline-none focus:ring-4 focus:ring-primary-300 disabled:opacity-25 dark:text-white dark:hover:bg-primary-700 dark:focus:ring-primary-800 sm:w-fit"
|
|
||||||
>
|
|
||||||
{at("deleteButton")}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { Menu } from "@headlessui/react";
|
import { Button, Menu } from "@headlessui/react";
|
||||||
import { signIn, signOut, useSession } from "next-auth/react";
|
import { signIn, signOut, useSession } from "next-auth/react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { type VariantProps, cva } from "class-variance-authority";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export const buttonVariants = cva(
|
||||||
|
"flex items-center justify-center transition-all duration-200",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
intent: {
|
||||||
|
primary: [
|
||||||
|
"text-center",
|
||||||
|
"text-white",
|
||||||
|
"focus:outline-none focus:ring-2 focus:ring-primary-500",
|
||||||
|
"bg-primary-600",
|
||||||
|
"hover:bg-primary-800",
|
||||||
|
"disabled:opacity-25",
|
||||||
|
"dark:text-white dark:hover:bg-primary-700",
|
||||||
|
],
|
||||||
|
|
||||||
|
secondary: [
|
||||||
|
"text-primary dark:text-white",
|
||||||
|
"hover:text-white dark:hover:text-white",
|
||||||
|
"hover:bg-primary hover:text-white",
|
||||||
|
"border border-primary",
|
||||||
|
"focus:outline-none focus:ring-2 focus:ring-primary-500",
|
||||||
|
"disabled:opacity-25",
|
||||||
|
],
|
||||||
|
|
||||||
|
tertiary: [
|
||||||
|
"text-gray-900 dark:text-neutral-400",
|
||||||
|
"hover:text-primary dark:hover:text-white",
|
||||||
|
"focus:outline-none focus:ring-2 focus:ring-primary-500",
|
||||||
|
"disabled:opacity-25",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
small: ["text-sm font-medium", "px-3 py-2", "sm:w-fit", "rounded-lg"],
|
||||||
|
medium: ["text-sm font-medium", "px-5 py-3", "sm:w-fit", "rounded-lg"],
|
||||||
|
compacted: ["rounded-none", "px-0 py-0"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
intent: "primary",
|
||||||
|
size: "medium",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export type ButtonVariants = VariantProps<typeof buttonVariants>;
|
||||||
|
export type ButtonProps = React.ComponentProps<"button"> & ButtonVariants;
|
||||||
|
|
||||||
|
export const Button = ({
|
||||||
|
className,
|
||||||
|
intent,
|
||||||
|
size,
|
||||||
|
...props
|
||||||
|
}: ButtonProps & { className?: string }) => (
|
||||||
|
<button
|
||||||
|
className={twMerge(buttonVariants({ intent, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { Float } from "@headlessui-float/react";
|
import { Float } from "@headlessui-float/react";
|
||||||
import { Menu } from "@headlessui/react";
|
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
||||||
import { IoMdMore } from "react-icons/io";
|
import { IoMdMore } from "react-icons/io";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
@@ -37,15 +37,15 @@ export const DropdownMenu = ({
|
|||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
{buttonComponent ?? (
|
{buttonComponent ?? (
|
||||||
<Menu.Button>
|
<MenuButton>
|
||||||
<IoMdMore />
|
<IoMdMore />
|
||||||
</Menu.Button>
|
</MenuButton>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Menu.Items className="z-20 flex w-auto items-start overflow-hidden rounded-md bg-neutral-200 focus:outline-none dark:bg-neutral-600">
|
<MenuItems className="z-20 flex w-auto items-start overflow-hidden rounded-md bg-neutral-200 focus:outline-none dark:bg-neutral-600">
|
||||||
<div className="flex flex-col px-1 py-1">
|
<div className="flex flex-col px-1 py-1">
|
||||||
{items.map(({ title, onClick, className, disabled }, i) => (
|
{items.map(({ title, onClick, className, disabled }, i) => (
|
||||||
<Menu.Item key={i}>
|
<MenuItem key={i}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
@@ -61,10 +61,10 @@ export const DropdownMenu = ({
|
|||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</button>
|
</button>
|
||||||
</Menu.Item>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Menu.Items>
|
</MenuItems>
|
||||||
</Float>
|
</Float>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import MarkerClusterGroup from "react-leaflet-cluster";
|
|||||||
import { debouncedHoveredListIdAtom, mapBoundsAtom } from "@/atoms";
|
import { debouncedHoveredListIdAtom, mapBoundsAtom } from "@/atoms";
|
||||||
import { MapEvents } from "@/components/MapEvents";
|
import { MapEvents } from "@/components/MapEvents";
|
||||||
|
|
||||||
|
import { Button } from "./Button";
|
||||||
|
|
||||||
export type MarkerRef = {
|
export type MarkerRef = {
|
||||||
getMarker: () => L.Marker<any>;
|
getMarker: () => L.Marker<any>;
|
||||||
bounce: () => void;
|
bounce: () => void;
|
||||||
@@ -238,12 +240,14 @@ export const Map = ({
|
|||||||
</MapContainer>
|
</MapContainer>
|
||||||
|
|
||||||
<div className="flex items-center justify-center lg:hidden">
|
<div className="flex items-center justify-center lg:hidden">
|
||||||
<button
|
<Button
|
||||||
className="p-3 text-primary-900 dark:text-white"
|
intent="tertiary"
|
||||||
|
size="compacted"
|
||||||
|
className="p-3"
|
||||||
onClick={onScrollToTable}
|
onClick={onScrollToTable}
|
||||||
>
|
>
|
||||||
<FaAngleDoubleDown />
|
<FaAngleDoubleDown />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import { twMerge } from "tailwind-merge";
|
|||||||
|
|
||||||
import { searchStringAtom } from "@/atoms";
|
import { searchStringAtom } from "@/atoms";
|
||||||
|
|
||||||
|
import { Button } from "./Button";
|
||||||
|
import { Input } from "./form/Input";
|
||||||
|
|
||||||
type SearchBarProps = {
|
type SearchBarProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
@@ -27,38 +30,38 @@ const SearchBar = ({ className }: SearchBarProps) => {
|
|||||||
<label htmlFor="table-search" className="sr-only">
|
<label htmlFor="table-search" className="sr-only">
|
||||||
{t("searchLabel")}
|
{t("searchLabel")}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
<Input
|
||||||
<svg
|
|
||||||
className="h-5 w-5 text-gray-500 dark:text-gray-400"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{searchString !== "" && (
|
|
||||||
<div className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-900 dark:text-white">
|
|
||||||
<button onClick={() => setSearchString("")}>
|
|
||||||
<IoCloseOutline className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="search"
|
type="search"
|
||||||
id="table-search"
|
id="table-search"
|
||||||
className={twMerge(
|
|
||||||
"block rounded-lg border border-gray-300 bg-gray-50 p-2.5 px-10 text-gray-900 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-primary-500",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
placeholder={t("searchPlaceholder")}
|
placeholder={t("searchPlaceholder")}
|
||||||
|
size="small"
|
||||||
|
startIcon={
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5 text-gray-500 dark:text-gray-400"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
endIcon={
|
||||||
|
searchString !== "" && (
|
||||||
|
<Button
|
||||||
|
intent="tertiary"
|
||||||
|
size="compacted"
|
||||||
|
onClick={() => setSearchString("")}
|
||||||
|
>
|
||||||
|
<IoCloseOutline className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
value={searchString}
|
value={searchString}
|
||||||
onChange={(e) => updateSearchString(e.target.value)}
|
onChange={(e) => updateSearchString(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@headlessui/react";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { signIn } from "next-auth/react";
|
import { signIn } from "next-auth/react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
@@ -91,13 +92,9 @@ export const SignInForm = ({ callbackPath }: SignInFormProps) => {
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<Button disabled={form.formState.isSubmitting} type="submit">
|
||||||
disabled={form.formState.isSubmitting}
|
|
||||||
type="submit"
|
|
||||||
className="rounded-lg bg-primary-600 px-5 py-3 text-center text-sm font-medium text-white hover:bg-primary-800 focus:outline-none focus:ring-4 focus:ring-primary-300 disabled:opacity-25 dark:text-white dark:hover:bg-primary-700 dark:focus:ring-primary-800 sm:w-fit"
|
|
||||||
>
|
|
||||||
{t("signInButton")}
|
{t("signInButton")}
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<InfoMessage
|
<InfoMessage
|
||||||
className="text-center"
|
className="text-center"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import AsyncSelect from "react-select/async";
|
|||||||
import { Prettify } from "@/types";
|
import { Prettify } from "@/types";
|
||||||
|
|
||||||
import { Field, GenericFieldProps } from "./Field";
|
import { Field, GenericFieldProps } from "./Field";
|
||||||
import { BaseOption, classNames } from "./Select";
|
import { BaseOption, createSelectClassNamesFn } from "./Select";
|
||||||
|
|
||||||
export type AsyncSelectFieldProps<
|
export type AsyncSelectFieldProps<
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
@@ -34,6 +34,7 @@ export type AsyncSelectFieldProps<
|
|||||||
loadOptions: (searchString?: string) => Promise<BaseOption<T, D>[]>;
|
loadOptions: (searchString?: string) => Promise<BaseOption<T, D>[]>;
|
||||||
onInformChange?: (data: BaseOption<T, D>[] | null) => void;
|
onInformChange?: (data: BaseOption<T, D>[] | null) => void;
|
||||||
separators?: boolean;
|
separators?: boolean;
|
||||||
|
size?: "small" | "medium";
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@@ -50,6 +51,7 @@ export const AsyncSelectField = <
|
|||||||
label,
|
label,
|
||||||
hideErrorMessage,
|
hideErrorMessage,
|
||||||
required,
|
required,
|
||||||
|
size,
|
||||||
|
|
||||||
loadOption,
|
loadOption,
|
||||||
loadOptions,
|
loadOptions,
|
||||||
@@ -132,6 +134,10 @@ export const AsyncSelectField = <
|
|||||||
return options;
|
return options;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const classNames = createSelectClassNamesFn<BaseOption<T, D>, IsMulti>({
|
||||||
|
size,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Field {...{ name, control, className, label, hideErrorMessage, required }}>
|
<Field {...{ name, control, className, label, hideErrorMessage, required }}>
|
||||||
<Controller
|
<Controller
|
||||||
@@ -184,10 +190,7 @@ export const AsyncSelectField = <
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
classNames={classNames<BaseOption<T, D>, IsMulti>(
|
classNames={classNames(hasError, separators ?? false)}
|
||||||
hasError,
|
|
||||||
separators ?? false,
|
|
||||||
)}
|
|
||||||
{...selectProps}
|
{...selectProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { ReactDatePickerCustomHeaderProps } from "react-datepicker";
|
|||||||
import { IoChevronBack, IoChevronForward } from "react-icons/io5";
|
import { IoChevronBack, IoChevronForward } from "react-icons/io5";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { Button } from "@/components/Button";
|
||||||
|
|
||||||
const chevronClasses = {
|
const chevronClasses = {
|
||||||
chevronRoot: "h-4 w-4 dark:text-white",
|
chevronRoot: "h-4 w-4 dark:text-white",
|
||||||
chevronClassDisabled: "!text-neutral-500",
|
chevronClassDisabled: "!text-neutral-500",
|
||||||
@@ -23,7 +25,9 @@ export const DatePickerCustomHeader = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-2 flex !w-full items-center justify-between border-b border-neutral-500 px-6 pb-4">
|
<div className="mb-2 flex !w-full items-center justify-between border-b border-neutral-500 px-6 pb-4">
|
||||||
<button
|
<Button
|
||||||
|
intent="tertiary"
|
||||||
|
size="compacted"
|
||||||
onClick={decreaseMonth}
|
onClick={decreaseMonth}
|
||||||
disabled={prevMonthButtonDisabled}
|
disabled={prevMonthButtonDisabled}
|
||||||
className={chevronClasses.chevronRoot}
|
className={chevronClasses.chevronRoot}
|
||||||
@@ -34,9 +38,10 @@ export const DatePickerCustomHeader = ({
|
|||||||
prevMonthButtonDisabled && chevronClasses.chevronClassDisabled,
|
prevMonthButtonDisabled && chevronClasses.chevronClassDisabled,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</button>
|
</Button>
|
||||||
{format(date, "LLLL yyyy", { locale: locale === "fr" ? fr : enGB })}
|
{format(date, "LLLL yyyy", { locale: locale === "fr" ? fr : enGB })}
|
||||||
<button
|
<Button
|
||||||
|
intent="tertiary"
|
||||||
onClick={increaseMonth}
|
onClick={increaseMonth}
|
||||||
disabled={nextMonthButtonDisabled}
|
disabled={nextMonthButtonDisabled}
|
||||||
className={chevronClasses.chevronRoot}
|
className={chevronClasses.chevronRoot}
|
||||||
@@ -47,7 +52,7 @@ export const DatePickerCustomHeader = ({
|
|||||||
nextMonthButtonDisabled && chevronClasses.chevronClassDisabled,
|
nextMonthButtonDisabled && chevronClasses.chevronClassDisabled,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { VariantProps, cva } from "class-variance-authority";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export const inputVariants = cva(
|
||||||
|
[
|
||||||
|
"flex w-full content-center border",
|
||||||
|
"transition-all duration-200",
|
||||||
|
"border-gray-300 bg-gray-50 text-gray-900 shadow-sm focus:border-primary-500 focus:ring-primary-500",
|
||||||
|
"dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-primary-500 dark:focus:ring-primary-500",
|
||||||
|
],
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
state: {
|
||||||
|
disabled: ["dark:bg-gray-50"],
|
||||||
|
|
||||||
|
error: ["!border-orange-700 focus:!border-orange-700"],
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
small: ["font-medium", "px-3 py-2", "rounded-lg"],
|
||||||
|
medium: ["font-medium", "p-3", "rounded-lg"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
size: "medium",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export type InputVariants = VariantProps<typeof inputVariants>;
|
||||||
|
export type InputProps = Omit<React.ComponentProps<"input">, "size"> &
|
||||||
|
Omit<InputVariants, "state"> & {
|
||||||
|
hasError?: boolean;
|
||||||
|
startIcon?: React.ReactNode;
|
||||||
|
endIcon?: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Input = ({
|
||||||
|
className,
|
||||||
|
hasError,
|
||||||
|
startIcon,
|
||||||
|
endIcon,
|
||||||
|
|
||||||
|
size,
|
||||||
|
|
||||||
|
disabled,
|
||||||
|
required,
|
||||||
|
...rest
|
||||||
|
}: InputProps) => {
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
{startIcon && (
|
||||||
|
<div className="absolute inset-y-0 left-0 flex items-center pl-3 text-gray-900 dark:text-white">
|
||||||
|
{startIcon}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<input
|
||||||
|
aria-required={required}
|
||||||
|
aria-invalid={hasError}
|
||||||
|
disabled={disabled}
|
||||||
|
className={twMerge(
|
||||||
|
inputVariants({
|
||||||
|
size,
|
||||||
|
state: hasError ? "error" : disabled ? "disabled" : undefined,
|
||||||
|
}),
|
||||||
|
startIcon && "pl-10",
|
||||||
|
endIcon && "pr-10",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{endIcon && (
|
||||||
|
<div className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-900 dark:text-white">
|
||||||
|
{endIcon}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -13,49 +13,57 @@ export type BaseOption<T = string, D = unknown> = {
|
|||||||
data?: D;
|
data?: D;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const classNames = <Option, IsMulti extends boolean = false>(
|
export const createSelectClassNamesFn =
|
||||||
hasError: boolean,
|
<Option, IsMulti extends boolean = false>({
|
||||||
separators: boolean,
|
size,
|
||||||
): ClassNamesConfig<Option, IsMulti, GroupBase<Option>> => ({
|
}: {
|
||||||
container: () => "w-full",
|
size?: "small" | "medium";
|
||||||
valueContainer: () => "text-gray-900 dark:text-white",
|
}) =>
|
||||||
indicatorsContainer: () => "flex items-center self-stretch shrink-0",
|
(
|
||||||
clearIndicator: () => "flex items-center pr-2 text-gray-900 dark:text-white",
|
hasError: boolean,
|
||||||
dropdownIndicator: () =>
|
separators: boolean,
|
||||||
"pointer-events-none flex items-center pr-2 text-gray-900 dark:text-white",
|
): ClassNamesConfig<Option, IsMulti, GroupBase<Option>> => ({
|
||||||
indicatorSeparator: () => "w-px text-gray-900 dark:text-white",
|
container: () => "w-full",
|
||||||
control: (state) =>
|
valueContainer: () => "text-gray-900 dark:text-white",
|
||||||
twMerge(
|
indicatorsContainer: () => "flex items-center self-stretch shrink-0",
|
||||||
"group flex w-full items-center justify-between rounded-lg border p-2.5",
|
clearIndicator: () =>
|
||||||
"border-gray-300 bg-gray-50 text-gray-900 focus-within:border-primary-500 focus-within:ring-primary-500",
|
"flex items-center pr-2 text-gray-900 dark:text-white",
|
||||||
"dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus-within:border-primary-500 dark:focus-within:ring-primary-500",
|
dropdownIndicator: () =>
|
||||||
|
"pointer-events-none flex items-center pr-2 text-gray-900 dark:text-white",
|
||||||
|
indicatorSeparator: () => "w-px text-gray-900 dark:text-white",
|
||||||
|
control: (state) =>
|
||||||
|
twMerge(
|
||||||
|
"group flex w-full items-center justify-between rounded-lg border p-3",
|
||||||
|
"border-gray-300 bg-gray-50 text-gray-900 focus-within:border-primary-500 focus-within:ring-primary-500",
|
||||||
|
"dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus-within:border-primary-500 dark:focus-within:ring-primary-500",
|
||||||
|
|
||||||
hasError && "ring-1 ring-error",
|
hasError && "ring-1 ring-error",
|
||||||
state.isDisabled && "cursor-not-allowed",
|
state.isDisabled && "cursor-not-allowed",
|
||||||
state.isFocused && "border-primary ring-1 ring-primary ring-opacity-50",
|
state.isFocused && "border-primary ring-1 ring-primary ring-opacity-50",
|
||||||
),
|
size === "small" && "p-2",
|
||||||
multiValue: () => "bg-fieldGray border rounded-lg flex space-x-1 pl-1 m-1",
|
),
|
||||||
multiValueLabel: () => "",
|
multiValue: () => "bg-fieldGray border rounded-lg flex space-x-1 pl-1 m-1",
|
||||||
multiValueRemove: () => "items-center px-1 hover:text-primary",
|
multiValueLabel: () => "",
|
||||||
placeholder: () =>
|
multiValueRemove: () => "items-center px-1 hover:text-primary",
|
||||||
"block truncate pr-2 placeholder-gray dark:placeholder-gray-400",
|
placeholder: () =>
|
||||||
menu: () =>
|
"block truncate pr-2 placeholder-gray dark:placeholder-gray-400",
|
||||||
twMerge(
|
menu: () =>
|
||||||
"!z-30 mt-2 rounded-lg border border-gray-200 bg-white p-1",
|
twMerge(
|
||||||
"border-gray-300 bg-gray-50 text-gray-900",
|
"!z-30 mt-2 rounded-lg border border-gray-200 bg-white p-1",
|
||||||
"dark:border-gray-600 dark:bg-gray-700 dark:text-white",
|
"border-gray-300 bg-gray-50 text-gray-900",
|
||||||
),
|
"dark:border-gray-600 dark:bg-gray-700 dark:text-white",
|
||||||
groupHeading: () => "mx-3 mt-2 mb-1 text-textGray text-sm uppercase",
|
),
|
||||||
option: ({ isFocused, isDisabled }) =>
|
groupHeading: () => "mx-3 mt-2 mb-1 text-textGray text-sm uppercase",
|
||||||
twMerge(
|
option: ({ isFocused, isDisabled }) =>
|
||||||
"px-3 py-2 hover:cursor-pointer",
|
twMerge(
|
||||||
separators && "border-b border-gray-200",
|
"px-3 py-2 hover:cursor-pointer",
|
||||||
isDisabled && "opacity-50",
|
separators && "border-b border-gray-200",
|
||||||
isFocused && "hover:bg-primary-500 hover:text-white",
|
isDisabled && "opacity-50",
|
||||||
),
|
isFocused && "hover:bg-primary-500 hover:text-white",
|
||||||
noOptionsMessage: () =>
|
),
|
||||||
"text-textGray p-2 bg-gray-50 border border-dashed border-gray-200 rounded-sm",
|
noOptionsMessage: () =>
|
||||||
});
|
"text-textGray p-2 bg-gray-50 border border-dashed border-gray-200 rounded-sm",
|
||||||
|
});
|
||||||
|
|
||||||
export type SelectProps<
|
export type SelectProps<
|
||||||
IsMulti extends boolean = false,
|
IsMulti extends boolean = false,
|
||||||
@@ -66,6 +74,7 @@ export type SelectProps<
|
|||||||
required?: boolean;
|
required?: boolean;
|
||||||
separators?: boolean;
|
separators?: boolean;
|
||||||
hasError?: boolean;
|
hasError?: boolean;
|
||||||
|
size?: "small" | "medium";
|
||||||
classNameOverrides?: ClassNamesConfig<
|
classNameOverrides?: ClassNamesConfig<
|
||||||
BaseOption<T, D>,
|
BaseOption<T, D>,
|
||||||
IsMulti,
|
IsMulti,
|
||||||
@@ -81,6 +90,7 @@ export const Select = <
|
|||||||
>({
|
>({
|
||||||
className,
|
className,
|
||||||
required,
|
required,
|
||||||
|
size,
|
||||||
|
|
||||||
placeholder,
|
placeholder,
|
||||||
separators,
|
separators,
|
||||||
@@ -90,6 +100,11 @@ export const Select = <
|
|||||||
...selectProps
|
...selectProps
|
||||||
}: SelectProps<IsMulti, T, D>) => {
|
}: SelectProps<IsMulti, T, D>) => {
|
||||||
const t = useTranslations("App");
|
const t = useTranslations("App");
|
||||||
|
|
||||||
|
const classNames = createSelectClassNamesFn<BaseOption<T, D>, IsMulti>({
|
||||||
|
size,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactSelect
|
<ReactSelect
|
||||||
options={options}
|
options={options}
|
||||||
@@ -105,10 +120,7 @@ export const Select = <
|
|||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
classNames={{
|
classNames={{
|
||||||
...classNames<BaseOption<T, D>, IsMulti>(
|
...classNames(hasError ?? false, separators ?? false),
|
||||||
hasError ?? false,
|
|
||||||
separators ?? false,
|
|
||||||
),
|
|
||||||
...(classNameOverrides ?? {}),
|
...(classNameOverrides ?? {}),
|
||||||
}}
|
}}
|
||||||
{...selectProps}
|
{...selectProps}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { twMerge } from "tailwind-merge";
|
|||||||
import { Prettify } from "@/types";
|
import { Prettify } from "@/types";
|
||||||
|
|
||||||
import { Field, GenericFieldProps } from "./Field";
|
import { Field, GenericFieldProps } from "./Field";
|
||||||
|
import { inputVariants } from "./Input";
|
||||||
|
|
||||||
type TextAreaFieldProps<
|
type TextAreaFieldProps<
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
@@ -72,33 +73,25 @@ export const TextAreaField = <
|
|||||||
control={control}
|
control={control}
|
||||||
name={name}
|
name={name}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<div className="relative">
|
<textarea
|
||||||
{startIcon && (
|
{...form.register(name, {
|
||||||
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
required,
|
||||||
{startIcon}
|
})}
|
||||||
</div>
|
aria-required={required}
|
||||||
|
aria-invalid={hasError}
|
||||||
|
disabled={disabled}
|
||||||
|
className={twMerge(
|
||||||
|
inputVariants({
|
||||||
|
state: hasError ? "error" : disabled ? "disabled" : undefined,
|
||||||
|
}),
|
||||||
|
className,
|
||||||
)}
|
)}
|
||||||
<textarea
|
{...rest}
|
||||||
{...form.register(name, {
|
onChange={(e) => {
|
||||||
required,
|
field.onChange(e);
|
||||||
})}
|
}}
|
||||||
aria-required={required}
|
value={field.value}
|
||||||
aria-invalid={hasError}
|
/>
|
||||||
disabled={disabled}
|
|
||||||
className={twMerge(
|
|
||||||
"flex w-full content-center rounded-lg border p-3",
|
|
||||||
"border-gray-300 bg-gray-50 text-gray-900 shadow-sm focus:border-primary-500 focus:ring-primary-500",
|
|
||||||
"dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-primary-500 dark:focus:ring-primary-500",
|
|
||||||
hasError && "!border-orange-700 focus:!border-orange-700",
|
|
||||||
disabled && "dark:bg-gray-50",
|
|
||||||
)}
|
|
||||||
{...rest}
|
|
||||||
onChange={(e) => {
|
|
||||||
field.onChange(e);
|
|
||||||
}}
|
|
||||||
value={field.value}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,13 +12,14 @@ import { twMerge } from "tailwind-merge";
|
|||||||
import { Prettify } from "@/types";
|
import { Prettify } from "@/types";
|
||||||
|
|
||||||
import { Field, GenericFieldProps } from "./Field";
|
import { Field, GenericFieldProps } from "./Field";
|
||||||
|
import { Input, InputProps } from "./Input";
|
||||||
|
|
||||||
type TextFieldProps<
|
type TextFieldProps<
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
> = Prettify<
|
> = Prettify<
|
||||||
GenericFieldProps<TFieldValues, TFieldName> &
|
GenericFieldProps<TFieldValues, TFieldName> &
|
||||||
Omit<React.HTMLProps<HTMLInputElement>, "ref" | "name"> & {
|
Omit<InputProps, "ref" | "name" | "size"> & {
|
||||||
handleChanged?: (props: { name: string }) => void;
|
handleChanged?: (props: { name: string }) => void;
|
||||||
startIcon?: React.ReactNode;
|
startIcon?: React.ReactNode;
|
||||||
}
|
}
|
||||||
@@ -89,39 +90,26 @@ export const TextField = <
|
|||||||
control={control}
|
control={control}
|
||||||
name={name}
|
name={name}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<div className="relative">
|
<Input
|
||||||
{startIcon && (
|
type={type}
|
||||||
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
{...form.register(name, {
|
||||||
{startIcon}
|
valueAsNumber: type === "number",
|
||||||
</div>
|
required,
|
||||||
)}
|
})}
|
||||||
<input
|
hasError={hasError}
|
||||||
type={type}
|
required={required}
|
||||||
{...form.register(name, {
|
disabled={disabled}
|
||||||
valueAsNumber: type === "number",
|
startIcon={startIcon}
|
||||||
required,
|
className={twMerge(startIcon && "pl-10")}
|
||||||
})}
|
{...rest}
|
||||||
aria-required={required}
|
onChange={(e) => {
|
||||||
aria-invalid={hasError}
|
field.onChange(output(e));
|
||||||
disabled={disabled}
|
}}
|
||||||
className={twMerge(
|
value={input(field.value)}
|
||||||
"flex w-full content-center rounded-lg border p-3",
|
onBlur={() => {
|
||||||
"border-gray-300 bg-gray-50 text-gray-900 shadow-sm focus:border-primary-500 focus:ring-primary-500",
|
if (handleChanged) handleChanged({ name });
|
||||||
"dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-primary-500 dark:focus:ring-primary-500",
|
}}
|
||||||
hasError && "!border-orange-700 focus:!border-orange-700",
|
/>
|
||||||
disabled && "dark:bg-gray-50",
|
|
||||||
startIcon && "pl-10",
|
|
||||||
)}
|
|
||||||
{...rest}
|
|
||||||
onChange={(e) => {
|
|
||||||
field.onChange(output(e));
|
|
||||||
}}
|
|
||||||
value={input(field.value)}
|
|
||||||
onBlur={() => {
|
|
||||||
if (handleChanged) handleChanged({ name });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
"signIn": "Se connecter",
|
"signIn": "Se connecter",
|
||||||
"signOut": "Se déconnecter",
|
"signOut": "Se déconnecter",
|
||||||
"myZones": "Vos Régions",
|
"myZones": "Vos Régions",
|
||||||
"deleteAccount": "Supprimer le compte"
|
"deleteAccount": "Supprimer mon compte"
|
||||||
},
|
},
|
||||||
|
|
||||||
"Footer": {
|
"Footer": {
|
||||||
@@ -263,9 +263,9 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"DeleteAccount": {
|
"DeleteAccount": {
|
||||||
"title": "Supprimer le compte",
|
"title": "Supprimer mon compte",
|
||||||
"info": "Êtes-vous sûr de vouloir supprimer votre compte\u00a0? Cette action est irréversible.",
|
"info": "Êtes-vous sûr de vouloir supprimer votre compte\u00a0? Cette action est irréversible.",
|
||||||
"deleteButton": "Supprimer le compte",
|
"deleteButton": "Supprimer mon compte",
|
||||||
"success": "Votre compte a été supprimé avec succès.",
|
"success": "Votre compte a été supprimé avec succès.",
|
||||||
"failure": "Oups, quelque chose s'est mal passé. Veuillez réessayer."
|
"failure": "Oups, quelque chose s'est mal passé. Veuillez réessayer."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2584,6 +2584,13 @@ chokidar@^3.5.3:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "~2.3.2"
|
fsevents "~2.3.2"
|
||||||
|
|
||||||
|
class-variance-authority@^0.7.0:
|
||||||
|
version "0.7.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/class-variance-authority/-/class-variance-authority-0.7.0.tgz#1c3134d634d80271b1837452b06d821915954522"
|
||||||
|
integrity sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==
|
||||||
|
dependencies:
|
||||||
|
clsx "2.0.0"
|
||||||
|
|
||||||
classnames@^2.2.6, classnames@^2.3.0:
|
classnames@^2.2.6, classnames@^2.3.0:
|
||||||
version "2.5.1"
|
version "2.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b"
|
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b"
|
||||||
@@ -2601,6 +2608,11 @@ client-only@0.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
|
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
|
||||||
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
||||||
|
|
||||||
|
clsx@2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b"
|
||||||
|
integrity sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==
|
||||||
|
|
||||||
clsx@^2.0.0, clsx@^2.1.1:
|
clsx@^2.0.0, clsx@^2.1.1:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
|
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
|
||||||
|
|||||||
Reference in New Issue
Block a user