mirror of
https://github.com/TheRealOwenRees/echecsfrance.git
synced 2026-07-23 04:26:57 +00:00
Clustering (#75)
* Clustering * Add package * Shift markers based on the time control to keep they separated visually
This commit is contained in:
Vendored
+6
-1
@@ -14,6 +14,11 @@
|
|||||||
"Rapide",
|
"Rapide",
|
||||||
"Rees",
|
"Rees",
|
||||||
"sommes",
|
"sommes",
|
||||||
"tournois"
|
"spiderfied",
|
||||||
|
"Spiderfy",
|
||||||
|
"spiderified",
|
||||||
|
"tournois",
|
||||||
|
"unspiderfied",
|
||||||
|
"unspiderfy"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { TimeControl, Tournament } from "@/types";
|
import { TimeControl } from "@/types";
|
||||||
import { LatLngLiteral } from "leaflet";
|
|
||||||
|
import { useMemo, useRef } from "react";
|
||||||
|
import L, { LatLngLiteral, Marker, DomUtil } from "leaflet";
|
||||||
|
import MarkerClusterGroup from "react-leaflet-cluster";
|
||||||
import {
|
import {
|
||||||
MapContainer,
|
MapContainer,
|
||||||
TileLayer,
|
TileLayer,
|
||||||
@@ -13,48 +16,205 @@ import { useAtomValue, useSetAtom } from "jotai";
|
|||||||
|
|
||||||
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";
|
||||||
|
import "@/css/marker-cluster.css";
|
||||||
import "leaflet-defaulticon-compatibility";
|
import "leaflet-defaulticon-compatibility";
|
||||||
|
import "leaflet.smooth_marker_bouncing";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
filteredTournamentsByTimeControlAtom,
|
|
||||||
mapBoundsAtom,
|
mapBoundsAtom,
|
||||||
|
debouncedHoveredListTournamentIdAtom,
|
||||||
|
tournamentsAtom,
|
||||||
|
classicAtom,
|
||||||
|
rapidAtom,
|
||||||
|
blitzAtom,
|
||||||
|
oneHourKOAtom,
|
||||||
} from "@/app/atoms";
|
} from "@/app/atoms";
|
||||||
|
|
||||||
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
|
||||||
|
// to keep Typescript happy
|
||||||
|
declare class BouncingMarker extends Marker {
|
||||||
|
isBouncing(): boolean;
|
||||||
|
bounce(): void;
|
||||||
|
stopBouncing(): void;
|
||||||
|
|
||||||
|
_icon: HTMLElement;
|
||||||
|
_bouncingMotion?: {
|
||||||
|
bouncingAnimationPlaying: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const MapEvents = () => {
|
const MapEvents = () => {
|
||||||
const setMapBounds = useSetAtom(mapBoundsAtom);
|
const setMapBounds = useSetAtom(mapBoundsAtom);
|
||||||
const map = useMapEvent("moveend", () => {
|
const map = useMapEvent("moveend", () => {
|
||||||
|
// Set the map bounds atoms when the user pans/zooms
|
||||||
setMapBounds(map.getBounds());
|
setMapBounds(map.getBounds());
|
||||||
});
|
});
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TournamentMap() {
|
type TimeControlGroupProps = {
|
||||||
const tournaments = useAtomValue(filteredTournamentsByTimeControlAtom);
|
timeControl: TimeControl;
|
||||||
const setMapBounds = useSetAtom(mapBoundsAtom);
|
colour: string;
|
||||||
const center: LatLngLiteral = { lat: 47.0844, lng: 2.3964 };
|
|
||||||
|
|
||||||
const onScrollToTable = () => {
|
|
||||||
const tournamentTable = document.getElementById("tournament-table");
|
|
||||||
tournamentTable?.scrollIntoView({ behavior: "smooth" });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const createLayerGroups = (
|
const stopBouncingMarkers = () => {
|
||||||
timeControl: TimeControl,
|
const markers =
|
||||||
colour: string,
|
// @ts-ignore
|
||||||
tournaments: Tournament[]
|
Marker.prototype._orchestration.getBouncingMarkers() as BouncingMarker[];
|
||||||
) => {
|
|
||||||
const filteredTournaments = tournaments.filter(
|
markers.forEach((marker) => {
|
||||||
(t) => t.timeControl === timeControl
|
if (marker.isBouncing()) {
|
||||||
|
try {
|
||||||
|
marker.stopBouncing();
|
||||||
|
// The plugin keeps bouncing until the end of the animation. We want to stop it immediately
|
||||||
|
// An issue has been raised on the project (https://github.com/hosuaby/Leaflet.SmoothMarkerBouncing/issues/52), until then we have this hack.
|
||||||
|
// We remove the class and reset some internal state
|
||||||
|
|
||||||
|
DomUtil.removeClass(marker._icon, "bouncing");
|
||||||
|
if (marker?._bouncingMotion?.bouncingAnimationPlaying === true)
|
||||||
|
marker._bouncingMotion.bouncingAnimationPlaying = false;
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const TimeControlGroup = ({ timeControl, colour }: TimeControlGroupProps) => {
|
||||||
|
const tournaments = useAtomValue(tournamentsAtom);
|
||||||
|
const markerRefs = useRef<Record<string, L.Marker<any> | null>>({});
|
||||||
|
const clusterRef = useRef<L.MarkerClusterGroup | null>(null);
|
||||||
|
const expandedClusterMarkerRef = useRef<L.MarkerCluster | null>(null);
|
||||||
|
|
||||||
|
const filteredTournaments = useMemo(
|
||||||
|
() => tournaments.filter((t) => t.timeControl === timeControl),
|
||||||
|
[timeControl, tournaments]
|
||||||
);
|
);
|
||||||
|
|
||||||
const layerGroup = filteredTournaments.map((tournament) => {
|
const hoveredListTournamentId = useAtomValue(
|
||||||
|
debouncedHoveredListTournamentIdAtom
|
||||||
|
);
|
||||||
|
|
||||||
|
const expandAndBounceIfNeeded = useCallback(() => {
|
||||||
|
if (hoveredListTournamentId) {
|
||||||
|
const markerRef = markerRefs.current[hoveredListTournamentId];
|
||||||
|
if (markerRef) {
|
||||||
|
if (clusterRef.current) {
|
||||||
|
const visibleMarker = clusterRef.current.getVisibleParent(markerRef);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
if (visibleMarker.__proto__ === L.MarkerCluster.prototype) {
|
||||||
|
// This is a cluster icon, we expand it.
|
||||||
|
const clusterMarker = visibleMarker as L.MarkerCluster;
|
||||||
|
|
||||||
|
if (
|
||||||
|
expandedClusterMarkerRef.current &&
|
||||||
|
expandedClusterMarkerRef.current !== clusterMarker
|
||||||
|
) {
|
||||||
|
expandedClusterMarkerRef.current.unspiderfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
clusterMarker.spiderfy();
|
||||||
|
|
||||||
|
// Sometimes there marker that's still bouncing from the last time the group was expanded.
|
||||||
|
// We stop it quickly.
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
stopBouncingMarkers();
|
||||||
|
}, 50);
|
||||||
|
} else {
|
||||||
|
// This is a standard marker, we bounce it.
|
||||||
|
const marker = visibleMarker as BouncingMarker;
|
||||||
|
if (!marker.isBouncing()) {
|
||||||
|
stopBouncingMarkers();
|
||||||
|
|
||||||
|
marker.bounce();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}, [hoveredListTournamentId]);
|
||||||
|
|
||||||
|
const onSpiderified = useCallback(
|
||||||
|
(e: L.MarkerClusterSpiderfyEvent) => {
|
||||||
|
// Once expanded, bounce the appropriate marker
|
||||||
|
|
||||||
|
if (hoveredListTournamentId) {
|
||||||
|
expandedClusterMarkerRef.current = e.cluster;
|
||||||
|
const markerRef = markerRefs.current[
|
||||||
|
hoveredListTournamentId
|
||||||
|
] as BouncingMarker;
|
||||||
|
|
||||||
|
if (markerRef && e.markers.includes(markerRef)) {
|
||||||
|
stopBouncingMarkers();
|
||||||
|
markerRef.bounce();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[hoveredListTournamentId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onUnSpiderified = useCallback(
|
||||||
|
(e: L.MarkerClusterSpiderfyEvent) => {
|
||||||
|
if (expandedClusterMarkerRef.current === e.cluster)
|
||||||
|
expandedClusterMarkerRef.current = null;
|
||||||
|
|
||||||
|
// Once closed, we can expand the next group if needed
|
||||||
|
expandAndBounceIfNeeded();
|
||||||
|
},
|
||||||
|
[expandAndBounceIfNeeded]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const ref = clusterRef.current;
|
||||||
|
|
||||||
|
if (clusterRef.current) {
|
||||||
|
clusterRef.current.on("spiderfied", onSpiderified);
|
||||||
|
clusterRef.current.on("unspiderfied", onUnSpiderified);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (ref) {
|
||||||
|
ref.off("spiderfied", onSpiderified);
|
||||||
|
ref.off("unspiderfied", onUnSpiderified);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [onSpiderified, onUnSpiderified]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Expand/contract as hoveredListTournamentId changes
|
||||||
|
if (expandAndBounceIfNeeded()) return;
|
||||||
|
|
||||||
|
if (expandedClusterMarkerRef.current)
|
||||||
|
expandedClusterMarkerRef.current.unspiderfy();
|
||||||
|
}, [expandAndBounceIfNeeded, hoveredListTournamentId]);
|
||||||
|
|
||||||
|
const createClusterCustomIcon = useCallback(
|
||||||
|
(cluster: any) => {
|
||||||
|
let childCount = cluster.getChildCount();
|
||||||
|
|
||||||
|
return new L.DivIcon({
|
||||||
|
html: "<div><span>" + childCount + "</span></div>",
|
||||||
|
className: `marker-cluster marker-cluster-${timeControl}`,
|
||||||
|
iconSize: new L.Point(40, 40),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[timeControl]
|
||||||
|
);
|
||||||
|
|
||||||
|
const markers = filteredTournaments.map((tournament) => {
|
||||||
return (
|
return (
|
||||||
<TournamentMarker
|
<TournamentMarker
|
||||||
|
ref={(ref) => (markerRefs.current[tournament._id] = ref)}
|
||||||
key={tournament._id}
|
key={tournament._id}
|
||||||
tournament={tournament}
|
tournament={tournament}
|
||||||
colour={colour}
|
colour={colour}
|
||||||
@@ -62,25 +222,47 @@ export default function TournamentMap() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return <LayerGroup>{layerGroup}</LayerGroup>;
|
const group = useMemo(
|
||||||
|
() => (
|
||||||
|
<LayerGroup>
|
||||||
|
<MarkerClusterGroup
|
||||||
|
ref={clusterRef}
|
||||||
|
chunkedLoading
|
||||||
|
iconCreateFunction={createClusterCustomIcon}
|
||||||
|
>
|
||||||
|
{markers}
|
||||||
|
</MarkerClusterGroup>
|
||||||
|
</LayerGroup>
|
||||||
|
),
|
||||||
|
[createClusterCustomIcon, markers]
|
||||||
|
);
|
||||||
|
|
||||||
|
return group;
|
||||||
};
|
};
|
||||||
|
|
||||||
const classicalMarkers = createLayerGroups(
|
export default function TournamentMap() {
|
||||||
TimeControl.Classic,
|
const setMapBounds = useSetAtom(mapBoundsAtom);
|
||||||
"green",
|
const hoveredListTournamentId = useAtomValue(
|
||||||
tournaments
|
debouncedHoveredListTournamentIdAtom
|
||||||
);
|
);
|
||||||
const rapidMarkers = createLayerGroups(
|
|
||||||
TimeControl.Rapid,
|
const classic = useAtomValue(classicAtom);
|
||||||
"blue",
|
const rapid = useAtomValue(rapidAtom);
|
||||||
tournaments
|
const blitz = useAtomValue(blitzAtom);
|
||||||
);
|
const other = useAtomValue(oneHourKOAtom);
|
||||||
const blitzMarkers = createLayerGroups(
|
|
||||||
TimeControl.Blitz,
|
useEffect(() => {
|
||||||
"yellow",
|
if (hoveredListTournamentId === null) {
|
||||||
tournaments
|
stopBouncingMarkers();
|
||||||
);
|
}
|
||||||
const otherMarkers = createLayerGroups(TimeControl.KO, "red", tournaments);
|
}, [hoveredListTournamentId]);
|
||||||
|
|
||||||
|
const center: LatLngLiteral = { lat: 47.0844, lng: 2.3964 };
|
||||||
|
|
||||||
|
const onScrollToTable = () => {
|
||||||
|
const tournamentTable = document.getElementById("tournament-table");
|
||||||
|
tournamentTable?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
@@ -111,10 +293,18 @@ export default function TournamentMap() {
|
|||||||
/>
|
/>
|
||||||
<Legend />
|
<Legend />
|
||||||
|
|
||||||
{classicalMarkers}
|
{classic && (
|
||||||
{rapidMarkers}
|
<TimeControlGroup timeControl={TimeControl.Classic} colour="green" />
|
||||||
{blitzMarkers}
|
)}
|
||||||
{otherMarkers}
|
{rapid && (
|
||||||
|
<TimeControlGroup timeControl={TimeControl.Rapid} colour="blue" />
|
||||||
|
)}
|
||||||
|
{blitz && (
|
||||||
|
<TimeControlGroup timeControl={TimeControl.Blitz} colour="yellow" />
|
||||||
|
)}
|
||||||
|
{other && (
|
||||||
|
<TimeControlGroup timeControl={TimeControl.KO} colour="red" />
|
||||||
|
)}
|
||||||
</MapContainer>
|
</MapContainer>
|
||||||
|
|
||||||
<div className="flex items-center justify-center lg:hidden">
|
<div className="flex items-center justify-center lg:hidden">
|
||||||
|
|||||||
@@ -1,77 +1,43 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useRef } from "react";
|
import { forwardRef, useMemo, useRef } from "react";
|
||||||
import { Tournament } from "@/types";
|
import { TimeControl, Tournament } from "@/types";
|
||||||
import L, { LatLngLiteral, DomUtil } from "leaflet";
|
import L from "leaflet";
|
||||||
import { Marker, Popup, MarkerProps } from "react-leaflet";
|
import { Marker, Popup, MarkerProps } from "react-leaflet";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useAtomValue, useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import "leaflet.smooth_marker_bouncing";
|
|
||||||
|
|
||||||
import {
|
import { debouncedHoveredMapTournamentIdAtom } from "@/app/atoms";
|
||||||
debouncedHoveredListTournamentIdAtom,
|
|
||||||
debouncedHoveredMapTournamentIdAtom,
|
|
||||||
} from "@/app/atoms";
|
|
||||||
|
|
||||||
const coordinateRandomisation = (latLng: LatLngLiteral): LatLngLiteral => {
|
|
||||||
const randomisation = () => Math.random() * (-0.01 - 0.01) + 0.01;
|
|
||||||
return {
|
|
||||||
lat: latLng.lat + randomisation(),
|
|
||||||
lng: latLng.lng + randomisation(),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type TournamentMarkerProps = {
|
type TournamentMarkerProps = {
|
||||||
tournament: Tournament;
|
tournament: Tournament;
|
||||||
colour: string;
|
colour: string;
|
||||||
} & Omit<MarkerProps, "position">;
|
} & Omit<MarkerProps, "position">;
|
||||||
|
|
||||||
export const TournamentMarker = ({
|
export const TournamentMarker = forwardRef<
|
||||||
tournament,
|
L.Marker<any> | null,
|
||||||
colour,
|
TournamentMarkerProps
|
||||||
...markerProps
|
>(({ tournament, colour, ...markerProps }, ref) => {
|
||||||
}: TournamentMarkerProps) => {
|
|
||||||
const t = useTranslations("Tournaments");
|
const t = useTranslations("Tournaments");
|
||||||
const markerRef = useRef<L.Marker<any> | null>(null);
|
|
||||||
const position = useRef(coordinateRandomisation(tournament.latLng));
|
|
||||||
|
|
||||||
const hoveredListTournamentId = useAtomValue(
|
// We add shifts based on the time control, so that they don't hide each other
|
||||||
debouncedHoveredListTournamentIdAtom
|
const position = useRef({
|
||||||
);
|
lat: tournament.latLng.lat,
|
||||||
|
lng:
|
||||||
|
tournament.latLng.lng +
|
||||||
|
(tournament.timeControl === TimeControl.Rapid
|
||||||
|
? -0.01
|
||||||
|
: tournament.timeControl === TimeControl.Blitz
|
||||||
|
? 0.01
|
||||||
|
: tournament.timeControl === TimeControl.KO
|
||||||
|
? 0.02
|
||||||
|
: 0),
|
||||||
|
});
|
||||||
|
|
||||||
const setHoveredMapTournamentId = useSetAtom(
|
const setHoveredMapTournamentId = useSetAtom(
|
||||||
debouncedHoveredMapTournamentIdAtom
|
debouncedHoveredMapTournamentIdAtom
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!markerRef.current) return;
|
|
||||||
if (hoveredListTournamentId === tournament._id) {
|
|
||||||
// @ts-ignore (the various bounce commands come from leaflet.smooth_marker_bouncing and aren't defined by the Typescript definitions)
|
|
||||||
markerRef.current.setBouncingOptions({ exclusive: true });
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
markerRef.current.bounce();
|
|
||||||
} else {
|
|
||||||
// @ts-ignore
|
|
||||||
if (markerRef.current.isBouncing()) {
|
|
||||||
// @ts-ignore
|
|
||||||
markerRef.current.stopBouncing();
|
|
||||||
|
|
||||||
// The plugin keeps bouncing until the end of the animation. We want to stop it immediately
|
|
||||||
// An issue has been raised on the project (https://github.com/hosuaby/Leaflet.SmoothMarkerBouncing/issues/52), until then we have this hack.
|
|
||||||
// We remove the class and reset some internal state
|
|
||||||
// @ts-ignore
|
|
||||||
DomUtil.removeClass(markerRef.current._icon, "bouncing");
|
|
||||||
|
|
||||||
if (
|
|
||||||
// @ts-ignore
|
|
||||||
markerRef.current?._bouncingMotion?.bouncingAnimationPlaying === true
|
|
||||||
)
|
|
||||||
// @ts-ignore
|
|
||||||
markerRef.current._bouncingMotion.bouncingAnimationPlaying = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [hoveredListTournamentId, tournament._id]);
|
|
||||||
|
|
||||||
const iconOptions = useMemo(
|
const iconOptions = useMemo(
|
||||||
() =>
|
() =>
|
||||||
new L.Icon({
|
new L.Icon({
|
||||||
@@ -87,7 +53,7 @@ export const TournamentMarker = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Marker
|
<Marker
|
||||||
ref={markerRef}
|
ref={ref}
|
||||||
position={position.current}
|
position={position.current}
|
||||||
icon={iconOptions}
|
icon={iconOptions}
|
||||||
eventHandlers={{
|
eventHandlers={{
|
||||||
@@ -108,4 +74,6 @@ export const TournamentMarker = ({
|
|||||||
</Popup>
|
</Popup>
|
||||||
</Marker>
|
</Marker>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
TournamentMarker.displayName = "TournamentMarker";
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ export const {
|
|||||||
|
|
||||||
export const {
|
export const {
|
||||||
debouncedValueAtom: debouncedHoveredListTournamentIdAtom,
|
debouncedValueAtom: debouncedHoveredListTournamentIdAtom,
|
||||||
} = atomWithDebounce<string | null>(null);
|
} = atomWithDebounce<string | null>(null, 300, true);
|
||||||
|
|
||||||
export const filteredTournamentsByTimeControlAtom = atom((get) => {
|
export const filteredTournamentsByTimeControlAtom = atom((get) => {
|
||||||
const tournaments = get(tournamentsAtom);
|
const tournaments = get(tournamentsAtom);
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
.marker-cluster-Classic {
|
||||||
|
background-color: rgba(145, 231, 135, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-cluster-Classic div {
|
||||||
|
background-color: rgba(36, 171, 33, 0.6)
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-cluster-Rapid {
|
||||||
|
background-color: rgba(85, 150, 201, 0.6)
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-cluster-Rapid div {
|
||||||
|
background-color: rgba(39, 128, 201, 0.6)
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-cluster-Blitz {
|
||||||
|
background-color: rgba(217, 211, 84, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-cluster-Blitz div {
|
||||||
|
background-color: rgba(202, 196, 39, 0.6)
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-cluster-KO {
|
||||||
|
background-color: rgba(211, 67, 84, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-cluster-KO div {
|
||||||
|
background-color: rgba(203, 41, 60, 0.6);
|
||||||
|
}
|
||||||
Generated
+34
@@ -19,6 +19,7 @@
|
|||||||
"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.smooth_marker_bouncing": "^3.0.3",
|
"leaflet.smooth_marker_bouncing": "^3.0.3",
|
||||||
"mongodb": "^5.6.0",
|
"mongodb": "^5.6.0",
|
||||||
"next": "^13.4.7",
|
"next": "^13.4.7",
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-icons": "^4.10.1",
|
"react-icons": "^4.10.1",
|
||||||
"react-leaflet": "^4.2.1",
|
"react-leaflet": "^4.2.1",
|
||||||
|
"react-leaflet-cluster": "^2.1.0",
|
||||||
"sharp": "^0.32.1",
|
"sharp": "^0.32.1",
|
||||||
"tailwind-merge": "^1.13.2",
|
"tailwind-merge": "^1.13.2",
|
||||||
"tailwindcss": "3.3.2",
|
"tailwindcss": "3.3.2",
|
||||||
@@ -39,6 +41,7 @@
|
|||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@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/nodemailer": "^6.4.8",
|
"@types/nodemailer": "^6.4.8",
|
||||||
"cypress": "^12.16.0",
|
"cypress": "^12.16.0",
|
||||||
"eslint-plugin-cypress": "^2.13.3"
|
"eslint-plugin-cypress": "^2.13.3"
|
||||||
@@ -923,6 +926,15 @@
|
|||||||
"@types/geojson": "*"
|
"@types/geojson": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/leaflet.markercluster": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/leaflet.markercluster/-/leaflet.markercluster-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-gzJzP10qO6Zkts5QNVmSAEDLYicQHTEBLT9HZpFrJiSww9eDAs5OWHvIskldf41MvDv1gbMukuEBQEawHn+wtA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/leaflet": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"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",
|
||||||
@@ -4584,6 +4596,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/leaflet-defaulticon-compatibility/-/leaflet-defaulticon-compatibility-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/leaflet-defaulticon-compatibility/-/leaflet-defaulticon-compatibility-0.1.1.tgz",
|
||||||
"integrity": "sha512-vDBFdlUAwjSEGep9ih8kfJilf6yN8V9zTbF5NC/1ZwLeGko3RUQepspPnGCRMFV51dY3Lb3hziboicrFz+rxQA=="
|
"integrity": "sha512-vDBFdlUAwjSEGep9ih8kfJilf6yN8V9zTbF5NC/1ZwLeGko3RUQepspPnGCRMFV51dY3Lb3hziboicrFz+rxQA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/leaflet.markercluster": {
|
||||||
|
"version": "1.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz",
|
||||||
|
"integrity": "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"leaflet": "^1.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/leaflet.smooth_marker_bouncing": {
|
"node_modules/leaflet.smooth_marker_bouncing": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/leaflet.smooth_marker_bouncing/-/leaflet.smooth_marker_bouncing-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/leaflet.smooth_marker_bouncing/-/leaflet.smooth_marker_bouncing-3.0.3.tgz",
|
||||||
@@ -5817,6 +5837,20 @@
|
|||||||
"react-dom": "^18.0.0"
|
"react-dom": "^18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-leaflet-cluster": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-leaflet-cluster/-/react-leaflet-cluster-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-16X7XQpRThQFC4PH4OpXHimGg19ouWmjxjtpxOeBKpvERSvIRqTx7fvhTwkEPNMFTQ8zTfddz6fRTUmUEQul7g==",
|
||||||
|
"dependencies": {
|
||||||
|
"leaflet.markercluster": "^1.5.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"leaflet": "^1.8.0",
|
||||||
|
"react": "^18.0.0",
|
||||||
|
"react-dom": "^18.0.0",
|
||||||
|
"react-leaflet": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/read-cache": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"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.smooth_marker_bouncing": "^3.0.3",
|
"leaflet.smooth_marker_bouncing": "^3.0.3",
|
||||||
"mongodb": "^5.6.0",
|
"mongodb": "^5.6.0",
|
||||||
"next": "^13.4.7",
|
"next": "^13.4.7",
|
||||||
@@ -34,6 +35,7 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-icons": "^4.10.1",
|
"react-icons": "^4.10.1",
|
||||||
"react-leaflet": "^4.2.1",
|
"react-leaflet": "^4.2.1",
|
||||||
|
"react-leaflet-cluster": "^2.1.0",
|
||||||
"sharp": "^0.32.1",
|
"sharp": "^0.32.1",
|
||||||
"tailwind-merge": "^1.13.2",
|
"tailwind-merge": "^1.13.2",
|
||||||
"tailwindcss": "3.3.2",
|
"tailwindcss": "3.3.2",
|
||||||
@@ -44,6 +46,7 @@
|
|||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@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/nodemailer": "^6.4.8",
|
"@types/nodemailer": "^6.4.8",
|
||||||
"cypress": "^12.16.0",
|
"cypress": "^12.16.0",
|
||||||
"eslint-plugin-cypress": "^2.13.3"
|
"eslint-plugin-cypress": "^2.13.3"
|
||||||
|
|||||||
Reference in New Issue
Block a user