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,13 +1,49 @@
|
|||||||
import { useLichess } from "#/hooks/useLichess.ts";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { type IUserStudyChapter, useLichess } from "#/hooks/useLichess.ts";
|
||||||
|
|
||||||
const SelectLichessStudy = () => {
|
interface IProps {
|
||||||
const { getLichessUserStudies } = useLichess();
|
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 () => {
|
const onClickHandler = async () => {
|
||||||
|
setIsOpen(!isOpen);
|
||||||
|
|
||||||
|
if (!userStudies.length) {
|
||||||
await getLichessUserStudies();
|
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 (
|
return (
|
||||||
|
<div ref={dropdownRef} className="relative">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-secondary"
|
className="btn btn-secondary"
|
||||||
@@ -15,6 +51,26 @@ const SelectLichessStudy = () => {
|
|||||||
>
|
>
|
||||||
Select Lichess Study
|
Select Lichess Study
|
||||||
</button>
|
</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,
|
currentPosition,
|
||||||
generatingPdf,
|
generatingPdf,
|
||||||
updateHeaders,
|
updateHeaders,
|
||||||
|
gameDispatch,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
+64
-2
@@ -1,9 +1,11 @@
|
|||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { useServerFn } from "@tanstack/react-start";
|
import { useServerFn } from "@tanstack/react-start";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useLichessUser } from "#/context/LichessUserContext.tsx";
|
import { useLichessUser } from "#/context/LichessUserContext.tsx";
|
||||||
|
import { useChessGame } from "#/hooks/useChessGame.ts";
|
||||||
import {
|
import {
|
||||||
getSession,
|
getSession,
|
||||||
|
getStudyChapters,
|
||||||
getToken,
|
getToken,
|
||||||
getUser,
|
getUser,
|
||||||
getUserStudies,
|
getUserStudies,
|
||||||
@@ -11,6 +13,7 @@ import {
|
|||||||
logout,
|
logout,
|
||||||
setSession,
|
setSession,
|
||||||
} from "#/server/lichess.ts";
|
} from "#/server/lichess.ts";
|
||||||
|
import { getHeaders } from "#/utils/pgnUtils.ts";
|
||||||
|
|
||||||
interface ITokenData {
|
interface ITokenData {
|
||||||
access_token: string;
|
access_token: string;
|
||||||
@@ -18,8 +21,26 @@ interface ITokenData {
|
|||||||
token_type: string;
|
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 [selectedStudyId, setSelectedStudyId] = useState<string | null>(null);
|
||||||
|
const [studyChapters, setStudyChapters] = useState<
|
||||||
|
IUserStudyChapter[] | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
const loginFn = useServerFn(login);
|
const loginFn = useServerFn(login);
|
||||||
const logoutFn = useServerFn(logout);
|
const logoutFn = useServerFn(logout);
|
||||||
@@ -28,6 +49,7 @@ export const useLichess = () => {
|
|||||||
const setSessionFn = useServerFn(setSession);
|
const setSessionFn = useServerFn(setSession);
|
||||||
const getSessionFn = useServerFn(getSession);
|
const getSessionFn = useServerFn(getSession);
|
||||||
const getUserStudiesFn = useServerFn(getUserStudies);
|
const getUserStudiesFn = useServerFn(getUserStudies);
|
||||||
|
const getStudyChaptersFn = useServerFn(getStudyChapters);
|
||||||
|
|
||||||
const lichessAccessToken = async ({ code }: { code: string }) => {
|
const lichessAccessToken = async ({ code }: { code: string }) => {
|
||||||
return accessTokenFn({ data: { code } });
|
return accessTokenFn({ data: { code } });
|
||||||
@@ -85,7 +107,39 @@ export const useLichess = () => {
|
|||||||
|
|
||||||
const getLichessUserStudies = async () => {
|
const getLichessUserStudies = async () => {
|
||||||
if (!user) throw new Error("User not found");
|
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 {
|
return {
|
||||||
@@ -97,5 +151,13 @@ export const useLichess = () => {
|
|||||||
getLichessSession,
|
getLichessSession,
|
||||||
useLichessCallback,
|
useLichessCallback,
|
||||||
getLichessUserStudies,
|
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 LichessStudyUrlInput from "#/components/LichessStudyUrlInput.tsx";
|
||||||
import Section from "#/components/Section.tsx";
|
import Section from "#/components/Section.tsx";
|
||||||
import SelectLichessStudy from "#/components/SelectLichessStudy.tsx";
|
import SelectLichessStudy from "#/components/SelectLichessStudy.tsx";
|
||||||
|
import SelectStudyChapter from "#/components/SelectStudyChapter.tsx";
|
||||||
import { useChessGame } from "#/hooks/useChessGame.ts";
|
import { useChessGame } from "#/hooks/useChessGame.ts";
|
||||||
|
import { useLichess } from "#/hooks/useLichess.ts";
|
||||||
|
|
||||||
const PgnViewer = lazy(() => import("#/components/PgnViewer.tsx"));
|
const PgnViewer = lazy(() => import("#/components/PgnViewer.tsx"));
|
||||||
|
|
||||||
@@ -33,8 +35,12 @@ const Chessboard = () => {
|
|||||||
handleToggleDiagram,
|
handleToggleDiagram,
|
||||||
handlePlyChange,
|
handlePlyChange,
|
||||||
updateHeaders,
|
updateHeaders,
|
||||||
|
gameDispatch,
|
||||||
} = useChessGame();
|
} = useChessGame();
|
||||||
|
|
||||||
|
const { selectedStudyId, setSelectedStudyId, setStudyChapters } =
|
||||||
|
useLichess();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="px-4 pb-8 place-self-center min-h-[calc(100vh-226px)]">
|
<main className="px-4 pb-8 place-self-center min-h-[calc(100vh-226px)]">
|
||||||
<Section title="Convert PGN to PDF">
|
<Section title="Convert PGN to PDF">
|
||||||
@@ -46,7 +52,15 @@ const Chessboard = () => {
|
|||||||
className="file-input max-w-xs"
|
className="file-input max-w-xs"
|
||||||
onChange={handleLoadPgn}
|
onChange={handleLoadPgn}
|
||||||
/>
|
/>
|
||||||
<SelectLichessStudy /> or <LichessStudyUrlInput />
|
<SelectLichessStudy
|
||||||
|
setSelectedStudyId={setSelectedStudyId}
|
||||||
|
setStudyChapters={setStudyChapters}
|
||||||
|
/>{" "}
|
||||||
|
or <LichessStudyUrlInput />
|
||||||
|
<SelectStudyChapter
|
||||||
|
selectedStudyId={selectedStudyId || ""}
|
||||||
|
gameDispatch={gameDispatch}
|
||||||
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<div className="grid lg:grid-cols-2 gap-4">
|
<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" })
|
export const getUser = createServerFn({ method: "GET" })
|
||||||
.inputValidator((data: { access_token: string }) => data)
|
.inputValidator((data: { access_token: string }) => data)
|
||||||
.handler(async ({ data }) => {
|
.handler(async ({ data }) => {
|
||||||
console.log(data.access_token);
|
|
||||||
|
|
||||||
const response = await fetch("https://lichess.org/api/account", {
|
const response = await fetch("https://lichess.org/api/account", {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${data.access_token}`,
|
Authorization: `Bearer ${data.access_token}`,
|
||||||
@@ -178,3 +176,33 @@ export const getUserStudies = createServerFn({ method: "POST" }).handler(
|
|||||||
.map((study) => JSON.parse(study));
|
.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