From f1d231d101457cd0cd37e5d9ab4bae0da425d8fd Mon Sep 17 00:00:00 2001 From: priyanshuvish Date: Fri, 10 Apr 2026 16:38:25 +0530 Subject: [PATCH] working on learner --- index.html | 2 +- package-lock.json | 100 ++ package.json | 2 + src/components/TopNav.tsx | 35 +- src/components/toast/useToast.ts | 17 + src/components/ui/sonner.tsx | 31 +- src/context/AuthContext.tsx | 136 ++ src/index.css | 4 + src/layouts/HRLayout.tsx | 8 +- src/layouts/components/HRSidebar.tsx | 38 +- src/main.tsx | 6 +- src/pages/DiscussionsPage/DiscussionsPage.tsx | 878 ++++-------- src/pages/DiscussionsPage/DiscussionsView.tsx | 296 ++++ src/pages/Learners/LearnersPage.tsx | 1265 +++++++++++++++-- src/pages/Login.tsx | 105 ++ src/redux/hooks.ts | 6 + src/redux/services/forumApi.ts | 139 ++ src/redux/services/learnersApi.ts | 461 ++++++ src/redux/services/loginApi.ts | 52 + src/redux/store.ts | 17 + src/routes/ProtectedRoute.tsx | 14 + src/routes/RootLayout.tsx | 12 + src/routes/index.tsx | 82 +- src/vite-env.d.ts | 1 + 24 files changed, 2843 insertions(+), 864 deletions(-) create mode 100644 src/components/toast/useToast.ts create mode 100644 src/context/AuthContext.tsx create mode 100644 src/pages/DiscussionsPage/DiscussionsView.tsx create mode 100644 src/pages/Login.tsx create mode 100644 src/redux/hooks.ts create mode 100644 src/redux/services/forumApi.ts create mode 100644 src/redux/services/learnersApi.ts create mode 100644 src/redux/services/loginApi.ts create mode 100644 src/redux/store.ts create mode 100644 src/routes/ProtectedRoute.tsx create mode 100644 src/routes/RootLayout.tsx create mode 100644 src/vite-env.d.ts diff --git a/index.html b/index.html index d27f1dd..a2f906a 100644 --- a/index.html +++ b/index.html @@ -7,7 +7,7 @@ HR Portal Dashboard version 0.1 - +
diff --git a/package-lock.json b/package-lock.json index 764e1f2..086c17d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "@radix-ui/react-toggle": "^1.1.2", "@radix-ui/react-toggle-group": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", + "@reduxjs/toolkit": "^2.11.2", "@tailwindcss/postcss": "^4.1.12", "class-variance-authority": "^0.7.1", "clsx": "*", @@ -47,6 +48,7 @@ "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", "react-hook-form": "^7.55.0", + "react-redux": "^9.2.0", "react-resizable-panels": "^2.1.7", "react-router-dom": "^7.13.1", "recharts": "^2.15.2", @@ -1890,6 +1892,32 @@ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -2177,6 +2205,18 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@swc/core": { "version": "1.13.5", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz", @@ -2764,6 +2804,12 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@vitejs/plugin-react-swc": { "version": "3.11.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz", @@ -3143,6 +3189,16 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/input-otp": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz", @@ -3626,6 +3682,29 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-remove-scroll": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", @@ -3806,6 +3885,27 @@ "decimal.js-light": "^2.4.1" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/rollup": { "version": "4.49.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.49.0.tgz", diff --git a/package.json b/package.json index 9c91917..659f251 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@radix-ui/react-toggle": "^1.1.2", "@radix-ui/react-toggle-group": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", + "@reduxjs/toolkit": "^2.11.2", "@tailwindcss/postcss": "^4.1.12", "class-variance-authority": "^0.7.1", "clsx": "*", @@ -42,6 +43,7 @@ "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", "react-hook-form": "^7.55.0", + "react-redux": "^9.2.0", "react-resizable-panels": "^2.1.7", "react-router-dom": "^7.13.1", "recharts": "^2.15.2", diff --git a/src/components/TopNav.tsx b/src/components/TopNav.tsx index aa6b5a3..222d00c 100644 --- a/src/components/TopNav.tsx +++ b/src/components/TopNav.tsx @@ -18,6 +18,7 @@ import { Button } from './ui/button'; import { Badge } from './ui/badge'; import { Avatar, AvatarFallback } from './ui/avatar'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from './ui/dropdown-menu'; +import { useAuth } from '../context/AuthContext'; interface TopNavProps { onMenuToggle?: () => void; @@ -61,6 +62,19 @@ export const TopNav: React.FC = ({ notificationCount = 0 }) => { const navigate = useNavigate(); + const { user, logout } = useAuth(); + const displayName = + user?.display_name?.trim() || + [user?.first_name, user?.last_name].filter(Boolean).join(' ').trim() || + 'HR User'; + const emailLabel = user?.email_address ?? 'hr@klc.com'; + const userInitials = + displayName + .split(/\s+/) + .map((p) => p[0]) + .join('') + .slice(0, 2) + .toUpperCase() || 'HR'; const [preferences, setPreferences] = useLocalStorage('userPreferences', { darkMode: false, prefersReducedMotion: false @@ -76,12 +90,7 @@ export const TopNav: React.FC = ({ }; const handleSignOut = () => { - // Add your sign out logic here - console.log('Signing out...'); - // Clear any auth tokens/user data - localStorage.removeItem('authToken'); - sessionStorage.clear(); - navigate('/login'); + logout(); }; const handleProfileClick = () => { @@ -105,7 +114,7 @@ export const TopNav: React.FC = ({ }; return ( -
+
{/* Left Section */}
{showMenuButton && ( @@ -178,12 +187,12 @@ export const TopNav: React.FC = ({ > - HR + {userInitials}
-

HR Manager

-

hr@klc.com

+

{displayName}

+

{emailLabel}

@@ -194,12 +203,12 @@ export const TopNav: React.FC = ({
- HR + {userInitials}
-

HR Manager

-

hr@klc.com

+

{displayName}

+

{emailLabel}

Administrator diff --git a/src/components/toast/useToast.ts b/src/components/toast/useToast.ts new file mode 100644 index 0000000..dd415bd --- /dev/null +++ b/src/components/toast/useToast.ts @@ -0,0 +1,17 @@ +import { toast } from 'sonner'; + +export type ToastVariant = 'success' | 'error' | 'info'; + +export function useToast() { + const showToast = (title: string, description: string, variant: ToastVariant = 'info') => { + if (variant === 'success') { + toast.success(title, { description }); + } else if (variant === 'error') { + toast.error(title, { description }); + } else { + toast(title, { description }); + } + }; + + return { showToast, toast }; +} diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx index 40842fb..83de216 100644 --- a/src/components/ui/sonner.tsx +++ b/src/components/ui/sonner.tsx @@ -1,20 +1,33 @@ -"use client"; - -import { useTheme } from "next-themes@0.4.6"; -import { Toaster as Sonner, ToasterProps } from "sonner@2.0.3"; +import * as React from 'react'; +import { Toaster as Sonner, type ToasterProps } from 'sonner'; const Toaster = ({ ...props }: ToasterProps) => { - const { theme = "system" } = useTheme(); + const [theme, setTheme] = React.useState(() => + typeof document !== 'undefined' && + document.documentElement.classList.contains('dark') + ? 'dark' + : 'light', + ); + + React.useEffect(() => { + const el = document.documentElement; + const update = () => + setTheme(el.classList.contains('dark') ? 'dark' : 'light'); + update(); + const observer = new MutationObserver(update); + observer.observe(el, { attributes: true, attributeFilter: ['class'] }); + return () => observer.disconnect(); + }, []); return ( Promise; + logout: () => void; + isAuthenticated: boolean; + token: string | null; +} + +const AuthContext = createContext(undefined); + +export const AuthProvider = ({ children }: { children: ReactNode }) => { + const [user, setUser] = useState(() => { + const storedUser = localStorage.getItem("user"); + if (storedUser) { + try { + // Try to parse as JSON first + return JSON.parse(storedUser); + } catch { + // If it fails (like "admin" string), return null or handle accordingly + return null; + } + } + return null; + }); + + const [token, setToken] = useState(() => { + const storedToken = localStorage.getItem("token"); + return storedToken || null; + }); + + const navigate = useNavigate(); + const { showToast } = useToast(); + const [loginMutation] = useLoginMutation(); + + const login = async (email: string, password: string) => { + try { + const response = await loginMutation({ + email_address: email, + password + }).unwrap(); + + if (response.success) { + // Store token and user info + localStorage.setItem("token", response.data.access_token); + localStorage.setItem("user", JSON.stringify(response.data.user_info)); + + setToken(response.data.access_token); + setUser(response.data.user_info); + + showToast( + "Login Successful", + response.message || "You have been logged in successfully.", + "success" + ); + + navigate("/hr/dashboard"); + } else { + showToast( + "Login Failed", + response.message || "Invalid credentials. Please try again.", + "error" + ); + } + } catch (error: any) { + console.error("Login error:", error); + + showToast( + "Error", + error.data?.message || error.message || "An error occurred during login", + "error" + ); + } + }; + + const logout = () => { + localStorage.removeItem("token"); + localStorage.removeItem("user"); + setToken(null); + setUser(null); + + showToast( + "Logged Out", + "You have been logged out successfully.", + "success" + ); + + navigate("/login"); + }; + + const isAuthenticated = !!token && !!user; + + // Optional: Add token expiration check + useEffect(() => { + if (token) { + try { + // Decode token to check expiration (if needed) + const payload = JSON.parse(atob(token.split('.')[1])); + const exp = payload.exp * 1000; // Convert to milliseconds + const now = Date.now(); + + if (now >= exp) { + // Token expired + logout(); + showToast( + "Session Expired", + "Your session has expired. Please login again.", + "error" + ); + } + } catch (error) { + // Invalid token format + console.error("Error checking token expiration:", error); + } + } + }, [token]); + + return ( + + {children} + + ); +}; + +export const useAuth = () => { + const ctx = useContext(AuthContext); + if (!ctx) throw new Error("useAuth must be used inside AuthProvider"); + return ctx; +}; \ No newline at end of file diff --git a/src/index.css b/src/index.css index feac2e0..fe41d73 100644 --- a/src/index.css +++ b/src/index.css @@ -1141,6 +1141,10 @@ flex: 1; } + .cs-height{ + height: calc(100vh - 56px); + } + .flex-shrink-0, .shrink-0 { flex-shrink: 0; } diff --git a/src/layouts/HRLayout.tsx b/src/layouts/HRLayout.tsx index 59daafc..e8b6957 100644 --- a/src/layouts/HRLayout.tsx +++ b/src/layouts/HRLayout.tsx @@ -71,13 +71,13 @@ const HRLayout: React.FC = () => { {/* Main Content */}
-
- -
+ {/*
+
*/} +
diff --git a/src/layouts/components/HRSidebar.tsx b/src/layouts/components/HRSidebar.tsx index 0856d3d..ee152d7 100644 --- a/src/layouts/components/HRSidebar.tsx +++ b/src/layouts/components/HRSidebar.tsx @@ -1,13 +1,13 @@ +import { + BarChart3, + Home, + MessageSquare, + Users +} from 'lucide-react'; import React from 'react'; import { NavLink } from 'react-router-dom'; -import { - Home, - Users, - BarChart3, - MessageSquare -} from 'lucide-react'; -import { Button } from '../../components/ui/button'; import { useLocalStorage } from '../../hooks/useLocalStorage'; +import { useAuth } from '../../context/AuthContext'; const menuItems = [ { id: 'dashboard', label: 'Dashboard', icon: Home, path: '/hr/dashboard' }, @@ -23,17 +23,27 @@ interface HRSidebarProps { export const HRSidebar: React.FC = ({ className = '', onNavigate }) => { const [prefersReducedMotion] = useLocalStorage('prefersReducedMotion', false); + const { user } = useAuth(); + const orgName = user?.principal_organization_name?.trim() || 'Tech Solutions Pvt Ltd'; + const orgInitials = + orgName + .split(/\s+/) + .filter(Boolean) + .map((part) => part[0]) + .join('') + .slice(0, 2) + .toUpperCase() || 'TS'; return ( -
-
-
-
- TS +
+
+
+
+ {orgInitials} +
+ {orgName}
- Tech Solutions Pvt Ltd
-