diff --git a/.vscode/settings.json b/.vscode/settings.json
index b74065b..fc9ff2a 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -53,5 +53,9 @@
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"css.lint.unknownAtRules": "ignore",
- "tailwindCSS.experimental.classRegex": ["[a-zA-Z]*ClassName=\"([^\"]+)\""]
+ "tailwindCSS.experimental.classRegex": [
+ ["[a-zA-Z]*ClassName=\"([^\"]+)\""],
+ ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
+ ["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
+ ]
}
diff --git a/package.json b/package.json
index 22cade2..80b747d 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
"@turf/boolean-point-in-polygon": "^7.0.0-alpha.114",
"@types/node": "^22.7.2",
"autoprefixer": "^10.4.16",
+ "class-variance-authority": "^0.7.0",
"date-fns": "^4.1.0",
"geojson": "^0.5.0",
"jotai": "^2.6.1",
diff --git a/src/app/[locale]/(main)/add-tournament/TournamentForm.tsx b/src/app/[locale]/(main)/add-tournament/TournamentForm.tsx
index 51e2b9c..d14a869 100644
--- a/src/app/[locale]/(main)/add-tournament/TournamentForm.tsx
+++ b/src/app/[locale]/(main)/add-tournament/TournamentForm.tsx
@@ -8,6 +8,7 @@ import dynamic from "next/dynamic";
import { FormProvider, useForm } from "react-hook-form";
import { z } from "zod";
+import { Button } from "@/components/Button";
import { clearMessage } from "@/components/InfoMessage";
import InfoMessage from "@/components/InfoMessage";
import LoadingMap from "@/components/LoadingMap";
@@ -174,12 +175,14 @@ const TournamentForm = () => {
required
/>
-
+
+
@@ -207,21 +210,16 @@ const TournamentForm = () => {
-
-
+
+
+
+
+
diff --git a/src/app/[locale]/(main)/contact-us/ContactForm.tsx b/src/app/[locale]/(main)/contact-us/ContactForm.tsx
index 963abe9..bbcd210 100644
--- a/src/app/[locale]/(main)/contact-us/ContactForm.tsx
+++ b/src/app/[locale]/(main)/contact-us/ContactForm.tsx
@@ -7,6 +7,7 @@ import { useTranslations } from "next-intl";
import { FormProvider, useForm } from "react-hook-form";
import { z } from "zod";
+import { Button } from "@/components/Button";
import InfoMessage from "@/components/InfoMessage";
import { clearMessage } from "@/components/InfoMessage";
import { TextAreaField } from "@/components/form/TextAreaField";
@@ -77,13 +78,9 @@ const ContactForm = () => {
required
/>
-
diff --git a/src/app/[locale]/(main)/delete-account/page.tsx b/src/app/[locale]/(main)/delete-account/page.tsx
index cb3405b..f390ffd 100644
--- a/src/app/[locale]/(main)/delete-account/page.tsx
+++ b/src/app/[locale]/(main)/delete-account/page.tsx
@@ -5,6 +5,7 @@ import { useState } from "react";
import { signOut } from "next-auth/react";
import { useTranslations } from "next-intl";
+import { Button } from "@/components/Button";
import InfoMessage from "@/components/InfoMessage";
import { deleteAccount } from "@/server/deleteAccount";
import { useRouter } from "@/utils/routing";
@@ -67,21 +68,21 @@ export default function Contact() {
)}
- 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")}
-
+
- onDeleteAccount()}
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")}
-
+
>
)}
diff --git a/src/app/[locale]/(main)/elo/ManualEloForm.tsx b/src/app/[locale]/(main)/elo/ManualEloForm.tsx
index 49a4378..6446f6f 100644
--- a/src/app/[locale]/(main)/elo/ManualEloForm.tsx
+++ b/src/app/[locale]/(main)/elo/ManualEloForm.tsx
@@ -8,6 +8,7 @@ import { IoAdd, IoCloseOutline } from "react-icons/io5";
import { twMerge } from "tailwind-merge";
import { z } from "zod";
+import { Button } from "@/components/Button";
import { RadioGroupField } from "@/components/form/RadioGroupField";
import { SelectField } from "@/components/form/SelectField";
import { TextField } from "@/components/form/TextField";
@@ -176,26 +177,31 @@ export const ManualEloForm = () => {
}))}
required
/>
+
{gameFields.length > 1 && (
- removeGame(i)}
>
-
-
+
+
)}
{gameFields.length > 1 && (
- removeGame(i)}
>
-
-
+
+
)}
@@ -244,26 +250,26 @@ export const ManualEloForm = () => {
{!isDefault && (
- {
form.reset();
}}
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")}
-
+
)}
- {
appendGame({});
}}
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"
>
{t("addGameButton")}
-
+
diff --git a/src/app/[locale]/(main)/elo/page.tsx b/src/app/[locale]/(main)/elo/page.tsx
index 35437fe..cda02c1 100644
--- a/src/app/[locale]/(main)/elo/page.tsx
+++ b/src/app/[locale]/(main)/elo/page.tsx
@@ -10,6 +10,7 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { FormProvider, useForm } from "react-hook-form";
import { z } from "zod";
+import { Button } from "@/components/Button";
import { ErrorBox } from "@/components/ErrorBox";
import { Spinner } from "@/components/Spinner";
import { TranslatedError } from "@/components/TranslatedError";
@@ -176,7 +177,7 @@ export default function Elo() {
return (
-
+
-
{t("enterManualResultsButton")}
-
+
)}
diff --git a/src/app/[locale]/(main)/tournaments/TournamentTable.tsx b/src/app/[locale]/(main)/tournaments/TournamentTable.tsx
index e8ecdf4..7044059 100644
--- a/src/app/[locale]/(main)/tournaments/TournamentTable.tsx
+++ b/src/app/[locale]/(main)/tournaments/TournamentTable.tsx
@@ -85,6 +85,7 @@ const TournamentTable = () => {
classNameOverrides={{
container: () => "flex flex-1",
}}
+ size="small"
/>
diff --git a/src/app/[locale]/(main)/zones/components/ZoneForm.tsx b/src/app/[locale]/(main)/zones/components/ZoneForm.tsx
index 0ea9a32..02ddf79 100644
--- a/src/app/[locale]/(main)/zones/components/ZoneForm.tsx
+++ b/src/app/[locale]/(main)/zones/components/ZoneForm.tsx
@@ -5,6 +5,7 @@ import { useTranslations } from "next-intl";
import { FormProvider, useForm } from "react-hook-form";
import { z } from "zod";
+import { Button } from "@/components/Button";
import { InlineSwitchField } from "@/components/form/InlineSwitchField";
import { Label } from "@/components/form/Label";
import { TextField } from "@/components/form/TextField";
@@ -110,20 +111,13 @@ export const ZoneForm = ({
-
+
{cancelTitle ?? at("cancelButton")}
-
+
-
+
{submitTitle ?? at("saveButton")}
-
+
diff --git a/src/app/[locale]/(main)/zones/page.tsx b/src/app/[locale]/(main)/zones/page.tsx
index 37b441e..cd5d7a4 100644
--- a/src/app/[locale]/(main)/zones/page.tsx
+++ b/src/app/[locale]/(main)/zones/page.tsx
@@ -6,6 +6,7 @@ import { useFormatter, useTranslations } from "next-intl";
import dynamic from "next/dynamic";
import { IoAdd } from "react-icons/io5";
+import { Button, buttonVariants } from "@/components/Button";
import InfoMessage, { clearMessage } from "@/components/InfoMessage";
import { Modal } from "@/components/Modal";
import { Spinner } from "@/components/Spinner";
@@ -116,17 +117,18 @@ const Zones = () => {
pathname: "/zones/edit/[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")}
- 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")}
-
+
@@ -155,19 +157,15 @@ const Zones = () => {
- 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")}
-
- onDelete()}
- 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")}
-
+
+
+ onDelete()}>{at("deleteButton")}
>
diff --git a/src/components/AuthButton.tsx b/src/components/AuthButton.tsx
index a95d7f7..ab1fcab 100644
--- a/src/components/AuthButton.tsx
+++ b/src/components/AuthButton.tsx
@@ -2,7 +2,7 @@
import { useState } from "react";
-import { Menu } from "@headlessui/react";
+import { Button, Menu } from "@headlessui/react";
import { signIn, signOut, useSession } from "next-auth/react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
diff --git a/src/components/Button.tsx b/src/components/Button.tsx
new file mode 100644
index 0000000..489fdcf
--- /dev/null
+++ b/src/components/Button.tsx
@@ -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;
+export type ButtonProps = React.ComponentProps<"button"> & ButtonVariants;
+
+export const Button = ({
+ className,
+ intent,
+ size,
+ ...props
+}: ButtonProps & { className?: string }) => (
+
+);
diff --git a/src/components/DropdownMenu.tsx b/src/components/DropdownMenu.tsx
index 157e623..98e3681 100644
--- a/src/components/DropdownMenu.tsx
+++ b/src/components/DropdownMenu.tsx
@@ -1,7 +1,7 @@
import React from "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 { twMerge } from "tailwind-merge";
@@ -37,15 +37,15 @@ export const DropdownMenu = ({
leaveTo="transform opacity-0 scale-95"
>
{buttonComponent ?? (
-
+
-
+
)}
-
+
{items.map(({ title, onClick, className, disabled }, i) => (
-
+
+
))}
-
+
);
diff --git a/src/components/Map.tsx b/src/components/Map.tsx
index b1cd651..d1909f3 100644
--- a/src/components/Map.tsx
+++ b/src/components/Map.tsx
@@ -16,6 +16,8 @@ import MarkerClusterGroup from "react-leaflet-cluster";
import { debouncedHoveredListIdAtom, mapBoundsAtom } from "@/atoms";
import { MapEvents } from "@/components/MapEvents";
+import { Button } from "./Button";
+
export type MarkerRef = {
getMarker: () => L.Marker;
bounce: () => void;
@@ -238,12 +240,14 @@ export const Map = ({
-
-
+
);
diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx
index f99d57b..c9e5309 100644
--- a/src/components/SearchBar.tsx
+++ b/src/components/SearchBar.tsx
@@ -7,6 +7,9 @@ import { twMerge } from "tailwind-merge";
import { searchStringAtom } from "@/atoms";
+import { Button } from "./Button";
+import { Input } from "./form/Input";
+
type SearchBarProps = {
className?: string;
};
@@ -27,38 +30,38 @@ const SearchBar = ({ className }: SearchBarProps) => {
+
-
-
- {searchString !== "" && (
-
- setSearchString("")}>
-
-
-
- )}
-
-
+
+
+ }
+ endIcon={
+ searchString !== "" && (
+
setSearchString("")}
+ >
+
+
+ )
+ }
value={searchString}
onChange={(e) => updateSearchString(e.target.value)}
/>
diff --git a/src/components/SignInForm.tsx b/src/components/SignInForm.tsx
index 81250e3..ab4cf78 100644
--- a/src/components/SignInForm.tsx
+++ b/src/components/SignInForm.tsx
@@ -2,6 +2,7 @@
import { useEffect, useState } from "react";
+import { Button } from "@headlessui/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { signIn } from "next-auth/react";
import { useTranslations } from "next-intl";
@@ -91,13 +92,9 @@ export const SignInForm = ({ callbackPath }: SignInFormProps) => {
required
/>
-
+
{t("signInButton")}
-
+
Promise[]>;
onInformChange?: (data: BaseOption[] | null) => void;
separators?: boolean;
+ size?: "small" | "medium";
}
>;
@@ -50,6 +51,7 @@ export const AsyncSelectField = <
label,
hideErrorMessage,
required,
+ size,
loadOption,
loadOptions,
@@ -132,6 +134,10 @@ export const AsyncSelectField = <
return options;
};
+ const classNames = createSelectClassNamesFn, IsMulti>({
+ size,
+ });
+
return (
, IsMulti>(
- hasError,
- separators ?? false,
- )}
+ classNames={classNames(hasError, separators ?? false)}
{...selectProps}
/>
);
diff --git a/src/components/form/DateField/components/DatePickerCustomHeader.tsx b/src/components/form/DateField/components/DatePickerCustomHeader.tsx
index 3aa93ad..96da1d1 100644
--- a/src/components/form/DateField/components/DatePickerCustomHeader.tsx
+++ b/src/components/form/DateField/components/DatePickerCustomHeader.tsx
@@ -7,6 +7,8 @@ import { ReactDatePickerCustomHeaderProps } from "react-datepicker";
import { IoChevronBack, IoChevronForward } from "react-icons/io5";
import { twMerge } from "tailwind-merge";
+import { Button } from "@/components/Button";
+
const chevronClasses = {
chevronRoot: "h-4 w-4 dark:text-white",
chevronClassDisabled: "!text-neutral-500",
@@ -23,7 +25,9 @@ export const DatePickerCustomHeader = ({
return (
-
-
+
{format(date, "LLLL yyyy", { locale: locale === "fr" ? fr : enGB })}
-
-
+
);
};
diff --git a/src/components/form/Input.tsx b/src/components/form/Input.tsx
new file mode 100644
index 0000000..aa75f26
--- /dev/null
+++ b/src/components/form/Input.tsx
@@ -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;
+export type InputProps = Omit, "size"> &
+ Omit & {
+ hasError?: boolean;
+ startIcon?: React.ReactNode;
+ endIcon?: React.ReactNode;
+ };
+
+export const Input = ({
+ className,
+ hasError,
+ startIcon,
+ endIcon,
+
+ size,
+
+ disabled,
+ required,
+ ...rest
+}: InputProps) => {
+ return (
+
+ {startIcon && (
+
+ {startIcon}
+
+ )}
+
+
+
+ {endIcon && (
+
+ {endIcon}
+
+ )}
+
+ );
+};
diff --git a/src/components/form/Select.tsx b/src/components/form/Select.tsx
index f9052d4..93c8fad 100644
--- a/src/components/form/Select.tsx
+++ b/src/components/form/Select.tsx
@@ -13,49 +13,57 @@ export type BaseOption = {
data?: D;
};
-export const classNames =
diff --git a/src/components/form/TextField.tsx b/src/components/form/TextField.tsx
index 24e87dd..affcbbd 100644
--- a/src/components/form/TextField.tsx
+++ b/src/components/form/TextField.tsx
@@ -12,13 +12,14 @@ import { twMerge } from "tailwind-merge";
import { Prettify } from "@/types";
import { Field, GenericFieldProps } from "./Field";
+import { Input, InputProps } from "./Input";
type TextFieldProps<
TFieldValues extends FieldValues = FieldValues,
TFieldName extends FieldPath = FieldPath,
> = Prettify<
GenericFieldProps &
- Omit, "ref" | "name"> & {
+ Omit & {
handleChanged?: (props: { name: string }) => void;
startIcon?: React.ReactNode;
}
@@ -89,39 +90,26 @@ export const TextField = <
control={control}
name={name}
render={({ field }) => (
-
- {startIcon && (
-
- {startIcon}
-
- )}
-
{
- field.onChange(output(e));
- }}
- value={input(field.value)}
- onBlur={() => {
- if (handleChanged) handleChanged({ name });
- }}
- />
-
+ {
+ field.onChange(output(e));
+ }}
+ value={input(field.value)}
+ onBlur={() => {
+ if (handleChanged) handleChanged({ name });
+ }}
+ />
)}
/>
diff --git a/src/messages/fr.json b/src/messages/fr.json
index c54cd2d..003588f 100644
--- a/src/messages/fr.json
+++ b/src/messages/fr.json
@@ -37,7 +37,7 @@
"signIn": "Se connecter",
"signOut": "Se déconnecter",
"myZones": "Vos Régions",
- "deleteAccount": "Supprimer le compte"
+ "deleteAccount": "Supprimer mon compte"
},
"Footer": {
@@ -263,9 +263,9 @@
},
"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.",
- "deleteButton": "Supprimer le compte",
+ "deleteButton": "Supprimer mon compte",
"success": "Votre compte a été supprimé avec succès.",
"failure": "Oups, quelque chose s'est mal passé. Veuillez réessayer."
},
diff --git a/yarn.lock b/yarn.lock
index 58f28fe..1269cb6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2584,6 +2584,13 @@ chokidar@^3.5.3:
optionalDependencies:
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:
version "2.5.1"
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"
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:
version "2.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"