Group tournaments into a single map marker based on position & date (#105)

This commit is contained in:
Timothy Armes
2023-07-18 11:52:19 +02:00
committed by GitHub
parent 1e7b5bf50e
commit b305b0578b
9 changed files with 234 additions and 58 deletions
+23 -10
View File
@@ -2,7 +2,7 @@
import { TimeControl } from "@/types"; import { TimeControl } from "@/types";
import { useMemo, useRef } from "react"; import { useMemo, useRef, useCallback, useEffect } from "react";
import L, { LatLngLiteral, Marker, DomUtil } from "leaflet"; import L, { LatLngLiteral, Marker, DomUtil } from "leaflet";
import MarkerClusterGroup from "react-leaflet-cluster"; import MarkerClusterGroup from "react-leaflet-cluster";
import { import {
@@ -13,6 +13,7 @@ import {
} from "react-leaflet"; } from "react-leaflet";
import { FaAngleDoubleDown } from "react-icons/fa"; import { FaAngleDoubleDown } from "react-icons/fa";
import { useAtomValue, useSetAtom } from "jotai"; import { useAtomValue, useSetAtom } from "jotai";
import { groupBy } from "lodash";
import "leaflet/dist/leaflet.css"; import "leaflet/dist/leaflet.css";
import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css"; import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css";
@@ -34,7 +35,6 @@ import {
import Legend from "./Legend"; import Legend from "./Legend";
import { TournamentMarker } from "./TournamentMarker"; import { TournamentMarker } from "./TournamentMarker";
import TimeControlFilters from "./TimeControlFilters"; import TimeControlFilters from "./TimeControlFilters";
import { useCallback, useEffect } from "react";
// Declare a class type that adds in methods etc. defined by leaflet.smooth_marker_bouncing // Declare a class type that adds in methods etc. defined by leaflet.smooth_marker_bouncing
// to keep Typescript happy // to keep Typescript happy
@@ -108,16 +108,24 @@ const TimeControlGroup = ({ timeControl, colour }: TimeControlGroupProps) => {
[normsOnly, timeControl, tournaments], [normsOnly, timeControl, tournaments],
); );
const groupedTournaments = groupBy(filteredTournaments, (t) => t.groupId);
const hoveredListTournamentId = useAtomValue( const hoveredListTournamentId = useAtomValue(
debouncedHoveredListTournamentIdAtom, debouncedHoveredListTournamentIdAtom,
); );
const expandAndBounceIfNeeded = useCallback(() => { const expandAndBounceIfNeeded = useCallback(() => {
if (hoveredListTournamentId) { if (hoveredListTournamentId) {
const markerRef = markerRefs.current[hoveredListTournamentId]; const tournament = filteredTournaments.find(
(t) => t.id === hoveredListTournamentId,
);
if (!tournament) return false;
const markerRef = markerRefs.current[tournament.groupId];
if (markerRef) { if (markerRef) {
if (clusterRef.current) { if (clusterRef.current) {
const visibleMarker = clusterRef.current.getVisibleParent(markerRef); const visibleMarker = clusterRef.current.getVisibleParent(markerRef);
if (!visibleMarker) return;
// @ts-ignore // @ts-ignore
if (visibleMarker.__proto__ === L.MarkerCluster.prototype) { if (visibleMarker.__proto__ === L.MarkerCluster.prototype) {
@@ -155,16 +163,21 @@ const TimeControlGroup = ({ timeControl, colour }: TimeControlGroupProps) => {
} }
return false; return false;
}, [hoveredListTournamentId]); }, [filteredTournaments, hoveredListTournamentId]);
const onSpiderified = useCallback( const onSpiderified = useCallback(
(e: L.MarkerClusterSpiderfyEvent) => { (e: L.MarkerClusterSpiderfyEvent) => {
// Once expanded, bounce the appropriate marker // Once expanded, bounce the appropriate marker
if (hoveredListTournamentId) { if (hoveredListTournamentId) {
const tournament = filteredTournaments.find(
(t) => t.id === hoveredListTournamentId,
);
if (!tournament) return;
expandedClusterMarkerRef.current = e.cluster; expandedClusterMarkerRef.current = e.cluster;
const markerRef = markerRefs.current[ const markerRef = markerRefs.current[
hoveredListTournamentId tournament.groupId
] as BouncingMarker; ] as BouncingMarker;
if (markerRef && e.markers.includes(markerRef)) { if (markerRef && e.markers.includes(markerRef)) {
@@ -173,7 +186,7 @@ const TimeControlGroup = ({ timeControl, colour }: TimeControlGroupProps) => {
} }
} }
}, },
[hoveredListTournamentId], [filteredTournaments, hoveredListTournamentId],
); );
const onUnSpiderified = useCallback( const onUnSpiderified = useCallback(
@@ -224,12 +237,12 @@ const TimeControlGroup = ({ timeControl, colour }: TimeControlGroupProps) => {
[timeControl], [timeControl],
); );
const markers = filteredTournaments.map((tournament) => { const markers = Object.values(groupedTournaments).map((tournamentGroup) => {
return ( return (
<TournamentMarker <TournamentMarker
ref={(ref) => (markerRefs.current[tournament._id] = ref)} ref={(ref) => (markerRefs.current[tournamentGroup[0].groupId] = ref)}
key={tournament._id} key={tournamentGroup[0].groupId}
tournament={tournament} tournamentGroup={tournamentGroup}
colour={colour} colour={colour}
/> />
); );
+40 -17
View File
@@ -7,36 +7,39 @@ import { Marker, Popup, MarkerProps } from "react-leaflet";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import { FaTrophy } from "react-icons/fa"; import { FaTrophy } from "react-icons/fa";
import { last } from "lodash";
import { debouncedHoveredMapTournamentIdAtom } from "@/app/atoms"; import { debouncedHoveredMapTournamentGroupIdAtom } from "@/app/atoms";
type TournamentMarkerProps = { type TournamentMarkerProps = {
tournament: Tournament; tournamentGroup: Tournament[];
colour: string; colour: string;
} & Omit<MarkerProps, "position">; } & Omit<MarkerProps, "position">;
export const TournamentMarker = forwardRef< export const TournamentMarker = forwardRef<
L.Marker<any> | null, L.Marker<any> | null,
TournamentMarkerProps TournamentMarkerProps
>(({ tournament, colour, ...markerProps }, ref) => { >(({ tournamentGroup, colour, ...markerProps }, ref) => {
const t = useTranslations("Tournaments"); const t = useTranslations("Tournaments");
const baseTournament = tournamentGroup[0];
// We add shifts based on the time control, so that they don't hide each other // We add shifts based on the time control, so that they don't hide each other
const position = useRef({ const position = useRef({
lat: tournament.latLng.lat, lat: baseTournament.latLng.lat,
lng: lng:
tournament.latLng.lng + baseTournament.latLng.lng +
(tournament.timeControl === TimeControl.Rapid (baseTournament.timeControl === TimeControl.Rapid
? -0.01 ? -0.01
: tournament.timeControl === TimeControl.Blitz : baseTournament.timeControl === TimeControl.Blitz
? 0.01 ? 0.01
: tournament.timeControl === TimeControl.Other : baseTournament.timeControl === TimeControl.Other
? 0.02 ? 0.02
: 0), : 0),
}); });
const setHoveredMapTournamentId = useSetAtom( const setHoveredMapTournamentGroupId = useSetAtom(
debouncedHoveredMapTournamentIdAtom, debouncedHoveredMapTournamentGroupIdAtom,
); );
const iconOptions = useMemo( const iconOptions = useMemo(
@@ -52,30 +55,50 @@ export const TournamentMarker = forwardRef<
[colour], [colour],
); );
const startDate = baseTournament.date;
const endDate = last(tournamentGroup)!.date;
return ( return (
<Marker <Marker
ref={ref} ref={ref}
position={position.current} position={position.current}
icon={iconOptions} icon={iconOptions}
eventHandlers={{ eventHandlers={{
mouseover: () => setHoveredMapTournamentId(tournament._id), mouseover: () =>
mouseout: () => setHoveredMapTournamentId(null), setHoveredMapTournamentGroupId(
`${baseTournament.groupId}_${baseTournament.timeControl}`,
),
mouseout: () => setHoveredMapTournamentGroupId(null),
}} }}
{...markerProps} {...markerProps}
> >
<Popup> <Popup maxWidth={10000}>
<div className="flex flex-col gap-3"> <div className="flex max-w-[calc(100vw-80px)] flex-col gap-3 lg:max-w-[calc(100vw/2-80px)]">
<b>{tournament.date}</b> <b>
{baseTournament.date}
{endDate !== startDate && ` - ${endDate}`}
</b>
<a href={tournament.url} target="_blank" rel="noopener noreferrer"> <div className="flex flex-col gap-0">
{tournamentGroup.map((tournament) => (
<a
key={tournament.id}
href={tournament.url}
target="_blank"
rel="noopener noreferrer"
>
{tournament.tournament} {tournament.tournament}
</a> </a>
{tournament.norm && ( ))}
</div>
{tournamentGroup.some((t) => t.norm) && (
<div className="flex items-center"> <div className="flex items-center">
<FaTrophy className="mr-3 h-4 w-4" /> <FaTrophy className="mr-3 h-4 w-4" />
{t("norm")} {t("norm")}
</div> </div>
)} )}
{t("approx")} {t("approx")}
</div> </div>
</Popup> </Popup>
+17 -14
View File
@@ -12,8 +12,8 @@ import {
filteredTournamentsListAtom, filteredTournamentsListAtom,
syncVisibleAtom, syncVisibleAtom,
normsOnlyAtom, normsOnlyAtom,
hoveredMapTournamentIdAtom, hoveredMapTournamentGroupIdAtom,
debouncedHoveredMapTournamentIdAtom, debouncedHoveredMapTournamentGroupIdAtom,
debouncedHoveredListTournamentIdAtom, debouncedHoveredListTournamentIdAtom,
} from "@/app/atoms"; } from "@/app/atoms";
import { useBreakpoint } from "@/hooks/tailwind"; import { useBreakpoint } from "@/hooks/tailwind";
@@ -28,9 +28,11 @@ export default function TournamentTable() {
const filteredTournaments = useAtomValue(filteredTournamentsListAtom); const filteredTournaments = useAtomValue(filteredTournamentsListAtom);
const [syncVisible, setSyncVisible] = useAtom(syncVisibleAtom); const [syncVisible, setSyncVisible] = useAtom(syncVisibleAtom);
const [normsOnly, setNormsOnly] = useAtom(normsOnlyAtom); const [normsOnly, setNormsOnly] = useAtom(normsOnlyAtom);
const hoveredMapTournamentId = useAtomValue(hoveredMapTournamentIdAtom); const hoveredMapTournamentGroupId = useAtomValue(
const debouncedHoveredMapTournamentId = useAtomValue( hoveredMapTournamentGroupIdAtom,
debouncedHoveredMapTournamentIdAtom, );
const debouncedHoveredMapTournamentGroupId = useAtomValue(
debouncedHoveredMapTournamentGroupIdAtom,
); );
const setHoveredListTournamentId = useSetAtom( const setHoveredListTournamentId = useSetAtom(
debouncedHoveredListTournamentIdAtom, debouncedHoveredListTournamentIdAtom,
@@ -39,14 +41,13 @@ export default function TournamentTable() {
const isLg = useBreakpoint("lg"); const isLg = useBreakpoint("lg");
useEffect(() => { useEffect(() => {
if (!isLg || debouncedHoveredMapTournamentId === null) return; if (!isLg || debouncedHoveredMapTournamentGroupId === null) return;
const tournamentRow = document.querySelector(
const tournamentRow = document.getElementById( `[data-group-id="${debouncedHoveredMapTournamentGroupId}"]`,
debouncedHoveredMapTournamentId,
); );
tournamentRow?.scrollIntoView({ behavior: "smooth" }); tournamentRow?.scrollIntoView({ behavior: "smooth" });
}, [debouncedHoveredMapTournamentId, isLg]); }, [debouncedHoveredMapTournamentGroupId, isLg]);
return ( return (
<section <section
@@ -118,13 +119,15 @@ export default function TournamentTable() {
) : ( ) : (
filteredTournaments.map((tournament) => ( filteredTournaments.map((tournament) => (
<tr <tr
key={tournament._id} key={tournament.id}
id={tournament._id} id={tournament.id}
onMouseEnter={() => setHoveredListTournamentId(tournament._id)} data-group-id={`${tournament.groupId}_${tournament.timeControl}`}
onMouseEnter={() => setHoveredListTournamentId(tournament.id)}
onMouseLeave={() => setHoveredListTournamentId(null)} onMouseLeave={() => setHoveredListTournamentId(null)}
className={twMerge( className={twMerge(
"scroll-m-20 bg-white text-gray-900 hover:bg-gray-200 dark:bg-gray-800 dark:text-white dark:hover:bg-gray-900", "scroll-m-20 bg-white text-gray-900 hover:bg-gray-200 dark:bg-gray-800 dark:text-white dark:hover:bg-gray-900",
hoveredMapTournamentId === tournament._id && hoveredMapTournamentGroupId ===
`${tournament.groupId}_${tournament.timeControl}` &&
"bg-gray-200 dark:bg-gray-900", "bg-gray-200 dark:bg-gray-900",
)} )}
> >
+54 -4
View File
@@ -2,6 +2,8 @@ import clientPromise from "@/lib/mongodb";
import { errorLog } from "@/utils/logger"; import { errorLog } from "@/utils/logger";
import { Tournament, TimeControl } from "@/types"; import { Tournament, TimeControl } from "@/types";
import { groupBy } from "lodash";
import { parse, differenceInDays, isSameDay } from "date-fns";
import TournamentsDisplay from "./TournamentsDisplay"; import TournamentsDisplay from "./TournamentsDisplay";
import { ObjectId } from "mongodb"; import { ObjectId } from "mongodb";
@@ -54,13 +56,61 @@ const getTournaments = async () => {
]) ])
.toArray(); .toArray();
return data.map<Tournament>((t) => ({ // Group the tournaments by their location
...t, const groupedByLocation = groupBy(
_id: t._id.toString(), data,
(t) => `${t.coordinates[0]}_${t.coordinates[1]}`,
);
// For each location, create an array of arrays of contiguous dates for this location
const dateRangesByLocation: Record<string, Date[][]> = {};
for (const location in groupedByLocation) {
const tournaments = groupedByLocation[location];
// Note that this works since the tournaments are sorted by date
const dateRanges = tournaments.reduce<Date[][]>(
(acc, tournament) => {
const group = acc[acc.length - 1];
const date = parse(tournament.date, "dd/MM/yyyy", new Date());
const diff = differenceInDays(date, group[group.length - 1] ?? date);
if (diff > 1) {
acc.push([date]);
} else if (group.length === 0 || diff === 1) {
group.push(date);
}
return acc;
},
[[]],
);
dateRangesByLocation[location] = dateRanges;
}
return data.map<Tournament>((t) => {
const location = `${t.coordinates[0]}_${t.coordinates[1]}`;
const date = parse(t.date, "dd/MM/yyyy", new Date());
const dateRanges = dateRangesByLocation[location];
const rangeIndex = dateRanges.findIndex((ranges) =>
ranges.some((d) => isSameDay(d, date)),
);
// We place each tournament into a group based on location and date, so that
// we can display a single map marker.
const groupId = `${location}_${rangeIndex}`;
return {
id: t._id.toString(),
groupId,
tournament: t.tournament,
town: t.town,
department: t.department,
date: t.date,
url: t.url,
timeControl: tcMap[t.time_control] ?? TimeControl.Other, timeControl: tcMap[t.time_control] ?? TimeControl.Other,
latLng: { lat: t.coordinates[0], lng: t.coordinates[1] }, latLng: { lat: t.coordinates[0], lng: t.coordinates[1] },
norm: t.norm_tournament, norm: t.norm_tournament,
})); };
});
} catch (error) { } catch (error) {
errorLog(error); errorLog(error);
throw new Error("Error fetching tournament data"); throw new Error("Error fetching tournament data");
+2 -2
View File
@@ -16,8 +16,8 @@ export const blitzAtom = atom(true);
export const otherAtom = atom(true); export const otherAtom = atom(true);
export const { export const {
currentValueAtom: hoveredMapTournamentIdAtom, currentValueAtom: hoveredMapTournamentGroupIdAtom,
debouncedValueAtom: debouncedHoveredMapTournamentIdAtom, debouncedValueAtom: debouncedHoveredMapTournamentGroupIdAtom,
} = atomWithDebounce<string | null>(null); } = atomWithDebounce<string | null>(null);
export const { debouncedValueAtom: debouncedHoveredListTournamentIdAtom } = export const { debouncedValueAtom: debouncedHoveredListTournamentIdAtom } =
+63 -2
View File
@@ -1,7 +1,68 @@
import create from "@kodingdotninja/use-tailwind-breakpoint"; import { useLayoutEffect, useEffect, useState, useRef, useMemo } from "react";
import resolveConfig from "tailwindcss/resolveConfig"; import resolveConfig from "tailwindcss/resolveConfig";
import tailwindConfig from "@/tailwind.config.js"; import tailwindConfig from "@/tailwind.config.js";
const config = resolveConfig(tailwindConfig); const config = resolveConfig(tailwindConfig);
export const { useBreakpoint } = create(config.theme!.screens); const isSSR =
typeof window === "undefined" || !window.navigator || /ServerSideRendering|^Deno\//.test(window.navigator.userAgent);
const isBrowser = !isSSR;
const useIsomorphicEffect = isBrowser ? useLayoutEffect : useEffect;
export type CreatorReturnType = {
useBreakpoint<B>(breakpoint: B, defaultValue?: boolean): boolean;
useBreakpointEffect<B>(breakpoint: B, effect: (match: boolean) => void): void;
useBreakpointValue<B, T, U>(breakpoint: B, valid: T, invalid: U): T | U;
};
function create(screens: object | undefined) {
if (!screens) {
throw new Error("Failed to create breakpoint hooks, given `screens` value is invalid.");
}
function useBreakpoint(breakpoint: string, defaultValue: boolean = false) {
const [match, setMatch] = useState(() => defaultValue);
const matchRef = useRef(defaultValue);
useIsomorphicEffect(() => {
if (!(isBrowser && "matchMedia" in window)) return undefined;
function track() {
// @ts-expect-error accessing index with uncertain `screens` type
const value = (screens[breakpoint] as string) ?? "999999px";
const query = window.matchMedia(`(min-width: ${value})`);
matchRef.current = query.matches;
if (matchRef.current != match) {
setMatch(matchRef.current);
}
}
window.addEventListener("resize", track);
track();
return () => window.removeEventListener("resize", track);
});
return match;
}
function useBreakpointEffect<Breakpoint extends string>(breakpoint: Breakpoint, effect: (match: boolean) => void) {
const match = useBreakpoint(breakpoint);
useEffect(() => effect(match));
return null;
}
function useBreakpointValue<Breakpoint extends string, T, U>(breakpoint: Breakpoint, valid: T, invalid: U) {
const match = useBreakpoint(breakpoint);
const value = useMemo(() => (match ? valid : invalid), [invalid, match, valid]);
return value;
}
return {
useBreakpoint,
useBreakpointEffect,
useBreakpointValue,
} as CreatorReturnType;
}
export const { useBreakpoint, useBreakpointEffect, useBreakpointValue } = create(config.theme!.screens);
+25 -3
View File
@@ -15,11 +15,13 @@
"@types/react-dom": "18.2.6", "@types/react-dom": "18.2.6",
"@vercel/analytics": "^1.0.1", "@vercel/analytics": "^1.0.1",
"autoprefixer": "10.4.14", "autoprefixer": "10.4.14",
"date-fns": "^2.30.0",
"jotai": "^2.2.1", "jotai": "^2.2.1",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"leaflet-defaulticon-compatibility": "^0.1.1", "leaflet-defaulticon-compatibility": "^0.1.1",
"leaflet.markercluster": "^1.5.3", "leaflet.markercluster": "^1.5.3",
"leaflet.smooth_marker_bouncing": "^3.0.3", "leaflet.smooth_marker_bouncing": "^3.0.3",
"lodash": "^4.17.21",
"mongodb": "^5.7.0", "mongodb": "^5.7.0",
"next": "^13.4.7", "next": "^13.4.7",
"next-intl": "^3.0.0-beta.7", "next-intl": "^3.0.0-beta.7",
@@ -43,6 +45,7 @@
"@testing-library/react": "^14.0.0", "@testing-library/react": "^14.0.0",
"@types/leaflet": "^1.9.3", "@types/leaflet": "^1.9.3",
"@types/leaflet.markercluster": "^1.5.1", "@types/leaflet.markercluster": "^1.5.1",
"@types/lodash": "^4.14.195",
"@types/nodemailer": "^6.4.8", "@types/nodemailer": "^6.4.8",
"cypress": "^12.16.0", "cypress": "^12.16.0",
"eslint": "8.43.0", "eslint": "8.43.0",
@@ -180,7 +183,6 @@
"version": "7.21.5", "version": "7.21.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz",
"integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==", "integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==",
"dev": true,
"dependencies": { "dependencies": {
"regenerator-runtime": "^0.13.11" "regenerator-runtime": "^0.13.11"
}, },
@@ -991,6 +993,12 @@
"@types/leaflet": "*" "@types/leaflet": "*"
} }
}, },
"node_modules/@types/lodash": {
"version": "4.14.195",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz",
"integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==",
"dev": true
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.3.2", "version": "20.3.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.2.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.2.tgz",
@@ -2331,6 +2339,21 @@
"node": ">=0.10" "node": ">=0.10"
} }
}, },
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"dependencies": {
"@babel/runtime": "^7.21.0"
},
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/dayjs": { "node_modules/dayjs": {
"version": "1.11.8", "version": "1.11.8",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.8.tgz", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.8.tgz",
@@ -6255,8 +6278,7 @@
"node_modules/regenerator-runtime": { "node_modules/regenerator-runtime": {
"version": "0.13.11", "version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
"dev": true
}, },
"node_modules/regexp.prototype.flags": { "node_modules/regexp.prototype.flags": {
"version": "1.5.0", "version": "1.5.0",
+3
View File
@@ -20,11 +20,13 @@
"@types/react-dom": "18.2.6", "@types/react-dom": "18.2.6",
"@vercel/analytics": "^1.0.1", "@vercel/analytics": "^1.0.1",
"autoprefixer": "10.4.14", "autoprefixer": "10.4.14",
"date-fns": "^2.30.0",
"jotai": "^2.2.1", "jotai": "^2.2.1",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"leaflet-defaulticon-compatibility": "^0.1.1", "leaflet-defaulticon-compatibility": "^0.1.1",
"leaflet.markercluster": "^1.5.3", "leaflet.markercluster": "^1.5.3",
"leaflet.smooth_marker_bouncing": "^3.0.3", "leaflet.smooth_marker_bouncing": "^3.0.3",
"lodash": "^4.17.21",
"mongodb": "^5.7.0", "mongodb": "^5.7.0",
"next": "^13.4.7", "next": "^13.4.7",
"next-intl": "^3.0.0-beta.7", "next-intl": "^3.0.0-beta.7",
@@ -48,6 +50,7 @@
"@testing-library/react": "^14.0.0", "@testing-library/react": "^14.0.0",
"@types/leaflet": "^1.9.3", "@types/leaflet": "^1.9.3",
"@types/leaflet.markercluster": "^1.5.1", "@types/leaflet.markercluster": "^1.5.1",
"@types/lodash": "^4.14.195",
"@types/nodemailer": "^6.4.8", "@types/nodemailer": "^6.4.8",
"cypress": "^12.16.0", "cypress": "^12.16.0",
"eslint": "8.43.0", "eslint": "8.43.0",
+2 -1
View File
@@ -8,7 +8,8 @@ export enum TimeControl {
} }
export interface Tournament { export interface Tournament {
_id: string; id: string;
groupId: string;
town: string; town: string;
department: string; department: string;
tournament: string; tournament: string;