Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
420 changes: 420 additions & 0 deletions SEARCH_SYSTEM_README.md

Large diffs are not rendered by default.

114 changes: 89 additions & 25 deletions app/components/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import Link from "next/link";
import { Road_Rage } from "next/font/google";
import { useState } from "react";
import SearchBar from "./search/SearchBar";

const roadRage = Road_Rage({
variable: "--font-road-rage",
Expand All @@ -13,6 +14,26 @@ export default function Navbar() {
const [menuOpen, setMenuOpen] = useState(false);

return (
<nav className="sticky h-auto min-h-14 top-0 left-0 w-full z-50 flex flex-col md:flex-row justify-between items-center gap-3 md:gap-4 px-4 md:px-6 py-3 bg-[#1B0D00] text-white">
{/* Logo and Menu Row */}
<div className="flex justify-between items-center w-full md:w-auto">
<Link href="/">
<div className="flex items-center gap-1">
<img src="/mascot.png" alt="openCSE" className="w-8 h-11 pt-0.5" />
<span
className={`${roadRage.className} text-3xl font-bold`}
style={{ color: "white", fontSize: "30px" }}
>
openCSE
</span>
</div>
</Link>

{/* Mobile Hamburger */}
<button
className="md:hidden flex flex-col justify-center items-center w-10 h-10"
aria-label="Toggle menu"
onClick={() => setMenuOpen((open) => !open)}
<nav className="fixed h-14 top-0 inset-x-0 z-50 flex justify-between items-center pr-6 pl-4 py-1 bg-[#1B0D00] text-white">
<Link href="/"><div className="flex items-center gap-1">
<img src="/mascot.png" alt="openCSE" className="w-8 h-11 pt-0.5" />
Expand All @@ -21,9 +42,29 @@ export default function Navbar() {
className={`${roadRage.className} text-3xl font-bold`}
style={{ color: "white", fontSize: "30px" }}
>
openCSE
</span>
</div></Link>
<span
className={`block w-8 h-1 bg-white mb-1 transition-all duration-300 ${
menuOpen ? "rotate-45 translate-y-2" : ""
}`}
/>
<span
className={`block w-8 h-1 bg-white mb-1 transition-all duration-300 ${
menuOpen ? "opacity-0" : ""
}`}
/>
<span
className={`block w-8 h-1 bg-white transition-all duration-300 ${
menuOpen ? "-rotate-45 -translate-y-2" : ""
}`}
/>
</button>
</div>

{/* Search Bar - Desktop */}
<div className="hidden md:flex flex-1 max-w-md mx-4">
<SearchBar />
</div>

{/* Desktop Menu */}
<ul
className="hidden md:flex gap-6 font-bold"
Expand Down Expand Up @@ -55,29 +96,51 @@ export default function Navbar() {
<Link href="/quiz">QUIZ</Link>
</li>
</ul>
{/* Mobile Hamburger */}
<button
className="md:hidden flex flex-col justify-center items-center w-10 h-10"
aria-label="Toggle menu"
onClick={() => setMenuOpen((open) => !open)}
>
<span
className={`block w-8 h-1 bg-white mb-1 transition-all duration-300 ${
menuOpen ? "rotate-45 translate-y-2" : ""
}`}
/>
<span
className={`block w-8 h-1 bg-white mb-1 transition-all duration-300 ${
menuOpen ? "opacity-0" : ""
}`}
/>
<span
className={`block w-8 h-1 bg-white transition-all duration-300 ${
menuOpen ? "-rotate-45 -translate-y-2" : ""
}`}
/>
</button>

{/* Mobile Search Bar */}
<div className="flex md:hidden w-full px-2">
<SearchBar />
</div>

{/* Mobile Menu */}
<<<<<<< HEAD
{menuOpen && (
<div className="md:hidden w-full bg-[#1B0D00]/95 shadow-lg">
<ul
className="flex flex-col items-center gap-4 py-4 font-bold"
style={{
color: "white",
fontFamily: '"Road Rage", sans-serif',
fontSize: "28px",
fontStyle: "normal",
fontWeight: 400,
lineHeight: "normal",
}}
>
<li>
<Link href="/" onClick={() => setMenuOpen(false)}>
HOME
</Link>
</li>
<li>
<Link href="/#subjects" onClick={() => setMenuOpen(false)}>
SUBJECTS
</Link>
</li>
<li>
<Link href="/#contribute" onClick={() => setMenuOpen(false)}>
CONTRIBUTE
</Link>
</li>
<li>
<Link href="/#sponsor" onClick={() => setMenuOpen(false)}>
SPONSOR
</Link>
</li>
</ul>
</div>
)}
=======
<div
className={`md:hidden absolute top-full left-0 w-full bg-[#1B0D00]/95 shadow-lg transition-all duration-300 origin-top ${
menuOpen
Expand Down Expand Up @@ -123,6 +186,7 @@ export default function Navbar() {
</li>
</ul>
</div>
>>>>>>> main
</nav>
);
}
122 changes: 122 additions & 0 deletions app/components/search/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { Search, X } from "lucide-react";
import "@/app/lib/search/initializeSearch"; // Ensure metadata is loaded
import { getSearchIndex } from "@/app/lib/search/searchIndex";
import { search } from "@/app/lib/search/searchEngine";
import { SearchResult } from "@/app/lib/search/searchTypes";
import SearchResults from "./SearchResults";

export default function SearchBar() {
const [query, setQuery] = useState("");
const [results, setResults] = useState<SearchResult[]>([]);
const [isOpen, setIsOpen] = useState(false);
const [isFocused, setIsFocused] = useState(false);
const searchRef = useRef<HTMLDivElement>(null);

// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (searchRef.current && !searchRef.current.contains(event.target as Node)) {
setIsOpen(false);
setIsFocused(false);
}
};

document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);

// Keyboard shortcut: / (forward slash)
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
// Forward slash to focus search (like GitHub, Reddit)
if (event.key === "/" && !["INPUT", "TEXTAREA"].includes((event.target as HTMLElement).tagName)) {
event.preventDefault();
const input = searchRef.current?.querySelector("input");
input?.focus();
}

// Escape to close
if (event.key === "Escape") {
setIsOpen(false);
setIsFocused(false);
const input = searchRef.current?.querySelector("input");
input?.blur();
}
};

document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, []);

// Perform search
useEffect(() => {
if (query.trim().length < 2) {
setResults([]);
setIsOpen(false);
return;
}

const searchIndex = getSearchIndex();
const searchResults = search(searchIndex, query, 8);
setResults(searchResults);
setIsOpen(searchResults.length > 0);
}, [query]);

const handleClear = () => {
setQuery("");
setResults([]);
setIsOpen(false);
};

const handleClose = () => {
setIsOpen(false);
setQuery("");
setResults([]);
};

return (
<div ref={searchRef} className="relative w-full">
{/* Search Input */}
<div
className={`flex items-center gap-2 px-3 py-2 rounded-lg transition-all duration-200 ${
isFocused
? "bg-[#2a1809] ring-2 ring-[#c7a669]"
: "bg-[#2a1809] hover:bg-[#3a2414]"
}`}
>
<Search className="w-5 h-5 text-[#c7a669] flex-shrink-0" />
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
onFocus={() => setIsFocused(true)}
placeholder="Search subjects or topics..."
className="flex-1 bg-transparent text-[#fae8d7] placeholder-[#8b7355] outline-none text-base min-w-0"
/>
{query && (
<button
onClick={handleClear}
className="text-[#8b7355] hover:text-[#fae8d7] transition flex-shrink-0"
aria-label="Clear search"
>
<X className="w-4 h-4" />
</button>
)}
</div>

{/* Results Dropdown */}
{isOpen && results.length > 0 && (
<SearchResults results={results} onClose={handleClose} />
)}

{/* No Results */}
{isOpen && query.length >= 2 && results.length === 0 && (
<div className="absolute top-full mt-2 w-full bg-[#2a1809] border border-[#c7a669] rounded-lg p-4 text-center text-[#8b7355] z-50">
No results found for &quot;{query}&quot;
</div>
)}
</div>
);
}
36 changes: 36 additions & 0 deletions app/components/search/SearchResultItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"use client";
import Link from "next/link";
import { SearchResult } from "@/app/lib/search/searchTypes";
import { BookOpen, FileText } from "lucide-react";

interface SearchResultItemProps {
result: SearchResult;
onClose: () => void;
}

export default function SearchResultItem({ result, onClose }: SearchResultItemProps) {
const Icon = result.type === "subject" ? BookOpen : FileText;

return (
<Link
href={result.url}
onClick={onClose}
className="flex items-start gap-3 px-4 py-3 hover:bg-[#3a2414] transition-colors cursor-pointer border-b border-[#3a2414] last:border-b-0"
>
<Icon className="w-5 h-5 text-[#c7a669] mt-0.5 flex-shrink-0" />
<div className="flex-1 min-w-0">
<div className="text-[#fae8d7] font-medium text-base truncate">
{result.title}
</div>
{result.subtitle && (
<div className="text-[#8b7355] text-sm mt-0.5">
{result.subtitle}
</div>
)}
</div>
<div className="text-xs text-[#8b7355] flex-shrink-0">
Sem {result.semester}
</div>
</Link>
);
}
42 changes: 42 additions & 0 deletions app/components/search/SearchResults.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"use client";
import { SearchResult } from "@/app/lib/search/searchTypes";
import SearchResultItem from "./SearchResultItem";

interface SearchResultsProps {
results: SearchResult[];
onClose: () => void;
}

export default function SearchResults({ results, onClose }: SearchResultsProps) {
// Group by type
const subjects = results.filter((r) => r.type === "subject");
const chapters = results.filter((r) => r.type === "chapter");

return (
<div className="absolute top-full mt-2 w-full max-h-[70vh] overflow-y-auto bg-[#2a1809] border border-[#c7a669] rounded-lg shadow-2xl z-50">
{/* Subjects Section */}
{subjects.length > 0 && (
<div className="border-b border-[#3a2414]">
<div className="px-4 py-2 text-xs font-semibold text-[#c7a669] uppercase tracking-wide">
Subjects
</div>
{subjects.map((result) => (
<SearchResultItem key={result.id} result={result} onClose={onClose} />
))}
</div>
)}

{/* Chapters Section */}
{chapters.length > 0 && (
<div>
<div className="px-4 py-2 text-xs font-semibold text-[#c7a669] uppercase tracking-wide">
Topics
</div>
{chapters.map((result) => (
<SearchResultItem key={result.id} result={result} onClose={onClose} />
))}
</div>
)}
</div>
);
}
Loading