Filtering by zone

This commit is contained in:
Timothy Armes
2024-04-12 18:55:10 +02:00
parent 45ea8960e0
commit d44110649b
17 changed files with 336 additions and 136 deletions
+1
View File
@@ -22,6 +22,7 @@
"@next/bundle-analyzer": "^14.0.4",
"@tanstack/react-query": "^5.29.0",
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
"@turf/boolean-point-in-polygon": "^7.0.0-alpha.114",
"@types/node": "^20.8.4",
"autoprefixer": "^10.4.16",
"date-fns": "^2.30.0",
+3 -5
View File
@@ -1,22 +1,20 @@
"use client";
import { useCallback, useMemo } from "react";
import { useMemo } from "react";
import { useAtomValue } from "jotai";
import L, { LatLngLiteral } from "leaflet";
import "leaflet-defaulticon-compatibility";
import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css";
import "leaflet.smooth_marker_bouncing";
import "leaflet/dist/leaflet.css";
import { countBy, groupBy } from "lodash";
import { clubsAtom } from "@/atoms";
import { filteredClubsByZoneAtom } from "@/atoms";
import { Map, MapMarker } from "@/components/Map";
import { ClubMarker } from "./ClubMarker";
const ClubMap = () => {
const clubs = useAtomValue(clubsAtom);
const clubs = useAtomValue(filteredClubsByZoneAtom);
const markers: MapMarker[] = useMemo(
() =>
+8 -16
View File
@@ -2,7 +2,7 @@
import { useEffect } from "react";
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { useAtomValue, useSetAtom } from "jotai";
import { useTranslations } from "next-intl";
import { FaExternalLinkAlt } from "react-icons/fa";
import { twMerge } from "tailwind-merge";
@@ -12,17 +12,15 @@ import {
debouncedHoveredMapIdAtom,
filteredClubsListAtom,
hoveredMapIdAtom,
syncVisibleAtom,
} from "@/atoms";
import { RegionSelect } from "@/components/RegionSelect";
import SearchBar from "@/components/SearchBar";
import { useBreakpoint } from "@/hooks/tailwind";
const ClubTable = () => {
const t = useTranslations("Clubs");
const at = useTranslations("App");
const filteredClubs = useAtomValue(filteredClubsListAtom);
const [syncVisible, setSyncVisible] = useAtom(syncVisibleAtom);
const hoveredMapId = useAtomValue(hoveredMapIdAtom);
const debouncedHoveredMapId = useAtomValue(debouncedHoveredMapIdAtom);
const setHoveredListId = useSetAtom(debouncedHoveredListIdAtom);
@@ -45,18 +43,12 @@ const ClubTable = () => {
>
<div className="z-10 flex w-full flex-wrap items-center justify-between gap-3 p-3">
<SearchBar />
<div className="flex flex-col gap-0 text-gray-900 dark:text-white">
<label>
<input
type="checkbox"
className="mr-2 h-4 w-4 rounded border-gray-400 text-primary focus:ring-primary"
checked={syncVisible}
onChange={() => setSyncVisible(!syncVisible)}
/>
{t("syncWithMapCheckbox")}
</label>
</div>
<RegionSelect
classNameOverrides={{
container: () => "flex flex-1",
}}
/>
<div className="flex flex-col gap-0 text-gray-900 dark:text-white"></div>
</div>
<div className="overflow-x-scroll bg-red-50">
@@ -5,14 +5,14 @@ import L from "leaflet";
import { useTranslations } from "next-intl";
import { useMap } from "react-leaflet";
import { filteredTournamentsByTimeControlAtom } from "@/atoms";
import { filteredTournamentsByTimeControlAndZoneAtom } from "@/atoms";
import { TimeControlColours } from "@/constants";
import { TimeControl } from "@/types";
const Legend = () => {
const at = useTranslations("App");
const map = useMap();
const tournaments = useAtomValue(filteredTournamentsByTimeControlAtom);
const tournaments = useAtomValue(filteredTournamentsByTimeControlAndZoneAtom);
const timeControls = useMemo(
() =>
@@ -10,7 +10,10 @@ import "leaflet.smooth_marker_bouncing";
import "leaflet/dist/leaflet.css";
import { countBy, groupBy } from "lodash";
import { filteredTournamentsByTimeControlAtom, normsOnlyAtom } from "@/atoms";
import {
filteredTournamentsByTimeControlAndZoneAtom,
normsOnlyAtom,
} from "@/atoms";
import { Map, MapMarker } from "@/components/Map";
import { TimeControlColours } from "@/constants";
import { generatePieSVG } from "@/lib/pie";
@@ -21,7 +24,7 @@ import TimeControlFilters from "./TimeControlFilters";
import { TournamentMarker } from "./TournamentMarker";
const TournamentMap = () => {
const tournaments = useAtomValue(filteredTournamentsByTimeControlAtom);
const tournaments = useAtomValue(filteredTournamentsByTimeControlAndZoneAtom);
const normsOnly = useAtomValue(normsOnlyAtom);
const filteredTournaments = useMemo(
@@ -18,9 +18,11 @@ import {
filteredTournamentsListAtom,
hoveredMapIdAtom,
normsOnlyAtom,
syncVisibleAtom,
regionFilterAtom,
} from "@/atoms";
import { RegionSelect } from "@/components/RegionSelect";
import SearchBar from "@/components/SearchBar";
import { Select } from "@/components/form/Select";
import { useBreakpoint } from "@/hooks/tailwind";
import useDatePickerWidth from "@/hooks/useDatePickerWidth";
import { DatePickerDirection } from "@/types";
@@ -36,7 +38,6 @@ const TournamentTable = () => {
const filteredTournaments = useAtomValue(filteredTournamentsListAtom);
const [syncVisible, setSyncVisible] = useAtom(syncVisibleAtom);
const [normsOnly, setNormsOnly] = useAtom(normsOnlyAtom);
const hoveredMapId = useAtomValue(hoveredMapIdAtom);
const debouncedHoveredMapId = useAtomValue(debouncedHoveredMapIdAtom);
@@ -87,16 +88,6 @@ const TournamentTable = () => {
/>
<div className="flex flex-col gap-0 text-gray-900 dark:text-white">
<label>
<input
type="checkbox"
className="mr-2 h-4 w-4 rounded border-gray-400 text-primary focus:ring-primary"
checked={syncVisible}
onChange={() => setSyncVisible(!syncVisible)}
/>
{t("syncWithMapCheckbox")}
</label>
<label>
<input
type="checkbox"
@@ -111,6 +102,8 @@ const TournamentTable = () => {
<div className="hidden lg:block">
<TimeControlFilters />
</div>
<RegionSelect />
</div>
<div className="flex justify-center" ref={datePickerRef}>
+64 -23
View File
@@ -1,6 +1,6 @@
import booleanPointInPolygon from "@turf/boolean-point-in-polygon";
import {
endOfDay,
formatISO,
isAfter,
isBefore,
parse,
@@ -8,9 +8,9 @@ import {
startOfDay,
} from "date-fns";
import { fr } from "date-fns/locale";
import { FeatureCollection } from "geojson";
import { atom } from "jotai";
import { LatLngBounds } from "leaflet";
import { c } from "next-safe-action/dist/index-EKyvnpX_.mjs";
import { Club, TimeControl, Tournament } from "@/types";
import atomWithDebounce from "@/utils/atomWithDebounce";
@@ -18,9 +18,18 @@ import { normalizedContains } from "@/utils/string";
setDefaultOptions({ locale: fr });
type RegionFilter =
| "all"
| "map"
| {
id: string;
name: string;
features: FeatureCollection;
};
export const burgerMenuIsOpenAtom = atom(false);
export const mapBoundsAtom = atom<LatLngBounds | null>(null);
export const syncVisibleAtom = atom(true);
export const regionFilterAtom = atom<RegionFilter>("map");
export const searchStringAtom = atom("");
export const tournamentsAtom = atom<Tournament[]>([]);
@@ -41,8 +50,9 @@ export const {
export const { debouncedValueAtom: debouncedHoveredListIdAtom } =
atomWithDebounce<string | null>(null, 1000, 100);
export const filteredTournamentsByTimeControlAtom = atom((get) => {
export const filteredTournamentsByTimeControlAndZoneAtom = atom((get) => {
const tournaments = get(tournamentsAtom);
const regionFilter = get(regionFilterAtom);
const classic = get(classicAtom);
const rapid = get(rapidAtom);
@@ -52,30 +62,43 @@ export const filteredTournamentsByTimeControlAtom = atom((get) => {
const dateRange = get(dateRangeAtom);
const { startDate, endDate } = dateRange[0];
const filteredTournaments = tournaments.filter((tournament) => {
const filterByTimeControl = tournaments.filter((tournament) => {
const tournamentDate = startOfDay(
parse(tournament.date, "dd/MM/yyyy", new Date()),
);
return (
!isBefore(tournamentDate, startDate) &&
(endDate === undefined || !isAfter(tournamentDate, endDate)) &&
!tournament.pending &&
tournament.status === "scheduled" &&
((tournament.timeControl === TimeControl.Classic && classic) ||
(tournament.timeControl === TimeControl.Rapid && rapid) ||
(tournament.timeControl === TimeControl.Blitz && blitz) ||
(tournament.timeControl === TimeControl.Other && other))
(endDate === undefined ||
(!isAfter(tournamentDate, endDate) &&
!tournament.pending &&
tournament.status === "scheduled" &&
((tournament.timeControl === TimeControl.Classic && classic) ||
(tournament.timeControl === TimeControl.Rapid && rapid) ||
(tournament.timeControl === TimeControl.Blitz && blitz) ||
(tournament.timeControl === TimeControl.Other && other))))
);
});
return filteredTournaments;
if (regionFilter === "all" || regionFilter === "map")
return filterByTimeControl;
return filterByTimeControl.filter((tournament) => {
return regionFilter.features?.features?.some(
(feature) =>
feature.geometry.type === "Polygon" &&
booleanPointInPolygon(
[tournament.latLng.lng, tournament.latLng.lat],
feature.geometry,
),
);
});
});
export const filteredTournamentsListAtom = atom((get) => {
const tournaments = get(filteredTournamentsByTimeControlAtom);
const tournaments = get(filteredTournamentsByTimeControlAndZoneAtom);
const mapBounds = get(mapBoundsAtom);
const syncVisible = get(syncVisibleAtom);
const regionFilter = get(regionFilterAtom);
const normsOnly = get(normsOnlyAtom);
const searchString = get(searchStringAtom).trim();
@@ -93,23 +116,41 @@ export const filteredTournamentsListAtom = atom((get) => {
}
// If we not syncing to the map, return all tournaments
if (mapBounds === null || !syncVisible) return filteredByNorm;
if (mapBounds === null || regionFilter !== "map") return filteredByNorm;
// Filter by those in the current map bounds
// Filter by the map bounds
return filteredByNorm.filter((tournament) =>
mapBounds.contains(tournament.latLng),
);
});
export const filteredClubsListAtom = atom((get) => {
export const filteredClubsByZoneAtom = atom((get) => {
const clubs = get(clubsAtom);
const regionFilter = get(regionFilterAtom);
if (regionFilter === "all" || regionFilter === "map") return clubs;
return clubs.filter((club) => {
return regionFilter.features?.features?.some(
(feature) =>
feature.geometry.type === "Polygon" &&
booleanPointInPolygon(
[club.latLng.lng, club.latLng.lat],
feature.geometry,
),
);
});
});
export const filteredClubsListAtom = atom((get) => {
const filteredByZone = get(filteredClubsByZoneAtom);
const mapBounds = get(mapBoundsAtom);
const syncVisible = get(syncVisibleAtom);
const regionFilter = get(regionFilterAtom);
const searchString = get(searchStringAtom).trim();
// When searching, we search all the tournament, regardless of the map display
if (searchString !== "") {
return clubs.filter(
return filteredByZone.filter(
(club) =>
normalizedContains(club.name, searchString) ||
(club.address && normalizedContains(club.address, searchString)),
@@ -117,10 +158,10 @@ export const filteredClubsListAtom = atom((get) => {
}
// If we not syncing to the map, return all clubs
if (mapBounds === null || !syncVisible) return clubs;
if (mapBounds === null || regionFilter !== "map") return filteredByZone;
// Filter by those in the current map bounds
return clubs.filter((club) => mapBounds.contains(club.latLng));
// Filter by the map bounds
return filteredByZone.filter((club) => mapBounds.contains(club.latLng));
});
// Date picker atoms
+58
View File
@@ -0,0 +1,58 @@
import { useAtom } from "jotai";
import { useTranslations } from "next-intl";
import { SingleValue } from "react-select";
import { regionFilterAtom } from "@/atoms";
import { BaseOption, Select, SelectProps } from "@/components/form/Select";
import { useZones } from "@/hooks/useZones";
import { Zone } from "@/server/myZones";
type RegionSelectProps = Omit<SelectProps, "options" | "value" | "onChange">;
export const RegionSelect = (selectProps: RegionSelectProps) => {
const t = useTranslations("Tournaments");
const [regionFilter, setRegionFilter] = useAtom(regionFilterAtom);
const { zones } = useZones();
const zoneFilterOptions: BaseOption<string, Zone | null>[] = [
{
value: "map",
label: t("syncWithMapOption"),
data: null,
},
{
value: "all",
label: t("ignoreMapOption"),
data: null,
},
...zones.map((zone) => ({
value: zone.id,
label: zone.name,
data: zone,
})),
];
const onChange = (option: SingleValue<BaseOption<string, Zone | null>>) => {
if (!option) return;
if (option.value === "map" || option.value === "all")
setRegionFilter(option.value);
else setRegionFilter(option.data!);
};
return (
<Select
options={zoneFilterOptions}
value={zoneFilterOptions.find((o) =>
regionFilter === "all" || regionFilter === "map"
? o.value === regionFilter
: o.value === regionFilter.id,
)}
onChange={(v) =>
onChange(v as SingleValue<BaseOption<string, Zone | null>>)
}
{...selectProps}
/>
);
};
+13 -1
View File
@@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useEffect, useState } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { signIn } from "next-auth/react";
@@ -37,6 +37,18 @@ export const SignInForm = ({ callbackPath }: SignInFormProps) => {
resolver: zodResolver(signInSchema),
});
useEffect(() => {
// Clear the response message when the email field changes
const subscription = form.watch((value, { name, type }) => {
if (type === "change") {
if (name === "email" && responseMessage.message === "") {
setResponseMessage({ isSuccessful: true, message: "" });
}
}
});
return () => subscription.unsubscribe();
}, [form, form.watch]);
const onSubmit = async ({ email }: SignInFormValues) => {
try {
const result = await signIn("nodemailer", {
+2 -1
View File
@@ -15,7 +15,8 @@ import AsyncSelect from "react-select/async";
import { Prettify } from "@/types";
import { Field, GenericFieldProps } from "./Field";
import { BaseOption, classNames } from "./SelectField";
import { classNames } from "./Select";
import { BaseOption } from "./SelectField";
export type AsyncSelectFieldProps<
TFieldValues extends FieldValues = FieldValues,
+120
View File
@@ -0,0 +1,120 @@
import React from "react";
import { useTranslations } from "next-intl";
import ReactSelect, { ClassNamesConfig, GroupBase, Props } from "react-select";
import { twMerge } from "tailwind-merge";
import { Prettify } from "@/types";
export type BaseOption<T = string, D = unknown> = {
value: T;
label: string | JSX.Element;
disabled?: boolean;
data?: D;
};
export const classNames = <Option, IsMulti extends boolean = false>(
hasError: boolean,
separators: boolean,
): ClassNamesConfig<Option, IsMulti, GroupBase<Option>> => ({
container: () => "w-full",
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",
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 text-sm",
"border-gray-300 bg-gray-50 text-gray-900 shadow-sm 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",
state.isDisabled && "cursor-not-allowed",
state.isFocused && "border-primary ring ring-primary ring-opacity-50",
),
multiValue: () => "bg-fieldGray border rounded-lg flex space-x-1 pl-1 m-1",
multiValueLabel: () => "",
multiValueRemove: () => "items-center px-1 hover:text-primary",
placeholder: () =>
"block truncate pr-2 placeholder-gray dark:placeholder-gray-400",
menu: () =>
twMerge(
"!z-30 mt-2 rounded-lg border border-gray-200 bg-white p-1",
"border-gray-300 bg-gray-50 text-gray-900",
"dark:border-gray-600 dark:bg-gray-700 dark:text-white",
),
groupHeading: () => "ml-3 mt-2 mb-1 text-textGray text-sm uppercase",
option: ({ isFocused, isDisabled }) =>
twMerge(
"px-3 py-2 hover:cursor-pointer",
separators && "border-b border-gray-200",
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",
});
export type SelectProps<
IsMulti extends boolean = false,
T = string,
D = unknown,
> = Prettify<
Omit<
Props<BaseOption<T, D>, IsMulti, GroupBase<BaseOption<T, D>>>,
"classNames"
> & {
required?: boolean;
separators?: boolean;
hasError?: boolean;
classNameOverrides?: ClassNamesConfig<
BaseOption<T, D>,
IsMulti,
GroupBase<BaseOption<T, D>>
>;
}
>;
export const Select = <
IsMulti extends boolean = false,
T = string,
D = unknown,
>({
className,
required,
placeholder,
separators,
hasError,
options,
classNameOverrides,
...selectProps
}: SelectProps<IsMulti, T, D>) => {
const t = useTranslations("App");
return (
<ReactSelect
options={options}
noOptionsMessage={() => t("noOptionsMessage")}
placeholder={placeholder ?? t("selectPlaceholder")}
unstyled
styles={{
input: (base) => ({
...base,
"input:focus": {
boxShadow: "none",
},
}),
}}
classNames={{
...classNames<BaseOption<T, D>, IsMulti>(
hasError ?? false,
separators ?? false,
),
...(classNameOverrides ?? {}),
}}
{...selectProps}
/>
);
};
+4 -67
View File
@@ -8,14 +8,9 @@ import {
FieldValues,
useFormContext,
} from "react-hook-form";
import Select, {
ClassNamesConfig,
GroupBase,
OnChangeValue,
Props,
} from "react-select";
import { twMerge } from "tailwind-merge";
import { GroupBase, OnChangeValue } from "react-select";
import { Select, SelectProps } from "@/components/form/Select";
import { Prettify } from "@/types";
import { Field, GenericFieldProps } from "./Field";
@@ -27,50 +22,6 @@ export type BaseOption<T = string, D = unknown> = {
data?: D;
};
export const classNames = <Option, IsMulti extends boolean = false>(
hasError: boolean,
separators: boolean,
): ClassNamesConfig<Option, IsMulti, GroupBase<Option>> => ({
container: () => "w-full",
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",
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 shadow-sm 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",
state.isDisabled && "cursor-not-allowed",
state.isFocused && "border-primary ring ring-primary ring-opacity-50",
),
multiValue: () => "bg-fieldGray border rounded-lg flex space-x-1 pl-1 m-1",
multiValueLabel: () => "",
multiValueRemove: () => "items-center px-1 hover:text-primary",
placeholder: () =>
"block truncate pr-2 placeholder-gray dark:placeholder-gray-400",
menu: () =>
twMerge(
"!z-30 mt-2 rounded-lg border border-gray-200 bg-white p-1",
"border-gray-300 bg-gray-50 text-gray-900",
"dark:border-gray-600 dark:bg-gray-700 dark:text-white",
),
groupHeading: () => "ml-3 mt-2 mb-1 text-textGray text-sm uppercase",
option: ({ isFocused, isDisabled }) =>
twMerge(
"px-3 py-2 hover:cursor-pointer",
separators && "border-b border-gray-200",
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",
});
export type SelectFieldProps<
TFieldValues extends FieldValues = FieldValues,
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
@@ -80,7 +31,7 @@ export type SelectFieldProps<
> = Prettify<
GenericFieldProps<TFieldValues, TFieldName> &
Omit<
Props<BaseOption<T, D>, IsMulti, GroupBase<BaseOption<T, D>>>,
SelectProps<IsMulti, T, D>,
"onChange" | "value" | "classNames" | "name"
> & {
required?: boolean;
@@ -168,21 +119,7 @@ export const SelectField = <
value={optionValue}
onChange={onSelectChange}
options={options}
noOptionsMessage={() => t("noOptionsMessage")}
placeholder={placeholder ?? t("selectPlaceholder")}
unstyled
styles={{
input: (base) => ({
...base,
"input:focus": {
boxShadow: "none",
},
}),
}}
classNames={classNames<BaseOption<T, D>, IsMulti>(
hasError,
separators ?? false,
)}
hasError={hasError}
{...selectProps}
/>
);
+2 -1
View File
@@ -80,7 +80,8 @@
"loading": "Loading...",
"searchLabel": "Search",
"searchPlaceholder": "Search",
"syncWithMapCheckbox": "Tournaments visible on the map",
"syncWithMapOption": "Only tournaments visible in the map view",
"ignoreMapOption": "No region-based filter",
"normsOnly": "Only norm tournaments",
"noneFound": "No tournaments found",
"date": "Date",
+2 -1
View File
@@ -79,7 +79,8 @@
"loading": "Téléchargement...",
"searchLabel": "Rechercher",
"searchPlaceholder": "Rechercher",
"syncWithMapCheckbox": "Tournois visibles sur la carte",
"syncWithMapOption": "Seuls les tournois visibles dans la vue carte",
"ignoreMapOption": "Pas de filtre de région",
"normsOnly": "Uniquement tournois à normes",
"noneFound": "Pas de tournois trouvé",
"date": "Date",
+12 -3
View File
@@ -1,5 +1,6 @@
"use server";
import { FeatureCollection } from "geojson";
import { omit } from "lodash";
import { ObjectId } from "mongodb";
import { z } from "zod";
@@ -7,8 +8,14 @@ import { z } from "zod";
import { auth } from "@/auth";
import { collections, dbConnect } from "@/server/mongodb";
import { ZoneModel } from "./models/zoneModel";
import { action } from "./safeAction";
export type Zone = Omit<ZoneModel, "userId" | "features"> & {
id: string;
features: FeatureCollection;
};
export const myZones = action(z.void(), async () => {
await dbConnect();
@@ -21,9 +28,11 @@ export const myZones = action(z.void(), async () => {
.zones!.find({ userId: new ObjectId(user.user!.id!) })
.toArray();
return zones.map((zone) => ({
...omit(zone, ["_id"]),
const result: Zone[] = zones.map((zone) => ({
...omit(zone, ["_id", "userId"]),
id: zone._id.toString(),
userId: zone.userId.toString(),
features: zone.features as FeatureCollection,
}));
return result;
});
+2
View File
@@ -147,6 +147,8 @@ export default function Nodemailer(
const t = await getTranslations({ locale, namespace: "SignIn" });
console.log("Connection URL", url);
const { host } = new URL(url);
const transport = createTransport(provider.server);
const result = await transport.sendMail({
+33 -2
View File
@@ -1759,6 +1759,32 @@
javascript-natural-sort "0.7.1"
lodash "^4.17.21"
"@turf/boolean-point-in-polygon@^7.0.0-alpha.114":
version "7.0.0-alpha.114"
resolved "https://registry.yarnpkg.com/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-7.0.0-alpha.114.tgz#274faa9ccc402cfa42815914040c58d9f20cc290"
integrity sha512-ytxwVqHbbBCG5DsN5QLBO6PD21EnnhnqFiGkg6a4pEOv/y7TnapSQ8RTKVJfSUdA+MhJ+uu3aSXOVH44udgNfA==
dependencies:
"@turf/helpers" "^7.0.0-alpha.114+e0bdd0add"
"@turf/invariant" "^7.0.0-alpha.114+e0bdd0add"
point-in-polygon-hao "^1.1.0"
tslib "^2.6.2"
"@turf/helpers@^7.0.0-alpha.114+e0bdd0add":
version "7.0.0-alpha.114"
resolved "https://registry.yarnpkg.com/@turf/helpers/-/helpers-7.0.0-alpha.114.tgz#03158ecd04c147165725c2e3994bb7190c23041c"
integrity sha512-hqoARkwAaFMK/8wOWDQhmvIxjUL2l9jUhn1GUzW3fsumImPxuHoJZbIZhiHjE7ceQngveCeUCtkiKw89lekN8w==
dependencies:
deep-equal "^2.2.3"
tslib "^2.6.2"
"@turf/invariant@^7.0.0-alpha.114+e0bdd0add":
version "7.0.0-alpha.114"
resolved "https://registry.yarnpkg.com/@turf/invariant/-/invariant-7.0.0-alpha.114.tgz#184ab4a6021839b9953b497e0aa28267947cbedb"
integrity sha512-d6KGoItyg2FFj6JSXlS23Ymvt6YA3DwAbpTwKiHCkrDP5UtHQwli+yYvmY/Bo2eC3HyDAgk/YTutmv3mNiWaJA==
dependencies:
"@turf/helpers" "^7.0.0-alpha.114+e0bdd0add"
tslib "^2.6.2"
"@types/aria-query@^5.0.1":
version "5.0.4"
resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708"
@@ -2632,7 +2658,7 @@ debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4:
dependencies:
ms "2.1.2"
deep-equal@^2.0.5:
deep-equal@^2.0.5, deep-equal@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.3.tgz#af89dafb23a396c7da3e862abc0be27cf51d56e1"
integrity sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==
@@ -4663,6 +4689,11 @@ pkg-dir@^4.1.0:
dependencies:
find-up "^4.0.0"
point-in-polygon-hao@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/point-in-polygon-hao/-/point-in-polygon-hao-1.1.0.tgz#37f5f4fbe14e89fa8a3bb7f67c9158079d2ede7c"
integrity sha512-3hTIM2j/v9Lio+wOyur3kckD4NxruZhpowUbEgmyikW+a2Kppjtu1eN+AhnMQtoHW46zld88JiYWv6fxpsDrTQ==
possible-typed-array-names@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f"
@@ -5692,7 +5723,7 @@ tsconfig-paths@^3.15.0:
minimist "^1.2.6"
strip-bom "^3.0.0"
tslib@^2.1.0, tslib@^2.4.0:
tslib@^2.1.0, tslib@^2.4.0, tslib@^2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==