mirror of
https://github.com/TheRealOwenRees/chess-scribe-website.git
synced 2026-07-23 01:46:57 +00:00
import chapter
This commit is contained in:
@@ -1,20 +1,76 @@
|
||||
import { useLichess } from "#/hooks/useLichess.ts";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { type IUserStudyChapter, useLichess } from "#/hooks/useLichess.ts";
|
||||
|
||||
const SelectLichessStudy = () => {
|
||||
const { getLichessUserStudies } = useLichess();
|
||||
interface IProps {
|
||||
setSelectedStudyId: (studyId: string) => void;
|
||||
setStudyChapters: (chapters: IUserStudyChapter[]) => void;
|
||||
}
|
||||
|
||||
const SelectLichessStudy = ({ setSelectedStudyId }: IProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { getLichessUserStudies, userStudies } = useLichess();
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const onClickHandler = async () => {
|
||||
await getLichessUserStudies();
|
||||
setIsOpen(!isOpen);
|
||||
|
||||
if (!userStudies.length) {
|
||||
await getLichessUserStudies();
|
||||
}
|
||||
};
|
||||
|
||||
const handleStudyClick = async (studyId: string) => {
|
||||
setSelectedStudyId(studyId);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
isOpen &&
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={onClickHandler}
|
||||
>
|
||||
Select Lichess Study
|
||||
</button>
|
||||
<div ref={dropdownRef} className="relative">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={onClickHandler}
|
||||
>
|
||||
Select Lichess Study
|
||||
</button>
|
||||
|
||||
{isOpen ? (
|
||||
<div className="absolute z-3 bg-(--bg-base) mt-2 max-h-75 border rounded-lg p-2 overflow-y-scroll">
|
||||
<input type="text" placeholder="Search..." />
|
||||
<ul>
|
||||
{userStudies.map((study) => (
|
||||
<li key={study.id}>
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 cursor-pointer hover:bg-(--neutral-content)/20 text-left w-full"
|
||||
onClick={() => handleStudyClick(study.id)}
|
||||
>
|
||||
{study.name}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { type IUserStudyChapter, useLichess } from "#/hooks/useLichess.ts";
|
||||
|
||||
interface IProps {
|
||||
selectedStudyId: string;
|
||||
gameDispatch: any;
|
||||
}
|
||||
|
||||
const SelectStudyChapter = ({ selectedStudyId, gameDispatch }: IProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [studyChapters, setStudyChapters] = useState<IUserStudyChapter[]>([]);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { getLichessStudyChapters, loadLichessStudyChapter } = useLichess();
|
||||
|
||||
const onClickHandler = async () => {
|
||||
const studies = await getLichessStudyChapters({ studyId: selectedStudyId });
|
||||
if (!studies) return;
|
||||
setStudyChapters(studies);
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
const handleChapterClick = async (chapter: IUserStudyChapter) => {
|
||||
const { headers, pgn } = await loadLichessStudyChapter({ chapter });
|
||||
|
||||
gameDispatch({ type: "SET_PGN", payload: headers, pgn });
|
||||
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={dropdownRef} className="relative">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={onClickHandler}
|
||||
>
|
||||
Import Study Chapter/Game
|
||||
</button>
|
||||
|
||||
{isOpen ? (
|
||||
<div className="absolute z-3 bg-(--bg-base) mt-2 max-h-75 border rounded-lg p-2 overflow-y-scroll">
|
||||
<input type="text" placeholder="Search..." />
|
||||
<ul>
|
||||
{studyChapters?.map((chapter) => (
|
||||
<li key={`${chapter.chapterId}${chapter.name}`}>
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 cursor-pointer hover:bg-(--neutral-content)/20 text-left w-full"
|
||||
onClick={() => handleChapterClick(chapter)}
|
||||
>
|
||||
{chapter.name}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectStudyChapter;
|
||||
@@ -189,5 +189,6 @@ export const useChessGame = () => {
|
||||
currentPosition,
|
||||
generatingPdf,
|
||||
updateHeaders,
|
||||
gameDispatch,
|
||||
};
|
||||
};
|
||||
|
||||
+64
-2
@@ -1,9 +1,11 @@
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { useServerFn } from "@tanstack/react-start";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useLichessUser } from "#/context/LichessUserContext.tsx";
|
||||
import { useChessGame } from "#/hooks/useChessGame.ts";
|
||||
import {
|
||||
getSession,
|
||||
getStudyChapters,
|
||||
getToken,
|
||||
getUser,
|
||||
getUserStudies,
|
||||
@@ -11,6 +13,7 @@ import {
|
||||
logout,
|
||||
setSession,
|
||||
} from "#/server/lichess.ts";
|
||||
import { getHeaders } from "#/utils/pgnUtils.ts";
|
||||
|
||||
interface ITokenData {
|
||||
access_token: string;
|
||||
@@ -18,8 +21,26 @@ interface ITokenData {
|
||||
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 = () => {
|
||||
const { user, setUser } = useLichessUser();
|
||||
const [userStudies, setUserStudies] = useState<IUserStudy[]>([]);
|
||||
const [selectedStudyId, setSelectedStudyId] = useState<string | null>(null);
|
||||
const [studyChapters, setStudyChapters] = useState<
|
||||
IUserStudyChapter[] | null
|
||||
>(null);
|
||||
|
||||
const loginFn = useServerFn(login);
|
||||
const logoutFn = useServerFn(logout);
|
||||
@@ -28,6 +49,7 @@ export const useLichess = () => {
|
||||
const setSessionFn = useServerFn(setSession);
|
||||
const getSessionFn = useServerFn(getSession);
|
||||
const getUserStudiesFn = useServerFn(getUserStudies);
|
||||
const getStudyChaptersFn = useServerFn(getStudyChapters);
|
||||
|
||||
const lichessAccessToken = async ({ code }: { code: string }) => {
|
||||
return accessTokenFn({ data: { code } });
|
||||
@@ -85,7 +107,39 @@ export const useLichess = () => {
|
||||
|
||||
const getLichessUserStudies = async () => {
|
||||
if (!user) throw new Error("User not found");
|
||||
await getUserStudiesFn();
|
||||
const studies: IUserStudy[] | undefined = await getUserStudiesFn();
|
||||
if (!studies) return;
|
||||
setUserStudies(studies);
|
||||
};
|
||||
|
||||
const getLichessStudyChapters = async ({ studyId }: { studyId: string }) => {
|
||||
if (!user) throw new Error("User not found");
|
||||
const chapters: IUserStudyChapter[] | undefined = await getStudyChaptersFn({
|
||||
data: { studyId },
|
||||
});
|
||||
if (!chapters) return;
|
||||
return chapters;
|
||||
};
|
||||
|
||||
// return headers and pgn from the Lichess chapter
|
||||
const loadLichessStudyChapter = async ({
|
||||
chapter,
|
||||
}: {
|
||||
chapter: IUserStudyChapter;
|
||||
}) => {
|
||||
const pgn = chapter.pgn.trim();
|
||||
|
||||
if (!chapter.pgn) throw new Error("No valid PGN found for chapter");
|
||||
|
||||
const headers = {
|
||||
...getHeaders(pgn),
|
||||
title: "",
|
||||
subtitle: "",
|
||||
author: "",
|
||||
};
|
||||
|
||||
console.log(headers, pgn);
|
||||
return { headers, pgn };
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -97,5 +151,13 @@ export const useLichess = () => {
|
||||
getLichessSession,
|
||||
useLichessCallback,
|
||||
getLichessUserStudies,
|
||||
userStudies,
|
||||
setUserStudies,
|
||||
setSelectedStudyId,
|
||||
selectedStudyId,
|
||||
getLichessStudyChapters,
|
||||
studyChapters,
|
||||
setStudyChapters,
|
||||
loadLichessStudyChapter,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,7 +5,9 @@ import LichessButton from "#/components/LichessButton.tsx";
|
||||
import LichessStudyUrlInput from "#/components/LichessStudyUrlInput.tsx";
|
||||
import Section from "#/components/Section.tsx";
|
||||
import SelectLichessStudy from "#/components/SelectLichessStudy.tsx";
|
||||
import SelectStudyChapter from "#/components/SelectStudyChapter.tsx";
|
||||
import { useChessGame } from "#/hooks/useChessGame.ts";
|
||||
import { useLichess } from "#/hooks/useLichess.ts";
|
||||
|
||||
const PgnViewer = lazy(() => import("#/components/PgnViewer.tsx"));
|
||||
|
||||
@@ -33,8 +35,12 @@ const Chessboard = () => {
|
||||
handleToggleDiagram,
|
||||
handlePlyChange,
|
||||
updateHeaders,
|
||||
gameDispatch,
|
||||
} = useChessGame();
|
||||
|
||||
const { selectedStudyId, setSelectedStudyId, setStudyChapters } =
|
||||
useLichess();
|
||||
|
||||
return (
|
||||
<main className="px-4 pb-8 place-self-center min-h-[calc(100vh-226px)]">
|
||||
<Section title="Convert PGN to PDF">
|
||||
@@ -46,7 +52,15 @@ const Chessboard = () => {
|
||||
className="file-input max-w-xs"
|
||||
onChange={handleLoadPgn}
|
||||
/>
|
||||
<SelectLichessStudy /> or <LichessStudyUrlInput />
|
||||
<SelectLichessStudy
|
||||
setSelectedStudyId={setSelectedStudyId}
|
||||
setStudyChapters={setStudyChapters}
|
||||
/>{" "}
|
||||
or <LichessStudyUrlInput />
|
||||
<SelectStudyChapter
|
||||
selectedStudyId={selectedStudyId || ""}
|
||||
gameDispatch={gameDispatch}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<div className="grid lg:grid-cols-2 gap-4">
|
||||
|
||||
+30
-2
@@ -96,8 +96,6 @@ export const getToken = createServerFn({ method: "POST" })
|
||||
export const getUser = createServerFn({ method: "GET" })
|
||||
.inputValidator((data: { access_token: string }) => data)
|
||||
.handler(async ({ data }) => {
|
||||
console.log(data.access_token);
|
||||
|
||||
const response = await fetch("https://lichess.org/api/account", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${data.access_token}`,
|
||||
@@ -178,3 +176,33 @@ export const getUserStudies = createServerFn({ method: "POST" }).handler(
|
||||
.map((study) => JSON.parse(study));
|
||||
},
|
||||
);
|
||||
|
||||
export const getStudyChapters = createServerFn({ method: "POST" })
|
||||
.inputValidator((data: { studyId: string }) => data)
|
||||
.handler(async ({ data }) => {
|
||||
const session = await getSession();
|
||||
|
||||
if (!session) return;
|
||||
|
||||
const response = await fetch(
|
||||
`https://lichess.org/api/study/${data.studyId}.pgn`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.token}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const text = await response.text();
|
||||
|
||||
return text
|
||||
.trim()
|
||||
.split(/\n{3,}/)
|
||||
.map((game) => {
|
||||
const eventHeaderText = game.match(/\[Event\s+"([^"]+)"]/)?.[1];
|
||||
const eventName = eventHeaderText?.match(/(?<=:\s+)[^"]+/)?.[0] || "";
|
||||
const siteHeaderText = game.match(/\[Site\s+"([^"]+)"]/)?.[1];
|
||||
const chapterId = siteHeaderText?.match(/\/([^/]+)$/)?.[1] || "";
|
||||
return { chapterId, pgn: game, name: eventName };
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user