basic search bar requests

This commit is contained in:
Dylan Knutson
2023-04-03 21:01:26 +09:00
parent d19255a2c9
commit 71f54ae5e7
4 changed files with 196 additions and 28 deletions

View File

@@ -1,47 +1,208 @@
import React from "react";
import React, { useEffect, useMemo } from "react";
import { useState } from "react";
import PropTypes from "prop-types";
import { debounce, isEmpty } from "lodash";
const COMMON_LIST_ELEM_CLASSES = `
w-full p-2 pl-8
text-xl font-light
border-slate-200 border-2
group-focus-within:border-slate-300
`;
const SVG_ELEM_CLASSES = `
stroke-slate-500 fill-slate-500
`;
const FOCUSABLE_SVG_ELEM_CLASSES = `
${SVG_ELEM_CLASSES}
group-focus-within:stroke-slate-800 group-focus-within:fill-slate-800
`;
const INPUT_ELEM_CLASSES = `
text-slate-500 group-focus-within:text-slate-800
placeholder-slate-500 group-focus-within:placeholder-slate-800
placeholder:font-extralight
`;
function UserSearchBar(props) {
const [userName, setUserName] = useState("yes!");
const [userName, setUserName] = useState("");
const [items, setUserList] = useState([]);
const [selectedIdx, setSelectedIdx] = useState(1);
const [showSpinner, setShowSpinner] = useState(false);
const searchForUser = useMemo(
() =>
debounce(async (userName) => {
if (isEmpty(userName)) {
return;
}
setShowSpinner(true);
const response = await fetch(
`/api/fa/search_users?name=${encodeURIComponent(userName)}`
);
const { users } = await response.json();
setShowSpinner(false);
setUserList(users);
}, 250),
[]
);
useEffect(() => {
searchForUser(userName);
return () => searchForUser.cancel();
}, [userName]);
function onSearchInputKeyDown(event) {
const { code } = event;
switch (code) {
case "ArrowDown":
selectNextListElem();
event.preventDefault();
break;
case "ArrowUp":
selectPrevListElem();
event.preventDefault();
break;
}
}
function selectNextListElem() {
setNewIdxTruncated(selectedIdx == null ? 0 : selectedIdx + 1);
}
function selectPrevListElem() {
setNewIdxTruncated(selectedIdx == null ? -1 : selectedIdx - 1);
}
function setNewIdxTruncated(newIdx) {
if (items.length == 0) {
newIdx = null;
} else {
if (newIdx >= items.length) {
newIdx = 0;
} else if (newIdx < 0) {
newIdx = items.length - 1;
}
}
setSelectedIdx(newIdx);
}
return (
<div
className="block mx-auto w-full p-2
sm:p-6 sm:border-2 sm:border-slate-100 sm:max-w-md rounded-3xl
className="group mx-auto w-full p-2
sm:p-2 sm:border-2 sm:border-slate-100 sm:max-w-md rounded-xl
focus-within:border-slate-200 focus-within:shadow-md
transition-all duration-75
transition-all duration-1000
"
>
<label className="relative block text-slate-500 focus-within:text-slate-900">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className="w-6 h-6 pointer-events-none absolute top-1/2 transform -translate-y-1/2 left-3"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
<label className={`relative block ${INPUT_ELEM_CLASSES}`}>
<Icon
type="magnifying_glass"
className={`ml-2 ${FOCUSABLE_SVG_ELEM_CLASSES}`}
/>
{showSpinner ? (
<Icon
type="spinner"
className={`ml-2 right-2 ${SVG_ELEM_CLASSES}`}
/>
</svg>
) : null}
<input
className="w-full text-xl font-extralight p-4 pl-10 outline-none rounded-xl
placeholder:italic transition-all duration-75
border-slate-200 border-2 bg-slate-100
focus:border-slate-400"
placeholder="Enter Username"
defaultValue={props.initialValue}
autoFocus
className={[
COMMON_LIST_ELEM_CLASSES,
INPUT_ELEM_CLASSES,
"outline-none rounded-lg",
"placeholder:italic bg-slate-100",
items.length > 0 ? "rounded-b-none" : null,
].join(" ")}
placeholder={"Enter Username"}
defaultValue={userName}
onChange={(v) => setUserName(v.target.value)}
onKeyDown={(v) => onSearchInputKeyDown(v)}
/>
</label>
<div>
{items.map(({ name }, idx) => (
<ListItem
key={name}
isLast={idx == items.length - 1}
selected={idx == selectedIdx}
value={name}
/>
))}
</div>
</div>
);
}
// UserSearchBar.propTypes = {
// initialValue: PropTypes.string,
// };
function ListItem({ value, isLast, selected }) {
return (
<div
className={`
${COMMON_LIST_ELEM_CLASSES} border-t-0
${isLast ? "rounded-b-lg" : ""}
${selected ? "bg-slate-700 text-slate-100" : ""}`}
>
{value}
</div>
);
}
ListItem.propTypes = {
name: PropTypes.string,
isLast: PropTypes.bool,
selected: PropTypes.bool,
};
// from https://heroicons.com/
function Icon(props) {
const { type } = props;
const className = `w-6 h-6 pointer-events-none absolute
transform top-1/2 -translate-y-1/2 ${props.className}`;
if (type == "magnifying_glass") {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={"2"}
className={className}
>
<path
fill="none"
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
/>
</svg>
);
}
if (type == "spinner") {
return (
<svg
version="1.1"
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="6 6 38 38"
className={className}
>
<path d="M25.251,6.461c-10.318,0-18.683,8.365-18.683,18.683h4.068c0-8.071,6.543-14.615,14.615-14.615V6.461z">
<animateTransform
attributeType="xml"
attributeName="transform"
type="rotate"
from="0 25 25"
to="360 25 25"
dur="0.6s"
repeatCount="indefinite"
/>
</path>
</svg>
);
}
throw `unhandled icon type ${type}`;
}
Icon.propTypes = {
type: PropTypes.oneOf(["magnifying_glass", "spinner"]),
className: PropTypes.string,
};
export default UserSearchBar;

View File

@@ -5,6 +5,7 @@ Rails.application.routes.draw do
namespace :api do
namespace :fa do
get :similar_users, to: "/domain/fa/api#similar_users"
get :search_users, to: "/domain/fa/api#search_users"
end
end

View File

@@ -11,6 +11,7 @@
"compression-webpack-plugin": "9",
"css-loader": "^6.7.3",
"css-minimizer-webpack-plugin": "^5.0.0",
"lodash": "^4.17.21",
"mini-css-extract-plugin": "^2.7.5",
"prop-types": "^15.8.1",
"react": "^18.2.0",

View File

@@ -2704,6 +2704,11 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==
lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"