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 ? ( -
- - +
+
+ + + + + + + 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) => ( -
  • - -
  • - ))} -
+
+
+ + + link + + + + + 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}