mirror of
https://github.com/TheRealOwenRees/echecsfrance.git
synced 2026-07-23 04:26:57 +00:00
Zone creation UI preparation
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
"@headlessui/react": "^1.7.17",
|
"@headlessui/react": "^1.7.17",
|
||||||
"@headlessui/tailwindcss": "^0.2.0",
|
"@headlessui/tailwindcss": "^0.2.0",
|
||||||
"@hookform/resolvers": "^3.3.3",
|
"@hookform/resolvers": "^3.3.3",
|
||||||
|
"@infra-blocks/zod-utils": "^0.4.2",
|
||||||
"@kodingdotninja/use-tailwind-breakpoint": "^0.0.5",
|
"@kodingdotninja/use-tailwind-breakpoint": "^0.0.5",
|
||||||
"@next/bundle-analyzer": "^14.0.4",
|
"@next/bundle-analyzer": "^14.0.4",
|
||||||
"@tanstack/react-query": "^5.29.0",
|
"@tanstack/react-query": "^5.29.0",
|
||||||
@@ -23,10 +24,12 @@
|
|||||||
"@types/node": "^20.8.4",
|
"@types/node": "^20.8.4",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
|
"geojson": "^0.5.0",
|
||||||
"jotai": "^2.6.1",
|
"jotai": "^2.6.1",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"leaflet-defaulticon-compatibility": "^0.1.2",
|
"leaflet-defaulticon-compatibility": "^0.1.2",
|
||||||
"leaflet-gesture-handling": "^1.2.2",
|
"leaflet-gesture-handling": "^1.2.2",
|
||||||
|
"leaflet-draw": "^1.0.4",
|
||||||
"leaflet.markercluster": "^1.5.3",
|
"leaflet.markercluster": "^1.5.3",
|
||||||
"leaflet.smooth_marker_bouncing": "^3.0.3",
|
"leaflet.smooth_marker_bouncing": "^3.0.3",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
@@ -47,6 +50,7 @@
|
|||||||
"react-input-mask": "^2.0.4",
|
"react-input-mask": "^2.0.4",
|
||||||
"react-leaflet": "^4.2.1",
|
"react-leaflet": "^4.2.1",
|
||||||
"react-leaflet-cluster": "^2.1.0",
|
"react-leaflet-cluster": "^2.1.0",
|
||||||
|
"react-leaflet-draw": "^0.20.4",
|
||||||
"react-select": "^5.7.4",
|
"react-select": "^5.7.4",
|
||||||
"react-tooltip": "^5.18.0",
|
"react-tooltip": "^5.18.0",
|
||||||
"sharp": "^0.33.0",
|
"sharp": "^0.33.0",
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { InlineSwitchField } from "@/components/form/InlineSwitchField";
|
||||||
|
import { Label } from "@/components/form/Label";
|
||||||
|
import { TextField } from "@/components/form/TextField";
|
||||||
|
import { ZoneEditorField } from "@/components/form/ZoneEditorField";
|
||||||
|
import { zoneSchema } from "@/schemas";
|
||||||
|
import { TimeControl } from "@/types";
|
||||||
|
|
||||||
|
type ZoneFormValues = z.infer<typeof zoneSchema>;
|
||||||
|
|
||||||
|
export const ZoneForm = () => {
|
||||||
|
const t = useTranslations("Zones");
|
||||||
|
const at = useTranslations("App");
|
||||||
|
const form = useForm<ZoneFormValues>({
|
||||||
|
resolver: zodResolver(zoneSchema),
|
||||||
|
defaultValues: {
|
||||||
|
classicNotifications: false,
|
||||||
|
rapidNotifications: false,
|
||||||
|
blitzNotifications: false,
|
||||||
|
features: {
|
||||||
|
type: "FeatureCollection",
|
||||||
|
features: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (data: ZoneFormValues) => {
|
||||||
|
try {
|
||||||
|
console.log(data);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-screen-md px-4 py-8 lg:py-16">
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||||
|
<div className="grid grid-cols-3 items-start gap-6">
|
||||||
|
<div className="col-span-3">
|
||||||
|
<TextField
|
||||||
|
name="name"
|
||||||
|
control={form.control}
|
||||||
|
label={t("zoneNameLabel")}
|
||||||
|
placeholder={t("zoneNamePlaceholder")}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-span-3">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<Label>{t("notificationsLabel")}</Label>
|
||||||
|
<div className="font-light text-gray-500 dark:text-gray-400">
|
||||||
|
{t("notificationsInfo")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<InlineSwitchField
|
||||||
|
name="classicNotifications"
|
||||||
|
control={form.control}
|
||||||
|
label={at("timeControlEnum", { tc: TimeControl.Classic })}
|
||||||
|
/>
|
||||||
|
<InlineSwitchField
|
||||||
|
name="rapidNotifications"
|
||||||
|
control={form.control}
|
||||||
|
label={at("timeControlEnum", { tc: TimeControl.Rapid })}
|
||||||
|
/>
|
||||||
|
<InlineSwitchField
|
||||||
|
name="blitzNotifications"
|
||||||
|
control={form.control}
|
||||||
|
label={at("timeControlEnum", { tc: TimeControl.Blitz })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<section id="map" className="z-0 col-span-3 flex h-auto">
|
||||||
|
<ZoneEditorField name="features" control={form.control} />
|
||||||
|
</section>
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
{t("saveButton")}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FeatureCollection } from "geojson";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
import { ZoneForm } from "../components/ZoneForm";
|
||||||
|
|
||||||
|
type ZoneFormValues = {
|
||||||
|
features: FeatureCollection;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CreateZone = () => {
|
||||||
|
const t = useTranslations("Zones");
|
||||||
|
|
||||||
|
const onSubmit = async (data: ZoneFormValues) => {
|
||||||
|
try {
|
||||||
|
console.log(data);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-screen-md px-4 py-8 lg:py-16">
|
||||||
|
<ZoneForm />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateZone;
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { Switch, SwitchProps } from "@headlessui/react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export const InlineSwitch = (
|
||||||
|
props: {
|
||||||
|
label?: React.ReactNode;
|
||||||
|
tooltip?: string;
|
||||||
|
tooltipId?: string;
|
||||||
|
} & SwitchProps<"button">,
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
label,
|
||||||
|
tooltip,
|
||||||
|
tooltipId,
|
||||||
|
disabled,
|
||||||
|
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Switch.Group>
|
||||||
|
<div className="flex w-fit items-center">
|
||||||
|
<Switch
|
||||||
|
className={twMerge(
|
||||||
|
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus:outline-none",
|
||||||
|
"ui-not-checked:bg-neutral-300 ui-not-checked:hover:bg-neutral-400",
|
||||||
|
"ui-not-checked:dark:bg-neutral-500 ui-not-checked:dark:hover:bg-neutral-600",
|
||||||
|
"ui-checked:bg-primary-500 ui-checked:hover:bg-primary-600",
|
||||||
|
)}
|
||||||
|
disabled={disabled}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<span className="inline-block h-[14px] w-[14px] translate-x-[3px] transform rounded-full bg-white transition-transform ui-checked:translate-x-[19px]" />
|
||||||
|
</Switch>
|
||||||
|
{label && (
|
||||||
|
<>
|
||||||
|
<Switch.Label className="ml-2 text-gray-900 dark:text-gray-300">
|
||||||
|
<div className="flex items-center">{label}</div>
|
||||||
|
</Switch.Label>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Switch.Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { SwitchProps } from "@headlessui/react";
|
||||||
|
import { Controller, FieldPath, FieldValues } from "react-hook-form";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { Field, GenericFieldProps } from "@/components/form/Field";
|
||||||
|
|
||||||
|
import { InlineSwitch } from "./InlineSwitch";
|
||||||
|
|
||||||
|
type InlineSwitchFieldProps<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
> = GenericFieldProps<TFieldValues, TFieldName> &
|
||||||
|
Omit<SwitchProps<"button">, "name">;
|
||||||
|
|
||||||
|
export const InlineSwitchField = <
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
>(
|
||||||
|
props: InlineSwitchFieldProps<TFieldValues, TFieldName>,
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
control,
|
||||||
|
label,
|
||||||
|
disabled,
|
||||||
|
className,
|
||||||
|
labelClassName,
|
||||||
|
childrenWrapperClassName,
|
||||||
|
hideErrorMessage,
|
||||||
|
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
{...{
|
||||||
|
name,
|
||||||
|
control,
|
||||||
|
type: "checkbox",
|
||||||
|
disabled,
|
||||||
|
className,
|
||||||
|
labelClassName,
|
||||||
|
childrenWrapperClassName,
|
||||||
|
hideErrorMessage,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex w-full flex-col">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={name}
|
||||||
|
render={({ field }) => (
|
||||||
|
<InlineSwitch
|
||||||
|
checked={field.value || false}
|
||||||
|
onChange={field.onChange}
|
||||||
|
label={label}
|
||||||
|
disabled={disabled}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import type { FeatureCollection } from "geojson";
|
||||||
|
import L, { LatLngLiteral } from "leaflet";
|
||||||
|
import "leaflet-defaulticon-compatibility";
|
||||||
|
import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css";
|
||||||
|
import "leaflet-draw/dist/leaflet.draw.css";
|
||||||
|
import "leaflet/dist/leaflet.css";
|
||||||
|
import {
|
||||||
|
FeatureGroup,
|
||||||
|
MapContainer,
|
||||||
|
TileLayer,
|
||||||
|
ZoomControl,
|
||||||
|
} from "react-leaflet";
|
||||||
|
import { EditControl } from "react-leaflet-draw";
|
||||||
|
|
||||||
|
import MapEvents from "../MapEvents";
|
||||||
|
|
||||||
|
const center: LatLngLiteral = { lat: 47.0844, lng: 2.3964 };
|
||||||
|
|
||||||
|
export type ZoneEditorProps = {
|
||||||
|
value: FeatureCollection;
|
||||||
|
onChange: (geoJson: FeatureCollection) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ZoneEditor = ({ value, onChange }: ZoneEditorProps) => {
|
||||||
|
const ref = React.useRef<L.FeatureGroup>(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (ref.current?.getLayers().length === 0 && value) {
|
||||||
|
L.geoJSON(value).eachLayer((layer) => {
|
||||||
|
if (
|
||||||
|
layer instanceof L.Polyline ||
|
||||||
|
layer instanceof L.Polygon ||
|
||||||
|
layer instanceof L.Marker
|
||||||
|
) {
|
||||||
|
if (layer?.feature?.properties.radius && ref.current) {
|
||||||
|
new L.Circle(layer.feature.geometry.coordinates.slice().reverse(), {
|
||||||
|
radius: layer.feature?.properties.radius,
|
||||||
|
}).addTo(ref.current);
|
||||||
|
} else {
|
||||||
|
ref.current?.addLayer(layer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const handleChange = () => {
|
||||||
|
const geoJson = ref.current?.toGeoJSON();
|
||||||
|
if (geoJson?.type === "FeatureCollection") {
|
||||||
|
onChange(geoJson);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MapContainer
|
||||||
|
center={center}
|
||||||
|
zoom={5}
|
||||||
|
style={{ height: "600px", flexGrow: 1 }}
|
||||||
|
>
|
||||||
|
<MapEvents />
|
||||||
|
<TileLayer
|
||||||
|
attribution='© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
|
||||||
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FeatureGroup ref={ref}>
|
||||||
|
<EditControl
|
||||||
|
position="topright"
|
||||||
|
onEdited={handleChange}
|
||||||
|
onCreated={handleChange}
|
||||||
|
onDeleted={handleChange}
|
||||||
|
draw={{
|
||||||
|
rectangle: false,
|
||||||
|
circle: true,
|
||||||
|
polyline: false,
|
||||||
|
polygon: true,
|
||||||
|
marker: false,
|
||||||
|
circlemarker: false,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FeatureGroup>
|
||||||
|
</MapContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ZoneEditor;
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { FeatureCollection } from "geojson";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
FieldPath,
|
||||||
|
FieldValues,
|
||||||
|
useFormContext,
|
||||||
|
} from "react-hook-form";
|
||||||
|
|
||||||
|
import { Prettify } from "@/types";
|
||||||
|
|
||||||
|
import LoadingMap from "../LoadingMap";
|
||||||
|
|
||||||
|
import { Field, GenericFieldProps } from "./Field";
|
||||||
|
import type { ZoneEditorProps } from "./ZoneEditor";
|
||||||
|
|
||||||
|
const ZoneEditor = dynamic(() => import("./ZoneEditor"), {
|
||||||
|
ssr: false,
|
||||||
|
loading: LoadingMap,
|
||||||
|
});
|
||||||
|
|
||||||
|
type ZoneEditorFieldProps<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
> = Prettify<
|
||||||
|
GenericFieldProps<TFieldValues, TFieldName> &
|
||||||
|
Omit<ZoneEditorProps, "value" | "onChange">
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const ZoneEditorField = <
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
>(
|
||||||
|
props: ZoneEditorFieldProps<TFieldValues, TFieldName>,
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
control,
|
||||||
|
label,
|
||||||
|
className,
|
||||||
|
labelClassName,
|
||||||
|
|
||||||
|
childrenWrapperClassName,
|
||||||
|
hideErrorMessage,
|
||||||
|
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const form = useFormContext<TFieldValues>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
{...{
|
||||||
|
name,
|
||||||
|
control,
|
||||||
|
label,
|
||||||
|
className,
|
||||||
|
labelClassName,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex w-full flex-col">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={name}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<ZoneEditor
|
||||||
|
value={value ?? { type: "FeatureCollection", features: [] }}
|
||||||
|
onChange={(geoJson: FeatureCollection) => onChange(geoJson)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -101,9 +101,15 @@ input[type="number"] {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
width: 0px;
|
width: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker-popper[data-placement^="bottom"] {
|
.react-datepicker-popper[data-placement^="bottom"] {
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-calendar::-webkit-calendar-picker-indicator {
|
.no-calendar::-webkit-calendar-picker-indicator {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sr-only {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|||||||
+12
-3
@@ -39,7 +39,8 @@
|
|||||||
|
|
||||||
"FormValidation": {
|
"FormValidation": {
|
||||||
"required": "Required",
|
"required": "Required",
|
||||||
"url": "Invalid URL"
|
"url": "Invalid URL",
|
||||||
|
"zone": "Please draw at least one region on the map"
|
||||||
},
|
},
|
||||||
|
|
||||||
"Home": {
|
"Home": {
|
||||||
@@ -221,10 +222,18 @@
|
|||||||
"checkEmail": "Please check your email for a sign-in link.",
|
"checkEmail": "Please check your email for a sign-in link.",
|
||||||
|
|
||||||
"email": {
|
"email": {
|
||||||
"subject": "Sign in to ${host}",
|
"subject": "Sign in to {host}",
|
||||||
"button": "Sign in",
|
"button": "Sign in",
|
||||||
"ignore": "If you didn't request this, you can safely ignore this email.",
|
"ignore": "If you didn't request this, you can safely ignore this email.",
|
||||||
"textMessage": "Click the following link to sign in to ${host}: {url}\n\n"
|
"textMessage": "Click the following link to sign in to {host}: {url}\n\n"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"Zones": {
|
||||||
|
"zoneNameLabel": "Zone name",
|
||||||
|
"zoneNamePlaceholder": "Name for your zone",
|
||||||
|
"notificationsLabel": "Notifications",
|
||||||
|
"notificationsInfo": "You can choose to turn on notifications for the time controls that interest you, and will receive an email when a new tournament is added to this zone.",
|
||||||
|
"saveButton": "Save"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-3
@@ -39,7 +39,8 @@
|
|||||||
|
|
||||||
"FormValidation": {
|
"FormValidation": {
|
||||||
"required": "Requis",
|
"required": "Requis",
|
||||||
"url": "L'URL n'est pas valide"
|
"url": "L'URL n'est pas valide",
|
||||||
|
"zone": "Veuillez dessiner au moins une région sur la carte"
|
||||||
},
|
},
|
||||||
|
|
||||||
"Home": {
|
"Home": {
|
||||||
@@ -222,10 +223,18 @@
|
|||||||
"checkEmail": "Veuillez vérifier votre e-mail pour un lien de connexion.",
|
"checkEmail": "Veuillez vérifier votre e-mail pour un lien de connexion.",
|
||||||
|
|
||||||
"email": {
|
"email": {
|
||||||
"subject": "Connexion à ${host}",
|
"subject": "Connexion à {host}",
|
||||||
"button": "Se connecter",
|
"button": "Se connecter",
|
||||||
"ignore": "Si vous n'avez pas demandé cela, vous pouvez ignorer en toute sécurité cet e-mail.",
|
"ignore": "Si vous n'avez pas demandé cela, vous pouvez ignorer en toute sécurité cet e-mail.",
|
||||||
"textMessage": "Cliquez sur le lien suivant pour vous connecter à ${host}: {url}\n\n"
|
"textMessage": "Cliquez sur le lien suivant pour vous connecter à {host}: {url}\n\n"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"Zones": {
|
||||||
|
"zoneNameLabel": "Nom de la zone",
|
||||||
|
"zoneNamePlaceholder": "Votre nom pour la zone",
|
||||||
|
"notificationsLabel": "Notifications",
|
||||||
|
"notificationsInfo": "Vous pouvez choisir d'activer les notifications pour les cadences qui vous intéressent, et vous recevrez un e-mail lorsqu'un nouveau tournoi est ajouté dans cette zone.",
|
||||||
|
"saveButton": "Sauvegarder"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-35
@@ -1,42 +1,10 @@
|
|||||||
import { LatLngLiteral } from "leaflet";
|
import { LatLngLiteral } from "leaflet";
|
||||||
import { ObjectId } from "mongodb";
|
|
||||||
import { SafeAction } from "next-safe-action";
|
import { SafeAction } from "next-safe-action";
|
||||||
import { z } from "zod";
|
|
||||||
|
import { TournamentModel } from "./server/models/tournamentModel";
|
||||||
|
|
||||||
export type Status = "scheduled" | "ongoing" | "finished" | "in-play";
|
export type Status = "scheduled" | "ongoing" | "finished" | "in-play";
|
||||||
|
|
||||||
export const tournamentDataSchema = z.object({
|
|
||||||
tournament_id: z.string(),
|
|
||||||
town: z.string(),
|
|
||||||
department: z.string(),
|
|
||||||
country: z.string(),
|
|
||||||
tournament: z.string(),
|
|
||||||
url: z.string(),
|
|
||||||
time_control: z.string(),
|
|
||||||
norm_tournament: z.boolean().optional(),
|
|
||||||
date: z.string(),
|
|
||||||
start_date: z.string(),
|
|
||||||
end_date: z.string(),
|
|
||||||
coordinates: z.array(z.number()).min(2).max(2),
|
|
||||||
entry_method: z.enum(["manual", "auto"]),
|
|
||||||
pending: z.boolean().optional(),
|
|
||||||
status: z.enum(["scheduled", "ongoing", "finished", "in-play"]),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TournamentData = z.infer<typeof tournamentDataSchema> & {
|
|
||||||
_id: ObjectId;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ClubData = {
|
|
||||||
_id: ObjectId;
|
|
||||||
name: string;
|
|
||||||
url?: string;
|
|
||||||
address?: string;
|
|
||||||
email?: string;
|
|
||||||
website?: string;
|
|
||||||
coordinates: [number, number];
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum TimeControl {
|
export enum TimeControl {
|
||||||
Classic = "Classic",
|
Classic = "Classic",
|
||||||
Rapid = "Rapid",
|
Rapid = "Rapid",
|
||||||
@@ -60,7 +28,7 @@ export type Tournament = {
|
|||||||
status: Status;
|
status: Status;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const tcMap: Record<TournamentData["time_control"], TimeControl> = {
|
export const tcMap: Record<TournamentModel["time_control"], TimeControl> = {
|
||||||
"Cadence Lente": TimeControl.Classic,
|
"Cadence Lente": TimeControl.Classic,
|
||||||
Rapide: TimeControl.Rapid,
|
Rapide: TimeControl.Rapid,
|
||||||
Blitz: TimeControl.Blitz,
|
Blitz: TimeControl.Blitz,
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@
|
|||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
|
|||||||
Reference in New Issue
Block a user