Reuben_OS / app /components /SpotlightSearch.tsx
Reubencf's picture
Fix quiz system: separate answers, backward compat, and bug fixes
7f6d612
'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>
)
}