diff --git a/app/[locale]/tournois/TournamentMap.tsx b/app/[locale]/tournois/TournamentMap.tsx index afff505..a513f70 100644 --- a/app/[locale]/tournois/TournamentMap.tsx +++ b/app/[locale]/tournois/TournamentMap.tsx @@ -1,80 +1,32 @@ "use client"; -import { useCallback, useEffect, useMemo, useRef } from "react"; +import { useCallback, useMemo } from "react"; -import { useAtomValue, useSetAtom } from "jotai"; -import L, { DomUtil, LatLngLiteral, Marker } from "leaflet"; +import { useAtomValue } from "jotai"; +import L, { LatLngLiteral } from "leaflet"; import "leaflet-defaulticon-compatibility"; import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css"; import "leaflet.smooth_marker_bouncing"; import "leaflet/dist/leaflet.css"; import { countBy, groupBy } from "lodash"; -import { FaAngleDoubleDown } from "react-icons/fa"; -import { LayerGroup, MapContainer, TileLayer } from "react-leaflet"; -import MarkerClusterGroup from "react-leaflet-cluster"; import { - debouncedHoveredListTournamentIdAtom, filteredTournamentsByTimeControlAtom, - mapBoundsAtom, normsOnlyAtom, } from "@/app/atoms"; import { TimeControlColours } from "@/app/constants"; -import MapEvents from "@/components/MapEvents"; +import { Map, MapMarker } from "@/components/Map"; import { generatePieSVG } from "@/lib/pie"; import { TimeControl } from "@/types"; import Legend from "./Legend"; import TimeControlFilters from "./TimeControlFilters"; -import { TournamentMarker, TournamentMarkerRef } from "./TournamentMarker"; +import { TournamentMarker } from "./TournamentMarker"; -// Declare a class type that adds in methods etc. defined by leaflet.smooth_marker_bouncing -// to keep Typescript happy -declare class BouncingMarker extends Marker { - isBouncing(): boolean; - bounce(): void; - stopBouncing(): void; - - _icon: HTMLElement; - _bouncingMotion?: { - bouncingAnimationPlaying: boolean; - }; -} - -const stopBouncingMarkers = () => { - const markers = - // @ts-ignore - Marker.prototype._orchestration.getBouncingMarkers() as BouncingMarker[]; - - markers.forEach((marker) => { - if (marker.isBouncing()) { - try { - marker.stopBouncing(); - // The plugin keeps bouncing until the end of the animation. We want to stop it immediately - // An issue has been raised on the project (https://github.com/hosuaby/Leaflet.SmoothMarkerBouncing/issues/52), until then we have this hack. - // We remove the class and reset some internal state - - DomUtil.removeClass(marker._icon, "bouncing"); - if (marker?._bouncingMotion?.bouncingAnimationPlaying === true) - marker._bouncingMotion.bouncingAnimationPlaying = false; - } catch {} - } - }); -}; - -export default function TournamentMap() { +const TournamentMap = () => { const tournaments = useAtomValue(filteredTournamentsByTimeControlAtom); const normsOnly = useAtomValue(normsOnlyAtom); - const setMapBounds = useSetAtom(mapBoundsAtom); - const hoveredListTournamentId = useAtomValue( - debouncedHoveredListTournamentIdAtom, - ); - - const markerRefs = useRef>({}); - const clusterRef = useRef(null); - const expandedClusterMarkerRef = useRef(null); - const filteredTournaments = useMemo( () => tournaments.filter((t) => (normsOnly ? t.norm : true)), [normsOnly, tournaments], @@ -82,125 +34,6 @@ export default function TournamentMap() { const groupedTournaments = groupBy(filteredTournaments, (t) => t.groupId); - const center: LatLngLiteral = { lat: 47.0844, lng: 2.3964 }; - - const onScrollToTable = () => { - const tournamentTable = document.getElementById("tournament-table"); - tournamentTable?.scrollIntoView({ behavior: "smooth" }); - }; - - const expandAndBounceIfNeeded = useCallback(() => { - if (hoveredListTournamentId) { - const tournament = filteredTournaments.find( - (t) => t.id === hoveredListTournamentId, - ); - - if (tournament) { - const markerRef = markerRefs.current[tournament.groupId]; - if (markerRef) { - if (clusterRef.current) { - const visibleMarker = clusterRef.current.getVisibleParent( - markerRef.getMarker(), - ); - if (!visibleMarker) return; - - // @ts-ignore - if (visibleMarker.__proto__ === L.MarkerCluster.prototype) { - // This is a cluster icon, we expand it. - const clusterMarker = visibleMarker as L.MarkerCluster; - - if ( - expandedClusterMarkerRef.current && - expandedClusterMarkerRef.current !== clusterMarker - ) { - expandedClusterMarkerRef.current.unspiderfy(); - } - - clusterMarker.spiderfy(); - - // Sometimes there marker that's still bouncing from the last time the group was expanded. - // We stop it quickly. - - setTimeout(() => { - stopBouncingMarkers(); - }, 50); - } else { - // This is a standard marker, we bounce it. - const marker = visibleMarker as BouncingMarker; - if (!marker.isBouncing()) { - stopBouncingMarkers(); - - markerRef.bounce(); - } - } - - return true; - } - } - } - } - - stopBouncingMarkers(); - return false; - }, [filteredTournaments, hoveredListTournamentId]); - - const onSpiderified = useCallback( - (e: L.MarkerClusterSpiderfyEvent) => { - // Once expanded, bounce the appropriate marker - - if (hoveredListTournamentId) { - const tournament = filteredTournaments.find( - (t) => t.id === hoveredListTournamentId, - ); - if (!tournament) return; - - expandedClusterMarkerRef.current = e.cluster; - const markerRef = markerRefs.current[tournament.groupId]; - - if (markerRef && e.markers.includes(markerRef.getMarker())) { - stopBouncingMarkers(); - markerRef.bounce(); - } - } - }, - [filteredTournaments, hoveredListTournamentId], - ); - - const onUnSpiderified = useCallback( - (e: L.MarkerClusterSpiderfyEvent) => { - if (expandedClusterMarkerRef.current === e.cluster) - expandedClusterMarkerRef.current = null; - - // Once closed, we can expand the next group if needed - expandAndBounceIfNeeded(); - }, - [expandAndBounceIfNeeded], - ); - - useEffect(() => { - const ref = clusterRef.current; - - if (clusterRef.current) { - clusterRef.current.on("spiderfied", onSpiderified); - clusterRef.current.on("unspiderfied", onUnSpiderified); - } - - return () => { - if (ref) { - ref.off("spiderfied", onSpiderified); - ref.off("unspiderfied", onUnSpiderified); - } - }; - }, [onSpiderified, onUnSpiderified]); - - useEffect(() => { - // Expand/contract as hoveredListTournamentId changes - if (expandAndBounceIfNeeded()) return; - - if (expandedClusterMarkerRef.current) - expandedClusterMarkerRef.current.unspiderfy(); - }, [expandAndBounceIfNeeded, hoveredListTournamentId]); - const createClusterCustomIcon = useCallback((cluster: any) => { const childCount = cluster.getChildCount(); const children = cluster.getAllChildMarkers(); @@ -237,71 +70,29 @@ export default function TournamentMap() { }); }, []); - const markers = useMemo( + const markers: MapMarker[] = useMemo( () => Object.values(groupedTournaments).map((tournamentGroup) => { const { groupId } = tournamentGroup[0]; - return ( - { - markerRefs.current[groupId] = ref!; - }} - key={groupId} - tournamentGroup={tournamentGroup} - /> - ); + return { + markerId: groupId, + tableIds: tournamentGroup.map((t) => t.id), + component: ( + + ), + }; }), [groupedTournaments], ); return ( -
-
- -
- - { - if (map) { - setMapBounds(map.getBounds()); - } - }} - > - - - - - - {markers} - - - $ - - -
- -
-
+ } + filters={} + iconCreateFunction={createClusterCustomIcon} + /> ); -} +}; + +export default TournamentMap; diff --git a/app/[locale]/tournois/TournamentMarker.tsx b/app/[locale]/tournois/TournamentMarker.tsx index a70fb5b..75d7423 100644 --- a/app/[locale]/tournois/TournamentMarker.tsx +++ b/app/[locale]/tournois/TournamentMarker.tsx @@ -9,109 +9,102 @@ import { useTranslations } from "next-intl"; import { FaTrophy } from "react-icons/fa"; import { Marker, MarkerProps, Popup } from "react-leaflet"; -import { debouncedHoveredMapTournamentGroupIdAtom } from "@/app/atoms"; +import { debouncedHoveredMapIdAtom } from "@/app/atoms"; import { TimeControlColours } from "@/app/constants"; +import type { MarkerRef } from "@/components/Map"; import type { BouncingMarker } from "@/leafletTypes"; import { Tournament } from "@/types"; -export type TournamentMarkerRef = { - getMarker: () => L.Marker; - bounce: () => void; -}; - type TournamentMarkerProps = { tournamentGroup: Tournament[]; } & Omit; -export const TournamentMarker = forwardRef< - TournamentMarkerRef, - TournamentMarkerProps ->(({ tournamentGroup, ...markerProps }, ref) => { - const t = useTranslations("Tournaments"); - const markerRef = useRef | null>(null); +export const TournamentMarker = forwardRef( + ({ tournamentGroup, ...markerProps }, ref) => { + const t = useTranslations("Tournaments"); + const markerRef = useRef | null>(null); - useImperativeHandle( - ref, - () => ({ - getMarker: () => markerRef.current!, - bounce: () => { - const bouncingMarker = markerRef.current as BouncingMarker; - bouncingMarker.setBouncingOptions({ contractHeight: 0 }); - bouncingMarker.bounce(); - }, - }), - [], - ); + useImperativeHandle( + ref, + () => ({ + getMarker: () => markerRef.current!, + bounce: () => { + const bouncingMarker = markerRef.current as BouncingMarker; + bouncingMarker.setBouncingOptions({ contractHeight: 0 }); + bouncingMarker.bounce(); + }, + }), + [], + ); - const { date, latLng, groupId, timeControl } = tournamentGroup[0]; + const { date, latLng, groupId, timeControl } = tournamentGroup[0]; - const setHoveredMapTournamentGroupId = useSetAtom( - debouncedHoveredMapTournamentGroupIdAtom, - ); + const setHoveredMapId = useSetAtom(debouncedHoveredMapIdAtom); - const iconOptions = useMemo( - () => - new L.DivIcon({ - html: ` + const iconOptions = useMemo( + () => + new L.DivIcon({ + html: ` `, - className: timeControl, - iconSize: [24, 40], - iconAnchor: [12, 40], - popupAnchor: [1, -40], - }), - [timeControl], - ); + className: timeControl, + iconSize: [24, 40], + iconAnchor: [12, 40], + popupAnchor: [1, -40], + }), + [timeControl], + ); - const startDate = date; - const endDate = last(tournamentGroup)!.date; + const startDate = date; + const endDate = last(tournamentGroup)!.date; - return ( - setHoveredMapTournamentGroupId(groupId), - mouseout: () => setHoveredMapTournamentGroupId(null), - }} - {...markerProps} - > - -
- - {date} - {endDate !== startDate && ` - ${endDate}`} - + return ( + setHoveredMapId(groupId), + mouseout: () => setHoveredMapId(null), + }} + {...markerProps} + > + +
+ + {date} + {endDate !== startDate && ` - ${endDate}`} + -
- {tournamentGroup.map((tournament) => ( - - {tournament.tournament} - - ))} -
- - {tournamentGroup.some((t) => t.norm) && ( -
- - {t("norm")} +
+ {tournamentGroup.map((tournament) => ( + + {tournament.tournament} + + ))}
- )} - {t("approx")} -
- - - ); -}); + {tournamentGroup.some((t) => t.norm) && ( +
+ + {t("norm")} +
+ )} + + {t("approx")} +
+
+
+ ); + }, +); TournamentMarker.displayName = "TournamentMarker"; diff --git a/app/[locale]/tournois/TournamentTable.tsx b/app/[locale]/tournois/TournamentTable.tsx index 808ee22..13a66bc 100644 --- a/app/[locale]/tournois/TournamentTable.tsx +++ b/app/[locale]/tournois/TournamentTable.tsx @@ -10,52 +10,45 @@ import { Tooltip } from "react-tooltip"; import { twMerge } from "tailwind-merge"; import { - debouncedHoveredListTournamentIdAtom, - debouncedHoveredMapTournamentGroupIdAtom, + debouncedHoveredListIdAtom, + debouncedHoveredMapIdAtom, filteredTournamentsListAtom, - hoveredMapTournamentGroupIdAtom, + hoveredMapIdAtom, normsOnlyAtom, syncVisibleAtom, } from "@/app/atoms"; +import ScrollToTopButton from "@/components/ScrollToTopButton"; +import SearchBar from "@/components/SearchBar"; import { useBreakpoint } from "@/hooks/tailwind"; -import ScrollToTopButton from "./ScrollToTopButton"; -import SearchBar from "./SearchBar"; import TimeControlFilters from "./TimeControlFilters"; -export default function TournamentTable() { +const TournamentTable = () => { const t = useTranslations("Tournaments"); const at = useTranslations("App"); const filteredTournaments = useAtomValue(filteredTournamentsListAtom); const [syncVisible, setSyncVisible] = useAtom(syncVisibleAtom); const [normsOnly, setNormsOnly] = useAtom(normsOnlyAtom); - const hoveredMapTournamentGroupId = useAtomValue( - hoveredMapTournamentGroupIdAtom, - ); - const debouncedHoveredMapTournamentGroupId = useAtomValue( - debouncedHoveredMapTournamentGroupIdAtom, - ); - const setHoveredListTournamentId = useSetAtom( - debouncedHoveredListTournamentIdAtom, - ); + const hoveredMapId = useAtomValue(hoveredMapIdAtom); + const debouncedHoveredMapId = useAtomValue(debouncedHoveredMapIdAtom); + const setHoveredListId = useSetAtom(debouncedHoveredListIdAtom); const isLg = useBreakpoint("lg"); useEffect(() => { - if (!isLg || debouncedHoveredMapTournamentGroupId === null) return; + if (!isLg || debouncedHoveredMapId === null) return; const tournamentRow = document.querySelector( - `[data-group-id="${debouncedHoveredMapTournamentGroupId}"]`, + `[data-group-id="${debouncedHoveredMapId}"]`, ); tournamentRow?.scrollIntoView({ behavior: "smooth" }); - }, [debouncedHoveredMapTournamentGroupId, isLg]); + }, [debouncedHoveredMapId, isLg]); return (
@@ -90,10 +83,7 @@ export default function TournamentTable() {
- +
- + @@ -125,11 +117,11 @@ export default function TournamentTable() { key={tournament.id} id={tournament.id} data-group-id={tournament.groupId} - onMouseEnter={() => setHoveredListTournamentId(tournament.id)} - onMouseLeave={() => setHoveredListTournamentId(null)} + onMouseEnter={() => setHoveredListId(tournament.id)} + onMouseLeave={() => setHoveredListId(null)} className={twMerge( "scroll-m-20 bg-white text-gray-900 hover:bg-gray-200 dark:bg-gray-800 dark:text-white dark:hover:bg-gray-900", - hoveredMapTournamentGroupId === tournament.groupId && + hoveredMapId === tournament.groupId && "bg-gray-200 dark:bg-gray-900", )} > @@ -139,7 +131,7 @@ export default function TournamentTable() { - )) @@ -173,4 +171,6 @@ export default function TournamentTable() { ); -} +}; + +export default TournamentTable; diff --git a/app/atoms.ts b/app/atoms.ts index f80e7cc..a0521fa 100644 --- a/app/atoms.ts +++ b/app/atoms.ts @@ -1,32 +1,35 @@ import { atom } from "jotai"; import { LatLngBounds, LatLngLiteral } from "leaflet"; -import { TimeControl, Tournament } from "@/types"; +import { Club, TimeControl, Tournament } from "@/types"; import atomWithDebounce from "@/utils/atomWithDebounce"; import { normalizedContains } from "@/utils/string"; -export const tournamentsAtom = atom([]); export const mapBoundsAtom = atom(null); export const syncVisibleAtom = atom(true); +export const searchStringAtom = atom(""); + +export const tournamentsAtom = atom([]); export const normsOnlyAtom = atom(false); -export const searchStringAtom = atom(""); export const classicAtom = atom(true); export const rapidAtom = atom(true); export const blitzAtom = atom(true); export const otherAtom = atom(true); +export const clubsAtom = atom([]); + export const franceCenterAtom = atom({ lat: 47.0844, lng: 2.3964, }); export const { - currentValueAtom: hoveredMapTournamentGroupIdAtom, - debouncedValueAtom: debouncedHoveredMapTournamentGroupIdAtom, + currentValueAtom: hoveredMapIdAtom, + debouncedValueAtom: debouncedHoveredMapIdAtom, } = atomWithDebounce(null); -export const { debouncedValueAtom: debouncedHoveredListTournamentIdAtom } = +export const { debouncedValueAtom: debouncedHoveredListIdAtom } = atomWithDebounce(null, 1000, 100); export const filteredTournamentsByTimeControlAtom = atom((get) => { @@ -39,7 +42,8 @@ export const filteredTournamentsByTimeControlAtom = atom((get) => { return tournaments.filter( (tournament) => - !tournament.pending && tournament.status === 'scheduled' && + !tournament.pending && + tournament.status === "scheduled" && ((tournament.timeControl === TimeControl.Classic && classic) || (tournament.timeControl === TimeControl.Rapid && rapid) || (tournament.timeControl === TimeControl.Blitz && blitz) || diff --git a/components/Map.tsx b/components/Map.tsx new file mode 100644 index 0000000..7c95170 --- /dev/null +++ b/components/Map.tsx @@ -0,0 +1,268 @@ +"use client"; + +import { useCallback, useEffect, useMemo, useRef } from "react"; +import React from "react"; + +import { useAtomValue, useSetAtom } from "jotai"; +import L, { + DomUtil, + LatLngLiteral, + Marker, + MarkerClusterGroupOptions, +} from "leaflet"; +import "leaflet-defaulticon-compatibility"; +import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css"; +import "leaflet.smooth_marker_bouncing"; +import "leaflet/dist/leaflet.css"; +import { FaAngleDoubleDown } from "react-icons/fa"; +import { LayerGroup, MapContainer, TileLayer } from "react-leaflet"; +import MarkerClusterGroup from "react-leaflet-cluster"; + +import { debouncedHoveredListIdAtom, mapBoundsAtom } from "@/app/atoms"; +import MapEvents from "@/components/MapEvents"; + +export type MarkerRef = { + getMarker: () => L.Marker; + bounce: () => void; +}; + +// Declare a class type that adds in methods etc. defined by leaflet.smooth_marker_bouncing +// to keep Typescript happy +declare class BouncingMarker extends Marker { + isBouncing(): boolean; + bounce(): void; + stopBouncing(): void; + + _icon: HTMLElement; + _bouncingMotion?: { + bouncingAnimationPlaying: boolean; + }; +} + +const stopBouncingMarkers = () => { + const markers = + // @ts-ignore + Marker.prototype._orchestration.getBouncingMarkers() as BouncingMarker[]; + + markers.forEach((marker) => { + if (marker.isBouncing()) { + try { + marker.stopBouncing(); + // The plugin keeps bouncing until the end of the animation. We want to stop it immediately + // An issue has been raised on the project (https://github.com/hosuaby/Leaflet.SmoothMarkerBouncing/issues/52), until then we have this hack. + // We remove the class and reset some internal state + + DomUtil.removeClass(marker._icon, "bouncing"); + if (marker?._bouncingMotion?.bouncingAnimationPlaying === true) + marker._bouncingMotion.bouncingAnimationPlaying = false; + } catch {} + } + }); +}; + +export type MapMarker = { + markerId: string; + tableIds: string[]; + component: React.ReactElement; +}; + +type MapProps = { + filters?: React.ReactNode; + legend?: React.ReactNode; + markers: MapMarker[]; + iconCreateFunction?: MarkerClusterGroupOptions["iconCreateFunction"]; +}; + +export const Map = ({ + filters, + legend, + markers: markers, + iconCreateFunction, +}: MapProps) => { + const setMapBounds = useSetAtom(mapBoundsAtom); + + const hoveredListTournamentId = useAtomValue(debouncedHoveredListIdAtom); + + const markerRefs = useRef>({}); + const clusterRef = useRef(null); + const expandedClusterMarkerRef = useRef(null); + + const center: LatLngLiteral = { lat: 47.0844, lng: 2.3964 }; + + const onScrollToTable = () => { + const tournamentTable = document.getElementById("listing"); + tournamentTable?.scrollIntoView({ behavior: "smooth" }); + }; + + const expandAndBounceIfNeeded = useCallback(() => { + if (hoveredListTournamentId) { + const marker = markers.find((m) => + m.tableIds.includes(hoveredListTournamentId), + ); + + if (marker) { + const markerRef = markerRefs.current[marker.markerId]; + if (markerRef) { + if (clusterRef.current) { + const visibleMarker = clusterRef.current.getVisibleParent( + markerRef.getMarker(), + ); + if (!visibleMarker) return; + + // @ts-ignore + if (visibleMarker.__proto__ === L.MarkerCluster.prototype) { + // This is a cluster icon, we expand it. + const clusterMarker = visibleMarker as L.MarkerCluster; + + if ( + expandedClusterMarkerRef.current && + expandedClusterMarkerRef.current !== clusterMarker + ) { + expandedClusterMarkerRef.current.unspiderfy(); + } + + clusterMarker.spiderfy(); + + // Sometimes there marker that's still bouncing from the last time the group was expanded. + // We stop it quickly. + + setTimeout(() => { + stopBouncingMarkers(); + }, 50); + } else { + // This is a standard marker, we bounce it. + const marker = visibleMarker as BouncingMarker; + if (!marker.isBouncing()) { + stopBouncingMarkers(); + + markerRef.bounce(); + } + } + + return true; + } + } + } + } + + stopBouncingMarkers(); + return false; + }, [markers, hoveredListTournamentId]); + + const onSpiderified = useCallback( + (e: L.MarkerClusterSpiderfyEvent) => { + // Once expanded, bounce the appropriate marker + + if (hoveredListTournamentId) { + const marker = markers.find((m) => + m.tableIds.includes(hoveredListTournamentId), + ); + if (!marker) return; + + expandedClusterMarkerRef.current = e.cluster; + const markerRef = markerRefs.current[marker.markerId]; + + if (markerRef && e.markers.includes(markerRef.getMarker())) { + stopBouncingMarkers(); + markerRef.bounce(); + } + } + }, + [markers, hoveredListTournamentId], + ); + + const onUnSpiderified = useCallback( + (e: L.MarkerClusterSpiderfyEvent) => { + if (expandedClusterMarkerRef.current === e.cluster) + expandedClusterMarkerRef.current = null; + + // Once closed, we can expand the next group if needed + expandAndBounceIfNeeded(); + }, + [expandAndBounceIfNeeded], + ); + + useEffect(() => { + const ref = clusterRef.current; + + if (clusterRef.current) { + clusterRef.current.on("spiderfied", onSpiderified); + clusterRef.current.on("unspiderfied", onUnSpiderified); + } + + return () => { + if (ref) { + ref.off("spiderfied", onSpiderified); + ref.off("unspiderfied", onUnSpiderified); + } + }; + }, [onSpiderified, onUnSpiderified]); + + useEffect(() => { + // Expand/contract as hoveredListTournamentId changes + if (expandAndBounceIfNeeded()) return; + + if (expandedClusterMarkerRef.current) + expandedClusterMarkerRef.current.unspiderfy(); + }, [expandAndBounceIfNeeded, hoveredListTournamentId]); + + const referencedMarkers = useMemo( + () => + markers.map((marker) => + React.cloneElement(marker.component, { + ref: (ref: MarkerRef) => { + markerRefs.current[marker.markerId] = ref; + }, + }), + ), + [markers], + ); + + return ( +
+
{filters}
+ + { + if (map) { + setMapBounds(map.getBounds()); + } + }} + > + + + {legend} + + + {referencedMarkers} + + + + +
+ +
+
+ ); +}; diff --git a/app/[locale]/tournois/ScrollToTopButton.tsx b/components/ScrollToTopButton.tsx similarity index 91% rename from app/[locale]/tournois/ScrollToTopButton.tsx rename to components/ScrollToTopButton.tsx index 1c81262..167e48e 100644 --- a/app/[locale]/tournois/ScrollToTopButton.tsx +++ b/components/ScrollToTopButton.tsx @@ -15,8 +15,7 @@ const ScrollToTopButton = () => { // determine scrollable element based on screen size - window or div useEffect(() => { isLgScreen - ? (scrollToTopElementRef.current = - document.getElementById("tournament-table")) + ? (scrollToTopElementRef.current = document.getElementById("listing")) : (scrollToTopElementRef.current = window); }, [isLgScreen]); diff --git a/app/[locale]/tournois/SearchBar.tsx b/components/SearchBar.tsx similarity index 100% rename from app/[locale]/tournois/SearchBar.tsx rename to components/SearchBar.tsx
@@ -108,7 +98,9 @@ export default function TournamentTable() { {t("timeControl")} + {t("ffe")} +
{tournament.town} + {tournament.norm && ( - - - +
+ + + +