Compare commits

...

5 Commits

9 changed files with 133 additions and 36 deletions
-1
View File
@@ -1,7 +1,6 @@
- og / json-sd
- Lichess login
- logger
- contact form
- 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
+3 -3
View File
@@ -14,13 +14,13 @@ const Section = ({ title, smallTitle, description, children }: IProps) => {
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">
<h3 className="col-span-full 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">
<h4 className="col-span-full 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) ">
<p className="col-span-full mb-8 w-1/2 text-center font-semibold text-(--neutral-content) ">
{description}
</p>
{children}
+21
View File
@@ -0,0 +1,21 @@
import { useEffect } from "react";
import { toast } from "react-toastify";
interface IProps {
state: {
success: boolean;
message: string;
} | null;
}
export const useToastStateChange = ({ state }: IProps) => {
useEffect(() => {
if (!state) return;
if (state.success) {
toast.success(state.message);
} else {
toast.error(state.message);
}
}, [state]);
};
+5 -5
View File
@@ -53,15 +53,15 @@ const Chessboard = () => {
gamePgn={gameState.pgn || ""}
handlePlyChange={handlePlyChange}
/>
<div className="grid grid-cols-3 gap-2 max-w-150 mt-2">
<div className="grid grid-cols-12 gap-4 max-w-150 mt-2">
<button
className="btn btn-primary"
className="btn btn-primary col-span-5 flex items-center justify-center"
type="button"
onClick={handleClearGame}
>
Clear Game
<p className="text-sm font-semibold">Clear Game</p>
</button>
<div className="flex gap-2 items-center">
<div className="flex gap-1 items-center col-span-3">
<label
htmlFor="diagram-add"
className="text-sm text-(--base-content)"
@@ -80,7 +80,7 @@ const Chessboard = () => {
/>
</div>
<label className="toggle text-sm text-(--base-content)">
<label className="toggle text-sm text-(--base-content) col-span-4">
Render move times
<input
id="render-times"
+14 -5
View File
@@ -1,17 +1,26 @@
import Contact from "#/pages/Contact/Contact.tsx";
import { sendToDiscord } from "#/server/contact.ts";
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");
const payload = {
name: (data.get("name") as string) || "",
email: (data.get("email") as string) || "",
subject: (data.get("subject") as string) || "",
message: (data.get("message") as string) || "",
};
try {
return { success: true, message: "Message sent!" };
await sendToDiscord({ data: payload });
return {
success: true,
message: "Message sent successfully!",
};
} catch (error) {
// TODO logger
console.error("Error sending message:", error);
return {
success: false,
message: "Failed to send message. Please try again.",
+40 -15
View File
@@ -1,12 +1,14 @@
import { useActionState } from "react";
import { LuSend } from "react-icons/lu";
import Section from "#/components/Section.tsx";
import { useToastStateChange } from "#/hooks/useToastStateChange.ts";
interface IFormField {
fieldName: string;
type: "text" | "email" | "textarea";
placeholder: string;
label: string;
required: boolean;
required?: boolean;
textarea?: boolean;
}
type IProps = {
@@ -20,23 +22,42 @@ const FormField = ({
type,
fieldName,
placeholder,
label,
required,
required = false,
textarea = false,
}: IFormField) => (
<div className="flex flex-col gap-2">
<label htmlFor={fieldName}>{label}</label>
<label
htmlFor={fieldName}
className="capitalize text-(--neutral-content) text-left"
>
{fieldName}
{required && <span>*</span>}
</label>
{textarea ? (
<textarea
id={fieldName}
name={fieldName}
placeholder={placeholder}
required={required}
rows={5}
className="text-(--base-content) w-full rounded-md border border-neutral-300 px-3 py-2 placeholder:text-neutral-400/50 outline-hidden transition-all duration-200 focus:border-(--accent) focus:outline-3 focus:outline-offset-2 focus:outline-(--accent)/50 resize-y"
/>
) : (
<input
type={type}
id={fieldName}
name={fieldName}
placeholder={placeholder}
required={required}
className="text-(--base-content) w-full rounded-md border border-neutral-300 px-3 py-2 placeholder:text-neutral-400/50 outline-hidden transition-all duration-200 focus:border-(--accent) focus:outline-3 focus:outline-offset-2 focus:outline-(--accent)/50"
/>
)}
</div>
);
const Contact = ({ handleSubmit }: IProps) => {
const [state, formAction, isPending] = useActionState(handleSubmit, null);
useToastStateChange({ state });
return (
<main className="px-4 pb-8 place-self-center min-h-[calc(100vh-226px)]">
@@ -44,43 +65,47 @@ const Contact = ({ handleSubmit }: IProps) => {
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}>
<form
action={formAction}
className="col-span-full max-w-150 flex flex-col gap-6 w-full"
>
<FormField
type="text"
fieldName="name"
placeholder="Your name"
label="Name"
required
/>
<FormField
fieldName="email"
type="text"
type="email"
placeholder="name@example.com"
label="Email"
required
/>
<input
<FormField
fieldName="subject"
type="text"
name="subject"
placeholder="Reason for contacting us"
/>
<textarea
name="message"
<FormField
fieldName="message"
type="text"
placeholder="Your message here..."
required
textarea
/>
<button
type="submit"
disabled={isPending}
className="btn btn-primary"
className="btn btn-primary flex items-center justify-center gap-2 w-full"
>
{isPending ? "Sending..." : "Send"}
<LuSend className="w-5 h-5" />
</button>
{state && <p>{state.message}</p>}
{/*{state && <p>{state.message}</p>}*/}
</form>
</Section>
</main>
+38
View File
@@ -0,0 +1,38 @@
import { createServerFn } from "@tanstack/react-start";
import { env } from "#/env.ts";
export const sendToDiscord = createServerFn({ method: "POST" })
.inputValidator(
(data: { name: string; email: string; subject: string; message: string }) =>
data,
)
.handler(async ({ data }) => {
const response = await fetch(env.DISCORD_CONTACT_WEBHOOK, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
embeds: [
{
title: "Contact Form Submission",
fields: [
{ name: "Name", value: data.name.trim() || "Not provided" },
{ name: "Email", value: data.email.trim() || "Not provided" },
{ name: "Subject", value: data.subject.trim() || "No Subject" },
{
name: "Message",
value: data.message.trim() || "Empty message",
},
],
},
],
}),
});
if (!response.ok) {
throw new Error("Discord API rejected the request");
}
return { success: true };
});
+1
View File
@@ -4,6 +4,7 @@
@import "./styles/_button.css";
@import "./styles/_file-input.css";
@import './styles/_pgn-viewer.css';
@import "./styles/_toastify-overrides.css";
@plugin "@tailwindcss/typography";
+4
View File
@@ -0,0 +1,4 @@
:root {
/* biome-ignore lint/complexity/noImportantStyles: <override vendor style requires !important> */
--toastify-color-success: var(--accent) !important;
}