import { motion, AnimatePresence } from "framer-motion"; import { useEffect, useMemo, useRef, useState } from "react"; import { Search, X, BookOpen, Zap, Target, Trophy, User, Home, ArrowRight, Clock, Flame, } from "lucide-react"; import type { PracticeSheet } from "../types/sheet"; import { useNavigate } from "react-router-dom"; import type { SearchItem } from "../types/search"; import { formatGroupTitle } from "../lib/utils"; interface Props { sheets: PracticeSheet[]; onClose: () => void; searchQuery: string; setSearchQuery: (value: string) => void; } // ─── Nav items ──────────────────────────────────────────────────────────────── const NAV_ITEMS: (SearchItem & { icon: React.ElementType; color: string; bg: string; })[] = [ { type: "route", title: "Hard Test Modules", description: "Tackle the hardest SAT questions", route: "/student/hard-test-modules", group: "Pages", icon: Trophy, color: "#84cc16", bg: "#f7ffe4", }, { type: "route", title: "Targeted Practice", description: "Focus on your weak spots", route: "/student/practice/targeted-practice", group: "Pages", icon: Target, color: "#ef4444", bg: "#fff5f5", }, { type: "route", title: "Drills", description: "Train speed and accuracy", route: "/student/practice/drills", group: "Pages", icon: Zap, color: "#0891b2", bg: "#ecfeff", }, { type: "route", title: "Leaderboard", description: "See how you rank against others", route: "/student/rewards", group: "Pages", icon: Trophy, color: "#f97316", bg: "#fff7ed", }, { type: "route", title: "Practice", description: "Browse all practice modes", route: "/student/practice", group: "Pages", icon: BookOpen, color: "#a855f7", bg: "#fdf4ff", }, { type: "route", title: "Lessons", description: "Watch expert SAT technique lessons", route: "/student/lessons", group: "Pages", icon: BookOpen, color: "#0891b2", bg: "#ecfeff", }, { type: "route", title: "Profile", description: "View your profile and settings", route: "/student/profile", group: "Pages", icon: User, color: "#e11d48", bg: "#fff1f2", }, { type: "route", title: "Home", description: "Go back to home", route: "/student/home", group: "Pages", icon: Home, color: "#f97316", bg: "#fff7ed", }, ]; const NAV_MAP = Object.fromEntries(NAV_ITEMS.map((n) => [n.route, n])); const STATUS_META = { IN_PROGRESS: { label: "In Progress", color: "#9333ea", bg: "#f3e8ff", icon: "🔄", }, COMPLETED: { label: "Completed", color: "#16a34a", bg: "#f0fdf4", icon: "✅", }, NOT_STARTED: { label: "Not Started", color: "#6b7280", bg: "#f3f4f6", icon: "📋", }, }; // ─── Recent items (session memory) ─────────────────────────────────────────── const SESSION_KEY = "so_recent"; const MAX_RECENT = 5; const getRecent = (): SearchItem[] => { try { return JSON.parse(sessionStorage.getItem(SESSION_KEY) ?? "[]"); } catch { return []; } }; const addRecent = (item: SearchItem) => { const prev = getRecent().filter((r) => r.route !== item.route); const next = [item, ...prev].slice(0, MAX_RECENT); sessionStorage.setItem(SESSION_KEY, JSON.stringify(next)); }; // ─── Highlight helper ───────────────────────────────────────────────────────── const highlightText = (text: string, query: string) => { if (!query.trim()) return <>{text}; const esc = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const regex = new RegExp(`(${esc})`, "gi"); const parts = text.split(regex); return ( <> {parts.map((part, i) => part.toLowerCase() === query.toLowerCase() ? ( {part} ) : ( part ), )} ); }; // ─── Styles ─────────────────────────────────────────────────────────────────── const STYLES = ` @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap'); .so-overlay { position: fixed; inset: 0; z-index: 50; background: rgba(0,0,0,0.35); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); display: flex; flex-direction: column; align-items: center; padding-top: 5rem; padding-left: 1rem; padding-right: 1rem; } .so-box { width: 100%; max-width: 560px; background: #fffbf4; border: 2.5px solid #f3f4f6; border-radius: 28px; box-shadow: 0 20px 60px rgba(0,0,0,0.18), 0 6px 16px rgba(0,0,0,0.08); overflow: hidden; display: flex; flex-direction: column; max-height: calc(100vh - 6rem); } /* Input row */ .so-input-row { display: flex; align-items: center; gap: 0.75rem; padding: 1rem 1.25rem; border-bottom: 2px solid #f3f4f6; flex-shrink: 0; } .so-input { flex: 1; outline: none; border: none; background: transparent; font-family: 'Nunito', sans-serif; font-size: 0.95rem; font-weight: 800; color: #1e1b4b; } .so-input::placeholder { color: #d1d5db; font-weight: 700; } .so-close-btn { width: 30px; height: 30px; border-radius: 50%; border: 2.5px solid #f3f4f6; background: white; cursor: pointer; flex-shrink: 0; display: flex; align-items: center; justify-content: center; transition: all 0.15s ease; } .so-close-btn:hover { border-color: #fecdd3; background: #fff1f2; } /* Scrollable results */ .so-results { overflow-y: auto; flex: 1; padding: 0.75rem 0.75rem 1rem; -webkit-overflow-scrolling: touch; display: flex; flex-direction: column; gap: 1rem; } /* Section label */ .so-section-label { font-size: 0.58rem; font-weight: 800; letter-spacing: 0.18em; text-transform: uppercase; color: #9ca3af; padding: 0 0.5rem; margin-bottom: -0.35rem; display: flex; align-items: center; gap: 0.4rem; } /* Result rows */ .so-item { display: flex; align-items: center; gap: 0.75rem; padding: 0.7rem 0.75rem; border-radius: 16px; cursor: pointer; transition: background 0.15s ease, transform 0.1s ease; border: 2px solid transparent; } .so-item:hover { background: white; border-color: #f3f4f6; box-shadow: 0 4px 12px rgba(0,0,0,0.05); transform: translateX(2px); } .so-item:active { transform: scale(0.98); } .so-item-icon { width: 36px; height: 36px; border-radius: 11px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 0.85rem; } .so-item-body { flex: 1; min-width: 0; } .so-item-title { font-family: 'Nunito', sans-serif; font-size: 0.88rem; font-weight: 900; color: #1e1b4b; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .so-item-desc { font-family: 'Nunito Sans', sans-serif; font-size: 0.72rem; font-weight: 600; color: #9ca3af; margin-top: 0.05rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .so-item-arrow { color: #d1d5db; flex-shrink: 0; transition: color 0.15s ease; } .so-item:hover .so-item-arrow { color: #a855f7; } /* Sheet status chip inline */ .so-status-chip { font-size: 0.6rem; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; border-radius: 100px; padding: 0.15rem 0.5rem; flex-shrink: 0; } /* Quick nav chips (shown when empty query) */ .so-quick-wrap { display: flex; flex-wrap: wrap; gap: 0.5rem; padding: 0 0.25rem; } .so-quick-chip { display: flex; align-items: center; gap: 0.4rem; background: white; border: 2.5px solid #f3f4f6; border-radius: 100px; padding: 0.45rem 0.85rem; cursor: pointer; font-family: 'Nunito', sans-serif; font-size: 0.75rem; font-weight: 800; color: #6b7280; box-shadow: 0 2px 8px rgba(0,0,0,0.04); transition: all 0.15s ease; } .so-quick-chip:hover { transform: translateY(-2px); box-shadow: 0 6px 14px rgba(0,0,0,0.07); border-color: #e9d5ff; color: #a855f7; } /* Empty state */ .so-empty { display: flex; flex-direction: column; align-items: center; padding: 2rem 1rem; gap: 0.5rem; font-family: 'Nunito Sans', sans-serif; } .so-empty-emoji { font-size: 2rem; } .so-empty-text { font-size: 0.85rem; font-weight: 700; color: #9ca3af; } .so-empty-sub { font-size: 0.75rem; font-weight: 600; color: #d1d5db; text-align: center; } /* Keyboard hint */ .so-kbd-row { display: flex; align-items: center; justify-content: center; gap: 1rem; padding: 0.6rem 1rem; border-top: 2px solid #f9fafb; flex-shrink: 0; } .so-kbd-hint { display: flex; align-items: center; gap: 0.3rem; font-family: 'Nunito Sans', sans-serif; font-size: 0.62rem; font-weight: 600; color: #d1d5db; } .so-kbd { background: white; border: 1.5px solid #e5e7eb; border-radius: 5px; padding: 0.1rem 0.4rem; font-size: 0.6rem; font-weight: 800; color: #9ca3af; box-shadow: 0 1px 0 #d1d5db; } /* Highlight count badge */ .so-count-badge { font-family: 'Nunito', sans-serif; font-size: 0.65rem; font-weight: 800; background: #f3e8ff; color: #9333ea; border-radius: 100px; padding: 0.15rem 0.5rem; flex-shrink: 0; } `; // ─── Main component ─────────────────────────────────────────────────────────── export const SearchOverlay = ({ sheets, onClose, searchQuery, setSearchQuery, }: Props) => { const navigate = useNavigate(); const inputRef = useRef(null); const [recent, setRecent] = useState(getRecent); const [focused, setFocused] = useState(-1); // keyboard nav index // Build full search item list const searchItems = useMemo(() => { const sheetItems = sheets.map((sheet) => ({ type: "sheet" as const, id: sheet.id, title: sheet.title, description: sheet.description ?? "Practice sheet", route: `/student/practice/${sheet.id}`, group: formatGroupTitle(sheet.user_status), status: sheet.user_status, })); return [...NAV_ITEMS, ...sheetItems]; }, [sheets]); // Filtered + grouped results const groupedResults = useMemo(() => { if (!searchQuery.trim()) return {}; const q = searchQuery.toLowerCase(); const filtered = searchItems.filter( (item) => item.title?.toLowerCase().includes(q) || item.description?.toLowerCase().includes(q), ); return filtered.reduce>((acc, item) => { (acc[item.group] ??= []).push(item); return acc; }, {}); }, [searchQuery, searchItems]); const flatResults = useMemo( () => Object.values(groupedResults).flat(), [groupedResults], ); // ESC to close, arrow keys + enter for keyboard nav useEffect(() => { const handle = (e: KeyboardEvent) => { if (e.key === "Escape") { onClose(); return; } if (e.key === "ArrowDown") { e.preventDefault(); setFocused((f) => Math.min(f + 1, flatResults.length - 1)); } if (e.key === "ArrowUp") { e.preventDefault(); setFocused((f) => Math.max(f - 1, 0)); } if (e.key === "Enter" && focused >= 0 && flatResults[focused]) { handleSelect(flatResults[focused]); } }; window.addEventListener("keydown", handle); return () => window.removeEventListener("keydown", handle); }, [onClose, focused, flatResults]); // Reset focused when query changes useEffect(() => { setFocused(-1); }, [searchQuery]); const handleSelect = (item: SearchItem) => { addRecent(item); setRecent(getRecent()); onClose(); navigate(item.route!); }; const totalCount = flatResults.length; return ( e.stopPropagation()} > {/* Input row */}
setSearchQuery(e.target.value)} placeholder="Search sheets, pages, topics..." /> {searchQuery && totalCount > 0 && ( {totalCount} result{totalCount !== 1 ? "s" : ""} )}
{/* Results */}
{/* ── Empty query: recent + quick nav ── */} {!searchQuery && ( <> {recent.length > 0 && (

Recent

{recent.map((item, i) => { const navMeta = NAV_MAP[item.route!]; const Icon = navMeta?.icon ?? BookOpen; const color = navMeta?.color ?? "#a855f7"; const bg = navMeta?.bg ?? "#fdf4ff"; return (
handleSelect(item)} >

{item.title}

{item.description && (

{item.description}

)}
); })}
)}

⚡ Quick nav

{NAV_ITEMS.map((item, i) => ( ))}
{sheets.length > 0 && (

In progress

{sheets .filter((s) => s.user_status === "IN_PROGRESS") .slice(0, 3) .map((sheet) => { const item: SearchItem = { type: "sheet", title: sheet.title, description: sheet.description, route: `/student/practice/${sheet.id}`, group: "In Progress", status: sheet.user_status, }; return (
handleSelect(item)} >

{sheet.title}

{sheet.description && (

{sheet.description}

)}
In Progress
); })}
)} )} {/* ── No results ── */} {searchQuery && totalCount === 0 && (
🔍

No results for "{searchQuery}"

Try searching for a topic, sheet title, or page name

)} {/* ── Results grouped ── */} {searchQuery && totalCount > 0 && Object.entries(groupedResults).map(([group, items]) => (

{group}

{items.map((item, index) => { const globalIdx = flatResults.indexOf(item); const isFocused = globalIdx === focused; const navMeta = NAV_MAP[item.route!]; const Icon = navMeta?.icon ?? BookOpen; const iconColor = navMeta?.color ?? "#a855f7"; const iconBg = navMeta?.bg ?? "#fdf4ff"; const statusMeta = item.status ? STATUS_META[item.status as keyof typeof STATUS_META] : null; return ( handleSelect(item)} >
{item.type === "sheet" ? ( {statusMeta?.icon ?? "📋"} ) : ( )}

{highlightText(item.title, searchQuery)}

{item.description && (

{highlightText(item.description, searchQuery)}

)}
{statusMeta && ( {statusMeta.label} )}
); })}
))}
{/* Keyboard hints */}
↑↓ Navigate
Open
Esc Close
); };