feat: predictive username search (#44)
This commit is contained in:
@@ -8,19 +8,85 @@ import {
|
|||||||
ListItemButton,
|
ListItemButton,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
TextField,
|
TextField,
|
||||||
|
Autocomplete,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
import { Icon } from "@iconify/react";
|
||||||
import { useDebounce } from "@/hooks/useDebounce";
|
import { useDebounce } from "@/hooks/useDebounce";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onSelect: (pgn: string, boardOrientation?: boolean) => void;
|
onSelect: (pgn: string, boardOrientation?: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChessComInput({ onSelect }: Props) {
|
export default function ChessComInput({ onSelect }: Props) {
|
||||||
const [chessComUsername, setChessComUsername] = useLocalStorage(
|
const [rawStoredValue, setStoredValues] = useLocalStorage<string>(
|
||||||
"chesscom-username",
|
"chesscom-username",
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const storedValues = useMemo(() => {
|
||||||
|
if (typeof rawStoredValue === "string")
|
||||||
|
return rawStoredValue
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
else return [];
|
||||||
|
}, [rawStoredValue]);
|
||||||
|
|
||||||
|
const [chessComUsername, setChessComUsername] = useState("");
|
||||||
|
const [hasEdited, setHasEdited] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!hasEdited &&
|
||||||
|
storedValues &&
|
||||||
|
storedValues.length > 0 &&
|
||||||
|
chessComUsername.trim().toLowerCase() !=
|
||||||
|
storedValues[0].trim().toLowerCase()
|
||||||
|
) {
|
||||||
|
setChessComUsername(storedValues[0].trim().toLowerCase());
|
||||||
|
}
|
||||||
|
}, [storedValues, hasEdited, chessComUsername]);
|
||||||
|
|
||||||
|
const updateHistory = (username: string) => {
|
||||||
|
const trimmed = username.trim();
|
||||||
|
if (!trimmed) return;
|
||||||
|
const lower = trimmed.toLowerCase();
|
||||||
|
|
||||||
|
const exists = storedValues.some((u) => u.toLowerCase() === lower);
|
||||||
|
if (!exists) {
|
||||||
|
const updated = [trimmed, ...storedValues.filter((u) => u !== trimmed)];
|
||||||
|
setStoredValues(updated.join(","));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteUsername = (usernameToDelete: string) => {
|
||||||
|
const updated = storedValues.filter((u) => u !== usernameToDelete);
|
||||||
|
setStoredValues(updated.join(","));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (_: React.SyntheticEvent, newValue: string | null) => {
|
||||||
|
const newInputValue = newValue ?? "";
|
||||||
|
if (
|
||||||
|
newInputValue.trim().toLowerCase() !=
|
||||||
|
chessComUsername.trim().toLowerCase()
|
||||||
|
)
|
||||||
|
setChessComUsername(newInputValue.trim().toLowerCase());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (
|
||||||
|
_: React.SyntheticEvent,
|
||||||
|
newInputValue: string
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
newInputValue.trim().toLowerCase() !=
|
||||||
|
chessComUsername.trim().toLowerCase()
|
||||||
|
) {
|
||||||
|
setChessComUsername(newInputValue.trim().toLowerCase());
|
||||||
|
if (!hasEdited) setHasEdited(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
const debouncedUsername = useDebounce(chessComUsername, 300);
|
const debouncedUsername = useDebounce(chessComUsername, 300);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -38,15 +104,48 @@ export default function ChessComInput({ onSelect }: Props) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormControl sx={{ m: 1, width: 300 }}>
|
<FormControl sx={{ m: 1, width: 300 }}>
|
||||||
|
<Autocomplete
|
||||||
|
freeSolo
|
||||||
|
options={storedValues}
|
||||||
|
inputValue={chessComUsername}
|
||||||
|
onInputChange={handleInputChange}
|
||||||
|
onChange={handleChange}
|
||||||
|
renderOption={(props, option) => {
|
||||||
|
const { key, ...rest } = props;
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={key}
|
||||||
|
{...rest}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingRight: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{option}</span>
|
||||||
|
<Icon
|
||||||
|
icon="mdi:close"
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
deleteUsername(option);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
renderInput={(params) => (
|
||||||
<TextField
|
<TextField
|
||||||
|
{...params}
|
||||||
label="Enter your Chess.com username..."
|
label="Enter your Chess.com username..."
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={chessComUsername ?? ""}
|
/>
|
||||||
onChange={(e) => setChessComUsername(e.target.value)}
|
)}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
{chessComUsername && (
|
{debouncedUsername && (
|
||||||
<Grid
|
<Grid
|
||||||
container
|
container
|
||||||
gap={2}
|
gap={2}
|
||||||
@@ -69,8 +168,9 @@ export default function ChessComInput({ onSelect }: Props) {
|
|||||||
games.map((game) => (
|
games.map((game) => (
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
updateHistory(debouncedUsername);
|
||||||
const boardOrientation =
|
const boardOrientation =
|
||||||
chessComUsername.toLowerCase() !==
|
debouncedUsername.toLowerCase() !==
|
||||||
game.black?.username?.toLowerCase();
|
game.black?.username?.toLowerCase();
|
||||||
onSelect(game.pgn, boardOrientation);
|
onSelect(game.pgn, boardOrientation);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -8,19 +8,84 @@ import {
|
|||||||
ListItemButton,
|
ListItemButton,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
TextField,
|
TextField,
|
||||||
|
Autocomplete,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
import { Icon } from "@iconify/react";
|
||||||
import { useDebounce } from "@/hooks/useDebounce";
|
import { useDebounce } from "@/hooks/useDebounce";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onSelect: (pgn: string, boardOrientation?: boolean) => void;
|
onSelect: (pgn: string, boardOrientation?: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LichessInput({ onSelect }: Props) {
|
export default function LichessInput({ onSelect }: Props) {
|
||||||
const [lichessUsername, setLichessUsername] = useLocalStorage(
|
const [rawStoredValue, setStoredValues] = useLocalStorage<string>(
|
||||||
"lichess-username",
|
"lichess-username",
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const storedValues = useMemo(() => {
|
||||||
|
if (typeof rawStoredValue === "string")
|
||||||
|
return rawStoredValue
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
else return [];
|
||||||
|
}, [rawStoredValue]);
|
||||||
|
|
||||||
|
const [lichessUsername, setLichessUsername] = useState("");
|
||||||
|
const [hasEdited, setHasEdited] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!hasEdited &&
|
||||||
|
storedValues &&
|
||||||
|
storedValues.length > 0 &&
|
||||||
|
lichessUsername.trim().toLowerCase() !=
|
||||||
|
storedValues[0].trim().toLowerCase()
|
||||||
|
) {
|
||||||
|
setLichessUsername(storedValues[0].trim().toLowerCase());
|
||||||
|
}
|
||||||
|
}, [storedValues, hasEdited, lichessUsername]);
|
||||||
|
|
||||||
|
const updateHistory = (username: string) => {
|
||||||
|
const trimmed = username.trim();
|
||||||
|
if (!trimmed) return;
|
||||||
|
const lower = trimmed.toLowerCase();
|
||||||
|
|
||||||
|
const exists = storedValues.some((u) => u.toLowerCase() === lower);
|
||||||
|
if (!exists) {
|
||||||
|
const updated = [trimmed, ...storedValues.filter((u) => u !== trimmed)];
|
||||||
|
setStoredValues(updated.join(","));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteUsername = (usernameToDelete: string) => {
|
||||||
|
const updated = storedValues.filter((u) => u !== usernameToDelete);
|
||||||
|
setStoredValues(updated.join(","));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (_: React.SyntheticEvent, newValue: string | null) => {
|
||||||
|
const newInputValue = newValue ?? "";
|
||||||
|
if (
|
||||||
|
newInputValue.trim().toLowerCase() != lichessUsername.trim().toLowerCase()
|
||||||
|
)
|
||||||
|
setLichessUsername(newInputValue.trim().toLowerCase());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (
|
||||||
|
_: React.SyntheticEvent,
|
||||||
|
newInputValue: string
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
newInputValue.trim().toLowerCase() != lichessUsername.trim().toLowerCase()
|
||||||
|
) {
|
||||||
|
setLichessUsername(newInputValue.trim().toLowerCase());
|
||||||
|
if (!hasEdited) setHasEdited(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const debouncedUsername = useDebounce(lichessUsername, 500);
|
const debouncedUsername = useDebounce(lichessUsername, 500);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -38,15 +103,48 @@ export default function LichessInput({ onSelect }: Props) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormControl sx={{ m: 1, width: 300 }}>
|
<FormControl sx={{ m: 1, width: 300 }}>
|
||||||
|
<Autocomplete
|
||||||
|
freeSolo
|
||||||
|
options={storedValues}
|
||||||
|
inputValue={lichessUsername}
|
||||||
|
onInputChange={handleInputChange}
|
||||||
|
onChange={handleChange}
|
||||||
|
renderOption={(props, option) => {
|
||||||
|
const { key, ...rest } = props;
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={key}
|
||||||
|
{...rest}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingRight: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{option}</span>
|
||||||
|
<Icon
|
||||||
|
icon="mdi:close"
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
deleteUsername(option);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
renderInput={(params) => (
|
||||||
<TextField
|
<TextField
|
||||||
|
{...params}
|
||||||
label="Enter your Lichess username..."
|
label="Enter your Lichess username..."
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={lichessUsername ?? ""}
|
/>
|
||||||
onChange={(e) => setLichessUsername(e.target.value)}
|
)}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
{lichessUsername && (
|
{debouncedUsername && (
|
||||||
<Grid
|
<Grid
|
||||||
container
|
container
|
||||||
gap={2}
|
gap={2}
|
||||||
@@ -69,8 +167,9 @@ export default function LichessInput({ onSelect }: Props) {
|
|||||||
games.map((game) => (
|
games.map((game) => (
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
updateHistory(debouncedUsername);
|
||||||
const boardOrientation =
|
const boardOrientation =
|
||||||
lichessUsername.toLowerCase() !==
|
debouncedUsername.toLowerCase() !==
|
||||||
game.players?.black?.user?.name?.toLowerCase();
|
game.players?.black?.user?.name?.toLowerCase();
|
||||||
onSelect(game.pgn, boardOrientation);
|
onSelect(game.pgn, boardOrientation);
|
||||||
}}
|
}}
|
||||||
|
|||||||
Reference in New Issue
Block a user