diff --git a/src/components/Input/index.tsx b/src/components/Input/index.tsx index 453c742..3899ae0 100644 --- a/src/components/Input/index.tsx +++ b/src/components/Input/index.tsx @@ -2,22 +2,103 @@ import "./input.scss"; import { fetchData } from "../../utils/fetch-data"; import { debounce } from "../../utils/deboucne"; import Loader from "../Loader"; +import { useState } from "react"; export interface InputProps { /** Placeholder of the input */ placeholder?: string; /** On click item handler */ onSelectItem: (item: string) => void; + /** debounce time (in ms) */ + debounceTime?: number; } -const Input = ({ placeholder, onSelectItem }: InputProps) => { +export interface SearchResult { + loading: boolean; + error?: string; + items?: string[]; +} + +const Input = ({ + placeholder, + onSelectItem, + debounceTime = 300, +}: InputProps) => { // DO NOT remove this log - console.log('input re-render') + console.log("input re-render"); // Your code start here - return + const [result, setResult] = useState(undefined); + + const hangleChange = debounce((e: React.ChangeEvent) => { + const searchQuery = e.target.value; + if (!searchQuery) { + setResult(undefined); + return; + } + + let ignored = false; + setResult({ loading: true }); + fetchData(searchQuery) + .then((result) => { + if (ignored) return; + setResult({ + loading: false, + items: result, + }); + }) + .catch((err) => { + if (ignored) return; + setResult({ + loading: false, + error: err, + }); + }); + + return () => { + ignored = true; + }; + }, debounceTime); + + const renderResult = () => { + if (result.loading) { + return ( +
+ +
+ ); + } + + if (result.error) { + return
{result.error}
; + } + + if (!result.items?.length) { + return
No result!
; + } + + return ( + + ); + }; + + return ( +
+ + {!!result &&
{renderResult()}
} +
+ ); // Your code end here }; export default Input; - diff --git a/src/components/Input/input.scss b/src/components/Input/input.scss index 1dafbe7..4463e6d 100644 --- a/src/components/Input/input.scss +++ b/src/components/Input/input.scss @@ -1,6 +1,66 @@ * { - box-sizing: border-box; + box-sizing: border-box; } -html{ - font-size: 16px; +html { + font-size: 16px; +} + +.search { + position: relative; + width: 300px; + line-height: 1.5em; + font-size: 16px; + + &__input { + border-radius: 4px; + padding: 0.5em 1em; + width: 100%; + } + + &__result { + position: absolute; + width: 100%; + background-color: #ffffff; + box-shadow: 0 0 10px 0 rgba(0, 0, 0, 10%); + top: calc(100% + 8px); + } + + &__loader { + padding: 2em; + } + + &__message { + padding: 0.5em 1em; + color: #03183f; + } + + &__list { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 4px; + max-height: 200px; + overflow-y: auto; + } + &__item { + button { + color: #03183f; + line-height: 1.5em; + font-size: 16px; + padding: 0.5em 1em; + cursor: pointer; + display: inline-flex; + align-items: flex-start; + margin: 0; + border: none; + background: none; + width: 100%; + + &:hover { + background-color: #f6f6f6; + } + } + } } diff --git a/src/stories/Input.stories.ts b/src/stories/Input.stories.ts index 4961b83..fe0bda7 100644 --- a/src/stories/Input.stories.ts +++ b/src/stories/Input.stories.ts @@ -1,18 +1,18 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import { fn } from '@storybook/test'; +import type { Meta, StoryObj } from "@storybook/react"; +import { fn } from "@storybook/test"; -import Input from '../components/Input'; +import Input from "../components/Input"; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { - title: 'Example/Input', + title: "Example/Input", component: Input, parameters: { // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout - layout: 'centered', + layout: "centered", }, // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs - tags: ['autodocs'], + tags: ["autodocs"], // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args // args: { onClick: fn() }, } satisfies Meta; @@ -24,6 +24,7 @@ type Story = StoryObj; export const Primary: Story = { args: { placeholder: "Type something to search...", - onSelectItem: fn() + debounceTime: 300, + onSelectItem: fn(), }, };