diff --git a/app/[lang]/tournois/Legend.tsx b/app/[lang]/tournois/Legend.tsx index eaff350..8fbd361 100644 --- a/app/[lang]/tournois/Legend.tsx +++ b/app/[lang]/tournois/Legend.tsx @@ -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 = ``; return div; diff --git a/app/[lang]/tournois/TimeControlFilters.tsx b/app/[lang]/tournois/TimeControlFilters.tsx new file mode 100644 index 0000000..2e7bed6 --- /dev/null +++ b/app/[lang]/tournois/TimeControlFilters.tsx @@ -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 ( +
+ {checkboxes.map((cb, i) => ( +
+ +
+ ))} +
+ ); +}; + +export default TimeControlFilters; diff --git a/app/[lang]/tournois/TournamentMap.tsx b/app/[lang]/tournois/TournamentMap.tsx index f271f21..e1eb5a0 100644 --- a/app/[lang]/tournois/TournamentMap.tsx +++ b/app/[lang]/tournois/TournamentMap.tsx @@ -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 ( - - {layerGroup} - - ); + return {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 ( -
+
+
+ +
+ { if (map) { @@ -91,12 +104,11 @@ export default function TournamentMap() { url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" /> - - {classicalMarkers} - {rapidMarkers} - {blitzMarkers} - {otherMarkers} - + + {classicalMarkers} + {rapidMarkers} + {blitzMarkers} + {otherMarkers}
); diff --git a/app/[lang]/tournois/TournamentMarker.tsx b/app/[lang]/tournois/TournamentMarker.tsx index 996667e..7d295ca 100644 --- a/app/[lang]/tournois/TournamentMarker.tsx +++ b/app/[lang]/tournois/TournamentMarker.tsx @@ -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 | null>(null); - const position = useRef( - coordinateRandomisation( - tournament.coordinates[0], - tournament.coordinates[1] - ) - ); + const position = useRef(coordinateRandomisation(tournament.latLng)); const hoveredListTournamentId = useAtomValue( debouncedHoveredListTournamentIdAtom diff --git a/app/[lang]/tournois/TournamentTable.tsx b/app/[lang]/tournois/TournamentTable.tsx index e5a1498..afc23bf 100644 --- a/app/[lang]/tournois/TournamentTable.tsx +++ b/app/[lang]/tournois/TournamentTable.tsx @@ -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() { + +
+ +
@@ -117,7 +123,7 @@ export default function TournamentTable() { - {tournament.time_control} + {tournament.timeControl} diff --git a/app/[lang]/tournois/TournamentsDisplay.tsx b/app/[lang]/tournois/TournamentsDisplay.tsx index c9fd9dd..d4adf62 100644 --- a/app/[lang]/tournois/TournamentsDisplay.tsx +++ b/app/[lang]/tournois/TournamentsDisplay.tsx @@ -38,7 +38,7 @@ export default async function TournamentsDisplay({ return (
-
+
diff --git a/app/[lang]/tournois/page.tsx b/app/[lang]/tournois/page.tsx index cb97587..2263876 100644 --- a/app/[lang]/tournois/page.tsx +++ b/app/[lang]/tournois/page.tsx @@ -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 = { + "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([ + .aggregate([ { $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((t) => ({ ...t, _id: t._id.toString(), + timeControl: tcMap[t.time_control], + latLng: { lat: t.coordinates[0], lng: t.coordinates[1] }, })); } catch (error) { errorLog(error); diff --git a/app/atoms.ts b/app/atoms.ts index 65e3b95..ebc5657 100644 --- a/app/atoms.ts +++ b/app/atoms.ts @@ -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([]); -export const searchStringAtom = atom(''); export const mapBoundsAtom = atom(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(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)) }) diff --git a/messages/en.json b/messages/en.json index a1109b3..c1668c0 100644 --- a/messages/en.json +++ b/messages/en.json @@ -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": { diff --git a/messages/fr.json b/messages/fr.json index 2a06713..c1398db 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -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": { diff --git a/types.ts b/types.ts index a7c14c5..54152ad 100644 --- a/types.ts +++ b/types.ts @@ -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;