Global filters (#40)

This commit is contained in:
Timothy Armes
2023-07-05 17:27:53 +02:00
parent caaad4e229
commit 15ead0b877
11 changed files with 168 additions and 71 deletions
+10 -5
View File
@@ -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;
+32 -20
View File
@@ -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>
</MapContainer>
</section>
);
+4 -9
View File
@@ -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
+11 -5
View File
@@ -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>
+1 -1
View File
@@ -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">
+24 -4
View File
@@ -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
View File
@@ -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))
})
+2 -8
View File
@@ -17,13 +17,6 @@
"contactAria": "Contact Us"
},
"App": {
"tcClassic": "Classic",
"tcRapid": "Rapid",
"tcBlitz": "Blitz",
"tcKO": "1h KO"
},
"Home": {
"title": "France Echecs",
"purpose": "Find chess tournaments, in France, on a map",
@@ -41,7 +34,8 @@
"town": "Town",
"tournament": "Tournament",
"timeControl": "Time Control",
"approx": "Géo-localisation is approximative"
"approx": "Géo-localisation is approximative",
"timeControlEnum": "{tc, select, Classic {Classic} Rapid {Rapid} Blitz {Blitz} KO {1h KO} other {{tc}}}"
},
"About": {
+2 -8
View File
@@ -17,13 +17,6 @@
"contactAria": "Contactez-nous"
},
"App": {
"tcClassic": "Cadence Lente",
"tcRapid": "Rapide",
"tcBlitz": "Blitz",
"tcKO": "1h KO"
},
"Home": {
"title": "Échecs France",
"purpose": "Trouvez Vos Tournois d'Échecs en France Sur Une Carte",
@@ -41,7 +34,8 @@
"town": "Ville",
"tournament": "Tournois",
"timeControl": "Cadence",
"approx": "Géolocalisation approximative"
"approx": "Géolocalisation approximative",
"timeControlEnum": "{tc, select, Classic {Cadence Lente} Rapid {Rapide} Blitz {Blitz} KO {1h KO} other {{tc}}}"
},
"About": {
+11 -2
View File
@@ -1,12 +1,21 @@
import { LatLngLiteral } from "leaflet";
export enum TimeControl {
Classic = "Classic",
Rapid = "Rapid",
Blitz = "Blitz",
KO = "KO",
};
export interface Tournament {
_id: string;
town: string;
department: string;
tournament: string;
url: string;
time_control: string;
timeControl: TimeControl;
date: string;
coordinates: [number, number];
latLng: LatLngLiteral;
}
export type ScrollableElement = Window | HTMLElement;