pass on fa user search

This commit is contained in:
Dylan Knutson
2023-08-18 19:22:39 -07:00
parent 7824deac2d
commit 0b0acd60f2
6 changed files with 92 additions and 312 deletions

View File

@@ -5,10 +5,7 @@ class Domain::Fa::ApiController < ApplicationController
def search_users
name = params[:name]
limit = (params[:limit] || 10).to_i.clamp(0, 15)
users = relevant = users_for_name(name).
map do |id, name, url_name|
{ id: id, name: name, url_name: url_name }
end
users = users_for_name(name)
if !Rails.env.production? && name == "error"
render status: 500, json: { error: "an error!" }
else
@@ -360,166 +357,26 @@ class Domain::Fa::ApiController < ApplicationController
end
def users_for_name(name)
return USERS if Rails.env.development?
Domain::Fa::User.where([
"(name ilike :name) OR (url_name ilike :name)",
{ name: "#{ReduxApplicationRecord.sanitize_sql_like(name)}%" },
]).order(name: :asc).
limit(limit).
pluck(:id, :name, :url_name)
end
]).
includes(:avatar).
select(:id, :state, :state_detail, :log_entry_detail, :name, :url_name).
order(name: :asc).
limit(10).
map do |user|
thumb = nil
if user.avatar&.log_entry
thumb = log_entries_path(user.avatar.log_entry, thumb: true)
end
USERS = [
[102661, "Twee", "twee"],
[102836, "Kobrin", "kobrin"],
[106292, "manadspaniel", "manadspaniel"],
[121171, "Sakura_the_wolf", "sakurathewolf"],
[123481, "Kai~skunk", "kai~skunk"],
[131219, "Luna.Moonflower", "luna.moonflower"],
[132018, "HeyImDaddy", "heyimdaddy"],
[132032, "frankenbunny", "frankenbunny"],
[154415, "Gh0stlyFurry", "gh0stlyfurry"],
[158837, "4r_da_Sqauw", "4rdasqauw"],
[173905, "ZombieUnicorn", "zombieunicorn"],
[181599, "space_fruit", "spacefruit"],
[182869, "Ragd360", "ragd360"],
[185649, "IBluefoxI", "ibluefoxi"],
[188437, "Yuricupcakes", "yuricupcakes"],
[190487, "lingaring", "lingaring"],
[197704, "Rayneier", "rayneier"],
[201974, "Clow", "clow"],
[208562, "rdraft", "rdraft"],
[215738, "Kaori_Keia", "kaorikeia"],
[22036, "tiddys", "tiddys"],
[223999, "SwampPossum", "swamppossum"],
[22841, "k-inukai", "k-inukai"],
[232171, "IPfix_da_Firefox", "ipfixdafirefox"],
[235933, "VisibleSuicide", "visiblesuicide"],
[258806, "Wizmas", "wizmas"],
[273285, "furrything", "furrything"],
[273634, "AngieTheOtter", "angietheotter"],
[282615, "catsrulekg", "catsrulekg"],
[285410, "Icyskills", "icyskills"],
[291374, "autumnfoxfur", "autumnfoxfur"],
[293860, "katamarulte", "katamarulte"],
[306985, "RadioactiveF0X", "radioactivef0x"],
[319368, "Soren123", "soren123"],
[349997, "kuroneko---sama", "kuroneko---sama"],
[350090, "SkylesFawkx", "skylesfawkx"],
[355982, "Shirilee", "shirilee"],
[358587, "Shosta", "shosta"],
[363811, "WhiteKIBA", "whitekiba"],
[374231, "starswillscream", "starswillscream"],
[379724, "dub-c", "dub-c"],
[386887, "Kirei-Uchiha", "kirei-uchiha"],
[407214, "MagicOFManga", "magicofmanga"],
[412404, "PoppyPath", "poppypath"],
[412923, "Sebastian_teh_Dober", "sebastiantehdober"],
[416072, "declinedkitty", "declinedkitty"],
[418580, "X-MXNE666", "x-mxne666"],
[418660, "Peelpeelpeel1", "peelpeelpeel1"],
[419237, "Fennixthepawlicker", "fennixthepawlicker"],
[431318, "GregorianBurns", "gregorianburns"],
[436280, "fionnaidh", "fionnaidh"],
[43684, "MLBat-Darkheart", "mlbat-darkheart"],
[437721, "White_Dragon_sisu", "whitedragonsisu"],
[441002, "someSkylah", "someskylah"],
[441268, "Eviscerator", "eviscerator"],
[442275, "HumanistFurs", "humanistfurs"],
[445549, "Ayrich", "ayrich"],
[448023, "NastyCalamari", "nastycalamari"],
[455220, "analfurs", "analfurs"],
[459851, "TotallySafe", "totallysafe"],
[464719, "Shadowfur13", "shadowfur13"],
[46619, "CandyShop", "candyshop"],
[466695, "ElFurroKawaii", "elfurrokawaii"],
[467565, "Brygaht", "brygaht"],
[472867, "Mitryll", "mitryll"],
[479071, "Shamanroach", "shamanroach"],
[497090, "!BlakeTheGamer", "blakethegamer"],
[507923, "Guro-Jans", "guro-jans"],
[520548, "Elseano", "elseano"],
[530399, "PUMPKINLIGHTTHEKITSUNE", "pumpkinlightthekitsune"],
[531212, "Ulti_199", "ulti199"],
[532461, "DozyDoe", "dozydoe"],
[537220, "fentible", "fentible"],
[539148, "StealthyIntheDark", "stealthyinthedark"],
[545990, "odinokiy", "odinokiy"],
[552205, "Namorria", "namorria"],
[557322, "Katana2", "katana2"],
[56360, "t0rrid", "t0rrid"],
[570571, "Zoralth8745", "zoralth8745"],
[570771, "Swagging", "swagging"],
[574502, "AMeanCow", "ameancow"],
[576501, "CaptianYeti", "captianyeti"],
[578205, "cyphurel", "cyphurel"],
[580442, "rraw", "rraw"],
[584446, "HelveluxDragon", "helveluxdragon"],
[592829, "clownshoes", "clownshoes"],
[594599, "AxisthewoIf", "axisthewoif"],
[597726, "GypsyCavarow", "gypsycavarow"],
[598375, "Cloud_Heart_Saber", "cloudheartsaber"],
[608812, "MidnightMapleMoon", "midnightmaplemoon"],
[608859, "pasteldad", "pasteldad"],
[609253, "mezzoforte2", "mezzoforte2"],
[616675, "Lil_Naz", "lilnaz"],
[617918, "tristian", "tristian"],
[629272, "esenya_valenok", "esenyavalenok"],
[637893, "Ink-EyesBae", "ink-eyesbae"],
[638713, "NavigatorTP", "navigatortp"],
[642236, "-EdenArcane-", "-edenarcane-"],
[660560, "Nevera66", "nevera66"],
[678313, "mog17", "mog17"],
[694556, "veroaid", "veroaid"],
[695130, "Furry_Writers", "furrywriters"],
[697418, "Lazywerewolf", "lazywerewolf"],
[711410, "Seas1de", "seas1de"],
[713084, "fuzzmuffin23", "fuzzmuffin23"],
[713759, "alp_Rik", "alprik"],
[716200, "RequiemDreamer", "requiemdreamer"],
[718333, "KhorneSoup", "khornesoup"],
[73267, "splendidguy44", "splendidguy44"],
[738005, "aurokhan", "aurokhan"],
[742214, "velin.art", "velin.art"],
[742375, "vampiretigress", "vampiretigress"],
[752692, "FlairMalin", "flairmalin"],
[756325, "Thorn_Mochag", "thornmochag"],
[75693, "numbsoul", "numbsoul"],
[758113, "leetf0x", "leetf0x"],
[758475, "Naspyta", "naspyta"],
[763834, "dankkushlove420", "dankkushlove420"],
[773150, "Fokatuh", "fokatuh"],
[777382, "Yorrick67", "yorrick67"],
[783391, "lovebigtits94", "lovebigtits94"],
[790219, "ValiantBadger", "valiantbadger"],
[793308, "promqueen192", "promqueen192"],
[794155, "Sola_the_Solgaleo", "solathesolgaleo"],
[802477, "kempo2k", "kempo2k"],
[804770, "vinv", "vinv"],
[808277, "AzartAlien", "azartalien"],
[819527, "curiosity6678", "curiosity6678"],
[820429, "GrayGrim", "graygrim"],
[822085, "rRICHr", "rrichr"],
[835929, "Kovuska", "kovuska"],
[836315, "knotthatbun", "knotthatbun"],
[836547, "1223223", "1223223"],
[841669, "DisheveledSloth", "disheveledsloth"],
[84257, "Naktian-Furs", "naktian-furs"],
[844032, "Fluffy_kozlik", "fluffykozlik"],
[854728, "pierre_dunn", "pierredunn"],
[856538, "onlyheretosponser", "onlyheretosponser"],
[861416, "tailsthefoxrules123", "tailsthefoxrules123"],
[862656, "EVXFY", "evxfy"],
[865110, "LowkeyArt", "lowkeyart"],
[873049, "Tedy", "tedy"],
[875191, "JamesTec666", "jamestec666"],
[875416, "Obscurum", "obscurum"],
[879928, "Solious6233", "solious6233"],
[882664, "princepixelbunny", "princepixelbunny"],
[887058, "Pyrote", "pyrote"],
[90246, "sonupi", "sonupi"],
[92111, "zevialexander", "zevialexander"],
[92831, "foxmacro", "foxmacro"],
]
{
id: user.id,
name: user.name,
url_name: user.url_name,
thumb: thumb,
}
end
end
end

View File

@@ -6,8 +6,8 @@ import ListItem from "./ListItem";
import Trie, { TrieNode } from "../lib/Trie";
// staging
const HOST = "http://scraper.local:3001";
// const HOST = "";
// const HOST = "http://scraper.local:3001";
const HOST = "";
const COMMON_LIST_ELEM_CLASSES = `
w-full p-2 pl-8
text-xl font-light
@@ -39,43 +39,19 @@ type TrieNodeType = TrieNode<TrieValue>;
export default function UserSearchBar({ isServerRendered }: PropTypes) {
isServerRendered = !!isServerRendered;
const [userName, setUserName] = useState("");
const [items, setUserList] = useState<ServerResponse["users"]>([]);
const [userList, setUserList] = useState<ServerResponse["users"]>([]);
const [selectedIdx, setSelectedIdx] = useState<number | null>(null);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [pendingRequest, setPendingRequest] = useState<AbortController | null>(
null
);
const [typingSettled, setTypingSettled] = useState(true);
const [trie, setTrie] = useState<TrieType | null>(null);
const inputRef = useRef(null);
const [isFocused, setIsFocused] = useState(
isServerRendered ? false : document.activeElement == inputRef.current
);
useEffect(() => {
const controller = new AbortController();
async function loadUserTrie() {
try {
let req = await fetch(`${HOST}/api/fa/load_users`, {
signal: controller.signal,
});
if (req.status != 200) {
setErrorMessage(`error loading users: ${await req.body}`);
} else {
let trieSerialized = await req.json();
const trie: TrieType = new Trie(trieSerialized);
setErrorMessage("loaded trie");
setTrie(trie);
}
} catch (err) {
console.log("error loading user trie: ", err);
}
}
loadUserTrie();
return () => controller.abort();
}, []);
const clearResults = useCallback(() => {
setUserList([]);
setErrorMessage(null);
@@ -84,123 +60,65 @@ export default function UserSearchBar({ isServerRendered }: PropTypes) {
const cancelPendingRequest = useCallback(async () => {
if (pendingRequest) {
clearResults();
setPendingRequest(null);
pendingRequest.abort();
}
}, [pendingRequest, userName]);
}, [pendingRequest]);
const searchForUserDebounced = useCallback(
debounce(async (userName) => {
setTypingSettled(true);
setUserName(userName);
}, 250),
[]
const sendSearchRequest = useCallback(
(userName) => {
cancelPendingRequest();
const controller = new AbortController();
setPendingRequest(controller);
async function sendRequest() {
try {
let req = await fetch(
`${HOST}/api/fa/search_users?name=${userName}`,
{
signal: controller.signal,
}
);
if (req.status != 200) {
setErrorMessage(`error loading users: ${await req.body}`);
} else {
let gotUserList = await req.json();
setPendingRequest(null);
setUserList(gotUserList.users);
}
} catch (err) {
console.log("error loading user trie: ", err);
setPendingRequest(null);
setErrorMessage("error loading users");
}
}
sendRequest();
return () => controller.abort();
},
[cancelPendingRequest]
);
const searchForUser = useCallback(
(userName: string) => {
// setTypingSettled(false);
// cancelPendingRequest();
if (trie == null) {
return;
} else if (isEmpty(userName)) {
setUserName(userName);
if (isEmpty(userName)) {
clearResults();
setUserName("");
} else {
const { node } = trie.nodeForPrefix(userName);
if (node) {
const users = nodeToList(node, 5).map((node) => ({
name: `${node.value[1]} (${node.value[0]})`,
}));
console.log("Set users", users);
setUserList(users);
}
sendSearchRequest(userName);
}
},
[cancelPendingRequest, trie]
[clearResults, sendSearchRequest]
);
function nodeToList(node: TrieNodeType, limit: number): TrieNodeType[] {
const list = [];
nodeToListImpl(list, node, limit);
return list;
}
function nodeToListImpl(
listOut: TrieNodeType[],
node: TrieNodeType,
limit: number
) {
if (listOut.length >= limit) return;
if (node.terminal) {
listOut.push(node);
if (listOut.length >= limit) return;
}
for (const [childKey, child] of node.children.entries()) {
nodeToListImpl(listOut, child, limit);
if (listOut.length >= limit) return;
}
}
// useEffect(() => {
// if (isEmpty(userName)) {
// return;
// }
// if (trie == null) {
// return;
// }
// const node = trie.nodeForPrefix(userName);
// setUserList([{ name: `node ${node}` }]);
// // const controller = new AbortController();
// // setPendingRequest(controller);
// // async function doFetch() {
// // try {
// // let response = await fetch(
// // `${HOST}/api/fa/search_users?name=${encodeURIComponent(userName)}`,
// // {
// // signal: controller.signal,
// // }
// // );
// // await handleServerResponse(response);
// // } catch (error) {
// // if (error.name == "AbortError") {
// // return;
// // }
// // clearResults();
// // setErrorMessage(error.toString());
// // } finally {
// // setPendingRequest(null);
// // }
// // }
// // doFetch();
// return () => {
// cancelPendingRequest();
// };
// }, [userName, trie]);
// const handleServerResponse = useCallback(async (response) => {
// clearResults();
// const contentType: string = response.headers.get("Content-Type");
// if (response.status == 200) {
// const { users }: ServerResponse = await response.json();
// setUserList(users);
// } else if (
// !isEmpty(contentType) &&
// contentType.startsWith("application/json")
// ) {
// const { error } = await response.json();
// setErrorMessage(`${response.status}: ${error}`);
// } else {
// const text = await response.text();
// setErrorMessage(`${response.status}: ${text}`);
// }
// }, []);
const searchForUserDebounced = useCallback(
debounce(async (userName) => {
setTypingSettled(true);
searchForUser(userName);
}, 250),
[setTypingSettled, searchForUser]
);
function onSearchInputKeyDown(event) {
const { code, shiftKey } = event;
@@ -220,7 +138,10 @@ export default function UserSearchBar({ isServerRendered }: PropTypes) {
break;
case "Enter":
if (selectedIdx != null) {
setUserName(items[selectedIdx].name);
const user = userList[selectedIdx];
console.log("selecting user: ", user);
setUserName(user.name);
inputRef.current.value = user.name;
}
break;
default:
@@ -239,13 +160,13 @@ export default function UserSearchBar({ isServerRendered }: PropTypes) {
}
function setNewIdxTruncated(newIdx) {
if (items.length == 0) {
if (userList.length == 0) {
newIdx = null;
} else {
if (newIdx >= items.length) {
if (newIdx >= userList.length) {
newIdx = 0;
} else if (newIdx < 0) {
newIdx = items.length - 1;
newIdx = userList.length - 1;
}
}
setSelectedIdx(newIdx);
@@ -257,9 +178,8 @@ export default function UserSearchBar({ isServerRendered }: PropTypes) {
!isEmpty(userName) &&
pendingRequest == null &&
typingSettled &&
items.length == 0;
const itemsShown =
isFocused && items.length > 0 && typingSettled && pendingRequest == null;
userList.length == 0;
const itemsShown = isFocused && userList.length > 0;
const anyShown = infoShown || errorShown || itemsShown;
function UserSearchBarItems() {
@@ -268,7 +188,7 @@ export default function UserSearchBar({ isServerRendered }: PropTypes) {
{errorShown ? (
<ListItem
key="error"
isLast={!infoShown && items.length == 0}
isLast={!infoShown && userList.length == 0}
selected={false}
style="error"
value={errorMessage}
@@ -284,10 +204,10 @@ export default function UserSearchBar({ isServerRendered }: PropTypes) {
/>
) : null}
{itemsShown
? items.map(({ name }, idx) => (
? userList.map(({ name }, idx) => (
<ListItem
key={"name-" + name}
isLast={idx == items.length - 1}
isLast={idx == userList.length - 1}
selected={idx == selectedIdx}
style="item"
value={name}
@@ -325,8 +245,14 @@ export default function UserSearchBar({ isServerRendered }: PropTypes) {
].join(" ")}
placeholder={"Enter Username"}
defaultValue={userName}
onChange={(v) => searchForUser(v.target.value)}
onKeyDown={(v) => onSearchInputKeyDown(v)}
onChange={(v) => {
setTypingSettled(false);
searchForUserDebounced(v.target.value);
}}
onKeyDown={(v) => {
setIsFocused(true);
onSearchInputKeyDown(v);
}}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
ref={inputRef}

View File

@@ -1,7 +1,7 @@
function buildUsersTrie(users) {
const rootNode = new trie();
users.forEach(([id, name, urlName]) => {
rootNode.insert(name.toLowerCase(), [id, name, urlName]);
users.forEach(([id, name]) => {
rootNode.insert(name.toLowerCase(), [id, name]);
});
return JSON.stringify(rootNode.serialize());
}

View File

@@ -22,7 +22,7 @@
<header class="bg-slate-100 border-slate-200 border-b-2">
<div class="mx-auto max-w-5xl py-6 px-6 sm:px-8 flex items-baseline">
<h1 class="text-4xl sm:text-5xl font-bold text-slate-900">
<%= @site_title %>
<%= link_to @site_title, root_path %>
</h1>
<div class="flex-grow"></div>
<h2 class="text-1xl sm:text-2xl italic font-bold text-slate-500">

View File

@@ -39,10 +39,7 @@ Rails.application.routes.draw do
end
end
# Defines the root path route ("/")
# root "articles#index"
resources :blobs, only: [:show], slug: :sha256
resources :log_entries, only: [:index, :show] do
get :contents, on: :member
get :stats, on: :collection

View File

@@ -13,7 +13,7 @@ class CreateIndexGoodJobsJobsOnPriorityCreatedAtWhenUnfinished < ActiveRecord::M
end
add_index :good_jobs, [:priority, :created_at], order: { priority: "DESC NULLS LAST", created_at: :asc },
where: "finished_at IS NULL", name: :index_good_jobs_jobs_on_priority_created_at_when_unfinished,
algorithm: :concurrently
where: "finished_at IS NULL", name: :index_good_jobs_jobs_on_priority_created_at_when_unfinished,
algorithm: :concurrently
end
end