mirror of
https://github.com/TheRealOwenRees/echecsfrance.git
synced 2026-07-23 04:26:57 +00:00
Group tournaments into a single map marker based on position & date (#105)
This commit is contained in:
@@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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">
|
||||||
{tournament.tournament}
|
{tournamentGroup.map((tournament) => (
|
||||||
</a>
|
<a
|
||||||
{tournament.norm && (
|
key={tournament.id}
|
||||||
|
href={tournament.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{tournament.tournament}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</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>
|
||||||
|
|||||||
@@ -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",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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,
|
||||||
timeControl: tcMap[t.time_control] ?? TimeControl.Other,
|
(t) => `${t.coordinates[0]}_${t.coordinates[1]}`,
|
||||||
latLng: { lat: t.coordinates[0], lng: t.coordinates[1] },
|
);
|
||||||
norm: t.norm_tournament,
|
|
||||||
}));
|
// 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,
|
||||||
|
latLng: { lat: t.coordinates[0], lng: t.coordinates[1] },
|
||||||
|
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
@@ -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
@@ -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);
|
||||||
|
|||||||
Generated
+25
-3
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user