Norms filter

This commit is contained in:
Timothy Armes
2023-07-17 11:51:10 +02:00
parent d3afc1d1f4
commit 22b8724e55
10 changed files with 98 additions and 11 deletions
+3
View File
@@ -28,6 +28,7 @@ import {
rapidAtom, rapidAtom,
blitzAtom, blitzAtom,
otherAtom, otherAtom,
normsOnlyAtom,
} from "@/app/atoms"; } from "@/app/atoms";
import Legend from "./Legend"; import Legend from "./Legend";
@@ -93,6 +94,8 @@ const stopBouncingMarkers = () => {
const TimeControlGroup = ({ timeControl, colour }: TimeControlGroupProps) => { const TimeControlGroup = ({ timeControl, colour }: TimeControlGroupProps) => {
const tournaments = useAtomValue(tournamentsAtom); const tournaments = useAtomValue(tournamentsAtom);
const normsOnly = useAtomValue(normsOnlyAtom);
const markerRefs = useRef<Record<string, L.Marker<any> | null>>({}); const markerRefs = useRef<Record<string, L.Marker<any> | null>>({});
const clusterRef = useRef<L.MarkerClusterGroup | null>(null); const clusterRef = useRef<L.MarkerClusterGroup | null>(null);
const expandedClusterMarkerRef = useRef<L.MarkerCluster | null>(null); const expandedClusterMarkerRef = useRef<L.MarkerCluster | null>(null);
+11 -4
View File
@@ -6,6 +6,7 @@ import L from "leaflet";
import { Marker, Popup, MarkerProps } from "react-leaflet"; 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 { debouncedHoveredMapTournamentIdAtom } from "@/app/atoms"; import { debouncedHoveredMapTournamentIdAtom } from "@/app/atoms";
@@ -63,14 +64,20 @@ export const TournamentMarker = forwardRef<
{...markerProps} {...markerProps}
> >
<Popup> <Popup>
<p> <div className="flex flex-col gap-3">
{tournament.date} <b>{tournament.date}</b>
<br />
<a href={tournament.url} target="_blank" rel="noopener noreferrer"> <a href={tournament.url} target="_blank" rel="noopener noreferrer">
{tournament.tournament} {tournament.tournament}
</a> </a>
</p> {tournament.norm && (
<div className="flex items-center">
<FaTrophy className="mr-3 h-4 w-4" />
{t("norm")}
</div>
)}
{t("approx")} {t("approx")}
</div>
</Popup> </Popup>
</Marker> </Marker>
); );
+34 -2
View File
@@ -5,10 +5,13 @@ import { useTranslations } from "next-intl";
import { useAtomValue, useSetAtom, useAtom } from "jotai"; import { useAtomValue, useSetAtom, useAtom } from "jotai";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { FaExternalLinkAlt } from "react-icons/fa"; import { FaExternalLinkAlt } from "react-icons/fa";
import { FaTrophy } from "react-icons/fa";
import { Tooltip } from "react-tooltip";
import { import {
filteredTournamentsListAtom, filteredTournamentsListAtom,
syncVisibleAtom, syncVisibleAtom,
normsOnlyAtom,
hoveredMapTournamentIdAtom, hoveredMapTournamentIdAtom,
debouncedHoveredMapTournamentIdAtom, debouncedHoveredMapTournamentIdAtom,
debouncedHoveredListTournamentIdAtom, debouncedHoveredListTournamentIdAtom,
@@ -24,6 +27,7 @@ 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 hoveredMapTournamentId = useAtomValue(hoveredMapTournamentIdAtom); const hoveredMapTournamentId = useAtomValue(hoveredMapTournamentIdAtom);
const debouncedHoveredMapTournamentId = useAtomValue( const debouncedHoveredMapTournamentId = useAtomValue(
debouncedHoveredMapTournamentIdAtom, debouncedHoveredMapTournamentIdAtom,
@@ -52,7 +56,8 @@ export default function TournamentTable() {
> >
<div className="z-10 flex w-full flex-wrap items-center justify-between gap-3 p-3"> <div className="z-10 flex w-full flex-wrap items-center justify-between gap-3 p-3">
<SearchBar /> <SearchBar />
<div className="text-gray-900 dark:text-white">
<div className="flex flex-col gap-0 text-gray-900 dark:text-white">
<label> <label>
<input <input
type="checkbox" type="checkbox"
@@ -62,6 +67,16 @@ export default function TournamentTable() {
/> />
{t("syncWithMapCheckbox")} {t("syncWithMapCheckbox")}
</label> </label>
<label>
<input
type="checkbox"
className="mr-2"
checked={normsOnly}
onChange={() => setNormsOnly(!normsOnly)}
/>
{t("normsOnly")}
</label>
</div> </div>
<div className="hidden lg:block"> <div className="hidden lg:block">
@@ -115,7 +130,17 @@ export default function TournamentTable() {
> >
<td className="p-3">{tournament.date}</td> <td className="p-3">{tournament.date}</td>
<td className="p-3">{tournament.town}</td> <td className="p-3">{tournament.town}</td>
<td className="p-3">{tournament.tournament}</td> <td className="p-3">
<span>
{tournament.norm && (
<FaTrophy
className="mr-2 inline-block h-4 w-4"
data-norm="norm"
/>
)}
{tournament.tournament}
</span>
</td>
<td className="p-3"> <td className="p-3">
{t("timeControlEnum", { tc: tournament.timeControl })} {t("timeControlEnum", { tc: tournament.timeControl })}
</td> </td>
@@ -129,6 +154,13 @@ export default function TournamentTable() {
)} )}
</tbody> </tbody>
</table> </table>
<Tooltip anchorSelect="[data-norm='norm']">
<div className="flex items-center">
<FaTrophy className="mr-3 h-4 w-4" />
{t("norm")}
</div>
</Tooltip>
</section> </section>
); );
} }
+2
View File
@@ -15,6 +15,7 @@ export interface TournamentData {
tournament: string; tournament: string;
url: string; url: string;
time_control: "Cadence Lente" | "Rapide" | "Blitz" | string; time_control: "Cadence Lente" | "Rapide" | "Blitz" | string;
norm_tournament: boolean;
date: string; date: string;
coordinates: [number, number]; coordinates: [number, number];
} }
@@ -58,6 +59,7 @@ const getTournaments = async () => {
_id: t._id.toString(), _id: t._id.toString(),
timeControl: tcMap[t.time_control] ?? TimeControl.Other, timeControl: tcMap[t.time_control] ?? TimeControl.Other,
latLng: { lat: t.coordinates[0], lng: t.coordinates[1] }, latLng: { lat: t.coordinates[0], lng: t.coordinates[1] },
norm: t.norm_tournament,
})); }));
} catch (error) { } catch (error) {
errorLog(error); errorLog(error);
+9 -4
View File
@@ -7,6 +7,7 @@ import atomWithDebounce from "@/utils/atomWithDebounce";
export const tournamentsAtom = atom<Tournament[]>([]); export const tournamentsAtom = atom<Tournament[]>([]);
export const mapBoundsAtom = atom<LatLngBounds | null>(null); export const mapBoundsAtom = atom<LatLngBounds | null>(null);
export const syncVisibleAtom = atom(true); export const syncVisibleAtom = atom(true);
export const normsOnlyAtom = atom(false);
export const searchStringAtom = atom(""); export const searchStringAtom = atom("");
export const classicAtom = atom(true); export const classicAtom = atom(true);
@@ -43,20 +44,24 @@ export const filteredTournamentsListAtom = atom((get) => {
const tournaments = get(filteredTournamentsByTimeControlAtom); const tournaments = get(filteredTournamentsByTimeControlAtom);
const mapBounds = get(mapBoundsAtom); const mapBounds = get(mapBoundsAtom);
const syncVisible = get(syncVisibleAtom); const syncVisible = get(syncVisibleAtom);
const normsOnly = get(normsOnlyAtom);
const searchString = get(searchStringAtom).trim(); const searchString = get(searchStringAtom).trim();
const filteredByNorm = normsOnly
? tournaments.filter((t) => t.norm)
: tournaments;
// When searching, we search all the tournament, regardless of the map display // When searching, we search all the tournament, regardless of the map display
if (searchString !== "") if (searchString !== "")
return tournaments.filter((t) => return filteredByNorm.filter((t) =>
t.town.includes(searchString.toUpperCase()), t.town.includes(searchString.toUpperCase()),
); );
// If we not syncing to the map, return all tournaments // If we not syncing to the map, return all tournaments
if (mapBounds === null || !syncVisible) return tournaments; if (mapBounds === null || !syncVisible) return filteredByNorm;
// Filter by those in the current map bounds // Filter by those in the current map bounds
return tournaments.filter((tournament) => return filteredByNorm.filter((tournament) =>
mapBounds.contains(tournament.latLng), mapBounds.contains(tournament.latLng),
); );
}); });
+2
View File
@@ -29,12 +29,14 @@
"searchLabel": "Search", "searchLabel": "Search",
"searchPlaceholder": "Search", "searchPlaceholder": "Search",
"syncWithMapCheckbox": "Tournaments visible on the map", "syncWithMapCheckbox": "Tournaments visible on the map",
"normsOnly": "Only norm tournaments",
"noneFound": "No tournaments found", "noneFound": "No tournaments found",
"date": "Date", "date": "Date",
"town": "Town", "town": "Town",
"tournament": "Tournament", "tournament": "Tournament",
"timeControl": "Time Control", "timeControl": "Time Control",
"approx": "Géo-localisation is approximative", "approx": "Géo-localisation is approximative",
"norm": "Norm Tournament",
"timeControlEnum": "{tc, select, Classic {Classic} Rapid {Rapid} Blitz {Blitz} Other {Other} other {{tc}}}", "timeControlEnum": "{tc, select, Classic {Classic} Rapid {Rapid} Blitz {Blitz} Other {Other} other {{tc}}}",
"clearButton": "Clear" "clearButton": "Clear"
}, },
+2
View File
@@ -29,12 +29,14 @@
"searchLabel": "Rechercher", "searchLabel": "Rechercher",
"searchPlaceholder": "Rechercher", "searchPlaceholder": "Rechercher",
"syncWithMapCheckbox": "Tournois visibles sur la carte", "syncWithMapCheckbox": "Tournois visibles sur la carte",
"normsOnly": "Uniquement tournois à normes",
"noneFound": "Pas de tournois trouvé", "noneFound": "Pas de tournois trouvé",
"date": "Date", "date": "Date",
"town": "Ville", "town": "Ville",
"tournament": "Tournois", "tournament": "Tournois",
"timeControl": "Cadence", "timeControl": "Cadence",
"approx": "Géolocalisation approximative", "approx": "Géolocalisation approximative",
"norm": "Tournoi à norme",
"timeControlEnum": "{tc, select, Classic {Cadence Lente} Rapid {Rapide} Blitz {Blitz} Other {Autres} other {{tc}}}", "timeControlEnum": "{tc, select, Classic {Cadence Lente} Rapid {Rapide} Blitz {Blitz} Other {Autres} other {{tc}}}",
"clearButton": "Effacer" "clearButton": "Effacer"
}, },
+32
View File
@@ -30,6 +30,7 @@
"react-icons": "^4.10.1", "react-icons": "^4.10.1",
"react-leaflet": "^4.2.1", "react-leaflet": "^4.2.1",
"react-leaflet-cluster": "^2.1.0", "react-leaflet-cluster": "^2.1.0",
"react-tooltip": "^5.18.0",
"sharp": "^0.32.1", "sharp": "^0.32.1",
"tailwind-merge": "^1.13.2", "tailwind-merge": "^1.13.2",
"tailwindcss": "3.3.2", "tailwindcss": "3.3.2",
@@ -327,6 +328,19 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
} }
}, },
"node_modules/@floating-ui/core": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.3.1.tgz",
"integrity": "sha512-Bu+AMaXNjrpjh41znzHqaz3r2Nr8hHuHZT6V2LBKMhyMl0FgKA62PNYbqnfgmzOhoWZj70Zecisbo4H1rotP5g=="
},
"node_modules/@floating-ui/dom": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.4.5.tgz",
"integrity": "sha512-96KnRWkRnuBSSFbj0sFGwwOUd8EkiecINVl0O9wiZlZ64EkpyAOG3Xc2vKKNJmru0Z7RqWNymA+6b8OZqjgyyw==",
"dependencies": {
"@floating-ui/core": "^1.3.1"
}
},
"node_modules/@formatjs/ecma402-abstract": { "node_modules/@formatjs/ecma402-abstract": {
"version": "1.17.0", "version": "1.17.0",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.17.0.tgz", "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.17.0.tgz",
@@ -1914,6 +1928,11 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/classnames": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
},
"node_modules/clean-stack": { "node_modules/clean-stack": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
@@ -6153,6 +6172,19 @@
"react-leaflet": "^4.0.0" "react-leaflet": "^4.0.0"
} }
}, },
"node_modules/react-tooltip": {
"version": "5.18.0",
"resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.18.0.tgz",
"integrity": "sha512-qjDK/skUJJ27sc9lTWeNxp2rLzmenBTskSsRiDOCPnupGSz2GhL5IZxDizK/sOsk0hn5iSCywt+3jKxUJ3Y4Sw==",
"dependencies": {
"@floating-ui/dom": "^1.0.0",
"classnames": "^2.3.0"
},
"peerDependencies": {
"react": ">=16.14.0",
"react-dom": ">=16.14.0"
}
},
"node_modules/read-cache": { "node_modules/read-cache": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+1
View File
@@ -35,6 +35,7 @@
"react-icons": "^4.10.1", "react-icons": "^4.10.1",
"react-leaflet": "^4.2.1", "react-leaflet": "^4.2.1",
"react-leaflet-cluster": "^2.1.0", "react-leaflet-cluster": "^2.1.0",
"react-tooltip": "^5.18.0",
"sharp": "^0.32.1", "sharp": "^0.32.1",
"tailwind-merge": "^1.13.2", "tailwind-merge": "^1.13.2",
"tailwindcss": "3.3.2", "tailwindcss": "3.3.2",
+1
View File
@@ -16,6 +16,7 @@ export interface Tournament {
timeControl: TimeControl; timeControl: TimeControl;
date: string; date: string;
latLng: LatLngLiteral; latLng: LatLngLiteral;
norm: boolean;
} }
export type ScrollableElement = Window | HTMLElement; export type ScrollableElement = Window | HTMLElement;