diff --git a/src/components/LichessStudyUrlInput.tsx b/src/components/LichessStudyUrlInput.tsx
index 9eb9fd1..5ef1f5f 100644
--- a/src/components/LichessStudyUrlInput.tsx
+++ b/src/components/LichessStudyUrlInput.tsx
@@ -1,5 +1,110 @@
+import { useState } from "react";
+import { useLichess } from "#/hooks/useLichess.ts";
+
+const parseStudyId = (url: string): string | null => {
+ const match = url.match(
+ /(?:https?:\/\/)?(?:www\.)?lichess\.org\/study\/([a-zA-Z0-9]+)/,
+ );
+ return match?.[1] ?? null;
+};
+
const LichessStudyUrlInput = () => {
- return ;
+ const [url, setUrl] = useState("");
+ const [feedback, setFeedback] = useState<{
+ type: "success" | "error";
+ message: string;
+ } | null>(null);
+
+ const { setSelectedStudyId, selectedStudyId } = useLichess();
+
+ const handleSubmit = () => {
+ if (!url.trim()) {
+ setFeedback({ type: "error", message: "Please enter a study URL" });
+ return;
+ }
+
+ const studyId = parseStudyId(url.trim());
+
+ if (!studyId) {
+ setFeedback({
+ type: "error",
+ message: "Invalid Lichess study URL",
+ });
+ return;
+ }
+
+ if (studyId === selectedStudyId) {
+ setFeedback({
+ type: "success",
+ message: "Study already selected",
+ });
+ return;
+ }
+
+ setSelectedStudyId(studyId);
+ setFeedback({ type: "success", message: "Study loaded — pick a chapter" });
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") handleSubmit();
+ };
+
+ return (
+
+
+
+
+
+
{
+ setUrl(e.target.value);
+ if (feedback) setFeedback(null);
+ }}
+ onKeyDown={handleKeyDown}
+ className={`w-full h-12 pl-10 pr-4 rounded-lg border text-(--base-content) placeholder-(--neutral-content)/50 outline-hidden transition-all duration-200 focus:border-(--accent) focus:outline-3 focus:outline-offset-2 focus:outline-(--accent)/50 ${
+ feedback?.type === "error"
+ ? "border-red-500"
+ : "border-(--neutral-content)"
+ }`}
+ />
+
+
+
+
+ {feedback ? (
+
+ {feedback.message}
+
+ ) : null}
+
+ );
};
export default LichessStudyUrlInput;
diff --git a/src/components/SelectLichessStudy.tsx b/src/components/SelectLichessStudy.tsx
index 86ded09..2e9de88 100644
--- a/src/components/SelectLichessStudy.tsx
+++ b/src/components/SelectLichessStudy.tsx
@@ -8,9 +8,14 @@ interface IProps {
const SelectLichessStudy = ({ setSelectedStudyId }: IProps) => {
const [isOpen, setIsOpen] = useState(false);
+ const [search, setSearch] = useState("");
const { getLichessUserStudies, userStudies } = useLichess();
const dropdownRef = useRef(null);
+ const filteredStudies = userStudies.filter((study) =>
+ study.name.toLowerCase().includes(search.toLowerCase()),
+ );
+
const onClickHandler = async () => {
setIsOpen(!isOpen);
@@ -22,6 +27,7 @@ const SelectLichessStudy = ({ setSelectedStudyId }: IProps) => {
const handleStudyClick = async (studyId: string) => {
setSelectedStudyId(studyId);
setIsOpen(false);
+ setSearch("");
};
useEffect(() => {
@@ -32,6 +38,7 @@ const SelectLichessStudy = ({ setSelectedStudyId }: IProps) => {
!dropdownRef.current.contains(event.target as Node)
) {
setIsOpen(false);
+ setSearch("");
}
};
@@ -53,21 +60,54 @@ const SelectLichessStudy = ({ setSelectedStudyId }: IProps) => {
{isOpen ? (
-
-
-
- {userStudies.map((study) => (
- -
-
-
- ))}
-
+
+
+
+
+
+
setSearch(e.target.value)}
+ className="w-full h-12 pl-10 pr-4 rounded-lg border border-(--neutral-content) text-(--base-content) placeholder-(--neutral-content)/50 outline-hidden transition-all duration-200 focus:border-(--accent) focus:outline-3 focus:outline-offset-2 focus:outline-(--accent)/50"
+ />
+
+
+
+
+ {filteredStudies.length > 0 ? (
+
+ {filteredStudies.map((study) => (
+ -
+
+
+ ))}
+
+ ) : (
+
+ No results found
+
+ )}
) : null}
diff --git a/src/components/SelectStudyChapter.tsx b/src/components/SelectStudyChapter.tsx
index a4cc0a8..22c3eeb 100644
--- a/src/components/SelectStudyChapter.tsx
+++ b/src/components/SelectStudyChapter.tsx
@@ -1,4 +1,4 @@
-import { useRef, useState, type Dispatch } from "react";
+import { type Dispatch, useEffect, useRef, useState } from "react";
import { type IUserStudyChapter, useLichess } from "#/hooks/useLichess.ts";
import type { GameAction } from "#/reducers/gameReducer.ts";
@@ -10,10 +10,34 @@ interface IProps {
const SelectStudyChapter = ({ selectedStudyId, gameDispatch }: IProps) => {
const [isOpen, setIsOpen] = useState(false);
const [studyChapters, setStudyChapters] = useState([]);
+ const [search, setSearch] = useState("");
const dropdownRef = useRef(null);
const { getLichessStudyChapters, loadLichessStudyChapter } = useLichess();
+ const filteredChapters = studyChapters.filter((chapter) =>
+ chapter.name.toLowerCase().includes(search.toLowerCase()),
+ );
+
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (
+ isOpen &&
+ dropdownRef.current &&
+ !dropdownRef.current.contains(event.target as Node)
+ ) {
+ setIsOpen(false);
+ setSearch("");
+ }
+ };
+
+ document.addEventListener("mousedown", handleClickOutside);
+
+ return () => {
+ document.removeEventListener("mousedown", handleClickOutside);
+ };
+ }, [isOpen]);
+
const onClickHandler = async () => {
const studies = await getLichessStudyChapters({ studyId: selectedStudyId });
if (!studies) return;
@@ -27,6 +51,7 @@ const SelectStudyChapter = ({ selectedStudyId, gameDispatch }: IProps) => {
gameDispatch({ type: "SET_GAME", payload: { pgn, headers } });
setIsOpen(false);
+ setSearch("");
};
return (
@@ -40,21 +65,55 @@ const SelectStudyChapter = ({ selectedStudyId, gameDispatch }: IProps) => {
{isOpen ? (
-
-
-
- {studyChapters?.map((chapter) => (
- -
-
-
- ))}
-
+
+
+
+
+
+
setSearch(e.target.value)}
+ className="w-full h-12 pl-10 pr-4 rounded-lg border border-(--neutral-content) text-(--base-content) placeholder-(--neutral-content)/50 outline-hidden transition-all duration-200 focus:border-(--accent) focus:outline-3 focus:outline-offset-2 focus:outline-(--accent)/50"
+ />
+
+
+
+
+ {filteredChapters.length > 0 ? (
+
+ {filteredChapters.map((chapter) => (
+ -
+
+
+ ))}
+
+ ) : (
+
+ No results found
+
+ )}
) : null}