mirror of
https://github.com/TheRealOwenRees/chess-scribe-website.git
synced 2026-07-23 01:46:57 +00:00
opencode suggestions for inputs
This commit is contained in:
@@ -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 = () => {
|
const LichessStudyUrlInput = () => {
|
||||||
return <input placeholder="Lichess Study URL" className="" />;
|
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 (
|
||||||
|
<div className="flex items-center gap-2 min-w-[500px]">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-(--neutral-content) pointer-events-none">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
||||||
|
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
placeholder="Lichess Study URL"
|
||||||
|
value={url}
|
||||||
|
onChange={(e) => {
|
||||||
|
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)"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary px-6 whitespace-nowrap"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
>
|
||||||
|
Load Study
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{feedback ? (
|
||||||
|
<span
|
||||||
|
className={`text-sm whitespace-nowrap ${
|
||||||
|
feedback.type === "error" ? "text-red-500" : "text-(--accent)"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{feedback.message}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LichessStudyUrlInput;
|
export default LichessStudyUrlInput;
|
||||||
|
|||||||
@@ -8,9 +8,14 @@ interface IProps {
|
|||||||
|
|
||||||
const SelectLichessStudy = ({ setSelectedStudyId }: IProps) => {
|
const SelectLichessStudy = ({ setSelectedStudyId }: IProps) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
const { getLichessUserStudies, userStudies } = useLichess();
|
const { getLichessUserStudies, userStudies } = useLichess();
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const filteredStudies = userStudies.filter((study) =>
|
||||||
|
study.name.toLowerCase().includes(search.toLowerCase()),
|
||||||
|
);
|
||||||
|
|
||||||
const onClickHandler = async () => {
|
const onClickHandler = async () => {
|
||||||
setIsOpen(!isOpen);
|
setIsOpen(!isOpen);
|
||||||
|
|
||||||
@@ -22,6 +27,7 @@ const SelectLichessStudy = ({ setSelectedStudyId }: IProps) => {
|
|||||||
const handleStudyClick = async (studyId: string) => {
|
const handleStudyClick = async (studyId: string) => {
|
||||||
setSelectedStudyId(studyId);
|
setSelectedStudyId(studyId);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
|
setSearch("");
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -32,6 +38,7 @@ const SelectLichessStudy = ({ setSelectedStudyId }: IProps) => {
|
|||||||
!dropdownRef.current.contains(event.target as Node)
|
!dropdownRef.current.contains(event.target as Node)
|
||||||
) {
|
) {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
|
setSearch("");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -53,14 +60,42 @@ const SelectLichessStudy = ({ setSelectedStudyId }: IProps) => {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isOpen ? (
|
{isOpen ? (
|
||||||
<div className="absolute z-3 bg-(--bg-base) mt-2 max-h-75 border rounded-lg p-2 overflow-y-scroll">
|
<div className="absolute z-3 bg-(--bg-base) mt-2 max-h-80 border border-(--neutral-content)/30 rounded-lg p-3 overflow-y-scroll transition-all duration-300 ease-in-out opacity-100 scale-100 shadow-lg">
|
||||||
<input type="text" placeholder="Search..." />
|
<div className="relative">
|
||||||
|
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-(--neutral-content) pointer-events-none">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="11" cy="11" r="8" />
|
||||||
|
<path d="m21 21-4.3-4.3" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search..."
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className="border-(--neutral-content)/15 my-2" />
|
||||||
|
|
||||||
|
{filteredStudies.length > 0 ? (
|
||||||
<ul>
|
<ul>
|
||||||
{userStudies.map((study) => (
|
{filteredStudies.map((study) => (
|
||||||
<li key={study.id}>
|
<li key={study.id}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="p-2 cursor-pointer hover:bg-(--neutral-content)/20 text-left w-full"
|
className="p-2 cursor-pointer hover:bg-(--neutral-content)/15 focus-visible:outline-2 focus-visible:outline-(--accent) focus-visible:outline-offset-2 rounded text-left w-full transition-colors duration-200"
|
||||||
onClick={() => handleStudyClick(study.id)}
|
onClick={() => handleStudyClick(study.id)}
|
||||||
>
|
>
|
||||||
{study.name}
|
{study.name}
|
||||||
@@ -68,6 +103,11 @@ const SelectLichessStudy = ({ setSelectedStudyId }: IProps) => {
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p className="text-(--neutral-content) text-sm text-center py-4">
|
||||||
|
No results found
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 IUserStudyChapter, useLichess } from "#/hooks/useLichess.ts";
|
||||||
import type { GameAction } from "#/reducers/gameReducer.ts";
|
import type { GameAction } from "#/reducers/gameReducer.ts";
|
||||||
|
|
||||||
@@ -10,10 +10,34 @@ interface IProps {
|
|||||||
const SelectStudyChapter = ({ selectedStudyId, gameDispatch }: IProps) => {
|
const SelectStudyChapter = ({ selectedStudyId, gameDispatch }: IProps) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [studyChapters, setStudyChapters] = useState<IUserStudyChapter[]>([]);
|
const [studyChapters, setStudyChapters] = useState<IUserStudyChapter[]>([]);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const { getLichessStudyChapters, loadLichessStudyChapter } = useLichess();
|
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 onClickHandler = async () => {
|
||||||
const studies = await getLichessStudyChapters({ studyId: selectedStudyId });
|
const studies = await getLichessStudyChapters({ studyId: selectedStudyId });
|
||||||
if (!studies) return;
|
if (!studies) return;
|
||||||
@@ -27,6 +51,7 @@ const SelectStudyChapter = ({ selectedStudyId, gameDispatch }: IProps) => {
|
|||||||
gameDispatch({ type: "SET_GAME", payload: { pgn, headers } });
|
gameDispatch({ type: "SET_GAME", payload: { pgn, headers } });
|
||||||
|
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
|
setSearch("");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -40,14 +65,43 @@ const SelectStudyChapter = ({ selectedStudyId, gameDispatch }: IProps) => {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isOpen ? (
|
{isOpen ? (
|
||||||
<div className="absolute z-3 bg-(--bg-base) mt-2 max-h-75 border rounded-lg p-2 overflow-y-scroll">
|
<div className="absolute z-3 bg-(--bg-base) mt-2 max-h-80 border border-(--neutral-content)/30 rounded-lg p-3 overflow-y-scroll transition-all duration-300 ease-in-out opacity-100 scale-100 shadow-lg">
|
||||||
<input type="text" placeholder="Search..." />
|
<div className="relative">
|
||||||
|
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-(--neutral-content) pointer-events-none">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<title>link</title>
|
||||||
|
<circle cx="11" cy="11" r="8" />
|
||||||
|
<path d="m21 21-4.3-4.3" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search..."
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className="border-(--neutral-content)/15 my-2" />
|
||||||
|
|
||||||
|
{filteredChapters.length > 0 ? (
|
||||||
<ul>
|
<ul>
|
||||||
{studyChapters?.map((chapter) => (
|
{filteredChapters.map((chapter) => (
|
||||||
<li key={`${chapter.chapterId}${chapter.name}`}>
|
<li key={`${chapter.chapterId}${chapter.name}`}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="p-2 cursor-pointer hover:bg-(--neutral-content)/20 text-left w-full"
|
className="p-2 cursor-pointer hover:bg-(--neutral-content)/15 focus-visible:outline-2 focus-visible:outline-(--accent) focus-visible:outline-offset-2 rounded text-left w-full transition-colors duration-200"
|
||||||
onClick={() => handleChapterClick(chapter)}
|
onClick={() => handleChapterClick(chapter)}
|
||||||
>
|
>
|
||||||
{chapter.name}
|
{chapter.name}
|
||||||
@@ -55,6 +109,11 @@ const SelectStudyChapter = ({ selectedStudyId, gameDispatch }: IProps) => {
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p className="text-(--neutral-content) text-sm text-center py-4">
|
||||||
|
No results found
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user