diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 7c8e0be..e553eea 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,50 +1,52 @@
-
-import { Routes, Route } from "react-router-dom";
-import MegaMenu from "./components/homepage/AppBar";
-// import FeatureTiles from "./components/homepage/HomeComponents";
-import HomePage from "./pages/home/Homepage";
-import DomainPage from "./pages/public/DomainsPage";
-import CoursePage from "./pages/public/CoursePage";
-import TutorialSearch from "./pages/public/TutorialSearch";
-import SubscriptionPage from "./pages/public/SubscriptionPage";
-import LoginPage from "./features/auth/pages/LoginPage";
-import DashboardLayout from "./features/dashboard/pages/DashboardLayout";
-import PublicLayout from "./pages/public/PublicLayout";
-import Dashboard from "./features/dashboard/pages/Dashboard";
-import TrainingPlanner from "./features/training/pages/TrainingPlanner";
-import TrainingAttendance from "./features/training/pages/TrainingAttendance";
-
-
-export default function App(){
-
- return (
- <>
- {/* */}
- {/* */}
- {/* */}
-
-
- {/* Public routes */}
- }>
- } />
- } />
- } />
- } />
- } />
- } />
-
-
- {/* Dashboard routes */}
- }>
- } />
- } />
- } />
- {/* } /> */}
-
-
- {/* catch-all for 404 */}
- Page Not Found} />
-
- >
- )
-}
+
+import { Routes, Route } from "react-router-dom";
+import MegaMenu from "./components/homepage/AppBar";
+// import FeatureTiles from "./components/homepage/HomeComponents";
+import HomePage from "./pages/home/Homepage";
+import DomainPage from "./pages/public/DomainsPage";
+import CoursePage from "./pages/public/CoursePage";
+import TutorialSearch from "./pages/public/TutorialSearch";
+import SubscriptionPage from "./pages/public/SubscriptionPage";
+import LoginPage from "./features/auth/pages/LoginPage";
+import DashboardLayout from "./features/dashboard/pages/DashboardLayout";
+import PublicLayout from "./pages/public/PublicLayout";
+import Dashboard from "./features/dashboard/pages/Dashboard";
+import TrainingPlanner from "./features/training/pages/TrainingPlanner";
+import TrainingAttendance from "./features/training/pages/TrainingAttendance";
+import RegisterPage from "./features/auth/pages/RegisterPage";
+
+
+export default function App(){
+
+ return (
+ <>
+ {/* */}
+ {/* */}
+ {/* */}
+
+
+ {/* Public routes */}
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+ {/* Dashboard routes */}
+ }>
+ } />
+ } />
+ } />
+ {/* } /> */}
+
+
+ {/* catch-all for 404 */}
+ Page Not Found} />
+
+ >
+ )
+}
diff --git a/frontend/src/components/homepage/AppBar.tsx b/frontend/src/components/homepage/AppBar.tsx
index 6abc743..e3d4fd4 100644
--- a/frontend/src/components/homepage/AppBar.tsx
+++ b/frontend/src/components/homepage/AppBar.tsx
@@ -1,373 +1,388 @@
-// ResponsiveAppBar.tsx
-import * as React from "react";
-import {
- AppBar, Toolbar, IconButton, Box, Button, Typography, Drawer,
- List, ListItemButton, ListItemText, Collapse, Divider, Popover,
- Link as MLink, Chip
-} from "@mui/material";
-import MenuIcon from "@mui/icons-material/Menu";
-import ExpandMore from "@mui/icons-material/ExpandMore";
-import ExpandLess from "@mui/icons-material/ExpandLess";
-import { useTheme } from "@mui/material/styles";
-import BrandLogo from "./BrandLogo";
-import Login from "../../features/auth/components/Login";
-import { useNavigate } from "react-router-dom";
-
-
-
-/* ---------- Types ---------- */
-type ChildLink = { section: string; label: string; href?: string; badge?: "new" | "beta" };
-type NavItem = { label: string; children?: ChildLink[] };
-
-/* ---------- Dummy data for mega-menu ---------- */
-const NAV_ITEMS: NavItem[] = [
- {
- label: "Software Training",
- children: [
- { section: "Software Training", label: "About the Training", href: "https://process.spoken-tutorial.org/index.php/Software-Training#About_SELF_Workshops" },
- { section: "Software Training", label: "Progress to Date", href: "https://process.spoken-tutorial.org/index.php/Software-Training#Progress_To_Date" },
- { section: "Software Training", label: "Software Offered", href: "https://process.spoken-tutorial.org/index.php/Software-Training#Software_Offered" },
- { section: "Software Training", label: "Contacts for Training", href: "https://process.spoken-tutorial.org/index.php/Software-Training#Contacts_For_Training" },
- { section: "Software Training", label: "Change in Training Policy", href: "https://spoken-tutorial.org/change-in-policy/" },
-
- { section: "Procedures", label: "Organising Training", href: "https://process.spoken-tutorial.org/index.php/Software-Training#Organising_Training" },
- { section: "Procedures", label: "Instruction for Downloading Tutorials", href: "https://process.spoken-tutorial.org/index.php/Software-Training#Downloading_Spoken_Tutorials" },
- { section: "Procedures", label: "Create Your Own Disc Image" },
- { section: "Procedures", label: "Resource Centers" },
-
- { section: "Training", label: "Training & Payment Dashboard" },
- { section: "Training", label: "STPS (Semester Planner) Summary" },
- { section: "Training", label: "Student Dashboard" },
- { section: "Training", label: "Individual Learning", badge: "new" },
- { section: "Training", label: "Verify ILW Test Certificate" },
- { section: "Training", label: "Email Verification Link", href: "https://spoken-tutorial.org/accounts/verify/" },
- { section: "Training", label: "Subscription", href: "/subscription" },
-
- { section: "Online Test", label: "Instruction for Invigilator", href: "https://process.spoken-tutorial.org/images/0/09/Instructions_for_Invigilator.pdf" },
- { section: "Online Test", label: "Instruction for Participants", href: "https://process.spoken-tutorial.org/images/9/95/Test_Instruction_for_Participants.pdf" },
- { section: "Online Test", label: "Certificate Verification Link", href: "https://spoken-tutorial.org/software-training/test/verify-test-certificate/" },
- { section: "Online Test", label: "Job Recommendation", badge: "new", href: "https://jrs.spoken-tutorial.org/" },
- ],
- },
- {
- label: "Creation",
- children: [
- { section: "Media", label: "Videos", href: "https://files.spoken-tutorial.org/english-videos/" },
- { section: "Media", label: "Graphics" },
- { section: "Media", label: "Docs" },
- { section: "Tools", label: "Script Templates" },
- { section: "Tools", label: "Brand Assets", badge: "beta" },
- { section: "Process", label: "Creation Process", href: "https://process.spoken-tutorial.org/index.php/Main_Page" },
- { section: "Process", label: "Outline and Script", href: "https://script.spoken-tutorial.org/index.php/Main_Page" },
- ],
- },
- { label: "News" },
- {
- label: "Academics",
- children: [
- { section: "Programs", label: "Courses" },
- { section: "Programs", label: "Workshops" },
- { section: "Resources", label: "Syllabi" },
- { section: "Resources", label: "Reading List" },
- ],
- },
- { label: "About Us" },
- { label: "Statistics" },
-];
-
-export default function ResponsiveAppBar() {
- const theme = useTheme();
- const navigate = useNavigate();
- const contrast = theme.palette.getContrastText(theme.palette.primary.main);
-
- /* ---------- Mobile drawer ---------- */
- const [drawerOpen, setDrawerOpen] = React.useState(false);
- const toggleDrawer = (val: boolean) => () => setDrawerOpen(val);
-
- /* ---------- Mobile submenu expand ---------- */
- const [expanded, setExpanded] = React.useState>({});
- const toggleExpand = (key: string) => setExpanded((s) => ({ ...s, [key]: !s[key] }));
-
- /* ---------- Desktop popover (click-to-open) ---------- */
- const [desktopMenu, setDesktopMenu] = React.useState<{ key: string | null; anchor: HTMLElement | null; }>(
- { key: null, anchor: null }
- );
- const openDesktopMenu = (key: string) => (e: React.MouseEvent) =>
- setDesktopMenu({ key, anchor: e.currentTarget });
- const closeDesktopMenu = () => setDesktopMenu({ key: null, anchor: null });
-
- /* ---------- Navigate (stub) ---------- */
- const go = (label: string) => {
- console.log("navigate to:", label);
- navigate("login/")
- closeDesktopMenu();
- setDrawerOpen(false);
- };
-
- /* ---------- Helpers ---------- */
- const groupBySection = (children: ChildLink[] = []) => {
- const map = new Map();
- children.forEach((c) => {
- if (!map.has(c.section)) map.set(c.section, []);
- map.get(c.section)!.push(c);
- });
- return Array.from(map.entries()); // [ [section, links[]], ... ]
- };
-
- const mediaUrl = import.meta.env.VITE_API_MEDIA_URL
- console.log(`mediaURL ********** ${mediaUrl}`)
-
- return (
- <>
-
-
- {/* Left logo */}
- {/* */}
- {/* LEFT logo (keep label hidden on xs; show from md if you want) */}
-
-
-
- {/* Desktop nav (centered) */}
-
- {NAV_ITEMS.map((item) => {
- const key = item.label.replace(/\s+/g, "_");
- const hasChildren = !!item.children?.length;
- const isActive = desktopMenu.key === key;
-
- const groups = isActive ? groupBySection(item.children) : [];
- const colCount = Math.min(groups.length || 1, 4); // up to 4 columns
- const COL_W = 240; // target width per column
- const P_X = 24; // horizontal padding (pop paper p:2.5)
- const paperWidth = Math.min(colCount * COL_W + 2 * P_X, 1000);
-
- return (
-
- {/* Top-level text link */}
- go(item.label)}
- sx={{
- display: "flex", alignItems: "center", gap: 0.5,
- px: 1, py: 0.75, borderRadius: 1, cursor: "pointer",
- fontSize: 14, fontWeight: 500, color: contrast,
- "&:hover": { backgroundColor: "rgba(255,255,255,0.12)" }
- }}
- >
- {item.label}
- {hasChildren && }
-
-
- {/* Auto-sizing Mega-menu Popover */}
- {hasChildren && isActive && (
-
- .col": { minWidth: 200 },
- }}
- >
- {groups.map(([section, links]) => (
-
-
- {section}
-
-
-
- {links.map((l) => (
- go(`${item.label} / ${l.label}`)}
- sx={{
- display: "inline-flex",
- alignItems: "center",
- gap: 0.75,
- py: 0.75,
- px: 1,
- borderRadius: 1,
- color: "text.primary",
- fontSize: 12.5,
- "&:hover": { bgcolor: "action.hover", color: "primary.main" },
- }}
- >
- {l.label}
- {l.badge && (
-
- )}
-
- ))}
-
-
- ))}
-
-
- )}
-
- );
- })}
-
-
- {/* Spacer for center alignment */}
-
-
- {/* Right actions (desktop) */}
- {/*
-
-
-
- */}
- {/* RIGHT logo (slightly larger, label optional) */}
-
-
-
-
-
-
-
-
-
- {/* Hamburger (mobile) */}
-
-
-
-
-
-
-
-
- {/* Mobile drawer */}
-
-
-
- Spoken Tutorial
-
-
-
-
- {NAV_ITEMS.map((item) => {
- const key = item.label.replace(/\s+/g, "_");
- const hasChildren = !!item.children?.length;
- const groups = hasChildren ? groupBySection(item.children) : [];
-
- return (
-
- toggleExpand(key) : () => go(item.label)}>
-
- {hasChildren ? (expanded[key] ? : ) : null}
-
-
- {hasChildren && (
-
-
- {groups.map(([section, links]) => (
-
-
- {section}
-
- {links.map((l) => (
- go(`${item.label} / ${l.label}`)}>
-
- {l.badge && }
-
- ))}
-
- ))}
-
-
- )}
-
- );
- })}
-
-
-
-
-
-
-
-
- >
- );
-}
+// ResponsiveAppBar.tsx
+import * as React from "react";
+import {
+ AppBar, Toolbar, IconButton, Box, Button, Typography, Drawer,
+ List, ListItemButton, ListItemText, Collapse, Divider, Popover,
+ Link as MLink, Chip
+} from "@mui/material";
+import MenuIcon from "@mui/icons-material/Menu";
+import ExpandMore from "@mui/icons-material/ExpandMore";
+import ExpandLess from "@mui/icons-material/ExpandLess";
+import { useTheme } from "@mui/material/styles";
+import BrandLogo from "./BrandLogo";
+import Login from "../../features/auth/components/Login";
+import Register from "../../features/auth/pages/RegisterPage";
+import { useNavigate } from "react-router-dom";
+
+
+
+/* ---------- Types ---------- */
+type ChildLink = { section: string; label: string; href?: string; badge?: "new" | "beta" };
+type NavItem = { label: string; children?: ChildLink[] };
+
+/* ---------- Dummy data for mega-menu ---------- */
+const NAV_ITEMS: NavItem[] = [
+ {
+ label: "Software Training",
+ children: [
+ { section: "Software Training", label: "About the Training", href: "https://process.spoken-tutorial.org/index.php/Software-Training#About_SELF_Workshops" },
+ { section: "Software Training", label: "Progress to Date", href: "https://process.spoken-tutorial.org/index.php/Software-Training#Progress_To_Date" },
+ { section: "Software Training", label: "Software Offered", href: "https://process.spoken-tutorial.org/index.php/Software-Training#Software_Offered" },
+ { section: "Software Training", label: "Contacts for Training", href: "https://process.spoken-tutorial.org/index.php/Software-Training#Contacts_For_Training" },
+ { section: "Software Training", label: "Change in Training Policy", href: "https://spoken-tutorial.org/change-in-policy/" },
+
+ { section: "Procedures", label: "Organising Training", href: "https://process.spoken-tutorial.org/index.php/Software-Training#Organising_Training" },
+ { section: "Procedures", label: "Instruction for Downloading Tutorials", href: "https://process.spoken-tutorial.org/index.php/Software-Training#Downloading_Spoken_Tutorials" },
+ { section: "Procedures", label: "Create Your Own Disc Image" },
+ { section: "Procedures", label: "Resource Centers" },
+
+ { section: "Training", label: "Training & Payment Dashboard" },
+ { section: "Training", label: "STPS (Semester Planner) Summary" },
+ { section: "Training", label: "Student Dashboard" },
+ { section: "Training", label: "Individual Learning", badge: "new" },
+ { section: "Training", label: "Verify ILW Test Certificate" },
+ { section: "Training", label: "Email Verification Link", href: "https://spoken-tutorial.org/accounts/verify/" },
+ { section: "Training", label: "Subscription", href: "/subscription" },
+
+ { section: "Online Test", label: "Instruction for Invigilator", href: "https://process.spoken-tutorial.org/images/0/09/Instructions_for_Invigilator.pdf" },
+ { section: "Online Test", label: "Instruction for Participants", href: "https://process.spoken-tutorial.org/images/9/95/Test_Instruction_for_Participants.pdf" },
+ { section: "Online Test", label: "Certificate Verification Link", href: "https://spoken-tutorial.org/software-training/test/verify-test-certificate/" },
+ { section: "Online Test", label: "Job Recommendation", badge: "new", href: "https://jrs.spoken-tutorial.org/" },
+ ],
+ },
+ {
+ label: "Creation",
+ children: [
+ { section: "Media", label: "Videos", href: "https://files.spoken-tutorial.org/english-videos/" },
+ { section: "Media", label: "Graphics" },
+ { section: "Media", label: "Docs" },
+ { section: "Tools", label: "Script Templates" },
+ { section: "Tools", label: "Brand Assets", badge: "beta" },
+ { section: "Process", label: "Creation Process", href: "https://process.spoken-tutorial.org/index.php/Main_Page" },
+ { section: "Process", label: "Outline and Script", href: "https://script.spoken-tutorial.org/index.php/Main_Page" },
+ ],
+ },
+ { label: "News" },
+ {
+ label: "Academics",
+ children: [
+ { section: "Programs", label: "Courses" },
+ { section: "Programs", label: "Workshops" },
+ { section: "Resources", label: "Syllabi" },
+ { section: "Resources", label: "Reading List" },
+ ],
+ },
+ { label: "About Us" },
+ { label: "Statistics" },
+];
+
+export default function ResponsiveAppBar() {
+ const theme = useTheme();
+ const navigate = useNavigate();
+ const contrast = theme.palette.getContrastText(theme.palette.primary.main);
+
+ /* ---------- Mobile drawer ---------- */
+ const [drawerOpen, setDrawerOpen] = React.useState(false);
+ const toggleDrawer = (val: boolean) => () => setDrawerOpen(val);
+
+ /* ---------- Mobile submenu expand ---------- */
+ const [expanded, setExpanded] = React.useState>({});
+ const toggleExpand = (key: string) => setExpanded((s) => ({ ...s, [key]: !s[key] }));
+
+ /* ---------- Desktop popover (click-to-open) ---------- */
+ const [desktopMenu, setDesktopMenu] = React.useState<{ key: string | null; anchor: HTMLElement | null; }>(
+ { key: null, anchor: null }
+ );
+ const openDesktopMenu = (key: string) => (e: React.MouseEvent) =>
+ setDesktopMenu({ key, anchor: e.currentTarget });
+ const closeDesktopMenu = () => setDesktopMenu({ key: null, anchor: null });
+
+ /* ---------- Navigate (stub) ---------- */
+ const go = (label: string) => {
+ console.log("navigate to:", label);
+
+ if (label === "Login") navigate("/login");
+ else if (label === "Register") navigate("/register");
+ else navigate("/");
+
+ closeDesktopMenu();
+ setDrawerOpen(false);
+};
+
+
+ /* ---------- Helpers ---------- */
+ const groupBySection = (children: ChildLink[] = []) => {
+ const map = new Map();
+ children.forEach((c) => {
+ if (!map.has(c.section)) map.set(c.section, []);
+ map.get(c.section)!.push(c);
+ });
+ return Array.from(map.entries()); // [ [section, links[]], ... ]
+ };
+
+ const mediaUrl = import.meta.env.VITE_API_MEDIA_URL
+ console.log(`mediaURL ********** ${mediaUrl}`)
+
+ return (
+ <>
+
+
+ {/* Left logo */}
+ {/* */}
+ {/* LEFT logo (keep label hidden on xs; show from md if you want) */}
+ navigate("/")}
+>
+
+
+
+ {/* Desktop nav (centered) */}
+
+ {NAV_ITEMS.map((item) => {
+ const key = item.label.replace(/\s+/g, "_");
+ const hasChildren = !!item.children?.length;
+ const isActive = desktopMenu.key === key;
+
+ const groups = isActive ? groupBySection(item.children) : [];
+ const colCount = Math.min(groups.length || 1, 4); // up to 4 columns
+ const COL_W = 240; // target width per column
+ const P_X = 24; // horizontal padding (pop paper p:2.5)
+ const paperWidth = Math.min(colCount * COL_W + 2 * P_X, 1000);
+
+ return (
+
+ {/* Top-level text link */}
+ go(item.label)}
+ sx={{
+ display: "flex", alignItems: "center", gap: 0.5,
+ px: 1, py: 0.75, borderRadius: 1, cursor: "pointer",
+ fontSize: 14, fontWeight: 500, color: contrast,
+ "&:hover": { backgroundColor: "rgba(255,255,255,0.12)" }
+ }}
+ >
+ {item.label}
+ {hasChildren && }
+
+
+ {/* Auto-sizing Mega-menu Popover */}
+ {hasChildren && isActive && (
+
+ .col": { minWidth: 200 },
+ }}
+ >
+ {groups.map(([section, links]) => (
+
+
+ {section}
+
+
+
+ {links.map((l) => (
+ go(`${item.label} / ${l.label}`)}
+ sx={{
+ display: "inline-flex",
+ alignItems: "center",
+ gap: 0.75,
+ py: 0.75,
+ px: 1,
+ borderRadius: 1,
+ color: "text.primary",
+ fontSize: 12.5,
+ "&:hover": { bgcolor: "action.hover", color: "primary.main" },
+ }}
+ >
+ {l.label}
+ {l.badge && (
+
+ )}
+
+ ))}
+
+
+ ))}
+
+
+ )}
+
+ );
+ })}
+
+
+ {/* Spacer for center alignment */}
+
+
+ {/* Right actions (desktop) */}
+ {/*
+
+
+
+ */}
+ {/* RIGHT logo (slightly larger, label optional) */}
+
+
+
+ navigate("/")}
+>
+
+
+
+
+
+ {/* Hamburger (mobile) */}
+
+
+
+
+
+
+
+
+ {/* Mobile drawer */}
+
+
+
+ Spoken Tutorial
+
+
+
+
+ {NAV_ITEMS.map((item) => {
+ const key = item.label.replace(/\s+/g, "_");
+ const hasChildren = !!item.children?.length;
+ const groups = hasChildren ? groupBySection(item.children) : [];
+
+ return (
+
+ toggleExpand(key) : () => go(item.label)}>
+
+ {hasChildren ? (expanded[key] ? : ) : null}
+
+
+ {hasChildren && (
+
+
+ {groups.map(([section, links]) => (
+
+
+ {section}
+
+ {links.map((l) => (
+ go(`${item.label} / ${l.label}`)}>
+
+ {l.badge && }
+
+ ))}
+
+ ))}
+
+
+ )}
+
+ );
+ })}
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/frontend/src/components/homepage/CascadingFilters.tsx b/frontend/src/components/homepage/CascadingFilters.tsx
index b7a0cf7..03c366f 100644
--- a/frontend/src/components/homepage/CascadingFilters.tsx
+++ b/frontend/src/components/homepage/CascadingFilters.tsx
@@ -1,168 +1,210 @@
-import * as React from "react";
-import { Autocomplete, TextField, Box, Button, Grid, useTheme } from "@mui/material";
-import { useNavigate } from "react-router-dom";
-import { useFiltersStore } from "../../features/filters/store/filters";
-
-
-/** ---- Types that match your JSON exactly ---- */
-type Domain = { id: number; name: string; slug: string };
-type Foss = { id: number; name: string; slug: string; languageIds: number[] };
-type Language = { id: number; name: string };
-type DomainFoss = { domainId: number; fossId: number; primary?: boolean | 0 | 1 };
-
-type Catalog = {
- domains: Domain[];
- foss: Foss[];
- languages: Language[];
- domainFoss: DomainFoss[];
-};
-
-type Props = {
- data: Catalog;
-};
-
-export default function CascadingFiltersManyToMany({ data }: Props) {
-// export default function CascadingFiltersManyToMany() {
- const theme = useTheme();
- const navigate = useNavigate();
-
- /** ---- Data from React Query ---- */
-
-
- /** ---- Global filters from Zustand ---- */
- const filters = useFiltersStore((s) => s.filters);
- const setPartial = useFiltersStore((s) => s.setPartial);
- const resetStore = useFiltersStore((s) => s.reset);
- const toSearchParams = useFiltersStore((s) => s.toSearchParams);
-
- const onSearch = () => {
- console.log('on serach clicked');
- const qs = toSearchParams();
- navigate(`/tutorial-search?${qs.toString()}`);
- };
-
- /** ---- Fast lookup maps ---- */
- const domainByName = React.useMemo(
- () => new Map(data.domains.map((d) => [d.name, d])),
- [data.domains]
- );
- const fossByName = React.useMemo(
- () => new Map(data.foss.map((f) => [f.name, f])),
- [data.foss]
- );
- const langByName = React.useMemo(
- () => new Map(data.languages.map((l) => [l.name, l])),
- [data.languages]
- );
-
- /** ---- Adjacency maps ---- */
- const domainToFossIds = React.useMemo(() => {
- const m = new Map>();
- data.domainFoss.forEach(({ domainId, fossId }) => {
- if (!m.has(domainId)) m.set(domainId, new Set());
- m.get(domainId)!.add(fossId);
- });
- return m;
- }, [data.domainFoss]);
-
- /** ---- Derived options ---- */
- const fossOptions = React.useMemo(() => {
- if (!filters.domain) return data.foss;
- const domain = domainByName.get(filters.domain);
- if (!domain) return [];
- const ids = domainToFossIds.get(domain.id);
- if (!ids) return [];
- return data.foss.filter((f) => ids.has(f.id));
- }, [filters.domain, data.foss, domainByName, domainToFossIds]);
-
- const languageOptions = React.useMemo(() => {
- if (!filters.foss) return [];
- const foss = fossByName.get(filters.foss);
- if (!foss) return [];
- return foss.languageIds.map((id) => data.languages.find((l) => l.id === id)!).filter(Boolean);
- }, [filters.foss, fossByName, data.languages]);
-
- /** ---- Styling ---- */
- const pillInputSx = (disabled = false) => ({
- "& .MuiOutlinedInput-root": {
- bgcolor: disabled ? theme.palette.action.disabledBackground : theme.palette.primary.main,
- color: theme.palette.getContrastText(theme.palette.primary.main),
- borderRadius: 0.5,
- "& fieldset": { border: "none" },
- "& .MuiSvgIcon-root": { color: theme.palette.getContrastText(theme.palette.primary.main) },
- "& input": { cursor: "pointer" },
- },
- "& .MuiInputLabel-root": {
- color: theme.palette.getContrastText(theme.palette.primary.main),
- "&.Mui-focused": { color: theme.palette.getContrastText(theme.palette.primary.main) },
- },
- });
-
- const handleReset = () => {
- resetStore();
- };
-
- // if (filtersLoading || !data) return Loading..........
;
-
- return (
-
-
- {/* Domain */}
-
- setPartial({ domain: v ? v.name : null, foss: null, language: null })}
- options={data.domains}
- getOptionLabel={(o) => o.name}
- isOptionEqualToValue={(o, v) => o.id === v.id}
- renderInput={(params) => }
- clearOnEscape
- />
-
-
- {/* FOSS */}
-
- setPartial({ foss: v ? v.name : null, language: null })}
- options={fossOptions}
- getOptionLabel={(o) => o.name}
- isOptionEqualToValue={(o, v) => o.id === v.id}
- renderInput={(params) => }
- clearOnEscape
- />
-
-
- {/* Language */}
-
- setPartial({ language: v ? v.name : null })}
- options={languageOptions}
- getOptionLabel={(o) => o.name}
- isOptionEqualToValue={(o, v) => o.id === v.id}
- renderInput={(params) => (
-
- )}
- clearOnEscape
- />
-
-
-
-
-
- {/*
-
- {/* */}
-
- Reset
-
-
-
-
- );
-}
+import * as React from "react";
+import { Autocomplete, TextField, Box, Button, Grid, useTheme } from "@mui/material";
+import { useNavigate } from "react-router-dom";
+import { useFiltersStore } from "../../features/filters/store/filters";
+
+
+/** ---- Types that match your JSON exactly ---- */
+type Domain = { id: number; name: string; slug: string };
+type Foss = { id: number; name: string; slug: string; languageIds: number[] };
+type Language = { id: number; name: string };
+type DomainFoss = { domainId: number; fossId: number; primary?: boolean | 0 | 1 };
+
+type Catalog = {
+ domains: Domain[];
+ foss: Foss[];
+ languages: Language[];
+ domainFoss: DomainFoss[];
+};
+
+type Props = {
+ data: Catalog;
+};
+
+export default function CascadingFiltersManyToMany({ data }: Props) {
+// export default function CascadingFiltersManyToMany() {
+ const theme = useTheme();
+ const navigate = useNavigate();
+
+ /** ---- Data from React Query ---- */
+
+
+ /** ---- Global filters from Zustand ---- */
+ const filters = useFiltersStore((s) => s.filters);
+ const setPartial = useFiltersStore((s) => s.setPartial);
+ const resetStore = useFiltersStore((s) => s.reset);
+ const toSearchParams = useFiltersStore((s) => s.toSearchParams);
+
+ const onSearch = () => {
+ console.log('on serach clicked');
+ const qs = toSearchParams();
+ navigate(`/tutorial-search?${qs.toString()}`);
+ };
+
+ /** ---- Fast lookup maps ---- */
+ const domainByName = React.useMemo(
+ () => new Map(data.domains.map((d) => [d.name, d])),
+ [data.domains]
+ );
+ const fossByName = React.useMemo(
+ () => new Map(data.foss.map((f) => [f.name, f])),
+ [data.foss]
+ );
+ const langByName = React.useMemo(
+ () => new Map(data.languages.map((l) => [l.name, l])),
+ [data.languages]
+ );
+
+ /** ---- Adjacency maps ---- */
+ const domainToFossIds = React.useMemo(() => {
+ const m = new Map>();
+ data.domainFoss.forEach(({ domainId, fossId }) => {
+ if (!m.has(domainId)) m.set(domainId, new Set());
+ m.get(domainId)!.add(fossId);
+ });
+ return m;
+ }, [data.domainFoss]);
+
+ /** ---- Derived options ---- */
+ const fossOptions = React.useMemo(() => {
+ if (!filters.domain) return data.foss;
+ const domain = domainByName.get(filters.domain);
+ if (!domain) return [];
+ const ids = domainToFossIds.get(domain.id);
+ if (!ids) return [];
+ return data.foss.filter((f) => ids.has(f.id));
+ }, [filters.domain, data.foss, domainByName, domainToFossIds]);
+
+ const languageOptions = React.useMemo(() => {
+ if (!filters.foss) return [];
+ const foss = fossByName.get(filters.foss);
+ if (!foss) return [];
+ return foss.languageIds.map((id) => data.languages.find((l) => l.id === id)!).filter(Boolean);
+ }, [filters.foss, fossByName, data.languages]);
+
+ /** ---- Styling ---- */
+ // const pillInputSx = (disabled = false) => ({
+ // "& .MuiOutlinedInput-root": {
+ // bgcolor: disabled ? theme.palette.action.disabledBackground : theme.palette.primary.main,
+ // color: theme.palette.getContrastText(theme.palette.primary.main),
+ // borderRadius: 0.5,
+ // "& fieldset": { border: "none" },
+ // "& .MuiSvgIcon-root": { color: theme.palette.getContrastText(theme.palette.primary.main) },
+ // "& input": { cursor: "pointer" },
+ // },
+ // "& .MuiInputLabel-root": {
+ // color: theme.palette.getContrastText(theme.palette.primary.main),
+ // "&.Mui-focused": { color: theme.palette.getContrastText(theme.palette.primary.main) },
+ // },
+ // });
+
+
+ const pillInputSx = (disabled = false) => ({
+ "& .MuiOutlinedInput-root": {
+ bgcolor: disabled
+ ? theme.palette.action.disabledBackground
+ : theme.palette.primary.main,
+
+ color: theme.palette.common.white,
+ borderRadius: 0.5,
+ "& fieldset": { border: "none" },
+ "& .MuiSvgIcon-root": {
+ color: theme.palette.common.white,
+ },
+ "& input": { cursor: "pointer" },
+ },
+
+ // NORMAL (not shrunk)
+ "& .MuiInputLabel-root": {
+ color: theme.palette.common.white,
+ },
+
+ // SHRUNKEN (floating label)
+ "& .MuiInputLabel-shrink": {
+ color: theme.palette.grey[300], // <-- VERY IMPORTANT
+ fontWeight: 600,
+ }
+});
+
+
+ const handleReset = () => {
+ resetStore();
+ };
+
+ // if (filtersLoading || !data) return Loading..........
;
+
+ return (
+
+
+ {/* Domain */}
+
+ setPartial({ domain: v ? v.name : null, foss: null, language: null })}
+ options={data.domains}
+ getOptionLabel={(o) => o.name}
+ isOptionEqualToValue={(o, v) => o.id === v.id}
+ renderInput={(params) => }
+ clearOnEscape
+ />
+
+
+ {/* FOSS */}
+
+ setPartial({ foss: v ? v.name : null, language: null })}
+ options={fossOptions}
+ getOptionLabel={(o) => o.name}
+ isOptionEqualToValue={(o, v) => o.id === v.id}
+ renderInput={(params) => }
+ clearOnEscape
+ />
+
+
+ {/* Language */}
+
+ setPartial({ language: v ? v.name : null })}
+ options={languageOptions}
+ getOptionLabel={(o) => o.name}
+ isOptionEqualToValue={(o, v) => o.id === v.id}
+ renderInput={(params) => (
+
+ )}
+ clearOnEscape
+ />
+
+
+
+
+
+ {/* */}
+
+ Search
+
+
+
+ {/* */}
+
+ Reset
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/homepage/Footer.tsx b/frontend/src/components/homepage/Footer.tsx
index 760b835..49811eb 100644
--- a/frontend/src/components/homepage/Footer.tsx
+++ b/frontend/src/components/homepage/Footer.tsx
@@ -36,12 +36,16 @@ export default function Footer() {
mt: 8,
}}
>
-
-
+
+
{[footerLinks.column1, footerLinks.column2, footerLinks.column3, footerLinks.column4].map((column, index) => (
-
+
{column.map((link) => (
{link.label}
@@ -90,38 +97,99 @@ export default function Footer() {
))}
+
- {/* Footer Copyright and License Section - below social icons */}
+
-
-
-
- Spoken Tutorial, created on or before {footerConfig.license.year}, by{" "}
-
- IIT Bombay
-
- {" "}is licensed under a{" "}
-
- Creative Commons Attribution-ShareAlike 4.0 International License
-
- , except where stated otherwise
+
+
+
+
+ {footerConfig.contact.title}
+
+ {footerConfig.contact.lines.map((line, index) => (
+
+ {line}
+
+ ))}
+
+
+
-
- Based on a work at{" "}
+ {/* Footer Copyright and License Section - below social icons */}
+
+
+
+
+
+ Spoken Tutorial, created on or before {footerConfig.license.year}, by{" "}
+
+ IIT Bombay
+
+ {" "}is licensed under a{" "}
+
+ Creative Commons Attribution-ShareAlike 4.0 International License
+
+ , except where stated otherwise. Based on a work at{" "}
{footerConfig.license.workUrl}
@@ -129,55 +197,14 @@ export default function Footer() {
{footerConfig.license.workUrl}
+ .
-
-
- Spoken Tutorial, developed at IIT Bombay, is brought to you by EduPyramids Educational Services Private Limited (DBA: EduPyramids Educational Services Pvt Ltd.). EduPyramids Educational Services Private Limited is currently incubated at SINE IIT Bombay. All transactions will be processed under the name EduPyramids Educational Services Private Limited.
-
-
-
-
-
-
-
-
- Developed at IIT Bombay
-
-
-
-
- {footerConfig.contact.title}
-
- {footerConfig.contact.lines.map((line, index) => (
-
- {line}
-
- ))}
-
-
+
+
+ Spoken Tutorial, developed at IIT Bombay, is brought to you by EduPyramids Educational Services Private Limited (DBA: EduPyramids Educational Services Pvt Ltd.). EduPyramids Educational Services Private Limited is currently incubated at SINE IIT Bombay. All transactions will be processed under the name EduPyramids Educational Services Private Limited.
+
+
);
diff --git a/frontend/src/features/auth/pages/RegisterPage.tsx b/frontend/src/features/auth/pages/RegisterPage.tsx
new file mode 100644
index 0000000..7580c7c
--- /dev/null
+++ b/frontend/src/features/auth/pages/RegisterPage.tsx
@@ -0,0 +1,209 @@
+import { useNavigate } from "react-router-dom";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+
+import {
+ RegisterRequestSchema,
+ type RegisterRequestType,
+ type RegisterResponseType,
+} from "../schema";
+
+import { useApiMutation } from "../../../api/rq-helpers";
+import { getErrorMessage, type ApiError } from "../../../api/api-error";
+
+import {
+ Alert,
+ Box,
+ Button,
+ Paper,
+ Stack,
+ TextField,
+ Typography,
+ Checkbox,
+ FormControlLabel,
+ Link,
+} from "@mui/material";
+
+import { useState } from "react";
+import ReCAPTCHA from "react-google-recaptcha";
+
+export default function RegisterPage() {
+ const navigate = useNavigate();
+
+ const [globalErr, setGlobalErr] = useState(null);
+ const [captchaValue, setCaptchaValue] = useState(null);
+
+ const {
+ register,
+ handleSubmit,
+ setError,
+ formState: { errors, isValid },
+ } = useForm({
+ resolver: zodResolver(RegisterRequestSchema),
+ mode: "onChange",
+ });
+
+ const m = useApiMutation(
+ "post",
+ "/auth/register/",
+ undefined,
+ {
+ onSuccess: () => navigate("/login"),
+
+ onError: (err: ApiError) => {
+ const fieldErrors = err.response?.data?.errors;
+ let handled = false;
+
+ if (fieldErrors) {
+ Object.entries(fieldErrors).forEach(([field, msgs]) => {
+ const msg = Array.isArray(msgs) ? msgs[0] : String(msgs);
+
+ setError(field as keyof RegisterRequestType, {
+ type: "server",
+ message: msg,
+ });
+
+ handled = true;
+ });
+ }
+
+ if (!handled) setGlobalErr(getErrorMessage(err, "Registration failed"));
+ },
+ }
+ );
+
+ const onSubmit = (data: RegisterRequestType) => {
+ if (!captchaValue) {
+ setGlobalErr("Please complete the CAPTCHA!");
+ return;
+ }
+
+ m.mutate({
+ ...data,
+ captcha: captchaValue, // send token to backend (recommended)
+ });
+ };
+
+ return (
+
+
+
+
+ {/* Title */}
+
+ Register
+
+
+
+ (as General User)
+
+
+ {/* Global Error */}
+ {globalErr && (
+ setGlobalErr(null)}>
+ {globalErr}
+
+ )}
+
+ {/* FORM */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* CAPTCHA */}
+ {
+ setCaptchaValue(value);
+ setGlobalErr(null); // clear error if user completes captcha
+ }}
+ />
+
+ {/* Submit Button */}
+
+ {m.isPending ? "Registering..." : "Register"}
+
+
+ {/* Link to Login */}
+
+ Already Registered?{" "}
+ navigate("/login")}
+ >
+ Login here
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/features/auth/schema.ts b/frontend/src/features/auth/schema.ts
index ad128b2..21710a2 100644
--- a/frontend/src/features/auth/schema.ts
+++ b/frontend/src/features/auth/schema.ts
@@ -1,23 +1,75 @@
-import { z } from "zod";
-
-// schema for login form request
-export const LoginRequestSchema = z.object({
- username: z.string(),
- password: z.string()
-});
-
-export type LoginRequestType = z.infer;
-
-
-// schema for login form response
-export const LoginResponseSchema = z.object({
- access: z.string(),
- user: z.object({
- id: z.number(),
- username: z.string(),
- email: z.email(),
- roles: z.array(z.string()).default([]),
- })
-})
-
-export type LoginResponseType = z.infer;
\ No newline at end of file
+import { z } from "zod";
+
+// schema for login form request
+export const LoginRequestSchema = z.object({
+ username: z.string(),
+ password: z.string()
+});
+
+export type LoginRequestType = z.infer;
+
+
+// schema for login form response
+export const LoginResponseSchema = z.object({
+ access: z.string(),
+ user: z.object({
+ id: z.number(),
+ username: z.string(),
+ email: z.email(),
+ roles: z.array(z.string()).default([]),
+ })
+})
+
+export type LoginResponseType = z.infer;
+
+
+export const RegisterRequestSchema = z
+ .object({
+ username: z
+ .string()
+ .min(3, "Username must be at least 3 characters long"),
+
+ first_name: z
+ .string()
+ .min(1, "First name is required"),
+
+ last_name: z
+ .string()
+ .min(1, "Last name is required"),
+
+ email: z
+ .string()
+ .email("Enter a valid email address"),
+
+ phone: z
+ .string()
+ .regex(/^[0-9+\-() ]{8,20}$/, "Enter a valid phone number"),
+
+ password: z
+ .string()
+ .min(6, "Password must be at least 6 characters"),
+
+ confirm_password: z
+ .string()
+ .min(6, "Please retype your password"),
+ })
+ .refine((data) => data.password === data.confirm_password, {
+ message: "Passwords do not match",
+ path: ["confirm_password"],
+ });
+
+export type RegisterRequestType = z.infer;
+
+
+
+export const RegisterResponseSchema = z.object({
+ access: z.string(),
+ user: z.object({
+ id: z.number(),
+ username: z.string(),
+ email: z.string().email(),
+ roles: z.array(z.string()).default([]),
+ }),
+});
+
+export type RegisterResponseType = z.infer;
diff --git a/frontend/src/features/dashboard/RoleBasedMenu.ts b/frontend/src/features/dashboard/RoleBasedMenu.ts
index 7884db3..1afca12 100644
--- a/frontend/src/features/dashboard/RoleBasedMenu.ts
+++ b/frontend/src/features/dashboard/RoleBasedMenu.ts
@@ -1,57 +1,102 @@
-// src/menu.ts
-export type MenuItem = { label: string; to: string };
-export type MenuSection = { title: string; items: MenuItem[] };
-
-export const MENU: MenuSection[] = [
- {
- title: "Training Manager",
- items: [
- { label: "Pending / Ongoing Training Request", to: "/dashboard/training/pending" },
- { label: "Completed Training Request", to: "/dashboard/training/completed" },
- { label: "Participation Certificate Request", to: "/dashboard/training/cert-requests" },
- ],
- },
- {
- title: "Online Assessment Test",
- items: [
- { label: "Approval Pending", to: "/dashboard/assessment/approval-pending" },
- { label: "Approval Assessment Test", to: "/dashboard/assessment/approval" },
- { label: "Completed Assessment Test", to: "/dashboard/assessment/completed" },
- ],
- },
- {
- title: "Paid Workshop Events",
- items: [
- { label: "Add New Event", to: "/dashboard/events/new" },
- { label: "View / Edit Event", to: "/dashboard/events/list" },
- { label: "Approve Event Registration", to: "/dashboard/events/approve-registrations" },
- { label: "Approve Attendance for Certificates", to: "/dashboard/events/approve-attendance" },
- { label: "Event Transactions", to: "/dashboard/events/transactions" },
- { label: "CD-Download Transactions", to: "/dashboard/events/cd-transactions" },
- ],
- },
- {
- title: "Account Executive",
- items: [
- { label: "Pay here to subscribe", to: "/dashboard/accounts/pay" },
- { label: "View Payment Details", to: "/dashboard/accounts/payments" },
- ],
- },
- {
- title: "Invigilator",
- items: [
- { label: "Approval Pending", to: "/dashboard/invigilator/approval-pending" },
- { label: "Ongoing Test", to: "/dashboard/invigilator/ongoing" },
- ],
- },
- {
- title: "Organiser",
- items: [
- { label: "Semester Training Planner (STPS)", to: "/dashboard/organiser/stps" },
- { label: "Add Participant Attendance", to: "/dashboard/organiser/attendance" },
- { label: "New Test Request", to: "/dashboard/organiser/test-request" },
- { label: "Approved Assessment Test", to: "/dashboard/organiser/approved" },
- { label: "Completed Assessment Test", to: "/dashboard/organiser/completed" },
- ],
- },
-];
+export type MenuItem = { label: string; to: string };
+export type MenuSection = { title: string; subSections?: MenuSubSection[]; items?: MenuItem[] };
+export type MenuSubSection = { subtitle: string; items: MenuItem[] };
+
+export const MENU: MenuSection[] = [
+ {
+ title: "Training Manager",
+ subSections: [
+ {
+ subtitle: "Training Manager",
+ items: [
+ { label: "Pending / Ongoing Training Request", to: "/dashboard/training/pending" },
+ { label: "Completed Training Request", to: "/dashboard/training/completed" },
+ { label: "Participation Certificate Request", to: "/dashboard/training/cert-requests" },
+ ],
+ },
+ {
+ subtitle: "Online Assessment Test",
+ items: [
+ { label: "Approval Pending", to: "/dashboard/assessment/approval-pending" },
+ { label: "Approval Assessment Test", to: "/dashboard/assessment/approval" },
+ { label: "Completed Assessment Test", to: "/dashboard/assessment/completed" },
+ ],
+ },
+ {
+ subtitle: "Paid Workshop Events",
+ items: [
+ { label: "Add New Event", to: "/dashboard/events/new" },
+ { label: "View / Edit Event", to: "/dashboard/events/list" },
+ { label: "Approve Event Registration", to: "/dashboard/events/approve-registrations" },
+ { label: "Approve Event Attendance for Certificates", to: "/dashboard/events/approve-attendance" },
+ { label: "Event Participant Transaction Details", to: "/dashboard/events/transactions" },
+ { label: "CD-Download Transaction Details", to: "/dashboard/events/cd-transactions" },
+ ],
+ },
+ {
+ subtitle: "List",
+ items: [
+ { label: "Organisers List", to: "/dashboard/lists/organisers" },
+ { label: "Invigilators List", to: "/dashboard/lists/invigilators" },
+ { label: "Institution List", to: "/dashboard/lists/institutions" },
+ { label: "Account Executive List", to: "/dashboard/lists/accounts" },
+ { label: "Company List", to: "/dashboard/lists/company" },
+ ],
+ },
+ {
+ subtitle: "Testimonials",
+ items: [
+ { label: "List Testimonials", to: "/dashboard/testimonials" },
+ ],
+ },
+ {
+ subtitle: "Academic Transactions",
+ items: [
+ { label: "Transaction Details", to: "/dashboard/academics/transactions" },
+ { label: "Activated Academics", to: "/dashboard/academics/activated" },
+ { label: "Add Academic Payments", to: "/dashboard/academics/add-payments" },
+ { label: "Add Academic Payments (via CSV)", to: "/dashboard/academics/add-payments-csv" },
+ ],
+ },
+ ],
+ },
+ {
+ title: "Account Executive",
+ items: [
+ { label: "Pay here to subscribe", to: "/dashboard/accounts/pay" },
+ { label: "View Payment Details", to: "/dashboard/accounts/payments" },
+ ],
+ },
+ {
+ title: "Invigilator",
+ subSections: [
+ {
+ subtitle: "Online Assessment Test",
+ items: [
+ { label: "Approval Pending", to: "/dashboard/invigilator/approval-pending" },
+ { label: "Ongoing Test", to: "/dashboard/invigilator/ongoing" },
+ ],
+ },
+ ],
+ },
+ {
+ title: "Organiser",
+ subSections: [
+ {
+ subtitle: "Training (To start the training go here)",
+ items: [
+ { label: "Semester Training Planner Summary (STPS)", to: "/dashboard/organiser/stps" },
+ { label: "Add Participant Attendance", to: "/dashboard/organiser/attendance" },
+ ],
+ },
+ {
+ subtitle: "Online Assessment Test",
+ items: [
+ { label: "New Test Request", to: "/dashboard/organiser/test-request" },
+ { label: "Approved Assessment Test", to: "/dashboard/organiser/approved" },
+ { label: "Completed Assessment Test", to: "/dashboard/organiser/completed" },
+ ],
+ },
+ ],
+ },
+];
diff --git a/frontend/src/features/dashboard/pages/DashboardLayout.tsx b/frontend/src/features/dashboard/pages/DashboardLayout.tsx
index 6cbc338..c8cffb5 100644
--- a/frontend/src/features/dashboard/pages/DashboardLayout.tsx
+++ b/frontend/src/features/dashboard/pages/DashboardLayout.tsx
@@ -1,109 +1,276 @@
-// src/layouts/ShellLayout.tsx
-import * as React from "react";
-import {
- AppBar, Box, CssBaseline, Divider, Drawer, IconButton, List, ListItemButton,
- ListItemText, ListSubheader, Toolbar, Typography, useMediaQuery
-} from "@mui/material";
-import MenuIcon from "@mui/icons-material/Menu";
-import { useTheme } from "@mui/material/styles";
-import { NavLink, Outlet, useLocation } from "react-router-dom";
-import { MENU } from "../RoleBasedMenu"
-const drawerWidth = 280;
-
-export default function DashboardLayout() {
- const theme = useTheme();
- const isMobile = useMediaQuery(theme.breakpoints.down("md"));
- const [open, setOpen] = React.useState(!isMobile);
- const location = useLocation();
-
- // React.useEffect(() => setOpen(!isMobile), [isMobile]);
-
- const toggle = () => setOpen(v => !v);
-
- const handleNavigate = () => {
- // On mobile, auto-close when a link is clicked
- if (isMobile) setOpen(false);
- };
-
- const drawer = (
-
-
-
-
- {MENU.map(section => (
-
-
- {section.title}
-
- {section.items.map(item => (
-
-
-
- ))}
-
-
- ))}
-
-
- );
-
- return (
-
-
-
-
-
-
-
-
- Dashboard
-
-
-
-
-
- {drawer}
-
-
-
- {/* Content panel */}
-
-
-
-
-
-
-
- );
-}
+import * as React from "react";
+import {
+ AppBar,
+ Box,
+ CssBaseline,
+ Divider,
+ Drawer,
+ IconButton,
+ List,
+ ListItemButton,
+ ListItemText,
+ Toolbar,
+ Typography,
+ useMediaQuery,
+ TextField,
+ Button,
+} from "@mui/material";
+import MenuIcon from "@mui/icons-material/Menu";
+import { useTheme } from "@mui/material/styles";
+import { NavLink, Outlet, useLocation } from "react-router-dom";
+import { MENU } from "../RoleBasedMenu"; // adjust the import path
+
+const DRAWER_WIDTH = 280;
+
+export default function DashboardLayout() {
+ const theme = useTheme();
+ const isMobile = useMediaQuery(theme.breakpoints.down("md"));
+ const [isDrawerOpen, setDrawerOpen] = React.useState(!isMobile);
+ const location = useLocation();
+
+ const toggleDrawer = () => setDrawerOpen((v) => !v);
+ const handleNavigation = () => { if (isMobile) setDrawerOpen(false); };
+
+ const renderDrawerContent = () => (
+
+
+
+
+
+ {MENU.map((section) => (
+
+ {/* 🔵 Section title */}
+
+ {section.title}
+
+
+ {/* 🧩 Sub-sections */}
+
+ {section.subSections?.map((sub) => (
+
+
+ {sub.subtitle}
+
+
+ {/* 🔹 Subpoints (links) */}
+ {sub.items.map((item) => (
+
+ {/* Bullet symbol before text */}
+
+ •
+
+
+
+ ))}
+
+ ))}
+
+ {/* If section has only flat items */}
+ {section.items?.map((item) => (
+
+ {/* Bullet */}
+
+ •
+
+
+
+ ))}
+
+
+ ))}
+
+
+);
+
+
+ return (
+
+
+
+ {/* 🟦 AppBar */}
+
+
+ {/* Left side */}
+
+
+
+
+
+ Software Training Dashboard
+
+
+
+
+
+
+ {/* 🟦 Sidebar Drawer */}
+
+
+ {renderDrawerContent()}
+
+
+
+ {/* 🟨 Main content area */}
+
+
+
+
+
+
+
+ );
+}