diff --git a/src/App.css b/src/App.css
index b9d355d..56e3d65 100644
--- a/src/App.css
+++ b/src/App.css
@@ -1,42 +1,134 @@
-#root {
- max-width: 1280px;
- margin: 0 auto;
- padding: 2rem;
- text-align: center;
+* {
+ box-sizing: border-box
}
-.logo {
- height: 6em;
- padding: 1.5em;
- will-change: filter;
- transition: filter 300ms;
+html {
+ font-size: 16px
}
-.logo:hover {
- filter: drop-shadow(0 0 2em #646cffaa);
+
+body {
+ justify-content: center;
+}
+
+.search-result {
+ width: 100%;
+ position: absolute;
+ border-radius: 4px;
+ min-height: 100px;
+ border: solid 1px #ddd;
+ margin-top: 4px;
+ max-height: 400px;
+ overflow-y: auto;
+}
+
+.lists {
+ display: flex;
+ flex-direction: column
}
-.logo.react:hover {
- filter: drop-shadow(0 0 2em #61dafbaa);
+
+.item {
+ padding: 1em 2em
+}
+
+.item:hover {
+ background-color: #eee;
+ cursor: pointer
+}
+
+.no-result {
+ font-style: italic;
+ color: #333;
+ text-align: center
}
-@keyframes logo-spin {
- from {
- transform: rotate(0deg);
+.error-message {
+ color: red;
+ font-style: italic;
+ padding: .5em 1em
+}
+
+.loader-container {
+ height: 50px;
+ width: 50px;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%) scale(.8)
+}
+
+.loader {
+ animation: rotate 1s infinite;
+ height: 50px;
+ width: 50px;
+ position: absolute
+}
+
+.loader:before,
+.loader:after {
+ border-radius: 50%;
+ content: "";
+ display: block;
+ height: 20px;
+ width: 20px
+}
+
+.loader:before {
+ animation: ball1 1s infinite;
+ background-color: #ccc;
+ box-shadow: 30px 0 #333;
+ margin-bottom: 10px
+}
+
+.loader:after {
+ animation: ball2 1s infinite;
+ background-color: #333;
+ box-shadow: 30px 0 #ccc
+}
+
+@keyframes rotate {
+ 0% {
+ transform: rotate(0) scale(.8)
+ }
+
+ 50% {
+ transform: rotate(360deg) scale(1.2)
}
+
to {
- transform: rotate(360deg);
+ transform: rotate(720deg) scale(.8)
}
}
-@media (prefers-reduced-motion: no-preference) {
- a:nth-of-type(2) .logo {
- animation: logo-spin infinite 20s linear;
+@keyframes ball1 {
+ 0% {
+ box-shadow: 30px 0 #333
}
-}
-.card {
- padding: 2em;
-}
+ 50% {
+ box-shadow: 0 0 #333;
+ margin-bottom: 0;
+ transform: translate(15px, 15px)
+ }
-.read-the-docs {
- color: #888;
+ to {
+ box-shadow: 30px 0 #333;
+ margin-bottom: 10px
+ }
}
+
+@keyframes ball2 {
+ 0% {
+ box-shadow: 30px 0 #ccc
+ }
+
+ 50% {
+ box-shadow: 0 0 #ccc;
+ margin-top: -20px;
+ transform: translate(15px, 15px)
+ }
+
+ to {
+ box-shadow: 30px 0 #ccc;
+ margin-top: 0
+ }
+}
\ No newline at end of file
diff --git a/src/App.tsx b/src/App.tsx
index afe48ac..0e92994 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,34 +1,14 @@
-import { useState } from 'react'
-import reactLogo from './assets/react.svg'
-import viteLogo from '/vite.svg'
import './App.css'
+import Input from './components/Input'
function App() {
- const [count, setCount] = useState(0)
+
+ const handleSelectItem = (item: string) => {
+ console.log(item)
+ }
return (
- <>
-
- Vite + React
-
-
-
- Edit src/App.tsx and save to test HMR
-
-
-
- Click on the Vite and React logos to learn more
-
- >
+
)
}
diff --git a/src/components/Input/SearchResults.tsx b/src/components/Input/SearchResults.tsx
new file mode 100644
index 0000000..42047c0
--- /dev/null
+++ b/src/components/Input/SearchResults.tsx
@@ -0,0 +1,39 @@
+import React from "react"
+import Loader from "../Loader"
+
+interface SearchResultsProps {
+ isLoading: boolean
+ errorMessage: string | null
+ searchResults: string[]
+ onSelectItem: (item: string) => void
+}
+
+const SearchResults: React.FC = ({
+ isLoading,
+ errorMessage,
+ searchResults,
+ onSelectItem
+}) => {
+ return (
+
+ {isLoading &&
}
+ {errorMessage &&
{errorMessage}
}
+ {searchResults.length === 0 && !isLoading && !errorMessage && (
+
No results found
+ )}
+
+ {searchResults.map((result, index) => (
+
onSelectItem(result)}
+ >
+ {result}
+
+ ))}
+
+
+ )
+}
+
+export default SearchResults
\ No newline at end of file
diff --git a/src/components/Input/index.tsx b/src/components/Input/index.tsx
index 453c742..949e2ef 100644
--- a/src/components/Input/index.tsx
+++ b/src/components/Input/index.tsx
@@ -1,23 +1,76 @@
-import "./input.scss";
-import { fetchData } from "../../utils/fetch-data";
-import { debounce } from "../../utils/deboucne";
-import Loader from "../Loader";
-
+import "./input.scss"
+import { useReducer, useEffect, useCallback } from "react"
+import { fetchData } from "../../utils/fetch-data"
+import { debounce } from "../../utils/deboucne"
+import SearchResults from "./SearchResults"
+import { reducer, initialState } from "./inputReducer"
export interface InputProps {
/** Placeholder of the input */
- placeholder?: string;
+ placeholder?: string
/** On click item handler */
- onSelectItem: (item: string) => void;
+ onSelectItem: (item: string) => void
}
-
+const DEBOUNCE_TIME = 500
const Input = ({ placeholder, onSelectItem }: InputProps) => {
// DO NOT remove this log
console.log('input re-render')
- // Your code start here
- return
- // Your code end here
-};
+ // Your code starts here
+ const [state, dispatch] = useReducer(reducer, initialState)
+ const { searchQuery, searchResults, isLoading, errorMessage } = state
+
+ useEffect(() => {
+ if (!searchQuery) {
+ return
+ }
+
+ const startFetching = async () => {
+ try {
+ const results = await fetchData(searchQuery)
+ if (!ignore) {
+ dispatch({ type: "SET_SEARCH_RESULTS", payload: results })
+ }
+ } catch (error: any) {
+ if (!ignore) {
+ dispatch({ type: "SET_ERROR", payload: error.message || "An error occurred" })
+ }
+ }
+ }
+ let ignore = false
+ startFetching()
-export default Input;
+ return () => {
+ ignore = true
+ }
+ }, [searchQuery])
+
+ const handleSearch = useCallback(
+ debounce((value: string) => {
+ dispatch({ type: "SET_SEARCH_QUERY", payload: value })
+ }, DEBOUNCE_TIME),
+ []
+ )
+
+ return (
+
+ )
+ // Your code ends here
+}
+export default Input
diff --git a/src/components/Input/input.scss b/src/components/Input/input.scss
index 1dafbe7..c591652 100644
--- a/src/components/Input/input.scss
+++ b/src/components/Input/input.scss
@@ -1,6 +1,15 @@
-* {
- box-sizing: border-box;
-}
-html{
- font-size: 16px;
+//** variables
+$input-text-color: #a3a3a3;
+
+.form {
+ position: relative;
+ &__field {
+ width: 360px;
+ background: #fff;
+ font: inherit;
+ border: 1px solid $input-text-color;
+ box-shadow: 0 6px 10px 0 rgba(0, 0, 0, .1);
+ outline: 0;
+ padding: 22px 18px;
+ }
}
diff --git a/src/components/Input/inputReducer.tsx b/src/components/Input/inputReducer.tsx
new file mode 100644
index 0000000..17e3eff
--- /dev/null
+++ b/src/components/Input/inputReducer.tsx
@@ -0,0 +1,34 @@
+type Action =
+ | { type: "SET_SEARCH_QUERY"; payload: string }
+ | { type: "SET_SEARCH_RESULTS"; payload: string[] }
+ | { type: "SET_LOADING"; payload: boolean }
+ | { type: "SET_ERROR"; payload: string | null }
+
+export interface State {
+ searchQuery: string
+ searchResults: string[]
+ isLoading: boolean
+ errorMessage: string | null
+}
+
+export const initialState: State = {
+ searchQuery: "",
+ searchResults: [],
+ isLoading: false,
+ errorMessage: null,
+}
+
+export const reducer = (state: State, action: Action): State => {
+ switch (action.type) {
+ case "SET_SEARCH_QUERY":
+ return { ...state, searchQuery: action.payload, searchResults: [], errorMessage: null, isLoading: true }
+ case "SET_SEARCH_RESULTS":
+ return { ...state, searchResults: action.payload, isLoading: false }
+ case "SET_LOADING":
+ return { ...state, isLoading: action.payload }
+ case "SET_ERROR":
+ return { ...state, errorMessage: action.payload, isLoading: false }
+ default:
+ return state
+ }
+}
\ No newline at end of file