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
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import "./TopNav.css";
|
||||
|
||||
const TopNav = ({ activeGrade, onGradeChange, activeSubject, onSubjectChange, activeChapter, onChapterChange }) => {
|
||||
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);
|
||||
@@ -54,25 +54,9 @@ const TopNav = ({ activeGrade, onGradeChange, activeSubject, onSubjectChange, ac
|
||||
{ id: "Grade 12", label: "Grade 12" },
|
||||
];
|
||||
|
||||
const subjects = [
|
||||
{ id: "choose-subject", label: "Choose Subject" },
|
||||
{ id: "math", label: "Mathematics" },
|
||||
{ id: "science", label: "Science" },
|
||||
{ id: "history", label: "History" },
|
||||
{ id: "language", label: "Language Arts" },
|
||||
{ id: "english", label: "English" },
|
||||
{ id: "geography", label: "Geography" },
|
||||
];
|
||||
|
||||
const chapters = [
|
||||
{ id: "choose-chapter", label: "Choose Chapter" },
|
||||
{ id: "chapter1", label: "Chapter 1: Rational Numbers" },
|
||||
{ id: "chapter2", label: "Chapter 2: Linear Equations" },
|
||||
{ id: "chapter3", label: "Chapter 3: Coordinate Geometry" },
|
||||
{ id: "chapter4", label: "Chapter 4: Life Processes" },
|
||||
{ id: "chapter5", label: "Chapter 5: Photosynthesis" },
|
||||
{ id: "chapter6", label: "Chapter 6: Human Physiology" },
|
||||
];
|
||||
// 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",
|
||||
@@ -83,6 +67,25 @@ const TopNav = ({ activeGrade, onGradeChange, activeSubject, onSubjectChange, ac
|
||||
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">
|
||||
@@ -125,16 +128,12 @@ const TopNav = ({ activeGrade, onGradeChange, activeSubject, onSubjectChange, ac
|
||||
<span className={`material-symbols-outlined topnav__pill-icon`}>
|
||||
{activeSubject === "choose-subject" ? "science" : subjectIcons[activeSubject] || "science"}
|
||||
</span>
|
||||
<span className="topnav__pill-label">
|
||||
{activeSubject === "choose-subject"
|
||||
? "Choose Subject"
|
||||
: subjects.find((s) => s.id === activeSubject)?.label || activeSubject}
|
||||
</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">
|
||||
{subjects.map((subject) => (
|
||||
{subjectItems?.map((subject) => (
|
||||
<li key={subject.id}>
|
||||
<button
|
||||
className={`topnav__dropdown-item ${activeSubject === subject.id ? "topnav__dropdown-item--active" : ""}`}
|
||||
@@ -160,15 +159,15 @@ const TopNav = ({ activeGrade, onGradeChange, activeSubject, onSubjectChange, ac
|
||||
aria-expanded={openDropdown === "chapter"}
|
||||
>
|
||||
<span className="material-symbols-outlined topnav__pill-icon">menu_book</span>
|
||||
<span className="topnav__pill-label">{activeChapter}</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">
|
||||
{chapters.map((chapter) => (
|
||||
{chapterOptions.map((chapter) => (
|
||||
<li key={chapter.id}>
|
||||
<button
|
||||
className={`topnav__dropdown-item ${activeChapter === chapter.label ? "topnav__dropdown-item--active" : ""}`}
|
||||
className={`topnav__dropdown-item ${(activeChapter === chapter.label || activeChapter === "choose-chapter") ? "topnav__dropdown-item--active" : ""}`}
|
||||
onClick={() => selectOption("chapter", chapter.label)}
|
||||
>
|
||||
{chapter.label}
|
||||
@@ -191,10 +190,17 @@ const TopNav = ({ activeGrade, onGradeChange, activeSubject, onSubjectChange, ac
|
||||
<span className="material-symbols-outlined">notifications_none</span>
|
||||
</button>
|
||||
|
||||
{/* User Avatar */}
|
||||
<div className="topnav__avatar">
|
||||
<span className="topnav__avatar-initial">S</span>
|
||||
</div>
|
||||
{/* 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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user