setSidebarOpen(false)}
- style={{ backgroundColor: "rgba(0,0,0,0.35)" }}
- />
- )}
-
- {/* Sidebar */}
- {showSidebar && (
-
- {/* Header */}
-
-
setSidebarOpen(false)}>
-

-
-
-
-
- {/* Content */}
- {sidebarContent?.(()=> setSidebarOpen(false))}
-
- )}
- >
- );
-};
-
-export default MobileNavbar;
\ No newline at end of file
diff --git a/main/src/components/Navbar.tsx b/main/src/components/Navbar.tsx
index 5c849cc..a901632 100644
--- a/main/src/components/Navbar.tsx
+++ b/main/src/components/Navbar.tsx
@@ -1,209 +1,84 @@
-import { Link, useLocation } from "react-router-dom";
+import { useState, useEffect } from "react";
import { useAuth } from "../context/AuthContext";
-import { Menu, MessageCirclePlus, Route, UserRound} from "lucide-react";
-import { useEffect, useState } from "react";
-import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./ui/dropdown-menu";
+import {
+ DevEnvironmentBanner,
+ MobileNavbar,
+ NavBrand,
+ NavPrimaryLinks,
+ UserProfileMenu,
+} from "@sage/ui";
+import { useRouteMode } from "../hooks/useRouteMode";
+import { PRIMARY_NAV_LINKS, MOBILE_NAV_LINKS } from "../lib/navLinks";
-// check environment
const ENVIRONMENT = import.meta.env.VITE_ENVIRONMENT as string | undefined;
const Navbar = () => {
const { user, logout, profilePicture } = useAuth();
- const [isInWebapp, setIsInWebapp] = useState(false);
- const [isOnboardingActive, setIsOnboardingActive] = useState(false);
-
- let location = useLocation().pathname;
+ const [isOnboardingActive, setIsOnboardingActive] = useState(false);
+ const { isInWebapp } = useRouteMode();
useEffect(() => {
const navHeight = ENVIRONMENT === 'development' ? '6rem' : '4.2rem';
document.documentElement.style.setProperty('--nav-height', navHeight);
}, []);
- useEffect(() => {
- if (location === "/" || location === "/login" || location === "/signup" || location === "/forgot-password") {
- setIsInWebapp(false);
- }
- else {
- setIsInWebapp(true);
- }
- }, [location]);
-
- // onboarding modal active check
useEffect(() => {
const checkOnboarding = () => {
const onboardingActive = document.body.hasAttribute('data-onboarding-active');
setIsOnboardingActive(onboardingActive);
};
-
+
checkOnboarding();
-
+
const observer = new MutationObserver(checkOnboarding);
observer.observe(document.body, { attributes: true, attributeFilter: ['data-onboarding-active'] });
-
+
return () => observer.disconnect();
}, []);
- // Dark mode when: NOT in webapp OR onboarding is active
- // Light mode when: in webapp AND onboarding is NOT active
const useDarkMode = !isInWebapp || isOnboardingActive;
+ const useLightNav = !useDarkMode;
return (
<>
- {/* Standard navbar */}
- <>
- {ENVIRONMENT === 'development' && (
-
- Dev Environment
-
- )}
-
-
- >
-
- {/* Mobile Navbar -- Dropdown navbar when screen width < md (768px) */}
+
+
>
);
};
-export default Navbar;
\ No newline at end of file
+export default Navbar;
diff --git a/main/src/components/Planner.tsx b/main/src/components/Planner.tsx
index 8b33c61..63f017c 100644
--- a/main/src/components/Planner.tsx
+++ b/main/src/components/Planner.tsx
@@ -1,9 +1,10 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import { useLocation } from "react-router-dom";
-import Sidebar from "@/components/planner/Sidebar";
+import PlannerSidebarDesktop from "@/components/planner/PlannerSidebarDesktop";
import SemesterBox from "@/components/planner/SemesterBox";
import { HelpCircle, PlusCircle, SquareAsterisk, Save, Check, Loader2, RefreshCw, ChevronDown, Settings, Pencil, Plus, Copy, Trash2, Download } from "lucide-react";
import PlannerNavbar from "./PlannerNavbar";
+import PlannerSidebarMobile from "@/components/planner/PlannerSidebarMobile";
import {
DropdownMenu,
DropdownMenuContent,
@@ -13,7 +14,7 @@ import {
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
-} from "./ui/dropdown-menu";
+} from "@sage/ui";
import { Toaster, toast } from "sonner";
import { calculateCatalogYear, calculateLatestYear, determineStudentType, getGPAState, isCurrentSemester } from "@/utils/studentInfo";
import YearDivider from "@/components/planner/YearDivider";
@@ -927,24 +928,29 @@ const Planner: React.FC
= ({ semesters, requirements, transcriptDa
<>
- handleDropCourse('', -1, null, sourceYear, sourceSemesterIndex, courseId, false)
- }
- placedSuggestedCourses={placedSuggestedCourses}
- allCompletedCourseCodes={allCompletedCourseCodes}
- allPlannedCoursesWithOrder={allPlannedCoursesWithOrder}
- onRestartOnboarding={onRestartOnboarding}
- availableSemesters={availableSemesters}
- onAddCourse={handleDropCourse}
- semesters={allSemesters}
- coursebookData={coursebookData}
- gradesData={gradesData}
- onOpenDiscovery={() => setShowDiscovery(true)}
- coursebookSemester={coursebookSemester}
+ sidebarContent={(onClose) => (
+
+ handleDropCourse('', -1, null, sourceYear, sourceSemesterIndex, courseId, false)
+ }
+ placedSuggestedCourses={placedSuggestedCourses}
+ allCompletedCourseCodes={allCompletedCourseCodes}
+ allPlannedCoursesWithOrder={allPlannedCoursesWithOrder}
+ onRestartOnboarding={onRestartOnboarding}
+ availableSemesters={availableSemesters}
+ onAddCourse={handleDropCourse}
+ semesters={allSemesters}
+ coursebookData={coursebookData}
+ gradesData={gradesData}
+ onOpenDiscovery={() => setShowDiscovery(true)}
+ coursebookSemester={coursebookSemester}
+ />
+ )}
/>
{/* Action buttons */}
@@ -1002,7 +1008,7 @@ const Planner: React.FC
= ({ semesters, requirements, transcriptDa
-
;
- onToggleCategory: (index: number) => void;
- transcriptData: any;
- onDropCourse?: (courseId: string, sourceYear: string, sourceSemesterIndex: number) => void;
- placedSuggestedCourses?: Set;
- allCompletedCourseCodes?: string[];
- allPlannedCoursesWithOrder?: Array<{
- code: string;
- yearKey: string;
- semesterIndex: number;
- semesterOrder: number;
- }>;
- availableSemesters?: Array<{yearKey: string, semesterIndex: number, title: string}>;
- onAddCourse?: (targetYear: string, targetSemesterIndex: number, course: any, sourceYear: string, sourceSemesterIndex: number, courseId?: string, isSuggested?: boolean) => void;
- onRestartOnboarding?: () => void;
- semesters?: import("@/utils/plannerCredits").SemestersForCredits;
- coursebookData?: Record;
- gradesData?: Record;
- onOpenDiscovery?: () => void;
- coursebookSemester?: string | null
+ sidebarContent: (onClose: () => void) => ReactNode;
}
-const PlannerNavbar: React.FC = ({
- requirements,
- expandedCategories,
- onToggleCategory,
- transcriptData,
- onDropCourse,
- placedSuggestedCourses,
- allCompletedCourseCodes = [],
- allPlannedCoursesWithOrder = [],
- onRestartOnboarding,
- availableSemesters,
- onAddCourse,
- semesters,
- coursebookData,
- gradesData,
- onOpenDiscovery,
- coursebookSemester,
-}) => {
+const PlannerNavbar: React.FC = ({ sidebarContent }) => {
const { user, logout, profilePicture } = useAuth();
- const location = useLocation();
-
- const [isInWebapp, setIsInWebapp] = useState(false);
-
- useEffect(() => {
- if (location.pathname === "/" || location.pathname === "/login" || location.pathname === "/signup" || location.pathname === "/forgot-password") {
- setIsInWebapp(false);
- } else {
- setIsInWebapp(true);
- }
- }, [location]);
+ const { isInWebapp } = useRouteMode();
+ const isDarkMode = !isInWebapp;
return (
<>
- {/* Desktop navbar */}
- {ENVIRONMENT === 'development' && (
-
- Dev Environment
-
- )}
+
- {/* Mobile navbar with sidebar */}
}
- sidebarContent={(onClose) => (
-
- )}
+ isDarkMode={isDarkMode}
+ isDevelopment={ENVIRONMENT === 'development'}
+ user={user}
+ logout={logout}
+ sidebarIcon={}
+ sidebarContent={sidebarContent}
+ navLinks={MOBILE_NAV_LINKS}
/>
>
);
};
-export default PlannerNavbar;
\ No newline at end of file
+export default PlannerNavbar;
diff --git a/main/src/components/auth/forgot-password-form.tsx b/main/src/components/auth/forgot-password-form.tsx
index fd97753..b50a3a0 100644
--- a/main/src/components/auth/forgot-password-form.tsx
+++ b/main/src/components/auth/forgot-password-form.tsx
@@ -6,7 +6,7 @@ import { sendPasswordResetEmail } from "firebase/auth";
import { auth } from "@/firebase-config";
import { Link } from "react-router-dom";
-import { Button } from "@/components/ui/button";
+import { Button } from "@sage/ui";
import {
Form,
FormControl,
@@ -14,8 +14,8 @@ import {
FormItem,
FormLabel,
FormMessage,
-} from "@/components/ui/form";
-import { Input } from "@/components/ui/input";
+} from "@sage/ui";
+import { Input } from "@sage/ui";
const formSchema = z.object({
email: z.string().email({ message: "Please enter a valid email address" }),
diff --git a/main/src/components/auth/login-form.tsx b/main/src/components/auth/login-form.tsx
index be53c2f..095f718 100644
--- a/main/src/components/auth/login-form.tsx
+++ b/main/src/components/auth/login-form.tsx
@@ -13,7 +13,7 @@ import { useAuth } from "@/context/AuthContext";
import { useNavigate, useLocation, Link } from "react-router-dom";
import { Toaster, toast } from "sonner";
-import { Button } from "@/components/ui/button";
+import { Button } from "@sage/ui";
import {
Form,
FormControl,
@@ -21,9 +21,9 @@ import {
FormItem,
FormLabel,
FormMessage,
-} from "@/components/ui/form";
-import { Input } from "@/components/ui/input";
-import { Separator } from "@/components/ui/separator";
+} from "@sage/ui";
+import { Input } from "@sage/ui";
+import { Separator } from "@sage/ui";
const VITE_CRUD_API = import.meta.env.VITE_CRUD_API;
diff --git a/main/src/components/auth/signup-form.tsx b/main/src/components/auth/signup-form.tsx
index 3ebb852..259727b 100644
--- a/main/src/components/auth/signup-form.tsx
+++ b/main/src/components/auth/signup-form.tsx
@@ -11,7 +11,7 @@ import { useAuth } from "@/context/AuthContext";
import { useNavigate } from "react-router-dom";
import { Toaster, toast } from "sonner";
-import { Button } from "@/components/ui/button";
+import { Button } from "@sage/ui";
import {
Form,
FormControl,
@@ -19,9 +19,9 @@ import {
FormItem,
FormLabel,
FormMessage,
-} from "@/components/ui/form";
-import { Input } from "@/components/ui/input";
-import { Separator } from "@/components/ui/separator";
+} from "@sage/ui";
+import { Input } from "@sage/ui";
+import { Separator } from "@sage/ui";
const VITE_CRUD_API = import.meta.env.VITE_CRUD_API;
diff --git a/main/src/components/chatbot/ChatSidebarContent.tsx b/main/src/components/chatbot/ChatSidebarContent.tsx
index ca7cea6..c4fc380 100644
--- a/main/src/components/chatbot/ChatSidebarContent.tsx
+++ b/main/src/components/chatbot/ChatSidebarContent.tsx
@@ -1,29 +1,27 @@
import { MessageCirclePlusIcon, Pencil, Trash2Icon } from "lucide-react";
-import { useEffect, useState } from "react";
-import { useChatbot } from "@/hooks/useChatbot"
+import { useState } from "react";
+import { useChatbotStore } from "@/stores/chatbotStore";
import { useAuth } from "@/context/AuthContext";
-import { chatEventEmitter } from "@/utils/chatEventEmitter";
import type { Conversation } from "@/types/chat";
interface ChatSidebarContentProps {
- onClose: () => void;
+ onClose?: () => void;
+ layout?: "mobile" | "template";
}
-const ChatSidebarContent: React.FC = ({ onClose }) => {
+const ChatSidebarContent: React.FC = ({ onClose, layout = "mobile" }) => {
const { user } = useAuth();
const {
conversations,
- conversation_id,
+ activeConversationId,
error,
loading,
- setConversations,
+ setActiveConversationId,
+ startNewChat,
deleteConversation,
renameConversation,
- setConversationId,
- initialLoad
- } = useChatbot();
+ } = useChatbotStore();
- // Modal states
const [showRenameModal, setShowRenameModal] = useState(false);
const [newName, setNewName] = useState("");
const [renaming, setRenaming] = useState(false);
@@ -32,31 +30,6 @@ const ChatSidebarContent: React.FC = ({ onClose }) => {
const [conversationToDelete, setConversationToDelete] = useState(null);
const [deleting, setDeleting] = useState(false);
- useEffect(() => {
- initialLoad();
- }, []);
-
- useEffect(() => {
- // Sync conversations from ChatBot when they update
- const handleConversationUpdate = (updatedConversations: Conversation[]) => {
- setConversations(updatedConversations);
- };
-
- // Sync active conversation ID from ChatBot
- const handleActiveConversationUpdate = (newId: string | null) => {
- setConversationId(newId);
- };
-
- chatEventEmitter.on('conversationUpdate', handleConversationUpdate);
- chatEventEmitter.on('activeConversationUpdate', handleActiveConversationUpdate);
- chatEventEmitter.emit('requestConversations'); // Request current state from ChatBot on mount
-
- return () => {
- chatEventEmitter.off('conversationUpdate', handleConversationUpdate);
- chatEventEmitter.off('activeConversationUpdate', handleActiveConversationUpdate);
- };
- }, []);
-
const groupConversationsByDate = (convs: Conversation[]) => {
const today = new Date();
const todayStart = new Date(today.getFullYear(), today.getMonth(), today.getDate());
@@ -80,14 +53,14 @@ const ChatSidebarContent: React.FC = ({ onClose }) => {
return { todayChats, pastChats };
};
- const truncateText = (text: string, maxLength: number = 24) => {
+ const truncateText = (text: string, maxLength = 24) => {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength) + "...";
};
const renderConversationItem = (conv: Conversation) => {
const displayName = conv.title || conv.conversation_name || conv.messages?.[0]?.content || "No messages";
- const active = conversation_id === conv.conversation_id;
+ const active = activeConversationId === conv.conversation_id;
const displayText = truncateText(displayName, 20);
return (
@@ -96,13 +69,8 @@ const ChatSidebarContent: React.FC = ({ onClose }) => {