Spaces:
Running
Running
| 'use client' | |
| import React, { useState, useRef, useEffect } from 'react' | |
| import { | |
| MagnifyingGlass, | |
| X, | |
| Folder, | |
| Calendar, | |
| Clock, | |
| Sparkle, | |
| Globe, | |
| DeviceMobile, | |
| Function as FunctionIcon, | |
| Brain | |
| } from '@phosphor-icons/react' | |
| import { motion, AnimatePresence } from 'framer-motion' | |
| interface SpotlightSearchProps { | |
| isOpen: boolean | |
| onClose: () => void | |
| onOpenApp: (appId: string) => void | |
| } | |
| interface SearchResult { | |
| id: string | |
| name: string | |
| type: 'app' | 'file' | 'setting' | |
| icon?: React.ReactNode | |
| } | |
| export function SpotlightSearch({ isOpen, onClose, onOpenApp }: SpotlightSearchProps) { | |
| const [query, setQuery] = useState('') | |
| const [results, setResults] = useState<SearchResult[]>([]) | |
| const inputRef = useRef<HTMLInputElement>(null) | |
| const apps: SearchResult[] = [ | |
| { id: 'files', name: 'Files', type: 'app', icon: <div className="w-6 h-6 bg-gradient-to-br from-blue-400 to-cyan-200 rounded-md flex items-center justify-center border border-white/30"><Folder size={16} weight="fill" className="text-blue-900" /></div> }, | |
| { id: 'calendar', name: 'Calendar', type: 'app', icon: <div className="w-6 h-6 bg-white rounded-md flex items-center justify-center border border-gray-200"><Calendar size={16} weight="regular" className="text-red-500" /></div> }, | |
| { id: 'clock', name: 'Clock', type: 'app', icon: <div className="w-6 h-6 bg-white rounded-full flex items-center justify-center border border-gray-200"><Clock size={16} weight="regular" className="text-black" /></div> }, | |
| { id: 'gemini', name: 'Gemini Chat', type: 'app', icon: <div className="w-6 h-6 bg-gradient-to-b from-white to-blue-50 rounded-md flex items-center justify-center border border-white/50"><Sparkle size={16} weight="fill" className="text-blue-500" /></div> }, | |
| { id: 'flutter-editor', name: 'Flutter IDE', type: 'app', icon: <div className="w-6 h-6 bg-gradient-to-b from-[#54C5F8] to-[#29B6F6] rounded-md flex items-center justify-center border border-white/20"><DeviceMobile size={16} weight="fill" className="text-white" /></div> }, | |
| { id: 'latex-editor', name: 'LaTeX Studio', type: 'app', icon: <div className="w-6 h-6 bg-gradient-to-b from-slate-700 to-slate-900 rounded-md flex items-center justify-center border border-white/20"><FunctionIcon size={16} weight="bold" className="text-green-400" /></div> }, | |
| { id: 'quiz', name: 'Quiz Master', type: 'app', icon: <div className="w-6 h-6 bg-gradient-to-br from-teal-400 to-emerald-500 rounded-md flex items-center justify-center border border-white/20"><Brain size={16} weight="fill" className="text-white" /></div> }, | |
| ] | |
| const files: SearchResult[] = [ | |
| { id: 'document', name: 'Document.pdf', type: 'file' }, | |
| { id: 'resume', name: 'Resume.docx', type: 'file' }, | |
| { id: 'budget', name: 'Budget.xlsx', type: 'file' }, | |
| { id: 'presentation', name: 'Presentation.pptx', type: 'file' }, | |
| ] | |
| const settings: SearchResult[] = [ | |
| { id: 'wallpaper', name: 'Change Wallpaper', type: 'setting' }, | |
| { id: 'theme', name: 'Dark Mode', type: 'setting' }, | |
| { id: 'display', name: 'Display Settings', type: 'setting' }, | |
| ] | |
| const allItems = [...apps, ...files, ...settings] | |
| useEffect(() => { | |
| if (isOpen) { | |
| setQuery('') | |
| setResults([]) | |
| setTimeout(() => inputRef.current?.focus(), 100) | |
| } | |
| }, [isOpen]) | |
| useEffect(() => { | |
| if (query.trim() === '') { | |
| setResults([]) | |
| return | |
| } | |
| const filtered = allItems.filter(item => | |
| item.name.toLowerCase().includes(query.toLowerCase()) | |
| ) | |
| setResults(filtered.slice(0, 8)) | |
| }, [query]) | |
| const handleSelect = (result: SearchResult) => { | |
| if (result.type === 'app') { | |
| onOpenApp(result.id) | |
| } | |
| onClose() | |
| setQuery('') | |
| } | |
| const handleKeyDown = (e: React.KeyboardEvent) => { | |
| if (e.key === 'Escape') { | |
| onClose() | |
| } else if (e.key === 'Enter' && results.length > 0) { | |
| handleSelect(results[0]) | |
| } | |
| } | |
| return ( | |
| <AnimatePresence> | |
| {isOpen && ( | |
| <> | |
| {/* Backdrop */} | |
| <motion.div | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| exit={{ opacity: 0 }} | |
| className="fixed inset-0 bg-black/20 z-[9998]" | |
| onClick={onClose} | |
| /> | |
| {/* Search Box */} | |
| <motion.div | |
| initial={{ opacity: 0, scale: 0.95, y: -20 }} | |
| animate={{ opacity: 1, scale: 1, y: 0 }} | |
| exit={{ opacity: 0, scale: 0.95, y: -20 }} | |
| transition={{ duration: 0.15, ease: 'easeOut' }} | |
| className="fixed top-[15%] sm:top-[20%] left-1/2 -translate-x-1/2 w-[95vw] sm:w-[600px] max-w-[600px] glass rounded-xl shadow-2xl z-[9999]" | |
| > | |
| <div className="flex items-center px-3 sm:px-4 py-2.5 sm:py-3 gap-2 sm:gap-3 border-b border-gray-200/20"> | |
| <MagnifyingGlass size={20} weight="regular" className="text-gray-600 sm:w-6 sm:h-6 flex-shrink-0" /> | |
| <input | |
| ref={inputRef} | |
| type="text" | |
| value={query} | |
| onChange={(e) => setQuery(e.target.value)} | |
| onKeyDown={handleKeyDown} | |
| className="flex-1 bg-transparent text-base sm:text-xl focus:outline-none text-gray-800 placeholder-gray-400 min-w-0" | |
| placeholder="Spotlight Search" | |
| autoComplete="off" | |
| spellCheck={false} | |
| /> | |
| <button | |
| onClick={onClose} | |
| className="text-gray-500 hover:text-gray-700 transition-colors flex-shrink-0" | |
| > | |
| <X size={18} weight="regular" className="sm:w-5 sm:h-5" /> | |
| </button> | |
| </div> | |
| {/* Results */} | |
| {results.length > 0 && ( | |
| <div className="max-h-64 sm:max-h-96 overflow-y-auto p-1.5 sm:p-2"> | |
| {results.map((result, index) => ( | |
| <div | |
| key={result.id} | |
| onClick={() => handleSelect(result)} | |
| className={`flex items-center px-2 sm:px-3 py-2 sm:py-2.5 hover:bg-blue-500 hover:text-white rounded-lg cursor-pointer transition-colors gap-2 sm:gap-3 ${index === 0 ? 'bg-blue-500/10' : '' | |
| }`} | |
| > | |
| {result.icon && ( | |
| <div className="w-7 h-7 sm:w-8 sm:h-8 flex items-center justify-center flex-shrink-0"> | |
| {result.icon} | |
| </div> | |
| )} | |
| <div className="flex-1 flex flex-col min-w-0"> | |
| <span className="font-medium text-sm sm:text-base truncate">{result.name}</span> | |
| <span className="text-[10px] sm:text-xs opacity-70 capitalize">{result.type}</span> | |
| </div> | |
| {result.type === 'app' && ( | |
| <span className="text-[10px] sm:text-xs opacity-50 hidden xs:inline flex-shrink-0">⌘ Enter</span> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| {/* No results */} | |
| {query.trim() !== '' && results.length === 0 && ( | |
| <div className="p-3 sm:p-4 text-center text-gray-500 text-sm sm:text-base"> | |
| No results found for "{query}" | |
| </div> | |
| )} | |
| {/* Quick Actions (when no query) */} | |
| {query === '' && ( | |
| <div className="p-3 sm:p-4"> | |
| <div className="text-[10px] sm:text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2"> | |
| Suggestions | |
| </div> | |
| <div className="grid grid-cols-2 gap-1.5 sm:gap-2"> | |
| {apps.slice(0, 4).map(app => ( | |
| <div | |
| key={app.id} | |
| onClick={() => handleSelect(app)} | |
| className="flex items-center px-2 sm:px-3 py-1.5 sm:py-2 hover:bg-gray-100 rounded-lg cursor-pointer transition-colors" | |
| > | |
| <span className="text-xs sm:text-sm truncate">{app.name}</span> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| </motion.div> | |
| </> | |
| )} | |
| </AnimatePresence> | |
| ) | |
| } |