Skip to content

Commit 35ad85c

Browse files
committed
feat: luxe command palette
1 parent 9c4d151 commit 35ad85c

File tree

3 files changed

+585
-288
lines changed

3 files changed

+585
-288
lines changed
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
import React, { useState, useEffect, useRef, useCallback } from 'react';
2+
import { useNavigate } from 'react-router-dom';
3+
import { AnimatePresence, motion } from 'framer-motion';
4+
import useSearchableData from '../hooks/useSearchableData';
5+
import { useAchievements } from '../context/AchievementContext';
6+
import { TerminalWindowIcon } from '@phosphor-icons/react';
7+
import { useCommandPalette } from '../context/CommandPaletteContext';
8+
import { filterItems } from '../utils/search';
9+
import { useCommandRegistry } from '../hooks/useCommandRegistry';
10+
11+
const categoryColorMap = {
12+
page: 'bg-red-400',
13+
command: 'bg-amber-400',
14+
post: 'bg-blue-400',
15+
project: 'bg-orange-400',
16+
log: 'bg-rose-400',
17+
app: 'bg-teal-400',
18+
story: 'bg-violet-400',
19+
notebook: 'bg-lime-400',
20+
};
21+
22+
const getCategoryColorClass = (type) => {
23+
return categoryColorMap[type] || 'bg-gray-500';
24+
};
25+
26+
const BrutalistCommandPalette = ({
27+
isOpen,
28+
setIsOpen,
29+
openGenericModal,
30+
toggleDigitalRain,
31+
toggleBSOD,
32+
}) => {
33+
const [searchTerm, setSearchTerm] = useState('');
34+
const [selectedIndex, setSelectedIndex] = useState(0);
35+
const { items, isLoading } = useSearchableData();
36+
const navigate = useNavigate();
37+
const inputRef = useRef(null);
38+
const resultsRef = useRef(null);
39+
40+
const { unlockAchievement } = useAchievements();
41+
const { setTriggerCommand } = useCommandPalette();
42+
43+
const { executeCommand } = useCommandRegistry({
44+
openGenericModal,
45+
toggleDigitalRain,
46+
toggleBSOD,
47+
items,
48+
});
49+
50+
const filteredItems = filterItems(items, searchTerm);
51+
52+
useEffect(() => {
53+
if (isOpen) {
54+
inputRef.current?.focus();
55+
unlockAchievement('the_hacker');
56+
}
57+
}, [isOpen, unlockAchievement]);
58+
59+
useEffect(() => {
60+
const lowerTerm = searchTerm.toLowerCase();
61+
if (lowerTerm === 'hello?' || lowerTerm === 'is anyone there?') {
62+
unlockAchievement('echo_in_the_void');
63+
}
64+
if (lowerTerm === 'command palette' || lowerTerm === 'the hacker') {
65+
unlockAchievement('the_paradox');
66+
}
67+
if (lowerTerm === '0028:c0de1337') {
68+
unlockAchievement('code_1337');
69+
}
70+
}, [searchTerm, unlockAchievement]);
71+
72+
useEffect(() => {
73+
setSelectedIndex(0);
74+
}, [searchTerm, items]);
75+
76+
const handleClose = useCallback(() => {
77+
setIsOpen(false);
78+
setSearchTerm('');
79+
}, [setIsOpen]);
80+
81+
const triggerBSOD = useCallback(() => {
82+
unlockAchievement('bsod');
83+
toggleBSOD();
84+
}, [unlockAchievement, toggleBSOD]);
85+
86+
useEffect(() => {
87+
setTriggerCommand(() => executeCommand);
88+
}, [setTriggerCommand, executeCommand]);
89+
90+
const handleItemClick = useCallback(
91+
(item) => {
92+
if (!item) return;
93+
94+
if (item.type === 'command') {
95+
executeCommand(item.commandId);
96+
} else {
97+
navigate(item.path);
98+
}
99+
handleClose();
100+
},
101+
[executeCommand, navigate, handleClose],
102+
);
103+
104+
useEffect(() => {
105+
const handleKeyDown = (event) => {
106+
if (!isOpen) return;
107+
108+
if (event.key === 'ArrowUp') {
109+
event.preventDefault();
110+
setSelectedIndex((prevIndex) =>
111+
prevIndex === 0 ? filteredItems.length - 1 : prevIndex - 1,
112+
);
113+
} else if (event.key === 'ArrowDown') {
114+
event.preventDefault();
115+
setSelectedIndex((prevIndex) =>
116+
prevIndex === filteredItems.length - 1 ? 0 : prevIndex + 1,
117+
);
118+
} else if (event.key === 'Enter') {
119+
event.preventDefault();
120+
if (searchTerm.toLowerCase() === 'bsod') {
121+
triggerBSOD();
122+
handleClose();
123+
} else if (filteredItems[selectedIndex]) {
124+
handleItemClick(filteredItems[selectedIndex]);
125+
}
126+
} else if (event.key === 'Escape') {
127+
handleClose();
128+
} else if (event.key === 'PageUp') {
129+
event.preventDefault();
130+
setSelectedIndex((prevIndex) =>
131+
prevIndex <= 0 ? filteredItems.length - 1 : prevIndex - 10,
132+
);
133+
} else if (event.key === 'PageDown') {
134+
event.preventDefault();
135+
setSelectedIndex((prevIndex) =>
136+
prevIndex >= filteredItems.length - 10 ? 0 : prevIndex + 10,
137+
);
138+
}
139+
};
140+
141+
window.addEventListener('keydown', handleKeyDown);
142+
return () => window.removeEventListener('keydown', handleKeyDown);
143+
}, [
144+
isOpen,
145+
filteredItems,
146+
selectedIndex,
147+
searchTerm,
148+
triggerBSOD,
149+
handleItemClick,
150+
handleClose,
151+
]);
152+
153+
useEffect(() => {
154+
const selectedItem = resultsRef.current?.children[selectedIndex];
155+
if (selectedItem) {
156+
selectedItem.scrollIntoView({
157+
block: 'nearest',
158+
});
159+
}
160+
}, [selectedIndex]);
161+
162+
return (
163+
<AnimatePresence>
164+
{isOpen && (
165+
<div
166+
className="fixed inset-0 bg-black/80 backdrop-blur-sm z-[1000] flex items-start justify-center pt-16 md:pt-32 "
167+
onClick={handleClose}
168+
>
169+
<motion.div
170+
initial={{ opacity: 0, scale: 0.98, y: -10 }}
171+
animate={{ opacity: 1, scale: 1, y: 0 }}
172+
exit={{ opacity: 0, scale: 0.98, y: -10 }}
173+
transition={{ duration: 0.2, ease: 'easeOut' }}
174+
className="bg-[#050505] text-white rounded-sm border border-white/10 shadow-2xl w-full max-w-2xl mx-4 overflow-hidden"
175+
onClick={(e) => e.stopPropagation()}
176+
>
177+
{/* Header / Search */}
178+
<div className="p-6 flex items-center gap-4 bg-white/[0.02]">
179+
<TerminalWindowIcon
180+
size={32}
181+
weight="fill"
182+
className="text-emerald-500"
183+
/>
184+
<input
185+
ref={inputRef}
186+
type="text"
187+
placeholder={
188+
isLoading
189+
? 'Initialising Data...'
190+
: 'Search registry...'
191+
}
192+
className="w-full bg-transparent text-xl md:text-2xl font-light placeholder-gray-700 focus:outline-none font-mono uppercase tracking-tight"
193+
value={searchTerm}
194+
onChange={(e) => setSearchTerm(e.target.value)}
195+
disabled={isLoading}
196+
/>
197+
</div>
198+
199+
{/* Results Section */}
200+
<div
201+
ref={resultsRef}
202+
className="border-t border-white/10 max-h-[50vh] overflow-y-auto scrollbar-hide bg-black/20"
203+
>
204+
{filteredItems.length > 0 ? (
205+
filteredItems.map((item, index) => (
206+
<div
207+
key={`${item.type}-${item.slug || item.commandId}-${index}`}
208+
className={`px-6 py-4 cursor-pointer flex justify-between items-center transition-all duration-200 border-l-2 ${
209+
selectedIndex === index
210+
? 'bg-white/10 border-emerald-500 translate-x-1'
211+
: 'hover:bg-white/5 border-transparent'
212+
}`}
213+
onClick={() => handleItemClick(item)}
214+
onMouseMove={() => setSelectedIndex(index)}
215+
>
216+
<div className="flex flex-col gap-0.5">
217+
<span
218+
className={`text-lg font-medium tracking-tight ${selectedIndex === index ? 'text-white' : 'text-gray-400'}`}
219+
>
220+
{item.title}
221+
</span>
222+
{item.description && (
223+
<span className="text-[10px] font-mono uppercase tracking-widest text-gray-600 truncate max-w-md">
224+
{item.description}
225+
</span>
226+
)}
227+
</div>
228+
229+
<span
230+
className={`px-2 py-0.5 text-[9px] font-mono font-bold uppercase tracking-widest text-black ${getCategoryColorClass(item.type)} rounded-sm`}
231+
>
232+
{item.type}
233+
</span>
234+
</div>
235+
))
236+
) : (
237+
<div className="p-12 text-center">
238+
<p className="font-mono text-sm text-gray-600 uppercase tracking-widest">
239+
{isLoading
240+
? 'Fetching Registry...'
241+
: `No matches found for "${searchTerm}"`}
242+
</p>
243+
</div>
244+
)}
245+
</div>
246+
247+
{/* Footer / Shortcuts */}
248+
<div className="border-t border-white/10 p-4 bg-white/[0.02] flex items-center justify-between text-[10px] font-mono uppercase tracking-[0.2em]">
249+
<div className="flex items-center gap-6 text-gray-500">
250+
<div className="flex items-center gap-2">
251+
<kbd className="px-1.5 py-0.5 bg-white/10 rounded border border-white/10 text-white">
252+
ESC
253+
</kbd>
254+
<span>Close</span>
255+
</div>
256+
<div className="flex items-center gap-2">
257+
<div className="flex gap-1">
258+
<kbd className="px-1.5 py-0.5 bg-white/10 rounded border border-white/10 text-white">
259+
260+
</kbd>
261+
<kbd className="px-1.5 py-0.5 bg-white/10 rounded border border-white/10 text-white">
262+
263+
</kbd>
264+
</div>
265+
<span>Navigate</span>
266+
</div>
267+
<div className="flex items-center gap-2">
268+
<kbd className="px-1.5 py-0.5 bg-white/10 rounded border border-white/10 text-white">
269+
270+
</kbd>
271+
<span>Select</span>
272+
</div>
273+
</div>
274+
275+
<div className="font-bold text-gray-400 flex items-center gap-2">
276+
<span className="h-1 w-1 bg-emerald-500 rounded-full animate-pulse" />
277+
Fez<span className="text-white">codex</span>
278+
</div>
279+
</div>
280+
</motion.div>
281+
</div>
282+
)}
283+
</AnimatePresence>
284+
);
285+
};
286+
287+
export default BrutalistCommandPalette;

0 commit comments

Comments
 (0)