Skip to content

Commit dfa88d7

Browse files
committed
feat: new searchbars
1 parent 9335449 commit dfa88d7

File tree

3 files changed

+331
-154
lines changed

3 files changed

+331
-154
lines changed

src/components/BrutalistSearch.jsx

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import React, { useState, useEffect, useRef } from 'react';
2+
import { Link } from 'react-router-dom';
3+
import { MagnifyingGlassIcon, XCircleIcon, HashIcon } from '@phosphor-icons/react';
4+
import useSearchableData from '../hooks/useSearchableData';
5+
import { filterItems } from '../utils/search';
6+
7+
const categoryColorMap = {
8+
page: 'text-red-400 border-red-400/20 bg-red-400/5',
9+
command: 'text-amber-400 border-amber-400/20 bg-amber-400/5',
10+
post: 'text-blue-400 border-blue-400/20 bg-blue-400/5',
11+
project: 'text-orange-400 border-orange-400/20 bg-orange-400/5',
12+
log: 'text-rose-400 border-rose-400/20 bg-rose-400/5',
13+
app: 'text-teal-400 border-teal-400/20 bg-teal-400/5',
14+
story: 'text-violet-400 border-violet-400/20 bg-violet-400/5',
15+
notebook: 'text-lime-400 border-lime-400/20 bg-lime-400/5',
16+
};
17+
18+
const getCategoryStyle = (type) => {
19+
return categoryColorMap[type] || 'text-gray-400 border-white/10 bg-white/5';
20+
};
21+
22+
const BrutalistSearch = ({ isVisible, toggleSearch }) => {
23+
const [searchTerm, setSearchTerm] = useState('');
24+
const [searchResults, setSearchResults] = useState([]);
25+
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
26+
const { items, isLoading } = useSearchableData();
27+
const searchRef = useRef(null);
28+
const inputRef = useRef(null);
29+
30+
useEffect(() => {
31+
if (isVisible && inputRef.current) {
32+
inputRef.current.focus();
33+
}
34+
}, [isVisible]);
35+
36+
useEffect(() => {
37+
if (searchTerm) {
38+
const results = filterItems(items, searchTerm).slice(0, 8); // Limit results for clean look
39+
setSearchResults(results);
40+
setIsDropdownOpen(true);
41+
} else {
42+
setSearchResults([]);
43+
setIsDropdownOpen(false);
44+
}
45+
}, [searchTerm, items]);
46+
47+
useEffect(() => {
48+
const handleClickOutside = (event) => {
49+
if (searchRef.current && !searchRef.current.contains(event.target)) {
50+
setIsDropdownOpen(false);
51+
}
52+
};
53+
54+
document.addEventListener('mousedown', handleClickOutside);
55+
return () => {
56+
document.removeEventListener('mousedown', handleClickOutside);
57+
};
58+
}, []);
59+
60+
const getResultLink = (result) => {
61+
return result.path || '/';
62+
};
63+
64+
if (!isVisible) {
65+
return null;
66+
}
67+
68+
return (
69+
<div
70+
ref={searchRef}
71+
className="w-full bg-[#050505]/95 backdrop-blur-md py-6 px-6 border-b border-white/10 relative z-50"
72+
>
73+
<form
74+
onSubmit={(e) => e.preventDefault()}
75+
className="relative w-full max-w-2xl mx-auto"
76+
>
77+
<div className="relative group">
78+
<MagnifyingGlassIcon
79+
size={20}
80+
className={`absolute left-0 top-1/2 transform -translate-y-1/2 transition-colors ${searchTerm ? 'text-emerald-500' : 'text-gray-600 group-focus-within:text-emerald-500'}`}
81+
/>
82+
<input
83+
ref={inputRef}
84+
type="text"
85+
placeholder={isLoading ? 'SYNCHRONIZING REGISTRY...' : 'SEARCH...'}
86+
value={searchTerm}
87+
onChange={(e) => setSearchTerm(e.target.value)}
88+
onFocus={() => setIsDropdownOpen(true)}
89+
className="w-full bg-transparent text-white border-b border-white/10 py-3 pl-10 pr-10 focus:outline-none focus:border-emerald-500 transition-colors font-mono uppercase tracking-[0.2em] text-sm placeholder-gray-800"
90+
disabled={isLoading}
91+
/>
92+
{searchTerm && (
93+
<button
94+
type="button"
95+
onClick={() => setSearchTerm('')}
96+
className="absolute right-0 top-1/2 transform -translate-y-1/2 text-gray-600 hover:text-red-500 transition-colors"
97+
>
98+
<XCircleIcon size={20} weight="fill" />
99+
</button>
100+
)}
101+
</div>
102+
103+
{isDropdownOpen && searchResults.length > 0 && (
104+
<div className="absolute mt-4 w-full bg-[#0a0a0a] border border-white/10 shadow-2xl z-[100] left-0 overflow-hidden">
105+
<div className="flex flex-col">
106+
{searchResults.map((result, index) => (
107+
<Link
108+
key={`${result.slug || result.commandId}-${index}`}
109+
to={getResultLink(result)}
110+
onClick={() => {
111+
setSearchTerm('');
112+
setIsDropdownOpen(false);
113+
if (toggleSearch) toggleSearch();
114+
}}
115+
className="flex items-center justify-between px-6 py-4 border-b border-white/5 hover:bg-emerald-500/10 hover:border-l-4 hover:border-l-emerald-500 transition-all group"
116+
>
117+
<div className="flex flex-col gap-1">
118+
<span className="font-bold text-gray-300 group-hover:text-white uppercase font-mono tracking-tight text-sm truncate">
119+
{result.title}
120+
</span>
121+
{result.description && (
122+
<span className="text-[9px] font-mono uppercase tracking-widest text-gray-600 group-hover:text-gray-400 line-clamp-1">
123+
{result.description}
124+
</span>
125+
)}
126+
</div>
127+
128+
<div className="flex items-center gap-3">
129+
<span className={`px-2 py-0.5 text-[8px] font-mono font-black uppercase tracking-widest border rounded-sm ${getCategoryStyle(result.type)}`}>
130+
{result.type}
131+
</span>
132+
<HashIcon size={14} className="text-gray-800 group-hover:text-emerald-500 transition-colors" />
133+
</div>
134+
</Link>
135+
))}
136+
</div>
137+
<div className="bg-white/[0.02] px-6 py-2 border-t border-white/5 flex justify-between items-center">
138+
<span className="text-[8px] font-mono text-gray-700 uppercase tracking-widest">
139+
Showing top {searchResults.length} matches
140+
</span>
141+
<span className="text-[8px] font-mono text-gray-700 uppercase tracking-widest">
142+
Fezcodex_Search_v2
143+
</span>
144+
</div>
145+
</div>
146+
)}
147+
148+
{isDropdownOpen && searchTerm && searchResults.length === 0 && (
149+
<div className="absolute mt-4 w-full bg-[#0a0a0a] border border-white/10 p-8 text-center shadow-2xl z-[100] left-0">
150+
<p className="font-mono text-xs text-gray-600 uppercase tracking-[0.3em]">
151+
No_Results_Found_For: <span className="text-red-500">{searchTerm}</span>
152+
</p>
153+
</div>
154+
)}
155+
</form>
156+
</div>
157+
);
158+
};
159+
160+
export default BrutalistSearch;

src/components/LuxeSearch.jsx

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import React, { useState, useEffect, useRef } from 'react';
2+
import { Link } from 'react-router-dom';
3+
import { MagnifyingGlassIcon, XCircleIcon, ArrowRightIcon } from '@phosphor-icons/react';
4+
import useSearchableData from '../hooks/useSearchableData';
5+
import { filterItems } from '../utils/search';
6+
7+
const luxeCategoryColors = {
8+
page: 'text-red-600 bg-red-50',
9+
command: 'text-amber-600 bg-amber-50',
10+
post: 'text-blue-600 bg-blue-50',
11+
project: 'text-orange-600 bg-orange-50',
12+
log: 'text-rose-600 bg-rose-50',
13+
app: 'text-teal-600 bg-teal-50',
14+
story: 'text-violet-600 bg-violet-50',
15+
notebook: 'text-lime-600 bg-lime-50',
16+
};
17+
18+
const getCategoryStyle = (type) => {
19+
return luxeCategoryColors[type] || 'text-gray-600 bg-gray-50';
20+
};
21+
22+
const LuxeSearch = ({ isVisible, toggleSearch }) => {
23+
const [searchTerm, setSearchTerm] = useState('');
24+
const [searchResults, setSearchResults] = useState([]);
25+
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
26+
const { items, isLoading } = useSearchableData();
27+
const searchRef = useRef(null);
28+
const inputRef = useRef(null);
29+
30+
useEffect(() => {
31+
if (isVisible && inputRef.current) {
32+
inputRef.current.focus();
33+
}
34+
}, [isVisible]);
35+
36+
useEffect(() => {
37+
if (searchTerm) {
38+
const results = filterItems(items, searchTerm).slice(0, 8);
39+
setSearchResults(results);
40+
setIsDropdownOpen(true);
41+
} else {
42+
setSearchResults([]);
43+
setIsDropdownOpen(false);
44+
}
45+
}, [searchTerm, items]);
46+
47+
useEffect(() => {
48+
const handleClickOutside = (event) => {
49+
if (searchRef.current && !searchRef.current.contains(event.target)) {
50+
setIsDropdownOpen(false);
51+
}
52+
};
53+
54+
document.addEventListener('mousedown', handleClickOutside);
55+
return () => {
56+
document.removeEventListener('mousedown', handleClickOutside);
57+
};
58+
}, []);
59+
60+
const getResultLink = (result) => {
61+
return result.path || '/';
62+
};
63+
64+
if (!isVisible) {
65+
return null;
66+
}
67+
68+
return (
69+
<div
70+
ref={searchRef}
71+
className="w-full bg-[#F5F5F0]/95 backdrop-blur-md py-8 px-6 border-b border-[#1A1A1A]/5 relative z-50 shadow-sm"
72+
>
73+
<form
74+
onSubmit={(e) => e.preventDefault()}
75+
className="relative w-full max-w-3xl mx-auto"
76+
>
77+
<div className="relative group">
78+
<MagnifyingGlassIcon
79+
size={24}
80+
weight="light"
81+
className={`absolute left-0 top-1/2 transform -translate-y-1/2 transition-colors ${searchTerm ? 'text-[#8D4004]' : 'text-[#1A1A1A]/20 group-focus-within:text-[#8D4004]'}`}
82+
/>
83+
<input
84+
ref={inputRef}
85+
type="text"
86+
placeholder={isLoading ? 'Synchronizing Registry...' : 'Search the archive...'}
87+
value={searchTerm}
88+
onChange={(e) => setSearchTerm(e.target.value)}
89+
onFocus={() => setIsDropdownOpen(true)}
90+
className="w-full bg-transparent text-[#1A1A1A] border-b border-[#1A1A1A]/10 py-4 pl-12 pr-12 focus:outline-none focus:border-[#8D4004] transition-all font-playfairDisplay italic text-xl md:text-2xl placeholder-[#1A1A1A]/10"
91+
disabled={isLoading}
92+
/>
93+
{searchTerm && (
94+
<button
95+
type="button"
96+
onClick={() => setSearchTerm('')}
97+
className="absolute right-0 top-1/2 transform -translate-y-1/2 text-[#1A1A1A]/20 hover:text-red-500 transition-colors"
98+
>
99+
<XCircleIcon size={24} weight="light" />
100+
</button>
101+
)}
102+
</div>
103+
104+
{isDropdownOpen && searchResults.length > 0 && (
105+
<div className="absolute mt-6 w-full bg-white border border-[#1A1A1A]/5 shadow-[0_30px_100px_-20px_rgba(0,0,0,0.1)] z-[100] left-0 overflow-hidden rounded-sm">
106+
<div className="flex flex-col">
107+
{searchResults.map((result, index) => (
108+
<Link
109+
key={`${result.slug || result.commandId}-${index}`}
110+
to={getResultLink(result)}
111+
onClick={() => {
112+
setSearchTerm('');
113+
setIsDropdownOpen(false);
114+
if (toggleSearch) toggleSearch();
115+
}}
116+
className="flex items-center justify-between px-8 py-5 border-b border-[#1A1A1A]/5 hover:bg-[#F5F5F0] transition-all group"
117+
>
118+
<div className="flex flex-col gap-1">
119+
<span className="font-playfairDisplay text-lg italic text-[#1A1A1A]/80 group-hover:text-[#1A1A1A] transition-colors truncate">
120+
{result.title}
121+
</span>
122+
{result.description && (
123+
<span className="text-[10px] font-outfit uppercase tracking-[0.2em] text-[#1A1A1A]/30 group-hover:text-[#1A1A1A]/50 transition-colors line-clamp-1">
124+
{result.description}
125+
</span>
126+
)}
127+
</div>
128+
129+
<div className="flex items-center gap-6">
130+
<span className={`px-2 py-0.5 text-[9px] font-outfit font-bold uppercase tracking-widest rounded-sm shadow-sm ${getCategoryStyle(result.type)}`}>
131+
{result.type}
132+
</span>
133+
<ArrowRightIcon size={16} className="text-[#1A1A1A]/10 group-hover:text-[#8D4004] group-hover:translate-x-1 transition-all" />
134+
</div>
135+
</Link>
136+
))}
137+
</div>
138+
<div className="bg-[#FAFAF8] px-8 py-3 border-t border-[#1A1A1A]/5 flex justify-between items-center">
139+
<span className="text-[9px] font-outfit text-[#1A1A1A]/20 uppercase tracking-[0.2em]">
140+
{searchResults.length} entries matching
141+
</span>
142+
<span className="text-[9px] font-playfairDisplay italic text-[#1A1A1A]/20">
143+
Fezcodex Architectural Search
144+
</span>
145+
</div>
146+
</div>
147+
)}
148+
149+
{isDropdownOpen && searchTerm && searchResults.length === 0 && (
150+
<div className="absolute mt-6 w-full bg-white border border-[#1A1A1A]/5 p-12 text-center shadow-xl z-[100] left-0 rounded-sm">
151+
<p className="font-playfairDisplay italic text-lg text-[#1A1A1A]/20">
152+
No archive records found for: <span className="text-[#8D4004] opacity-100">{searchTerm}</span>
153+
</p>
154+
</div>
155+
)}
156+
</form>
157+
</div>
158+
);
159+
};
160+
161+
export default LuxeSearch;

0 commit comments

Comments
 (0)