Skip to content

Commit 24d01f9

Browse files
Merge pull request #53 from Srijoy2007/feature/search-ui
Added responsive searchbar with debounce integration in components/Ho…
2 parents 6897dc7 + dbefa58 commit 24d01f9

File tree

4 files changed

+200
-0
lines changed

4 files changed

+200
-0
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
2+
import { useState } from "react";
3+
import { Search, X } from "lucide-react";
4+
import { useSearch } from "./useSearch";
5+
import type { SearchItemData } from "./types";
6+
import SearchItem from "./SearchItem";
7+
8+
interface Props {
9+
onSearch: (query: string) => Promise<SearchItemData[]>;
10+
}
11+
12+
export default function SearchBar({ onSearch }: Props) {
13+
const [open, setOpen] = useState(false);
14+
15+
const [recent, setRecent] = useState<SearchItemData[]>([
16+
{ id: 1, name: "Mayra S.", meta: "128k Followers" },
17+
{ id: 2, name: "Vihan Singh", meta: "112k Followers" },
18+
]);
19+
20+
const { query, setQuery, results, loading } = useSearch(onSearch);
21+
22+
const handleSelect = (item: SearchItemData) => {
23+
setRecent((prev) =>
24+
[item, ...prev.filter((i) => i.id !== item.id)].slice(0, 5)
25+
);
26+
setOpen(false);
27+
setQuery("");
28+
};
29+
30+
return (
31+
<div className="relative w-full max-w-lg">
32+
<div
33+
onClick={() => setOpen(true)}
34+
className="flex items-center gap-2 px-4 py-2.5 rounded-xl
35+
bg-[#0b1330] border border-white/10
36+
focus-within:ring-2 focus-within:ring-blue-500/70
37+
transition"
38+
>
39+
<Search className="w-4 h-4 text-white/60" />
40+
41+
<input
42+
value={query}
43+
onChange={(e) => setQuery(e.target.value)}
44+
placeholder="Search for something..."
45+
className="flex-1 bg-transparent outline-none
46+
text-sm text-white placeholder-white/50"
47+
/>
48+
49+
{query && (
50+
<button
51+
type="button"
52+
onClick={() => setQuery("")}
53+
className="text-white/50 hover:text-white transition"
54+
>
55+
<X className="w-4 h-4" />
56+
</button>
57+
)}
58+
</div>
59+
{open && (
60+
<div
61+
className="absolute z-50 mt-2 w-full rounded-2xl
62+
bg-[#0a1130] border border-white/10
63+
shadow-2xl overflow-hidden"
64+
>
65+
<div
66+
className="flex items-center justify-between px-4 py-3
67+
border-b border-white/10"
68+
>
69+
<span className="text-xs font-medium text-white/70">
70+
{query ? "Search results" : "Recent"}
71+
</span>
72+
73+
{!query && recent.length > 0 && (
74+
<button
75+
type="button"
76+
onClick={() => setRecent([])}
77+
className="text-white/40 hover:text-white transition"
78+
title="Clear all"
79+
>
80+
<X className="w-4 h-4" />
81+
</button>
82+
)}
83+
</div>
84+
<div className="p-2 max-h-72 overflow-y-auto">
85+
{loading && (
86+
<p className="text-center text-xs text-white/50 py-4">
87+
Searching…
88+
</p>
89+
)}
90+
91+
{!loading && query && results.length === 0 && (
92+
<p className="text-center text-xs text-white/40 py-4">
93+
No results found
94+
</p>
95+
)}
96+
97+
{!query &&
98+
recent.map((item) => (
99+
<SearchItem
100+
key={item.id}
101+
item={item}
102+
onSelect={handleSelect}
103+
/>
104+
))}
105+
106+
{query &&
107+
results.map((item) => (
108+
<SearchItem
109+
key={item.id}
110+
item={item}
111+
onSelect={handleSelect}
112+
/>
113+
))}
114+
</div>
115+
</div>
116+
)}
117+
</div>
118+
);
119+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
2+
import { X } from "lucide-react";
3+
import type { SearchItemData } from "./types";
4+
5+
interface Props {
6+
item: SearchItemData;
7+
onSelect: (item: SearchItemData) => void;
8+
}
9+
10+
export default function SearchItem({ item, onSelect }: Props) {
11+
return (
12+
<button
13+
type="button"
14+
onClick={() => onSelect(item)}
15+
className="group w-full flex items-center gap-3 px-3 py-2 rounded-lg
16+
text-left transition
17+
hover:bg-white/5 active:bg-white/10"
18+
>
19+
<div
20+
className="w-9 h-9 shrink-0 rounded-full
21+
bg-gradient-to-br from-blue-500/30 to-purple-500/30
22+
flex items-center justify-center
23+
text-sm font-semibold text-white"
24+
>
25+
{item.name[0]}
26+
</div>
27+
<div className="flex-1 min-w-0">
28+
<p className="text-sm text-white truncate">{item.name}</p>
29+
{item.meta && (
30+
<p className="text-xs text-white/60 truncate">{item.meta}</p>
31+
)}
32+
</div>
33+
<X className="w-4 h-4 text-white/20 group-hover:text-white/40 transition" />
34+
</button>
35+
);
36+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export interface SearchItemData {
2+
id: string | number;
3+
name: string;
4+
meta?: string;
5+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
2+
import { useEffect, useState } from "react";
3+
import type {SearchItemData} from "./types.ts";
4+
export function useSearch(
5+
searchFn: (query: string) => Promise<SearchItemData[]>,
6+
delay = 300
7+
) {
8+
const [query, setQuery] = useState<string>("");
9+
const [results, setResults] = useState<SearchItemData[]>([]);
10+
const [loading, setLoading] = useState<boolean>(false);
11+
12+
useEffect(() => {
13+
if (!query.trim()) {
14+
setResults([]);
15+
return;
16+
}
17+
18+
const timer = setTimeout(async () => {
19+
setLoading(true);
20+
try {
21+
const data = await searchFn(query);
22+
setResults(data ?? []);
23+
} catch (err) {
24+
console.error("Search failed:", err);
25+
setResults([]);
26+
} finally {
27+
setLoading(false);
28+
}
29+
}, delay);
30+
31+
return () => clearTimeout(timer);
32+
}, [query, searchFn, delay]);
33+
34+
return {
35+
query,
36+
setQuery,
37+
results,
38+
loading,
39+
};
40+
}

0 commit comments

Comments
 (0)