diff --git a/package-lock.json b/package-lock.json
index 5bcb44c..a0e88c6 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.9.0",
"@tailwindcss/postcss": "^4.1.12",
"class-variance-authority": "^0.7.1",
"clsx": "*",
@@ -47,7 +48,9 @@
"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": "^6.30.1",
"recharts": "^2.15.2",
"sonner": "^2.0.3",
"tailwind-merge": "*",
@@ -1889,6 +1892,41 @@
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
"license": "MIT"
},
+ "node_modules/@reduxjs/toolkit": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz",
+ "integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==",
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "@standard-schema/utils": "^0.3.0",
+ "immer": "^10.0.3",
+ "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/@remix-run/router": {
+ "version": "1.23.0",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
+ "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
@@ -2176,6 +2214,18 @@
"win32"
]
},
+ "node_modules/@standard-schema/spec": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
+ "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
+ "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",
@@ -2763,6 +2813,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",
@@ -3129,6 +3185,16 @@
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
+ "node_modules/immer": {
+ "version": "10.1.3",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz",
+ "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==",
+ "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",
@@ -3612,6 +3678,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",
@@ -3669,6 +3758,38 @@
"react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
+ "node_modules/react-router": {
+ "version": "6.30.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz",
+ "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.30.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz",
+ "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.0",
+ "react-router": "6.30.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
"node_modules/react-smooth": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
@@ -3754,6 +3875,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 909892d..bcf4b48 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.9.0",
"@tailwindcss/postcss": "^4.1.12",
"class-variance-authority": "^0.7.1",
"clsx": "*",
@@ -42,7 +43,9 @@
"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": "^6.30.1",
"recharts": "^2.15.2",
"sonner": "^2.0.3",
"tailwind-merge": "*",
diff --git a/src/App.css b/src/App.css
new file mode 100644
index 0000000..0cf865b
--- /dev/null
+++ b/src/App.css
@@ -0,0 +1,73 @@
+/* Add this to your main global CSS file */
+@media (max-width: 640px) {
+ .employee-card-mobile-view {
+ display: block;
+ }
+
+ .employee-card-desktop-view {
+ display: none;
+ }
+}
+
+@media (min-width: 641px) {
+ .employee-card-mobile-view {
+ display: none;
+ }
+
+ .employee-card-desktop-view {
+ display: block;
+ }
+}
+
+/* Custom styles for the compact table */
+.compact-table {
+ font-size: 0.875rem;
+}
+
+.compact-table th,
+.compact-table td {
+ padding: 0.5rem 0.75rem;
+}
+
+/* Responsive table container */
+.table-container {
+ overflow-x: auto;
+ max-width: 100%;
+}
+
+/* Ensure buttons are touch-friendly on mobile */
+.min-tap-44 {
+ min-height: 44px;
+ min-width: 44px;
+}
+
+/* Truncate text for small containers */
+.truncate {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* Animation for card entrance */
+@keyframes slideUp {
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.animate-slide-up {
+ animation: slideUp 0.5s ease-out forwards;
+}
+
+/* Reduced motion support */
+@media (prefers-reduced-motion: reduce) {
+ .animate-slide-up {
+ animation: none;
+ }
+}
\ No newline at end of file
diff --git a/src/App.tsx b/src/App.tsx
index ac5b9ee..157d55b 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,2142 +1,32 @@
-import React, { useState, useEffect, useCallback } from 'react';
-import { Button } from './components/ui/button';
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './components/ui/card';
-import { Badge } from './components/ui/badge';
-import { Input } from './components/ui/input';
-import { Textarea } from './components/ui/textarea';
-import { Checkbox } from './components/ui/checkbox';
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './components/ui/select';
-import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from './components/ui/dialog';
-import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from './components/ui/sheet';
-import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './components/ui/table';
-import { Progress } from './components/ui/progress';
-import { Skeleton } from './components/ui/skeleton';
-import { Tabs, TabsContent, TabsList, TabsTrigger } from './components/ui/tabs';
-import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from './components/ui/breadcrumb';
-import { Alert, AlertDescription } from './components/ui/alert';
-import logo from '../src/assets/klc-logo.png';
-import {
- Home,
- Users,
- BarChart3,
- MessageSquare,
- Settings,
- Search,
- Filter,
- Download,
- Plus,
- Upload,
- Edit,
- MoreHorizontal,
- TrendingUp,
- Calendar,
- Clock,
- BookOpen,
- Award,
- Bell,
- ChevronDown,
- Menu,
- X,
- ArrowLeft,
- ChevronRight,
- RefreshCw,
- CheckCircle,
- AlertCircle,
- XCircle,
- FileText,
- Mail,
- Phone,
- Eye,
- Trash2,
- Building2,
- CreditCard,
- Shield,
- ExternalLink,
- Info
-} from 'lucide-react';
-
-// Types
-interface KPIData {
- title: string;
- value: number;
- change?: number;
- trend?: 'up' | 'down' | 'neutral';
-}
-
-interface Employee {
- id: string;
- name: string;
- email: string;
- phone: string;
- status: 'Active' | 'Inactive' | 'Pending';
- programme?: string;
- course?: string;
- progress?: number;
- lastActivity?: string;
-}
-
-interface Announcement {
- id: string;
- title: string;
- content: string;
- type: 'announcement' | 'reminder';
- timestamp: string;
- pinned?: boolean;
-}
-
-interface Deadline {
- id: string;
- title: string;
- type: 'webinar' | 'profiler';
- dueDate: string;
- dueTime: string;
-}
-
-interface TestimonialFormData {
- name: string;
- email: string;
- phone: string;
- organisation: string;
- programme: string;
- testimonialText: string;
- consentToPublish: boolean;
-}
-
-// Custom hooks
-const useLocalStorage = (key: string, initialValue: any) => {
- const [storedValue, setStoredValue] = useState(() => {
- try {
- const item = window.localStorage.getItem(key);
- return item ? JSON.parse(item) : initialValue;
- } catch (error) {
- return initialValue;
- }
- });
-
- const setValue = (value: any) => {
- try {
- setStoredValue(value);
- window.localStorage.setItem(key, JSON.stringify(value));
- } catch (error) {
- console.error('Error saving to localStorage:', error);
- }
- };
-
- return [storedValue, setValue];
-};
-
-const useCountUp = (end: number, duration: number = 1200) => {
- const [count, setCount] = useState(0);
-
- useEffect(() => {
- let start = 0;
- const increment = end / (duration / 16);
- const timer = setInterval(() => {
- start += increment;
- if (start >= end) {
- setCount(end);
- clearInterval(timer);
- } else {
- setCount(Math.floor(start));
- }
- }, 16);
-
- return () => clearInterval(timer);
- }, [end, duration]);
-
- return count;
-};
-
-// Mock data
-const mockKPIData: KPIData[] = [
- { title: 'Total Learners', value: 1247, change: 12, trend: 'up' },
- { title: 'Active Courses', value: 89, change: 5, trend: 'up' },
- { title: 'Completed Profilers', value: 342, change: -8, trend: 'down' },
- { title: 'Average Progress', value: 73, change: 7, trend: 'up' }
-];
-
-const mockEmployees: Employee[] = [
- { id: '1', name: 'Sarah Chen', email: 'sarah.chen@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Leadership Development', course: 'Strategic Thinking', progress: 85, lastActivity: '2 hours ago' },
- { id: '2', name: 'Michael Rodriguez', email: 'michael.r@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Technical Skills', course: 'Data Analysis', progress: 62, lastActivity: '1 day ago' },
- { id: '3', name: 'Emma Thompson', email: 'emma.thompson@company.com', phone: '+61 4XX XXX XXX', status: 'Pending', programme: 'Communication', course: 'Public Speaking', progress: 0, lastActivity: 'Never' },
- { id: '4', name: 'David Kim', email: 'david.kim@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Project Management', course: 'Agile Methodology', progress: 94, lastActivity: '3 hours ago' },
- { id: '5', name: 'Lisa Wang', email: 'lisa.wang@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Leadership Development', course: 'Team Management', progress: 78, lastActivity: '5 hours ago' },
- { id: '6', name: 'James Wilson', email: 'james.wilson@company.com', phone: '+61 4XX XXX XXX', status: 'Inactive', programme: 'Technical Skills', course: 'Programming Basics', progress: 34, lastActivity: '2 weeks ago' },
- { id: '7', name: 'Maria Garcia', email: 'maria.garcia@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Sales Training', course: 'Customer Relations', progress: 56, lastActivity: '1 day ago' },
- { id: '8', name: 'Robert Lee', email: 'robert.lee@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Leadership Development', course: 'Decision Making', progress: 89, lastActivity: '4 hours ago' },
- { id: '9', name: 'Jennifer Davis', email: 'jennifer.davis@company.com', phone: '+61 4XX XXX XXX', status: 'Pending', programme: 'Communication', course: 'Written Communication', progress: 0, lastActivity: 'Never' },
- { id: '10', name: 'Thomas Brown', email: 'thomas.brown@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Project Management', course: 'Risk Management', progress: 71, lastActivity: '6 hours ago' }
-];
-
-const mockAnnouncements: Announcement[] = [
- { id: '1', title: 'New Learning Module Available', content: 'Advanced Analytics course is now live in the system.', type: 'announcement', timestamp: '2 hours ago', pinned: true },
- { id: '2', title: 'Reminder: Quarterly Reviews Due', content: 'Please complete all quarterly progress reviews by Friday.', type: 'reminder', timestamp: '5 hours ago' },
- { id: '3', title: 'System Maintenance Scheduled', content: 'Learning platform will be offline Saturday 2-4 AM for updates.', type: 'announcement', timestamp: '1 day ago' }
-];
-
-const mockDeadlines: Deadline[] = [
- { id: '1', title: 'Leadership Webinar Series', type: 'webinar', dueDate: 'Today', dueTime: '2:00 PM' },
- { id: '2', title: 'Communication Skills Assessment', type: 'profiler', dueDate: 'Tomorrow', dueTime: '11:59 PM' },
- { id: '3', title: 'Project Management Workshop', type: 'webinar', dueDate: 'Dec 30', dueTime: '10:00 AM' },
- { id: '4', title: 'Technical Skills Profiler', type: 'profiler', dueDate: 'Jan 2', dueTime: '5:00 PM' },
- { id: '5', title: 'Team Building Session', type: 'webinar', dueDate: 'Jan 5', dueTime: '3:30 PM' }
-];
-
-// Components
-const KPICard: React.FC<{ data: KPIData; onClick?: () => void; className?: string }> = ({ data, onClick, className = '' }) => {
- const countedValue = useCountUp(data.value);
- const [prefersReducedMotion] = useLocalStorage('prefersReducedMotion', false);
-
- return (
- {
- if (e.key === 'Enter' || e.key === ' ') {
- e.preventDefault();
- onClick?.();
- }
- }}
- >
-
- {data.title}
-
-
-
-
- {prefersReducedMotion ? data.value : countedValue}
- {data.title.includes('Progress') && '%'}
-
- {data.change && (
-
- {data.trend === 'up' ? '+' : ''}{data.change}{data.title.includes('Progress') ? '%' : ''}
-
- )}
-
-
-
- );
-};
-
-const EmployeeTable: React.FC<{
- employees: Employee[];
- onEdit?: (employee: Employee) => void;
- showProgress?: boolean;
- maxHeight?: string;
-}> = ({ employees, onEdit, showProgress = true, maxHeight = "400px" }) => {
- return (
-
-
-
-
- Employee
- Email
- Phone
- Status
- {showProgress && (
- <>
- Programme/Course
- Progress
- Last Activity
- >
- )}
- Actions
-
-
-
- {employees.map((employee) => (
- onEdit?.(employee)}
- role="button"
- tabIndex={0}
- onKeyDown={(e) => {
- if (e.key === 'Enter' || e.key === ' ') {
- e.preventDefault();
- onEdit?.(employee);
- }
- }}
- >
- {employee.name}
- {employee.email}
- {employee.phone}
-
-
- {employee.status}
-
-
- Employee status is {employee.status}
-
-
- {showProgress && (
- <>
-
-
-
{employee.programme}
-
{employee.course}
-
-
-
- {employee.progress !== undefined && (
-
-
-
{employee.progress}%
-
- Progress: {employee.progress} percent complete
-
-
- )}
-
- {employee.lastActivity}
- >
- )}
-
- {
- e.stopPropagation();
- onEdit?.(employee);
- }}
- aria-label={`Edit ${employee.name}`}
- >
-
-
-
-
- ))}
-
-
-
- );
-};
-
-const HRSidebar: React.FC<{
- activeScreen: string;
- onNavigate: (screen: string) => void;
- className?: string;
-}> = ({ activeScreen, onNavigate, className = '' }) => {
- const [prefersReducedMotion] = useLocalStorage('prefersReducedMotion', false);
-
- const menuItems = [
- { id: 'home', label: 'Dashboard', icon: Home, path: '/hr/home' },
- { id: 'learners', label: 'Learners', icon: Users, path: '/hr/learners' },
- { id: 'analytics', label: 'Analytics', icon: BarChart3, path: '/hr/analytics' },
- { id: 'testimonials', label: 'Testimonials', icon: MessageSquare, path: '/hr/testimonials' },
- { id: 'settings', label: 'Settings', icon: Settings, path: '/hr/settings' }
- ];
-
- return (
-
-
-
-
-
- {menuItems.map((item) => {
- const Icon = item.icon;
- const isActive = activeScreen === item.id;
-
- return (
-
- onNavigate(item.id)}
- className={`
- w-full flex items-center gap-3 px-3 py-2 rounded-md text-sm min-tap-44
- transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-sidebar-ring focus:ring-offset-2 focus:ring-offset-sidebar
- ${isActive
- ? 'bg-sidebar-primary text-sidebar-primary-foreground shadow-sm'
- : 'text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground'
- }
- ${prefersReducedMotion ? '' : 'animate-scale-hover'}
- `}
- aria-current={isActive ? 'page' : undefined}
- aria-label={`Navigate to ${item.label}`}
- >
-
- {item.label}
-
-
- );
- })}
-
-
-
- );
-};
-
-const TopNav: React.FC<{
- onMenuToggle?: () => void;
- showMenuButton?: boolean;
-}> = ({ onMenuToggle, showMenuButton = false }) => {
- return (
-
-
- {showMenuButton && (
-
-
-
- )}
-
-
-
-
-
-
-
- );
-};
-
-const BreadcrumbNav: React.FC<{ currentScreen: string }> = ({ currentScreen }) => {
- const getBreadcrumbText = (screen: string) => {
- switch (screen) {
- case 'home': return 'HR Home';
- case 'learners': return 'Learners';
- case 'analytics': return 'Analytics & Reports';
- case 'settings': return 'HR Settings';
- case 'testimonials': return 'Testimonials';
- default: return 'HR Portal';
- }
- };
-
- return (
-
-
-
- HR Portal
-
-
-
- {getBreadcrumbText(currentScreen)}
-
-
-
- );
-};
-
-const ChatBot: React.FC<{ currentScreen?: string }> = ({ currentScreen }) => {
- const [isOpen, setIsOpen] = useState(false);
-
- const getChipsForScreen = (screen?: string) => {
- if (screen === 'testimonials') {
- return [
- "How do I submit a testimonial?",
- "When will my testimonial be reviewed?",
- "Can I edit my testimonial?",
- "What makes a good testimonial?"
- ];
- }
- return [
- "How do I upload a roster?",
- "How to assign courses?",
- "View progress reports",
- "Export learner data"
- ];
- };
-
- const chips = getChipsForScreen(currentScreen);
-
- return (
-
- {isOpen && (
-
-
-
HR Assistant
- setIsOpen(false)}
- className="h-6 w-6"
- aria-label="Close chat"
- >
-
-
-
-
- {chips.map((chip, index) => (
-
- {chip}
-
- ))}
-
-
- )}
-
setIsOpen(!isOpen)}
- className="rounded-full h-12 w-12 shadow-lg min-tap-44"
- aria-label="Open HR chat assistant"
- aria-expanded={isOpen}
- >
-
-
-
- );
-};
-
-// Screen Components
-const HRHomeScreen: React.FC<{ onNavigate: (screen: string, filters?: any) => void }> = ({ onNavigate }) => {
- const [loading, setLoading] = useState(true);
- const [prefersReducedMotion] = useLocalStorage('prefersReducedMotion', false);
-
- useEffect(() => {
- const timer = setTimeout(() => setLoading(false), 1000);
- return () => clearTimeout(timer);
- }, []);
-
- const handleKPIClick = (kpiTitle: string) => {
- let filters = {};
- switch (kpiTitle) {
- case 'Total Learners':
- filters = { status: 'all' };
- break;
- case 'Active Courses':
- filters = { status: 'active' };
- break;
- case 'Completed Profilers':
- filters = { completed: true };
- break;
- default:
- filters = {};
- }
- onNavigate('learners', filters);
- };
-
- const cohortData = [
- { name: 'Leadership Development', notStarted: 15, inProgress: 28, completed: 42 },
- { name: 'Technical Skills', notStarted: 22, inProgress: 35, completed: 38 },
- { name: 'Communication', notStarted: 18, inProgress: 24, completed: 31 },
- { name: 'Project Management', notStarted: 12, inProgress: 19, completed: 28 }
- ];
-
- if (loading) {
- return (
-
-
- {[...Array(4)].map((_, i) => (
-
-
-
-
-
-
-
-
- ))}
-
-
-
-
-
-
-
-
-
-
- );
- }
-
- return (
-
- {/* Welcome Section */}
-
-
-
Hello HR Pooja 👋
-
See what's happening today at Acme Corp
-
-
-
- {/* KPI Cards */}
-
- {mockKPIData.map((kpi, index) => (
- handleKPIClick(kpi.title)}
- className={prefersReducedMotion ? '' : 'animate-fade-in'}
- style={{ animationDelay: prefersReducedMotion ? '0ms' : `${index * 100 + 200}ms` }}
- />
- ))}
-
-
- {/* Employee Assignment & Progress */}
-
-
-
-
- Employee Assignment & Progress
- Snapshot of current learning activities
-
-
-
-
-
-
-
- All Programmes
- Leadership Development
- Technical Skills
- Communication
-
-
- onNavigate('analytics')}
- className="min-tap-44"
- >
-
- View all in Analytics
-
-
-
- Download CSV
-
-
-
-
-
- onNavigate('learners', { editEmployee: employee.id })}
- maxHeight="360px"
- />
-
-
-
-
- {/* Cohort Progress Chart */}
-
-
-
-
- Cohort Progress
- Progress overview by programme
-
-
-
- Auto-refresh
-
-
-
-
-
-
- Stacked bar chart showing progress across different learning programmes.
- Each bar represents not started, in progress, and completed learners.
-
- {cohortData.map((cohort, index) => {
- const total = cohort.notStarted + cohort.inProgress + cohort.completed;
- const notStartedPercent = (cohort.notStarted / total) * 100;
- const inProgressPercent = (cohort.inProgress / total) * 100;
- const completedPercent = (cohort.completed / total) * 100;
-
- return (
-
-
- {cohort.name}
- {total} learners
-
-
-
- Not Started: {cohort.notStarted}
- In Progress: {cohort.inProgress}
- Completed: {cohort.completed}
-
-
- );
- })}
-
-
-
-
- {/* Upcoming Deadlines */}
-
-
- Upcoming Deadlines
- Next 7 days
-
-
-
- {mockDeadlines.map((deadline) => (
-
-
-
- {deadline.type === 'webinar' ?
- :
-
- }
-
-
-
{deadline.title}
-
{deadline.type}
-
-
-
-
- {deadline.dueDate}
-
-
{deadline.dueTime}
-
-
- ))}
-
-
-
-
-
- {/* Quick Links */}
-
-
- Quick Actions
- Common HR tasks
-
-
-
- {[
- { title: 'Add Learners', icon: Plus, action: () => onNavigate('learners', { action: 'add' }) },
- { title: 'Assign Courses', icon: BookOpen, action: () => onNavigate('learners', { action: 'assign' }) },
- { title: 'Download Reports', icon: Download, action: () => onNavigate('analytics') },
- { title: 'Testimonials Queue', icon: MessageSquare, action: () => onNavigate('testimonials') }
- ].map((link, index) => {
- const Icon = link.icon;
- return (
-
-
- {link.title}
-
- );
- })}
-
-
-
-
- {/* Announcements & Reminders */}
-
-
-
-
- Announcements & Reminders
- Recent updates and notifications
-
-
-
-
-
-
- All
- Announcements
- Reminders
-
-
-
-
-
-
- {mockAnnouncements.map((item) => (
-
-
-
-
-
{item.title}
- {item.pinned && (
- Pinned
- )}
-
-
{item.content}
-
-
- {item.type}
-
- {item.timestamp}
-
-
-
-
-
-
-
- ))}
-
-
-
-
- );
-};
-
-const LearnersScreen: React.FC<{ filters?: any }> = ({ filters }) => {
- const [employees, setEmployees] = useState(mockEmployees);
- const [searchTerm, setSearchTerm] = useState('');
- const [statusFilter, setStatusFilter] = useState('all');
- const [selectedEmployees, setSelectedEmployees] = useState([]);
- const [showAddDrawer, setShowAddDrawer] = useState(false);
- const [showImportModal, setShowImportModal] = useState(false);
- const [showAssignModal, setShowAssignModal] = useState(false);
- const [showEditDrawer, setShowEditDrawer] = useState(false);
- const [editingEmployee, setEditingEmployee] = useState(null);
- const [newEmployee, setNewEmployee] = useState({ name: '', email: '', phone: '' });
- const [bulkActionVisible, setBulkActionVisible] = useState(false);
-
- const debouncedSearch = useCallback(
- (term: string) => {
- // Simulating search with 300ms delay
- const timer = setTimeout(() => {
- setSearchTerm(term);
- }, 300);
- return () => clearTimeout(timer);
- },
- []
- );
-
- const filteredEmployees = employees.filter(emp => {
- const matchesSearch = emp.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
- emp.email.toLowerCase().includes(searchTerm.toLowerCase());
- const matchesStatus = statusFilter === 'all' || emp.status === statusFilter;
- return matchesSearch && matchesStatus;
- });
-
- const handleEmployeeSelect = (employeeId: string, selected: boolean) => {
- if (selected) {
- setSelectedEmployees(prev => [...prev, employeeId]);
- } else {
- setSelectedEmployees(prev => prev.filter(id => id !== employeeId));
- }
- };
-
- const handleBulkSelect = (selectAll: boolean) => {
- if (selectAll) {
- setSelectedEmployees(filteredEmployees.map(emp => emp.id));
- } else {
- setSelectedEmployees([]);
- }
- };
-
- useEffect(() => {
- setBulkActionVisible(selectedEmployees.length > 0);
- }, [selectedEmployees]);
-
- const handleAddEmployee = () => {
- if (newEmployee.name && newEmployee.email) {
- const newEmp: Employee = {
- id: Date.now().toString(),
- name: newEmployee.name,
- email: newEmployee.email,
- phone: newEmployee.phone,
- status: 'Pending'
- };
- setEmployees(prev => [...prev, newEmp]);
- setNewEmployee({ name: '', email: '', phone: '' });
- setShowAddDrawer(false);
- // Show success toast (simulated)
- console.log('Employee added successfully');
- }
- };
-
- const handleEditEmployee = (employee: Employee) => {
- setEditingEmployee(employee);
- setShowEditDrawer(true);
- };
-
- return (
-
- {/* Toolbar */}
-
-
-
-
-
-
- debouncedSearch(e.target.value)}
- aria-label="Search learners by name or email"
- />
-
-
-
-
-
-
- All Status
- Active
- Inactive
- Pending
-
-
-
-
-
setShowAddDrawer(true)}
- className="min-tap-44"
- aria-label="Add new learner"
- >
-
- Add Learner
-
-
setShowImportModal(true)}
- className="min-tap-44"
- aria-label="Import learners from CSV"
- >
-
- Import Learners
-
-
-
-
-
-
- {/* Bulk Action Bar */}
- {bulkActionVisible && (
-
-
-
-
- {selectedEmployees.length} learner{selectedEmployees.length !== 1 ? 's' : ''} selected
-
-
- setShowAssignModal(true)}
- className="min-tap-44"
- >
- Assign to Programme/Course
-
-
- Deactivate
-
-
- Reactivate
-
-
-
-
-
- )}
-
- {/* Learners Table */}
-
-
-
-
- Learners ({filteredEmployees.length})
- Manage learner accounts and assignments
-
-
- handleBulkSelect(selectedEmployees.length !== filteredEmployees.length)}
- className="min-tap-44"
- >
- {selectedEmployees.length === filteredEmployees.length ? 'Deselect All' : 'Select All'}
-
-
-
-
-
-
-
-
-
- {/* Add Learner Drawer */}
-
-
-
- Add New Learner
-
- Add a new learner to the system. Email cannot be changed after saving.
-
-
-
-
-
- Employee Name *
-
- setNewEmployee(prev => ({ ...prev, name: e.target.value }))}
- placeholder="Enter full name"
- required
- aria-required="true"
- />
-
-
-
- Email Address *
-
- setNewEmployee(prev => ({ ...prev, email: e.target.value }))}
- placeholder="email@company.com"
- required
- aria-required="true"
- />
-
-
-
- Phone Number
-
- setNewEmployee(prev => ({ ...prev, phone: e.target.value }))}
- placeholder="+61 4XX XXX XXX"
- />
-
-
-
- Save Learner
-
- setShowAddDrawer(false)} className="flex-1">
- Cancel
-
-
-
-
-
-
- {/* Import Modal */}
-
-
-
- Import Learners
-
- Upload a CSV file to import multiple learners. Maximum file size: 5MB.
-
-
-
-
-
Step 1: Download Template
-
- Download our CSV template with the required fields: Name, Email, Phone.
-
-
-
- Download CSV Template
-
-
-
-
Step 2: Upload File
-
-
-
- Drag and drop your CSV file here, or click to browse
-
-
- Choose File
-
-
-
-
- Import Learners
- setShowImportModal(false)} className="flex-1">
- Cancel
-
-
-
-
-
-
- {/* Assign Modal */}
-
-
-
- Assign to Programme/Course
-
- Assign {selectedEmployees.length} selected learner{selectedEmployees.length !== 1 ? 's' : ''} to a programme or course.
-
-
-
-
-
- Select Programme/Course
-
-
-
-
-
-
- Leadership Development
- Technical Skills
- Communication
- Project Management
-
-
-
-
-
- Start Date (Optional)
-
-
-
-
- Assign
- setShowAssignModal(false)} className="flex-1">
- Cancel
-
-
-
-
-
-
- {/* Edit/Assign Drawer */}
-
-
-
-
- {editingEmployee?.name}
-
-
- Edit learner details and manage course assignments.
-
-
- {editingEmployee && (
-
-
- Details
- Enrolments
-
-
-
-
- Name
-
-
-
-
-
- Phone
-
-
-
-
-
- Status
-
-
-
-
-
-
- Active
- Inactive
- Pending
-
-
-
-
-
-
-
-
Current Enrolments
-
-
- Assign Course
-
-
- {editingEmployee.programme && (
-
-
-
-
-
{editingEmployee.programme}
-
{editingEmployee.course}
- {editingEmployee.progress !== undefined && (
-
- )}
-
-
- Unassign
-
-
-
-
- )}
-
-
-
- Save Changes
- setShowEditDrawer(false)} className="flex-1">
- Cancel
-
-
-
- )}
-
-
-
- );
-};
-
-const AnalyticsScreen: React.FC<{ filters?: any }> = ({ filters }) => {
- const [dateRange, setDateRange] = useState('last-30-days');
- const [selectedProgrammes, setSelectedProgrammes] = useState(['all']);
- const [loading, setLoading] = useState(false);
- const [exporting, setExporting] = useState(false);
-
- const analyticsKPIData: KPIData[] = [
- { title: 'Total Learners', value: 1247, change: 8.2, trend: 'up' },
- { title: 'New Enrolments', value: 89, change: 15.3, trend: 'up' },
- { title: 'Course Completions', value: 342, change: -2.1, trend: 'down' },
- { title: 'Assessment Rates', value: 78, change: 5.7, trend: 'up' }
- ];
-
- const handleExport = async (format: 'csv' | 'pdf') => {
- setExporting(true);
- // Simulate export process
- await new Promise(resolve => setTimeout(resolve, 2000));
- setExporting(false);
- console.log(`Exported as ${format.toUpperCase()}`);
- };
-
- const handleRunReport = () => {
- setLoading(true);
- // Simulate report generation
- setTimeout(() => setLoading(false), 1500);
- };
-
- const chartData = [
- { month: 'Jan', enrolments: 45, completions: 38, assessments: 42 },
- { month: 'Feb', enrolments: 52, completions: 41, assessments: 38 },
- { month: 'Mar', enrolments: 48, completions: 44, assessments: 46 },
- { month: 'Apr', enrolments: 61, completions: 49, assessments: 52 },
- { month: 'May', enrolments: 55, completions: 52, assessments: 48 },
- { month: 'Jun', enrolments: 67, completions: 58, assessments: 61 }
- ];
-
- return (
-
- {/* Filter Bar */}
-
-
-
-
-
-
- Date Range
-
-
-
-
-
-
- Last 7 days
- Last 30 days
- Last 90 days
- Custom range
-
-
-
-
-
- Programmes
-
-
-
-
-
-
- All Programmes
- Leadership Development
- Technical Skills
- Communication
-
-
-
-
-
- {loading && }
- Run Report
-
-
-
-
-
- {/* KPI Cards */}
-
- {analyticsKPIData.map((kpi, index) => (
-
- ))}
-
-
- {/* Charts Panel */}
-
-
-
-
- Learning Analytics Overview
- Key metrics over time
-
-
- Last refreshed: 10 minutes ago
-
-
-
-
-
-
-
-
Interactive chart would be rendered here
-
- Line/Bar chart showing enrolments, completions, and assessments over time
-
-
-
-
- Line and bar chart showing learning analytics over the selected time period.
- Displays new enrolments, course completions, and assessment completion rates.
- Chart includes interactive legend for toggling data series visibility.
-
-
-
-
- {/* Detailed Table */}
-
-
-
-
- Assignments & Progress Detail
- Complete learner progress breakdown
-
-
- handleExport('csv')}
- disabled={exporting}
- className="min-tap-44"
- aria-live="polite"
- >
- {exporting ? (
-
- ) : (
-
- )}
- Export CSV
-
- handleExport('pdf')}
- disabled={exporting}
- className="min-tap-44"
- aria-live="polite"
- >
- {exporting ? (
-
- ) : (
-
- )}
- Export PDF
-
-
-
-
-
-
-
-
-
- {/* Data Freshness Note */}
-
-
- Last refreshed: {new Date().toLocaleTimeString()} •
- Next refresh in 4 minutes
-
-
-
- );
-};
-
-const TestimonialsScreen: React.FC = () => {
- const [loading, setLoading] = useState(true);
- const [formData, setFormData] = useState({
- name: 'Alex Sharma',
- email: 'alex.sharma@company.com',
- phone: '',
- organisation: 'Acme Corp',
- programme: '',
- testimonialText: '',
- consentToPublish: false
- });
- const [formErrors, setFormErrors] = useState>({});
- const [isSubmitting, setIsSubmitting] = useState(false);
- const [submitSuccess, setSubmitSuccess] = useState(false);
- const [submitError, setSubmitError] = useState('');
- const [charCount, setCharCount] = useState(0);
- const [prefersReducedMotion] = useLocalStorage('prefersReducedMotion', false);
-
- // Simulate profile pre-fill loading
- useEffect(() => {
- const timer = setTimeout(() => setLoading(false), 300);
- return () => clearTimeout(timer);
- }, []);
-
- const programmes = [
- 'Leadership Development',
- 'Technical Skills',
- 'Communication',
- 'Project Management',
- 'Sales Training'
- ];
-
- const validateForm = () => {
- const errors: Record = {};
-
- if (!formData.testimonialText.trim()) {
- errors.testimonialText = 'Testimonial text is required';
- } else if (formData.testimonialText.length < 1) {
- errors.testimonialText = 'Testimonial must be at least 1 character';
- } else if (formData.testimonialText.length > 2000) {
- errors.testimonialText = 'Testimonial must be 2000 characters or less';
- }
-
- if (!formData.consentToPublish) {
- errors.consentToPublish = 'You must consent to publish your testimonial';
- }
-
- return errors;
- };
-
- const isFormValid = () => {
- const errors = validateForm();
- return Object.keys(errors).length === 0 && formData.testimonialText.trim().length > 0;
- };
-
- const handleInputChange = (field: keyof TestimonialFormData, value: string | boolean) => {
- setFormData(prev => ({ ...prev, [field]: value }));
-
- if (field === 'testimonialText' && typeof value === 'string') {
- setCharCount(value.length);
- }
-
- // Clear field error when user starts typing
- if (formErrors[field]) {
- setFormErrors(prev => {
- const newErrors = { ...prev };
- delete newErrors[field];
- return newErrors;
- });
- }
- };
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
-
- const errors = validateForm();
- if (Object.keys(errors).length > 0) {
- setFormErrors(errors);
- return;
- }
-
- setIsSubmitting(true);
- setSubmitError('');
-
- try {
- // Simulate API call
- await new Promise(resolve => setTimeout(resolve, 1500));
-
- // Simulate occasional error for testing
- if (Math.random() > 0.9) {
- throw new Error('Submission failed. Please try again.');
- }
-
- setSubmitSuccess(true);
- setFormData(prev => ({ ...prev, testimonialText: '', programme: '', consentToPublish: false }));
- setCharCount(0);
- } catch (error) {
- setSubmitError(error instanceof Error ? error.message : 'An error occurred. Please try again.');
- } finally {
- setIsSubmitting(false);
- }
- };
-
- const resetForm = () => {
- setSubmitSuccess(false);
- setSubmitError('');
- setFormErrors({});
- };
-
- if (loading) {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- }
-
- return (
-
- {/* Success State */}
- {submitSuccess && (
-
-
-
- Thanks — your testimonial is pending review by KLC.
-
- Submit another testimonial
-
-
-
- )}
-
- {/* Error State */}
- {submitError && (
-
-
-
- {submitError}
-
-
- )}
-
- {/* Testimonial Form */}
-
-
- Submit Testimonial
-
- Share your experience with KLC programmes to help others discover the value of our learning solutions.
-
-
-
-
-
-
-
- {/* Status Notice */}
-
-
-
-
-
-
- Submissions are reviewed by KLC Super-Admins before appearing publicly.
-
- View policy
-
-
-
-
-
-
-
-
- );
-};
-
-const SettingsScreen: React.FC = () => {
- const [activeTab, setActiveTab] = useState('profile');
-
- return (
-
- {/* Settings Tabs */}
-
-
-
-
-
-
- Org Profile
- Profile
-
-
-
- Billing
-
-
-
- Roles
-
-
-
-
-
-
- Organisation Profile
- Manage your organisation's profile and settings
-
-
-
-
-
Organisation profile settings would be displayed here
-
-
-
-
-
-
-
-
- Billing & Subscriptions
- Manage your billing information and subscription plans
-
-
-
-
-
Billing and subscription management would be displayed here
-
-
-
-
-
-
-
-
- Roles & Permissions
- Manage user roles and access permissions
-
-
-
-
-
Roles and permissions management would be displayed here
-
-
-
-
-
-
-
-
- );
-};
-
-// Main App Component
+import React from "react";
+import { Routes, Route, Navigate, useNavigate } from "react-router-dom";
+import HRHomeScreen from "./pages/HRHomeScreen";
+import LearnersScreen from "./pages/LearnersScreen";
+import AnalyticsScreen from "./pages/AnalyticsScreen";
+import TestimonialsScreen from "./pages/TestimonialsScreen";
+import SettingsScreen from "./pages/SettingsScreen";
+// import Layout from "./pages/layout/layout";
+import Layout from "./components/layout/layout";
export default function App() {
- const [currentScreen, setCurrentScreen] = useState('home');
- const [isDark, setIsDark] = useLocalStorage('darkMode', false);
- const [prefersReducedMotion, setPrefersReducedMotion] = useLocalStorage('prefersReducedMotion', false);
- const [sidebarOpen, setSidebarOpen] = useState(false);
- const [screenFilters, setScreenFilters] = useState({});
-
- // Apply theme
- useEffect(() => {
- document.documentElement.classList.toggle('dark', isDark);
- }, [isDark]);
-
- // Handle navigation
- const handleNavigate = useCallback((screen: string, filters?: any) => {
- setCurrentScreen(screen);
-
- if (filters) {
- setScreenFilters(prev => ({ ...prev, [screen]: filters }));
- }
- setSidebarOpen(false); // Close sidebar on mobile after navigation
- }, []);
-
- const renderCurrentScreen = () => {
- const filters = screenFilters[currentScreen];
-
- switch (currentScreen) {
- case 'home':
- return ;
- case 'learners':
- return ;
- case 'analytics':
- return ;
- case 'testimonials':
- return ;
- case 'settings':
- return ;
- default:
- return ;
- }
- };
+ const navigate = useNavigate();
return (
-
- {/* Skip to main content link for accessibility */}
-
- Skip to main content
-
+
+ } />
- {/* Top Navigation */}
- setSidebarOpen(!sidebarOpen)}
- showMenuButton={true}
- />
-
-
- {/* Desktop Sidebar */}
-
}>
+
} />
+
navigate(`/hr/${screen}`)} />}
/>
+ } />
+ } />
+ } />
+ } />
+
- {/* Mobile Sidebar Overlay */}
- {sidebarOpen && (
-
-
setSidebarOpen(false)}
- />
-
-
- )}
-
- {/* Main Content */}
-
-
-
- {renderCurrentScreen()}
-
-
-
-
- {/* Chat Bot FAB */}
-
-
- {/* Footer */}
-
-
+ } />
+
);
-}
\ No newline at end of file
+}
diff --git a/src/components/BreadcrumbNav.tsx b/src/components/BreadcrumbNav.tsx
new file mode 100644
index 0000000..d93551f
--- /dev/null
+++ b/src/components/BreadcrumbNav.tsx
@@ -0,0 +1,37 @@
+import { Link } from "react-router-dom";
+import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "./ui/breadcrumb";
+
+export const BreadcrumbNav: React.FC<{ currentScreen: string }> = ({ currentScreen }) => {
+ const getBreadcrumbText = (screen: string) => {
+ switch (screen) {
+ case "home":
+ return "HR Home";
+ case "learners":
+ return "Learners";
+ case "analytics":
+ return "Analytics & Reports";
+ case "settings":
+ return "HR Settings";
+ case "testimonials":
+ return "Testimonials";
+ default:
+ return "HR Portal";
+ }
+ };
+
+ return (
+
+
+
+
+ HR Portal
+
+
+
+
+ {getBreadcrumbText(currentScreen)}
+
+
+
+ );
+};
diff --git a/src/components/ChatBot.tsx b/src/components/ChatBot.tsx
new file mode 100644
index 0000000..b1d02bf
--- /dev/null
+++ b/src/components/ChatBot.tsx
@@ -0,0 +1,65 @@
+import { useState } from "react";
+import { Button } from "./ui/button";
+import { MessageSquare, X } from "lucide-react";
+
+export const ChatBot: React.FC<{ currentScreen?: string }> = ({ currentScreen }) => {
+ const [isOpen, setIsOpen] = useState(false);
+
+ const getChipsForScreen = (screen?: string) => {
+ if (screen === 'testimonials') {
+ return [
+ "How do I submit a testimonial?",
+ "When will my testimonial be reviewed?",
+ "Can I edit my testimonial?",
+ "What makes a good testimonial?"
+ ];
+ }
+ return [
+ "How do I upload a roster?",
+ "How to assign courses?",
+ "View progress reports",
+ "Export learner data"
+ ];
+ };
+
+ const chips = getChipsForScreen(currentScreen);
+
+ return (
+
+ {isOpen && (
+
+
+
HR Assistant
+ setIsOpen(false)}
+ className="h-6 w-6"
+ aria-label="Close chat"
+ >
+
+
+
+
+ {chips.map((chip, index) => (
+
+ {chip}
+
+ ))}
+
+
+ )}
+
setIsOpen(!isOpen)}
+ className="rounded-full h-12 w-12 shadow-lg min-tap-44"
+ aria-label="Open HR chat assistant"
+ aria-expanded={isOpen}
+ >
+
+
+
+ );
+};
diff --git a/src/components/EmployeeTable.tsx b/src/components/EmployeeTable.tsx
new file mode 100644
index 0000000..bc203a1
--- /dev/null
+++ b/src/components/EmployeeTable.tsx
@@ -0,0 +1,265 @@
+import React from 'react';
+import { Button } from '../components/ui/button';
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../components/ui/table';
+import { Progress } from '../components/ui/progress';
+import { Badge } from '../components/ui/badge';
+import { Edit } from 'lucide-react';
+import type { Employee } from '../types';
+
+export const EmployeeTable: React.FC<{
+ employees: Employee[];
+ onEdit?: (employee: Employee) => void;
+ showProgress?: boolean;
+ maxHeight?: string;
+ compact?: any
+}> = ({ employees, onEdit, showProgress = true, maxHeight = '400px' }) => {
+ return (
+
+
+
+
+ Employee
+ Email
+ Phone
+ Status
+ {showProgress && (
+ <>
+ Programme/Course
+ Progress
+ Last Activity
+ >
+ )}
+ Actions
+
+
+
+ {employees.map((employee) => (
+ onEdit?.(employee)}
+ role="button"
+ tabIndex={0}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ onEdit?.(employee);
+ }
+ }}
+ >
+ {employee.name}
+ {employee.email}
+ {employee.phone}
+
+
+ {employee.status}
+
+
+ Employee status is {employee.status}
+
+
+ {showProgress && (
+ <>
+
+
+
{employee.programme}
+
{employee.course}
+
+
+
+ {employee.progress !== undefined && (
+
+
+
{employee.progress}%
+
+ Progress: {employee.progress} percent complete
+
+
+ )}
+
+ {employee.lastActivity}
+ >
+ )}
+
+ {
+ e.stopPropagation();
+ onEdit?.(employee);
+ }}
+ aria-label={`Edit ${employee.name}`}
+ >
+
+
+
+
+ ))}
+
+
+
+ );
+};
+
+
+// import React from "react";
+// import { Button } from "../components/ui/button";
+// import {
+// Table,
+// TableBody,
+// TableCell,
+// TableHead,
+// TableHeader,
+// TableRow,
+// } from "../components/ui/table";
+// import { Progress } from "../components/ui/progress";
+// import { Badge } from "../components/ui/badge";
+// import { Edit } from "lucide-react";
+// import type { Employee } from "../types";
+
+// export const EmployeeTable: React.FC<{
+// employees: Employee[];
+// onEdit?: (employee: Employee) => void;
+// showProgress?: boolean;
+// maxHeight?: string;
+// compact?: any
+// }> = ({ employees, onEdit, showProgress = true, maxHeight = "400px" }) => {
+// return (
+//
+//
+// {/* sticky header */}
+//
+//
+// Employee
+// Email
+// Phone
+// Status
+// {showProgress && (
+// <>
+// Programme/Course
+// Progress
+// Last Activity
+// >
+// )}
+// Actions
+//
+//
+
+// {/* table body */}
+//
+// {employees.map((employee) => (
+// onEdit?.(employee)}
+// role="button"
+// tabIndex={0}
+// onKeyDown={(e) => {
+// if (e.key === "Enter" || e.key === " ") {
+// e.preventDefault();
+// onEdit?.(employee);
+// }
+// }}
+// >
+//
+// {employee.name}
+//
+//
+// {employee.email}
+//
+//
+// {employee.phone}
+//
+//
+//
+// {employee.status}
+//
+//
+// Employee status is {employee.status}
+//
+//
+
+// {showProgress && (
+// <>
+//
+//
+//
+// {employee.programme}
+//
+//
+// {employee.course}
+//
+//
+//
+//
+// {employee.progress !== undefined && (
+//
+//
+//
+// {employee.progress}%
+//
+//
+// Progress: {employee.progress} percent complete
+//
+//
+// )}
+//
+//
+// {employee.lastActivity}
+//
+// >
+// )}
+
+//
+// {
+// e.stopPropagation();
+// onEdit?.(employee);
+// }}
+// aria-label={`Edit ${employee.name}`}
+// >
+//
+//
+//
+//
+// ))}
+//
+//
+//
+// );
+// };
+
+
diff --git a/src/components/HRSidebar.tsx b/src/components/HRSidebar.tsx
new file mode 100644
index 0000000..3ad7d26
--- /dev/null
+++ b/src/components/HRSidebar.tsx
@@ -0,0 +1,153 @@
+import { Home, Users, Settings, BarChart3, MessageSquare } from "lucide-react";
+import { useLocalStorage } from "../hooks/useLocalStorage";
+import { NavLink } from "react-router-dom";
+
+export const HRSidebar: React.FC<{
+ activePath: string;
+ onNavigatePath?: (path: string) => void;
+ className?: string;
+}> = ({ activePath, onNavigatePath, className = '' }) => {
+ const [prefersReducedMotion] = useLocalStorage('prefersReducedMotion', false);
+
+ const menuItems = [
+ { id: 'home', label: 'Dashboard', icon: Home, path: '/hr/home' },
+ { id: 'learners', label: 'Learners', icon: Users, path: '/hr/learners' },
+ { id: 'analytics', label: 'Analytics', icon: BarChart3, path: '/hr/analytics' },
+ { id: 'testimonials', label: 'Testimonials', icon: MessageSquare, path: '/hr/testimonials' },
+ { id: 'settings', label: 'Settings', icon: Settings, path: '/hr/settings' }
+ ];
+
+ return (
+
+
+
+
+
+
+ {menuItems.map((item) => {
+ const Icon = item.icon;
+ const isActive = activePath.startsWith(item.path);
+
+ return (
+
+
+
+ {item.label}
+
+
+ );
+ })}
+
+
+
+
+
+
+ );
+};
+
+
+
+
+
+// import React, { useState } from "react";
+// import { Home, Users, Settings, BarChart3, MessageSquare, Menu } from "lucide-react";
+// import { useLocalStorage } from "../hooks/useLocalStorage";
+// import { NavLink } from "react-router-dom";
+
+// export const HRSidebar: React.FC<{
+// activePath: string;
+// onNavigatePath?: (path: string) => void;
+// className?: string;
+// }> = ({ activePath, onNavigatePath, className = "" }) => {
+// const [prefersReducedMotion] = useLocalStorage("prefersReducedMotion", false);
+
+// // 🔹 Collapsed state
+// const [collapsed, setCollapsed] = useState(false);
+
+// const menuItems = [
+// { id: "home", label: "Dashboard", icon: Home, path: "/hr/home" },
+// { id: "learners", label: "Learners", icon: Users, path: "/hr/learners" },
+// { id: "analytics", label: "Analytics", icon: BarChart3, path: "/hr/analytics" },
+// { id: "testimonials", label: "Testimonials", icon: MessageSquare, path: "/hr/testimonials" },
+// { id: "settings", label: "Settings", icon: Settings, path: "/hr/settings" },
+// ];
+
+// return (
+//
+// {/* Header */}
+//
+//
+//
+// AC
+//
+// {!collapsed && (
+//
Acme Corp
+// )}
+//
+
+// {/* 🔹 Toggle button */}
+//
setCollapsed((prev) => !prev)}
+// className="text-sidebar-foreground hover:bg-sidebar-accent rounded-md p-1"
+// >
+//
+//
+//
+
+// {/* Navigation */}
+//
+//
+// {menuItems.map((item) => {
+// const Icon = item.icon;
+// const isActive = activePath.startsWith(item.path);
+
+// return (
+//
+//
+//
+// {!collapsed && {item.label} }
+//
+//
+// );
+// })}
+//
+//
+//
+// );
+// };
diff --git a/src/components/KPICard.css b/src/components/KPICard.css
new file mode 100644
index 0000000..30f694a
--- /dev/null
+++ b/src/components/KPICard.css
@@ -0,0 +1,85 @@
+/* KPICard.css */
+.kpi-card {
+ min-height: 96px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+}
+
+/* Responsive adjustments */
+@media (max-width: 640px) {
+ .kpi-card {
+ min-height: 88px;
+ }
+}
+
+/* Compact mode */
+.kpi-card.compact {
+ min-height: 80px;
+}
+
+/* Animation for counting up */
+@keyframes countUp {
+ from {
+ opacity: 0;
+ transform: translateY(5px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.animate-count-up {
+ animation: countUp 0.6s ease-out forwards;
+}
+
+/* High contrast mode support */
+@media (prefers-contrast: high) {
+ .kpi-card {
+ border: 1px solid;
+ }
+}
+
+/* Reduced motion support */
+@media (prefers-reduced-motion: reduce) {
+ .animate-count-up {
+ animation: none;
+ }
+
+ .kpi-card {
+ transition: none;
+ }
+}
+
+/* Dark mode support */
+@media (prefers-color-scheme: dark) {
+ .kpi-card {
+ background-color: rgba(255, 255, 255, 0.05);
+ }
+}
+
+/* Hover effects for non-touch devices */
+@media (hover: hover) and (pointer: fine) {
+ .kpi-card:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ }
+}
+
+/* Touch device adjustments */
+@media (pointer: coarse) {
+ .kpi-card {
+ min-height: 96px;
+ /* Larger tap target */
+ }
+}
+
+/* Very small screens (smartwatch size) */
+@media (max-width: 320px) {
+ .kpi-card {
+ min-height: 76px;
+ padding: 0.5rem;
+ }
+}
\ No newline at end of file
diff --git a/src/components/KPICard.tsx b/src/components/KPICard.tsx
new file mode 100644
index 0000000..d2e91a5
--- /dev/null
+++ b/src/components/KPICard.tsx
@@ -0,0 +1,132 @@
+import React from 'react';
+import { Card, CardContent, CardHeader, CardTitle } from './ui/Card';
+import { Badge } from '../components/ui/badge';
+import { useLocalStorage } from '../hooks/useLocalStorage';
+import { useCountUp } from '../hooks/useCountUp';
+import type { KPIData } from '../types';
+import './KPICard.css'; // We'll create this CSS file
+
+export const KPICard: React.FC<{
+ data: KPIData;
+ onClick?: () => void;
+ className?: string;
+ style?: any;
+ compact?: boolean;
+}> = ({ data, onClick, className = '', style = {}, compact = false }) => {
+ const countedValue = useCountUp(data.value);
+ const [prefersReducedMotion] = useLocalStorage('prefersReducedMotion', false);
+ const [screenSize, setScreenSize] = React.useState(getScreenSize());
+
+ // Get initial screen size
+ function getScreenSize() {
+ const width = window.innerWidth;
+ if (width < 640) return 'xs';
+ if (width < 768) return 'sm';
+ if (width < 1024) return 'md';
+ if (width < 1280) return 'lg';
+ return 'xl';
+ }
+
+ React.useEffect(() => {
+ const handleResize = () => {
+ setScreenSize(getScreenSize());
+ };
+
+ window.addEventListener('resize', handleResize);
+ return () => window.removeEventListener('resize', handleResize);
+ }, []);
+
+ // Responsive values based on screen size
+ const responsiveValues = {
+ fontSize: compact || screenSize === 'xs' ? 'text-xl' : 'text-2xl',
+ showFullTitle: !compact && screenSize !== 'xs',
+ badgeSize: compact || screenSize === 'xs' ? 'text-xs' : 'text-xs',
+ padding: compact || screenSize === 'xs' ? 'p-3' : 'p-4'
+ };
+
+ return (
+
{
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ onClick?.();
+ }
+ }}
+ style={style}
+ >
+
+
+ {responsiveValues.showFullTitle
+ ? data.title
+ : truncateTitle(data.title, screenSize, compact)
+ }
+
+
+
+
+
+ {prefersReducedMotion ? data.value : countedValue}
+ {data.title.includes('Progress') && '%'}
+
+ {data.change !== undefined && (
+
+ {data.trend === 'up' ? '+' : ''}{data.change}{data.title.includes('Progress') ? '%' : ''}
+
+ )}
+
+ {!responsiveValues.showFullTitle && (
+
+ {data.title}
+
+ )}
+
+
+
+ );
+};
+
+// Helper function to truncate titles for small screens
+function truncateTitle(title: string, screenSize: string, compact: boolean): string {
+ if (compact) {
+ // For compact mode, use abbreviations
+ const abbreviations: Record
= {
+ 'Total Learners': 'Learners',
+ 'Active Courses': 'Courses',
+ 'Completion Rate': 'Complete %',
+ 'Avg. Progress': 'Progress',
+ 'Completed Profilers': 'Profiles',
+ 'Satisfaction Score': 'Satisfaction'
+ };
+
+ return abbreviations[title] || title.substring(0, 12) + (title.length > 12 ? '...' : '');
+ }
+
+ // For different screen sizes
+ const maxLengths: Record = {
+ 'xs': 12,
+ 'sm': 16,
+ 'md': 20,
+ 'lg': 24,
+ 'xl': 30
+ };
+
+ const maxLength = maxLengths[screenSize] || 20;
+ if (title.length <= maxLength) return title;
+ return title
+ // return title.substring(0, maxLength - 3) + '...';
+}
\ No newline at end of file
diff --git a/src/components/TopNav.tsx b/src/components/TopNav.tsx
new file mode 100644
index 0000000..0b25530
--- /dev/null
+++ b/src/components/TopNav.tsx
@@ -0,0 +1,42 @@
+import { Bell, Menu } from "lucide-react";
+import { Button } from "./ui/button";
+// import logo from '../src/assets/klc-logo.png';
+import logo from '../assets/klc-logo.png'
+export const TopNav: React.FC<{
+ onMenuToggle?: () => void;
+ showMenuButton?: boolean;
+}> = ({ onMenuToggle, showMenuButton = false }) => {
+ return (
+
+
+ {showMenuButton && (
+
+
+
+ )}
+
+
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/components/layout/layout.tsx b/src/components/layout/layout.tsx
new file mode 100644
index 0000000..525597f
--- /dev/null
+++ b/src/components/layout/layout.tsx
@@ -0,0 +1,73 @@
+import React, { useState } from "react";
+import { Outlet, useLocation } from "react-router-dom";
+import { TopNav } from "../../components/TopNav";
+import { HRSidebar } from "../../components/HRSidebar";
+import { BreadcrumbNav } from "../../components/BreadcrumbNav";
+import { ChatBot } from "../../components/ChatBot";
+
+
+export default function Layout() {
+ const [sidebarOpen, setSidebarOpen] = useState(false);
+ const location = useLocation();
+
+ const pathToScreen = (pathname: string) => {
+ if (pathname.startsWith("/hr/learners")) return "learners";
+ if (pathname.startsWith("/hr/analytics")) return "analytics";
+ if (pathname.startsWith("/hr/testimonials")) return "testimonials";
+ if (pathname.startsWith("/hr/settings")) return "settings";
+ return "home";
+ };
+
+ const currentScreen = pathToScreen(location.pathname);
+
+ return (
+
+ {/* Header */}
+
setSidebarOpen(!sidebarOpen)} showMenuButton />
+
+
+ {/* Sidebar */}
+
+
+
+
+ {sidebarOpen && (
+
+
setSidebarOpen(false)}
+ />
+
+
+ )}
+
+ {/* Content */}
+
+
+
+
+ {/*
+
+
*/}
+
+
+
+
+ {/* Footer */}
+
+
+
+
+
+ {/* ChatBot */}
+
+
+
+ );
+}
diff --git a/src/components/ui/Card.css b/src/components/ui/Card.css
new file mode 100644
index 0000000..54a0911
--- /dev/null
+++ b/src/components/ui/Card.css
@@ -0,0 +1,103 @@
+/* Card.css */
+.responsive-card {
+ /* transition: all 0.2s ease; */
+}
+
+/* Mobile-first responsive adjustments */
+@media (max-width: 640px) {
+ .responsive-card {
+ border-radius: 0.75rem;
+ }
+}
+
+/* Tablet and larger screens */
+@media (min-width: 768px) {
+ .responsive-card {
+ border-radius: 0.875rem;
+ }
+}
+
+/* Desktop screens */
+@media (min-width: 1024px) {
+ .responsive-card {
+ border-radius: 1rem;
+ }
+}
+
+/* Card hover effects for non-touch devices */
+@media (hover: hover) and (pointer: fine) {
+ .responsive-card:hover {
+ /* box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), */
+ /* 0 8px 10px -6px rgba(0, 0, 0, 0.1); */
+ /* transform: translateY(-2px); */
+ }
+}
+
+/* High contrast mode support */
+@media (prefers-contrast: high) {
+ .responsive-card {
+ border-width: 2px;
+ }
+}
+
+/* Reduced motion support */
+@media (prefers-reduced-motion: reduce) {
+ .responsive-card {
+ transition: none;
+ }
+
+ .responsive-card:hover {
+ transform: none;
+ }
+}
+
+/* Dark mode adjustments */
+@media (prefers-color-scheme: dark) {
+ .responsive-card {
+ border-color: rgba(112, 112, 112, 0.1);
+ }
+}
+
+/* Very small screens (smartwatch size) */
+@media (max-width: 320px) {
+ .responsive-card {
+ border-radius: 0.5rem;
+ }
+}
+
+/* Container query support for card header */
+@container card-header (max-width: 300px) {
+ [data-slot="card-header"] {
+ grid-template-columns: 1fr !important;
+ }
+
+ [data-slot="card-action"] {
+ grid-column: 1;
+ grid-row: 3;
+ justify-self: start;
+ margin-top: 0.5rem;
+ }
+}
+
+/* Touch device optimizations */
+@media (pointer: coarse) {
+ .responsive-card {
+ min-height: 44px;
+ /* Minimum tap target size */
+ }
+
+ [data-slot="card-action"] button,
+ [data-slot="card-action"] a {
+ min-height: 44px;
+ min-width: 44px;
+ }
+}
+
+/* Print styles */
+@media print {
+ .responsive-card {
+ border: 1px solid #000;
+ box-shadow: none;
+ break-inside: avoid;
+ }
+}
\ No newline at end of file
diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx
index 5f9d58a..82e359e 100644
--- a/src/components/ui/card.tsx
+++ b/src/components/ui/card.tsx
@@ -1,13 +1,46 @@
import * as React from "react";
-
import { cn } from "./utils";
+import "./Card.css"; // We'll create this CSS file
function Card({ className, ...props }: React.ComponentProps<"div">) {
+ const [screenSize, setScreenSize] = React.useState(getScreenSize());
+
+ // Get initial screen size
+ function getScreenSize() {
+ const width = window.innerWidth;
+ if (width < 640) return 'xs';
+ if (width < 768) return 'sm';
+ if (width < 1024) return 'md';
+ if (width < 1280) return 'lg';
+ return 'xl';
+ }
+
+ React.useEffect(() => {
+ const handleResize = () => {
+ setScreenSize(getScreenSize());
+ };
+
+ window.addEventListener('resize', handleResize);
+ return () => window.removeEventListener('resize', handleResize);
+ }, []);
+
+ // Responsive values based on screen size
+ const responsivePadding = {
+ 'xs': 'p-4',
+ 'sm': 'p-5',
+ 'md': 'p-5',
+ 'lg': 'p-6',
+ 'xl': 'p-6'
+ };
+
return (
) {
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
+ const [screenSize] = React.useState(getScreenSize());
+
+ function getScreenSize() {
+ const width = window.innerWidth;
+ if (width < 640) return 'xs';
+ if (width < 768) return 'sm';
+ if (width < 1024) return 'md';
+ if (width < 1280) return 'lg';
+ return 'xl';
+ }
+
+ // Responsive values based on screen size
+ const responsiveGap = {
+ 'xs': 'gap-1',
+ 'sm': 'gap-1.5',
+ 'md': 'gap-1.5',
+ 'lg': 'gap-1.5',
+ 'xl': 'gap-1.5'
+ };
+
+ const responsivePadding = {
+ 'xs': 'px-4 pt-4',
+ 'sm': 'px-5 pt-5',
+ 'md': 'px-5 pt-5',
+ 'lg': 'px-6 pt-6',
+ 'xl': 'px-6 pt-6'
+ };
+
return (
) {
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+ const [screenSize] = React.useState(getScreenSize());
+
+ function getScreenSize() {
+ const width = window.innerWidth;
+ if (width < 640) return 'xs';
+ if (width < 768) return 'sm';
+ if (width < 1024) return 'md';
+ if (width < 1280) return 'lg';
+ return 'xl';
+ }
+
+ // Responsive font sizes
+ const responsiveFont = {
+ 'xs': 'text-lg',
+ 'sm': 'text-xl',
+ 'md': 'text-xl',
+ 'lg': 'text-2xl',
+ 'xl': 'text-2xl'
+ };
+
return (
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
+ const [screenSize] = React.useState(getScreenSize());
+
+ function getScreenSize() {
+ const width = window.innerWidth;
+ if (width < 640) return 'xs';
+ if (width < 768) return 'sm';
+ if (width < 1024) return 'md';
+ if (width < 1280) return 'lg';
+ return 'xl';
+ }
+
+ // Responsive font sizes
+ const responsiveFont = {
+ 'xs': 'text-sm',
+ 'sm': 'text-base',
+ 'md': 'text-base',
+ 'lg': 'text-base',
+ 'xl': 'text-base'
+ };
+
return (
);
@@ -62,20 +166,65 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
+ const [screenSize] = React.useState(getScreenSize());
+
+ function getScreenSize() {
+ const width = window.innerWidth;
+ if (width < 640) return 'xs';
+ if (width < 768) return 'sm';
+ if (width < 1024) return 'md';
+ if (width < 1280) return 'lg';
+ return 'xl';
+ }
+
+ // Responsive padding
+ const responsivePadding = {
+ 'xs': 'px-4',
+ 'sm': 'px-5',
+ 'md': 'px-5',
+ 'lg': 'px-6',
+ 'xl': 'px-6'
+ };
+
return (
);
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
+ const [screenSize] = React.useState(getScreenSize());
+
+ function getScreenSize() {
+ const width = window.innerWidth;
+ if (width < 640) return 'xs';
+ if (width < 768) return 'sm';
+ if (width < 1024) return 'md';
+ if (width < 1280) return 'lg';
+ return 'xl';
+ }
+
+ // Responsive padding
+ const responsivePadding = {
+ 'xs': 'px-4 pb-4',
+ 'sm': 'px-5 pb-5',
+ 'md': 'px-5 pb-5',
+ 'lg': 'px-6 pb-6',
+ 'xl': 'px-6 pb-6'
+ };
+
return (
);
@@ -89,4 +238,4 @@ export {
CardAction,
CardDescription,
CardContent,
-};
+};
\ No newline at end of file
diff --git a/src/hooks/useCountUp.ts b/src/hooks/useCountUp.ts
new file mode 100644
index 0000000..c32a7c2
--- /dev/null
+++ b/src/hooks/useCountUp.ts
@@ -0,0 +1,25 @@
+import { useEffect, useState } from 'react';
+
+export const useCountUp = (end: number, duration: number = 1200) => {
+ const [count, setCount] = useState(0);
+
+ useEffect(() => {
+ let start = 0;
+ const increment = end / (duration / 16);
+ const timer = setInterval(() => {
+ start += increment;
+ if (start >= end) {
+ setCount(end);
+ clearInterval(timer);
+ } else {
+ setCount(Math.floor(start));
+ }
+ }, 16);
+
+ return () => clearInterval(timer);
+ }, [end, duration]);
+
+ return count;
+};
+
+
diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts
new file mode 100644
index 0000000..1705c3a
--- /dev/null
+++ b/src/hooks/useLocalStorage.ts
@@ -0,0 +1,25 @@
+import { useState } from 'react';
+
+export const useLocalStorage =
(key: string, initialValue: T) => {
+ const [storedValue, setStoredValue] = useState(() => {
+ try {
+ const item = window.localStorage.getItem(key);
+ return item ? (JSON.parse(item) as T) : initialValue;
+ } catch (error) {
+ return initialValue;
+ }
+ });
+
+ const setValue = (value: T) => {
+ try {
+ setStoredValue(value);
+ window.localStorage.setItem(key, JSON.stringify(value));
+ } catch (error) {
+ console.error('Error saving to localStorage:', error);
+ }
+ };
+
+ return [storedValue, setValue] as const;
+};
+
+
diff --git a/src/main.tsx b/src/main.tsx
index 132b145..8652b8b 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,7 +1,15 @@
- import { createRoot } from "react-dom/client";
- import App from "./App.tsx";
- import "./styles/globals.css";
+import { createRoot } from "react-dom/client";
+import { BrowserRouter } from "react-router-dom";
+import App from "./App";
+import "./styles/globals.css";
+import { Provider } from "react-redux";
+import { store } from "./redux/store";
- createRoot(document.getElementById("root")!).render( );
-
\ No newline at end of file
+createRoot(document.getElementById("root")!).render(
+
+
+
+
+
+);
diff --git a/src/mock.ts b/src/mock.ts
new file mode 100644
index 0000000..877d970
--- /dev/null
+++ b/src/mock.ts
@@ -0,0 +1,37 @@
+import type { KPIData, Employee, Announcement, Deadline } from './types';
+
+export const mockKPIData: KPIData[] = [
+ { title: 'Total Learners', value: 1247, change: 12, trend: 'up' },
+ { title: 'Active Courses', value: 89, change: 5, trend: 'up' },
+ { title: 'Completed Profilers', value: 342, change: -8, trend: 'down' },
+ { title: 'Average Progress', value: 73, change: 7, trend: 'up' }
+];
+
+export const mockEmployees: Employee[] = [
+ { id: '1', name: 'Sarah Chen', email: 'sarah.chen@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Leadership Development', course: 'Strategic Thinking', progress: 85, lastActivity: '2 hours ago' },
+ { id: '2', name: 'Michael Rodriguez', email: 'michael.r@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Technical Skills', course: 'Data Analysis', progress: 62, lastActivity: '1 day ago' },
+ { id: '3', name: 'Emma Thompson', email: 'emma.thompson@company.com', phone: '+61 4XX XXX XXX', status: 'Pending', programme: 'Communication', course: 'Public Speaking', progress: 0, lastActivity: 'Never' },
+ { id: '4', name: 'David Kim', email: 'david.kim@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Project Management', course: 'Agile Methodology', progress: 94, lastActivity: '3 hours ago' },
+ { id: '5', name: 'Lisa Wang', email: 'lisa.wang@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Leadership Development', course: 'Team Management', progress: 78, lastActivity: '5 hours ago' },
+ { id: '6', name: 'James Wilson', email: 'james.wilson@company.com', phone: '+61 4XX XXX XXX', status: 'Inactive', programme: 'Technical Skills', course: 'Programming Basics', progress: 34, lastActivity: '2 weeks ago' },
+ { id: '7', name: 'Maria Garcia', email: 'maria.garcia@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Sales Training', course: 'Customer Relations', progress: 56, lastActivity: '1 day ago' },
+ { id: '8', name: 'Robert Lee', email: 'robert.lee@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Leadership Development', course: 'Decision Making', progress: 89, lastActivity: '4 hours ago' },
+ { id: '9', name: 'Jennifer Davis', email: 'jennifer.davis@company.com', phone: '+61 4XX XXX XXX', status: 'Pending', programme: 'Communication', course: 'Written Communication', progress: 0, lastActivity: 'Never' },
+ { id: '10', name: 'Thomas Brown', email: 'thomas.brown@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Project Management', course: 'Risk Management', progress: 71, lastActivity: '6 hours ago' }
+];
+
+export const mockAnnouncements: Announcement[] = [
+ { id: '1', title: 'New Learning Module Available', content: 'Advanced Analytics course is now live in the system.', type: 'announcement', timestamp: '2 hours ago', pinned: true },
+ { id: '2', title: 'Reminder: Quarterly Reviews Due', content: 'Please complete all quarterly progress reviews by Friday.', type: 'reminder', timestamp: '5 hours ago' },
+ { id: '3', title: 'System Maintenance Scheduled', content: 'Learning platform will be offline Saturday 2-4 AM for updates.', type: 'announcement', timestamp: '1 day ago' }
+];
+
+export const mockDeadlines: Deadline[] = [
+ { id: '1', title: 'Leadership Webinar Series', type: 'webinar', dueDate: 'Today', dueTime: '2:00 PM' },
+ { id: '2', title: 'Communication Skills Assessment', type: 'profiler', dueDate: 'Tomorrow', dueTime: '11:59 PM' },
+ { id: '3', title: 'Project Management Workshop', type: 'webinar', dueDate: 'Dec 30', dueTime: '10:00 AM' },
+ { id: '4', title: 'Technical Skills Profiler', type: 'profiler', dueDate: 'Jan 2', dueTime: '5:00 PM' },
+ { id: '5', title: 'Team Building Session', type: 'webinar', dueDate: 'Jan 5', dueTime: '3:30 PM' }
+];
+
+
diff --git a/src/pages/AnalyticsScreen.tsx b/src/pages/AnalyticsScreen.tsx
new file mode 100644
index 0000000..c49e0c1
--- /dev/null
+++ b/src/pages/AnalyticsScreen.tsx
@@ -0,0 +1,218 @@
+import { useState } from "react";
+import { Employee, KPIData } from "../types";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../components/ui/Card";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../components/ui/select";
+import { Button } from "../components/ui/button";
+import { RefreshCw } from "lucide-react";
+import { Download } from "lucide-react";
+import { FileText } from "lucide-react";
+import { EmployeeTable } from "../components/EmployeeTable";
+import { BarChart3 } from "lucide-react";
+import { KPICard } from "../components/KPICard";
+// import { mockEmployees } from "../data/mockEmployees";
+
+export default function AnalyticsScreen({ filters }: { filters?: any }) {
+ const [dateRange, setDateRange] = useState('last-30-days');
+ const [selectedProgrammes, setSelectedProgrammes] = useState(['all']);
+ const [loading, setLoading] = useState(false);
+ const [exporting, setExporting] = useState(false);
+
+ const analyticsKPIData: KPIData[] = [
+ { title: 'Total Learners', value: 1247, change: 8.2, trend: 'up' },
+ { title: 'New Enrolments', value: 89, change: 15.3, trend: 'up' },
+ { title: 'Course Completions', value: 342, change: -2.1, trend: 'down' },
+ { title: 'Assessment Rates', value: 78, change: 5.7, trend: 'up' }
+ ];
+
+ const handleExport = async (format: 'csv' | 'pdf') => {
+ setExporting(true);
+ // Simulate export process
+ await new Promise(resolve => setTimeout(resolve, 2000));
+ setExporting(false);
+ console.log(`Exported as ${format.toUpperCase()}`);
+ };
+
+ const handleRunReport = () => {
+ setLoading(true);
+ // Simulate report generation
+ setTimeout(() => setLoading(false), 1500);
+ };
+ const mockEmployees: Employee[] = [
+ { id: '1', name: 'Sarah Chen', email: 'sarah.chen@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Leadership Development', course: 'Strategic Thinking', progress: 85, lastActivity: '2 hours ago' },
+ { id: '2', name: 'Michael Rodriguez', email: 'michael.r@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Technical Skills', course: 'Data Analysis', progress: 62, lastActivity: '1 day ago' },
+ { id: '3', name: 'Emma Thompson', email: 'emma.thompson@company.com', phone: '+61 4XX XXX XXX', status: 'Pending', programme: 'Communication', course: 'Public Speaking', progress: 0, lastActivity: 'Never' },
+ { id: '4', name: 'David Kim', email: 'david.kim@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Project Management', course: 'Agile Methodology', progress: 94, lastActivity: '3 hours ago' },
+ { id: '5', name: 'Lisa Wang', email: 'lisa.wang@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Leadership Development', course: 'Team Management', progress: 78, lastActivity: '5 hours ago' },
+ { id: '6', name: 'James Wilson', email: 'james.wilson@company.com', phone: '+61 4XX XXX XXX', status: 'Inactive', programme: 'Technical Skills', course: 'Programming Basics', progress: 34, lastActivity: '2 weeks ago' },
+ { id: '7', name: 'Maria Garcia', email: 'maria.garcia@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Sales Training', course: 'Customer Relations', progress: 56, lastActivity: '1 day ago' },
+ { id: '8', name: 'Robert Lee', email: 'robert.lee@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Leadership Development', course: 'Decision Making', progress: 89, lastActivity: '4 hours ago' },
+ { id: '9', name: 'Jennifer Davis', email: 'jennifer.davis@company.com', phone: '+61 4XX XXX XXX', status: 'Pending', programme: 'Communication', course: 'Written Communication', progress: 0, lastActivity: 'Never' },
+ { id: '10', name: 'Thomas Brown', email: 'thomas.brown@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Project Management', course: 'Risk Management', progress: 71, lastActivity: '6 hours ago' }
+ ];
+
+ const chartData = [
+ { month: 'Jan', enrolments: 45, completions: 38, assessments: 42 },
+ { month: 'Feb', enrolments: 52, completions: 41, assessments: 38 },
+ { month: 'Mar', enrolments: 48, completions: 44, assessments: 46 },
+ { month: 'Apr', enrolments: 61, completions: 49, assessments: 52 },
+ { month: 'May', enrolments: 55, completions: 52, assessments: 48 },
+ { month: 'Jun', enrolments: 67, completions: 58, assessments: 61 }
+ ];
+
+ return (
+
+ {/* Filter Bar */}
+
+
+
+
+
+
+ Date Range
+
+
+
+
+
+
+ Last 7 days
+ Last 30 days
+ Last 90 days
+ Custom range
+
+
+
+
+
+ Programmes
+
+
+
+
+
+
+ All Programmes
+ Leadership Development
+ Technical Skills
+ Communication
+
+
+
+
+
+ {loading && }
+ Run Report
+
+
+
+
+
+ {/* KPI Cards */}
+
+ {analyticsKPIData.map((kpi, index) => (
+
+ ))}
+
+
+ {/* Charts Panel */}
+
+
+
+
+ Learning Analytics Overview
+ Key metrics over time
+
+
+ Last refreshed: 10 minutes ago
+
+
+
+
+
+
+
+
Interactive chart would be rendered here
+
+ Line/Bar chart showing enrolments, completions, and assessments over time
+
+
+
+
+ Line and bar chart showing learning analytics over the selected time period.
+ Displays new enrolments, course completions, and assessment completion rates.
+ Chart includes interactive legend for toggling data series visibility.
+
+
+
+
+ {/* Detailed Table */}
+
+
+
+
+ Assignments & Progress Detail
+ Complete learner progress breakdown
+
+
+ handleExport('csv')}
+ disabled={exporting}
+ className="min-tap-44"
+ aria-live="polite"
+ >
+ {exporting ? (
+
+ ) : (
+
+ )}
+ Export CSV
+
+ handleExport('pdf')}
+ disabled={exporting}
+ className="min-tap-44"
+ aria-live="polite"
+ >
+ {exporting ? (
+
+ ) : (
+
+ )}
+ Export PDF
+
+
+
+
+
+
+
+
+
+ {/* Data Freshness Note */}
+
+
+ Last refreshed: {new Date().toLocaleTimeString()} •
+ Next refresh in 4 minutes
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/pages/HRHomeScreen.tsx b/src/pages/HRHomeScreen.tsx
new file mode 100644
index 0000000..d313212
--- /dev/null
+++ b/src/pages/HRHomeScreen.tsx
@@ -0,0 +1,581 @@
+import { useEffect, useState } from "react";
+import { useLocalStorage } from "../hooks/useLocalStorage";
+import { Card } from "../components/ui/Card";
+import { CardHeader } from "../components/ui/Card";
+import { CardTitle } from "../components/ui/Card";
+import { CardDescription } from "../components/ui/Card";
+import { Skeleton } from "../components/ui/skeleton";
+import { CardContent } from "../components/ui/Card";
+import { KPICard } from "../components/KPICard";
+import { BarChart3, ChevronRight } from "lucide-react";
+import { Download } from "lucide-react";
+import { RefreshCw } from "lucide-react";
+import { Badge } from "../components/ui/badge";
+import { Select } from "../components/ui/select";
+import { SelectItem } from "../components/ui/select";
+import { SelectTrigger } from "../components/ui/select";
+import { Button } from "../components/ui/button";
+import { SelectValue } from "../components/ui/select";
+import { SelectContent } from "../components/ui/select";
+import { mockKPIData } from "../mock";
+import { mockDeadlines } from "../mock";
+import { Plus } from "lucide-react";
+import { BookOpen } from "lucide-react";
+import { MessageSquare } from "lucide-react";
+import { Calendar } from "lucide-react";
+import { FileText } from "lucide-react";
+import { EmployeeTable } from "../components/EmployeeTable";
+import { Announcement, Employee } from "../types";
+import { useGetPostsQuery } from "../redux/services/demo.services";
+
+const mockEmployees: Employee[] = [
+ { id: '1', name: 'Sarah Chen', email: 'sarah.chen@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Leadership Development', course: 'Strategic Thinking', progress: 85, lastActivity: '2 hours ago' },
+ { id: '2', name: 'Michael Rodriguez', email: 'michael.r@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Technical Skills', course: 'Data Analysis', progress: 62, lastActivity: '1 day ago' },
+ { id: '3', name: 'Emma Thompson', email: 'emma.thompson@company.com', phone: '+61 4XX XXX XXX', status: 'Pending', programme: 'Communication', course: 'Public Speaking', progress: 0, lastActivity: 'Never' },
+ { id: '4', name: 'David Kim', email: 'david.kim@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Project Management', course: 'Agile Methodology', progress: 94, lastActivity: '3 hours ago' },
+ { id: '5', name: 'Lisa Wang', email: 'lisa.wang@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Leadership Development', course: 'Team Management', progress: 78, lastActivity: '5 hours ago' },
+ { id: '6', name: 'James Wilson', email: 'james.wilson@company.com', phone: '+61 4XX XXX XXX', status: 'Inactive', programme: 'Technical Skills', course: 'Programming Basics', progress: 34, lastActivity: '2 weeks ago' },
+ { id: '7', name: 'Maria Garcia', email: 'maria.garcia@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Sales Training', course: 'Customer Relations', progress: 56, lastActivity: '1 day ago' },
+ { id: '8', name: 'Robert Lee', email: 'robert.lee@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Leadership Development', course: 'Decision Making', progress: 89, lastActivity: '4 hours ago' },
+ { id: '9', name: 'Jennifer Davis', email: 'jennifer.davis@company.com', phone: '+61 4XX XXX XXX', status: 'Pending', programme: 'Communication', course: 'Written Communication', progress: 0, lastActivity: 'Never' },
+ { id: '10', name: 'Thomas Brown', email: 'thomas.brown@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Project Management', course: 'Risk Management', progress: 71, lastActivity: '6 hours ago' }
+];
+// Screen Components
+export default function HRHomeScreen({ onNavigate }: { onNavigate: (screen: string, filters?: any) => void }) {
+
+ const [loading, setLoading] = useState(true);
+ const [prefersReducedMotion] = useLocalStorage('prefersReducedMotion', false);
+ const { data: posts, isLoading, error } = useGetPostsQuery();
+ console.log(posts, 'posts');
+ useEffect(() => {
+ const timer = setTimeout(() => setLoading(false), 1000);
+ return () => clearTimeout(timer);
+ }, []);
+
+ const handleKPIClick = (kpiTitle: string) => {
+ let filters = {};
+ switch (kpiTitle) {
+ case 'Total Learners':
+ filters = { status: 'all' };
+ break;
+ case 'Active Courses':
+ filters = { status: 'active' };
+ break;
+ case 'Completed Profilers':
+ filters = { completed: true };
+ break;
+ default:
+ filters = {};
+ }
+ onNavigate('learners', filters);
+ };
+
+ const cohortData = [
+ { name: 'Leadership Development', notStarted: 15, inProgress: 28, completed: 42 },
+ { name: 'Technical Skills', notStarted: 22, inProgress: 35, completed: 38 },
+ { name: 'Communication', notStarted: 18, inProgress: 24, completed: 31 },
+ { name: 'Project Management', notStarted: 12, inProgress: 19, completed: 28 }
+ ];
+
+ if (loading) {
+ return (
+
+
+ {[...Array(4)].map((_, i) => (
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+ const mockAnnouncements: Announcement[] = [
+ { id: '1', title: 'New Learning Module Available', content: 'Advanced Analytics course is now live in the system.', type: 'announcement', timestamp: '2 hours ago', pinned: true },
+ { id: '2', title: 'Reminder: Quarterly Reviews Due', content: 'Please complete all quarterly progress reviews by Friday.', type: 'reminder', timestamp: '5 hours ago' },
+ { id: '3', title: 'System Maintenance Scheduled', content: 'Learning platform will be offline Saturday 2-4 AM for updates.', type: 'announcement', timestamp: '1 day ago' }
+ ];
+ return (
+
+ {/* Welcome Section */}
+
+
+
Hello HR Pooja 👋
+
See what's happening today at Acme Corp
+
+
+
+ {/* KPI Cards */}
+
+ {/*
*/}
+
+ {mockKPIData.map((kpi, index) => (
+ handleKPIClick(kpi.title)}
+ className={prefersReducedMotion ? '' : 'animate-fade-in'}
+ style={{ animationDelay: prefersReducedMotion ? '0ms' : `${index * 100 + 200}ms`, }}
+ />
+ ))}
+
+
+
+ {/* Employee Assignment & Progress */}
+
+
+
+
+
+ Employee Assignment & Progress
+ Snapshot of current learning activities
+
+
+
+
+
+
+
+ All Programmes
+ Leadership Development
+ Technical Skills
+ Communication
+
+
+
+
+ onNavigate('analytics')}
+ className="min-tap-44 flex-1 sm:flex-initial"
+ size="sm"
+ >
+
+ View all in Analytics
+ Analytics
+
+
+
+ Download CSV
+ CSV
+
+
+
+
+
+
+
+ onNavigate('learners', { editEmployee: employee.id })}
+ maxHeight="360px"
+ compact={true}
+ />
+
+
+ {/* Mobile view alternative */}
+
+
+ onNavigate('analytics')}
+ className="text-sm"
+ >
+ View full employee table in Analytics
+
+
+
+
+ {/* {mockEmployees.slice(0, 3).map((employee) => (
+
+
+
+
{employee.name}
+
{employee.department}
+
+
+ {employee.status}
+
+
+
+
+ Progress: {employee.completion}%
+
+ onNavigate('learners', { editEmployee: employee.id })}
+ className="h-8 px-2"
+ >
+ Edit
+
+
+
+ ))} */}
+
+
+
+
+
+ {/*
+ {/* Cohort Progress Chart */}
+ {/*
+
+
+
+ Cohort Progress
+ Progress overview by programme
+
+
+
+ Auto-refresh
+
+
+
+
+
+
+ Stacked bar chart showing progress across different learning programmes.
+ Each bar represents not started, in progress, and completed learners.
+
+ {cohortData.map((cohort, index) => {
+ const total = cohort.notStarted + cohort.inProgress + cohort.completed;
+ const notStartedPercent = (cohort.notStarted / total) * 100;
+ const inProgressPercent = (cohort.inProgress / total) * 100;
+ const completedPercent = (cohort.completed / total) * 100;
+
+ return (
+
+
+ {cohort.name}
+ {total} learners
+
+
+
+ Not Started: {cohort.notStarted}
+ In Progress: {cohort.inProgress}
+ Completed: {cohort.completed}
+
+
+ );
+ })}
+
+
+ */}
+
+ {/* Upcoming Deadlines */}
+ {/*
+
+ Upcoming Deadlines
+ Next 7 days
+
+
+
+ {mockDeadlines.map((deadline) => (
+
+
+
+ {deadline.type === 'webinar' ?
+ :
+
+ }
+
+
+
{deadline.title}
+
{deadline.type}
+
+
+
+
+ {deadline.dueDate}
+
+
{deadline.dueTime}
+
+
+ ))}
+
+
+ */}
+ {/*
*/}
+
+
+ {/* Cohort Progress Chart */}
+
+
+
+
+ Cohort Progress
+
+ Progress overview by programme
+
+
+
+
+ Auto-refresh
+
+
+
+
+
+
+
+ Stacked bar chart showing progress across different learning programmes.
+ Each bar represents not started, in progress, and completed learners.
+
+
+ {cohortData.map((cohort, index) => {
+ const total = cohort.notStarted + cohort.inProgress + cohort.completed;
+ const notStartedPercent = (cohort.notStarted / total) * 100;
+ const inProgressPercent = (cohort.inProgress / total) * 100;
+ const completedPercent = (cohort.completed / total) * 100;
+
+ return (
+
+
+ {cohort.name}
+ {total} learners
+
+
+
+ Not Started: {cohort.notStarted}
+ In Progress: {cohort.inProgress}
+ Completed: {cohort.completed}
+
+
+ );
+ })}
+
+
+
+
+ {/* Upcoming Deadlines */}
+
+
+ Upcoming Deadlines
+ Next 7 days
+
+
+
+
+ {mockDeadlines.map((deadline) => (
+
+
+
+ {deadline.type === 'webinar' ? (
+
+ ) : (
+
+ )}
+
+
+
{deadline.title}
+
+ {deadline.type}
+
+
+
+
+
+ {deadline.dueDate}
+
+
+ {deadline.dueTime}
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+ {/* Quick Links */}
+
+
+ Quick Actions
+ Common HR tasks
+
+
+
+ {[
+ { title: 'Add Learners', icon: Plus, action: () => onNavigate('learners', { action: 'add' }) },
+ { title: 'Assign Courses', icon: BookOpen, action: () => onNavigate('learners', { action: 'assign' }) },
+ { title: 'Download Reports', icon: Download, action: () => onNavigate('analytics') },
+ { title: 'Testimonials Queue', icon: MessageSquare, action: () => onNavigate('testimonials') }
+ ].map((link, index) => {
+ const Icon = link.icon;
+ return (
+
+
+ {link.title}
+
+ );
+ })}
+
+
+
+
+ {/* Announcements & Reminders */}
+
+
+
+
+ Announcements & Reminders
+ Recent updates and notifications
+
+
+
+
+
+
+ All
+ Announcements
+ Reminders
+
+
+
+
+
+
+ {mockAnnouncements.map((item) => (
+
+
+
+
+
{item.title}
+ {item.pinned && (
+ Pinned
+ )}
+
+
{item.content}
+
+
+ {item.type}
+
+ {item.timestamp}
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/pages/LearnersScreen.tsx b/src/pages/LearnersScreen.tsx
new file mode 100644
index 0000000..dc90f3e
--- /dev/null
+++ b/src/pages/LearnersScreen.tsx
@@ -0,0 +1,593 @@
+import { useCallback, useEffect, useState } from "react";
+import { Employee } from "../types";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../components/ui/Card";
+
+import { Badge } from "../components/ui/badge";
+import { Input } from "../components/ui/input";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../components/ui/select";
+import { Button } from "../components/ui/button";
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
+import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "../components/ui/sheet";
+import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "../components/ui/dialog";
+import { Tabs } from "../components/ui/tabs";
+import { TabsList } from "../components/ui/tabs";
+import { TabsTrigger } from "../components/ui/tabs";
+import { TabsContent } from "../components/ui/tabs";
+import { Progress } from "../components/ui/progress";
+import { Download, Edit, MoreHorizontal, Plus, Search, Upload } from "lucide-react";
+// import { mockEmployees } from "../data/mockEmployees";
+
+export default function LearnersScreen({ filters }: { filters?: any }) {
+ const mockEmployees: Employee[] = [
+ { id: '1', name: 'Sarah Chen', email: 'sarah.chen@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Leadership Development', course: 'Strategic Thinking', progress: 85, lastActivity: '2 hours ago' },
+ { id: '2', name: 'Michael Rodriguez', email: 'michael.r@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Technical Skills', course: 'Data Analysis', progress: 62, lastActivity: '1 day ago' },
+ { id: '3', name: 'Emma Thompson', email: 'emma.thompson@company.com', phone: '+61 4XX XXX XXX', status: 'Pending', programme: 'Communication', course: 'Public Speaking', progress: 0, lastActivity: 'Never' },
+ { id: '4', name: 'David Kim', email: 'david.kim@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Project Management', course: 'Agile Methodology', progress: 94, lastActivity: '3 hours ago' },
+ { id: '5', name: 'Lisa Wang', email: 'lisa.wang@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Leadership Development', course: 'Team Management', progress: 78, lastActivity: '5 hours ago' },
+ { id: '6', name: 'James Wilson', email: 'james.wilson@company.com', phone: '+61 4XX XXX XXX', status: 'Inactive', programme: 'Technical Skills', course: 'Programming Basics', progress: 34, lastActivity: '2 weeks ago' },
+ { id: '7', name: 'Maria Garcia', email: 'maria.garcia@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Sales Training', course: 'Customer Relations', progress: 56, lastActivity: '1 day ago' },
+ { id: '8', name: 'Robert Lee', email: 'robert.lee@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Leadership Development', course: 'Decision Making', progress: 89, lastActivity: '4 hours ago' },
+ { id: '9', name: 'Jennifer Davis', email: 'jennifer.davis@company.com', phone: '+61 4XX XXX XXX', status: 'Pending', programme: 'Communication', course: 'Written Communication', progress: 0, lastActivity: 'Never' },
+ { id: '10', name: 'Thomas Brown', email: 'thomas.brown@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Project Management', course: 'Risk Management', progress: 71, lastActivity: '6 hours ago' }
+ ];
+ const [employees, setEmployees] = useState(mockEmployees);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [statusFilter, setStatusFilter] = useState('all');
+ const [selectedEmployees, setSelectedEmployees] = useState
([]);
+ const [showAddDrawer, setShowAddDrawer] = useState(false);
+ const [showImportModal, setShowImportModal] = useState(false);
+ const [showAssignModal, setShowAssignModal] = useState(false);
+ const [showEditDrawer, setShowEditDrawer] = useState(false);
+ const [editingEmployee, setEditingEmployee] = useState(null);
+ const [newEmployee, setNewEmployee] = useState({ name: '', email: '', phone: '' });
+ const [bulkActionVisible, setBulkActionVisible] = useState(false);
+
+ const debouncedSearch = useCallback(
+ (term: string) => {
+ // Simulating search with 300ms delay
+ const timer = setTimeout(() => {
+ setSearchTerm(term);
+ }, 300);
+ return () => clearTimeout(timer);
+ },
+ []
+ );
+
+ const filteredEmployees = employees.filter((emp: any) => {
+ const matchesSearch = emp.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ emp.email.toLowerCase().includes(searchTerm.toLowerCase());
+ const matchesStatus = statusFilter === 'all' || emp.status === statusFilter;
+ return matchesSearch && matchesStatus;
+ });
+
+ const handleEmployeeSelect = (employeeId: string, selected: boolean) => {
+ if (selected) {
+ setSelectedEmployees(prev => [...prev, employeeId]);
+ } else {
+ setSelectedEmployees(prev => prev.filter(id => id !== employeeId));
+ }
+ };
+
+ const handleBulkSelect = (selectAll: boolean) => {
+ if (selectAll) {
+ setSelectedEmployees(filteredEmployees.map((emp: any) => emp.id));
+ } else {
+ setSelectedEmployees([]);
+ }
+ };
+
+ useEffect(() => {
+ setBulkActionVisible(selectedEmployees.length > 0);
+ }, [selectedEmployees]);
+
+ const handleAddEmployee = () => {
+ if (newEmployee.name && newEmployee.email) {
+ const newEmp: Employee = {
+ id: Date.now().toString(),
+ name: newEmployee.name,
+ email: newEmployee.email,
+ phone: newEmployee.phone,
+ status: 'Pending'
+ };
+ setEmployees((prev: any) => [...prev, newEmp]);
+ setNewEmployee({ name: '', email: '', phone: '' });
+ setShowAddDrawer(false);
+ // Show success toast (simulated)
+ console.log('Employee added successfully');
+ }
+ };
+
+ const handleEditEmployee = (employee: Employee) => {
+ setEditingEmployee(employee);
+ setShowEditDrawer(true);
+ };
+
+ return (
+
+ {/* Toolbar */}
+ {/*
+
+
+
+
+
+ debouncedSearch(e.target.value)}
+ aria-label="Search learners by name or email"
+ />
+
+
+
+
+
+
+ All Status
+ Active
+ Inactive
+ Pending
+
+
+
+
+
setShowAddDrawer(true)}
+ className="min-tap-44"
+ aria-label="Add new learner"
+ >
+
+ Add Learner
+
+
setShowImportModal(true)}
+ className="min-tap-44"
+ aria-label="Import learners from CSV"
+ >
+
+ Import Learners
+
+
+
+
+ */}
+
+
+
+ {/* Left side (Search + Filter) */}
+
+
+
+ debouncedSearch(e.target.value)}
+ aria-label="Search learners by name or email"
+ />
+
+
+
+
+
+
+ All Status
+ Active
+ Inactive
+ Pending
+
+
+
+
+ {/* Right side (Buttons) */}
+
+
setShowAddDrawer(true)}
+ className="w-full sm:w-auto py-6"
+ aria-label="Add new learner"
+ >
+
+ Add Learner
+
+
setShowImportModal(true)}
+ className="w-full sm:w-auto py-6"
+ aria-label="Import learners from CSV"
+ >
+
+ Import Learners
+
+
+
+
+
+
+
+ {/* Bulk Action Bar */}
+ {bulkActionVisible && (
+
+
+
+
+ {selectedEmployees.length} learner{selectedEmployees.length !== 1 ? 's' : ''} selected
+
+
+ setShowAssignModal(true)}
+ className="min-tap-44"
+ >
+ Assign to Programme/Course
+
+
+ Deactivate
+
+
+ Reactivate
+
+
+
+
+
+ )}
+
+ {/* Learners Table */}
+
+
+
+
+ Learners ({filteredEmployees.length})
+ Manage learner accounts and assignments
+
+
+ handleBulkSelect(selectedEmployees.length !== filteredEmployees.length)}
+ className="min-tap-44"
+ >
+ {selectedEmployees.length === filteredEmployees.length ? 'Deselect All' : 'Select All'}
+
+
+
+
+
+
+
+
+
+ {/* Add Learner Drawer */}
+
+
+
+ Add New Learner
+
+ Add a new learner to the system. Email cannot be changed after saving.
+
+
+
+
+
+ Employee Name *
+
+ setNewEmployee(prev => ({ ...prev, name: e.target.value }))}
+ placeholder="Enter full name"
+ required
+ aria-required="true"
+ />
+
+
+
+ Email Address *
+
+ setNewEmployee(prev => ({ ...prev, email: e.target.value }))}
+ placeholder="email@company.com"
+ required
+ aria-required="true"
+ />
+
+
+
+ Phone Number
+
+ setNewEmployee(prev => ({ ...prev, phone: e.target.value }))}
+ placeholder="+61 4XX XXX XXX"
+ />
+
+
+
+ Save Learner
+
+ setShowAddDrawer(false)} className="flex-1">
+ Cancel
+
+
+
+
+
+
+ {/* Import Modal */}
+
+
+
+ Import Learners
+
+ Upload a CSV file to import multiple learners. Maximum file size: 5MB.
+
+
+
+
+
Step 1: Download Template
+
+ Download our CSV template with the required fields: Name, Email, Phone.
+
+
+
+ Download CSV Template
+
+
+
+
Step 2: Upload File
+
+
+
+ Drag and drop your CSV file here, or click to browse
+
+
+ Choose File
+
+
+
+
+ Import Learners
+ setShowImportModal(false)} className="flex-1">
+ Cancel
+
+
+
+
+
+
+ {/* Assign Modal */}
+
+
+
+ Assign to Programme/Course
+
+ Assign {selectedEmployees.length} selected learner{selectedEmployees.length !== 1 ? 's' : ''} to a programme or course.
+
+
+
+
+
+ Select Programme/Course
+
+
+
+
+
+
+ Leadership Development
+ Technical Skills
+ Communication
+ Project Management
+
+
+
+
+
+ Start Date (Optional)
+
+
+
+
+ Assign
+ setShowAssignModal(false)} className="flex-1">
+ Cancel
+
+
+
+
+
+
+ {/* Edit/Assign Drawer */}
+
+
+
+
+ {editingEmployee?.name}
+
+
+ Edit learner details and manage course assignments.
+
+
+ {editingEmployee && (
+
+
+ Details
+ Enrolments
+
+
+
+
+ Name
+
+
+
+
+
+ Phone
+
+
+
+
+
+ Status
+
+
+
+
+
+
+ Active
+ Inactive
+ Pending
+
+
+
+
+
+
+
+
Current Enrolments
+
+
+ Assign Course
+
+
+ {editingEmployee.programme && (
+
+
+
+
+
{editingEmployee.programme}
+
{editingEmployee.course}
+ {editingEmployee.progress !== undefined && (
+
+ )}
+
+
+ Unassign
+
+
+
+
+ )}
+
+
+
+ Save Changes
+ setShowEditDrawer(false)} className="flex-1">
+ Cancel
+
+
+
+ )}
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/pages/SettingsScreen.tsx b/src/pages/SettingsScreen.tsx
new file mode 100644
index 0000000..64ae42a
--- /dev/null
+++ b/src/pages/SettingsScreen.tsx
@@ -0,0 +1,109 @@
+import { useState } from "react";
+import { Card, CardContent } from "../components/ui/Card";
+import { Tabs, TabsList, TabsContent } from "../components/ui/tabs";
+import { TabsTrigger } from "../components/ui/tabs";
+import { Building2 } from "lucide-react";
+import { CreditCard } from "lucide-react";
+import { Shield } from "lucide-react";
+import { CardHeader } from "../components/ui/Card";
+import { CardTitle } from "../components/ui/Card";
+import { CardDescription } from "../components/ui/Card";
+
+export default function SettingsScreen() {
+ const [activeTab, setActiveTab] = useState('profile');
+
+ return (
+
+ {/* Settings Tabs */}
+
+
+
+
+
+
+ Org Profile
+ Profile
+
+
+
+ Billing
+
+
+
+ Roles
+
+
+
+
+
+
+ Organisation Profile
+ Manage your organisation's profile and settings
+
+
+
+
+
Organisation profile settings would be displayed here
+
+
+
+
+
+
+
+
+ Billing & Subscriptions
+ Manage your billing information and subscription plans
+
+
+
+
+
Billing and subscription management would be displayed here
+
+
+
+
+
+
+
+
+ Roles & Permissions
+ Manage user roles and access permissions
+
+
+
+
+
Roles and permissions management would be displayed here
+
+
+
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/pages/TestimonialsScreen.tsx b/src/pages/TestimonialsScreen.tsx
new file mode 100644
index 0000000..aec7b0f
--- /dev/null
+++ b/src/pages/TestimonialsScreen.tsx
@@ -0,0 +1,399 @@
+import { useState } from "react";
+import { useLocalStorage } from "../hooks/useLocalStorage";
+import { useEffect } from "react";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../components/ui/Card";
+import { Skeleton } from "../components/ui/skeleton";
+import { Alert, AlertDescription } from "../components/ui/alert";
+import { CheckCircle } from "lucide-react";
+import { Button } from "../components/ui/button";
+import { Input } from "../components/ui/input";
+import { Textarea } from "../components/ui/textarea";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../components/ui/select";
+import { Checkbox } from "../components/ui/checkbox";
+import { Info } from "lucide-react";
+import { ExternalLink } from "lucide-react";
+import { TestimonialFormData } from "../types";
+import { RefreshCw } from "lucide-react";
+import { AlertCircle } from "lucide-react";
+
+export default function TestimonialsScreen() {
+ const [loading, setLoading] = useState(true);
+ const [formData, setFormData] = useState({
+ name: 'Alex Sharma',
+ email: 'alex.sharma@company.com',
+ phone: '',
+ organisation: 'Acme Corp',
+ programme: '',
+ testimonialText: '',
+ consentToPublish: false
+ });
+ const [formErrors, setFormErrors] = useState>({});
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [submitSuccess, setSubmitSuccess] = useState(false);
+ const [submitError, setSubmitError] = useState('');
+ const [charCount, setCharCount] = useState(0);
+ const [prefersReducedMotion] = useLocalStorage('prefersReducedMotion', false);
+
+ // Simulate profile pre-fill loading
+ useEffect(() => {
+ const timer = setTimeout(() => setLoading(false), 300);
+ return () => clearTimeout(timer);
+ }, []);
+
+ const programmes = [
+ 'Leadership Development',
+ 'Technical Skills',
+ 'Communication',
+ 'Project Management',
+ 'Sales Training'
+ ];
+
+ const validateForm = () => {
+ const errors: Record = {};
+
+ if (!formData.testimonialText.trim()) {
+ errors.testimonialText = 'Testimonial text is required';
+ } else if (formData.testimonialText.length < 1) {
+ errors.testimonialText = 'Testimonial must be at least 1 character';
+ } else if (formData.testimonialText.length > 2000) {
+ errors.testimonialText = 'Testimonial must be 2000 characters or less';
+ }
+
+ if (!formData.consentToPublish) {
+ errors.consentToPublish = 'You must consent to publish your testimonial';
+ }
+
+ return errors;
+ };
+
+ const isFormValid = () => {
+ const errors = validateForm();
+ return Object.keys(errors).length === 0 && formData.testimonialText.trim().length > 0;
+ };
+
+ const handleInputChange = (field: keyof TestimonialFormData, value: string | boolean) => {
+ setFormData(prev => ({ ...prev, [field]: value }));
+
+ if (field === 'testimonialText' && typeof value === 'string') {
+ setCharCount(value.length);
+ }
+
+ // Clear field error when user starts typing
+ if (formErrors[field]) {
+ setFormErrors(prev => {
+ const newErrors = { ...prev };
+ delete newErrors[field];
+ return newErrors;
+ });
+ }
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ const errors = validateForm();
+ if (Object.keys(errors).length > 0) {
+ setFormErrors(errors);
+ return;
+ }
+
+ setIsSubmitting(true);
+ setSubmitError('');
+
+ try {
+ // Simulate API call
+ await new Promise(resolve => setTimeout(resolve, 1500));
+
+ // Simulate occasional error for testing
+ if (Math.random() > 0.9) {
+ throw new Error('Submission failed. Please try again.');
+ }
+
+ setSubmitSuccess(true);
+ setFormData(prev => ({ ...prev, testimonialText: '', programme: '', consentToPublish: false }));
+ setCharCount(0);
+ } catch (error) {
+ setSubmitError(error instanceof Error ? error.message : 'An error occurred. Please try again.');
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const resetForm = () => {
+ setSubmitSuccess(false);
+ setSubmitError('');
+ setFormErrors({});
+ };
+
+ if (loading) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Success State */}
+ {submitSuccess && (
+
+
+
+ Thanks — your testimonial is pending review by KLC.
+
+ Submit another testimonial
+
+
+
+ )}
+
+ {/* Error State */}
+ {submitError && (
+
+
+
+ {submitError}
+
+
+ )}
+
+ {/* Testimonial Form */}
+
+
+ Submit Testimonial
+
+ Share your experience with KLC programmes to help others discover the value of our learning solutions.
+
+
+
+
+
+ {/* Your Name */}
+
+
+ Your Name
+
+
+
+ Pre-filled from your profile
+
+
+
+ {/* Work Email */}
+
+
+ Work Email
+
+
+
+ Pre-filled from your profile
+
+
+
+ {/* Phone */}
+
+
+ Phone (Optional)
+
+ handleInputChange('phone', e.target.value)}
+ placeholder="+61 4XX XXX XXX"
+ className="min-tap-44"
+ />
+
+
+ {/* Organisation */}
+
+
+ Organisation
+
+
+
+ Pre-filled from your profile
+
+
+
+
+ {/* Programme */}
+
+
+ Programme (Optional)
+
+ handleInputChange('programme', value)}
+ >
+
+
+
+
+ {programmes.map((programme) => (
+
+ {programme}
+
+ ))}
+
+
+
+
+ {/* Testimonial Text */}
+
+
+ Testimonial Text *
+
+
handleInputChange('testimonialText', e.target.value)}
+ placeholder="Share your experience (1–2000 chars)…"
+ className={`min-h-[120px] min-tap-44 ${formErrors.testimonialText ? 'border-status-error' : ''}`}
+ aria-invalid={!!formErrors.testimonialText}
+ aria-describedby="testimonial-help testimonial-counter testimonial-error"
+ maxLength={2000}
+ required
+ />
+
+
+
+ Share what you learned, how it helped, or what you'd recommend to others
+
+ {formErrors.testimonialText && (
+
+ {formErrors.testimonialText}
+
+ )}
+
+
2000 ? 'text-status-error' : 'text-muted-foreground'}`}
+ aria-live="polite"
+ >
+ {charCount}/2000
+
+
+
+
+ {/* Consent Checkbox */}
+
+
+
handleInputChange('consentToPublish', !!checked)}
+ className={`min-tap-44 mt-1 ${formErrors.consentToPublish ? 'border-status-error' : ''}`}
+ aria-invalid={!!formErrors.consentToPublish}
+ aria-describedby="consent-label consent-error"
+ required
+ />
+
+
+ I consent to publish this testimonial *
+
+
+ Your testimonial may be used in marketing materials and on our website.
+ You can request removal at any time by contacting us.
+
+ {formErrors.consentToPublish && (
+
+ {formErrors.consentToPublish}
+
+ )}
+
+
+
+
+ {/* Submit Button */}
+
+
+ {isSubmitting ? (
+ <>
+
+ Submitting...
+ >
+ ) : (
+ 'Submit Testimonial'
+ )}
+
+
+ Your testimonial will be reviewed before being published
+
+
+
+
+
+
+ {/* Status Notice */}
+
+
+
+
+
+
+ Submissions are reviewed by KLC Super-Admins before appearing publicly.
+
+ View policy
+
+
+
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/redux/services/demo.services.ts b/src/redux/services/demo.services.ts
new file mode 100644
index 0000000..bc5c1a2
--- /dev/null
+++ b/src/redux/services/demo.services.ts
@@ -0,0 +1,24 @@
+// src/services/demo.service.ts
+import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
+
+export const demoApi = createApi({
+ reducerPath: "demoApi",
+ baseQuery: fetchBaseQuery({ baseUrl: "https://jsonplaceholder.typicode.com" }),
+ endpoints: (builder) => ({
+ // GET example
+ getPosts: builder.query({
+ query: () => "/posts",
+ }),
+
+ // POST example
+ createPost: builder.mutation({
+ query: (newPost) => ({
+ url: "/posts",
+ method: "POST",
+ body: newPost,
+ }),
+ }),
+ }),
+});
+
+export const { useGetPostsQuery, useCreatePostMutation } = demoApi;
diff --git a/src/redux/store.tsx b/src/redux/store.tsx
new file mode 100644
index 0000000..7e67006
--- /dev/null
+++ b/src/redux/store.tsx
@@ -0,0 +1,13 @@
+import { configureStore } from "@reduxjs/toolkit";
+import { demoApi } from "./services/demo.services";
+export const store = configureStore({
+ reducer: {
+ [demoApi.reducerPath]: demoApi.reducer,
+ },
+ middleware: (getDefaultMiddleware) =>
+ getDefaultMiddleware().concat(demoApi.middleware),
+});
+
+// Types
+export type RootState = ReturnType;
+export type AppDispatch = typeof store.dispatch;
diff --git a/src/styles/globals.css b/src/styles/globals.css
index 19ac19a..66cb71b 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -4,7 +4,7 @@
:root {
--font-size: 14px;
-
+
/* KLC Brand Color Primitives */
--brand-gold: #F8C301;
--brand-gold-foreground: #26231a;
@@ -15,7 +15,7 @@
--silver-grey: #C0C0C0;
--burgundy: #89002D;
--burgundy-foreground: #ffffff;
-
+
/* Semantic Tokens - Light Mode */
--background: #ffffff;
--foreground: #26231A;
@@ -32,7 +32,7 @@
--status-error: #d64545;
--status-error-foreground: #ffffff;
--chrome-divider: rgba(0, 0, 0, 0.1);
-
+
/* Component Variables */
--card: #ffffff;
--card-foreground: #26231A;
@@ -53,50 +53,69 @@
--input-background: #f3f3f5;
--switch-background: #cbced4;
--ring: #04045B;
-
+
/* Chart Colors */
--chart-1: #04045B;
--chart-2: #F8C301;
--chart-3: #21a36a;
--chart-4: #89002D;
--chart-5: #C0C0C0;
-
+
/* Spacing Scale (4-32px) */
- --spacing-4: 0.286rem; /* 4px */
- --spacing-8: 0.571rem; /* 8px */
- --spacing-12: 0.857rem; /* 12px */
- --spacing-16: 1.143rem; /* 16px */
- --spacing-20: 1.429rem; /* 20px */
- --spacing-24: 1.714rem; /* 24px */
- --spacing-28: 2rem; /* 28px */
- --spacing-32: 2.286rem; /* 32px */
-
+ --spacing-4: 0.286rem;
+ /* 4px */
+ --spacing-8: 0.571rem;
+ /* 8px */
+ --spacing-12: 0.857rem;
+ /* 12px */
+ --spacing-16: 1.143rem;
+ /* 16px */
+ --spacing-20: 1.429rem;
+ /* 20px */
+ --spacing-24: 1.714rem;
+ /* 24px */
+ --spacing-28: 2rem;
+ /* 28px */
+ --spacing-32: 2.286rem;
+ /* 32px */
+
/* Radius Scale */
- --radius-4: 0.286rem; /* 4px */
- --radius-8: 0.571rem; /* 8px */
- --radius-12: 0.857rem; /* 12px */
+ --radius-4: 0.286rem;
+ /* 4px */
+ --radius-8: 0.571rem;
+ /* 8px */
+ --radius-12: 0.857rem;
+ /* 12px */
--radius: var(--radius-8);
-
+
/* Shadow Scale */
--shadow-1: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-2: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
--shadow-3: 0 4px 6px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.06);
-
+
/* Type Scale (12-32px) */
- --text-12: 0.857rem; /* 12px */
- --text-14: 1rem; /* 14px */
- --text-16: 1.143rem; /* 16px */
- --text-18: 1.286rem; /* 18px */
- --text-20: 1.429rem; /* 20px */
- --text-24: 1.714rem; /* 24px */
- --text-28: 2rem; /* 28px */
- --text-32: 2.286rem; /* 32px */
-
+ --text-12: 0.857rem;
+ /* 12px */
+ --text-14: 1rem;
+ /* 14px */
+ --text-16: 1.143rem;
+ /* 16px */
+ --text-18: 1.286rem;
+ /* 18px */
+ --text-20: 1.429rem;
+ /* 20px */
+ --text-24: 1.714rem;
+ /* 24px */
+ --text-28: 2rem;
+ /* 28px */
+ --text-32: 2.286rem;
+ /* 32px */
+
--font-weight-medium: 500;
--font-weight-normal: 400;
--font-weight-semibold: 600;
--font-weight-bold: 700;
-
+
/* HR Sidebar Variables - Navy Blue Background */
--sidebar: #04045B;
--sidebar-foreground: #ffffff;
@@ -125,7 +144,7 @@
--status-error: #ef4444;
--status-error-foreground: #ffffff;
--chrome-divider: rgba(255, 255, 255, 0.1);
-
+
/* Component Variables - Dark Mode */
--card: #2a2a2a;
--card-foreground: #ffffff;
@@ -146,14 +165,14 @@
--input-background: #374151;
--switch-background: #4b5563;
--ring: #6366f1;
-
+
/* Chart Colors - Dark Mode */
--chart-1: #6366f1;
--chart-2: #F8C301;
--chart-3: #22c55e;
--chart-4: #ef4444;
--chart-5: #a1a1aa;
-
+
/* HR Sidebar Variables - Keep Navy Blue in Dark Mode */
--sidebar: #04045B;
--sidebar-foreground: #ffffff;
@@ -396,28 +415,55 @@
}
@keyframes fadeIn {
- from { opacity: 0; }
- to { opacity: 1; }
+ from {
+ opacity: 0;
+ }
+
+ to {
+ opacity: 1;
+ }
}
@keyframes slideUp {
- from { opacity: 0; transform: translateY(20px); }
- to { opacity: 1; transform: translateY(0); }
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
}
@keyframes slideInRight {
- from { opacity: 0; transform: translateX(100%); }
- to { opacity: 1; transform: translateX(0); }
+ from {
+ opacity: 0;
+ transform: translateX(100%);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
}
@keyframes countUp {
- from { transform: scale(0.8); opacity: 0; }
- to { transform: scale(1); opacity: 1; }
+ from {
+ transform: scale(0.8);
+ opacity: 0;
+ }
+
+ to {
+ transform: scale(1);
+ opacity: 1;
+ }
}
}
/* Accessibility: Reduce motion support */
@media (prefers-reduced-motion: reduce) {
+
.animate-fade-in,
.animate-slide-up,
.animate-count-up,
@@ -518,18 +564,96 @@
}
.skeleton {
- background: linear-gradient(
- 90deg,
- var(--muted) 0%,
- var(--accent) 50%,
- var(--muted) 100%
- );
+ background: linear-gradient(90deg,
+ var(--muted) 0%,
+ var(--accent) 50%,
+ var(--muted) 100%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
- 0% { background-position: -200% 0; }
- 100% { background-position: 200% 0; }
+ 0% {
+ background-position: -200% 0;
+ }
+
+ 100% {
+ background-position: 200% 0;
+ }
+ }
+}
+
+/* Employee Card Mobile View */
+/* Add this to your main global CSS file */
+@media (max-width: 640px) {
+ .employee-card-mobile-view {
+ display: block;
+ }
+
+ .employee-card-desktop-view {
+ display: none;
+ }
+}
+
+@media (min-width: 641px) {
+ .employee-card-mobile-view {
+ display: none;
+ }
+
+ .employee-card-desktop-view {
+ display: block;
+ }
+}
+
+/* Custom styles for the compact table */
+.compact-table {
+ font-size: 0.875rem;
+}
+
+.compact-table th,
+.compact-table td {
+ padding: 0.5rem 0.75rem;
+}
+
+/* Responsive table container */
+.table-container {
+ overflow-x: auto;
+ max-width: 100%;
+}
+
+/* Ensure buttons are touch-friendly on mobile */
+.min-tap-44 {
+ min-height: 44px;
+ min-width: 44px;
+}
+
+/* Truncate text for small containers */
+.truncate {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* Animation for card entrance */
+@keyframes slideUp {
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.animate-slide-up {
+ animation: slideUp 0.5s ease-out forwards;
+}
+
+/* Reduced motion support */
+@media (prefers-reduced-motion: reduce) {
+ .animate-slide-up {
+ animation: none;
}
}
\ No newline at end of file
diff --git a/src/types.ts b/src/types.ts
new file mode 100644
index 0000000..fd5167e
--- /dev/null
+++ b/src/types.ts
@@ -0,0 +1,47 @@
+export interface KPIData {
+ title: string;
+ value: number;
+ change?: number;
+ trend?: 'up' | 'down' | 'neutral';
+}
+
+export interface Employee {
+ id: string;
+ name: string;
+ email: string;
+ phone: string;
+ status: 'Active' | 'Inactive' | 'Pending';
+ programme?: string;
+ course?: string;
+ progress?: number;
+ lastActivity?: string;
+}
+
+export interface Announcement {
+ id: string;
+ title: string;
+ content: string;
+ type: 'announcement' | 'reminder';
+ timestamp: string;
+ pinned?: boolean;
+}
+
+export interface Deadline {
+ id: string;
+ title: string;
+ type: 'webinar' | 'profiler';
+ dueDate: string;
+ dueTime: string;
+}
+
+export interface TestimonialFormData {
+ name: string;
+ email: string;
+ phone: string;
+ organisation: string;
+ programme: string;
+ testimonialText: string;
+ consentToPublish: boolean;
+}
+
+