mirror of
https://github.com/TheRealOwenRees/echecsfrance.git
synced 2026-07-23 12:36:57 +00:00
Zone creation UI preparation
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user