├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.tsx
├── LiveSearch.tsx
├── index.css
└── index.tsx
├── tailwind.config.js
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Live Sear Demo React App
2 |
3 | Made with React JS and Tailwind CSS
4 |
5 | just one command should show you the demo `npm install && npm start`
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "live-search",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.16.5",
7 | "@testing-library/react": "^13.3.0",
8 | "@testing-library/user-event": "^13.5.0",
9 | "@types/jest": "^27.5.2",
10 | "@types/node": "^16.11.49",
11 | "@types/react": "^18.0.17",
12 | "@types/react-dom": "^18.0.6",
13 | "react": "^18.2.0",
14 | "react-dom": "^18.2.0",
15 | "react-scripts": "5.0.1",
16 | "typescript": "^4.7.4",
17 | "web-vitals": "^2.1.4"
18 | },
19 | "scripts": {
20 | "start": "react-scripts start",
21 | "build": "react-scripts build",
22 | "test": "react-scripts test",
23 | "eject": "react-scripts eject"
24 | },
25 | "eslintConfig": {
26 | "extends": [
27 | "react-app",
28 | "react-app/jest"
29 | ]
30 | },
31 | "browserslist": {
32 | "production": [
33 | ">0.2%",
34 | "not dead",
35 | "not op_mini all"
36 | ],
37 | "development": [
38 | "last 1 chrome version",
39 | "last 1 firefox version",
40 | "last 1 safari version"
41 | ]
42 | },
43 | "devDependencies": {
44 | "autoprefixer": "^10.4.8",
45 | "postcss": "^8.4.16",
46 | "tailwindcss": "^3.1.8"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ndpniraj/live-search-react/e6647b6a61a5de6462dcef3aa7dcac33d86ee7e4/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ndpniraj/live-search-react/e6647b6a61a5de6462dcef3aa7dcac33d86ee7e4/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ndpniraj/live-search-react/e6647b6a61a5de6462dcef3aa7dcac33d86ee7e4/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useState } from "react";
2 | import LiveSearch from "./LiveSearch";
3 |
4 | interface Props {}
5 | const profiles = [
6 | { id: "1", name: "Allie Grater" },
7 | { id: "2", name: "Aida Bugg" },
8 | { id: "3", name: "Gabrielle" },
9 | { id: "4", name: "Grace" },
10 | { id: "5", name: "Hannah" },
11 | { id: "6", name: "Heather" },
12 | { id: "7", name: "John Doe" },
13 | { id: "8", name: "Anne Teak" },
14 | { id: "9", name: "Audie Yose" },
15 | { id: "10", name: "Addie Minstra" },
16 | { id: "11", name: "Anne Ortha" },
17 | ];
18 |
19 | const App: FC = (props): JSX.Element => {
20 | const [results, setResults] = useState<{ id: string; name: string }[]>();
21 | const [selectedProfile, setSelectedProfile] = useState<{
22 | id: string;
23 | name: string;
24 | }>();
25 |
26 | type changeHandler = React.ChangeEventHandler;
27 | const handleChange: changeHandler = (e) => {
28 | const { target } = e;
29 | if (!target.value.trim()) return setResults([]);
30 |
31 | const filteredValue = profiles.filter((profile) =>
32 | profile.name.toLowerCase().startsWith(target.value)
33 | );
34 | setResults(filteredValue);
35 | };
36 | return (
37 | {item.name}
}
41 | onChange={handleChange}
42 | onSelect={(item) => setSelectedProfile(item)}
43 | />
44 | );
45 | };
46 |
47 | export default App;
48 |
--------------------------------------------------------------------------------
/src/LiveSearch.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useCallback, useEffect, useRef, useState } from "react";
2 |
3 | interface Props {
4 | results?: T[];
5 | renderItem(item: T): JSX.Element;
6 | onChange?: React.ChangeEventHandler;
7 | onSelect?: (item: T) => void;
8 | value?: string;
9 | }
10 |
11 | const LiveSearch = ({
12 | results = [],
13 | renderItem,
14 | value,
15 | onChange,
16 | onSelect,
17 | }: Props): JSX.Element => {
18 | const [focusedIndex, setFocusedIndex] = useState(-1);
19 | const resultContainer = useRef(null);
20 | const [showResults, setShowResults] = useState(false);
21 | const [defaultValue, setDefaultValue] = useState("");
22 |
23 | const handleSelection = (selectedIndex: number) => {
24 | const selectedItem = results[selectedIndex];
25 | if (!selectedItem) return resetSearchComplete();
26 | onSelect && onSelect(selectedItem);
27 | resetSearchComplete();
28 | };
29 |
30 | const resetSearchComplete = useCallback(() => {
31 | setFocusedIndex(-1);
32 | setShowResults(false);
33 | }, []);
34 |
35 | const handleKeyDown: React.KeyboardEventHandler = (e) => {
36 | const { key } = e;
37 | let nextIndexCount = 0;
38 |
39 | // move down
40 | if (key === "ArrowDown")
41 | nextIndexCount = (focusedIndex + 1) % results.length;
42 |
43 | // move up
44 | if (key === "ArrowUp")
45 | nextIndexCount = (focusedIndex + results.length - 1) % results.length;
46 |
47 | // hide search results
48 | if (key === "Escape") {
49 | resetSearchComplete();
50 | }
51 |
52 | // select the current item
53 | if (key === "Enter") {
54 | e.preventDefault();
55 | handleSelection(focusedIndex);
56 | }
57 |
58 | setFocusedIndex(nextIndexCount);
59 | };
60 |
61 | type changeHandler = React.ChangeEventHandler;
62 | const handleChange: changeHandler = (e) => {
63 | setDefaultValue(e.target.value);
64 | onChange && onChange(e);
65 | };
66 |
67 | useEffect(() => {
68 | if (!resultContainer.current) return;
69 |
70 | resultContainer.current.scrollIntoView({
71 | block: "center",
72 | });
73 | }, [focusedIndex]);
74 |
75 | useEffect(() => {
76 | if (results.length > 0 && !showResults) setShowResults(true);
77 |
78 | if (results.length <= 0) setShowResults(false);
79 | }, [results]);
80 |
81 | useEffect(() => {
82 | if (value) setDefaultValue(value);
83 | }, [value]);
84 |
85 | return (
86 |
87 |
93 |
100 |
101 | {/* Search Results Container */}
102 | {showResults && (
103 |
104 | {results.map((item, index) => {
105 | return (
106 |
handleSelection(index)}
109 | ref={index === focusedIndex ? resultContainer : null}
110 | style={{
111 | backgroundColor:
112 | index === focusedIndex ? "rgba(0,0,0,0.1)" : "",
113 | }}
114 | className="cursor-pointer hover:bg-black hover:bg-opacity-10 p-2"
115 | >
116 | {renderItem(item)}
117 |
118 | );
119 | })}
120 |
121 | )}
122 |
123 |
124 | );
125 | };
126 |
127 | export default LiveSearch;
128 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import "./index.css";
4 | import App from "./App";
5 |
6 | const root = ReactDOM.createRoot(
7 | document.getElementById("root") as HTMLElement
8 | );
9 | root.render(
10 |
11 |
12 |
13 | );
14 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: ["./src/**/*.{js,jsx,ts,tsx}"],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [],
8 | };
9 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------