mirror of
https://github.com/TheRealOwenRees/chess-scribe-website.git
synced 2026-07-23 01:46:57 +00:00
Compare commits
17 Commits
f475732715
...
2e05f86d5f
| Author | SHA1 | Date | |
|---|---|---|---|
|
2e05f86d5f
|
|||
|
4577755c93
|
|||
|
a110c7490c
|
|||
|
0b10637df4
|
|||
|
41cbd5db16
|
|||
|
af6a8426ad
|
|||
|
0dde848cfe
|
|||
|
ee9968e99b
|
|||
|
7384a3f825
|
|||
|
887a43636f
|
|||
|
734c631356
|
|||
|
802d46191e
|
|||
|
4edcb49f58
|
|||
|
030d44226d
|
|||
|
f12176e713
|
|||
|
3da81680a1
|
|||
|
0b32e77d7e
|
+4
-1
@@ -1,4 +1,7 @@
|
|||||||
|
VITE_API_BASE_URL=
|
||||||
|
VITE_MATOMO_URL=
|
||||||
|
VITE_MATOMO_SITE_ID=
|
||||||
|
|
||||||
DISCORD_CONTACT_WEBHOOK=
|
DISCORD_CONTACT_WEBHOOK=
|
||||||
DISCORD_ERROR_LOG_WEBHOOK=
|
DISCORD_ERROR_LOG_WEBHOOK=
|
||||||
LICHESS_CLIENT_ID=
|
LICHESS_CLIENT_ID=
|
||||||
VITE_API_BASE_URL=
|
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
- og / json-sd
|
- og / json-sd
|
||||||
- logger
|
- logger
|
||||||
- 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)
|
- 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
|
- 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 - footer colour contrast
|
||||||
- check wcag compliance
|
- 429 lichess error handling - retry after 10 seconds
|
||||||
- 429 lichess error handling
|
|
||||||
- add throw errors or return errors in server code
|
|
||||||
- try/catch where needed in client code
|
- try/catch where needed in client code
|
||||||
- generate state and check it against the state returned from lichess https://lichess.org/api#tag/oauth/GET/oauth
|
- generate state and check it against the state returned from lichess https://lichess.org/api#tag/oauth/GET/oauth
|
||||||
- checkbox and toggle disabled states
|
- checkbox and toggle disabled states
|
||||||
- 404 page
|
- react query on for study and chapter load, to allow caching
|
||||||
- lichess data with zod validation
|
- https://specification.website/checklist/
|
||||||
- react query on for study and chapter load, to allow caching
|
|
||||||
@@ -9,10 +9,7 @@ interface IProps {
|
|||||||
const AccordionItem = ({ title, id, children }: IProps) => (
|
const AccordionItem = ({ title, id, children }: IProps) => (
|
||||||
<div className="hero__accordion-item">
|
<div className="hero__accordion-item">
|
||||||
<details className="hero__accordion-details" name="linked">
|
<details className="hero__accordion-details" name="linked">
|
||||||
<summary
|
<summary className="hero__accordion-item__summary" id={`${id}-summary`}>
|
||||||
className="hero__accordion-item__summary"
|
|
||||||
aria-describedby={title}
|
|
||||||
>
|
|
||||||
<span className="hero__accordion-item__title-container">
|
<span className="hero__accordion-item__title-container">
|
||||||
<span className="hero__accordion-item__title">{title}</span>
|
<span className="hero__accordion-item__title">{title}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const CoffeeWidget = ({ height, width }: IProps) => {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
className="ml-auto flex items-center gap-2 text-sm"
|
className="ml-auto flex items-center gap-2 text-sm"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
|
aria-label="Buy Me A Coffee"
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={buyMeACoffeeLogo}
|
src={buyMeACoffeeLogo}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ interface IProps {
|
|||||||
|
|
||||||
const Feature = ({ title, children }: IProps) => (
|
const Feature = ({ title, children }: IProps) => (
|
||||||
<div className="mb-auto">
|
<div className="mb-auto">
|
||||||
<h6 className="text-(--neutral-content) text-2xl font-bold">{title}</h6>
|
<h5 className="text-(--neutral-content) text-2xl font-bold">{title}</h5>
|
||||||
<p className="text-(--neutral-content) font-medium" data-testid="text">
|
<p className="text-(--neutral-content) font-medium" data-testid="text">
|
||||||
{children}
|
{children}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -10,26 +10,26 @@ import footerLogo from "@/assets/images/footerLogo.svg?url";
|
|||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
return (
|
return (
|
||||||
<footer className="text-white w-full grid-cols-2 items-center bg-(--accent) px-10 py-5">
|
<footer className="text-white w-full grid-cols-2 items-center bg-(--accent) px-10 py-5">
|
||||||
<div className="flex w-full items-center">
|
<div className="flex w-full items-center gap-2">
|
||||||
<Link to="/">
|
<Link to="/" aria-label="Home">
|
||||||
<Image
|
<Image
|
||||||
src={footerLogo}
|
src={footerLogo}
|
||||||
alt="ChessScribe Logo"
|
alt="ChessScribe Logo"
|
||||||
width={10}
|
width={10}
|
||||||
height={10}
|
height={10}
|
||||||
className="hidden h-10 w-10 sm:block"
|
className="hidden h-10 w-10 sm:block"
|
||||||
|
aria-label="ChessScribe Logo"
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
<div>
|
<div>
|
||||||
<p data-testid="copyright">
|
<p>© 2023 - {new Date().getFullYear()}</p>
|
||||||
Copyright © 2023 - {new Date().getFullYear()}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-auto flex gap-2">
|
<div className="ml-auto flex gap-2">
|
||||||
<a
|
<a
|
||||||
href={GITHUB_URL}
|
href={GITHUB_URL}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
aria-label="GitHub"
|
||||||
className="text-2xl hover:text-(--neutral-content) transition-colors duration-300 ease-in-out cursor-pointer"
|
className="text-2xl hover:text-(--neutral-content) transition-colors duration-300 ease-in-out cursor-pointer"
|
||||||
>
|
>
|
||||||
<LuGithub />
|
<LuGithub />
|
||||||
@@ -38,6 +38,7 @@ export default function Footer() {
|
|||||||
<Link
|
<Link
|
||||||
to="/contact"
|
to="/contact"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
aria-label="Contact"
|
||||||
className="text-2xl hover:text-(--neutral-content) transition-colors duration-300 ease-in-out"
|
className="text-2xl hover:text-(--neutral-content) transition-colors duration-300 ease-in-out"
|
||||||
>
|
>
|
||||||
<LuMail />
|
<LuMail />
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const LichessButton = () => {
|
|||||||
const { user } = useLichessUser();
|
const { user } = useLichessUser();
|
||||||
|
|
||||||
const onClickHandler = () => {
|
const onClickHandler = () => {
|
||||||
return user?.isLoggedIn ? lichessLogout() : lichessLogin();
|
return user ? lichessLogout() : lichessLogin();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -26,9 +26,7 @@ const LichessButton = () => {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>
|
<p>{user ? `Logout ${user.username}` : "Log into Lichess.org"}</p>
|
||||||
{user?.isLoggedIn ? `Logout ${user.username}` : "Log into Lichess.org"}
|
|
||||||
</p>
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { trackAppRouter } from "@socialgouv/matomo-next";
|
import { trackAppRouter } from "@socialgouv/matomo-next";
|
||||||
import { useLocation, useSearch } from "@tanstack/react-router";
|
import { useLocation, useSearch } from "@tanstack/react-router";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
import { env } from "#/env.ts";
|
||||||
|
|
||||||
const MATOMO_URL = import.meta.env.NEXT_PUBLIC_MATOMO_URL;
|
const MATOMO_URL = env.VITE_MATOMO_URL;
|
||||||
const MATOMO_SITE_ID = import.meta.env.NEXT_PUBLIC_MATOMO_SITE_ID;
|
const MATOMO_SITE_ID = env.VITE_MATOMO_SITE_ID;
|
||||||
|
|
||||||
export function MatomoAnalytics() {
|
export function MatomoAnalytics() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
const NotFoundView = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-[calc(100vh-194px)] flex-col items-center justify-center p-6">
|
||||||
|
<div className="relative flex max-w-md flex-col items-center text-center">
|
||||||
|
<h1 className="text-2xl font-bold tracking-tight mb-4 text-(--accent)">
|
||||||
|
The page you are looking for does not exist.
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className="flex w-full gap-3 justify-center">
|
||||||
|
<Link to="/" className="btn btn-primary">
|
||||||
|
Go Home
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotFoundView;
|
||||||
@@ -14,7 +14,7 @@ const Section = ({ title, smallTitle, description, children }: IProps) => {
|
|||||||
|
|
||||||
return (
|
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)">
|
<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="col-span-full mb-4 text-center text-lg font-bold tracking-wider text-primary">
|
<h3 className="col-span-full mb-4 text-center text-2xl font-bold tracking-wider text-primary">
|
||||||
{smallTitle}
|
{smallTitle}
|
||||||
</h3>
|
</h3>
|
||||||
<h4 className="col-span-full 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">
|
||||||
|
|||||||
@@ -9,14 +9,10 @@ import {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import type { ISession } from "#/interfaces.ts";
|
||||||
import { getSession } from "#/server/lichess.ts";
|
import { getSession } from "#/server/lichess.ts";
|
||||||
|
|
||||||
interface ILichessUser {
|
interface ILichessUser extends Omit<ISession, "token"> {}
|
||||||
username: string;
|
|
||||||
id: string;
|
|
||||||
isLoggedIn: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ILichessUserContextType {
|
interface ILichessUserContextType {
|
||||||
user: ILichessUser | null;
|
user: ILichessUser | null;
|
||||||
@@ -38,7 +34,6 @@ export const LichessUserProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
setUser({
|
setUser({
|
||||||
username: session.username,
|
username: session.username,
|
||||||
id: session.id,
|
id: session.id,
|
||||||
isLoggedIn: true,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { z } from "zod";
|
|||||||
|
|
||||||
const clientSchema = z.object({
|
const clientSchema = z.object({
|
||||||
VITE_API_BASE_URL: z.string("Invalid API Base URL format"),
|
VITE_API_BASE_URL: z.string("Invalid API Base URL format"),
|
||||||
|
VITE_MATOMO_URL: z.string("Invalid URL format"),
|
||||||
|
VITE_MATOMO_SITE_ID: z.string("Invalid Site ID"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const serverSchema = z.object({
|
const serverSchema = z.object({
|
||||||
|
|||||||
+6
-20
@@ -1,6 +1,11 @@
|
|||||||
import { useServerFn } from "@tanstack/react-start";
|
import { useServerFn } from "@tanstack/react-start";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useLichessUser } from "#/context/LichessUserContext.tsx";
|
import { useLichessUser } from "#/context/LichessUserContext.tsx";
|
||||||
|
import type {
|
||||||
|
ITokenData,
|
||||||
|
IUserStudy,
|
||||||
|
IUserStudyChapter,
|
||||||
|
} from "#/interfaces.ts";
|
||||||
import {
|
import {
|
||||||
getSession,
|
getSession,
|
||||||
getStudyChapters,
|
getStudyChapters,
|
||||||
@@ -15,25 +20,6 @@ import { getHeaders } from "#/utils/pgnUtils.ts";
|
|||||||
|
|
||||||
type Status = "loading" | "success" | "error";
|
type Status = "loading" | "success" | "error";
|
||||||
|
|
||||||
interface ITokenData {
|
|
||||||
access_token: string;
|
|
||||||
expires_in: number;
|
|
||||||
token_type: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type IUserStudy = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
createdAt: number;
|
|
||||||
updatedAt: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface IUserStudyChapter {
|
|
||||||
chapterId: string;
|
|
||||||
name: string;
|
|
||||||
pgn: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useLichess = () => {
|
export const useLichess = () => {
|
||||||
const { user, setUser } = useLichessUser();
|
const { user, setUser } = useLichessUser();
|
||||||
const [userStudies, setUserStudies] = useState<IUserStudy[]>([]);
|
const [userStudies, setUserStudies] = useState<IUserStudy[]>([]);
|
||||||
@@ -65,7 +51,7 @@ export const useLichess = () => {
|
|||||||
token: string;
|
token: string;
|
||||||
}) => {
|
}) => {
|
||||||
await setSessionFn({ data: { username, id, token } });
|
await setSessionFn({ data: { username, id, token } });
|
||||||
setUser({ username, id, isLoggedIn: true });
|
setUser({ username, id });
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLichessUser = async ({ tokenData }: { tokenData: ITokenData }) => {
|
const getLichessUser = async ({ tokenData }: { tokenData: ITokenData }) => {
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
import type { infer } from "zod";
|
||||||
|
import type { SessionSchema, TokenResponseSchema } from "#/schemas.ts";
|
||||||
|
|
||||||
|
export interface ISession extends infer<typeof SessionSchema> {}
|
||||||
|
export interface ITokenData extends infer<typeof TokenResponseSchema> {}
|
||||||
|
|
||||||
|
export interface IUserStudy {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUserStudyChapter {
|
||||||
|
chapterId: string;
|
||||||
|
name: string;
|
||||||
|
pgn: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IPosition {
|
export interface IPosition {
|
||||||
ply: number;
|
ply: number;
|
||||||
fen: string;
|
fen: string;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { ToastContainer } from "react-toastify";
|
|||||||
import GenericErrorView from "#/components/GenericErrorView.tsx";
|
import GenericErrorView from "#/components/GenericErrorView.tsx";
|
||||||
import LoadingView from "#/components/LoadingView.tsx";
|
import LoadingView from "#/components/LoadingView.tsx";
|
||||||
import { MatomoAnalytics } from "#/components/MatomoAnalytics.tsx";
|
import { MatomoAnalytics } from "#/components/MatomoAnalytics.tsx";
|
||||||
|
import NotFoundView from "#/components/NotFoundView.tsx";
|
||||||
import { LichessUserProvider } from "#/context/LichessUserContext.tsx";
|
import { LichessUserProvider } from "#/context/LichessUserContext.tsx";
|
||||||
import Footer from "../components/Footer";
|
import Footer from "../components/Footer";
|
||||||
import Header from "../components/Header";
|
import Header from "../components/Header";
|
||||||
@@ -47,6 +48,7 @@ export const Route = createRootRoute({
|
|||||||
shellComponent: RootDocument,
|
shellComponent: RootDocument,
|
||||||
errorComponent: ({ error }) => <GenericErrorView error={error} />,
|
errorComponent: ({ error }) => <GenericErrorView error={error} />,
|
||||||
pendingComponent: () => <LoadingView />,
|
pendingComponent: () => <LoadingView />,
|
||||||
|
notFoundComponent: () => <NotFoundView />,
|
||||||
});
|
});
|
||||||
|
|
||||||
function RootDocument({ children }: { children: ReactNode }) {
|
function RootDocument({ children }: { children: ReactNode }) {
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const SessionSchema = z.object({
|
||||||
|
username: z.string(),
|
||||||
|
id: z.string(),
|
||||||
|
token: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TokenResponseSchema = z.object({
|
||||||
|
access_token: z.string(),
|
||||||
|
expires_in: z.number(),
|
||||||
|
scope: z.string().optional(),
|
||||||
|
token_type: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const LichessAccountSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
username: z.string(),
|
||||||
|
});
|
||||||
+29
-30
@@ -6,8 +6,14 @@ import {
|
|||||||
getCookie,
|
getCookie,
|
||||||
setCookie,
|
setCookie,
|
||||||
} from "@tanstack/react-start/server";
|
} from "@tanstack/react-start/server";
|
||||||
|
import { z } from "zod";
|
||||||
import { env } from "#/env.ts";
|
import { env } from "#/env.ts";
|
||||||
import { base64UrlEncode, sha256 } from "#/lib/encodeDecode.ts";
|
import { base64UrlEncode, sha256 } from "#/lib/encodeDecode.ts";
|
||||||
|
import {
|
||||||
|
LichessAccountSchema,
|
||||||
|
SessionSchema,
|
||||||
|
TokenResponseSchema,
|
||||||
|
} from "#/schemas.ts";
|
||||||
|
|
||||||
const createVerifier = () => base64UrlEncode(randomBytes(32));
|
const createVerifier = () => base64UrlEncode(randomBytes(32));
|
||||||
const createChallenge = (verifier: string) => base64UrlEncode(sha256(verifier));
|
const createChallenge = (verifier: string) => base64UrlEncode(sha256(verifier));
|
||||||
@@ -22,11 +28,8 @@ export const getSession = createServerFn({ method: "GET" }).handler(
|
|||||||
const session = getCookie("lichess-session");
|
const session = getCookie("lichess-session");
|
||||||
if (!session) return null;
|
if (!session) return null;
|
||||||
try {
|
try {
|
||||||
return JSON.parse(session) as {
|
const parsed = SessionSchema.safeParse(JSON.parse(session));
|
||||||
username: string;
|
return parsed.success ? parsed.data : null;
|
||||||
id: string;
|
|
||||||
token: string;
|
|
||||||
};
|
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -34,25 +37,15 @@ export const getSession = createServerFn({ method: "GET" }).handler(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const setSession = createServerFn({ method: "POST" })
|
export const setSession = createServerFn({ method: "POST" })
|
||||||
.inputValidator(
|
.inputValidator(SessionSchema)
|
||||||
(data: { username: string; id: string; token: string }) => data,
|
|
||||||
)
|
|
||||||
.handler(async ({ data }) => {
|
.handler(async ({ data }) => {
|
||||||
setCookie(
|
setCookie("lichess-session", JSON.stringify(data), {
|
||||||
"lichess-session",
|
path: "/",
|
||||||
JSON.stringify({
|
httpOnly: true,
|
||||||
username: data.username,
|
secure: process.env.NODE_ENV === "production",
|
||||||
id: data.id,
|
sameSite: "lax",
|
||||||
token: data.token,
|
maxAge: 60 * 60 * 24 * 7,
|
||||||
}),
|
});
|
||||||
{
|
|
||||||
path: "/",
|
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === "production",
|
|
||||||
sameSite: "lax",
|
|
||||||
maxAge: 60 * 60 * 24 * 7,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
setCookie("lichess-oauth-verifier", "", {
|
setCookie("lichess-oauth-verifier", "", {
|
||||||
path: "/",
|
path: "/",
|
||||||
@@ -66,12 +59,13 @@ export const setSession = createServerFn({ method: "POST" })
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const getToken = createServerFn({ method: "POST" })
|
export const getToken = createServerFn({ method: "POST" })
|
||||||
.inputValidator((data: { code: string }) => data)
|
.inputValidator(z.object({ code: z.string() }))
|
||||||
.handler(async ({ data }) => {
|
.handler(async ({ data }) => {
|
||||||
const verifier = getCookie("lichess-oauth-verifier");
|
const verifier = getCookie("lichess-oauth-verifier");
|
||||||
|
|
||||||
if (!verifier)
|
if (!verifier) {
|
||||||
throw new Error("OAuth session expired. Please login again.");
|
throw new Error("OAuth session expired. Please login again.");
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch("https://lichess.org/api/token", {
|
const response = await fetch("https://lichess.org/api/token", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -87,14 +81,16 @@ export const getToken = createServerFn({ method: "POST" })
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok)
|
if (!response.ok) {
|
||||||
throw new Error(`Token exchange failed: ${await response.text()}`);
|
throw new Error(`Token exchange failed: ${await response.text()}`);
|
||||||
|
}
|
||||||
|
|
||||||
return response.json();
|
const json = await response.json();
|
||||||
|
return TokenResponseSchema.parse(json);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getUser = createServerFn({ method: "GET" })
|
export const getUser = createServerFn({ method: "POST" })
|
||||||
.inputValidator((data: { access_token: string }) => data)
|
.inputValidator(z.object({ access_token: z.string() }))
|
||||||
.handler(async ({ data }) => {
|
.handler(async ({ data }) => {
|
||||||
const response = await fetch("https://lichess.org/api/account", {
|
const response = await fetch("https://lichess.org/api/account", {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -102,7 +98,10 @@ export const getUser = createServerFn({ method: "GET" })
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.json();
|
if (!response.ok) throw new Error("Failed to fetch Lichess account");
|
||||||
|
|
||||||
|
const json = await response.json();
|
||||||
|
return LichessAccountSchema.parse(json);
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteAccessToken = createServerFn().handler(async () => {
|
const deleteAccessToken = createServerFn().handler(async () => {
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ const config = defineConfig({
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
crawlLinks: true,
|
crawlLinks: true,
|
||||||
},
|
},
|
||||||
|
pages: [
|
||||||
|
{
|
||||||
|
path: "/callback",
|
||||||
|
prerender: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
sitemap: {
|
sitemap: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
host: HOST_URL,
|
host: HOST_URL,
|
||||||
|
|||||||
Reference in New Issue
Block a user