Zone creation UI preparation

This commit is contained in:
Timothy Armes
2024-04-10 14:59:58 +02:00
parent 1aba39b0e9
commit 6a3b58f02d
12 changed files with 442 additions and 42 deletions
+47
View File
@@ -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>
);
};
+66
View File
@@ -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>
);
};
+88
View File
@@ -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='&copy; <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;
+77
View File
@@ -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>
);
};