Skip to content

Commit 188c6dc

Browse files
committed
feat(terracotta): add TerracottaSearch component
1 parent 5a5148d commit 188c6dc

1 file changed

Lines changed: 157 additions & 0 deletions

File tree

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

0 commit comments

Comments
 (0)