debounced searching

Co-authored-by: William Oldham <wegg7250@gmail.com>
This commit is contained in:
Jelle van Snik 2022-02-13 19:29:25 +01:00
parent e75fcd3002
commit f1ffa98a2b
8 changed files with 749 additions and 1609 deletions

View File

@ -49,6 +49,8 @@
"@types/react-router-dom": "^5.3.3",
"autoprefixer": "^10.4.2",
"postcss": "^8.4.6",
"prettier": "^2.5.1",
"prettier-plugin-tailwindcss": "^0.1.7",
"tailwindcss": "^3.0.20",
"typescript": "^4.5.5"
}

View File

@ -1,12 +1,7 @@
import { ButtonControlProps, ButtonControl } from "./ButtonControl";
import { Icon, Icons } from "components/Icon";
import React, {
useRef,
Ref,
Dispatch,
SetStateAction,
MouseEventHandler,
KeyboardEvent,
SyntheticEvent,
useEffect,
useState,
@ -17,9 +12,9 @@ import { Backdrop, useBackdrop } from "components/layout/Backdrop";
export interface DropdownButtonProps extends ButtonControlProps {
icon: Icons;
open: boolean;
setOpen: Dispatch<SetStateAction<boolean>>;
setOpen: (open: boolean) => void;
selectedItem: string;
setSelectedItem: Dispatch<SetStateAction<string>>;
setSelectedItem: (value: string) => void;
options: Array<OptionItem>;
}
@ -38,7 +33,7 @@ export interface OptionItem {
function Option({ option, onClick, tabIndex }: OptionProps) {
return (
<div
className="text-denim-700 h-10 px-4 py-2 text-left cursor-pointer flex items-center space-x-2 hover:text-white transition-colors"
className="text-denim-700 flex h-10 cursor-pointer items-center space-x-2 px-4 py-2 text-left transition-colors hover:text-white"
onClick={onClick}
tabIndex={tabIndex}
>
@ -73,6 +68,7 @@ export const DropdownButton = React.forwardRef<
return () => {
if (id) clearTimeout(id);
};
/* eslint-disable-next-line */
}, [props.open]);
const selectedItem: OptionItem = props.options.find(
@ -81,6 +77,7 @@ export const DropdownButton = React.forwardRef<
useEffect(() => {
setBackdrop(props.open);
/* eslint-disable-next-line */
}, [props.open]);
const onOptionClick = (e: SyntheticEvent, option: OptionItem) => {
@ -90,7 +87,7 @@ export const DropdownButton = React.forwardRef<
};
return (
<div className="w-full sm:w-auto min-w-[140px]">
<div className="w-full min-w-[140px] sm:w-auto">
<div
ref={ref}
className="relative w-full sm:w-auto"
@ -98,7 +95,7 @@ export const DropdownButton = React.forwardRef<
>
<ButtonControl
{...props}
className="flex items-center justify-center sm:justify-left px-4 py-2 space-x-2 bg-bink-200 relative z-20 hover:bg-bink-300 text-white h-10 rounded-[20px] w-full"
className="sm:justify-left bg-bink-200 hover:bg-bink-300 relative z-20 flex h-10 w-full items-center justify-center space-x-2 rounded-[20px] px-4 py-2 text-white"
>
<Icon icon={selectedItem.icon} />
<span className="flex-1">{selectedItem.name}</span>
@ -108,22 +105,20 @@ export const DropdownButton = React.forwardRef<
/>
</ButtonControl>
<div
className={`absolute pt-[40px] top-0 duration-200 transition-all w-full rounded-[20px] z-10 bg-denim-300 ${
className={`bg-denim-300 absolute top-0 z-10 w-full rounded-[20px] pt-[40px] transition-all duration-200 ${
props.open
? "opacity-100 max-h-60 block"
: "opacity-0 max-h-0 invisible"
? "block max-h-60 opacity-100"
: "invisible max-h-0 opacity-0"
}`}
>
{props.options
.filter((opt) => opt.id != delayedSelectedId)
.filter((opt) => opt.id !== delayedSelectedId)
.map((opt) => (
<Option
option={opt}
key={opt.id}
onClick={(e) => onOptionClick(e, opt)}
tabIndex={
props.open ? 0 : undefined
} /*onKeyPress={active ? handleOptionKeyPress(opt, i) : undefined}*/
tabIndex={props.open ? 0 : undefined}
/>
))}
</div>

View File

@ -1,43 +1,38 @@
import { DropdownButton } from "./Buttons/DropdownButton";
import { Icons } from "./Icon";
import {
TextInputControl,
TextInputControlPropsNoLabel,
} from "./TextInputs/TextInputControl";
import { TextInputControl } from "./TextInputs/TextInputControl";
import { useState, useRef, useEffect } from "react";
import { useState } from "react";
import { MWMediaType, MWQuery } from "scrapers";
export interface SearchBarProps extends TextInputControlPropsNoLabel {
export interface SearchBarProps {
buttonText?: string;
onClick?: () => void;
placeholder?: string;
onChange: (value: MWQuery) => void;
value: MWQuery;
}
export function SearchBarInput(props: SearchBarProps) {
const [dropdownOpen, setDropdownOpen] = useState(false);
const [dropdownSelected, setDropdownSelected] = useState("movie");
const dropdownRef = useRef<any>();
const handleClick = (e: MouseEvent) => {
if (dropdownRef.current?.contains(e.target as Node)) {
// inside click
return;
function setSearch(value: string) {
props.onChange({
...props.value,
searchQuery: value,
});
}
function setType(type: string) {
props.onChange({
...props.value,
type: type as MWMediaType,
});
}
// outside click
closeDropdown();
};
const closeDropdown = () => {
setDropdownOpen(false);
};
return (
<div className="flex flex-col sm:flex-row items-center gap-4 px-4 py-4 sm:pl-8 sm:pr-2 sm:py-2 bg-denim-300 rounded-[28px] hover:bg-denim-400 focus-within:bg-denim-400 transition-colors">
<div className="bg-denim-300 hover:bg-denim-400 focus-within:bg-denim-400 flex flex-col items-center gap-4 rounded-[28px] px-4 py-4 transition-colors sm:flex-row sm:py-2 sm:pl-8 sm:pr-2">
<TextInputControl
onChange={props.onChange}
value={props.value}
className="placeholder-denim-700 w-full bg-transparent flex-1 focus:outline-none text-white"
onChange={setSearch}
value={props.value.searchQuery}
className="placeholder-denim-700 w-full flex-1 bg-transparent text-white focus:outline-none"
placeholder={props.placeholder}
/>
@ -45,27 +40,26 @@ export function SearchBarInput(props: SearchBarProps) {
icon={Icons.SEARCH}
open={dropdownOpen}
setOpen={setDropdownOpen}
selectedItem={dropdownSelected}
setSelectedItem={setDropdownSelected}
selectedItem={props.value.type}
setSelectedItem={setType}
options={[
{
id: "movie",
id: MWMediaType.MOVIE,
name: "Movie",
icon: Icons.FILM,
},
{
id: "series",
id: MWMediaType.SERIES,
name: "Series",
icon: Icons.CLAPPER_BOARD,
},
{
id: "anime",
id: MWMediaType.ANIME,
name: "Anime",
icon: Icons.DRAGON,
},
]}
onClick={() => setDropdownOpen((old) => !old)}
ref={dropdownRef}
>
{props.buttonText || "Search"}
</DropdownButton>

View File

@ -46,17 +46,19 @@ export function Backdrop(props: BackdropProps) {
useEffect(() => {
setVisible(!!props.active);
}, [props.active]);
/* eslint-disable-next-line */
}, [props.active, setVisible]);
useEffect(() => {
if (!isVisible) animationEvent();
/* eslint-disable-next-line */
}, [isVisible]);
if (!isVisible) return null;
return (
<div
className={`fixed h-screen z-[999] top-0 left-0 right-0 bg-black bg-opacity-50 transition-opacity opacity-100 ${
className={`fixed top-0 left-0 right-0 z-[999] h-screen bg-black bg-opacity-50 opacity-100 transition-opacity ${
!isVisible ? "opacity-0" : ""
}`}
{...fadeProps}

20
src/hooks/useDebounce.ts Normal file
View File

@ -0,0 +1,20 @@
import { useEffect, useState } from "react";
export function useDebounce<T>(value: T, delay: number): T {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(
() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
},
[value, delay]
);
return debouncedValue;
}

View File

@ -1,6 +1,7 @@
export enum MWMediaType {
MOVIE = "movie",
SERIES = "series",
ANIME = "anime",
}
export interface MWPortableMedia {

View File

@ -1,29 +1,38 @@
import { WatchedMediaCard } from "components/media/WatchedMediaCard";
import { SearchBarInput } from "components/SearchBar";
import { MWMedia, MWMediaType, SearchProviders } from "scrapers";
import { useState } from "react";
import { MWMedia, MWMediaType, MWQuery, SearchProviders } from "scrapers";
import { useEffect, useState } from "react";
import { ThinContainer } from "components/layout/ThinContainer";
import { SectionHeading } from "components/layout/SectionHeading";
import { Icons } from "components/Icon";
import { Loading } from "components/layout/Loading";
import { Tagline } from "components/Text/Tagline";
import { Title } from "components/Text/Title";
import { useDebounce } from "hooks/useDebounce";
export function SearchView() {
const [results, setResults] = useState<MWMedia[]>([]);
const [search, setSearch] = useState("");
async function runSearch() {
const results = await SearchProviders({
const [search, setSearch] = useState<MWQuery>({
searchQuery: "",
type: MWMediaType.MOVIE,
searchQuery: search,
});
const debouncedSearch = useDebounce<MWQuery>(search, 2000);
useEffect(() => {
if (debouncedSearch.searchQuery !== "") runSearch(debouncedSearch);
}, [debouncedSearch]);
useEffect(() => {
setResults([]);
}, [search]);
async function runSearch(query: MWQuery) {
const results = await SearchProviders(query);
setResults(results);
}
return (
<ThinContainer>
<div className="mt-36 text-center space-y-16">
<div className="mt-36 space-y-16 text-center">
<div className="space-y-4">
<Tagline>Because watching legally is boring</Tagline>
<Title>What movie do you want to watch?</Title>
@ -31,16 +40,17 @@ export function SearchView() {
<SearchBarInput
onChange={setSearch}
value={search}
onClick={runSearch}
placeholder="What movie do you want to watch?"
/>
</div>
<SectionHeading title="Yoink" icon={Icons.SEARCH}>
{results.length > 0 ? (
<SectionHeading title="Search results" icon={Icons.SEARCH}>
{results.map((v) => (
<WatchedMediaCard media={v} />
))}
</SectionHeading>
<Loading />
) : null}
{search.searchQuery !== "" && results.length == 0 ? <Loading /> : null}
</ThinContainer>
);
}

2198
yarn.lock

File diff suppressed because it is too large Load Diff