basic search bar requests
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user