mirror of
https://github.com/TheRealOwenRees/chess-scribe-website.git
synced 2026-07-23 09:56:57 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
38fc389776
|
|||
|
af6809874c
|
|||
|
ad4d9ab12c
|
|||
|
70d65cfcfd
|
|||
|
9fe1b2043c
|
|||
|
48d0c4e47d
|
|||
|
e30bb5927a
|
|||
|
1d9ee25e1f
|
@@ -1,6 +1,10 @@
|
|||||||
- favicon and page titles - SEO related things
|
- og / json-sd
|
||||||
- rearrange buttons / header space - collapse on smaller screens
|
|
||||||
- Lichess login
|
- Lichess login
|
||||||
- logger
|
- logger
|
||||||
- contact form
|
- contact form
|
||||||
- bring sections headers on all pages other than home page further up (reduce top margin / padding)
|
- bring sections headers on all pages other than home page further up (reduce top margin / padding)
|
||||||
|
- set game current position on game load to ply 0 and whatever the FEN is (starting position probably, but could be custom position)
|
||||||
|
- privacy policy - have no access to anything you generate, only keep a count of success/failed pdf generations with timestamp and error message
|
||||||
|
- a11y and lighthouse fixes
|
||||||
|
- check wcag compliance
|
||||||
|
- white background?
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="64.000000pt" height="64.000000pt" viewBox="0 0 64.000000 64.000000"
|
||||||
|
preserveAspectRatio="xMidYMid meet">
|
||||||
|
|
||||||
|
<g transform="translate(0.000000,64.000000) scale(0.100000,-0.100000)"
|
||||||
|
fill="#6f884d" stroke="black">
|
||||||
|
<path d="M460 605 c0 -8 -7 -15 -15 -15 -10 0 -15 -10 -15 -30 0 -20 5 -30 15
|
||||||
|
-30 20 0 19 -16 -1 -24 -12 -4 -15 -14 -11 -34 3 -15 1 -34 -4 -44 -16 -29 0
|
||||||
|
-48 41 -48 42 0 44 -2 55 -80 4 -25 9 -53 11 -62 5 -16 -2 -18 -46 -18 -40 0
|
||||||
|
-51 3 -47 13 12 32 19 130 9 123 -6 -3 -14 -32 -18 -62 -4 -36 -13 -62 -25
|
||||||
|
-72 -11 -9 -19 -22 -19 -28 0 -7 -7 -14 -15 -18 -8 -3 -15 -17 -15 -31 0 -25
|
||||||
|
0 -25 90 -25 53 0 90 4 90 10 0 6 -33 10 -80 10 -47 0 -80 4 -80 10 0 6 43 10
|
||||||
|
110 10 67 0 110 -4 110 -10 0 -5 -9 -10 -20 -10 -11 0 -20 -4 -20 -10 0 -5 14
|
||||||
|
-10 30 -10 25 0 30 4 30 25 0 14 -7 28 -15 31 -8 4 -15 12 -15 20 0 7 -7 17
|
||||||
|
-16 22 -11 6 -21 35 -29 83 -11 64 -11 76 2 91 9 10 12 22 6 30 -4 7 -8 29 -8
|
||||||
|
48 0 32 -3 35 -32 38 -25 2 -33 8 -33 23 0 10 -7 19 -15 19 -8 0 -15 5 -15 10
|
||||||
|
0 6 7 10 15 10 8 0 15 7 15 15 0 8 5 15 10 15 6 0 10 -7 10 -15 0 -8 7 -15 15
|
||||||
|
-15 8 0 15 -4 15 -10 0 -5 -7 -10 -15 -10 -8 0 -15 -4 -15 -10 0 -5 11 -10 25
|
||||||
|
-10 21 0 25 5 25 30 0 20 -5 30 -15 30 -8 0 -15 7 -15 15 0 10 -10 15 -30 15
|
||||||
|
-20 0 -30 -5 -30 -15z m66 -131 c-3 -9 -6 -20 -6 -25 0 -5 -13 -9 -30 -9 -16
|
||||||
|
0 -30 4 -30 9 0 5 -3 16 -6 25 -5 13 2 16 36 16 34 0 41 -3 36 -16z m14 -64
|
||||||
|
c0 -5 -22 -10 -50 -10 -27 0 -50 5 -50 10 0 6 23 10 50 10 28 0 50 -4 50 -10z
|
||||||
|
m30 -220 c0 -6 -33 -10 -80 -10 -47 0 -80 4 -80 10 0 6 33 10 80 10 47 0 80
|
||||||
|
-4 80 -10z"/>
|
||||||
|
<path d="M118 519 c-56 -29 -78 -80 -78 -179 0 -47 4 -80 10 -80 6 0 10 34 10
|
||||||
|
83 0 64 4 89 20 115 11 17 24 32 30 32 14 0 13 -5 -10 -42 -17 -28 -20 -51
|
||||||
|
-20 -160 0 -79 4 -128 10 -128 6 0 10 49 10 126 0 135 9 169 41 159 22 -7 23
|
||||||
|
-2 8 27 -16 29 -2 34 20 7 22 -25 28 -11 11 22 -9 16 -7 19 14 19 29 0 76 -19
|
||||||
|
76 -32 0 -10 -3 -10 -34 2 -16 6 -27 6 -31 0 -4 -6 -2 -10 3 -10 5 0 30 -16
|
||||||
|
56 -35 48 -37 64 -78 26 -68 -22 6 -28 -12 -7 -20 24 -10 -21 -7 -47 3 -34 13
|
||||||
|
-55 4 -62 -27 -6 -21 2 -33 50 -77 45 -42 56 -58 56 -84 0 -27 -4 -32 -24 -32
|
||||||
|
-17 0 -27 9 -36 30 -13 32 -37 56 -46 47 -4 -3 3 -18 15 -33 11 -15 21 -31 21
|
||||||
|
-35 0 -5 -31 -9 -68 -9 -70 0 -92 -10 -92 -41 0 -9 -7 -19 -15 -23 -17 -6 -21
|
||||||
|
-56 -5 -56 6 0 10 9 10 20 0 19 7 20 150 20 143 0 150 -1 150 -20 0 -11 5 -20
|
||||||
|
10 -20 16 0 12 50 -5 56 -8 4 -15 14 -15 24 0 10 -7 23 -15 30 -8 7 -15 28
|
||||||
|
-15 48 0 30 -8 43 -57 87 -41 38 -54 57 -49 70 4 11 12 15 24 10 46 -18 85
|
||||||
|
-18 98 0 22 30 17 71 -11 94 -20 15 -24 25 -20 44 5 20 1 27 -28 41 -46 21
|
||||||
|
-93 20 -139 -5z m190 -421 c3 -16 -8 -18 -117 -18 -94 0 -121 3 -121 13 0 25
|
||||||
|
14 28 124 25 96 -3 111 -5 114 -20z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.4 KiB |
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"short_name": "TanStack App",
|
|
||||||
"name": "Create TanStack App Sample",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "favicon.ico",
|
|
||||||
"sizes": "64x64 32x32 24x24 16x16",
|
|
||||||
"type": "image/x-icon"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "logo192.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "192x192"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "logo512.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "512x512"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"start_url": ".",
|
|
||||||
"display": "standalone",
|
|
||||||
"theme_color": "#000000",
|
|
||||||
"background_color": "#ffffff"
|
|
||||||
}
|
|
||||||
+3
-2
@@ -1,3 +1,4 @@
|
|||||||
# https://www.robotstxt.org/robotstxt.html
|
|
||||||
User-agent: *
|
User-agent: *
|
||||||
Disallow:
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: https://chess-scribe.org/sitemap.xml
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ interface IProps {
|
|||||||
|
|
||||||
const CustomHeaders = ({ headers, updateHeaders }: IProps) => {
|
const CustomHeaders = ({ headers, updateHeaders }: IProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 w-full">
|
<div className="mt-4 w-full max-w-175">
|
||||||
<details className="bg-(--neutral-content)/20 p-4 rounded-lg">
|
<details className="bg-(--neutral-content)/20 p-4 rounded-lg">
|
||||||
<summary>
|
<summary>
|
||||||
Custom Headers (PDF)
|
Custom Headers (PDF)
|
||||||
@@ -19,7 +19,7 @@ const CustomHeaders = ({ headers, updateHeaders }: IProps) => {
|
|||||||
</p>
|
</p>
|
||||||
</summary>
|
</summary>
|
||||||
<div>
|
<div>
|
||||||
<form className="grid place-items-center gap-4 sm:grid-cols-2 mt-2">
|
<form className="flex flex-wrap mt-2 justify-center gap-4">
|
||||||
<FormField
|
<FormField
|
||||||
fieldName="title"
|
fieldName="title"
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ interface IProps {
|
|||||||
|
|
||||||
const HeaderFields = ({ headers, updateHeaders }: IProps) => {
|
const HeaderFields = ({ headers, updateHeaders }: IProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 w-full">
|
<div className="mt-4 w-full max-w-175">
|
||||||
<details className="bg-(--neutral-content)/15 p-4 rounded-lg">
|
<details className="bg-(--neutral-content)/15 p-4 rounded-lg">
|
||||||
<summary>
|
<summary>
|
||||||
Headers
|
Headers
|
||||||
<p className="text-sm font-normal">Edit the game PGN headers</p>
|
<p className="text-sm font-normal">Edit the game PGN headers</p>
|
||||||
</summary>
|
</summary>
|
||||||
<div>
|
<div>
|
||||||
<form className="grid place-items-center gap-4 sm:grid-cols-2 mt-2">
|
<form className="flex flex-wrap mt-2 justify-center gap-4">
|
||||||
<FormField
|
<FormField
|
||||||
fieldName="event"
|
fieldName="event"
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import LichessPgnViewer from "lichess-pgn-viewer";
|
import { useLichessPgnViewer } from "#/hooks/useLichessPgnViewer.ts";
|
||||||
import { useEffect, useRef } from "react";
|
|
||||||
import type { IPosition } from "#/interfaces.ts";
|
import type { IPosition } from "#/interfaces.ts";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
@@ -8,80 +7,9 @@ interface IProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PgnViewer = ({ gamePgn, handlePlyChange }: IProps) => {
|
const PgnViewer = ({ gamePgn, handlePlyChange }: IProps) => {
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
useLichessPgnViewer({ gamePgn, handlePlyChange });
|
||||||
const viewerRef = useRef(null);
|
|
||||||
|
|
||||||
// TODO make thin, and pull functions out
|
return <div className="lpv-board" />;
|
||||||
useEffect(() => {
|
|
||||||
const element: HTMLElement | null = document.querySelector(".lpv-board");
|
|
||||||
if (!element) return;
|
|
||||||
|
|
||||||
viewerRef.current = LichessPgnViewer(element, {
|
|
||||||
pgn: gamePgn,
|
|
||||||
scrollToMove: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// set initial position - TODO change to on file load
|
|
||||||
// handlePlyChange({
|
|
||||||
// ply: 0,
|
|
||||||
// fen: viewerRef.current?.curData().fen,
|
|
||||||
// });
|
|
||||||
|
|
||||||
const boardButtons = document.querySelectorAll(".lpv__controls");
|
|
||||||
const movesList = document.querySelectorAll(".lpv__moves");
|
|
||||||
if (!boardButtons || !movesList) return;
|
|
||||||
|
|
||||||
const clickHandlerDelay = () => {
|
|
||||||
setTimeout(() => handleClick(), 250);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Hack using DOM manipulation
|
|
||||||
const boardEventListener = () => {
|
|
||||||
const variationTags = document.querySelector("variation");
|
|
||||||
const moves = [...document.querySelectorAll("move")].filter((m) => {
|
|
||||||
if (!variationTags) return m.className !== "empty";
|
|
||||||
return (
|
|
||||||
m.parentNode?.querySelector("variation") && m.className !== "empty"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
const ply = moves.findIndex((m) => m.classList.contains("current")) + 1;
|
|
||||||
return { moves, ply };
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClick = () => {
|
|
||||||
const { ply } = boardEventListener();
|
|
||||||
const fen = viewerRef.current?.curData().fen;
|
|
||||||
handlePlyChange({ ply, fen });
|
|
||||||
};
|
|
||||||
|
|
||||||
boardButtons.forEach((button) => {
|
|
||||||
button.addEventListener("click", handleClick);
|
|
||||||
button.addEventListener("touchstart", handleClick);
|
|
||||||
});
|
|
||||||
|
|
||||||
movesList.forEach((move) => {
|
|
||||||
move.addEventListener("click", clickHandlerDelay);
|
|
||||||
move.addEventListener("touchstart", clickHandlerDelay);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (containerRef.current) {
|
|
||||||
containerRef.current = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
boardButtons.forEach((button) => {
|
|
||||||
button.removeEventListener("click", handleClick);
|
|
||||||
button.removeEventListener("touchstart", handleClick);
|
|
||||||
});
|
|
||||||
|
|
||||||
movesList.forEach((move) => {
|
|
||||||
move.removeEventListener("click", clickHandlerDelay);
|
|
||||||
move.removeEventListener("touchstart", clickHandlerDelay);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}, [gamePgn, handlePlyChange]);
|
|
||||||
|
|
||||||
return <div ref={containerRef} className="lpv-board" />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PgnViewer;
|
export default PgnViewer;
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
|
export const HOST_URL = "https://chess-scribe.org";
|
||||||
|
|
||||||
export const GITHUB_URL =
|
export const GITHUB_URL =
|
||||||
"https://github.com/therealowenrees/chess-scribe-website";
|
"https://github.com/therealowenrees/chess-scribe-website";
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import LichessPgnViewer from "lichess-pgn-viewer";
|
||||||
|
import { type RefObject, useEffect, useRef } from "react";
|
||||||
|
import type { IPosition } from "#/interfaces.ts";
|
||||||
|
|
||||||
|
type ViewerRef = ReturnType<typeof LichessPgnViewer>;
|
||||||
|
type HandlePlyChange = ({ ply, fen }: IPosition) => void;
|
||||||
|
|
||||||
|
interface IHandleBoardClick {
|
||||||
|
viewerRef: RefObject<ViewerRef | null>;
|
||||||
|
handlePlyChange: HandlePlyChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
gamePgn: string;
|
||||||
|
handlePlyChange: HandlePlyChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBoardEventData = () => {
|
||||||
|
const variationTags = document.querySelector("variation");
|
||||||
|
const moves = [...document.querySelectorAll("move")].filter((m) => {
|
||||||
|
if (!variationTags) return m.className !== "empty";
|
||||||
|
return m.parentNode?.querySelector("variation") && m.className !== "empty";
|
||||||
|
});
|
||||||
|
const ply = moves.findIndex((m) => m.classList.contains("current")) + 1;
|
||||||
|
return { moves, ply };
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBoardClick = ({
|
||||||
|
viewerRef,
|
||||||
|
handlePlyChange,
|
||||||
|
}: IHandleBoardClick) => {
|
||||||
|
const { ply } = getBoardEventData();
|
||||||
|
const fen = viewerRef.current?.curData().fen ?? "";
|
||||||
|
handlePlyChange({ ply, fen });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLichessPgnViewer = ({ gamePgn, handlePlyChange }: IProps) => {
|
||||||
|
const viewerRef = useRef<ViewerRef | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const element: HTMLElement | null = document.querySelector(".lpv-board");
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
|
viewerRef.current = LichessPgnViewer(element, {
|
||||||
|
pgn: gamePgn,
|
||||||
|
scrollToMove: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const boardButtons = document.querySelectorAll(".lpv__controls");
|
||||||
|
const movesList = document.querySelectorAll(".lpv__moves");
|
||||||
|
if (!boardButtons || !movesList) return;
|
||||||
|
|
||||||
|
const handleClick = () => handleBoardClick({ viewerRef, handlePlyChange });
|
||||||
|
const clickHandlerDelay = () => setTimeout(() => handleClick, 250);
|
||||||
|
|
||||||
|
boardButtons.forEach((button) => {
|
||||||
|
button.addEventListener("click", handleClick);
|
||||||
|
button.addEventListener("touchstart", handleClick);
|
||||||
|
});
|
||||||
|
|
||||||
|
movesList.forEach((move) => {
|
||||||
|
move.addEventListener("click", clickHandlerDelay);
|
||||||
|
move.addEventListener("touchstart", clickHandlerDelay);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
boardButtons.forEach((button) => {
|
||||||
|
button.removeEventListener("click", handleClick);
|
||||||
|
button.removeEventListener("touchstart", handleClick);
|
||||||
|
});
|
||||||
|
|
||||||
|
movesList.forEach((move) => {
|
||||||
|
move.removeEventListener("click", clickHandlerDelay);
|
||||||
|
move.removeEventListener("touchstart", clickHandlerDelay);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, [gamePgn, handlePlyChange]);
|
||||||
|
|
||||||
|
return viewerRef;
|
||||||
|
};
|
||||||
@@ -46,9 +46,9 @@ const Chessboard = () => {
|
|||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid lg:grid-cols-2 gap-4">
|
||||||
<Suspense fallback={<LoadingBoard />}>
|
<Suspense fallback={<LoadingBoard />}>
|
||||||
<div>
|
<div className="mx-auto">
|
||||||
<PgnViewer
|
<PgnViewer
|
||||||
gamePgn={gameState.pgn || ""}
|
gamePgn={gameState.pgn || ""}
|
||||||
handlePlyChange={handlePlyChange}
|
handlePlyChange={handlePlyChange}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
const Contact = () => <div>Hello "/contact"!</div>;
|
|
||||||
|
|
||||||
export default Contact;
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import Contact from "#/pages/Contact/Contact.tsx";
|
||||||
|
|
||||||
|
const ContactContainer = () => {
|
||||||
|
const handleSubmit = async (_prevState: unknown, data: FormData) => {
|
||||||
|
const name = data.get("name");
|
||||||
|
const email = data.get("email");
|
||||||
|
const subject = data.get("subject");
|
||||||
|
const message = data.get("message");
|
||||||
|
|
||||||
|
try {
|
||||||
|
return { success: true, message: "Message sent!" };
|
||||||
|
} catch (error) {
|
||||||
|
// TODO logger
|
||||||
|
console.error("Error sending message:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Failed to send message. Please try again.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Contact handleSubmit={handleSubmit} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContactContainer;
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { useActionState } from "react";
|
||||||
|
import Section from "#/components/Section.tsx";
|
||||||
|
|
||||||
|
interface IFormField {
|
||||||
|
fieldName: string;
|
||||||
|
type: "text" | "email" | "textarea";
|
||||||
|
placeholder: string;
|
||||||
|
label: string;
|
||||||
|
required: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type IProps = {
|
||||||
|
handleSubmit: (
|
||||||
|
prevState: unknown,
|
||||||
|
formData: FormData,
|
||||||
|
) => Promise<{ success: boolean; message: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormField = ({
|
||||||
|
type,
|
||||||
|
fieldName,
|
||||||
|
placeholder,
|
||||||
|
label,
|
||||||
|
required,
|
||||||
|
}: IFormField) => (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label htmlFor={fieldName}>{label}</label>
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
id={fieldName}
|
||||||
|
name={fieldName}
|
||||||
|
placeholder={placeholder}
|
||||||
|
required={required}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Contact = ({ handleSubmit }: IProps) => {
|
||||||
|
const [state, formAction, isPending] = useActionState(handleSubmit, null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="px-4 pb-8 place-self-center min-h-[calc(100vh-226px)]">
|
||||||
|
<Section
|
||||||
|
title="Contact Us"
|
||||||
|
description="Do you have suggestions on how to improve this service? Would you like to get involved with this project? Ask us anything you like."
|
||||||
|
>
|
||||||
|
<form action={formAction}>
|
||||||
|
<FormField
|
||||||
|
type="text"
|
||||||
|
fieldName="name"
|
||||||
|
placeholder="Your name"
|
||||||
|
label="Name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
fieldName="email"
|
||||||
|
type="text"
|
||||||
|
placeholder="name@example.com"
|
||||||
|
label="Email"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="subject"
|
||||||
|
placeholder="Reason for contacting us"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
name="message"
|
||||||
|
placeholder="Your message here..."
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isPending}
|
||||||
|
className="btn btn-primary"
|
||||||
|
>
|
||||||
|
{isPending ? "Sending..." : "Send"}
|
||||||
|
</button>
|
||||||
|
{state && <p>{state.message}</p>}
|
||||||
|
</form>
|
||||||
|
</Section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Contact;
|
||||||
@@ -21,12 +21,24 @@ export const Route = createRootRoute({
|
|||||||
{
|
{
|
||||||
title: "ChessScribe",
|
title: "ChessScribe",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "description",
|
||||||
|
content: "Create PDFs of your chess games from a PGN file",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "keywords",
|
||||||
|
content: "chess, pgn, pdf, chess games, chess notation",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
rel: "stylesheet",
|
rel: "stylesheet",
|
||||||
href: appCss,
|
href: appCss,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
rel: "icon",
|
||||||
|
href: "/favicon.svg",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
shellComponent: RootDocument,
|
shellComponent: RootDocument,
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { HOST_URL } from "#/config.ts";
|
||||||
import Chessboard from "#/pages/Chessboard.tsx";
|
import Chessboard from "#/pages/Chessboard.tsx";
|
||||||
|
|
||||||
export const Route = createFileRoute("/chessboard")({
|
export const Route = createFileRoute("/chessboard")({
|
||||||
|
head: () => ({
|
||||||
|
meta: [{ title: "ChessScribe | Chessboard" }],
|
||||||
|
links: [{ rel: "canonical", href: `${HOST_URL}/chessboard` }],
|
||||||
|
}),
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import Contact from "#/pages/Contact.tsx";
|
import { HOST_URL } from "#/config.ts";
|
||||||
|
import ContactContainer from "#/pages/Contact/Contact.container.tsx";
|
||||||
|
|
||||||
export const Route = createFileRoute("/contact")({
|
export const Route = createFileRoute("/contact")({
|
||||||
|
head: () => ({
|
||||||
|
meta: [{ title: "ChessScribe | Contact" }],
|
||||||
|
links: [{ rel: "canonical", href: `${HOST_URL}/contact` }],
|
||||||
|
}),
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
});
|
});
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
return <Contact />;
|
return <ContactContainer />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { HOST_URL } from "#/config.ts";
|
||||||
import Home from "#/pages";
|
import Home from "#/pages";
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({ component: App });
|
export const Route = createFileRoute("/")({
|
||||||
|
head: () => ({
|
||||||
|
meta: [{ title: "ChessScribe | Home" }],
|
||||||
|
links: [{ rel: "canonical", href: HOST_URL }],
|
||||||
|
}),
|
||||||
|
component: App,
|
||||||
|
});
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return <Home />;
|
return <Home />;
|
||||||
|
|||||||
+26
-16
@@ -1,21 +1,31 @@
|
|||||||
import { defineConfig } from 'vite'
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import { devtools } from '@tanstack/devtools-vite'
|
import { devtools } from "@tanstack/devtools-vite";
|
||||||
|
|
||||||
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
|
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
|
||||||
|
|
||||||
import viteReact from '@vitejs/plugin-react'
|
import viteReact from "@vitejs/plugin-react";
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import { nitro } from "nitro/vite";
|
||||||
import { nitro } from 'nitro/vite'
|
import { defineConfig } from "vite";
|
||||||
|
import { HOST_URL } from "#/config.ts";
|
||||||
|
|
||||||
const config = defineConfig({
|
const config = defineConfig({
|
||||||
resolve: { tsconfigPaths: true },
|
resolve: { tsconfigPaths: true },
|
||||||
plugins: [
|
plugins: [
|
||||||
devtools(),
|
devtools(),
|
||||||
nitro({ rollupConfig: { external: [/^@sentry\//] } }),
|
nitro({ rollupConfig: { external: [/^@sentry\//] } }),
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
tanstackStart(),
|
tanstackStart({
|
||||||
viteReact(),
|
prerender: {
|
||||||
],
|
enabled: true,
|
||||||
})
|
crawlLinks: true,
|
||||||
|
},
|
||||||
|
sitemap: {
|
||||||
|
enabled: true,
|
||||||
|
host: HOST_URL,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
viteReact(),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
export default config
|
export default config;
|
||||||
|
|||||||
Reference in New Issue
Block a user