From f1d2e90bbb1d385389f968bf9edaca02e701d80d Mon Sep 17 00:00:00 2001 From: ayishanishana21 Date: Thu, 13 Nov 2025 10:23:47 +0530 Subject: [PATCH 1/2] Update DashboardLayout and RoleBasedMenu --- .../src/features/dashboard/RoleBasedMenu.ts | 159 +++++--- .../dashboard/pages/DashboardLayout.tsx | 385 +++++++++++++----- 2 files changed, 378 insertions(+), 166 deletions(-) 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 */} + + + + + + + + ); +} From e5efc748747434beb98e0bb12c1bea2062d12b1f Mon Sep 17 00:00:00 2001 From: ayishanishana21 Date: Mon, 17 Nov 2025 14:16:11 +0530 Subject: [PATCH 2/2] Updated RegisterPage, Footer, schema, AppBar, CascadingFilters, and App --- frontend/src/App.tsx | 102 +-- frontend/src/components/homepage/AppBar.tsx | 761 +++++++++--------- .../components/homepage/CascadingFilters.tsx | 378 +++++---- frontend/src/components/homepage/Footer.tsx | 177 ++-- .../src/features/auth/pages/RegisterPage.tsx | 209 +++++ frontend/src/features/auth/schema.ts | 98 ++- 6 files changed, 1036 insertions(+), 689 deletions(-) create mode 100644 frontend/src/features/auth/pages/RegisterPage.tsx 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 - /> - - - - - - {/* - - - {/* - - - - ); -} +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 + /> + + + + + + {/* + + + {/* + + + + ); +} 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 */} + + + {/* 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;