mirror of
https://github.com/TheRealOwenRees/echecsfrance.git
synced 2026-07-23 04:26:57 +00:00
@@ -1,33 +1,18 @@
|
||||
//TODO readme
|
||||
// TODO tests for tournament page:
|
||||
- map and table mounts in tournament page <- get data to send to map/table
|
||||
----------------------------------------------------------------
|
||||
|
||||
//TODO hover on table needs fixing - this only seemed to happen once I moved the logic into its own hook <- error is occurring on all hovers at the moment. Client/SSR issue?
|
||||
|
||||
//TODO font size on mobile screen
|
||||
|
||||
//TODO need a 'return to top' arrow button on smaller screens
|
||||
|
||||
//TODO mobile map needs improving
|
||||
|
||||
//TODO multi-language i18n support - https://nextjs.org/docs/app/building-your-application/routing/internationalization
|
||||
|
||||
//TODO tests for tournois, testing the loading of map and table, then counting the markers
|
||||
|
||||
//TODO logo for navbar and favicon
|
||||
|
||||
//TODO document my new hook <- consider removing as I believe this is the reason hover is broken
|
||||
|
||||
//TODO error handling
|
||||
|
||||
//TODO e2e tests for tournament page:
|
||||
- make sure number of markers = tournamentData.length
|
||||
|
||||
//TODO SRP for web and API data fetching
|
||||
//TODO about page
|
||||
|
||||
//TODO contact page needs working mailer
|
||||
|
||||
//TODO SEO
|
||||
|
||||
//TODO font size on mobile screen
|
||||
//TODO SEO - next headers etc
|
||||
//TODO data fetching tests
|
||||
//TODO redo layer groups tests
|
||||
----------------------------------------------------------------
|
||||
//TODO logo for navbar and favicon
|
||||
//TODO mobile map needs improving
|
||||
//TODO multi-language i18n support - https://nextjs.org/docs/app/building-your-application/routing/internationalization
|
||||
//TODO error handling
|
||||
//TODO consider offering GraphQL support
|
||||
|
||||
//TODO move smaller ui components into a new folder, and make them reusable - such as using generic prop names
|
||||
//TODO readme
|
||||
|
||||
@@ -3,8 +3,8 @@ import { dateOrderingFrance } from "@/utils/dbDateOrdering";
|
||||
|
||||
/**
|
||||
* Tournament data API endpoint
|
||||
* @route /api/tournaments/france
|
||||
* @internal
|
||||
* @route /api/v1/tournaments/france
|
||||
* @public
|
||||
*/
|
||||
export const revalidate = 3600; // revalidate cache every 6 hours
|
||||
export async function GET() {
|
||||
@@ -15,7 +15,11 @@ export async function GET() {
|
||||
const client = await clientPromise;
|
||||
const db = client.db("tournamentsFranceDB");
|
||||
|
||||
const data = await dateOrderingFrance(db);
|
||||
const results = await dateOrderingFrance(db);
|
||||
const data = results.map(({ _id, __v, ...rest }) => ({
|
||||
id: _id,
|
||||
...rest,
|
||||
}));
|
||||
|
||||
return new Response(JSON.stringify(data), { status: 200, headers });
|
||||
} catch (error) {
|
||||
@@ -6,7 +6,10 @@ export default function Contact() {
|
||||
<Layout>
|
||||
<section className="grid place-items-center bg-white dark:bg-gray-800">
|
||||
<div className="pt-8 pb-32 lg:pt-16 px-4 mx-auto max-w-screen-md">
|
||||
<h2 className="mb-4 text-4xl tracking-tight font-extrabold text-center text-gray-900 dark:text-white">
|
||||
<h2
|
||||
className="mb-4 text-4xl tracking-tight font-extrabold text-center text-gray-900 dark:text-white"
|
||||
data-cy="header2"
|
||||
>
|
||||
Contactez-Nous
|
||||
</h2>
|
||||
<p className="mb-8 lg:mb-16 font-light text-center text-gray-500 dark:text-gray-400 sm:text-xl">
|
||||
|
||||
+3
-1
@@ -16,7 +16,9 @@ export default function Home() {
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute text-center text-white">
|
||||
<h1 className="text-5xl p-5">Echecs France</h1>
|
||||
<h1 className="text-5xl p-5" data-cy="header1">
|
||||
Echecs France
|
||||
</h1>
|
||||
<h2 className="text-3xl p-5">
|
||||
Trouvez Vos Tournois d'Echecs en France Sur Une Carte
|
||||
</h2>
|
||||
|
||||
@@ -6,7 +6,10 @@ export default function About() {
|
||||
<header className="grid h-[calc(100%-216px)] md:h-[calc(100%-174px)] place-items-center">
|
||||
<div className="relative h-full w-full bg-white dark:bg-gray-800"></div>
|
||||
<div className="absolute">
|
||||
<h2 className="mb-4 text-4xl tracking-tight font-extrabold text-center text-gray-900 dark:text-white">
|
||||
<h2
|
||||
className="mb-4 text-4xl tracking-tight font-extrabold text-center text-gray-900 dark:text-white"
|
||||
data-cy="header2"
|
||||
>
|
||||
Qui Sommes-Nous?
|
||||
</h2>
|
||||
<p className="mb-8 lg:mb-16 font-light text-center text-gray-500 dark:text-gray-400 sm:text-xl">
|
||||
|
||||
@@ -37,11 +37,11 @@ export default async function Tournaments() {
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<main className="grid lg:grid-cols-2">
|
||||
<main className="relative grid lg:grid-cols-2">
|
||||
<div className="">
|
||||
<TournamentMap tournamentData={JSON.parse(tournamentData)} />
|
||||
</div>
|
||||
<div className="bg-white dark:bg-gray-800 lg:overflow-y-auto">
|
||||
<div className="relative bg-white dark:bg-gray-800 lg:overflow-y-auto">
|
||||
<TournamentTable tournamentData={JSON.parse(tournamentData)} />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -3,7 +3,10 @@ import { FaGithub, FaRegEnvelope } from "react-icons/fa";
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="grid w-full fixed bottom-0 justify-items-center py-2 px-5 text-white bg-teal-600 text-gray-900 dark:bg-gray-700">
|
||||
<footer
|
||||
className="grid w-full fixed bottom-0 justify-items-center py-2 px-5 text-white bg-teal-600 text-gray-900 dark:bg-gray-700 z-30"
|
||||
data-cy="footer"
|
||||
>
|
||||
<div className="p-2">
|
||||
<p>© {new Date().getFullYear()} - Owen Rees</p>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ const Hamburger = () => {
|
||||
<div
|
||||
ref={hamburgerButtonRef}
|
||||
className="hamburger-button space-y-2 relative z-[99999]"
|
||||
data-cy="hamburger-button"
|
||||
onClick={() => setMenuVisible(!menuVisible)}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -4,7 +4,10 @@ import Hamburger from "@/components/Hamburger";
|
||||
|
||||
export default function Navbar() {
|
||||
return (
|
||||
<nav className="w-full relative border-b-[1px] pt-5 mt-0 px-5 md:pt-2 bg-white dark:bg-gray-800 dark:border-gray-700 overflow-x-clip">
|
||||
<nav
|
||||
className="w-full relative border-b-[1px] pt-5 mt-0 px-5 md:pt-2 bg-white dark:bg-gray-800 dark:border-gray-700 overflow-x-clip"
|
||||
data-cy="navbar"
|
||||
>
|
||||
<div className="container mx-auto flex items-center">
|
||||
<div className="pb-3 justify-center flex w-full md:w-1/2 md:pb-0 md:justify-start font-extrabold">
|
||||
<Link
|
||||
@@ -14,11 +17,14 @@ export default function Navbar() {
|
||||
<span className="text-2xl">Echecs France</span>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="mobile-menu pb-2 md:hidden">
|
||||
<div className="pb-2 md:hidden" data-cy="mobile-menu">
|
||||
<Hamburger />
|
||||
</div>
|
||||
|
||||
<div className="desktop-menu hidden pt-2 justify-center md:flex md:w-1/2 md:justify-end">
|
||||
<div
|
||||
className="hidden pt-2 justify-center md:flex md:w-1/2 md:justify-end"
|
||||
data-cy="desktop-menu"
|
||||
>
|
||||
<ul className="list-reset text-gray-900 dark:text-white no-underline flex flex-1 justify-around md:flex-none items-center">
|
||||
<li className="mr-10">
|
||||
<Link
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { FaArrowUp } from "react-icons/fa";
|
||||
import { handleScrollToTop } from "@/handlers/scrollHandlers";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
const ScrollToTopButton = ({ isLgScreen }: { isLgScreen: boolean }) => {
|
||||
const scrollToTopElementRef = useRef<HTMLElement | null>(null);
|
||||
// determine scrollable element based on screen size - window or div
|
||||
useEffect(() => {
|
||||
if (isLgScreen) {
|
||||
scrollToTopElementRef.current =
|
||||
document.getElementById("tournament-table");
|
||||
}
|
||||
}, [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-cy="scroll-to-top-button"
|
||||
>
|
||||
<FaArrowUp
|
||||
onClick={() =>
|
||||
handleScrollToTop(scrollToTopElementRef.current || window)
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScrollToTopButton;
|
||||
@@ -10,7 +10,6 @@ const SearchBar = ({
|
||||
setTournamentFilter,
|
||||
}: SearchBarProps) => {
|
||||
return (
|
||||
<>
|
||||
<div className="p-3 bg-white dark:bg-gray-800">
|
||||
<label htmlFor="table-search" className="sr-only">
|
||||
Search
|
||||
@@ -40,7 +39,6 @@ const SearchBar = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -20,11 +20,19 @@ const ThemeSwitcher = () => {
|
||||
return (
|
||||
<div>
|
||||
{colorTheme === "light" ? (
|
||||
<div className="toggle-dark" onClick={() => setTheme("light")}>
|
||||
<div
|
||||
className="toggle-dark"
|
||||
data-cy="toggle-dark"
|
||||
onClick={() => setTheme("light")}
|
||||
>
|
||||
<MdBrightness2 className="theme-icon-dark" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="toggle" onClick={() => setTheme("dark")}>
|
||||
<div
|
||||
className="toggle"
|
||||
data-cy="toggle"
|
||||
onClick={() => setTheme("dark")}
|
||||
>
|
||||
<MdCircle className="theme-icon" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -2,39 +2,111 @@
|
||||
|
||||
import { TournamentDataProps } from "@/types";
|
||||
import { useEffect, useState } from "react";
|
||||
import useTournamentDataFilter from "@/hooks/useTournamentFilter";
|
||||
import SearchBar from "@/components/SearchBar";
|
||||
import ScrollToTopButton from "@/components/ScrollToTopButton";
|
||||
|
||||
export default function TournamentTable({
|
||||
tournamentData,
|
||||
}: TournamentDataProps) {
|
||||
const [tournamentFilter, setTournamentFilter] = useState(""); // text from search bar
|
||||
let tableData;
|
||||
const [searchQuery, setSearchQuery] = useState(""); // text from search bar
|
||||
const [filteredTournamentData, setFilteredTournamentData] =
|
||||
useTournamentDataFilter(tournamentData);
|
||||
useState(tournamentData);
|
||||
const [isLgScreen, setIsLgScreen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setFilteredTournamentData(tournamentFilter);
|
||||
}, [tournamentFilter]);
|
||||
setFilteredTournamentData(
|
||||
tournamentData.filter((t) => t.town.includes(searchQuery.toUpperCase()))
|
||||
);
|
||||
}, [searchQuery]);
|
||||
|
||||
const stickyHeader =
|
||||
"sticky top-0 p-3 bg-teal-600 text-white dark:bg-gray-600";
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setIsLgScreen(window.innerWidth >= 1024);
|
||||
};
|
||||
handleResize();
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 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">
|
||||
No tournaments found
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
} else {
|
||||
tableData = filteredTournamentData.map((t) => (
|
||||
<tr
|
||||
className="text-gray-900 bg-white dark:bg-gray-800 dark:text-white hover:bg-gray-200 dark:hover:bg-gray-900"
|
||||
key={t._id}
|
||||
>
|
||||
<td className="p-3">
|
||||
<a href={t.url} target="_blank">
|
||||
{t.date}
|
||||
</a>
|
||||
</td>
|
||||
<td className="p-3">
|
||||
<a href={t.url} target="_blank">
|
||||
{t.town}
|
||||
</a>
|
||||
</td>
|
||||
<td className="p-3">
|
||||
<a href={t.url} target="_blank">
|
||||
{t.tournament}
|
||||
</a>
|
||||
</td>
|
||||
<td className="p-3">
|
||||
<a href={t.url} target="_blank">
|
||||
{t.time_control}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
));
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="w-full grid auto-rows-max pb-20 lg:h-[calc(100vh-174px)] lg:col-start-2 lg:col-end-3">
|
||||
<section
|
||||
className="tournament-table w-full grid auto-rows-max pb-20 lg:h-[calc(100vh-174px)] lg:col-start-2 lg:col-end-3 lg:overflow-y-scroll"
|
||||
id="tournament-table"
|
||||
data-cy="tournament-table-div"
|
||||
>
|
||||
<div className="flex z-10">
|
||||
<SearchBar
|
||||
tournamentFilter={tournamentFilter}
|
||||
setTournamentFilter={setTournamentFilter}
|
||||
tournamentFilter={searchQuery}
|
||||
setTournamentFilter={setSearchQuery}
|
||||
/>
|
||||
<table className="relative table-fixed w-full text-center text-xs">
|
||||
<div>
|
||||
<ScrollToTopButton isLgScreen={isLgScreen} />
|
||||
</div>
|
||||
</div>
|
||||
<table
|
||||
className="relative table-fixed w-full text-center text-xs"
|
||||
data-cy="tournament-table"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={stickyHeader}>Date</th>
|
||||
<th className={stickyHeader}>Ville</th>
|
||||
<th className={stickyHeader}>Tournois</th>
|
||||
<th className={stickyHeader}>Cadence</th>
|
||||
<th className="sticky top-0 p-3 bg-teal-600 text-white dark:bg-gray-600">
|
||||
Date
|
||||
</th>
|
||||
<th className="sticky top-0 p-3 bg-teal-600 text-white dark:bg-gray-600">
|
||||
Ville
|
||||
</th>
|
||||
<th className="sticky top-0 p-3 bg-teal-600 text-white dark:bg-gray-600">
|
||||
Tournois
|
||||
</th>
|
||||
<th className="sticky top-0 p-3 bg-teal-600 text-white dark:bg-gray-600">
|
||||
Cadence
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{filteredTournamentData}</tbody>
|
||||
<tbody>{tableData}</tbody>
|
||||
</table>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -4,11 +4,11 @@ import Contact from "@/app/contactez-nous/page";
|
||||
|
||||
const navbarFooterCheck = () => {
|
||||
it("includes navbar", () => {
|
||||
cy.get("nav");
|
||||
cy.get("[data-cy='navbar']");
|
||||
});
|
||||
|
||||
it("includes footer", () => {
|
||||
cy.get("footer");
|
||||
cy.get("[data-cy='footer']");
|
||||
});
|
||||
};
|
||||
|
||||
@@ -19,7 +19,9 @@ describe("Verify component mount", () => {
|
||||
});
|
||||
|
||||
it("correct h1 tags with website name included", () => {
|
||||
cy.get("h1").contains("echecs france", { matchCase: false });
|
||||
cy.get("[data-cy='header1']").contains("echecs france", {
|
||||
matchCase: false,
|
||||
});
|
||||
});
|
||||
|
||||
navbarFooterCheck();
|
||||
@@ -31,7 +33,9 @@ describe("Verify component mount", () => {
|
||||
});
|
||||
|
||||
it("correct h1 tags with page title included", () => {
|
||||
cy.get("h2").contains("qui sommes-nous", { matchCase: false });
|
||||
cy.get("[data-cy='header2']").contains("qui sommes-nous", {
|
||||
matchCase: false,
|
||||
});
|
||||
});
|
||||
|
||||
navbarFooterCheck();
|
||||
@@ -43,7 +47,9 @@ describe("Verify component mount", () => {
|
||||
});
|
||||
|
||||
it("correct h1 tags with page title included", () => {
|
||||
cy.get("h2").contains("contactez-nous", { matchCase: false });
|
||||
cy.get("[data-cy='header2']").contains("contactez-nous", {
|
||||
matchCase: false,
|
||||
});
|
||||
});
|
||||
|
||||
navbarFooterCheck();
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import ScrollToTopButton from "@/components/ScrollToTopButton";
|
||||
|
||||
describe("Scroll to top button", () => {
|
||||
it("desktop scroll button clickable", () => {
|
||||
cy.viewport("macbook-15");
|
||||
cy.mount(<ScrollToTopButton isLgScreen={true} />);
|
||||
cy.get("[data-cy='scroll-to-top-button']").should("exist").click();
|
||||
});
|
||||
|
||||
it("mobile scroll button clickable", () => {
|
||||
cy.viewport(600, 600);
|
||||
cy.mount(<ScrollToTopButton isLgScreen={false} />);
|
||||
cy.get("[data-cy='scroll-to-top-button']").should("exist").click();
|
||||
});
|
||||
});
|
||||
@@ -1,26 +1,23 @@
|
||||
import React from "react";
|
||||
import ThemeSwitcher from "../../components/ThemeSwitcher";
|
||||
import Home from "@/app/page";
|
||||
import "@/css/theme-toggle.css";
|
||||
|
||||
describe("ThemeSwitcher component", () => {
|
||||
it("should toggle between light and dark mode", () => {
|
||||
cy.mount(<Home />);
|
||||
cy.mount(<ThemeSwitcher />);
|
||||
cy.wait(100);
|
||||
|
||||
// checking that the toggle is there and light mode is active
|
||||
cy.get(".toggle").should("exist");
|
||||
cy.get(".toggle").should(($toggle) => {
|
||||
cy.get("[data-cy='toggle']").should("exist");
|
||||
cy.get("[data-cy='toggle']").should(($toggle) => {
|
||||
const backgroundImage = $toggle.css("background-image");
|
||||
expect(backgroundImage).to.include(
|
||||
"linear-gradient(rgb(0, 255, 255), rgb(135, 206, 235)"
|
||||
);
|
||||
});
|
||||
// checking that the toggle is clickable and dark mode is active
|
||||
cy.get(".toggle").click();
|
||||
cy.get(".toggle-dark").should("exist");
|
||||
cy.get(".toggle-dark").should(($toggle) => {
|
||||
cy.get("[data-cy='toggle']").click();
|
||||
cy.get("[data-cy='toggle-dark']").should("exist");
|
||||
cy.get("[data-cy='toggle-dark']").should(($toggle) => {
|
||||
const backgroundImage = $toggle.css("background-image");
|
||||
expect(backgroundImage).to.include(
|
||||
"linear-gradient(rgb(25, 25, 112), rgb(102, 51, 153)"
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
let tableRows: number;
|
||||
describe("Data fetching for map", () => {
|
||||
it("map markers is equal to table rows", () => {
|
||||
cy.visit("/tournois");
|
||||
cy.get('[data-cy="tournament-table"]')
|
||||
.find("tr")
|
||||
.then((rows) => {
|
||||
tableRows = rows.length - 1;
|
||||
});
|
||||
cy.get(".leaflet-marker-icon").then((markers) => {
|
||||
expect(markers.length).to.eq(tableRows);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Data fetching from API endpoints", () => {
|
||||
it("api call should equal website data count", () => {
|
||||
cy.request("GET", "http://localhost:3000/api/v1/tournaments/france").then(
|
||||
(response) => {
|
||||
expect(response.status).to.eq(200);
|
||||
const responseData = response.body;
|
||||
const itemCount = responseData.length;
|
||||
expect(itemCount).to.eq(tableRows);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -9,7 +9,7 @@ describe("Test all links", () => {
|
||||
cy.viewport(600, 600);
|
||||
cy.visit("/");
|
||||
pages.forEach((page) => {
|
||||
cy.get(".hamburger-button").click();
|
||||
cy.get("[data-cy='hamburger-button']").click();
|
||||
cy.contains(page, { matchCase: false }).click();
|
||||
cy.location("pathname").should("eq", `/${navLinkToSlug(page)}`); // url path matches link name, replacing whitespace with hyphens
|
||||
cy.go("back");
|
||||
|
||||
@@ -5,12 +5,12 @@ describe("Mobile Navbar", () => {
|
||||
});
|
||||
|
||||
it("hamburger menu on mobile screens", () => {
|
||||
cy.get(".mobile-menu").should("be.not.hidden");
|
||||
cy.get(".desktop-menu").should("be.hidden");
|
||||
cy.get("[data-cy='mobile-menu']").should("be.visible");
|
||||
cy.get("[data-cy='desktop-menu']").should("be.not.visible");
|
||||
});
|
||||
|
||||
it("hamburger button available", () => {
|
||||
cy.get(".hamburger-button").should("be.visible");
|
||||
cy.get("[data-cy='hamburger-button']").should("be.visible");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,11 +21,11 @@ describe("Desktop Navbar", () => {
|
||||
});
|
||||
|
||||
it("desktop menu on larger screens", () => {
|
||||
cy.get(".mobile-menu").should("be.hidden");
|
||||
cy.get(".desktop-menu").should("be.not.hidden");
|
||||
cy.get("[data-cy='mobile-menu']").should("be.hidden");
|
||||
cy.get("[data-cy='desktop-menu']").should("be.not.hidden");
|
||||
});
|
||||
|
||||
it("hamburger button hidden", () => {
|
||||
cy.get(".hamburger-button").should("be.not.visible");
|
||||
cy.get("[data-cy='hamburger-button']").should("be.not.visible");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
describe("Scroll to top button", () => {
|
||||
beforeEach(() => {
|
||||
cy.visit("/tournois");
|
||||
});
|
||||
|
||||
it("desktop scroll to top", () => {
|
||||
cy.viewport(1024, 800);
|
||||
cy.get("[data-cy='tournament-table-div']").scrollTo("bottom");
|
||||
cy.get("[data-cy='scroll-to-top-button']").click();
|
||||
// cy.get("[data-cy='tournament-table-div']")
|
||||
// .invoke("scrollTop")
|
||||
// .should("equal", 0);
|
||||
// cy.get("[data-cy='tournament-table-div']")
|
||||
// .its("scrollY")
|
||||
// .should("not.equal", 0);
|
||||
// TODO need to test element scrolls back to 0
|
||||
});
|
||||
|
||||
it("mobile scroll to top", () => {
|
||||
cy.viewport(600, 600);
|
||||
cy.scrollTo("bottom");
|
||||
cy.window().its("scrollY").should("not.equal", 0);
|
||||
cy.get("[data-cy='scroll-to-top-button']").click();
|
||||
cy.window().its("scrollY").should("equal", 0);
|
||||
});
|
||||
});
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
import { ScrollableElement } from "@/types";
|
||||
|
||||
const isBrowser = () => typeof window !== "undefined";
|
||||
|
||||
export const handleScrollToTop = (element: ScrollableElement | null) => {
|
||||
if (!isBrowser()) return;
|
||||
element?.scrollTo({ top: 0, behavior: "smooth" });
|
||||
};
|
||||
@@ -1,57 +0,0 @@
|
||||
import { Tournament } from "@/types";
|
||||
import { useState } from "react";
|
||||
|
||||
// TODO change 'any'
|
||||
// TODO document this hook with TSDoc
|
||||
const useTournamentDataFilter = (
|
||||
initialState: Tournament[]
|
||||
): (any | ((searchQuery: string) => void))[] => {
|
||||
const [filtered, setFiltered] = useState(initialState);
|
||||
let state;
|
||||
const setState = (searchQuery: string) => {
|
||||
setFiltered(
|
||||
initialState.filter((t) => t.town.includes(searchQuery.toUpperCase()))
|
||||
);
|
||||
};
|
||||
|
||||
if (filtered.length === 0) {
|
||||
state = (
|
||||
<tr className="bg-white text-gray-900 dark:bg-gray-800 dark:text-white">
|
||||
<td colSpan={4} className="p-3">
|
||||
No tournaments found
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
} else {
|
||||
state = filtered.map((t) => (
|
||||
<tr
|
||||
className="text-gray-900 bg-white dark:bg-gray-800 dark:text-white hover:bg-gray-400 hover:text-black"
|
||||
key={t._id}
|
||||
>
|
||||
<td className="p-3">
|
||||
<a href={t.url} target="_blank">
|
||||
{t.date}
|
||||
</a>
|
||||
</td>
|
||||
<td className="p-3">
|
||||
<a href={t.url} target="_blank">
|
||||
{t.town}
|
||||
</a>
|
||||
</td>
|
||||
<td className="p-3">
|
||||
<a href={t.url} target="_blank">
|
||||
{t.tournament}
|
||||
</a>
|
||||
</td>
|
||||
<td className="p-3">
|
||||
<a href={t.url} target="_blank">
|
||||
{t.time_control}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
));
|
||||
}
|
||||
return [state, setState];
|
||||
};
|
||||
|
||||
export default useTournamentDataFilter;
|
||||
@@ -0,0 +1,2 @@
|
||||
GET http://localhost:3000/api/v1/tournaments/france
|
||||
Accept: application/json
|
||||
@@ -12,3 +12,5 @@ export interface Tournament {
|
||||
export interface TournamentDataProps {
|
||||
tournamentData: Tournament[];
|
||||
}
|
||||
|
||||
export type ScrollableElement = Window | HTMLElement;
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Db } from "mongodb";
|
||||
/**
|
||||
* Converts date from string into a date to allow ordering
|
||||
*/
|
||||
// TODO remove the .toArray method as this is not SRP friendly
|
||||
export const dateOrderingFrance = async (db: Db) => {
|
||||
return await db
|
||||
.collection("tournaments")
|
||||
|
||||
Reference in New Issue
Block a user