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", "@next/bundle-analyzer": "^14.0.4",
"@tanstack/react-query": "^5.29.0", "@tanstack/react-query": "^5.29.0",
"@trivago/prettier-plugin-sort-imports": "^4.2.0", "@trivago/prettier-plugin-sort-imports": "^4.2.0",
"@turf/boolean-point-in-polygon": "^7.0.0-alpha.114",
"@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",
+3 -5
View File
@@ -1,22 +1,20 @@
"use client"; "use client";
import { useCallback, useMemo } from "react"; import { useMemo } from "react";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import L, { LatLngLiteral } from "leaflet";
import "leaflet-defaulticon-compatibility"; import "leaflet-defaulticon-compatibility";
import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css"; import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css";
import "leaflet.smooth_marker_bouncing"; import "leaflet.smooth_marker_bouncing";
import "leaflet/dist/leaflet.css"; 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 { Map, MapMarker } from "@/components/Map";
import { ClubMarker } from "./ClubMarker"; import { ClubMarker } from "./ClubMarker";
const ClubMap = () => { const ClubMap = () => {
const clubs = useAtomValue(clubsAtom); const clubs = useAtomValue(filteredClubsByZoneAtom);
const markers: MapMarker[] = useMemo( const markers: MapMarker[] = useMemo(
() => () =>
+8 -16
View File
@@ -2,7 +2,7 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { useAtomValue, useSetAtom } from "jotai";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { FaExternalLinkAlt } from "react-icons/fa"; import { FaExternalLinkAlt } from "react-icons/fa";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -12,17 +12,15 @@ import {
debouncedHoveredMapIdAtom, debouncedHoveredMapIdAtom,
filteredClubsListAtom, filteredClubsListAtom,
hoveredMapIdAtom, hoveredMapIdAtom,
syncVisibleAtom,
} from "@/atoms"; } from "@/atoms";
import { RegionSelect } from "@/components/RegionSelect";
import SearchBar from "@/components/SearchBar"; import SearchBar from "@/components/SearchBar";
import { useBreakpoint } from "@/hooks/tailwind"; import { useBreakpoint } from "@/hooks/tailwind";
const ClubTable = () => { const ClubTable = () => {
const t = useTranslations("Clubs"); const t = useTranslations("Clubs");
const at = useTranslations("App");
const filteredClubs = useAtomValue(filteredClubsListAtom); const filteredClubs = useAtomValue(filteredClubsListAtom);
const [syncVisible, setSyncVisible] = useAtom(syncVisibleAtom);
const hoveredMapId = useAtomValue(hoveredMapIdAtom); const hoveredMapId = useAtomValue(hoveredMapIdAtom);
const debouncedHoveredMapId = useAtomValue(debouncedHoveredMapIdAtom); const debouncedHoveredMapId = useAtomValue(debouncedHoveredMapIdAtom);
const setHoveredListId = useSetAtom(debouncedHoveredListIdAtom); 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"> <div className="z-10 flex w-full flex-wrap items-center justify-between gap-3 p-3">
<SearchBar /> <SearchBar />
<RegionSelect
<div className="flex flex-col gap-0 text-gray-900 dark:text-white"> classNameOverrides={{
<label> container: () => "flex flex-1",
<input }}
type="checkbox" />
className="mr-2 h-4 w-4 rounded border-gray-400 text-primary focus:ring-primary" <div className="flex flex-col gap-0 text-gray-900 dark:text-white"></div>
checked={syncVisible}
onChange={() => setSyncVisible(!syncVisible)}
/>
{t("syncWithMapCheckbox")}
</label>
</div>
</div> </div>
<div className="overflow-x-scroll bg-red-50"> <div className="overflow-x-scroll bg-red-50">
@@ -5,14 +5,14 @@ import L from "leaflet";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useMap } from "react-leaflet"; import { useMap } from "react-leaflet";
import { filteredTournamentsByTimeControlAtom } from "@/atoms"; import { filteredTournamentsByTimeControlAndZoneAtom } from "@/atoms";
import { TimeControlColours } from "@/constants"; import { TimeControlColours } from "@/constants";
import { TimeControl } from "@/types"; import { TimeControl } from "@/types";
const Legend = () => { const Legend = () => {
const at = useTranslations("App"); const at = useTranslations("App");
const map = useMap(); const map = useMap();
const tournaments = useAtomValue(filteredTournamentsByTimeControlAtom); const tournaments = useAtomValue(filteredTournamentsByTimeControlAndZoneAtom);
const timeControls = useMemo( const timeControls = useMemo(
() => () =>
@@ -10,7 +10,10 @@ import "leaflet.smooth_marker_bouncing";
import "leaflet/dist/leaflet.css"; import "leaflet/dist/leaflet.css";
import { countBy, groupBy } from "lodash"; import { countBy, groupBy } from "lodash";
import { filteredTournamentsByTimeControlAtom, normsOnlyAtom } from "@/atoms"; import {
filteredTournamentsByTimeControlAndZoneAtom,
normsOnlyAtom,
} from "@/atoms";
import { Map, MapMarker } from "@/components/Map"; import { Map, MapMarker } from "@/components/Map";
import { TimeControlColours } from "@/constants"; import { TimeControlColours } from "@/constants";
import { generatePieSVG } from "@/lib/pie"; import { generatePieSVG } from "@/lib/pie";
@@ -21,7 +24,7 @@ import TimeControlFilters from "./TimeControlFilters";
import { TournamentMarker } from "./TournamentMarker"; import { TournamentMarker } from "./TournamentMarker";
const TournamentMap = () => { const TournamentMap = () => {
const tournaments = useAtomValue(filteredTournamentsByTimeControlAtom); const tournaments = useAtomValue(filteredTournamentsByTimeControlAndZoneAtom);
const normsOnly = useAtomValue(normsOnlyAtom); const normsOnly = useAtomValue(normsOnlyAtom);
const filteredTournaments = useMemo( const filteredTournaments = useMemo(
@@ -18,9 +18,11 @@ import {
filteredTournamentsListAtom, filteredTournamentsListAtom,
hoveredMapIdAtom, hoveredMapIdAtom,
normsOnlyAtom, normsOnlyAtom,
syncVisibleAtom, regionFilterAtom,
} from "@/atoms"; } from "@/atoms";
import { RegionSelect } from "@/components/RegionSelect";
import SearchBar from "@/components/SearchBar"; import SearchBar from "@/components/SearchBar";
import { Select } from "@/components/form/Select";
import { useBreakpoint } from "@/hooks/tailwind"; import { useBreakpoint } from "@/hooks/tailwind";
import useDatePickerWidth from "@/hooks/useDatePickerWidth"; import useDatePickerWidth from "@/hooks/useDatePickerWidth";
import { DatePickerDirection } from "@/types"; import { DatePickerDirection } from "@/types";
@@ -36,7 +38,6 @@ const TournamentTable = () => {
const filteredTournaments = useAtomValue(filteredTournamentsListAtom); const filteredTournaments = useAtomValue(filteredTournamentsListAtom);
const [syncVisible, setSyncVisible] = useAtom(syncVisibleAtom);
const [normsOnly, setNormsOnly] = useAtom(normsOnlyAtom); const [normsOnly, setNormsOnly] = useAtom(normsOnlyAtom);
const hoveredMapId = useAtomValue(hoveredMapIdAtom); const hoveredMapId = useAtomValue(hoveredMapIdAtom);
const debouncedHoveredMapId = useAtomValue(debouncedHoveredMapIdAtom); const debouncedHoveredMapId = useAtomValue(debouncedHoveredMapIdAtom);
@@ -87,16 +88,6 @@ const TournamentTable = () => {
/> />
<div className="flex flex-col gap-0 text-gray-900 dark:text-white"> <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> <label>
<input <input
type="checkbox" type="checkbox"
@@ -111,6 +102,8 @@ const TournamentTable = () => {
<div className="hidden lg:block"> <div className="hidden lg:block">
<TimeControlFilters /> <TimeControlFilters />
</div> </div>
<RegionSelect />
</div> </div>
<div className="flex justify-center" ref={datePickerRef}> <div className="flex justify-center" ref={datePickerRef}>
+64 -23
View File
@@ -1,6 +1,6 @@
import booleanPointInPolygon from "@turf/boolean-point-in-polygon";
import { import {
endOfDay, endOfDay,
formatISO,
isAfter, isAfter,
isBefore, isBefore,
parse, parse,
@@ -8,9 +8,9 @@ import {
startOfDay, startOfDay,
} from "date-fns"; } from "date-fns";
import { fr } from "date-fns/locale"; import { fr } from "date-fns/locale";
import { FeatureCollection } from "geojson";
import { atom } from "jotai"; import { atom } from "jotai";
import { LatLngBounds } from "leaflet"; import { LatLngBounds } from "leaflet";
import { c } from "next-safe-action/dist/index-EKyvnpX_.mjs";
import { Club, TimeControl, Tournament } from "@/types"; import { Club, TimeControl, Tournament } from "@/types";
import atomWithDebounce from "@/utils/atomWithDebounce"; import atomWithDebounce from "@/utils/atomWithDebounce";
@@ -18,9 +18,18 @@ import { normalizedContains } from "@/utils/string";
setDefaultOptions({ locale: fr }); setDefaultOptions({ locale: fr });
type RegionFilter =
| "all"
| "map"
| {
id: string;
name: string;
features: FeatureCollection;
};
export const burgerMenuIsOpenAtom = atom(false); export const burgerMenuIsOpenAtom = atom(false);
export const mapBoundsAtom = atom<LatLngBounds | null>(null); export const mapBoundsAtom = atom<LatLngBounds | null>(null);
export const syncVisibleAtom = atom(true); export const regionFilterAtom = atom<RegionFilter>("map");
export const searchStringAtom = atom(""); export const searchStringAtom = atom("");
export const tournamentsAtom = atom<Tournament[]>([]); export const tournamentsAtom = atom<Tournament[]>([]);
@@ -41,8 +50,9 @@ export const {
export const { debouncedValueAtom: debouncedHoveredListIdAtom } = export const { debouncedValueAtom: debouncedHoveredListIdAtom } =
atomWithDebounce<string | null>(null, 1000, 100); atomWithDebounce<string | null>(null, 1000, 100);
export const filteredTournamentsByTimeControlAtom = atom((get) => { export const filteredTournamentsByTimeControlAndZoneAtom = atom((get) => {
const tournaments = get(tournamentsAtom); const tournaments = get(tournamentsAtom);
const regionFilter = get(regionFilterAtom);
const classic = get(classicAtom); const classic = get(classicAtom);
const rapid = get(rapidAtom); const rapid = get(rapidAtom);
@@ -52,30 +62,43 @@ export const filteredTournamentsByTimeControlAtom = atom((get) => {
const dateRange = get(dateRangeAtom); const dateRange = get(dateRangeAtom);
const { startDate, endDate } = dateRange[0]; const { startDate, endDate } = dateRange[0];
const filteredTournaments = tournaments.filter((tournament) => { const filterByTimeControl = tournaments.filter((tournament) => {
const tournamentDate = startOfDay( const tournamentDate = startOfDay(
parse(tournament.date, "dd/MM/yyyy", new Date()), parse(tournament.date, "dd/MM/yyyy", new Date()),
); );
return ( return (
!isBefore(tournamentDate, startDate) && !isBefore(tournamentDate, startDate) &&
(endDate === undefined || !isAfter(tournamentDate, endDate)) && (endDate === undefined ||
!tournament.pending && (!isAfter(tournamentDate, endDate) &&
tournament.status === "scheduled" && !tournament.pending &&
((tournament.timeControl === TimeControl.Classic && classic) || tournament.status === "scheduled" &&
(tournament.timeControl === TimeControl.Rapid && rapid) || ((tournament.timeControl === TimeControl.Classic && classic) ||
(tournament.timeControl === TimeControl.Blitz && blitz) || (tournament.timeControl === TimeControl.Rapid && rapid) ||
(tournament.timeControl === TimeControl.Other && other)) (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) => { export const filteredTournamentsListAtom = atom((get) => {
const tournaments = get(filteredTournamentsByTimeControlAtom); const tournaments = get(filteredTournamentsByTimeControlAndZoneAtom);
const mapBounds = get(mapBoundsAtom); const mapBounds = get(mapBoundsAtom);
const syncVisible = get(syncVisibleAtom); const regionFilter = get(regionFilterAtom);
const normsOnly = get(normsOnlyAtom); const normsOnly = get(normsOnlyAtom);
const searchString = get(searchStringAtom).trim(); 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 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) => return filteredByNorm.filter((tournament) =>
mapBounds.contains(tournament.latLng), mapBounds.contains(tournament.latLng),
); );
}); });
export const filteredClubsListAtom = atom((get) => { export const filteredClubsByZoneAtom = atom((get) => {
const clubs = get(clubsAtom); 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 mapBounds = get(mapBoundsAtom);
const syncVisible = get(syncVisibleAtom); const regionFilter = get(regionFilterAtom);
const searchString = get(searchStringAtom).trim(); const searchString = get(searchStringAtom).trim();
// When searching, we search all the tournament, regardless of the map display // When searching, we search all the tournament, regardless of the map display
if (searchString !== "") { if (searchString !== "") {
return clubs.filter( return filteredByZone.filter(
(club) => (club) =>
normalizedContains(club.name, searchString) || normalizedContains(club.name, searchString) ||
(club.address && normalizedContains(club.address, 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 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 // Filter by the map bounds
return clubs.filter((club) => mapBounds.contains(club.latLng)); return filteredByZone.filter((club) => mapBounds.contains(club.latLng));
}); });
// Date picker atoms // 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"; "use client";
import { useState } from "react"; import { useEffect, useState } from "react";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { signIn } from "next-auth/react"; import { signIn } from "next-auth/react";
@@ -37,6 +37,18 @@ export const SignInForm = ({ callbackPath }: SignInFormProps) => {
resolver: zodResolver(signInSchema), 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) => { const onSubmit = async ({ email }: SignInFormValues) => {
try { try {
const result = await signIn("nodemailer", { const result = await signIn("nodemailer", {
+2 -1
View File
@@ -15,7 +15,8 @@ import AsyncSelect from "react-select/async";
import { Prettify } from "@/types"; import { Prettify } from "@/types";
import { Field, GenericFieldProps } from "./Field"; import { Field, GenericFieldProps } from "./Field";
import { BaseOption, classNames } from "./SelectField"; import { classNames } from "./Select";
import { BaseOption } from "./SelectField";
export type AsyncSelectFieldProps< export type AsyncSelectFieldProps<
TFieldValues extends FieldValues = FieldValues, 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, FieldValues,
useFormContext, useFormContext,
} from "react-hook-form"; } from "react-hook-form";
import Select, { import { GroupBase, OnChangeValue } from "react-select";
ClassNamesConfig,
GroupBase,
OnChangeValue,
Props,
} from "react-select";
import { twMerge } from "tailwind-merge";
import { Select, SelectProps } from "@/components/form/Select";
import { Prettify } from "@/types"; import { Prettify } from "@/types";
import { Field, GenericFieldProps } from "./Field"; import { Field, GenericFieldProps } from "./Field";
@@ -27,50 +22,6 @@ export type BaseOption<T = string, D = unknown> = {
data?: D; 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< export type SelectFieldProps<
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
@@ -80,7 +31,7 @@ export type SelectFieldProps<
> = Prettify< > = Prettify<
GenericFieldProps<TFieldValues, TFieldName> & GenericFieldProps<TFieldValues, TFieldName> &
Omit< Omit<
Props<BaseOption<T, D>, IsMulti, GroupBase<BaseOption<T, D>>>, SelectProps<IsMulti, T, D>,
"onChange" | "value" | "classNames" | "name" "onChange" | "value" | "classNames" | "name"
> & { > & {
required?: boolean; required?: boolean;
@@ -168,21 +119,7 @@ export const SelectField = <
value={optionValue} value={optionValue}
onChange={onSelectChange} onChange={onSelectChange}
options={options} options={options}
noOptionsMessage={() => t("noOptionsMessage")} hasError={hasError}
placeholder={placeholder ?? t("selectPlaceholder")}
unstyled
styles={{
input: (base) => ({
...base,
"input:focus": {
boxShadow: "none",
},
}),
}}
classNames={classNames<BaseOption<T, D>, IsMulti>(
hasError,
separators ?? false,
)}
{...selectProps} {...selectProps}
/> />
); );
+2 -1
View File
@@ -80,7 +80,8 @@
"loading": "Loading...", "loading": "Loading...",
"searchLabel": "Search", "searchLabel": "Search",
"searchPlaceholder": "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", "normsOnly": "Only norm tournaments",
"noneFound": "No tournaments found", "noneFound": "No tournaments found",
"date": "Date", "date": "Date",
+2 -1
View File
@@ -79,7 +79,8 @@
"loading": "Téléchargement...", "loading": "Téléchargement...",
"searchLabel": "Rechercher", "searchLabel": "Rechercher",
"searchPlaceholder": "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", "normsOnly": "Uniquement tournois à normes",
"noneFound": "Pas de tournois trouvé", "noneFound": "Pas de tournois trouvé",
"date": "Date", "date": "Date",
+12 -3
View File
@@ -1,5 +1,6 @@
"use server"; "use server";
import { FeatureCollection } from "geojson";
import { omit } from "lodash"; import { omit } from "lodash";
import { ObjectId } from "mongodb"; import { ObjectId } from "mongodb";
import { z } from "zod"; import { z } from "zod";
@@ -7,8 +8,14 @@ import { z } from "zod";
import { auth } from "@/auth"; import { auth } from "@/auth";
import { collections, dbConnect } from "@/server/mongodb"; import { collections, dbConnect } from "@/server/mongodb";
import { ZoneModel } from "./models/zoneModel";
import { action } from "./safeAction"; import { action } from "./safeAction";
export type Zone = Omit<ZoneModel, "userId" | "features"> & {
id: string;
features: FeatureCollection;
};
export const myZones = action(z.void(), async () => { export const myZones = action(z.void(), async () => {
await dbConnect(); await dbConnect();
@@ -21,9 +28,11 @@ export const myZones = action(z.void(), async () => {
.zones!.find({ userId: new ObjectId(user.user!.id!) }) .zones!.find({ userId: new ObjectId(user.user!.id!) })
.toArray(); .toArray();
return zones.map((zone) => ({ const result: Zone[] = zones.map((zone) => ({
...omit(zone, ["_id"]), ...omit(zone, ["_id", "userId"]),
id: zone._id.toString(), 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" }); const t = await getTranslations({ locale, namespace: "SignIn" });
console.log("Connection URL", url);
const { host } = new URL(url); const { host } = new URL(url);
const transport = createTransport(provider.server); const transport = createTransport(provider.server);
const result = await transport.sendMail({ const result = await transport.sendMail({
+33 -2
View File
@@ -1759,6 +1759,32 @@
javascript-natural-sort "0.7.1" javascript-natural-sort "0.7.1"
lodash "^4.17.21" 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": "@types/aria-query@^5.0.1":
version "5.0.4" version "5.0.4"
resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708" 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: dependencies:
ms "2.1.2" ms "2.1.2"
deep-equal@^2.0.5: deep-equal@^2.0.5, deep-equal@^2.2.3:
version "2.2.3" version "2.2.3"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.3.tgz#af89dafb23a396c7da3e862abc0be27cf51d56e1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.3.tgz#af89dafb23a396c7da3e862abc0be27cf51d56e1"
integrity sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA== integrity sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==
@@ -4663,6 +4689,11 @@ pkg-dir@^4.1.0:
dependencies: dependencies:
find-up "^4.0.0" 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: possible-typed-array-names@^1.0.0:
version "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" 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" minimist "^1.2.6"
strip-bom "^3.0.0" 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" version "2.6.2"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==