initial commit

This commit is contained in:
2026-06-01 22:27:42 +02:00
commit 2805bd74df
57 changed files with 6036 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
import type { ReactNode } from "react";
interface IProps {
children: ReactNode;
title: string;
id: string;
}
const AccordionItem = ({ title, id, children }: IProps) => (
<div className="hero__accordion-item">
<details className="hero__accordion-details" name="linked">
<summary
className="hero__accordion-item__summary"
aria-describedby={title}
>
<span className="hero__accordion-item__title-container">
<span className="hero__accordion-item__title">{title}</span>
</span>
</summary>
</details>
<div className="hero__accordion-item__content">
<p
className="hero__accordion-item__content__inner"
role="definition"
id={id}
>
{children}
</p>
</div>
</div>
);
export default AccordionItem;
+28
View File
@@ -0,0 +1,28 @@
import { Image } from "@unpic/react";
import buyMeACoffeeLogo from "@/assets/images/buymeacoffeelogo.svg?url";
interface IProps {
height: number;
width: number;
}
const CoffeeWidget = ({ height, width }: IProps) => {
return (
<a
href="https://buymeacoffee.com/owenreesdev"
target="_blank"
className="ml-auto flex items-center gap-2 text-sm"
rel="noopener"
>
<Image
src={buyMeACoffeeLogo}
alt="Buy Me A Coffee Logo"
width={width}
height={height}
/>
<p>Support This Project</p>
</a>
);
};
export default CoffeeWidget;
+17
View File
@@ -0,0 +1,17 @@
import type { ReactNode } from "react";
interface IProps {
title: string;
children?: ReactNode;
}
const Feature = ({ title, children }: IProps) => (
<div className="mb-auto">
<h6 className="text-(--base-content) text-2xl font-bold">{title}</h6>
<p className="text-(--neutral-content) font-medium" data-testid="text">
{children}
</p>
</div>
);
export default Feature;
+49
View File
@@ -0,0 +1,49 @@
import { Link } from "@tanstack/react-router";
import { Image } from "@unpic/react";
import { LuGithub, LuMail } from "react-icons/lu";
import CoffeeWidget from "#/components/CoffeeWidget.tsx";
import footerLogo from "@/assets/images/footerLogo.svg?url";
export default function Footer() {
return (
<footer className="text-white w-full grid-cols-2 items-center bg-(--accent) px-10 py-5">
<div className="flex w-full items-center">
<Link to="/">
<Image
src={footerLogo}
alt="ChessScribe Logo"
width={10}
height={10}
className="hidden h-10 w-10 sm:block"
/>
</Link>
<div>
<p data-testid="copyright">
Copyright © 2023 - {new Date().getFullYear()}
</p>
</div>
<div className="ml-auto flex gap-2">
<a
href={import.meta.env.VITE_GITHUB_URL} // TODO ZOD
target="_blank"
rel="noopener noreferrer"
className="text-2xl hover:text-(--neutral-content) transition-colors duration-300 ease-in-out cursor-pointer"
>
<LuGithub />
</a>
<Link
to="/contact"
rel="noopener noreferrer"
className="text-2xl hover:text-(--neutral-content) transition-colors duration-300 ease-in-out"
>
<LuMail />
</Link>
</div>
<CoffeeWidget width={20} height={50} />
</div>
</footer>
);
}
+45
View File
@@ -0,0 +1,45 @@
import { Link } from "@tanstack/react-router";
import { Image } from "@unpic/react";
import logo from "@/assets/images/logo.svg?url";
export default function Header() {
return (
<header>
<nav className="w-full max-w-screen-2xl place-self-center p-8">
<div className="container mx-auto flex items-center justify-between">
<div className="flex items-center">
<Link to="/" className="flex flex-row items-center justify-center">
<Image
src={logo}
alt="ChessScribe Logo"
width={50}
height={50}
className="mr-4"
/>
<span className="nav-title hidden text-2xl font-extrabold sm:block">
ChessScribe
</span>
</Link>
</div>
<div>
<ul className="flex flex-1 items-center gap-14 font-semibold">
<li>
<Link to="/chessboard" className="nav-item">
Chessboard
</Link>
</li>
<li>
<Link to="/contact" className="nav-item">
Contact
</Link>
</li>
</ul>
</div>
</div>
</nav>
</header>
);
}
+28
View File
@@ -0,0 +1,28 @@
interface IProps {
onClickHandler: () => void;
}
const LichessButton = ({ onClickHandler }: IProps) => {
return (
<button
className="text-(--accent) border rounded-xl px-6 py-3 cursor-pointer flex items-center gap-2 p-2 hover:bg-(--accent) hover:text-(--header-bg) transition-colors duration-300 ease-in-out"
onClick={onClickHandler}
type="button"
>
<div className="w-6 h-6">
<svg
viewBox="0 0 50 50"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
>
<title>Lichess Logo</title>
<path d="M38.956.5c-3.53.418-6.452.902-9.286 2.984C5.534 1.786-.692 18.533.68 29.364 3.493 50.214 31.918 55.785 41.329 41.7c-7.444 7.696-19.276 8.752-28.323 3.084C3.959 39.116-.506 27.392 4.683 17.567 9.873 7.742 18.996 4.535 29.03 6.405c2.43-1.418 5.225-3.22 7.655-3.187l-1.694 4.86 12.752 21.37c-.439 5.654-5.459 6.112-5.459 6.112-.574-1.47-1.634-2.942-4.842-6.036-3.207-3.094-17.465-10.177-15.788-16.207-2.001 6.967 10.311 14.152 14.04 17.663 3.73 3.51 5.426 6.04 5.795 6.756 0 0 9.392-2.504 7.838-8.927L37.4 7.171z" />
</svg>
</div>
<p>Log into Lichess.org</p>
</button>
);
};
export default LichessButton;
+33
View File
@@ -0,0 +1,33 @@
import { trackAppRouter } from "@socialgouv/matomo-next";
import { useLocation, useSearch } from "@tanstack/react-router";
import { useEffect } from "react";
const MATOMO_URL = import.meta.env.NEXT_PUBLIC_MATOMO_URL;
const MATOMO_SITE_ID = import.meta.env.NEXT_PUBLIC_MATOMO_SITE_ID;
export function MatomoAnalytics() {
const location = useLocation();
const searchParams = useSearch({ strict: false });
useEffect(() => {
if (typeof window === "undefined") return;
const serializedParams = new URLSearchParams(
searchParams as Record<string, string>,
);
trackAppRouter({
url: MATOMO_URL || "",
siteId: MATOMO_SITE_ID || "",
pathname: location.pathname,
searchParams: serializedParams,
enableHeatmapSessionRecording: false,
enableHeartBeatTimer: true,
cleanUrl: true,
disableCookies: true,
debug: import.meta.env.DEV,
});
}, [location.pathname, searchParams]);
return null;
}
+32
View File
@@ -0,0 +1,32 @@
import LichessPgnViewer from "lichess-pgn-viewer";
import type PgnViewerType from "lichess-pgn-viewer/pgnViewer";
import { useEffect, useRef } from "react";
interface IProps {
gamePgn: string;
}
const PgnViewer = ({ gamePgn }: IProps) => {
const containerRef = useRef<HTMLDivElement | null>(null);
const viewerRef = useRef<PgnViewerType | null>(null);
useEffect(() => {
const element: HTMLElement | null = document.querySelector(".lpv-board");
if (!element) return;
viewerRef.current = LichessPgnViewer(element, {
pgn: gamePgn,
scrollToMove: false,
});
return () => {
if (containerRef.current) {
containerRef.current = null;
}
};
}, [gamePgn]);
return <div ref={containerRef} className="lpv-board" />;
};
export default PgnViewer;
+31
View File
@@ -0,0 +1,31 @@
import type { ReactNode } from "react";
interface IProps {
title: string;
smallTitle?: string;
description?: string;
children: ReactNode;
}
const Section = ({ title, smallTitle, description, children }: IProps) => {
const words = title.trim().split(" ");
const lastWord = words.pop();
const mainTitle = words.join(" ");
return (
<section className="mx-auto mt-8 grid max-w-5xl place-items-center gap-4 p-8 text-center md:grid-cols-3 md:text-left text-(--accent)">
<h3 className="md:col-span-3 mb-4 text-center text-lg font-bold tracking-wider text-primary">
{smallTitle}
</h3>
<h4 className="md:col-span-3 mb-4 text-center text-4xl font-extrabold text-(--neutral-content) md:text-5xl">
{mainTitle} <span className="text-(--accent)">{lastWord}</span>
</h4>
<p className="md:col-span-3 mb-8 w-1/2 text-center font-semibold text-(--neutral-content) ">
{description}
</p>
{children}
</section>
);
};
export default Section;