mirror of
https://github.com/TheRealOwenRees/echecsfrance.git
synced 2026-07-23 04:26:57 +00:00
Filtering by zone
This commit is contained in:
@@ -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(
|
||||
() =>
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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", {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user