Merge pull request #56 from timothyarmes/feature/sync

Sync list to map (#42)
This commit is contained in:
Owen Rees
2023-07-05 14:04:11 +02:00
committed by GitHub
8 changed files with 48 additions and 10 deletions
+1
View File
@@ -9,6 +9,7 @@
"Française", "Française",
"Lente", "Lente",
"localisation", "localisation",
"moveend",
"randomisation", "randomisation",
"Rapide", "Rapide",
"Rees", "Rees",
+1 -1
View File
@@ -8,7 +8,7 @@ const SearchBar = () => {
const [searchString, setSearchString] = useAtom(searchStringAtom); const [searchString, setSearchString] = useAtom(searchStringAtom);
return ( return (
<div className="bg-white p-3 dark:bg-gray-800"> <div className="bg-white dark:bg-gray-800">
<label htmlFor="table-search" className="sr-only"> <label htmlFor="table-search" className="sr-only">
{t("searchLabel")} {t("searchLabel")}
</label> </label>
+13 -2
View File
@@ -7,18 +7,28 @@ import {
TileLayer, TileLayer,
LayersControl, LayersControl,
LayerGroup, LayerGroup,
useMapEvent,
} from "react-leaflet"; } from "react-leaflet";
import { useAtomValue } from "jotai"; 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 "leaflet-defaulticon-compatibility"; import "leaflet-defaulticon-compatibility";
import { tournamentsAtom } from "@/app/atoms"; import { tournamentsAtom, mapBoundsAtom } from "@/app/atoms";
import Legend from "./Legend"; import Legend from "./Legend";
import { TournamentMarker } from "./TournamentMarker"; import { TournamentMarker } from "./TournamentMarker";
const MapEvents = () => {
const setMapBounds = useSetAtom(mapBoundsAtom);
const map = useMapEvent("moveend", () => {
setMapBounds(map.getBounds());
});
return null;
};
export default function TournamentMap() { export default function TournamentMap() {
const tournaments = useAtomValue(tournamentsAtom); const tournaments = useAtomValue(tournamentsAtom);
const center: LatLngLiteral = { lat: 47.0844, lng: 2.3964 }; const center: LatLngLiteral = { lat: 47.0844, lng: 2.3964 };
@@ -68,6 +78,7 @@ export default function TournamentMap() {
height: "100%", height: "100%",
}} }}
> >
<MapEvents />
<TileLayer <TileLayer
attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors' attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
+16 -4
View File
@@ -1,11 +1,12 @@
"use client"; "use client";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useAtomValue, useSetAtom } from "jotai"; import { useAtomValue, useSetAtom, useAtom } from "jotai";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { import {
filteredTournamentsAtom, filteredTournamentsAtom,
syncVisibleAtom,
hoveredMapTournamentIdAtom, hoveredMapTournamentIdAtom,
debouncedHoveredMapTournamentIdAtom, debouncedHoveredMapTournamentIdAtom,
debouncedHoveredListTournamentIdAtom, debouncedHoveredListTournamentIdAtom,
@@ -19,6 +20,7 @@ export default function TournamentTable() {
const t = useTranslations("Tournaments"); const t = useTranslations("Tournaments");
const filteredTournaments = useAtomValue(filteredTournamentsAtom); const filteredTournaments = useAtomValue(filteredTournamentsAtom);
const [syncVisible, setSyncVisible] = useAtom(syncVisibleAtom);
const hoveredMapTournamentId = useAtomValue(hoveredMapTournamentIdAtom); const hoveredMapTournamentId = useAtomValue(hoveredMapTournamentIdAtom);
const debouncedHoveredMapTournamentId = useAtomValue( const debouncedHoveredMapTournamentId = useAtomValue(
debouncedHoveredMapTournamentIdAtom debouncedHoveredMapTournamentIdAtom
@@ -42,12 +44,22 @@ export default function TournamentTable() {
id="tournament-table" id="tournament-table"
data-test="tournament-table-div" data-test="tournament-table-div"
> >
<div className="z-10 flex"> <div className="z-10 flex w-full flex-wrap items-center justify-between gap-3 p-3">
<SearchBar /> <SearchBar />
<div> <div className="text-gray-900 dark:text-white">
<ScrollToTopButton /> <label>
<input
type="checkbox"
checked={syncVisible}
onChange={() => setSyncVisible(!syncVisible)}
/>{" "}
{t("syncWithMapCheckbox")}
</label>
</div> </div>
</div> </div>
<ScrollToTopButton />
<table <table
className="relative w-full table-fixed text-center text-xs" className="relative w-full table-fixed text-center text-xs"
data-test="tournament-table" data-test="tournament-table"
+14 -2
View File
@@ -1,10 +1,13 @@
import { Tournament } from '@/types'; import { Tournament } from '@/types';
import { atom } from 'jotai'; import { atom } from 'jotai';
import { LatLngBounds } from 'leaflet';
import atomWithDebounce from "@/utils/atomWithDebounce"; import atomWithDebounce from "@/utils/atomWithDebounce";
export const tournamentsAtom = atom<Tournament[]>([]); export const tournamentsAtom = atom<Tournament[]>([]);
export const searchStringAtom = atom(''); export const searchStringAtom = atom('');
export const mapBoundsAtom = atom<LatLngBounds | null>(null);
export const syncVisibleAtom = atom(true);
export const { export const {
currentValueAtom: hoveredMapTournamentIdAtom, currentValueAtom: hoveredMapTournamentIdAtom,
@@ -18,7 +21,16 @@ export const {
export const filteredTournamentsAtom = atom((get) => { export const filteredTournamentsAtom = atom((get) => {
const tournaments = get(tournamentsAtom); const tournaments = get(tournamentsAtom);
const searchString = get(searchStringAtom).trim(); const searchString = get(searchStringAtom).trim();
const mapBounds = get(mapBoundsAtom);
const syncVisible = get(syncVisibleAtom);
if (searchString === '') return tournaments; // When searching, we search all the tournament, regardless of the map display
return tournaments.filter((t) => t.town.includes(searchString.toUpperCase())) if (searchString !== '')
return tournaments.filter((t) => t.town.includes(searchString.toUpperCase()))
// If we not syncing to the map, return all tournaments
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]}))
}) })
+1
View File
@@ -35,6 +35,7 @@
"loading": "Loading...", "loading": "Loading...",
"searchLabel": "Search", "searchLabel": "Search",
"searchPlaceholder": "Search", "searchPlaceholder": "Search",
"syncWithMapCheckbox": "Tournaments visible on the map",
"noneFound": "No tournaments found", "noneFound": "No tournaments found",
"date": "Date", "date": "Date",
"town": "Town", "town": "Town",
+1
View File
@@ -35,6 +35,7 @@
"loading": "Téléchargement...", "loading": "Téléchargement...",
"searchLabel": "Rechercher", "searchLabel": "Rechercher",
"searchPlaceholder": "Rechercher", "searchPlaceholder": "Rechercher",
"syncWithMapCheckbox": "Tournois visibles sur la carte",
"noneFound": "Pas de tournois trouvé", "noneFound": "Pas de tournois trouvé",
"date": "Date", "date": "Date",
"town": "Ville", "town": "Ville",
+1 -1
View File
@@ -52,7 +52,7 @@ export default function atomWithDebounce<T>(
); );
// exported atom setter to clear timeout if needed // exported atom setter to clear timeout if needed
const clearTimeoutAtom = atom(null, (get, set, _arg) => { const clearTimeoutAtom = atom(null, (get, set) => {
clearTimeout(get(prevTimeoutAtom)); clearTimeout(get(prevTimeoutAtom));
set(isDebouncingAtom, false); set(isDebouncingAtom, false);
}); });