mirror of
https://github.com/TheRealOwenRees/echecsfrance.git
synced 2026-07-23 04:26:57 +00:00
Global filters (#40)
This commit is contained in:
@@ -2,9 +2,10 @@ import { useMap } from "react-leaflet";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import L from "leaflet";
|
||||
import { TimeControl } from "@/types";
|
||||
|
||||
const Legend = () => {
|
||||
const t = useTranslations("App");
|
||||
const t = useTranslations("Tournaments");
|
||||
const map = useMap();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -20,16 +21,20 @@ const Legend = () => {
|
||||
);
|
||||
div.innerHTML = `<ul>
|
||||
<li><span style='background:#00ac39; display: block; height: 16px; width: 30px; border: 1px solid #999; float: left; margin-right: 5px'></span>${t(
|
||||
"tcClassic"
|
||||
"timeControlEnum",
|
||||
{ tc: TimeControl.Classic }
|
||||
)}</li>
|
||||
<li><span style='background:#0086c7; display: block; height: 16px; width: 30px; border: 1px solid #999; float: left; margin-right: 5px'></span>${t(
|
||||
"tcRapid"
|
||||
"timeControlEnum",
|
||||
{ tc: TimeControl.Rapid }
|
||||
)}</li>
|
||||
<li><span style='background:#cec348; display: block; height: 16px; width: 30px; border: 1px solid #999; float: left; margin-right: 5px'></span>${t(
|
||||
"tcBlitz"
|
||||
"timeControlEnum",
|
||||
{ tc: TimeControl.Blitz }
|
||||
)}</li>
|
||||
<li><span style='background:#d10c3e; display: block; height: 16px; width: 30px; border: 1px solid #999; float: left; margin-right: 5px'></span>${t(
|
||||
"tcKO"
|
||||
"timeControlEnum",
|
||||
{ tc: TimeControl.KO }
|
||||
)}</li>
|
||||
</ul>`;
|
||||
return div;
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useAtom } from "jotai";
|
||||
|
||||
import { classicAtom, rapidAtom, blitzAtom, oneHourKOAtom } from "@/app/atoms";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { TimeControl } from "@/types";
|
||||
|
||||
const TimeControlFilters = () => {
|
||||
const t = useTranslations("Tournaments");
|
||||
const classic = useAtom(classicAtom);
|
||||
const rapid = useAtom(rapidAtom);
|
||||
const blitz = useAtom(blitzAtom);
|
||||
const oneHourKO = useAtom(oneHourKOAtom);
|
||||
|
||||
const checkboxes = [
|
||||
{ title: t("timeControlEnum", { tc: TimeControl.Classic }), atom: classic },
|
||||
{ title: t("timeControlEnum", { tc: TimeControl.Rapid }), atom: rapid },
|
||||
{ title: t("timeControlEnum", { tc: TimeControl.Blitz }), atom: blitz },
|
||||
{ title: t("timeControlEnum", { tc: TimeControl.KO }), atom: oneHourKO },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{checkboxes.map((cb, i) => (
|
||||
<div key={i} className="text-gray-900 dark:text-white">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mr-2"
|
||||
checked={cb.atom[0]}
|
||||
onChange={() => cb.atom[1](!cb.atom[0])}
|
||||
/>
|
||||
{cb.title}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimeControlFilters;
|
||||
@@ -1,11 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { Tournament } from "@/types";
|
||||
import { TimeControl, Tournament } from "@/types";
|
||||
import { LatLngLiteral } from "leaflet";
|
||||
import {
|
||||
MapContainer,
|
||||
TileLayer,
|
||||
LayersControl,
|
||||
LayerGroup,
|
||||
useMapEvent,
|
||||
} from "react-leaflet";
|
||||
@@ -15,10 +14,14 @@ import "leaflet/dist/leaflet.css";
|
||||
import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css";
|
||||
import "leaflet-defaulticon-compatibility";
|
||||
|
||||
import { tournamentsAtom, mapBoundsAtom } from "@/app/atoms";
|
||||
import {
|
||||
filteredTournamentsByTimeControlAtom,
|
||||
mapBoundsAtom,
|
||||
} from "@/app/atoms";
|
||||
|
||||
import Legend from "./Legend";
|
||||
import { TournamentMarker } from "./TournamentMarker";
|
||||
import TimeControlFilters from "./TimeControlFilters";
|
||||
|
||||
const MapEvents = () => {
|
||||
const setMapBounds = useSetAtom(mapBoundsAtom);
|
||||
@@ -30,17 +33,17 @@ const MapEvents = () => {
|
||||
};
|
||||
|
||||
export default function TournamentMap() {
|
||||
const tournaments = useAtomValue(tournamentsAtom);
|
||||
const tournaments = useAtomValue(filteredTournamentsByTimeControlAtom);
|
||||
const setMapBounds = useSetAtom(mapBoundsAtom);
|
||||
const center: LatLngLiteral = { lat: 47.0844, lng: 2.3964 };
|
||||
|
||||
const createLayerGroups = (
|
||||
timeControl: string,
|
||||
timeControl: TimeControl,
|
||||
colour: string,
|
||||
tournaments: Tournament[]
|
||||
) => {
|
||||
const filteredTournaments = tournaments.filter(
|
||||
(t) => t.time_control === timeControl
|
||||
(t) => t.timeControl === timeControl
|
||||
);
|
||||
|
||||
const layerGroup = filteredTournaments.map((tournament) => {
|
||||
@@ -53,31 +56,41 @@ export default function TournamentMap() {
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<LayersControl.Overlay checked name={timeControl}>
|
||||
<LayerGroup>{layerGroup}</LayerGroup>
|
||||
</LayersControl.Overlay>
|
||||
);
|
||||
return <LayerGroup>{layerGroup}</LayerGroup>;
|
||||
};
|
||||
|
||||
const classicalMarkers = createLayerGroups(
|
||||
"Cadence Lente",
|
||||
TimeControl.Classic,
|
||||
"green",
|
||||
tournaments
|
||||
);
|
||||
|
||||
const rapidMarkers = createLayerGroups("Rapide", "blue", tournaments);
|
||||
const blitzMarkers = createLayerGroups("Blitz", "yellow", tournaments);
|
||||
const otherMarkers = createLayerGroups("1h KO", "red", tournaments);
|
||||
const rapidMarkers = createLayerGroups(
|
||||
TimeControl.Rapid,
|
||||
"blue",
|
||||
tournaments
|
||||
);
|
||||
const blitzMarkers = createLayerGroups(
|
||||
TimeControl.Blitz,
|
||||
"yellow",
|
||||
tournaments
|
||||
);
|
||||
const otherMarkers = createLayerGroups(TimeControl.KO, "red", tournaments);
|
||||
|
||||
return (
|
||||
<section id="tournament-map" className="grid h-[calc(100vh-144px)]">
|
||||
<section
|
||||
id="tournament-map"
|
||||
className="flex h-[calc(100vh-144px)] flex-col"
|
||||
>
|
||||
<div className="p-3 lg:hidden">
|
||||
<TimeControlFilters />
|
||||
</div>
|
||||
|
||||
<MapContainer
|
||||
center={center}
|
||||
zoom={5}
|
||||
scrollWheelZoom={false}
|
||||
style={{
|
||||
height: "100%",
|
||||
flexGrow: 1,
|
||||
}}
|
||||
ref={(map) => {
|
||||
if (map) {
|
||||
@@ -91,12 +104,11 @@ export default function TournamentMap() {
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
/>
|
||||
<Legend />
|
||||
<LayersControl>
|
||||
{classicalMarkers}
|
||||
{rapidMarkers}
|
||||
{blitzMarkers}
|
||||
{otherMarkers}
|
||||
</LayersControl>
|
||||
|
||||
{classicalMarkers}
|
||||
{rapidMarkers}
|
||||
{blitzMarkers}
|
||||
{otherMarkers}
|
||||
</MapContainer>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -13,11 +13,11 @@ import {
|
||||
debouncedHoveredMapTournamentIdAtom,
|
||||
} from "@/app/atoms";
|
||||
|
||||
const coordinateRandomisation = (lat: number, lng: number): LatLngLiteral => {
|
||||
const coordinateRandomisation = (latLng: LatLngLiteral): LatLngLiteral => {
|
||||
const randomisation = () => Math.random() * (-0.01 - 0.01) + 0.01;
|
||||
return {
|
||||
lat: lat + randomisation(),
|
||||
lng: lng + randomisation(),
|
||||
lat: latLng.lat + randomisation(),
|
||||
lng: latLng.lng + randomisation(),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -33,12 +33,7 @@ export const TournamentMarker = ({
|
||||
}: TournamentMarkerProps) => {
|
||||
const t = useTranslations("Tournaments");
|
||||
const markerRef = useRef<L.Marker<any> | null>(null);
|
||||
const position = useRef(
|
||||
coordinateRandomisation(
|
||||
tournament.coordinates[0],
|
||||
tournament.coordinates[1]
|
||||
)
|
||||
);
|
||||
const position = useRef(coordinateRandomisation(tournament.latLng));
|
||||
|
||||
const hoveredListTournamentId = useAtomValue(
|
||||
debouncedHoveredListTournamentIdAtom
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useAtomValue, useSetAtom, useAtom } from "jotai";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import {
|
||||
filteredTournamentsAtom,
|
||||
filteredTournamentsListAtom,
|
||||
syncVisibleAtom,
|
||||
hoveredMapTournamentIdAtom,
|
||||
debouncedHoveredMapTournamentIdAtom,
|
||||
@@ -13,13 +14,13 @@ import {
|
||||
} from "@/app/atoms";
|
||||
|
||||
import SearchBar from "./SearchBar";
|
||||
import TimeControlFilters from "./TimeControlFilters";
|
||||
import ScrollToTopButton from "./ScrollToTopButton";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function TournamentTable() {
|
||||
const t = useTranslations("Tournaments");
|
||||
|
||||
const filteredTournaments = useAtomValue(filteredTournamentsAtom);
|
||||
const filteredTournaments = useAtomValue(filteredTournamentsListAtom);
|
||||
const [syncVisible, setSyncVisible] = useAtom(syncVisibleAtom);
|
||||
const hoveredMapTournamentId = useAtomValue(hoveredMapTournamentIdAtom);
|
||||
const debouncedHoveredMapTournamentId = useAtomValue(
|
||||
@@ -50,12 +51,17 @@ export default function TournamentTable() {
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mr-2"
|
||||
checked={syncVisible}
|
||||
onChange={() => setSyncVisible(!syncVisible)}
|
||||
/>{" "}
|
||||
/>
|
||||
{t("syncWithMapCheckbox")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="hidden lg:block">
|
||||
<TimeControlFilters />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ScrollToTopButton />
|
||||
@@ -117,7 +123,7 @@ export default function TournamentTable() {
|
||||
</td>
|
||||
<td className="p-3">
|
||||
<a href={tournament.url} target="_blank">
|
||||
{tournament.time_control}
|
||||
{tournament.timeControl}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -38,7 +38,7 @@ export default async function TournamentsDisplay({
|
||||
|
||||
return (
|
||||
<main className="relative grid h-full w-full lg:grid-cols-2">
|
||||
<div className="">
|
||||
<div>
|
||||
<TournamentMap />
|
||||
</div>
|
||||
<div className="relative bg-white dark:bg-gray-800 lg:overflow-y-auto">
|
||||
|
||||
@@ -1,19 +1,38 @@
|
||||
import clientPromise from "@/lib/mongodb";
|
||||
|
||||
import { errorLog } from "@/utils/logger";
|
||||
import { Tournament } from "@/types";
|
||||
import { Tournament, TimeControl } from "@/types";
|
||||
|
||||
import TournamentsDisplay from "./TournamentsDisplay";
|
||||
import { ObjectId } from "mongodb";
|
||||
|
||||
export const revalidate = 3600; // Revalidate cache every 6 hours
|
||||
|
||||
export interface TournamentData {
|
||||
_id: ObjectId;
|
||||
town: string;
|
||||
department: string;
|
||||
tournament: string;
|
||||
url: string;
|
||||
time_control: "Cadence Lente" | "Rapide" | "Blitz" | "1h KO";
|
||||
date: string;
|
||||
coordinates: [number, number];
|
||||
}
|
||||
|
||||
const tcMap: Record<TournamentData["time_control"], TimeControl> = {
|
||||
"Cadence Lente": TimeControl.Classic,
|
||||
Rapide: TimeControl.Rapid,
|
||||
Blitz: TimeControl.Blitz,
|
||||
"1h KO": TimeControl.KO,
|
||||
};
|
||||
|
||||
const getTournaments = async () => {
|
||||
try {
|
||||
const client = await clientPromise;
|
||||
const db = client.db("tournamentsFranceDB");
|
||||
const data = await db
|
||||
.collection("tournaments")
|
||||
.aggregate<Tournament>([
|
||||
.aggregate<TournamentData>([
|
||||
{
|
||||
$addFields: {
|
||||
dateParts: {
|
||||
@@ -35,10 +54,11 @@ const getTournaments = async () => {
|
||||
])
|
||||
.toArray();
|
||||
|
||||
// We map the ObjectId to a string so that it can be serialized and sent to a client component
|
||||
return data.map((t) => ({
|
||||
return data.map<Tournament>((t) => ({
|
||||
...t,
|
||||
_id: t._id.toString(),
|
||||
timeControl: tcMap[t.time_control],
|
||||
latLng: { lat: t.coordinates[0], lng: t.coordinates[1] },
|
||||
}));
|
||||
} catch (error) {
|
||||
errorLog(error);
|
||||
|
||||
+27
-5
@@ -1,14 +1,19 @@
|
||||
import { Tournament } from '@/types';
|
||||
import { TimeControl, Tournament } from '@/types';
|
||||
import { atom } from 'jotai';
|
||||
import { LatLngBounds } from 'leaflet';
|
||||
|
||||
import atomWithDebounce from "@/utils/atomWithDebounce";
|
||||
|
||||
export const tournamentsAtom = atom<Tournament[]>([]);
|
||||
export const searchStringAtom = atom('');
|
||||
export const mapBoundsAtom = atom<LatLngBounds | null>(null);
|
||||
export const syncVisibleAtom = atom(true);
|
||||
|
||||
export const searchStringAtom = atom('');
|
||||
export const classicAtom = atom(true);
|
||||
export const rapidAtom = atom(true);
|
||||
export const blitzAtom = atom(true);
|
||||
export const oneHourKOAtom = atom(true);
|
||||
|
||||
export const {
|
||||
currentValueAtom: hoveredMapTournamentIdAtom,
|
||||
debouncedValueAtom: debouncedHoveredMapTournamentIdAtom,
|
||||
@@ -18,12 +23,29 @@ export const {
|
||||
debouncedValueAtom: debouncedHoveredListTournamentIdAtom,
|
||||
} = atomWithDebounce<string | null>(null);
|
||||
|
||||
export const filteredTournamentsAtom = atom((get) => {
|
||||
export const filteredTournamentsByTimeControlAtom = atom((get) => {
|
||||
const tournaments = get(tournamentsAtom);
|
||||
const searchString = get(searchStringAtom).trim();
|
||||
|
||||
const classic = get(classicAtom);
|
||||
const rapid = get(rapidAtom);
|
||||
const blitz = get(blitzAtom);
|
||||
const oneHourKO = get(oneHourKOAtom);
|
||||
|
||||
return tournaments.filter(tournament =>
|
||||
(tournament.timeControl === TimeControl.Classic && classic) ||
|
||||
(tournament.timeControl === TimeControl.Rapid && rapid) ||
|
||||
(tournament.timeControl === TimeControl.Blitz && blitz) ||
|
||||
(tournament.timeControl === TimeControl.KO && oneHourKO));
|
||||
});
|
||||
|
||||
export const filteredTournamentsListAtom = atom((get) => {
|
||||
const tournaments = get(filteredTournamentsByTimeControlAtom);
|
||||
const mapBounds = get(mapBoundsAtom);
|
||||
const syncVisible = get(syncVisibleAtom);
|
||||
|
||||
const searchString = get(searchStringAtom).trim();
|
||||
|
||||
|
||||
// When searching, we search all the tournament, regardless of the map display
|
||||
if (searchString !== '')
|
||||
return tournaments.filter((t) => t.town.includes(searchString.toUpperCase()))
|
||||
@@ -32,5 +54,5 @@ export const filteredTournamentsAtom = atom((get) => {
|
||||
if (mapBounds === null || !syncVisible) return tournaments;
|
||||
|
||||
// Filter by those in the current map bounds
|
||||
return tournaments.filter(tournament => mapBounds.contains({ lat: tournament.coordinates[0], lng: tournament.coordinates[1]}))
|
||||
return tournaments.filter(tournament => mapBounds.contains(tournament.latLng))
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user