diff --git a/src/components/LichessButton.tsx b/src/components/LichessButton.tsx index 648fc5f..ffdc55b 100644 --- a/src/components/LichessButton.tsx +++ b/src/components/LichessButton.tsx @@ -1,8 +1,8 @@ import { useLichessUser } from "#/context/LichessUserContext.tsx"; -import { useLichessOAuth } from "#/hooks/useLichessOAuth.ts"; +import { useLichess } from "#/hooks/useLichess.ts"; const LichessButton = () => { - const { lichessLogin, lichessLogout } = useLichessOAuth(); + const { lichessLogin, lichessLogout } = useLichess(); const { user } = useLichessUser(); const onClickHandler = () => { diff --git a/src/components/LichessStudyUrlInput.tsx b/src/components/LichessStudyUrlInput.tsx new file mode 100644 index 0000000..9eb9fd1 --- /dev/null +++ b/src/components/LichessStudyUrlInput.tsx @@ -0,0 +1,5 @@ +const LichessStudyUrlInput = () => { + return ; +}; + +export default LichessStudyUrlInput; diff --git a/src/components/SelectLichessStudy.tsx b/src/components/SelectLichessStudy.tsx new file mode 100644 index 0000000..febe7d6 --- /dev/null +++ b/src/components/SelectLichessStudy.tsx @@ -0,0 +1,21 @@ +import { useLichess } from "#/hooks/useLichess.ts"; + +const SelectLichessStudy = () => { + const { getLichessUserStudies } = useLichess(); + + const onClickHandler = async () => { + await getLichessUserStudies(); + }; + + return ( + + ); +}; + +export default SelectLichessStudy; diff --git a/src/hooks/useLichessOAuth.ts b/src/hooks/useLichess.ts similarity index 75% rename from src/hooks/useLichessOAuth.ts rename to src/hooks/useLichess.ts index 51d51da..33e4f1c 100644 --- a/src/hooks/useLichessOAuth.ts +++ b/src/hooks/useLichess.ts @@ -6,6 +6,7 @@ import { getSession, getToken, getUser, + getUserStudies, login, logout, setSession, @@ -17,8 +18,8 @@ interface ITokenData { token_type: string; } -export const useLichessOAuth = () => { - const { setUser } = useLichessUser(); +export const useLichess = () => { + const { user, setUser } = useLichessUser(); const loginFn = useServerFn(login); const logoutFn = useServerFn(logout); @@ -26,6 +27,7 @@ export const useLichessOAuth = () => { const getUserFn = useServerFn(getUser); const setSessionFn = useServerFn(setSession); const getSessionFn = useServerFn(getSession); + const getUserStudiesFn = useServerFn(getUserStudies); const lichessAccessToken = async ({ code }: { code: string }) => { return accessTokenFn({ data: { code } }); @@ -34,11 +36,13 @@ export const useLichessOAuth = () => { const setLichessUser = async ({ username, id, + token, }: { username: string; id: string; + token: string; }) => { - await setSessionFn({ data: { username, id } }); + await setSessionFn({ data: { username, id, token } }); setUser({ username, id, isLoggedIn: true }); }; @@ -48,9 +52,7 @@ export const useLichessOAuth = () => { }); }; - const lichessLogin = async () => { - await loginFn(); - }; + const lichessLogin = async () => await loginFn(); const lichessLogout = async () => { await logoutFn(); @@ -70,12 +72,22 @@ export const useLichessOAuth = () => { (async () => { const tokenData = await lichessAccessToken({ code }); const userData = await getLichessUser({ tokenData }); - await setLichessUser({ username: userData.username, id: userData.id }); + + await setLichessUser({ + username: userData.username, + id: userData.id, + token: tokenData.access_token, + }); await navigate({ to: "/chessboard" }); })(); }, [navigate, code]); }; + const getLichessUserStudies = async () => { + if (!user) throw new Error("User not found"); + await getUserStudiesFn(); + }; + return { lichessLogin, lichessLogout, @@ -84,5 +96,6 @@ export const useLichessOAuth = () => { setLichessUser, getLichessSession, useLichessCallback, + getLichessUserStudies, }; }; diff --git a/src/pages/Callback.tsx b/src/pages/Callback.tsx index 045dacb..a3f7fd7 100644 --- a/src/pages/Callback.tsx +++ b/src/pages/Callback.tsx @@ -1,14 +1,14 @@ import { getRouteApi } from "@tanstack/react-router"; -import { useLichessOAuth } from "#/hooks/useLichessOAuth.ts"; +import { useLichess } from "#/hooks/useLichess.ts"; const routeApi = getRouteApi("/callback"); const Callback = () => { - const { useLichessCallback } = useLichessOAuth(); + const { useLichessCallback } = useLichess(); const { code } = routeApi.useSearch(); useLichessCallback({ code }).then(); - return
{code}
; + return
Loading...
; }; export default Callback; diff --git a/src/pages/Chessboard.tsx b/src/pages/Chessboard.tsx index e0fc36d..ad929b9 100644 --- a/src/pages/Chessboard.tsx +++ b/src/pages/Chessboard.tsx @@ -2,7 +2,9 @@ import { lazy, Suspense } from "react"; import CustomHeaders from "#/components/CustomHeaders.tsx"; import HeaderFields from "#/components/HeaderFields.tsx"; 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 { useChessGame } from "#/hooks/useChessGame.ts"; const PgnViewer = lazy(() => import("#/components/PgnViewer.tsx")); @@ -44,6 +46,7 @@ const Chessboard = () => { className="file-input max-w-xs" onChange={handleLoadPgn} /> + or
diff --git a/src/server/lichess.ts b/src/server/lichess.ts index 2b2bc46..c7a9f72 100644 --- a/src/server/lichess.ts +++ b/src/server/lichess.ts @@ -22,7 +22,11 @@ export const getSession = createServerFn({ method: "GET" }).handler( const session = getCookie("lichess-session"); if (!session) return null; try { - return JSON.parse(session) as { username: string; id: string }; + return JSON.parse(session) as { + username: string; + id: string; + token: string; + }; } catch { return null; } @@ -30,11 +34,17 @@ export const getSession = createServerFn({ method: "GET" }).handler( ); export const setSession = createServerFn({ method: "POST" }) - .inputValidator((data: { username: string; id: string }) => data) + .inputValidator( + (data: { username: string; id: string; token: string }) => data, + ) .handler(async ({ data }) => { setCookie( "lichess-session", - JSON.stringify({ username: data.username, id: data.id }), + JSON.stringify({ + username: data.username, + id: data.id, + token: data.token, + }), { path: "/", httpOnly: true, @@ -86,6 +96,8 @@ 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}`, @@ -95,9 +107,26 @@ export const getUser = createServerFn({ method: "GET" }) return response.json(); }); +const deleteAccessToken = createServerFn().handler(async () => { + const session = await getSession(); + const token = session?.token; + + if (token) { + const response = await fetch("https://lichess.org/api/token", { + method: "DELETE", + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + return await response.json(); + } +}); + export const logout = createServerFn().handler(async () => { + await deleteAccessToken(); deleteCookie("lichess-session", { path: "/" }); - deleteCookie("lichess-oauth-verifier", { path: "/" }); + deleteCookie("lichess-oauth-verifier", { path: "/" }); // cookie shouldn't exist }); export const login = createServerFn({ method: "GET" }).handler(async () => { @@ -125,3 +154,27 @@ export const login = createServerFn({ method: "GET" }).handler(async () => { href: `https://lichess.org/oauth?${params.toString()}`, }); }); + +export const getUserStudies = createServerFn({ method: "POST" }).handler( + async () => { + const session = await getSession(); + + if (!session) return; + + const response = await fetch( + `https://lichess.org/api/study/by/${session.username}`, + { + headers: { + Authorization: `Bearer ${session.token}`, + }, + }, + ); + + const studies = await response.text(); + + return studies + .trim() + .split("\n") + .map((study) => JSON.parse(study)); + }, +);