diff --git a/src/components/SearchOverlay.tsx b/src/components/SearchOverlay.tsx new file mode 100644 index 0000000..cad6429 --- /dev/null +++ b/src/components/SearchOverlay.tsx @@ -0,0 +1,232 @@ +import { motion, AnimatePresence } from "framer-motion"; +import { useEffect, useMemo } from "react"; +import { Search, X } 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; +} + +const navigationItems: SearchItem[] = [ + { + type: "route", + title: "Hard Test Modules", + description: "Access advanced SAT modules", + route: "/student/hard-test-modules", + group: "Pages", + }, + { + type: "route", + title: "Targeted Practice", + description: "Focus on what matters", + route: "/student/practice/targeted-practice", + group: "Pages", + }, + { + type: "route", + title: "Drills", + description: "Train speed and accuracy", + route: "/student/practice/drills", + group: "Pages", + }, + { + type: "route", + title: "Leaderboard", + description: "View student rankings", + route: "/student/rewards", + group: "Pages", + }, + { + type: "route", + title: "Practice", + description: "See how you can practice", + route: "/student/practice", + group: "Pages", + }, + { + type: "route", + title: "Lessons", + description: "Watch detailed lessons on SAT techniques", + route: "/student/lessons", + group: "Pages", + }, + { + type: "route", + title: "Profile", + description: "View your profile", + route: "/student/profile", + group: "Pages", + }, +]; + +const highlightText = (text: string, query: string) => { + if (!query.trim()) return text; + + const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const regex = new RegExp(`(${escapedQuery})`, "gi"); + + const parts = text.split(regex); + + return parts.map((part, index) => { + const isMatch = part.toLowerCase() === query.toLowerCase(); + + return isMatch ? ( + + {part} + + ) : ( + part + ); + }); +}; + +export const SearchOverlay = ({ + sheets, + onClose, + searchQuery, + setSearchQuery, +}: Props) => { + const navigate = useNavigate(); + const searchItems = useMemo(() => { + const sheetItems = sheets.map((sheet) => ({ + type: "sheet", + id: sheet.id, + title: sheet.title, + description: sheet.description, + route: `/student/practice/${sheet.id}`, + group: formatGroupTitle(sheet.user_status), // 👈 reuse your grouping + })); + + return [...navigationItems, ...sheetItems]; + }, [sheets]); + + // Close on ESC + useEffect(() => { + const handleKey = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose(); + }; + window.addEventListener("keydown", handleKey); + return () => window.removeEventListener("keydown", handleKey); + }, [onClose]); + + const groupedResults = useMemo(() => { + if (!searchQuery.trim()) return {}; + + const q = searchQuery.toLowerCase(); + + const filtered = searchItems.filter((item) => { + const title = item.title?.toLowerCase() || ""; + const description = item.description?.toLowerCase() || ""; + + return title.includes(q) || description.includes(q); + }); + + return filtered.reduce>((acc, item) => { + if (!acc[item.group]) { + acc[item.group] = []; + } + acc[item.group].push(item); + return acc; + }, {}); + }, [searchQuery, searchItems]); + + return ( + + + {/* Search Box */} + e.stopPropagation()} + className="w-full max-w-2xl bg-white rounded-3xl shadow-2xl p-6" + > +
+ + setSearchQuery(e.target.value)} + placeholder="Search..." + className="flex-1 outline-none font-satoshi text-lg" + /> + +
+ + {/* Results */} +
+ {/* {!searchQuery && ( +

+ Start typing to search... +

+ )} */} + + {searchQuery.length === 0 ? ( +

+ Start typing to search... +

+ ) : Object.keys(groupedResults).length === 0 ? ( +

No results found.

+ ) : ( + Object.entries(groupedResults).map(([group, items]) => ( +
+

+ {group} +

+ +
+ {items.map((item, index) => ( +
{ + onClose(); + navigate(item.route!); + }} + className="p-4 rounded-2xl hover:bg-gray-100 cursor-pointer transition" + > +

+ {highlightText(item.title, searchQuery)} +

+ + {item.description && ( +

+ {highlightText(item.description, searchQuery)} +

+ )} + +

+ {item.type === "route" ? "" : "Practice Sheet"} +

+
+ ))} +
+
+ )) + )} +
+
+
+
+ ); +}; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index f0983d4..249e658 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -98,3 +98,16 @@ export const slideVariants = { transition: { duration: 0.25 }, }), }; + +export const formatGroupTitle = (status: string) => { + switch (status) { + case "IN_PROGRESS": + return "In Progress"; + case "NOT_STARTED": + return "Not Started"; + case "COMPLETED": + return "Completed"; + default: + return status; + } +}; diff --git a/src/pages/student/Home.tsx b/src/pages/student/Home.tsx index d204916..81c3f0f 100644 --- a/src/pages/student/Home.tsx +++ b/src/pages/student/Home.tsx @@ -6,14 +6,7 @@ import { TabsContent, } from "../../components/ui/tabs"; import { useAuthStore } from "../../stores/authStore"; -import { - CheckCircle, - DecimalsArrowRight, - DraftingCompass, - List, - Search, - SquarePen, -} from "lucide-react"; +import { CheckCircle, Search } from "lucide-react"; import { api } from "../../utils/api"; import { Card, @@ -28,8 +21,7 @@ import { Button } from "../../components/ui/button"; import type { PracticeSheet } from "../../types/sheet"; import { formatStatus } from "../../lib/utils"; import { useNavigate } from "react-router-dom"; -import { Progress } from "../../components/ui/progress"; -import { Field, FieldLabel } from "../../components/ui/field"; +import { SearchOverlay } from "../../components/SearchOverlay"; export const Home = () => { const user = useAuthStore((state) => state.user); @@ -40,6 +32,9 @@ export const Home = () => { const [inProgressSheets, setInProgressSheets] = useState([]); const [completedSheets, setCompletedSheets] = useState([]); + const [isSearchOpen, setIsSearchOpen] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + useEffect(() => { const sortPracticeSheets = (sheets: PracticeSheet[]) => { const notStarted = sheets.filter( @@ -75,7 +70,6 @@ export const Home = () => { } const sheets = await api.getPracticeSheets(token, 1, 10); setPracticeSheets(sheets.data); - console.log("All Practice Sheets: ", sheets.data); sortPracticeSheets(sheets.data); } catch (error) { console.error("Error fetching practice sheets:", error); @@ -94,66 +88,21 @@ export const Home = () => {

Welcome, {user?.name || "Student"}

- - {/*
-

- Your predictive SAT score is low. Take a practice test to increase - your scores now! -

-
*/} - {/* -
- - - Your score is low! - - - - - - - Score - - 854/1600 - - - - -

- Taking more practice tests can increase your score today! -

-
- - - - -
-
- -
-
*/}

What are you looking for?

setIsSearchOpen(true)} + placeholder="Search practice sheets..." + readOnly + className="font-satoshi w-full pl-10 pr-4 py-3 border border-gray-300 rounded-2xl shadow-sm cursor-pointer" />
+

Pick up where you left off @@ -402,6 +351,17 @@ export const Home = () => {

+ {isSearchOpen && ( + { + setIsSearchOpen(false); + setSearchQuery(""); + }} + searchQuery={searchQuery} + setSearchQuery={setSearchQuery} + /> + )} ); }; diff --git a/src/types/search.ts b/src/types/search.ts new file mode 100644 index 0000000..a51429c --- /dev/null +++ b/src/types/search.ts @@ -0,0 +1,16 @@ +export type SearchItem = + | { + type: "sheet"; + id: string; + title: string; + description?: string; + status?: string; + group: string; + } + | { + type: "route"; + title: string; + description?: string; + route: string; + group: string; + };