Fix layout issues & co-locate components (#46)

* Fix layout issues & co-locate components

* Fix front page

* Fix centering
This commit is contained in:
Timothy Armes
2023-07-04 13:26:25 +02:00
committed by GitHub
parent a9822b65fe
commit 75a6c74306
26 changed files with 306 additions and 305 deletions
+44
View File
@@ -0,0 +1,44 @@
import { useMap } from "react-leaflet";
import { useEffect } from "react";
import { useTranslations } from "next-intl";
import L from "leaflet";
const Legend = () => {
const t = useTranslations("App");
const map = useMap();
useEffect(() => {
if (map) {
// @ts-ignore
const legend = L.control({ position: "bottomleft" });
legend.onAdd = () => {
const div = L.DomUtil.create("div", "map-legend");
div.setAttribute(
"style",
"background: white; color: black; border: 2px solid grey; border-radius: 6px; padding: 10px;"
);
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"
)}</li>
<li><span style='background:#0086c7; display: block; height: 16px; width: 30px; border: 1px solid #999; float: left; margin-right: 5px'></span>${t(
"tcRapid"
)}</li>
<li><span style='background:#cec348; display: block; height: 16px; width: 30px; border: 1px solid #999; float: left; margin-right: 5px'></span>${t(
"tcBlitz"
)}</li>
<li><span style='background:#d10c3e; display: block; height: 16px; width: 30px; border: 1px solid #999; float: left; margin-right: 5px'></span>${t(
"tcKO"
)}</li>
</ul>`;
return div;
};
legend.addTo(map);
}
}, [map, t]);
return null;
};
export default Legend;
+48
View File
@@ -0,0 +1,48 @@
"use client";
import { ScrollableElement } from "@/types";
import { FaArrowUp } from "react-icons/fa";
import { handleScrollToTop } from "@/handlers/scrollHandlers";
import { useEffect, useRef, useState } from "react";
const ScrollToTopButton = () => {
const scrollToTopElementRef = useRef<ScrollableElement | null>(null);
const [isLgScreen, setIsLgScreen] = useState(false);
// calculate screen size
useEffect(() => {
const handleResize = () => {
setIsLgScreen(window.innerWidth >= 1024);
};
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
});
// determine scrollable element based on screen size - window or div
useEffect(() => {
isLgScreen
? (scrollToTopElementRef.current =
document.getElementById("tournament-table"))
: (scrollToTopElementRef.current = window);
}, [isLgScreen]);
const scrollToTopButtonClass = isLgScreen
? "absolute bottom-0 right-3 p-10"
: "fixed top-20 right-3";
return (
<button
className={`${scrollToTopButtonClass} z-10 text-2xl text-teal-900 dark:text-white`}
data-test="scroll-to-top-button"
>
<FaArrowUp
onClick={() => handleScrollToTop(scrollToTopElementRef.current)}
/>
</button>
);
};
export default ScrollToTopButton;
+48
View File
@@ -0,0 +1,48 @@
import { Dispatch, SetStateAction } from "react";
import { useTranslations } from "next-intl";
type SearchBarProps = {
tournamentFilter: string;
setTournamentFilter: Dispatch<SetStateAction<string>>;
};
const SearchBar = ({
tournamentFilter,
setTournamentFilter,
}: SearchBarProps) => {
const t = useTranslations("Tournaments");
return (
<div className="bg-white p-3 dark:bg-gray-800">
<label htmlFor="table-search" className="sr-only">
{t("searchLabel")}
</label>
<div className="relative mt-1">
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<svg
className="h-5 w-5 text-gray-500 dark:text-gray-400"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
clipRule="evenodd"
></path>
</svg>
</div>
<input
type="text"
id="table-search"
className="block w-80 rounded-lg border border-gray-300 bg-gray-50 p-2.5 pl-10 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
placeholder={t("searchPlaceholder")}
value={tournamentFilter}
onChange={(e) => setTournamentFilter(e.target.value)}
/>
</div>
</div>
);
};
export default SearchBar;
+54
View File
@@ -0,0 +1,54 @@
"use client";
import { TournamentDataProps } from "@/types";
import { LatLngLiteral } from "leaflet";
import "leaflet/dist/leaflet.css";
import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css";
import "leaflet-defaulticon-compatibility";
import { MapContainer, TileLayer, LayersControl } from "react-leaflet";
import { createLayerGroups } from "@/utils/layerGroups";
import Legend from "./Legend";
export default function TournamentMap({ tournamentData }: TournamentDataProps) {
const center: LatLngLiteral = { lat: 47.0844, lng: 2.3964 };
const classicalMarkers = createLayerGroups("Cadence Lente", "green", {
tournamentData,
});
const rapidMarkers = createLayerGroups("Rapide", "blue", {
tournamentData,
});
const blitzMarkers = createLayerGroups("Blitz", "yellow", {
tournamentData,
});
const otherMarkers = createLayerGroups("1h KO", "red", { tournamentData });
return (
<section id="tournament-map" className="grid h-[calc(100vh-144px)]">
<MapContainer
center={center}
zoom={5}
scrollWheelZoom={false}
style={{
height: "100%",
}}
>
<TileLayer
attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<Legend />
<LayersControl>
{classicalMarkers}
{rapidMarkers}
{blitzMarkers}
{otherMarkers}
</LayersControl>
</MapContainer>
</section>
);
}
+103
View File
@@ -0,0 +1,103 @@
"use client";
import { TournamentDataProps } from "@/types";
import { useEffect, useState } from "react";
import { useTranslations } from "next-intl";
import SearchBar from "./SearchBar";
import ScrollToTopButton from "./ScrollToTopButton";
export default function TournamentTable({
tournamentData,
}: TournamentDataProps) {
let tableData;
const t = useTranslations("Tournaments");
const [searchQuery, setSearchQuery] = useState(""); // text from search bar
const [filteredTournamentData, setFilteredTournamentData] =
useState(tournamentData);
useEffect(() => {
setFilteredTournamentData(
tournamentData.filter((t) => t.town.includes(searchQuery.toUpperCase()))
);
}, [searchQuery, tournamentData]);
// TODO move this section into its own function
if (filteredTournamentData.length === 0) {
tableData = (
<tr className="bg-white text-gray-900 dark:bg-gray-800 dark:text-white">
<td colSpan={4} className="p-3">
{t("noneFound")}
</td>
</tr>
);
} else {
tableData = filteredTournamentData.map((data) => (
<tr
className="bg-white text-gray-900 hover:bg-gray-200 dark:bg-gray-800 dark:text-white dark:hover:bg-gray-900"
key={data._id}
>
<td className="p-3">
<a href={data.url} target="_blank">
{data.date}
</a>
</td>
<td className="p-3">
<a href={data.url} target="_blank">
{data.town}
</a>
</td>
<td className="p-3">
<a href={data.url} target="_blank">
{data.tournament}
</a>
</td>
<td className="p-3">
<a href={data.url} target="_blank">
{data.time_control}
</a>
</td>
</tr>
));
}
return (
<section
className="tournament-table grid w-full auto-rows-max pb-20 lg:col-start-2 lg:col-end-3 lg:h-[calc(100vh-144px)] lg:overflow-y-scroll"
id="tournament-table"
data-test="tournament-table-div"
>
<div className="z-10 flex">
<SearchBar
tournamentFilter={searchQuery}
setTournamentFilter={setSearchQuery}
/>
<div>
<ScrollToTopButton />
</div>
</div>
<table
className="relative w-full table-fixed text-center text-xs"
data-test="tournament-table"
>
<thead>
<tr>
<th className="sticky top-0 bg-teal-600 p-3 text-white dark:bg-gray-600">
{t("date")}
</th>
<th className="sticky top-0 bg-teal-600 p-3 text-white dark:bg-gray-600">
{t("town")}
</th>
<th className="sticky top-0 bg-teal-600 p-3 text-white dark:bg-gray-600">
{t("tournament")}
</th>
<th className="sticky top-0 bg-teal-600 p-3 text-white dark:bg-gray-600">
{t("timeControl")}
</th>
</tr>
</thead>
<tbody>{tableData}</tbody>
</table>
</section>
);
}
+12 -14
View File
@@ -2,15 +2,15 @@ import clientPromise from "@/lib/mongodb";
import dynamic from "next/dynamic";
import { useTranslations } from "next-intl";
import Layout from "@/components/Layout";
import TournamentTable from "@/components/TournamentTable";
import { dateOrderingFrance } from "@/utils/dbDateOrdering";
import { errorLog } from "@/utils/logger";
import TournamentTable from "./TournamentTable";
export const revalidate = 3600; // revalidate cache every 6 hours;
const LoadingMap = () => {
const t = useTranslations("Competitions");
const t = useTranslations("Tournaments");
return (
<div className="grid h-screen place-items-center bg-white text-gray-900 dark:bg-gray-800 dark:text-white">
<p>{t("loading")}</p>
@@ -22,7 +22,7 @@ const LoadingMap = () => {
* Imports the tournament map component, ensuring CSR only.
* @remarks SSR is not supported by react-leaflet
*/
const TournamentMap = dynamic(() => import("@/components/TournamentMap"), {
const TournamentMap = dynamic(() => import("./TournamentMap"), {
ssr: false,
loading: LoadingMap,
});
@@ -43,15 +43,13 @@ export default async function Tournaments() {
const tournamentData = await getTournaments();
return (
<Layout>
<main className="relative grid lg:grid-cols-2">
<div className="">
<TournamentMap tournamentData={JSON.parse(tournamentData)} />
</div>
<div className="relative bg-white dark:bg-gray-800 lg:overflow-y-auto">
<TournamentTable tournamentData={JSON.parse(tournamentData)} />
</div>
</main>
</Layout>
<main className="relative grid h-full lg:grid-cols-2">
<div className="">
<TournamentMap tournamentData={JSON.parse(tournamentData)} />
</div>
<div className="relative bg-white dark:bg-gray-800 lg:overflow-y-auto">
<TournamentTable tournamentData={JSON.parse(tournamentData)} />
</div>
</main>
);
}