Files
padhle/frontend/src/components/top-nav/TopNav.jsx
T
hahachait ff9da4d324 feat: auth, persistence, and AI integration
- Cookie-based auth via backend proxy (httpOnly JWTs)
- Supabase Postgres persistence for sessions/messages/profiles + RLS
- Fix cross-user session leak (token cache keyed by full token, not 50-char prefix)
- Fix missing table grants (42501) via migration; auto-provision profiles on user creation
- Chat validation, ownership checks, rate limiting, /api/chat/sessions route ordering
- Frontend auth-state reset + credentials include
- OpenRouter AI provider (OpenAI-compatible base URL, reasoning disabled)
- Tests: chatValidation, appState
2026-08-19 09:41:40 -04:00

210 lines
7.7 KiB
React

/* ========================================
TopNav Component — padhle (Refined Workspace)
======================================== */
import { useState, useRef, useEffect } from "react";
import "./TopNav.css";
const TopNav = ({ subjectItems, chapterData, activeGrade, onGradeChange, activeSubject, onSubjectChange, activeChapter, onChapterChange, user, onSignOut }) => {
const [openDropdown, setOpenDropdown] = useState(null);
const gradeRef = useRef(null);
const subjectRef = useRef(null);
const chapterRef = useRef(null);
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (e) => {
const refs = [gradeRef, subjectRef, chapterRef];
const isInside = refs.some((ref) => ref.current && ref.current.contains(e.target));
if (!isInside) {
setOpenDropdown(null);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const toggleDropdown = (name) => {
setOpenDropdown((prev) => (prev === name ? null : name));
};
const selectOption = (name, value) => {
setOpenDropdown(null);
switch (name) {
case "grade":
onGradeChange(value);
break;
case "subject":
onSubjectChange(value);
break;
case "chapter":
onChapterChange(value);
break;
}
};
const grades = [
{ id: "choose-standard", label: "Choose standard" },
{ id: "Grade 6", label: "Grade 6" },
{ id: "Grade 7", label: "Grade 7" },
{ id: "Grade 8", label: "Grade 8" },
{ id: "Grade 9", label: "Grade 9" },
{ id: "Grade 10", label: "Grade 10" },
{ id: "Grade 11", label: "Grade 11" },
{ id: "Grade 12", label: "Grade 12" },
];
// Chapters are now generated dynamically from chapterData (see chapterOptions above)
// Subjects now come from subjectItems prop
// Static list removed — was not subject-aware.
const subjectIcons = {
math: "calculate",
science: "science",
history: "account_balance",
language: "menu_book",
english: "edit",
geography: "public",
};
/* ─── Resolve subject display (ID → label) ─── */
const resolvedSubjectLabel = activeSubject === "choose-subject"
? "Choose Subject"
: subjectItems?.find((s) => s.id === activeSubject)?.label || activeSubject || "Choose Subject";
/* ─── Resolve chapter pill label ─── */
const resolvedChapterLabel = activeChapter === "choose-chapter" || !activeChapter
? "Choose Chapter"
: activeChapter;
/* ─── Subject-aware chapter options (from chapterData prop) ─── */
const chapterOptions = (() => {
const chapters = chapterData?.[activeSubject] || [];
return [
{ id: "choose-chapter", label: "Choose Chapter" },
...chapters,
];
})();
return (
<header className="topnav">
<div className="topnav__selectors">
{/* Grade Selector */}
<div className="topnav__selector" ref={gradeRef}>
<button
className={`topnav__pill topnav__pill--dropdown ${openDropdown === "grade" ? "topnav__pill--open" : ""}`}
onClick={() => toggleDropdown("grade")}
aria-label="Select Grade"
aria-expanded={openDropdown === "grade"}
>
<span className="material-symbols-outlined topnav__pill-icon">person_outline</span>
<span className="topnav__pill-label">{activeGrade}</span>
<span className="material-symbols-outlined topnav__pill-arrow">expand_more</span>
</button>
{openDropdown === "grade" && (
<ul className="topnav__dropdown">
{grades.map((grade) => (
<li key={grade.id}>
<button
className={`topnav__dropdown-item ${activeGrade === grade.label ? "topnav__dropdown-item--active" : ""}`}
onClick={() => selectOption("grade", grade.label)}
>
{grade.label}
</button>
</li>
))}
</ul>
)}
</div>
{/* Subject Selector */}
<div className="topnav__selector" ref={subjectRef}>
<button
className={`topnav__pill topnav__pill--dropdown ${openDropdown === "subject" ? "topnav__pill--open" : ""}`}
onClick={() => toggleDropdown("subject")}
aria-label="Select Subject"
aria-expanded={openDropdown === "subject"}
>
<span className={`material-symbols-outlined topnav__pill-icon`}>
{activeSubject === "choose-subject" ? "science" : subjectIcons[activeSubject] || "science"}
</span>
<span className="topnav__pill-label">{resolvedSubjectLabel}</span>
<span className="material-symbols-outlined topnav__pill-arrow">expand_more</span>
</button>
{openDropdown === "subject" && (
<ul className="topnav__dropdown">
{subjectItems?.map((subject) => (
<li key={subject.id}>
<button
className={`topnav__dropdown-item ${activeSubject === subject.id ? "topnav__dropdown-item--active" : ""}`}
onClick={() => selectOption("subject", subject.id)}
>
<span className={`material-symbols-outlined topnav__dropdown-icon`}>
{subject.id === "choose-subject" ? "science" : subjectIcons[subject.id]}
</span>
{subject.label}
</button>
</li>
))}
</ul>
)}
</div>
{/* Chapter Selector */}
<div className="topnav__selector" ref={chapterRef}>
<button
className={`topnav__pill topnav__pill--dropdown ${openDropdown === "chapter" ? "topnav__pill--open" : ""}`}
onClick={() => toggleDropdown("chapter")}
aria-label="Select Chapter"
aria-expanded={openDropdown === "chapter"}
>
<span className="material-symbols-outlined topnav__pill-icon">menu_book</span>
<span className="topnav__pill-label">{resolvedChapterLabel}</span>
<span className="material-symbols-outlined topnav__pill-arrow">expand_more</span>
</button>
{openDropdown === "chapter" && (
<ul className="topnav__dropdown">
{chapterOptions.map((chapter) => (
<li key={chapter.id}>
<button
className={`topnav__dropdown-item ${(activeChapter === chapter.label || activeChapter === "choose-chapter") ? "topnav__dropdown-item--active" : ""}`}
onClick={() => selectOption("chapter", chapter.label)}
>
{chapter.label}
</button>
</li>
))}
</ul>
)}
</div>
</div>
<div className="topnav__actions">
{/* Search */}
<button className="topnav__action" aria-label="Search">
<span className="material-symbols-outlined">search</span>
</button>
{/* Notifications */}
<button className="topnav__action" aria-label="Notifications">
<span className="material-symbols-outlined">notifications_none</span>
</button>
{/* User Avatar — Sign out on click */}
<button
className="topnav__avatar-btn"
onClick={onSignOut}
aria-label="Sign out"
title="Sign out"
>
<span className="topnav__avatar-initial">
{user?.email?.charAt(0).toUpperCase() || "U"}
</span>
</button>
</div>
</header>
);
};
export default TopNav;