full src replace with new one
@@ -1,13 +0,0 @@
|
|||||||
import svgPaths from "./svg-50ykfce76w";
|
|
||||||
|
|
||||||
export default function CurrentLearningHeaderIcon() {
|
|
||||||
return (
|
|
||||||
<div className="relative size-full" data-name="current learning header icon">
|
|
||||||
<svg className="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 24 24">
|
|
||||||
<g id="current learning header icon">
|
|
||||||
<path d={svgPaths.p3fb52080} fill="var(--fill-0, black)" id="Vector" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export default {
|
|
||||||
p3fb52080: "M12.4789 3.26119C12.1804 3.09839 11.8196 3.09839 11.5211 3.26119L2.61015 8.12174C1.91514 8.50083 1.91532 9.49885 2.61045 9.8777L4.47854 10.8958C4.79998 11.071 5 11.4078 5 11.7739V16.5865C5 16.9524 5.19981 17.289 5.52097 17.4643L11.521 20.7386C11.8195 20.9015 12.1805 20.9015 12.479 20.7386L18.479 17.4643C18.8002 17.289 19 16.9524 19 16.5865V11.7739C19 11.4078 19.2 11.071 19.5215 10.8958V10.8958C20.1878 10.5326 21 11.015 21 11.7739V16C21 16.5523 21.4477 17 22 17V17C22.5523 17 23 16.5523 23 16V9.59363C23 9.22769 22.8001 8.89097 22.4789 8.71574L12.4789 3.26119ZM17.2105 8.1221C17.9054 8.50112 17.9054 9.49888 17.2105 9.8779L12.4789 12.4588C12.1804 12.6216 11.8196 12.6216 11.5211 12.4588L6.78947 9.87789C6.09461 9.49888 6.09461 8.50112 6.78948 8.1221L11.5211 5.54119C11.8196 5.37839 12.1804 5.37839 12.4789 5.54119L17.2105 8.1221ZM17 15.4056C17 15.772 16.7997 16.109 16.4779 16.284L12.4779 18.46C12.1799 18.6221 11.8201 18.6221 11.5221 18.46L7.52213 16.284C7.20032 16.109 7 15.772 7 15.4056V13.9554C7 13.1961 7.81284 12.7138 8.47922 13.0777L11.5208 14.7383C11.8195 14.9014 12.1806 14.9014 12.4792 14.7383L15.5208 13.0777C16.1872 12.7138 17 13.1961 17 13.9553V15.4056Z",
|
|
||||||
}
|
|
||||||
144
src/App.tsx
@@ -1,34 +1,122 @@
|
|||||||
import { Routes, Route } from "react-router-dom";
|
import React, { useState } from 'react';
|
||||||
// import AppLayout from "./components/AppLayout";
|
import { AppShell } from './components/AppShell';
|
||||||
|
import { MyCourses } from './components/MyCourses';
|
||||||
|
import { Leaderboard } from './components/Leaderboard';
|
||||||
|
import { ReportsAndCertificates } from './components/ReportsAndCertificates';
|
||||||
|
import { Notes } from './components/Notes';
|
||||||
|
import { AIMentor } from './components/AIMentor';
|
||||||
|
import { Blog } from './components/Blog';
|
||||||
|
import { DiscussionForums } from './components/DiscussionForums';
|
||||||
|
import { Notifications } from './components/Notifications';
|
||||||
|
import { Settings } from './components/Settings';
|
||||||
|
import { CoursePlayer } from './components/CoursePlayer';
|
||||||
|
|
||||||
// Pages
|
// Mock user data - in real app this would come from authentication
|
||||||
import IndividualWebinars from "./pages/IndividualWebinars";
|
const mockUser = {
|
||||||
import Leaderboard from "./pages/Leaderboard";
|
id: '1',
|
||||||
import HomePage from "./pages/HomePage";
|
firstName: 'Priya',
|
||||||
import Dashboard from "./pages/learner/Dashboard";
|
lastName: 'Sharma',
|
||||||
import { Library } from "./pages/learner/Library";
|
email: 'priya.sharma@techsolutions.com',
|
||||||
import { CourseTimeline } from "./pages/learner/CourseTimeline";
|
persona: 'corporate' as const,
|
||||||
import { Settings } from "./pages/Settings";
|
orgName: 'Tech Solutions Pvt Ltd',
|
||||||
import { Surveys } from "./pages/Surveys";
|
canSwitchMode: true,
|
||||||
|
canSwitchAccount: true,
|
||||||
|
avatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=64&h=64&fit=crop&crop=face'
|
||||||
|
};
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
const [currentPage, setCurrentPage] = useState('my-courses');
|
||||||
<>
|
const [currentCourse, setCurrentCourse] = useState<{ courseId: string; lessonId?: string } | null>(null);
|
||||||
<Routes>
|
|
||||||
{/* Main */}
|
|
||||||
<Route path="/" element={<Dashboard />} />
|
|
||||||
<Route path="/dashboard" element={<Dashboard />} />
|
|
||||||
<Route path="/library" element={<Library />} />
|
|
||||||
<Route path="/course" element={<CourseTimeline />} />
|
|
||||||
<Route path="/settings" element={<Settings userType="individual" />} />
|
|
||||||
<Route path="/surveys" element={<Surveys userType="individual" />} />
|
|
||||||
<Route path="/webinars" element={<IndividualWebinars />} />
|
|
||||||
<Route path="/individual-webinars" element={<IndividualWebinars />} />
|
|
||||||
<Route path="/leaderboard" element={<Leaderboard />} />
|
|
||||||
|
|
||||||
{/* Fallback */}
|
const handleCourseClick = (courseId: string, lessonId?: string) => {
|
||||||
<Route path="*" element={<HomePage />} />
|
setCurrentCourse({ courseId, lessonId });
|
||||||
</Routes>
|
};
|
||||||
</>
|
|
||||||
|
const handleBackToLibrary = () => {
|
||||||
|
setCurrentCourse(null);
|
||||||
|
setCurrentPage('my-courses');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNavigate = (page: string) => {
|
||||||
|
// If navigating to my-courses, clear any active course
|
||||||
|
if (page === 'my-courses') {
|
||||||
|
setCurrentCourse(null);
|
||||||
|
}
|
||||||
|
setCurrentPage(page);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderCurrentPage = () => {
|
||||||
|
// If viewing a course, show the course player
|
||||||
|
if (currentCourse) {
|
||||||
|
return (
|
||||||
|
<CoursePlayer
|
||||||
|
user={mockUser}
|
||||||
|
courseId={currentCourse.courseId}
|
||||||
|
lessonId={currentCourse.lessonId}
|
||||||
|
onNavigateBack={handleBackToLibrary}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise show the normal pages
|
||||||
|
switch (currentPage) {
|
||||||
|
case 'my-courses':
|
||||||
|
return <MyCourses user={mockUser} onCourseClick={handleCourseClick} />;
|
||||||
|
case 'leaderboard':
|
||||||
|
return <Leaderboard user={mockUser} />;
|
||||||
|
case 'reports-certificates':
|
||||||
|
return <ReportsAndCertificates />;
|
||||||
|
case 'notes':
|
||||||
|
return <Notes />;
|
||||||
|
case 'blog':
|
||||||
|
return <Blog />;
|
||||||
|
case 'forums':
|
||||||
|
return <DiscussionForums user={mockUser} />;
|
||||||
|
case 'notifications':
|
||||||
|
return <Notifications />;
|
||||||
|
case 'settings':
|
||||||
|
return <Settings user={mockUser} />;
|
||||||
|
default:
|
||||||
|
return <MyCourses user={mockUser} onCourseClick={handleCourseClick} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-white">
|
||||||
|
{/* Always show AppShell with top navigation */}
|
||||||
|
<AppShell
|
||||||
|
currentPage={currentCourse ? 'course-player' : currentPage}
|
||||||
|
user={mockUser}
|
||||||
|
onNavigate={handleNavigate}
|
||||||
|
isInCourseMode={!!currentCourse}
|
||||||
|
onLogoClick={handleBackToLibrary}
|
||||||
|
>
|
||||||
|
{renderCurrentPage()}
|
||||||
|
</AppShell>
|
||||||
|
|
||||||
|
{/* AI Mentor FAB - Always visible */}
|
||||||
|
<AIMentor />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholder component for unimplemented pages
|
||||||
|
function PlaceholderPage({ title, description }: { title: string; description: string }) {
|
||||||
|
return (
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-medium">{title}</h1>
|
||||||
|
<p className="text-muted-foreground">{description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-2 border-dashed border-border rounded-lg p-12 text-center">
|
||||||
|
<h2 className="text-xl font-medium text-muted-foreground mb-2">
|
||||||
|
{title} Coming Soon
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
This feature is currently under development and will be available soon.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
# KLC Website Guidelines
|
|
||||||
|
|
||||||
## Query Parameter Dashboard System
|
|
||||||
|
|
||||||
### Unified Dashboard Routing
|
|
||||||
* **Primary Route**: `/dashboard` - Auto-detects user type or defaults to individual
|
|
||||||
* **Explicit Routes**:
|
|
||||||
- `/dashboard?view=individual` - Forces individual learner view
|
|
||||||
- `/dashboard?view=corporate` - Forces corporate learner view
|
|
||||||
* **All Learner Portal Pages** now support query parameters:
|
|
||||||
- `/library?view=individual` or `/library?view=corporate`
|
|
||||||
- `/settings?view=individual` or `/settings?view=corporate`
|
|
||||||
- `/surveys?view=individual` or `/surveys?view=corporate`
|
|
||||||
- `/webinars?view=individual` or `/webinars?view=corporate`
|
|
||||||
- `/leaderboard?view=individual` or `/leaderboard?view=corporate`
|
|
||||||
|
|
||||||
### Legacy Route Handling
|
|
||||||
* **Automatic Redirects**: Old `/corporate/*` routes redirect to query parameter equivalents
|
|
||||||
* **Backward Compatibility**: Legacy routes still work but redirect users to new URLs
|
|
||||||
* **URL Updates**: Browser history automatically updates to new query parameter format
|
|
||||||
|
|
||||||
## Page Header & Navigation Standards
|
|
||||||
|
|
||||||
### Universal Back Button Requirements
|
|
||||||
* **All internal pages must include a back button** in the header for consistent navigation
|
|
||||||
* **Header structure**: Use `pt-24 pb-8` to account for fixed navbar (70px + padding)
|
|
||||||
* **Back button placement**: Position in top-left of header with proper spacing and alignment
|
|
||||||
* **Navigation logic**: Back button should return users to their expected previous page:
|
|
||||||
- Individual learner pages → Individual dashboard (`/dashboard?view=individual`)
|
|
||||||
- Corporate learner pages → Corporate dashboard (`/dashboard?view=corporate`)
|
|
||||||
- Service pages → Services overview (`/services`) [Deprecated - redirects to home]
|
|
||||||
- About pages → About overview (`/about-us/our-vision`) [Deprecated - redirects to home]
|
|
||||||
- Learning pages → Learning hub (`/learning/articles`) [Deprecated - redirects to home]
|
|
||||||
- General pages → Home page (`/`)
|
|
||||||
|
|
||||||
### Header Component Pattern
|
|
||||||
```tsx
|
|
||||||
// Standard header pattern for all pages
|
|
||||||
<div className="bg-primary text-primary-foreground pt-24 pb-8">
|
|
||||||
<div className="container mx-auto px-4 lg:px-8">
|
|
||||||
<div className="max-w-4xl mx-auto"> {/* Adjust max-width based on content */}
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={handleBackNavigation}
|
|
||||||
className="text-primary-foreground hover:bg-primary-foreground/10 min-h-[44px] min-w-[44px] mt-1"
|
|
||||||
aria-label="Go back to [appropriate page name]"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="h-6 w-6" />
|
|
||||||
</Button>
|
|
||||||
<div className="flex-1">
|
|
||||||
<h1 className="text-4xl mb-3">[Page Title]</h1>
|
|
||||||
<p className="text-lg text-primary-foreground/90 leading-relaxed">
|
|
||||||
[Page description]
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Required Imports for Back Navigation
|
|
||||||
```tsx
|
|
||||||
import { ArrowLeft } from 'lucide-react';
|
|
||||||
const navigate = useNavigate();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Standard Navigation Handler
|
|
||||||
```tsx
|
|
||||||
const handleBackNavigation = () => {
|
|
||||||
// Choose appropriate navigation based on page context
|
|
||||||
navigate('/dashboard?view=individual'); // or /dashboard?view=corporate for corporate learners
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## Typography & Font Sizing
|
|
||||||
|
|
||||||
### Minimum Font Size Requirements
|
|
||||||
* **Absolute minimum font size**: 14px (0.875rem at 14px base)
|
|
||||||
* **Preferred minimum font size**: 16px (1rem at 14px base)
|
|
||||||
* **Never use font sizes smaller than 14px** for any text content
|
|
||||||
* Use 16px (1rem) for all body text, labels, buttons, and form inputs
|
|
||||||
* Use 14px (0.875rem) only for small text elements like captions, metadata, or legal text
|
|
||||||
|
|
||||||
### Typography Hierarchy
|
|
||||||
* **H1**: 36px (2.25rem) - Page titles
|
|
||||||
* **H2**: 30px (1.875rem) - Section headings
|
|
||||||
* **H3**: 24px (1.5rem) - Subsection headings
|
|
||||||
* **H4**: 20px (1.25rem) - Component titles
|
|
||||||
* **H5**: 18px (1.125rem) - Card titles
|
|
||||||
* **H6**: 16px (1rem) - Small headings
|
|
||||||
* **Body text**: 16px (1rem) - All paragraph text
|
|
||||||
* **Labels/Buttons**: 16px (1rem) - Form labels, button text
|
|
||||||
* **Small text**: 14px (0.875rem) - Captions, metadata, fine print
|
|
||||||
|
|
||||||
### Font Implementation Rules
|
|
||||||
* Always use explicit font sizes in Tailwind classes when overriding defaults
|
|
||||||
* Use `text-base` (16px) as the default for most text content
|
|
||||||
* Use `text-sm` (14px) sparingly for secondary information
|
|
||||||
* Never use `text-xs` (12px) - override with `text-sm` minimum
|
|
||||||
* Ensure good contrast ratios (minimum 4.5:1) with all font sizes
|
|
||||||
|
|
||||||
## Layout & Spacing
|
|
||||||
|
|
||||||
### Horizontal Padding
|
|
||||||
* Apply consistent horizontal padding to all page sections using the same values as the navigation bar
|
|
||||||
* Use `px-4 lg:px-8` pattern for consistent horizontal spacing
|
|
||||||
* Container sections should use `container mx-auto px-4 lg:px-8`
|
|
||||||
|
|
||||||
### Accessibility
|
|
||||||
* Minimum 44×44px touch targets for buttons and interactive elements
|
|
||||||
* Respect `prefers-reduced-motion` for animations
|
|
||||||
* Maintain WCAG 2.1 AA compliance
|
|
||||||
* Test all font sizes for readability across devices
|
|
||||||
|
|
||||||
## Component Guidelines
|
|
||||||
|
|
||||||
### Text Elements
|
|
||||||
* Override default component font sizes if they fall below 14px
|
|
||||||
* Explicitly set typography classes on all text elements
|
|
||||||
* Use semantic HTML elements with appropriate font sizes
|
|
||||||
* Ensure form inputs and labels are minimum 16px for mobile usability
|
|
||||||
|
|
||||||
### Responsive Design
|
|
||||||
* Test font sizes across all breakpoints
|
|
||||||
* Ensure readability on mobile devices (minimum 16px for body text)
|
|
||||||
* Use responsive typography classes where appropriate
|
|
||||||
|
|
||||||
## Quality Checks
|
|
||||||
|
|
||||||
Before finalizing any page or component:
|
|
||||||
1. ✅ Verify no text is smaller than 14px
|
|
||||||
2. ✅ Confirm body text and interactive elements are 16px
|
|
||||||
3. ✅ Test readability across different screen sizes
|
|
||||||
4. ✅ Check contrast ratios meet accessibility standards
|
|
||||||
5. ✅ Ensure consistent horizontal padding with navigation
|
|
||||||
|
|
||||||
Some of the base components you are using may have styling (eg. gap/typography) baked in as defaults. Make sure you explicitly set any styling information from the guidelines in the generated React to override the defaults.
|
|
||||||
|
Before Width: | Height: | Size: 22 KiB |
BIN
src/assets/0c4562ded1d0bd9c2bc15a4240e05c7fa92725f6.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 899 KiB |
BIN
src/assets/1e150e43f238df3e08fcbf5d8f4899c233264e9f.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 310 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 20 KiB |
@@ -1,337 +0,0 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
|
||||||
import { Button } from './ui/button';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
|
||||||
import { Badge } from './ui/badge';
|
|
||||||
import {
|
|
||||||
MessageCircle,
|
|
||||||
X,
|
|
||||||
Send,
|
|
||||||
Bot,
|
|
||||||
User,
|
|
||||||
Minimize2,
|
|
||||||
Maximize2
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { Input } from './ui/input';
|
|
||||||
import { ScrollArea } from './ui/scroll-area';
|
|
||||||
|
|
||||||
interface Message {
|
|
||||||
id: string;
|
|
||||||
type: 'user' | 'bot';
|
|
||||||
content: string;
|
|
||||||
timestamp: Date;
|
|
||||||
suggestions?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialMessage: Message = {
|
|
||||||
id: '1',
|
|
||||||
type: 'bot',
|
|
||||||
content: "Hi! I'm here to help you explore KLC's leadership programs and facilities. What are you looking for today?",
|
|
||||||
timestamp: new Date(),
|
|
||||||
suggestions: [
|
|
||||||
"Show me leadership programs",
|
|
||||||
"Book a facility tour",
|
|
||||||
"Upcoming webinars",
|
|
||||||
"Contact information"
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
const botResponses: Record<string, { content: string; suggestions?: string[] }> = {
|
|
||||||
"programs": {
|
|
||||||
content: "Great! We offer various leadership development programs including Executive Leadership, Strategic Management, and Team Building workshops. Would you like to explore specific programs or see our full catalog?",
|
|
||||||
suggestions: ["View all programs", "Executive programs", "Team building", "Custom corporate training"]
|
|
||||||
},
|
|
||||||
"facilities": {
|
|
||||||
content: "Our state-of-the-art facilities include modern conference rooms, training halls, and collaboration spaces. You can take a virtual tour or book a facility for your event.",
|
|
||||||
suggestions: ["Virtual tour", "Book conference room", "Training halls", "Facility pricing"]
|
|
||||||
},
|
|
||||||
"webinars": {
|
|
||||||
content: "We host regular webinars on leadership topics. You can view upcoming sessions, register for live events, or access our library of recorded sessions.",
|
|
||||||
suggestions: ["Upcoming webinars", "Recorded sessions", "Register for webinar", "Webinar schedule"]
|
|
||||||
},
|
|
||||||
"contact": {
|
|
||||||
content: "You can reach us at info@klc.edu.in or call +91 11 4567 8900. Our team is available Monday to Friday, 9 AM to 6 PM IST. You can also schedule a consultation.",
|
|
||||||
suggestions: ["Schedule consultation", "Email us", "Office locations", "Support hours"]
|
|
||||||
},
|
|
||||||
"default": {
|
|
||||||
content: "I can help you with information about our programs, facilities, webinars, and more. What would you like to know?",
|
|
||||||
suggestions: ["Leadership programs", "Facility booking", "Webinars", "Contact us"]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export function AIChatbot() {
|
|
||||||
const [isVisible, setIsVisible] = useState(true); // Show immediately for testing
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [isMinimized, setIsMinimized] = useState(false);
|
|
||||||
const [messages, setMessages] = useState<Message[]>([initialMessage]);
|
|
||||||
const [inputValue, setInputValue] = useState('');
|
|
||||||
const [isTyping, setIsTyping] = useState(false);
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
||||||
const inactivityTimerRef = useRef<NodeJS.Timeout>();
|
|
||||||
|
|
||||||
// Show chatbot after shorter delay for better UX
|
|
||||||
useEffect(() => {
|
|
||||||
const resetTimer = () => {
|
|
||||||
if (inactivityTimerRef.current) {
|
|
||||||
clearTimeout(inactivityTimerRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
inactivityTimerRef.current = setTimeout(() => {
|
|
||||||
if (!isOpen) {
|
|
||||||
setIsVisible(true);
|
|
||||||
}
|
|
||||||
}, 5000); // Reduced to 5 seconds for better visibility
|
|
||||||
};
|
|
||||||
|
|
||||||
const events = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart'];
|
|
||||||
|
|
||||||
const addEventListeners = () => {
|
|
||||||
events.forEach(event => {
|
|
||||||
document.addEventListener(event, resetTimer, true);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeEventListeners = () => {
|
|
||||||
events.forEach(event => {
|
|
||||||
document.removeEventListener(event, resetTimer, true);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
addEventListeners();
|
|
||||||
resetTimer();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
removeEventListeners();
|
|
||||||
if (inactivityTimerRef.current) {
|
|
||||||
clearTimeout(inactivityTimerRef.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
// Auto-scroll to bottom of messages
|
|
||||||
useEffect(() => {
|
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
||||||
}, [messages]);
|
|
||||||
|
|
||||||
const getBotResponse = (userMessage: string): { content: string; suggestions?: string[] } => {
|
|
||||||
const message = userMessage.toLowerCase();
|
|
||||||
|
|
||||||
if (message.includes('program') || message.includes('course') || message.includes('training')) {
|
|
||||||
return botResponses.programs;
|
|
||||||
} else if (message.includes('facility') || message.includes('book') || message.includes('room')) {
|
|
||||||
return botResponses.facilities;
|
|
||||||
} else if (message.includes('webinar') || message.includes('session') || message.includes('online')) {
|
|
||||||
return botResponses.webinars;
|
|
||||||
} else if (message.includes('contact') || message.includes('phone') || message.includes('email')) {
|
|
||||||
return botResponses.contact;
|
|
||||||
} else {
|
|
||||||
return botResponses.default;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSendMessage = async (content: string) => {
|
|
||||||
if (!content.trim()) return;
|
|
||||||
|
|
||||||
// Add user message
|
|
||||||
const userMessage: Message = {
|
|
||||||
id: Date.now().toString(),
|
|
||||||
type: 'user',
|
|
||||||
content: content.trim(),
|
|
||||||
timestamp: new Date()
|
|
||||||
};
|
|
||||||
|
|
||||||
setMessages(prev => [...prev, userMessage]);
|
|
||||||
setInputValue('');
|
|
||||||
setIsTyping(true);
|
|
||||||
|
|
||||||
// Simulate typing delay
|
|
||||||
setTimeout(() => {
|
|
||||||
const response = getBotResponse(content);
|
|
||||||
const botMessage: Message = {
|
|
||||||
id: (Date.now() + 1).toString(),
|
|
||||||
type: 'bot',
|
|
||||||
content: response.content,
|
|
||||||
timestamp: new Date(),
|
|
||||||
suggestions: response.suggestions
|
|
||||||
};
|
|
||||||
|
|
||||||
setMessages(prev => [...prev, botMessage]);
|
|
||||||
setIsTyping(false);
|
|
||||||
}, 1000 + Math.random() * 1000); // 1-2 second delay
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSuggestionClick = (suggestion: string) => {
|
|
||||||
handleSendMessage(suggestion);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatTime = (date: Date) => {
|
|
||||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isVisible) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* Chatbot Toggle Button */}
|
|
||||||
{!isOpen && (
|
|
||||||
<Button
|
|
||||||
onClick={() => setIsOpen(true)}
|
|
||||||
className="fixed bottom-6 right-6 w-16 h-16 rounded-full shadow-lg z-50 bg-primary hover:bg-primary/90 transition-all duration-300 hover:scale-105 text-base"
|
|
||||||
size="lg"
|
|
||||||
>
|
|
||||||
<MessageCircle className="w-6 h-6 text-primary-foreground" />
|
|
||||||
<span className="sr-only">Open KLC Assistant chat</span>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Chat Window */}
|
|
||||||
{isOpen && (
|
|
||||||
<Card className={`fixed bottom-6 right-6 w-80 md:w-96 shadow-2xl z-50 transition-all duration-300 ${
|
|
||||||
isMinimized ? 'h-16' : 'h-[500px]'
|
|
||||||
}`}>
|
|
||||||
{/* Header */}
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-3 bg-primary text-primary-foreground rounded-t-lg">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-10 h-10 bg-primary-foreground/20 rounded-full flex items-center justify-center">
|
|
||||||
<Bot className="w-5 h-5" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<CardTitle className="text-base font-medium">KLC Assistant</CardTitle>
|
|
||||||
<div className="flex items-center gap-2 text-sm opacity-90">
|
|
||||||
<div className="w-2 h-2 bg-success rounded-full animate-pulse"></div>
|
|
||||||
<span>Online & Ready to Help</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setIsMinimized(!isMinimized)}
|
|
||||||
className="w-10 h-10 p-0 hover:bg-primary-foreground/20 text-primary-foreground text-base"
|
|
||||||
aria-label={isMinimized ? "Maximize chat" : "Minimize chat"}
|
|
||||||
>
|
|
||||||
{isMinimized ? (
|
|
||||||
<Maximize2 className="w-4 h-4" />
|
|
||||||
) : (
|
|
||||||
<Minimize2 className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setIsOpen(false)}
|
|
||||||
className="w-10 h-10 p-0 hover:bg-primary-foreground/20 text-primary-foreground text-base"
|
|
||||||
aria-label="Close chat"
|
|
||||||
>
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
{/* Chat Content */}
|
|
||||||
{!isMinimized && (
|
|
||||||
<CardContent className="p-0 flex flex-col h-[420px]">
|
|
||||||
{/* Messages */}
|
|
||||||
<ScrollArea className="flex-1 p-4 scrollbar-minimal">
|
|
||||||
<div className="space-y-4">
|
|
||||||
{messages.map((message) => (
|
|
||||||
<div key={message.id} className={`flex ${message.type === 'user' ? 'justify-end' : 'justify-start'}`}>
|
|
||||||
<div className={`flex items-start gap-3 max-w-[85%] ${message.type === 'user' ? 'flex-row-reverse' : ''}`}>
|
|
||||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
|
|
||||||
message.type === 'user'
|
|
||||||
? 'bg-primary text-primary-foreground'
|
|
||||||
: 'bg-muted text-muted-foreground'
|
|
||||||
}`}>
|
|
||||||
{message.type === 'user' ? (
|
|
||||||
<User className="w-4 h-4" />
|
|
||||||
) : (
|
|
||||||
<Bot className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className={`p-3 rounded-lg ${
|
|
||||||
message.type === 'user'
|
|
||||||
? 'bg-primary text-primary-foreground'
|
|
||||||
: 'bg-muted'
|
|
||||||
}`}>
|
|
||||||
<p className="text-sm leading-relaxed">{message.content}</p>
|
|
||||||
</div>
|
|
||||||
{message.suggestions && (
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{message.suggestions.map((suggestion, index) => (
|
|
||||||
<Badge
|
|
||||||
key={index}
|
|
||||||
variant="outline"
|
|
||||||
className="cursor-pointer hover:bg-primary hover:text-primary-foreground transition-colors text-sm py-1 px-3 rounded-full font-normal"
|
|
||||||
onClick={() => handleSuggestionClick(suggestion)}
|
|
||||||
>
|
|
||||||
{suggestion}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<p className="text-sm text-muted-foreground px-1">
|
|
||||||
{formatTime(message.timestamp)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{isTyping && (
|
|
||||||
<div className="flex justify-start">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<div className="w-8 h-8 bg-muted rounded-full flex items-center justify-center">
|
|
||||||
<Bot className="w-4 h-4" />
|
|
||||||
</div>
|
|
||||||
<div className="bg-muted p-3 rounded-lg">
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<div className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce"></div>
|
|
||||||
<div className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
|
|
||||||
<div className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div ref={messagesEndRef} />
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
|
|
||||||
{/* Input */}
|
|
||||||
<div className="border-t p-4">
|
|
||||||
<form
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
handleSendMessage(inputValue);
|
|
||||||
}}
|
|
||||||
className="flex gap-2"
|
|
||||||
role="form"
|
|
||||||
aria-label="Chat message form"
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
value={inputValue}
|
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
|
||||||
placeholder="Type your message..."
|
|
||||||
className="flex-1 text-base min-h-[44px]"
|
|
||||||
disabled={isTyping}
|
|
||||||
aria-label="Chat message input"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
size="sm"
|
|
||||||
disabled={!inputValue.trim() || isTyping}
|
|
||||||
className="px-4 min-h-[44px] min-w-[44px] text-base"
|
|
||||||
>
|
|
||||||
<Send className="w-4 h-4" />
|
|
||||||
<span className="sr-only">Send message</span>
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
439
src/components/AIMentor.tsx
Normal file
@@ -0,0 +1,439 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Send,
|
||||||
|
Bot,
|
||||||
|
User,
|
||||||
|
Lightbulb,
|
||||||
|
BookOpen,
|
||||||
|
Target,
|
||||||
|
TrendingUp,
|
||||||
|
MessageCircle,
|
||||||
|
X,
|
||||||
|
Youtube,
|
||||||
|
FileText,
|
||||||
|
Headphones,
|
||||||
|
ExternalLink,
|
||||||
|
Upload,
|
||||||
|
Edit3
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
import { Input } from './ui/input';
|
||||||
|
import { Badge } from './ui/badge';
|
||||||
|
import { ScrollArea } from './ui/scroll-area';
|
||||||
|
import { Separator } from './ui/separator';
|
||||||
|
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from './ui/sheet';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
|
||||||
|
import { Textarea } from './ui/textarea';
|
||||||
|
|
||||||
|
// Mock conversation data
|
||||||
|
const mockConversation = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
type: 'assistant',
|
||||||
|
content: "Hello! I'm your KLC Assistant. I'm here to help you with your learning journey. How can I assist you today?",
|
||||||
|
timestamp: '2024-09-03T09:00:00Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
type: 'user',
|
||||||
|
content: "What should I do next in my learning path?",
|
||||||
|
timestamp: '2024-09-03T09:01:00Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
type: 'assistant',
|
||||||
|
content: "Based on your current progress in the Strategic Leadership Development course, I recommend:\n\n1. **Complete Module 3, Lesson 2** - You're 65% through 'Risk Assessment Strategies'\n2. **Review your notes** - You have 4 notes that could benefit from additional practice\n3. **Join the upcoming webinar** - 'Future of Leadership in Digital Age' on Sep 15th\n\nWould you like me to explain any specific concept from your current lesson?",
|
||||||
|
timestamp: '2024-09-03T09:02:00Z'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const quickChips = [
|
||||||
|
{ id: '1', text: "What should I do next?", icon: Target },
|
||||||
|
{ id: '2', text: "Explain my Module 3 gap", icon: BookOpen },
|
||||||
|
{ id: '3', text: "Review my progress", icon: TrendingUp },
|
||||||
|
{ id: '4', text: "Find YouTube videos", icon: Youtube },
|
||||||
|
{ id: '5', text: "Get feedback on my notes", icon: Edit3 },
|
||||||
|
{ id: '6', text: "Leadership challenge help", icon: Lightbulb }
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockRecommendations = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
type: 'youtube',
|
||||||
|
title: 'KLC Leadership Interview: Digital Transformation',
|
||||||
|
description: 'CEO discusses modern leadership challenges in digital transformation',
|
||||||
|
url: 'https://youtube.com/watch?v=example1',
|
||||||
|
thumbnail: 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=400&h=200&fit=crop'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
type: 'article',
|
||||||
|
title: 'Research: Emotional Intelligence in Leadership',
|
||||||
|
description: 'Latest findings on EQ impact on team performance and organizational outcomes',
|
||||||
|
url: 'https://klc.edu/research/emotional-intelligence-2024',
|
||||||
|
thumbnail: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=200&fit=crop'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
type: 'podcast',
|
||||||
|
title: 'Future Leaders Podcast: Episode 24',
|
||||||
|
description: 'Discussion on adaptive leadership strategies with industry experts',
|
||||||
|
url: 'https://podcasts.klc.edu/future-leaders-24',
|
||||||
|
thumbnail: 'https://images.unsplash.com/photo-1535015853489-4c0e8b7cd34e?w=400&h=200&fit=crop'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export function AIMentor() {
|
||||||
|
const [messages, setMessages] = useState(mockConversation);
|
||||||
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
const [isTyping, setIsTyping] = useState(false);
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [activeTab, setActiveTab] = useState('chat');
|
||||||
|
const [notesFeedback, setNotesFeedback] = useState('');
|
||||||
|
|
||||||
|
const handleSendMessage = async () => {
|
||||||
|
if (!inputValue.trim()) return;
|
||||||
|
|
||||||
|
const newUserMessage = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
type: 'user' as const,
|
||||||
|
content: inputValue,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
setMessages(prev => [...prev, newUserMessage]);
|
||||||
|
setInputValue('');
|
||||||
|
setIsTyping(true);
|
||||||
|
|
||||||
|
// Enhanced AI response with recommendations
|
||||||
|
setTimeout(() => {
|
||||||
|
let responseContent = "I understand your question. Let me provide you with a helpful response based on your learning context and progress.";
|
||||||
|
|
||||||
|
// Add contextual recommendations based on query
|
||||||
|
if (inputValue.toLowerCase().includes('youtube') || inputValue.toLowerCase().includes('video')) {
|
||||||
|
responseContent += "\n\nHere are some relevant KLC YouTube interviews I'd recommend:\n• Digital Transformation Leadership with Sarah Chen\n• Emotional Intelligence Masterclass with Dr. Kumar\n• Crisis Leadership Strategies - Executive Panel";
|
||||||
|
} else if (inputValue.toLowerCase().includes('research') || inputValue.toLowerCase().includes('article')) {
|
||||||
|
responseContent += "\n\nBased on your current learning path, here are relevant research articles:\n• 'Adaptive Leadership in Remote Teams' - Harvard Business Review\n• 'Data-Driven Decision Making for Leaders' - KLC Research\n• 'Building Psychological Safety' - Stanford Leadership Institute";
|
||||||
|
} else if (inputValue.toLowerCase().includes('leadership challenge')) {
|
||||||
|
responseContent += "\n\nFor leadership challenges, I recommend:\n1. Review Module 3 content on conflict resolution\n2. Practice scenario planning with the KLC Leadership Simulator\n3. Connect with mentor Dr. Patel for personalized guidance\n\nWould you like me to schedule a mentoring session?";
|
||||||
|
}
|
||||||
|
|
||||||
|
const aiResponse = {
|
||||||
|
id: (Date.now() + 1).toString(),
|
||||||
|
type: 'assistant' as const,
|
||||||
|
content: responseContent,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
setMessages(prev => [...prev, aiResponse]);
|
||||||
|
setIsTyping(false);
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNotesFeedback = async () => {
|
||||||
|
if (!notesFeedback.trim()) return;
|
||||||
|
|
||||||
|
setIsTyping(true);
|
||||||
|
// Simulate AI feedback processing
|
||||||
|
setTimeout(() => {
|
||||||
|
const feedbackResponse = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
type: 'assistant' as const,
|
||||||
|
content: `I've analyzed your notes. Here's my feedback:\n\n**Strengths:**\n• Clear structure and logical flow\n• Good use of real-world examples\n• Well-connected to course concepts\n\n**Suggestions:**\n• Consider adding more specific metrics/data\n• Link to Module 4 content on implementation\n• Try the STAR method for case study format\n\n**Learning Resources:**\n• KLC Note-Taking Best Practices Guide\n• Strategic Thinking Framework Workshop\n\nWould you like me to suggest related courses based on your notes?`,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
setMessages(prev => [...prev, feedbackResponse]);
|
||||||
|
setNotesFeedback('');
|
||||||
|
setIsTyping(false);
|
||||||
|
setActiveTab('chat');
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChipClick = (chipText: string) => {
|
||||||
|
setInputValue(chipText);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (timestamp: string) => {
|
||||||
|
return new Date(timestamp).toLocaleTimeString('en-IN', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
timeZone: 'Asia/Kolkata'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Floating Action Button */}
|
||||||
|
<div className="fixed bottom-6 right-6 z-50">
|
||||||
|
<Button
|
||||||
|
onClick={() => setIsOpen(true)}
|
||||||
|
className="w-14 h-14 rounded-full bg-[var(--color-brand-primary)] hover:bg-[var(--color-brand-primary)]/90 text-white shadow-lg hover:shadow-xl transition-all duration-200"
|
||||||
|
size="icon"
|
||||||
|
>
|
||||||
|
<Bot className="h-6 w-6" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Enhanced Chat Panel with Tabs */}
|
||||||
|
<Sheet open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<SheetContent side="right" className="w-[400px] sm:w-[500px] p-0 bg-white">
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
{/* Header */}
|
||||||
|
<SheetHeader className="px-6 py-4 border-b bg-[var(--color-brand-primary)] text-white">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center">
|
||||||
|
<Bot className="h-5 w-5 text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<SheetTitle className="text-white text-lg">KLC Assistant</SheetTitle>
|
||||||
|
<SheetDescription className="text-blue-100 text-sm">
|
||||||
|
Strategic Leadership Development - Module 3
|
||||||
|
</SheetDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
|
className="text-white hover:bg-white/20"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</SheetHeader>
|
||||||
|
|
||||||
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col">
|
||||||
|
<TabsList className="mx-6 my-4 grid w-full grid-cols-3">
|
||||||
|
<TabsTrigger value="chat" className="text-xs">Chat</TabsTrigger>
|
||||||
|
<TabsTrigger value="resources" className="text-xs">Resources</TabsTrigger>
|
||||||
|
<TabsTrigger value="feedback" className="text-xs">Feedback</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="chat" className="flex-1 flex flex-col m-0">
|
||||||
|
{/* Quick Action Chips */}
|
||||||
|
<div className="px-6 py-4 border-b bg-gray-50">
|
||||||
|
<p className="text-sm font-medium mb-3">Quick questions:</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{quickChips.map((chip) => {
|
||||||
|
const Icon = chip.icon;
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={chip.id}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="text-xs h-8"
|
||||||
|
onClick={() => handleChipClick(chip.text)}
|
||||||
|
>
|
||||||
|
<Icon className="h-3 w-3 mr-1" />
|
||||||
|
{chip.text}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Messages Area */}
|
||||||
|
<ScrollArea className="flex-1 px-6 py-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{messages.map((message) => (
|
||||||
|
<div
|
||||||
|
key={message.id}
|
||||||
|
className={`flex gap-3 ${
|
||||||
|
message.type === 'user' ? 'justify-end' : 'justify-start'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{message.type === 'assistant' && (
|
||||||
|
<div className="w-8 h-8 rounded-full bg-[var(--color-brand-primary)] flex items-center justify-center flex-shrink-0">
|
||||||
|
<Bot className="h-4 w-4 text-white" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`max-w-[80%] p-3 rounded-lg ${
|
||||||
|
message.type === 'user'
|
||||||
|
? 'bg-[var(--color-brand-primary)] text-white'
|
||||||
|
: 'bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<p className="whitespace-pre-line text-sm">{message.content}</p>
|
||||||
|
<p className={`text-xs mt-1 ${
|
||||||
|
message.type === 'user' ? 'text-blue-100' : 'text-gray-600'
|
||||||
|
}`}>
|
||||||
|
{formatTime(message.timestamp)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{message.type === 'user' && (
|
||||||
|
<div className="w-8 h-8 rounded-full bg-[var(--color-brand-accent)] flex items-center justify-center flex-shrink-0">
|
||||||
|
<User className="h-4 w-4 text-[var(--color-brand-black)]" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{isTyping && (
|
||||||
|
<div className="flex gap-3 justify-start">
|
||||||
|
<div className="w-8 h-8 rounded-full bg-[var(--color-brand-primary)] flex items-center justify-center">
|
||||||
|
<Bot className="h-4 w-4 text-white" />
|
||||||
|
</div>
|
||||||
|
<div className="bg-gray-100 p-3 rounded-lg">
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
|
||||||
|
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
|
||||||
|
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
{/* Input Area */}
|
||||||
|
<div className="px-6 py-4 border-t bg-white">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder="Ask me anything..."
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleSendMessage();
|
||||||
|
}
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="flex-1 bg-white"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={handleSendMessage}
|
||||||
|
disabled={!inputValue.trim() || isTyping}
|
||||||
|
className="bg-[var(--color-brand-primary)] hover:bg-[var(--color-brand-primary)]/90 text-white"
|
||||||
|
>
|
||||||
|
<Send className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-600 mt-2">
|
||||||
|
Press Enter to send, Esc to close
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="resources" className="flex-1 m-0">
|
||||||
|
<ScrollArea className="h-full px-6 py-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium mb-3">Recommended for You</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{mockRecommendations.map((rec) => (
|
||||||
|
<Card key={rec.id} className="bg-white border">
|
||||||
|
<CardContent className="p-3">
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<img src={rec.thumbnail} alt={rec.title} className="w-16 h-12 object-cover rounded" />
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
{rec.type === 'youtube' && <Youtube className="h-3 w-3 text-red-500" />}
|
||||||
|
{rec.type === 'article' && <FileText className="h-3 w-3 text-blue-500" />}
|
||||||
|
{rec.type === 'podcast' && <Headphones className="h-3 w-3 text-green-500" />}
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{rec.type}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<h4 className="font-medium text-sm line-clamp-2 mb-1">{rec.title}</h4>
|
||||||
|
<p className="text-xs text-gray-600 line-clamp-2 mb-2">{rec.description}</p>
|
||||||
|
<Button size="sm" variant="outline" className="text-xs h-6">
|
||||||
|
<ExternalLink className="h-3 w-3 mr-1" />
|
||||||
|
View
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium mb-3">External References</h3>
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div className="flex items-center gap-2 p-2 bg-gray-50 rounded">
|
||||||
|
<FileText className="h-4 w-4 text-gray-500" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="font-medium">Harvard Business Review</p>
|
||||||
|
<p className="text-xs text-gray-600">Leadership in Crisis Management</p>
|
||||||
|
</div>
|
||||||
|
<Button size="sm" variant="ghost" className="text-xs h-6">
|
||||||
|
<ExternalLink className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 p-2 bg-gray-50 rounded">
|
||||||
|
<Youtube className="h-4 w-4 text-red-500" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="font-medium">MIT Leadership Center</p>
|
||||||
|
<p className="text-xs text-gray-600">Digital Leadership Strategies</p>
|
||||||
|
</div>
|
||||||
|
<Button size="sm" variant="ghost" className="text-xs h-6">
|
||||||
|
<ExternalLink className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="feedback" className="flex-1 m-0">
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<div className="px-6 py-4 border-b">
|
||||||
|
<h3 className="font-medium mb-2">Get AI Feedback</h3>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Upload your notes, case studies, or learning reflections for personalized feedback.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 px-6 py-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium mb-2 block">Your Notes/Case Study</label>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Paste your notes, case study, or reflection here..."
|
||||||
|
value={notesFeedback}
|
||||||
|
onChange={(e) => setNotesFeedback(e.target.value)}
|
||||||
|
className="min-h-[200px] bg-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={handleNotesFeedback}
|
||||||
|
disabled={!notesFeedback.trim() || isTyping}
|
||||||
|
className="flex-1 bg-[var(--color-brand-primary)] hover:bg-[var(--color-brand-primary)]/90 text-white"
|
||||||
|
>
|
||||||
|
Get AI Feedback
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
title="Upload file"
|
||||||
|
>
|
||||||
|
<Upload className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-xs text-gray-600 space-y-1">
|
||||||
|
<p>• Get suggestions for improvement</p>
|
||||||
|
<p>• Receive learning resource recommendations</p>
|
||||||
|
<p>• Connect insights to KLC framework</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
470
src/components/AppShell.tsx
Normal file
@@ -0,0 +1,470 @@
|
|||||||
|
import image_1e150e43f238df3e08fcbf5d8f4899c233264e9f from '../assets/1e150e43f238df3e08fcbf5d8f4899c233264e9f.png';
|
||||||
|
import React, { useState, useContext, createContext, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Bell,
|
||||||
|
Search,
|
||||||
|
Menu,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
BookOpen,
|
||||||
|
Library,
|
||||||
|
Award,
|
||||||
|
Trophy,
|
||||||
|
StickyNote,
|
||||||
|
Bot,
|
||||||
|
PenTool,
|
||||||
|
Settings,
|
||||||
|
User,
|
||||||
|
LogOut,
|
||||||
|
Building2,
|
||||||
|
UserCircle2,
|
||||||
|
ChevronDown,
|
||||||
|
MessageCircle,
|
||||||
|
GraduationCap
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
import { Input } from './ui/input';
|
||||||
|
import { Badge } from './ui/badge';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger
|
||||||
|
} from './ui/dropdown-menu';
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
||||||
|
import { Separator } from './ui/separator';
|
||||||
|
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||||
|
import { GlobalSearch } from './GlobalSearch';
|
||||||
|
|
||||||
|
// Persona and user context
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
firstName: string;
|
||||||
|
persona: 'corporate' | 'individual';
|
||||||
|
orgName?: string;
|
||||||
|
canSwitchMode?: boolean;
|
||||||
|
canSwitchAccount?: boolean;
|
||||||
|
avatar?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppContextType {
|
||||||
|
user: User;
|
||||||
|
currentMode: 'corporate' | 'hr' | 'individual';
|
||||||
|
currentAccount: 'corporate' | 'personal';
|
||||||
|
sidebarCollapsed: boolean;
|
||||||
|
setSidebarCollapsed: (collapsed: boolean) => void;
|
||||||
|
switchMode: (mode: 'corporate' | 'hr') => void;
|
||||||
|
switchAccount: (account: 'corporate' | 'personal') => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppContext = createContext<AppContextType | null>(null);
|
||||||
|
|
||||||
|
export const useAppContext = () => {
|
||||||
|
const context = useContext(AppContext);
|
||||||
|
if (!context) throw new Error('useAppContext must be used within AppShell');
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AppShellProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
currentPage: string;
|
||||||
|
user: User;
|
||||||
|
onNavigate?: (page: string) => void;
|
||||||
|
isInCourseMode?: boolean;
|
||||||
|
onLogoClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AppShell({ children, currentPage, user, onNavigate, isInCourseMode = false, onLogoClick }: AppShellProps) {
|
||||||
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||||
|
const [currentMode, setCurrentMode] = useState<'corporate' | 'hr' | 'individual'>(
|
||||||
|
user.persona === 'corporate' ? 'corporate' : 'individual'
|
||||||
|
);
|
||||||
|
const [currentAccount, setCurrentAccount] = useState<'corporate' | 'personal'>('corporate');
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [searchOpen, setSearchOpen] = useState(false);
|
||||||
|
|
||||||
|
const switchMode = (mode: 'corporate' | 'hr') => {
|
||||||
|
setCurrentMode(mode);
|
||||||
|
};
|
||||||
|
|
||||||
|
const switchAccount = (account: 'corporate' | 'personal') => {
|
||||||
|
setCurrentAccount(account);
|
||||||
|
};
|
||||||
|
|
||||||
|
const contextValue: AppContextType = {
|
||||||
|
user,
|
||||||
|
currentMode,
|
||||||
|
currentAccount,
|
||||||
|
sidebarCollapsed,
|
||||||
|
setSidebarCollapsed,
|
||||||
|
switchMode,
|
||||||
|
switchAccount
|
||||||
|
};
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{
|
||||||
|
id: 'my-courses',
|
||||||
|
label: 'My Courses',
|
||||||
|
icon: GraduationCap,
|
||||||
|
href: '/my-courses',
|
||||||
|
available: ['corporate', 'individual']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'leaderboard',
|
||||||
|
label: 'Leaderboard',
|
||||||
|
icon: Trophy,
|
||||||
|
href: '/leaderboard',
|
||||||
|
available: ['corporate', 'individual']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'reports-certificates',
|
||||||
|
label: 'Reports & Certificates',
|
||||||
|
icon: Award,
|
||||||
|
href: '/reports-certificates',
|
||||||
|
available: ['corporate', 'individual']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'notes',
|
||||||
|
label: 'Notes',
|
||||||
|
icon: StickyNote,
|
||||||
|
href: '/notes',
|
||||||
|
available: ['corporate', 'individual']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'blog',
|
||||||
|
label: 'Blog',
|
||||||
|
icon: PenTool,
|
||||||
|
href: '/blog',
|
||||||
|
available: ['corporate', 'individual']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'forums',
|
||||||
|
label: 'Discussion Forums',
|
||||||
|
icon: MessageCircle,
|
||||||
|
href: '/forums',
|
||||||
|
available: ['corporate', 'individual']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'settings',
|
||||||
|
label: 'Settings',
|
||||||
|
icon: Settings,
|
||||||
|
href: '/settings',
|
||||||
|
available: ['corporate', 'individual']
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleSearch = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === '/') {
|
||||||
|
e.preventDefault();
|
||||||
|
setSearchOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchClick = () => {
|
||||||
|
setSearchOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Global keyboard shortcut
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === '/' && !searchOpen) {
|
||||||
|
e.preventDefault();
|
||||||
|
setSearchOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [searchOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppContext.Provider value={contextValue}>
|
||||||
|
<div className="min-h-screen bg-white">
|
||||||
|
{/* Top Bar */}
|
||||||
|
<header className="h-16 border-b border-border bg-white flex items-center px-4 sticky top-0 z-50">
|
||||||
|
<div className="flex items-center gap-4 flex-1">
|
||||||
|
{/* Logo */}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (onLogoClick) {
|
||||||
|
onLogoClick();
|
||||||
|
} else {
|
||||||
|
onNavigate?.('my-courses');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-3 hover:opacity-80 transition-opacity cursor-pointer"
|
||||||
|
aria-label="Go to My Courses"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={image_1e150e43f238df3e08fcbf5d8f4899c233264e9f}
|
||||||
|
alt="Kautilya Leadership Centre (KLC)"
|
||||||
|
className="h-8 w-auto"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Universal Search */}
|
||||||
|
<div className="relative max-w-md flex-1">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full justify-start text-left font-normal bg-white"
|
||||||
|
onClick={handleSearchClick}
|
||||||
|
>
|
||||||
|
<Search className="mr-2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">Search... (Press '/' to focus)</span>
|
||||||
|
<kbd className="ml-auto pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
|
||||||
|
/
|
||||||
|
</kbd>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{/* Notifications */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="relative"
|
||||||
|
onClick={() => onNavigate?.('notifications')}
|
||||||
|
>
|
||||||
|
<Bell className="h-5 w-5" />
|
||||||
|
<Badge className="absolute -top-1 -right-1 h-5 w-5 flex items-center justify-center p-0 text-xs bg-[var(--color-brand-accent)] text-[var(--color-brand-black)]">
|
||||||
|
3
|
||||||
|
</Badge>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* User Menu */}
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" className="flex items-center gap-2 px-3 py-2 h-10 hover:bg-accent/50 transition-colors">
|
||||||
|
<Avatar className="h-8 w-8 ring-2 ring-transparent hover:ring-primary/20 transition-all">
|
||||||
|
<AvatarImage src={user.avatar} />
|
||||||
|
<AvatarFallback className="bg-gradient-to-br from-primary to-primary/80 text-white font-medium">
|
||||||
|
{user.firstName.charAt(0)}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="hidden md:flex flex-col items-start">
|
||||||
|
<span className="text-sm font-medium text-foreground">{user.firstName}</span>
|
||||||
|
<span className="text-xs text-muted-foreground capitalize">{user.persona}</span>
|
||||||
|
</div>
|
||||||
|
<ChevronDown className="h-4 w-4 text-muted-foreground transition-transform ui-open:rotate-180" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-72 p-0 shadow-lg border-border/50">
|
||||||
|
{/* User Info Header */}
|
||||||
|
<div className="px-4 py-3 bg-gradient-to-r from-primary/5 to-primary/10 border-b border-border/50">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Avatar className="h-10 w-10 ring-2 ring-primary/20">
|
||||||
|
<AvatarImage src={user.avatar} />
|
||||||
|
<AvatarFallback className="bg-gradient-to-br from-primary to-primary/80 text-white font-medium">
|
||||||
|
{user.firstName.charAt(0)}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-medium text-foreground">{user.firstName}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{user.persona === 'corporate' && user.orgName ? user.orgName : 'Individual Learner'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Switch Mode - Corporate Only */}
|
||||||
|
{user.canSwitchMode && (
|
||||||
|
<>
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full bg-primary"></div>
|
||||||
|
<p className="text-sm font-medium text-foreground">Switch Mode</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={currentMode === 'corporate' ? 'default' : 'outline'}
|
||||||
|
onClick={() => switchMode('corporate')}
|
||||||
|
className={`h-9 text-xs font-medium transition-all ${
|
||||||
|
currentMode === 'corporate'
|
||||||
|
? 'bg-primary text-primary-foreground shadow-sm'
|
||||||
|
: 'hover:bg-primary/10 hover:border-primary/30'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<GraduationCap className="h-3.5 w-3.5 mr-1.5" />
|
||||||
|
Learning
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={currentMode === 'hr' ? 'default' : 'outline'}
|
||||||
|
onClick={() => switchMode('hr')}
|
||||||
|
className={`h-9 text-xs font-medium transition-all ${
|
||||||
|
currentMode === 'hr'
|
||||||
|
? 'bg-primary text-primary-foreground shadow-sm'
|
||||||
|
: 'hover:bg-primary/10 hover:border-primary/30'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Building2 className="h-3.5 w-3.5 mr-1.5" />
|
||||||
|
HR Mode
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator className="bg-border/50" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Switch Accounts - If user has both */}
|
||||||
|
{user.canSwitchAccount && (
|
||||||
|
<>
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full bg-accent-foreground"></div>
|
||||||
|
<p className="text-sm font-medium text-foreground">Switch Accounts</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={currentAccount === 'corporate' ? 'default' : 'outline'}
|
||||||
|
onClick={() => switchAccount('corporate')}
|
||||||
|
className={`h-9 text-xs font-medium transition-all ${
|
||||||
|
currentAccount === 'corporate'
|
||||||
|
? 'bg-primary text-primary-foreground shadow-sm'
|
||||||
|
: 'hover:bg-primary/10 hover:border-primary/30'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Building2 className="h-3.5 w-3.5 mr-1.5" />
|
||||||
|
Corporate
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={currentAccount === 'personal' ? 'default' : 'outline'}
|
||||||
|
onClick={() => switchAccount('personal')}
|
||||||
|
className={`h-9 text-xs font-medium transition-all ${
|
||||||
|
currentAccount === 'personal'
|
||||||
|
? 'bg-primary text-primary-foreground shadow-sm'
|
||||||
|
: 'hover:bg-primary/10 hover:border-primary/30'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<UserCircle2 className="h-3.5 w-3.5 mr-1.5" />
|
||||||
|
Personal
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator className="bg-border/50" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Menu Items */}
|
||||||
|
<div className="p-2">
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="h-10 px-3 rounded-md hover:bg-accent/50 focus:bg-accent/50 transition-colors cursor-pointer"
|
||||||
|
onClick={() => onNavigate?.('settings')}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 w-full">
|
||||||
|
<div className="flex items-center justify-center h-8 w-8 rounded-md bg-primary/10">
|
||||||
|
<User className="h-4 w-4 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-sm font-medium text-foreground">Profile & Settings</div>
|
||||||
|
<div className="text-xs text-muted-foreground">Manage your account</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="h-10 px-3 rounded-md hover:bg-destructive/10 focus:bg-destructive/10 transition-colors cursor-pointer mt-1"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 w-full">
|
||||||
|
<div className="flex items-center justify-center h-8 w-8 rounded-md bg-destructive/10">
|
||||||
|
<LogOut className="h-4 w-4 text-destructive" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-sm font-medium text-destructive">Sign out</div>
|
||||||
|
<div className="text-xs text-muted-foreground">End your session</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="flex">
|
||||||
|
{/* Sidebar - Hidden during course mode */}
|
||||||
|
{!isInCourseMode && (
|
||||||
|
<aside
|
||||||
|
className={`${
|
||||||
|
sidebarCollapsed ? 'w-16' : 'w-64'
|
||||||
|
} transition-all duration-200 border-r border-border bg-sidebar h-[calc(100vh-4rem)] sticky top-16 overflow-hidden`}
|
||||||
|
>
|
||||||
|
{/* Sidebar Toggle */}
|
||||||
|
<div className="flex items-center justify-between p-4 border-b border-sidebar-border">
|
||||||
|
{!sidebarCollapsed && (
|
||||||
|
<span className="font-medium text-sidebar-foreground">Navigation</span>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
|
||||||
|
className="h-8 w-8"
|
||||||
|
>
|
||||||
|
{sidebarCollapsed ? (
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation Items */}
|
||||||
|
<nav className="p-2">
|
||||||
|
{navItems.map((item) => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
const isActive = currentPage === item.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={item.id} className="mb-1">
|
||||||
|
<Button
|
||||||
|
variant={isActive ? "secondary" : "ghost"}
|
||||||
|
className={`w-full justify-start h-10 px-3 ${
|
||||||
|
sidebarCollapsed ? 'px-2' : 'px-3'
|
||||||
|
}`}
|
||||||
|
title={sidebarCollapsed ? item.label : undefined}
|
||||||
|
onClick={() => onNavigate?.(item.id)}
|
||||||
|
>
|
||||||
|
<Icon className={`h-4 w-4 ${sidebarCollapsed ? '' : 'mr-3'}`} />
|
||||||
|
{!sidebarCollapsed && (
|
||||||
|
<span className="flex-1 text-left">{item.label}</span>
|
||||||
|
)}
|
||||||
|
{!sidebarCollapsed && item.badge && (
|
||||||
|
<Badge variant="secondary" className="ml-auto text-xs">
|
||||||
|
{item.badge}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main className="flex-1 overflow-x-hidden">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Global Search Modal */}
|
||||||
|
<GlobalSearch
|
||||||
|
isOpen={searchOpen}
|
||||||
|
onClose={() => setSearchOpen(false)}
|
||||||
|
onOpenAIMentor={() => {
|
||||||
|
setSearchOpen(false);
|
||||||
|
// This would trigger AI Mentor opening
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</AppContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
|
||||||
|
|
||||||
interface User {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
role: string;
|
|
||||||
corporateName: string;
|
|
||||||
avatar?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AuthContextType {
|
|
||||||
user: User | null;
|
|
||||||
isAuthenticated: boolean;
|
|
||||||
signIn: (userData: User) => void;
|
|
||||||
signOut: () => void;
|
|
||||||
login: (email: string, password: string) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
||||||
|
|
||||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
||||||
const [user, setUser] = useState<User | null>(null);
|
|
||||||
|
|
||||||
// Check for existing session on app load
|
|
||||||
useEffect(() => {
|
|
||||||
const savedUser = localStorage.getItem('klc_user');
|
|
||||||
if (savedUser) {
|
|
||||||
try {
|
|
||||||
setUser(JSON.parse(savedUser));
|
|
||||||
} catch (error) {
|
|
||||||
localStorage.removeItem('klc_user');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const signIn = (userData: User) => {
|
|
||||||
setUser(userData);
|
|
||||||
localStorage.setItem('klc_user', JSON.stringify(userData));
|
|
||||||
};
|
|
||||||
|
|
||||||
const signOut = () => {
|
|
||||||
setUser(null);
|
|
||||||
localStorage.removeItem('klc_user');
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mock login function that simulates authentication
|
|
||||||
const login = async (email: string, password: string): Promise<void> => {
|
|
||||||
// Simulate API call delay
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
|
||||||
|
|
||||||
// Create mock user data based on email to determine user type
|
|
||||||
const isCorporateUser = email.includes('corporate') || email.includes('@company') || email.includes('@corp');
|
|
||||||
|
|
||||||
const mockUser: User = {
|
|
||||||
id: Date.now().toString(),
|
|
||||||
name: isCorporateUser
|
|
||||||
? 'Priya Sharma'
|
|
||||||
: 'Priya Sharma',
|
|
||||||
email: email || 'demo@klc.edu',
|
|
||||||
role: isCorporateUser ? 'corporate' : 'individual',
|
|
||||||
corporateName: isCorporateUser ? 'Demo Corporation' : '',
|
|
||||||
avatar: undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set user data
|
|
||||||
setUser(mockUser);
|
|
||||||
localStorage.setItem('klc_user', JSON.stringify(mockUser));
|
|
||||||
};
|
|
||||||
|
|
||||||
const value = {
|
|
||||||
user,
|
|
||||||
isAuthenticated: !!user,
|
|
||||||
signIn,
|
|
||||||
signOut,
|
|
||||||
login
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AuthContext.Provider value={value}>
|
|
||||||
{children}
|
|
||||||
</AuthContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useAuth() {
|
|
||||||
const context = useContext(AuthContext);
|
|
||||||
if (context === undefined) {
|
|
||||||
throw new Error('useAuth must be used within an AuthProvider');
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
468
src/components/Blog.tsx
Normal file
@@ -0,0 +1,468 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Plus,
|
||||||
|
Edit,
|
||||||
|
Eye,
|
||||||
|
Trash2,
|
||||||
|
Clock,
|
||||||
|
CheckCircle,
|
||||||
|
XCircle,
|
||||||
|
AlertCircle,
|
||||||
|
Upload,
|
||||||
|
Image,
|
||||||
|
Video
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
import { Input } from './ui/input';
|
||||||
|
import { Textarea } from './ui/textarea';
|
||||||
|
import { Badge } from './ui/badge';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger
|
||||||
|
} from './ui/dialog';
|
||||||
|
import { Label } from './ui/label';
|
||||||
|
import { useAppContext } from './AppShell';
|
||||||
|
|
||||||
|
// Mock blog submissions data
|
||||||
|
const mockBlogData = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
title: 'The Future of Remote Leadership: Lessons from the Pandemic',
|
||||||
|
excerpt: 'Exploring how the COVID-19 pandemic transformed leadership approaches and what lessons we can carry forward...',
|
||||||
|
content: 'Full article content would be here...',
|
||||||
|
status: 'published',
|
||||||
|
submittedAt: '2024-08-15T10:00:00Z',
|
||||||
|
publishedAt: '2024-08-18T14:00:00Z',
|
||||||
|
views: 1247,
|
||||||
|
tags: ['Remote Leadership', 'Digital Transformation', 'Crisis Management']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
title: 'Building Resilient Teams in Uncertain Times',
|
||||||
|
excerpt: 'Strategies for developing team resilience and maintaining high performance during periods of uncertainty...',
|
||||||
|
content: 'Full article content would be here...',
|
||||||
|
status: 'under-review',
|
||||||
|
submittedAt: '2024-09-01T15:30:00Z',
|
||||||
|
tags: ['Team Building', 'Resilience', 'Leadership']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
title: 'Emotional Intelligence in Modern Leadership',
|
||||||
|
excerpt: 'Why EQ matters more than ever in today\'s complex business environment and how to develop it...',
|
||||||
|
content: 'Full article content would be here...',
|
||||||
|
status: 'rejected',
|
||||||
|
submittedAt: '2024-08-20T09:15:00Z',
|
||||||
|
rejectedAt: '2024-08-22T11:00:00Z',
|
||||||
|
rejectionReason: 'Content needs more original research and citations. Please add specific examples and data to support your claims.',
|
||||||
|
tags: ['Emotional Intelligence', 'Leadership Development']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
title: 'Sustainable Leadership Practices',
|
||||||
|
excerpt: 'Draft exploring how leaders can integrate sustainability into their decision-making processes...',
|
||||||
|
content: 'Full article content would be here...',
|
||||||
|
status: 'draft',
|
||||||
|
updatedAt: '2024-09-02T16:45:00Z',
|
||||||
|
tags: ['Sustainability', 'Strategic Leadership']
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export function Blog() {
|
||||||
|
const { user } = useAppContext();
|
||||||
|
const [activeTab, setActiveTab] = useState('my-posts');
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
const [editingPost, setEditingPost] = useState<any>(null);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
title: '',
|
||||||
|
excerpt: '',
|
||||||
|
content: '',
|
||||||
|
tags: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const getStatusIcon = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'published': return CheckCircle;
|
||||||
|
case 'under-review': return Clock;
|
||||||
|
case 'rejected': return XCircle;
|
||||||
|
case 'draft': return Edit;
|
||||||
|
default: return AlertCircle;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'published': return 'bg-green-100 text-green-800';
|
||||||
|
case 'under-review': return 'bg-blue-100 text-blue-800';
|
||||||
|
case 'rejected': return 'bg-red-100 text-red-800';
|
||||||
|
case 'draft': return 'bg-gray-100 text-gray-800';
|
||||||
|
default: return 'bg-gray-100 text-gray-800';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateStr: string) => {
|
||||||
|
return new Date(dateStr).toLocaleDateString('en-IN', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
timeZone: 'Asia/Kolkata'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
// TODO: Submit blog post
|
||||||
|
console.log('Submitting:', formData);
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
setFormData({ title: '', excerpt: '', content: '', tags: '' });
|
||||||
|
setEditingPost(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const BlogPostCard = ({ post }: { post: any }) => {
|
||||||
|
const StatusIcon = getStatusIcon(post.status);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4 space-y-3">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1 space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<StatusIcon className="h-4 w-4" />
|
||||||
|
<Badge className={`capitalize ${getStatusColor(post.status)}`}>
|
||||||
|
{post.status.replace('-', ' ')}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="font-medium">{post.title}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground line-clamp-2">
|
||||||
|
{post.excerpt}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{post.tags.map((tag: string) => (
|
||||||
|
<Badge key={tag} variant="outline" className="text-xs">
|
||||||
|
{tag}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||||
|
<span>
|
||||||
|
Submitted {formatDate(post.submittedAt)}
|
||||||
|
</span>
|
||||||
|
{post.publishedAt && (
|
||||||
|
<span>
|
||||||
|
Published {formatDate(post.publishedAt)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{post.views && (
|
||||||
|
<span>
|
||||||
|
{post.views} views
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{post.rejectionReason && (
|
||||||
|
<div className="p-3 bg-red-50 border border-red-200 rounded-lg">
|
||||||
|
<p className="text-sm font-medium text-red-800 mb-1">Rejection Reason:</p>
|
||||||
|
<p className="text-sm text-red-700">{post.rejectionReason}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 ml-4">
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingPost(post);
|
||||||
|
setFormData({
|
||||||
|
title: post.title,
|
||||||
|
excerpt: post.excerpt,
|
||||||
|
content: post.content,
|
||||||
|
tags: post.tags.join(', ')
|
||||||
|
});
|
||||||
|
setIsDialogOpen(true);
|
||||||
|
}}
|
||||||
|
className="h-8 w-8"
|
||||||
|
>
|
||||||
|
<Edit className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{post.status === 'published' && (
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8"
|
||||||
|
>
|
||||||
|
<Eye className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8 text-destructive hover:text-destructive"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-medium">Leadership Blog</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Share your leadership insights and experiences
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button onClick={() => {
|
||||||
|
setEditingPost(null);
|
||||||
|
setFormData({ title: '', excerpt: '', content: '', tags: '' });
|
||||||
|
}}>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
Write Article
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{editingPost ? 'Edit Article' : 'Write New Article'}
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="title">Title</Label>
|
||||||
|
<Input
|
||||||
|
id="title"
|
||||||
|
placeholder="Enter article title..."
|
||||||
|
value={formData.title}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="excerpt">Excerpt</Label>
|
||||||
|
<Textarea
|
||||||
|
id="excerpt"
|
||||||
|
placeholder="Brief summary of your article..."
|
||||||
|
className="h-20"
|
||||||
|
value={formData.excerpt}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, excerpt: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="content">Content</Label>
|
||||||
|
<div className="border rounded-lg">
|
||||||
|
<div className="flex items-center gap-2 p-2 border-b bg-muted">
|
||||||
|
<Button size="sm" variant="ghost">
|
||||||
|
<Image className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="ghost">
|
||||||
|
<Video className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="ghost">
|
||||||
|
<Upload className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<span className="text-xs text-muted-foreground ml-auto">
|
||||||
|
Markdown supported
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Textarea
|
||||||
|
id="content"
|
||||||
|
placeholder="Write your article content here. You can use markdown formatting and embed images/videos..."
|
||||||
|
className="min-h-[300px] border-0 resize-none focus-visible:ring-0"
|
||||||
|
value={formData.content}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, content: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="tags">Tags</Label>
|
||||||
|
<Input
|
||||||
|
id="tags"
|
||||||
|
placeholder="Leadership, Strategy, Team Building (comma separated)"
|
||||||
|
value={formData.tags}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, tags: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||||
|
<h4 className="font-medium text-blue-800 mb-2">Submission Guidelines</h4>
|
||||||
|
<ul className="text-sm text-blue-700 space-y-1">
|
||||||
|
<li>• Articles are reviewed by our editorial team before publication</li>
|
||||||
|
<li>• Focus on original insights and practical leadership advice</li>
|
||||||
|
<li>• Include specific examples and actionable takeaways</li>
|
||||||
|
<li>• Maintain professional tone and cite sources when needed</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline">
|
||||||
|
Save Draft
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSubmit}>
|
||||||
|
{editingPost ? 'Update Article' : 'Submit for Review'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Cards */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-green-500 flex items-center justify-center text-white">
|
||||||
|
<CheckCircle className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl font-bold">{mockBlogData.filter(p => p.status === 'published').length}</p>
|
||||||
|
<p className="text-sm text-muted-foreground">Published</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-blue-500 flex items-center justify-center text-white">
|
||||||
|
<Clock className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl font-bold">{mockBlogData.filter(p => p.status === 'under-review').length}</p>
|
||||||
|
<p className="text-sm text-muted-foreground">Under Review</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-gray-500 flex items-center justify-center text-white">
|
||||||
|
<Edit className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl font-bold">{mockBlogData.filter(p => p.status === 'draft').length}</p>
|
||||||
|
<p className="text-sm text-muted-foreground">Drafts</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-[#F8C301] flex items-center justify-center text-[#04045B]">
|
||||||
|
<Eye className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl font-bold">
|
||||||
|
{mockBlogData.reduce((sum, post) => sum + (post.views || 0), 0)}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">Total Views</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="my-posts">My Submissions</TabsTrigger>
|
||||||
|
<TabsTrigger value="guidelines">Guidelines</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="my-posts" className="space-y-4">
|
||||||
|
{mockBlogData.length === 0 ? (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-8 text-center">
|
||||||
|
<Edit className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
|
||||||
|
<h3 className="font-medium mb-2">No articles yet</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
Start sharing your leadership insights with the community
|
||||||
|
</p>
|
||||||
|
<Button>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
Write Your First Article
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{mockBlogData.map((post) => (
|
||||||
|
<BlogPostCard key={post.id} post={post} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="guidelines">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Article Submission Guidelines</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium mb-2">Content Requirements</h3>
|
||||||
|
<ul className="text-sm space-y-1 text-muted-foreground">
|
||||||
|
<li>• Minimum 800 words, maximum 3000 words</li>
|
||||||
|
<li>• Original content with practical leadership insights</li>
|
||||||
|
<li>• Include specific examples and actionable advice</li>
|
||||||
|
<li>• Cite sources and provide references where applicable</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium mb-2">Review Process</h3>
|
||||||
|
<ul className="text-sm space-y-1 text-muted-foreground">
|
||||||
|
<li>• Articles are reviewed within 5-7 business days</li>
|
||||||
|
<li>• Editorial team may suggest revisions</li>
|
||||||
|
<li>• Approved articles are published on the KLC website</li>
|
||||||
|
<li>• Authors are notified of publication via email</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium mb-2">Topics We're Looking For</h3>
|
||||||
|
<ul className="text-sm space-y-1 text-muted-foreground">
|
||||||
|
<li>• Strategic leadership and vision</li>
|
||||||
|
<li>• Team building and management</li>
|
||||||
|
<li>• Change management and innovation</li>
|
||||||
|
<li>• Digital transformation leadership</li>
|
||||||
|
<li>• Emotional intelligence and soft skills</li>
|
||||||
|
<li>• Industry-specific leadership challenges</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
// CTABanner component has been removed
|
|
||||||
// This file is left empty to prevent import errors
|
|
||||||
796
src/components/CalendarWidget.tsx
Normal file
@@ -0,0 +1,796 @@
|
|||||||
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
import { Badge } from './ui/badge';
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover';
|
||||||
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuCheckboxItem } from './ui/dropdown-menu';
|
||||||
|
import { Checkbox } from './ui/checkbox';
|
||||||
|
import {
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
Calendar as CalendarIcon,
|
||||||
|
MapPin,
|
||||||
|
Clock,
|
||||||
|
ExternalLink,
|
||||||
|
Download,
|
||||||
|
Bell,
|
||||||
|
Video,
|
||||||
|
Users,
|
||||||
|
AlertCircle,
|
||||||
|
Filter,
|
||||||
|
Palette
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { motion, AnimatePresence } from 'motion/react';
|
||||||
|
|
||||||
|
// Types based on the data contract
|
||||||
|
interface Programme {
|
||||||
|
programmeId: string;
|
||||||
|
programmeName: string;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CalendarEvent {
|
||||||
|
id: string;
|
||||||
|
programmeId: string;
|
||||||
|
type: 'webinar' | 'class' | 'course_due' | 'content_due' | 'profiler_due' | 'programme_end';
|
||||||
|
title: string;
|
||||||
|
start: string; // ISO date string with timezone
|
||||||
|
end?: string;
|
||||||
|
location?: string; // for classes
|
||||||
|
facilitator?: string; // for webinars
|
||||||
|
joinUrl?: string; // internal route for webinars
|
||||||
|
href?: string; // fallback route
|
||||||
|
joinWindowMinutes?: number; // enables Join button when within window
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CalendarFilters {
|
||||||
|
programmeIds: string[];
|
||||||
|
types: ('webinar' | 'class' | 'due')[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface for upcoming events from MyCourses
|
||||||
|
interface UpcomingEvent {
|
||||||
|
id: string;
|
||||||
|
topic: string;
|
||||||
|
startTime: string; // ISO format
|
||||||
|
duration: number; // minutes
|
||||||
|
host: string; // For webinars: facilitator name, For offline-class: venue/location
|
||||||
|
type: 'webinar' | 'offline-class';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CalendarWidgetProps {
|
||||||
|
programmes?: Programme[];
|
||||||
|
events?: CalendarEvent[];
|
||||||
|
filters?: CalendarFilters;
|
||||||
|
timezone?: string;
|
||||||
|
upcomingEvents?: UpcomingEvent[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock data based on the specification - integrated with learner dashboard
|
||||||
|
const mockProgrammes: Programme[] = [
|
||||||
|
{ programmeId: 'prog1', programmeName: 'Leadership Development', color: '#5B8DEF' },
|
||||||
|
{ programmeId: 'prog2', programmeName: 'Digital Skills', color: '#EF5B9C' },
|
||||||
|
{ programmeId: 'prg_strategy', programmeName: 'Strategic Thinking', color: '#5BEF8A' },
|
||||||
|
{ programmeId: 'prg_innovation', programmeName: 'Innovation Lab', color: '#EFAB5B' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockEvents: CalendarEvent[] = [
|
||||||
|
{
|
||||||
|
id: 'evt_001',
|
||||||
|
programmeId: 'prog1',
|
||||||
|
type: 'webinar',
|
||||||
|
title: 'Future of Work Webinar',
|
||||||
|
start: '2025-09-29T14:00:00+05:30',
|
||||||
|
end: '2025-09-29T15:30:00+05:30',
|
||||||
|
joinUrl: '/learner/webinar/zoom_001',
|
||||||
|
joinWindowMinutes: 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'evt_002',
|
||||||
|
programmeId: 'prog1',
|
||||||
|
type: 'class',
|
||||||
|
title: 'Leadership Workshop - Mumbai',
|
||||||
|
start: '2025-10-01T09:00:00+05:30',
|
||||||
|
end: '2025-10-01T15:00:00+05:30',
|
||||||
|
location: 'Mumbai Office / Conference Room A',
|
||||||
|
href: '/learner/class/leadership_mumbai'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'evt_003',
|
||||||
|
programmeId: 'prog1',
|
||||||
|
type: 'course_due',
|
||||||
|
title: 'Leadership Fundamentals Module Due',
|
||||||
|
start: '2025-09-28T23:59:00+05:30',
|
||||||
|
href: '/learner/course/lead_fund_mod'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'evt_004',
|
||||||
|
programmeId: 'prog2',
|
||||||
|
type: 'webinar',
|
||||||
|
title: 'AI in Business Webinar',
|
||||||
|
start: '2025-10-03T10:30:00+05:30',
|
||||||
|
end: '2025-10-03T12:00:00+05:30',
|
||||||
|
joinUrl: '/learner/webinar/ai_business',
|
||||||
|
joinWindowMinutes: 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'evt_005',
|
||||||
|
programmeId: 'prg_innovation',
|
||||||
|
type: 'profiler_due',
|
||||||
|
title: 'Innovation Assessment Due',
|
||||||
|
start: '2025-09-27T23:59:00+05:30',
|
||||||
|
href: '/learner/profiler/innovation_assess'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'evt_006',
|
||||||
|
programmeId: 'prog2',
|
||||||
|
type: 'class',
|
||||||
|
title: 'Crisis Management Workshop - Delhi',
|
||||||
|
start: '2025-10-04T10:00:00+05:30',
|
||||||
|
end: '2025-10-04T18:00:00+05:30',
|
||||||
|
location: 'Delhi Office / Training Hall B',
|
||||||
|
href: '/learner/class/crisis_management_delhi'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'evt_007',
|
||||||
|
programmeId: 'prog2',
|
||||||
|
type: 'course_due',
|
||||||
|
title: 'Digital Marketing Assignment Due',
|
||||||
|
start: '2025-09-29T23:59:00+05:30',
|
||||||
|
href: '/learner/course/digital_marketing_assignment'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export function CalendarWidget({
|
||||||
|
programmes = mockProgrammes,
|
||||||
|
events = mockEvents,
|
||||||
|
filters = { programmeIds: [], types: ['webinar', 'class', 'due'] },
|
||||||
|
timezone = 'IST',
|
||||||
|
upcomingEvents = []
|
||||||
|
}: Partial<CalendarWidgetProps>) {
|
||||||
|
// Convert upcoming events to calendar events format
|
||||||
|
const convertedUpcomingEvents: CalendarEvent[] = upcomingEvents.map(event => {
|
||||||
|
const startDate = new Date(event.startTime);
|
||||||
|
const endDate = new Date(startDate.getTime() + event.duration * 60000); // Add duration in milliseconds
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `upcoming_${event.id}`,
|
||||||
|
programmeId: 'prog1', // Default to first programme for now
|
||||||
|
type: event.type === 'webinar' ? 'webinar' : 'class',
|
||||||
|
title: event.topic,
|
||||||
|
start: event.startTime,
|
||||||
|
end: endDate.toISOString(),
|
||||||
|
// For offline classes, set location from host (venue information)
|
||||||
|
// For webinars, set facilitator from host (instructor name) and joinUrl
|
||||||
|
...(event.type === 'offline-class' && { location: event.host }),
|
||||||
|
...(event.type === 'webinar' && {
|
||||||
|
facilitator: event.host,
|
||||||
|
joinUrl: '/learner/webinar/upcoming',
|
||||||
|
joinWindowMinutes: 15 // Allow joining 15 minutes before
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Merge all events
|
||||||
|
const allEvents = [...events, ...convertedUpcomingEvents];
|
||||||
|
|
||||||
|
const [currentDate, setCurrentDate] = useState(new Date());
|
||||||
|
const [selectedDate, setSelectedDate] = useState(new Date());
|
||||||
|
const [activeFilters, setActiveFilters] = useState<CalendarFilters>(filters);
|
||||||
|
const [focusedDateIndex, setFocusedDateIndex] = useState(0);
|
||||||
|
const [showLegend, setShowLegend] = useState(false);
|
||||||
|
const [selectedEvent, setSelectedEvent] = useState<CalendarEvent | null>(null);
|
||||||
|
|
||||||
|
const miniCalendarRef = useRef<HTMLDivElement>(null);
|
||||||
|
const todayRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
// Get the first day of the month and calculate grid
|
||||||
|
const firstDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1);
|
||||||
|
const lastDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0);
|
||||||
|
const startDate = new Date(firstDayOfMonth);
|
||||||
|
startDate.setDate(startDate.getDate() - firstDayOfMonth.getDay());
|
||||||
|
|
||||||
|
// Generate calendar grid (6 weeks)
|
||||||
|
const calendarDays = Array.from({ length: 42 }, (_, i) => {
|
||||||
|
const date = new Date(startDate);
|
||||||
|
date.setDate(startDate.getDate() + i);
|
||||||
|
return date;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter events based on active filters
|
||||||
|
const filteredEvents = allEvents.filter(event => {
|
||||||
|
const programmeMatch = activeFilters.programmeIds.length === 0 ||
|
||||||
|
activeFilters.programmeIds.includes(event.programmeId);
|
||||||
|
|
||||||
|
let typeMatch = false;
|
||||||
|
if (activeFilters.types.includes('webinar') && event.type === 'webinar') typeMatch = true;
|
||||||
|
if (activeFilters.types.includes('class') && event.type === 'class') typeMatch = true;
|
||||||
|
if (activeFilters.types.includes('due') &&
|
||||||
|
['course_due', 'content_due', 'profiler_due', 'programme_end'].includes(event.type)) {
|
||||||
|
typeMatch = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return programmeMatch && typeMatch;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get events for a specific date
|
||||||
|
const getEventsForDate = (date: Date) => {
|
||||||
|
return filteredEvents.filter(event => {
|
||||||
|
const eventDate = new Date(event.start);
|
||||||
|
return eventDate.toDateString() === date.toDateString();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get agenda events (selected date + upcoming)
|
||||||
|
const getAgendaEvents = () => {
|
||||||
|
const selectedEvents = getEventsForDate(selectedDate);
|
||||||
|
const futureEvents = filteredEvents
|
||||||
|
.filter(event => {
|
||||||
|
const eventDate = new Date(event.start);
|
||||||
|
return eventDate > selectedDate;
|
||||||
|
})
|
||||||
|
.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime())
|
||||||
|
.slice(0, 5);
|
||||||
|
|
||||||
|
return [...selectedEvents, ...futureEvents];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Event type icons and labels
|
||||||
|
const getEventTypeInfo = (type: CalendarEvent['type']) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'webinar':
|
||||||
|
return { icon: '🎥', label: 'Webinar' };
|
||||||
|
case 'class':
|
||||||
|
return { icon: '🏫', label: 'Class' };
|
||||||
|
case 'course_due':
|
||||||
|
case 'content_due':
|
||||||
|
case 'profiler_due':
|
||||||
|
case 'programme_end':
|
||||||
|
return { icon: '⏳', label: 'Due' };
|
||||||
|
default:
|
||||||
|
return { icon: '📅', label: 'Event' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if event can be joined (for webinars)
|
||||||
|
const canJoinEvent = (event: CalendarEvent) => {
|
||||||
|
if (event.type !== 'webinar' || !event.joinWindowMinutes || !event.end) return false;
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const eventStart = new Date(event.start);
|
||||||
|
const eventEnd = new Date(event.end);
|
||||||
|
const joinWindowStart = new Date(eventStart.getTime() - (event.joinWindowMinutes * 60 * 1000));
|
||||||
|
|
||||||
|
return now >= joinWindowStart && now <= eventEnd;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format time for display
|
||||||
|
const formatTime = (dateString: string, showDate = false) => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
const timeFormat = date.toLocaleTimeString('en-IN', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (showDate) {
|
||||||
|
const dateFormat = date.toLocaleDateString('en-GB', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'short'
|
||||||
|
});
|
||||||
|
return `${dateFormat}, ${timeFormat}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeFormat;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Keyboard navigation for mini calendar
|
||||||
|
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||||
|
const totalDays = calendarDays.length;
|
||||||
|
let newIndex = focusedDateIndex;
|
||||||
|
|
||||||
|
switch (e.key) {
|
||||||
|
case 'ArrowRight':
|
||||||
|
e.preventDefault();
|
||||||
|
newIndex = (focusedDateIndex + 1) % totalDays;
|
||||||
|
break;
|
||||||
|
case 'ArrowLeft':
|
||||||
|
e.preventDefault();
|
||||||
|
newIndex = focusedDateIndex === 0 ? totalDays - 1 : focusedDateIndex - 1;
|
||||||
|
break;
|
||||||
|
case 'ArrowDown':
|
||||||
|
e.preventDefault();
|
||||||
|
newIndex = (focusedDateIndex + 7) % totalDays;
|
||||||
|
break;
|
||||||
|
case 'ArrowUp':
|
||||||
|
e.preventDefault();
|
||||||
|
newIndex = focusedDateIndex - 7 < 0 ? focusedDateIndex + 35 : focusedDateIndex - 7;
|
||||||
|
break;
|
||||||
|
case 'Home':
|
||||||
|
e.preventDefault();
|
||||||
|
newIndex = Math.floor(focusedDateIndex / 7) * 7;
|
||||||
|
break;
|
||||||
|
case 'End':
|
||||||
|
e.preventDefault();
|
||||||
|
newIndex = Math.floor(focusedDateIndex / 7) * 7 + 6;
|
||||||
|
break;
|
||||||
|
case 'PageUp':
|
||||||
|
e.preventDefault();
|
||||||
|
navigateMonth(-1);
|
||||||
|
return;
|
||||||
|
case 'PageDown':
|
||||||
|
e.preventDefault();
|
||||||
|
navigateMonth(1);
|
||||||
|
return;
|
||||||
|
case 'Enter':
|
||||||
|
case ' ':
|
||||||
|
e.preventDefault();
|
||||||
|
setSelectedDate(calendarDays[focusedDateIndex]);
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFocusedDateIndex(newIndex);
|
||||||
|
}, [focusedDateIndex, calendarDays]);
|
||||||
|
|
||||||
|
// Navigate months
|
||||||
|
const navigateMonth = (direction: number) => {
|
||||||
|
const newDate = new Date(currentDate);
|
||||||
|
newDate.setMonth(currentDate.getMonth() + direction);
|
||||||
|
setCurrentDate(newDate);
|
||||||
|
|
||||||
|
// Announce change for screen readers
|
||||||
|
const monthName = newDate.toLocaleDateString('en-GB', { month: 'long', year: 'numeric' });
|
||||||
|
const eventsCount = getEventsForDate(newDate).length;
|
||||||
|
const announcement = `${monthName}, ${eventsCount} events`;
|
||||||
|
|
||||||
|
// Create temporary element for screen reader announcement
|
||||||
|
const announcement_el = document.createElement('div');
|
||||||
|
announcement_el.setAttribute('aria-live', 'polite');
|
||||||
|
announcement_el.setAttribute('aria-atomic', 'true');
|
||||||
|
announcement_el.className = 'sr-only';
|
||||||
|
announcement_el.textContent = announcement;
|
||||||
|
document.body.appendChild(announcement_el);
|
||||||
|
setTimeout(() => document.body.removeChild(announcement_el), 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export to ICS
|
||||||
|
const exportToICS = (event: CalendarEvent) => {
|
||||||
|
const programme = programmes.find(p => p.programmeId === event.programmeId);
|
||||||
|
const typeInfo = getEventTypeInfo(event.type);
|
||||||
|
|
||||||
|
const startDate = new Date(event.start);
|
||||||
|
const endDate = event.end ? new Date(event.end) : new Date(startDate.getTime() + 60 * 60 * 1000);
|
||||||
|
|
||||||
|
const formatICSDate = (date: Date) => {
|
||||||
|
return date.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';
|
||||||
|
};
|
||||||
|
|
||||||
|
const icsContent = [
|
||||||
|
'BEGIN:VCALENDAR',
|
||||||
|
'VERSION:2.0',
|
||||||
|
'PRODID:-//KLC//Calendar Widget//EN',
|
||||||
|
'BEGIN:VEVENT',
|
||||||
|
`UID:${event.id}@klc.com`,
|
||||||
|
`DTSTART:${formatICSDate(startDate)}`,
|
||||||
|
`DTEND:${formatICSDate(endDate)}`,
|
||||||
|
`SUMMARY:${event.title}`,
|
||||||
|
`DESCRIPTION:${programme?.programmeName} - ${typeInfo.label}`,
|
||||||
|
event.location ? `LOCATION:${event.location}` : '',
|
||||||
|
'BEGIN:VALARM',
|
||||||
|
'TRIGGER:-PT30M',
|
||||||
|
'ACTION:DISPLAY',
|
||||||
|
'DESCRIPTION:Reminder',
|
||||||
|
'END:VALARM',
|
||||||
|
'END:VEVENT',
|
||||||
|
'END:VCALENDAR'
|
||||||
|
].filter(line => line !== '').join('\r\n');
|
||||||
|
|
||||||
|
const blob = new Blob([icsContent], { type: 'text/calendar' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = `${event.title.replace(/[^a-z0-9]/gi, '_')}.ics`;
|
||||||
|
link.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get today button
|
||||||
|
const goToToday = () => {
|
||||||
|
const today = new Date();
|
||||||
|
setCurrentDate(today);
|
||||||
|
setSelectedDate(today);
|
||||||
|
setFocusedDateIndex(calendarDays.findIndex(date =>
|
||||||
|
date.toDateString() === today.toDateString()
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set reminder function
|
||||||
|
const setReminder = (event: CalendarEvent) => {
|
||||||
|
// In a real implementation, this would integrate with the notification system
|
||||||
|
const reminderTime = event.type === 'webinar' || event.type === 'class' ? '30 minutes' : '24 hours';
|
||||||
|
|
||||||
|
// Create a temporary notification element for confirmation
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = 'fixed top-4 right-4 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg z-50 transition-opacity';
|
||||||
|
notification.innerHTML = `
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span>✓</span>
|
||||||
|
<div>
|
||||||
|
<div class="font-medium">Reminder set</div>
|
||||||
|
<div class="text-sm opacity-90">You'll be notified ${reminderTime} before ${event.title}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
// Remove after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.opacity = '0';
|
||||||
|
setTimeout(() => {
|
||||||
|
if (notification.parentNode) {
|
||||||
|
document.body.removeChild(notification);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Toggle filter type
|
||||||
|
const toggleFilterType = (type: 'webinar' | 'class' | 'due') => {
|
||||||
|
setActiveFilters(prev => ({
|
||||||
|
...prev,
|
||||||
|
types: prev.types.includes(type)
|
||||||
|
? prev.types.filter(t => t !== type)
|
||||||
|
: [...prev.types, type]
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const monthName = currentDate.toLocaleDateString('en-GB', { month: 'long', year: 'numeric' });
|
||||||
|
const agendaEvents = getAgendaEvents();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full bg-white border">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-[var(--color-brand-primary)] font-medium">Calendar</CardTitle>
|
||||||
|
|
||||||
|
{/* Month navigation */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-7 w-7"
|
||||||
|
onClick={() => navigateMonth(-1)}
|
||||||
|
aria-label="Previous month"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
<span className="text-sm font-medium min-w-[100px] text-center">{monthName}</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-7 w-7"
|
||||||
|
onClick={() => navigateMonth(1)}
|
||||||
|
aria-label="Next month"
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={goToToday}
|
||||||
|
ref={todayRef}
|
||||||
|
className="text-xs h-7 px-2"
|
||||||
|
>
|
||||||
|
Today
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="space-y-3 p-4">
|
||||||
|
{/* Mini Calendar Grid */}
|
||||||
|
<div
|
||||||
|
ref={miniCalendarRef}
|
||||||
|
role="grid"
|
||||||
|
aria-label={`Calendar for ${monthName}`}
|
||||||
|
className="grid grid-cols-7 gap-1"
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
{/* Day headers */}
|
||||||
|
{['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day, index) => (
|
||||||
|
<div key={index} className="h-6 flex items-center justify-center text-xs text-muted-foreground font-medium">
|
||||||
|
{day}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Calendar days */}
|
||||||
|
{calendarDays.map((date, index) => {
|
||||||
|
const dayEvents = getEventsForDate(date);
|
||||||
|
const isCurrentMonth = date.getMonth() === currentDate.getMonth();
|
||||||
|
const isToday = date.toDateString() === new Date().toDateString();
|
||||||
|
const isSelected = date.toDateString() === selectedDate.toDateString();
|
||||||
|
const isFocused = index === focusedDateIndex;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
role="gridcell"
|
||||||
|
aria-selected={isSelected}
|
||||||
|
aria-current={isToday ? 'date' : undefined}
|
||||||
|
tabIndex={isFocused ? 0 : -1}
|
||||||
|
className={`
|
||||||
|
h-7 w-7 text-xs relative focus:outline-none focus:ring-2 focus:ring-[var(--color-brand-primary)] rounded flex items-center justify-center
|
||||||
|
${isCurrentMonth ? 'text-foreground' : 'text-muted-foreground'}
|
||||||
|
${isToday ? 'bg-[var(--color-brand-primary)] text-white font-medium' : ''}
|
||||||
|
${isSelected && !isToday ? 'bg-gray-100' : ''}
|
||||||
|
${isFocused ? 'ring-2 ring-[var(--color-brand-primary)]' : ''}
|
||||||
|
hover:bg-gray-50 transition-colors
|
||||||
|
`}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedDate(date);
|
||||||
|
setFocusedDateIndex(index);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{date.getDate()}
|
||||||
|
|
||||||
|
{/* Event dots */}
|
||||||
|
{dayEvents.length > 0 && (
|
||||||
|
<div className="absolute bottom-0 left-1/2 transform -translate-x-1/2 flex gap-0.5">
|
||||||
|
{dayEvents.slice(0, 3).map((event, eventIndex) => {
|
||||||
|
const programme = programmes.find(p => p.programmeId === event.programmeId);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={eventIndex}
|
||||||
|
className="w-1 h-1 rounded-full"
|
||||||
|
style={{ backgroundColor: programme?.color || '#gray' }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{dayEvents.length > 3 && (
|
||||||
|
<span className="text-[8px] text-muted-foreground">+{dayEvents.length - 3}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Timezone indicator */}
|
||||||
|
<div className="text-xs text-muted-foreground text-center py-1">
|
||||||
|
Times are in {timezone}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Agenda List */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h4 className="text-sm font-medium">Agenda</h4>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{selectedDate.toLocaleDateString('en-GB', { day: 'numeric', month: 'short' })}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{agendaEvents.length === 0 ? (
|
||||||
|
<div className="text-center py-6 space-y-2">
|
||||||
|
<CalendarIcon className="h-8 w-8 text-muted-foreground mx-auto" />
|
||||||
|
<p className="text-sm text-muted-foreground">No events today.</p>
|
||||||
|
<p className="text-xs text-muted-foreground">Explore your programme.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2 max-h-40 overflow-y-auto scrollbar-none [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
|
||||||
|
{agendaEvents.map((event) => {
|
||||||
|
const programme = programmes.find(p => p.programmeId === event.programmeId);
|
||||||
|
const typeInfo = getEventTypeInfo(event.type);
|
||||||
|
const canJoin = canJoinEvent(event);
|
||||||
|
const isPast = new Date(event.start) < new Date();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover key={event.id}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<button
|
||||||
|
className={`
|
||||||
|
w-full p-2 border rounded-lg text-left hover:shadow-sm transition-shadow focus:outline-none focus:ring-2 focus:ring-[var(--color-brand-primary)]
|
||||||
|
${isPast ? 'opacity-60' : ''}
|
||||||
|
`}
|
||||||
|
style={{ borderLeftColor: programme?.color, borderLeftWidth: '3px' }}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-2">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-1 mb-1">
|
||||||
|
<span className="text-xs">{typeInfo.icon}</span>
|
||||||
|
<h5 className="text-xs font-medium truncate">{event.title}</h5>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||||
|
<Clock className="h-3 w-3" />
|
||||||
|
{event.end ?
|
||||||
|
`${formatTime(event.start)} - ${formatTime(event.end)}` :
|
||||||
|
event.type.includes('due') ? 'Due' : formatTime(event.start)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{(event.location || event.facilitator) && (
|
||||||
|
<div className="flex items-center gap-1 text-xs text-muted-foreground mt-1">
|
||||||
|
{event.location && (
|
||||||
|
<>
|
||||||
|
<MapPin className="h-3 w-3" />
|
||||||
|
<span className="truncate">{event.location}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{event.facilitator && (
|
||||||
|
<>
|
||||||
|
<Users className="h-3 w-3" />
|
||||||
|
<span className="truncate">{event.facilitator}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{programme && (
|
||||||
|
<Badge variant="outline" className="mt-1 text-[10px] py-0 px-1">
|
||||||
|
{programme.programmeName}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action button */}
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
{canJoin && (
|
||||||
|
<Button size="sm" className="h-6 px-2 text-xs bg-[var(--color-brand-primary)] hover:bg-[var(--color-brand-primary)]/90">
|
||||||
|
Join
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{event.type === 'webinar' && !canJoin && !isPast && (
|
||||||
|
<Button size="sm" variant="outline" className="h-6 px-2 text-xs">
|
||||||
|
View
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{event.type === 'class' && (
|
||||||
|
<Button size="sm" variant="outline" className="h-6 px-2 text-xs">
|
||||||
|
Details
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{event.type.includes('due') && (
|
||||||
|
<Button size="sm" variant="outline" className="h-6 px-2 text-xs">
|
||||||
|
Open
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
|
||||||
|
<PopoverContent className="w-72 p-3" side="left">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium">{event.title}</h4>
|
||||||
|
<p className="text-sm text-muted-foreground">{programme?.programmeName} • {typeInfo.label}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
{event.end ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Clock className="h-4 w-4" />
|
||||||
|
<span>{formatTime(event.start, true)} - {formatTime(event.end)}</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Clock className="h-4 w-4" />
|
||||||
|
<span>{event.type.includes('due') ? 'Due by' : ''} {formatTime(event.start, true)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{event.location && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<MapPin className="h-4 w-4" />
|
||||||
|
<span>{event.location}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{event.facilitator && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Users className="h-4 w-4" />
|
||||||
|
<span>Facilitator: {event.facilitator}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{event.joinUrl && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Video className="h-4 w-4" />
|
||||||
|
<span>Online session</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 pt-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => exportToICS(event)}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Download className="h-3 w-3 mr-1" />
|
||||||
|
Add to calendar
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setReminder(event)}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Bell className="h-3 w-3 mr-1" />
|
||||||
|
Set reminder
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer Actions */}
|
||||||
|
<div className="flex items-center justify-between pt-2 border-t">
|
||||||
|
{/* Legend Dropdown */}
|
||||||
|
<DropdownMenu open={showLegend} onOpenChange={setShowLegend}>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="outline" size="sm" className="text-xs h-7 px-2">
|
||||||
|
<Palette className="h-3 w-3 mr-1" />
|
||||||
|
Legend
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="w-56">
|
||||||
|
{programmes.map((programme) => (
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
key={programme.programmeId}
|
||||||
|
checked={activeFilters.programmeIds.length === 0 || activeFilters.programmeIds.includes(programme.programmeId)}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setActiveFilters(prev => ({
|
||||||
|
...prev,
|
||||||
|
programmeIds: checked
|
||||||
|
? prev.programmeIds.includes(programme.programmeId)
|
||||||
|
? prev.programmeIds
|
||||||
|
: [...prev.programmeIds, programme.programmeId]
|
||||||
|
: prev.programmeIds.filter(id => id !== programme.programmeId)
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
className="w-2 h-2 rounded-full"
|
||||||
|
style={{ backgroundColor: programme.color }}
|
||||||
|
/>
|
||||||
|
<span className="text-xs truncate">{programme.programmeName}</span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
|
||||||
|
{/* Type Filters */}
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{[
|
||||||
|
{ key: 'webinar' as const, label: '🎥', icon: '🎥' },
|
||||||
|
{ key: 'class' as const, label: '🏫', icon: '🏫' },
|
||||||
|
{ key: 'due' as const, label: '⏳', icon: '⏳' }
|
||||||
|
].map((filter) => (
|
||||||
|
<Button
|
||||||
|
key={filter.key}
|
||||||
|
variant={activeFilters.types.includes(filter.key) ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => toggleFilterType(filter.key)}
|
||||||
|
className="text-xs h-7 px-2"
|
||||||
|
title={filter.key.charAt(0).toUpperCase() + filter.key.slice(1)}
|
||||||
|
>
|
||||||
|
{filter.icon}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,358 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { Button } from './ui/button';
|
|
||||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
|
||||||
import { X, ShoppingCart, CheckCircle, Trash2 } from 'lucide-react';
|
|
||||||
|
|
||||||
interface CartItem {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
price: string;
|
|
||||||
originalPrice?: string;
|
|
||||||
image: string;
|
|
||||||
type: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CartConfirmationModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
item: CartItem | null;
|
|
||||||
onGoToCart: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CartConfirmationModal({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
item,
|
|
||||||
onGoToCart
|
|
||||||
}: CartConfirmationModalProps) {
|
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
|
||||||
const [cartItems, setCartItems] = useState<CartItem[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isOpen) {
|
|
||||||
setIsVisible(true);
|
|
||||||
// Load all cart items
|
|
||||||
const existingCart = JSON.parse(localStorage.getItem('cart') || '[]');
|
|
||||||
setCartItems(existingCart);
|
|
||||||
// Prevent body scroll when modal is open
|
|
||||||
document.body.style.overflow = 'hidden';
|
|
||||||
} else {
|
|
||||||
setIsVisible(false);
|
|
||||||
document.body.style.overflow = 'unset';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup on unmount
|
|
||||||
return () => {
|
|
||||||
document.body.style.overflow = 'unset';
|
|
||||||
};
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
setIsVisible(false);
|
|
||||||
setTimeout(() => {
|
|
||||||
onClose();
|
|
||||||
}, 300); // Wait for animation to complete
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBackdropClick = (e: React.MouseEvent) => {
|
|
||||||
if (e.target === e.currentTarget) {
|
|
||||||
handleClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGoToCart = () => {
|
|
||||||
handleClose();
|
|
||||||
// Direct navigation to cart page
|
|
||||||
window.location.href = '/cart';
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveItem = (itemId: string) => {
|
|
||||||
const updatedCart = cartItems.filter(item => item.id !== itemId);
|
|
||||||
setCartItems(updatedCart);
|
|
||||||
localStorage.setItem('cart', JSON.stringify(updatedCart));
|
|
||||||
};
|
|
||||||
|
|
||||||
const calculateSubtotal = () => {
|
|
||||||
return cartItems.reduce((total, cartItem) => {
|
|
||||||
const price = parseInt(cartItem.price.replace(/[₹,]/g, ''));
|
|
||||||
return total + price;
|
|
||||||
}, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatPrice = (price: string) => {
|
|
||||||
const numericPrice = parseInt(price.replace(/[₹,]/g, ''));
|
|
||||||
return `₹${numericPrice.toLocaleString()}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isOpen || !item) return null;
|
|
||||||
|
|
||||||
const subtotal = calculateSubtotal();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 z-50 flex items-end justify-center lg:justify-end lg:items-stretch">
|
|
||||||
{/* Backdrop */}
|
|
||||||
<div
|
|
||||||
className={`absolute inset-0 bg-black transition-opacity duration-300 ${
|
|
||||||
isVisible ? 'opacity-50' : 'opacity-0'
|
|
||||||
}`}
|
|
||||||
onClick={handleBackdropClick}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Modal Content - Full Height */}
|
|
||||||
<div
|
|
||||||
className={`relative bg-white shadow-2xl transition-all duration-300 ease-out flex flex-col
|
|
||||||
${isVisible
|
|
||||||
? 'lg:translate-x-0 translate-y-0 opacity-100'
|
|
||||||
: 'lg:translate-x-full translate-y-full opacity-0'
|
|
||||||
}
|
|
||||||
/* Desktop: Slide from right, 30% width, full height */
|
|
||||||
lg:w-[30%] lg:h-full lg:max-w-md lg:min-w-[400px]
|
|
||||||
/* Mobile/Tablet: Slide from bottom, full width, full height */
|
|
||||||
w-full h-full
|
|
||||||
lg:rounded-none rounded-t-2xl
|
|
||||||
lg:border-l border-t lg:border-t-0
|
|
||||||
`}
|
|
||||||
role="dialog"
|
|
||||||
aria-labelledby="cart-modal-title"
|
|
||||||
aria-describedby="cart-modal-description"
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between p-6 border-b border-border bg-white lg:sticky lg:top-0 z-10">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-10 h-10 bg-success/10 rounded-full flex items-center justify-center">
|
|
||||||
<CheckCircle className="w-5 h-5 text-success" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 id="cart-modal-title" className="text-lg font-semibold text-foreground">
|
|
||||||
Added to Cart
|
|
||||||
</h3>
|
|
||||||
<p className="text-base text-muted-foreground">
|
|
||||||
{cartItems.length} programme{cartItems.length !== 1 ? 's' : ''} in your cart
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleClose}
|
|
||||||
className="h-10 w-10 p-0 hover:bg-muted text-base"
|
|
||||||
aria-label="Close modal"
|
|
||||||
>
|
|
||||||
<X className="w-5 h-5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content - Scrollable */}
|
|
||||||
<div className="flex-1 p-6 overflow-y-auto scrollbar-minimal">
|
|
||||||
<div id="cart-modal-description" className="space-y-6">
|
|
||||||
{/* Success Message for newly added item */}
|
|
||||||
<div className="bg-success/5 border border-success/20 rounded-lg p-4">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<CheckCircle className="w-5 h-5 text-success flex-shrink-0 mt-0.5" />
|
|
||||||
<div>
|
|
||||||
<p className="text-base font-semibold text-success mb-1">
|
|
||||||
"{item.title}" added successfully!
|
|
||||||
</p>
|
|
||||||
<p className="text-base text-success/80">
|
|
||||||
You can continue browsing or proceed to checkout.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* All Cart Items */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h4 className="text-lg font-semibold text-foreground">Your Cart ({cartItems.length})</h4>
|
|
||||||
<span className="text-base text-muted-foreground">Total: {formatPrice(subtotal.toString())}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
{cartItems.map((cartItem, index) => (
|
|
||||||
<div key={cartItem.id} className="bg-muted/20 rounded-lg p-4 border border-border">
|
|
||||||
<div className="flex gap-4">
|
|
||||||
{/* Product Image */}
|
|
||||||
<div className="w-20 h-20 rounded-lg overflow-hidden flex-shrink-0">
|
|
||||||
<ImageWithFallback
|
|
||||||
src={cartItem.image}
|
|
||||||
alt={cartItem.title}
|
|
||||||
className="w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Product Info */}
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-start justify-between gap-2">
|
|
||||||
<div className="flex-1">
|
|
||||||
<h5 className="font-semibold text-foreground text-base leading-tight mb-2 line-clamp-2">
|
|
||||||
{cartItem.title}
|
|
||||||
</h5>
|
|
||||||
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-base text-muted-foreground">Type:</span>
|
|
||||||
<span className="text-base font-medium capitalize">{cartItem.type}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-base text-muted-foreground">Price:</span>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-base font-bold text-primary">{formatPrice(cartItem.price)}</span>
|
|
||||||
{cartItem.originalPrice && cartItem.originalPrice !== cartItem.price && (
|
|
||||||
<span className="text-base text-muted-foreground line-through">
|
|
||||||
{formatPrice(cartItem.originalPrice)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Remove Button */}
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleRemoveItem(cartItem.id)}
|
|
||||||
className="h-8 w-8 p-0 hover:bg-destructive/10 hover:text-destructive"
|
|
||||||
aria-label={`Remove ${cartItem.title} from cart`}
|
|
||||||
>
|
|
||||||
<Trash2 className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Highlight newly added item */}
|
|
||||||
{cartItem.id === item.id && (
|
|
||||||
<div className="mt-2 flex items-center gap-2">
|
|
||||||
<div className="w-2 h-2 bg-success rounded-full animate-pulse"></div>
|
|
||||||
<span className="text-base text-success font-medium">Just added</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Cart Summary */}
|
|
||||||
<div className="bg-primary/5 border border-primary/20 rounded-lg p-6">
|
|
||||||
<div className="flex items-center gap-3 mb-4">
|
|
||||||
<ShoppingCart className="w-6 h-6 text-primary" />
|
|
||||||
<span className="text-lg font-semibold text-primary">Cart Summary</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="text-base text-muted-foreground">Total Items:</span>
|
|
||||||
<span className="text-base font-medium">
|
|
||||||
{cartItems.length} programme{cartItems.length !== 1 ? 's' : ''}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="text-base text-muted-foreground">Subtotal:</span>
|
|
||||||
<span className="text-lg font-bold text-primary">
|
|
||||||
{formatPrice(subtotal.toString())}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-2 border-t border-border">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="text-base font-medium text-foreground">Estimated Total:</span>
|
|
||||||
<span className="text-xl font-bold text-primary">
|
|
||||||
{formatPrice(subtotal.toString())}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-base text-muted-foreground mt-1">
|
|
||||||
Final total calculated at checkout
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Benefits */}
|
|
||||||
<div className="bg-muted/20 border border-border rounded-lg p-6">
|
|
||||||
<h4 className="text-lg font-semibold text-foreground mb-4">What's Next?</h4>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<CheckCircle className="w-5 h-5 text-success flex-shrink-0 mt-0.5" />
|
|
||||||
<div>
|
|
||||||
<p className="text-base font-medium text-foreground">Review Your Selection</p>
|
|
||||||
<p className="text-base text-muted-foreground">Modify quantities and remove items in your cart</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<CheckCircle className="w-5 h-5 text-success flex-shrink-0 mt-0.5" />
|
|
||||||
<div>
|
|
||||||
<p className="text-base font-medium text-foreground">Secure Checkout</p>
|
|
||||||
<p className="text-base text-muted-foreground">Complete enrollment with secure payment</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<CheckCircle className="w-5 h-5 text-success flex-shrink-0 mt-0.5" />
|
|
||||||
<div>
|
|
||||||
<p className="text-base font-medium text-foreground">Instant Confirmation</p>
|
|
||||||
<p className="text-base text-muted-foreground">Receive program details and access information</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Empty Cart State */}
|
|
||||||
{cartItems.length === 0 && (
|
|
||||||
<div className="text-center py-8">
|
|
||||||
<ShoppingCart className="w-12 h-12 text-muted-foreground mx-auto mb-4" />
|
|
||||||
<p className="text-lg font-medium text-foreground mb-2">Your cart is empty</p>
|
|
||||||
<p className="text-base text-muted-foreground">Browse our programmes to get started</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer - Sticky at bottom */}
|
|
||||||
<div className="p-6 border-t border-border bg-white lg:sticky lg:bottom-0">
|
|
||||||
<div className="space-y-4">
|
|
||||||
{cartItems.length > 0 && (
|
|
||||||
<div className="bg-primary/10 rounded-lg p-4 mb-4">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-base text-muted-foreground">Total Value</div>
|
|
||||||
<div className="text-2xl font-bold text-primary">
|
|
||||||
{formatPrice(subtotal.toString())}
|
|
||||||
</div>
|
|
||||||
<div className="text-base text-muted-foreground">
|
|
||||||
{cartItems.length} programme{cartItems.length !== 1 ? 's' : ''} selected
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={handleGoToCart}
|
|
||||||
className="w-full h-14 text-base font-medium"
|
|
||||||
size="lg"
|
|
||||||
disabled={cartItems.length === 0}
|
|
||||||
>
|
|
||||||
<ShoppingCart className="w-5 h-5 mr-2" />
|
|
||||||
{cartItems.length > 0 ? 'Proceed to Checkout' : 'Browse Programmes'}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleClose}
|
|
||||||
className="w-full h-14 text-base font-medium"
|
|
||||||
size="lg"
|
|
||||||
>
|
|
||||||
Continue Shopping
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-center text-base text-muted-foreground mt-4 leading-relaxed">
|
|
||||||
Secure checkout • 30-day money-back guarantee
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
2075
src/components/CoursePlayer.tsx
Normal file
410
src/components/Dashboard.tsx
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Play,
|
||||||
|
Edit,
|
||||||
|
TrendingUp,
|
||||||
|
Users,
|
||||||
|
Clock,
|
||||||
|
Target,
|
||||||
|
Flame,
|
||||||
|
Star,
|
||||||
|
Award,
|
||||||
|
ChevronRight,
|
||||||
|
Calendar,
|
||||||
|
BarChart3
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
import { Badge } from './ui/badge';
|
||||||
|
import { Progress } from './ui/progress';
|
||||||
|
import { Separator } from './ui/separator';
|
||||||
|
import { useAppContext } from './AppShell';
|
||||||
|
import {
|
||||||
|
PieChart,
|
||||||
|
Pie,
|
||||||
|
Cell,
|
||||||
|
BarChart,
|
||||||
|
Bar,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
CartesianGrid,
|
||||||
|
Tooltip,
|
||||||
|
ResponsiveContainer
|
||||||
|
} from 'recharts';
|
||||||
|
|
||||||
|
// Mock data - in real app this would come from API
|
||||||
|
const mockDashboardData = {
|
||||||
|
currentCourse: {
|
||||||
|
id: '1',
|
||||||
|
title: 'Strategic Leadership Development',
|
||||||
|
moduleTitle: 'Module 3: Decision Making Framework',
|
||||||
|
lessonTitle: 'Lesson 2: Risk Assessment Strategies',
|
||||||
|
dueAt: '2024-09-10T18:00:00Z',
|
||||||
|
progressPct: 65
|
||||||
|
},
|
||||||
|
weeklyConsistency: {
|
||||||
|
start: '2024-09-01',
|
||||||
|
end: '2024-09-07',
|
||||||
|
points: [
|
||||||
|
{ date: '2024-09-01', hours: 2.5, day: 'Mon' },
|
||||||
|
{ date: '2024-09-02', hours: 1.8, day: 'Tue' },
|
||||||
|
{ date: '2024-09-03', hours: 3.2, day: 'Wed' },
|
||||||
|
{ date: '2024-09-04', hours: 0, day: 'Thu' },
|
||||||
|
{ date: '2024-09-05', hours: 2.1, day: 'Fri' },
|
||||||
|
{ date: '2024-09-06', hours: 1.5, day: 'Sat' },
|
||||||
|
{ date: '2024-09-07', hours: 0.8, day: 'Sun' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
goals: {
|
||||||
|
daysOfWeek: [1, 2, 3, 4, 5], // Mon-Fri
|
||||||
|
minutesPerDay: 120
|
||||||
|
},
|
||||||
|
streaks: {
|
||||||
|
loginStreakDays: 12,
|
||||||
|
goalAlignedDays: 8
|
||||||
|
},
|
||||||
|
badges: [
|
||||||
|
{ id: '1', name: 'Early Bird', earnedAt: '2024-08-15', iconUrl: '', desc: 'Complete 5 lessons before 9 AM' },
|
||||||
|
{ id: '2', name: 'Consistent Learner', earnedAt: '2024-08-20', iconUrl: '', desc: '7-day learning streak' },
|
||||||
|
{ id: '3', name: 'Discussion Champion', earnedAt: '2024-08-25', iconUrl: '', desc: 'Participate in 10 discussions' }
|
||||||
|
],
|
||||||
|
leaderboardGlance: {
|
||||||
|
rank: 23,
|
||||||
|
total: 156,
|
||||||
|
xp: 2450,
|
||||||
|
scope: 'org'
|
||||||
|
},
|
||||||
|
recommendations: [
|
||||||
|
{
|
||||||
|
courseId: '101',
|
||||||
|
title: 'Advanced Communication Skills',
|
||||||
|
level: 'Intermediate',
|
||||||
|
trendingSkill: 'Leadership',
|
||||||
|
currentLearnersCount: 89,
|
||||||
|
price: 299
|
||||||
|
},
|
||||||
|
{
|
||||||
|
courseId: '102',
|
||||||
|
title: 'Financial Management for Leaders',
|
||||||
|
level: 'Advanced',
|
||||||
|
trendingSkill: 'Finance',
|
||||||
|
currentLearnersCount: 67,
|
||||||
|
price: 399
|
||||||
|
},
|
||||||
|
{
|
||||||
|
courseId: '103',
|
||||||
|
title: 'Team Building Essentials',
|
||||||
|
level: 'Beginner',
|
||||||
|
trendingSkill: 'Management',
|
||||||
|
currentLearnersCount: 134,
|
||||||
|
price: 199
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Dashboard() {
|
||||||
|
const { user, currentMode } = useAppContext();
|
||||||
|
const [dateRange, setDateRange] = useState({ start: '2024-09-01', end: '2024-09-07' });
|
||||||
|
|
||||||
|
const data = mockDashboardData;
|
||||||
|
|
||||||
|
// Donut chart data for module progress
|
||||||
|
const moduleProgressData = [
|
||||||
|
{ name: 'Completed', value: data.currentCourse.progressPct, fill: '#04045B' },
|
||||||
|
{ name: 'Remaining', value: 100 - data.currentCourse.progressPct, fill: '#C0C0C0' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const formatTime = (dateStr: string) => {
|
||||||
|
return new Date(dateStr).toLocaleString('en-IN', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
timeZone: 'Asia/Kolkata'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalWeeklyHours = data.weeklyConsistency.points.reduce((sum, point) => sum + point.hours, 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
{/* Header Greeting */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h1 className="text-3xl">Welcome back, {user.firstName}</h1>
|
||||||
|
<span className="text-2xl">👋</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Badge variant="secondary" className="capitalize">
|
||||||
|
{user.persona} Learner
|
||||||
|
</Badge>
|
||||||
|
{user.orgName && (
|
||||||
|
<Badge variant="outline">
|
||||||
|
{user.orgName}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
<Badge variant="outline">
|
||||||
|
Asia/Kolkata (GMT+5:30)
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Button>
|
||||||
|
<Play className="h-4 w-4 mr-2" />
|
||||||
|
Resume last activity
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline">
|
||||||
|
<Calendar className="h-4 w-4 mr-2" />
|
||||||
|
View course timeline
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||||||
|
{/* Left Column */}
|
||||||
|
<div className="lg:col-span-8 space-y-6">
|
||||||
|
{/* Module Progress */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Module Progress</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex items-center gap-6">
|
||||||
|
<div className="relative w-24 h-24">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={moduleProgressData}
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
innerRadius={25}
|
||||||
|
outerRadius={40}
|
||||||
|
startAngle={90}
|
||||||
|
endAngle={-270}
|
||||||
|
dataKey="value"
|
||||||
|
>
|
||||||
|
{moduleProgressData.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={entry.fill} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<span className="text-sm font-medium">{data.currentCourse.progressPct}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 space-y-1">
|
||||||
|
<h3 className="font-medium">{data.currentCourse.moduleTitle}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">{data.currentCourse.lessonTitle}</p>
|
||||||
|
<p className="text-sm">
|
||||||
|
<span className="text-muted-foreground">Due: </span>
|
||||||
|
{formatTime(data.currentCourse.dueAt)}
|
||||||
|
</p>
|
||||||
|
<Button size="sm" className="mt-2">
|
||||||
|
Continue Learning
|
||||||
|
<ChevronRight className="h-4 w-4 ml-1" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Weekly Consistency Chart */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
|
<CardTitle>Weekly Consistency Chart</CardTitle>
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<BarChart3 className="h-4 w-4" />
|
||||||
|
{totalWeeklyHours.toFixed(1)}h this week
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ResponsiveContainer width="100%" height={200}>
|
||||||
|
<BarChart data={data.weeklyConsistency.points}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="day" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip
|
||||||
|
formatter={(value) => [`${value}h`, 'Hours Spent']}
|
||||||
|
labelFormatter={(label) => `${label}`}
|
||||||
|
/>
|
||||||
|
<Bar dataKey="hours" fill="#04045B" radius={[4, 4, 0, 0]} />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Continue Learning */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Continue Learning</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between p-4 border rounded-lg">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm text-muted-foreground">Strategic Leadership Programme › Advanced Leadership › Module 3</p>
|
||||||
|
<h3 className="font-medium">{data.currentCourse.lessonTitle}</h3>
|
||||||
|
<Progress value={data.currentCourse.progressPct} className="w-32" />
|
||||||
|
</div>
|
||||||
|
<Button>
|
||||||
|
<Play className="h-4 w-4 mr-2" />
|
||||||
|
Resume
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Recommendations */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
|
<CardTitle>Based on your activity, here are our course recommendations</CardTitle>
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
<Edit className="h-4 w-4 mr-2" />
|
||||||
|
Edit recommendations
|
||||||
|
</Button>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{data.recommendations.map((course) => (
|
||||||
|
<div key={course.courseId} className="flex items-center justify-between p-4 border rounded-lg">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="font-medium">{course.title}</h3>
|
||||||
|
<Badge variant="secondary">{course.level}</Badge>
|
||||||
|
<Badge variant="outline" className="bg-[#F8C301] text-black">
|
||||||
|
{course.trendingSkill}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Users className="h-4 w-4" />
|
||||||
|
{course.currentLearnersCount} current learners
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{user.persona === 'individual' && (
|
||||||
|
<span className="font-medium">₹{course.price}</span>
|
||||||
|
)}
|
||||||
|
<Button>
|
||||||
|
{user.persona === 'individual' ? 'Enrol' : 'View details'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Column */}
|
||||||
|
<div className="lg:col-span-4 space-y-6">
|
||||||
|
{/* Learning Goals & Streaks */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Learning Goals & Streaks</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm text-muted-foreground">Weekly Goal</p>
|
||||||
|
<p className="font-medium">{data.goals.minutesPerDay} min/day, 5 days/week</p>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
<Edit className="h-4 w-4 mr-2" />
|
||||||
|
Edit My Goal
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Flame className="h-5 w-5 text-orange-500" />
|
||||||
|
<span className="font-medium">Login Streak</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-2xl font-bold">{data.streaks.loginStreakDays}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Target className="h-5 w-5 text-green-500" />
|
||||||
|
<span className="font-medium">Goal Aligned Days</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-2xl font-bold">{data.streaks.goalAlignedDays}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Badges */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Badges</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
{data.badges.map((badge) => (
|
||||||
|
<div key={badge.id} className="flex items-center gap-3 p-2 rounded-lg border">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-[#F8C301] flex items-center justify-center">
|
||||||
|
<Award className="h-5 w-5 text-[#04045B]" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="font-medium text-sm">{badge.name}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{badge.desc}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Global Leaderboard Glance */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Leaderboard</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
<div className="text-center space-y-2">
|
||||||
|
<div className="text-3xl font-bold">#{data.leaderboardGlance.rank}</div>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
out of {data.leaderboardGlance.total} {data.leaderboardGlance.scope === 'org' ? 'colleagues' : 'learners'}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-center gap-1">
|
||||||
|
<Star className="h-4 w-4 text-[#F8C301]" />
|
||||||
|
<span className="font-medium">{data.leaderboardGlance.xp} XP</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" className="w-full">
|
||||||
|
View Full Leaderboard
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* What other leaders are training for */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>What other leaders are training for</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
{data.recommendations.slice(0, 2).map((course) => (
|
||||||
|
<div key={course.courseId} className="p-3 border rounded-lg space-y-2">
|
||||||
|
<h3 className="font-medium text-sm">{course.title}</h3>
|
||||||
|
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Users className="h-3 w-3" />
|
||||||
|
{course.currentLearnersCount} learners
|
||||||
|
</div>
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{course.trendingSkill}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
431
src/components/DiscussionForums.tsx
Normal file
@@ -0,0 +1,431 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||||
|
import { Badge } from './ui/badge';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
||||||
|
import { Input } from './ui/input';
|
||||||
|
import { Textarea } from './ui/textarea';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from './ui/dialog';
|
||||||
|
import { MessageCircle, Plus, User, Calendar, Pin, Flag, ThumbsUp, Reply } from 'lucide-react';
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
persona: 'corporate' | 'individual';
|
||||||
|
firstName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Thread {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
author: string;
|
||||||
|
authorAvatar?: string;
|
||||||
|
createdAt: string;
|
||||||
|
lastReplyAt: string;
|
||||||
|
replies: number;
|
||||||
|
pinned?: boolean;
|
||||||
|
tags?: string[];
|
||||||
|
excerpt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Post {
|
||||||
|
id: string;
|
||||||
|
author: string;
|
||||||
|
authorAvatar?: string;
|
||||||
|
createdAt: string;
|
||||||
|
bodyHtml: string;
|
||||||
|
canEdit: boolean;
|
||||||
|
likes: number;
|
||||||
|
liked: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock data
|
||||||
|
const mockThreads: Thread[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
title: 'Best practices for remote team leadership',
|
||||||
|
author: 'Priya Sharma',
|
||||||
|
authorAvatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=64&h=64&fit=crop&crop=face',
|
||||||
|
createdAt: '2024-01-15T10:30:00Z',
|
||||||
|
lastReplyAt: '2024-01-16T14:20:00Z',
|
||||||
|
replies: 12,
|
||||||
|
pinned: true,
|
||||||
|
tags: ['leadership', 'remote-work'],
|
||||||
|
excerpt: 'Looking for insights on managing distributed teams effectively...'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
title: 'Digital transformation challenges in traditional industries',
|
||||||
|
author: 'Rajesh Kumar',
|
||||||
|
authorAvatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=64&h=64&fit=crop&crop=face',
|
||||||
|
createdAt: '2024-01-14T09:15:00Z',
|
||||||
|
lastReplyAt: '2024-01-16T11:45:00Z',
|
||||||
|
replies: 8,
|
||||||
|
tags: ['digital-transformation', 'strategy'],
|
||||||
|
excerpt: 'How do we overcome resistance to change in established organizations?'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
title: 'Effective feedback techniques for managers',
|
||||||
|
author: 'Sneha Patel',
|
||||||
|
createdAt: '2024-01-13T16:20:00Z',
|
||||||
|
lastReplyAt: '2024-01-15T13:10:00Z',
|
||||||
|
replies: 15,
|
||||||
|
tags: ['feedback', 'management'],
|
||||||
|
excerpt: 'Share your experiences with giving constructive feedback...'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockPosts: Post[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
author: 'Priya Sharma',
|
||||||
|
authorAvatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=64&h=64&fit=crop&crop=face',
|
||||||
|
createdAt: '2024-01-15T10:30:00Z',
|
||||||
|
bodyHtml: '<p>I have been leading a remote team for the past 2 years and wanted to share some insights while also learning from your experiences.</p><p><strong>Key challenges I have faced:</strong></p><ul><li>Maintaining team cohesion across time zones</li><li>Ensuring clear communication without over-communicating</li><li>Building trust with team members I rarely see in person</li></ul><p>What strategies have worked best for you?</p>',
|
||||||
|
canEdit: true,
|
||||||
|
likes: 8,
|
||||||
|
liked: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
author: 'Amit Singh',
|
||||||
|
authorAvatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=64&h=64&fit=crop&crop=face',
|
||||||
|
createdAt: '2024-01-15T14:45:00Z',
|
||||||
|
bodyHtml: '<p>Great topic, Priya! I have found that regular one-on-ones are crucial. Here is what works for me:</p><p>📅 <strong>Weekly 30-min check-ins</strong> - not just about work, but also personal connection</p><p>🎯 <strong>Clear goal setting</strong> - using OKRs to align everyone</p><p>🛠️ <strong>Right tools</strong> - Slack for quick comms, Zoom for face-to-face, Notion for documentation</p><p>The key is being intentional about relationship building, not just task management.</p>',
|
||||||
|
canEdit: false,
|
||||||
|
likes: 12,
|
||||||
|
liked: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export function DiscussionForums({ user }: { user: User }) {
|
||||||
|
const [selectedCohort, setSelectedCohort] = useState('cohort1');
|
||||||
|
const [currentView, setCurrentView] = useState<'threads' | 'thread'>('threads');
|
||||||
|
const [selectedThread, setSelectedThread] = useState<Thread | null>(null);
|
||||||
|
const [newThreadOpen, setNewThreadOpen] = useState(false);
|
||||||
|
const [newThreadTitle, setNewThreadTitle] = useState('');
|
||||||
|
const [newThreadBody, setNewThreadBody] = useState('');
|
||||||
|
const [replyText, setReplyText] = useState('');
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
const now = new Date();
|
||||||
|
const diffTime = now.getTime() - date.getTime();
|
||||||
|
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
if (diffDays === 0) return 'Today';
|
||||||
|
if (diffDays === 1) return 'Yesterday';
|
||||||
|
if (diffDays < 7) return `${diffDays} days ago`;
|
||||||
|
return date.toLocaleDateString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleThreadClick = (thread: Thread) => {
|
||||||
|
setSelectedThread(thread);
|
||||||
|
setCurrentView('thread');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateThread = () => {
|
||||||
|
// Mock thread creation
|
||||||
|
console.log('Creating thread:', { title: newThreadTitle, body: newThreadBody });
|
||||||
|
setNewThreadOpen(false);
|
||||||
|
setNewThreadTitle('');
|
||||||
|
setNewThreadBody('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReply = () => {
|
||||||
|
// Mock reply creation
|
||||||
|
console.log('Creating reply:', replyText);
|
||||||
|
setReplyText('');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (currentView === 'thread' && selectedThread) {
|
||||||
|
return (
|
||||||
|
<div className="p-6 space-y-6 bg-white min-h-screen">
|
||||||
|
{/* Thread Header */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setCurrentView('threads')}
|
||||||
|
className="mb-4"
|
||||||
|
>
|
||||||
|
← Back to Forums
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{selectedThread.pinned && <Pin className="h-4 w-4 text-[var(--color-brand-accent)]" />}
|
||||||
|
<h1 className="text-2xl font-medium">{selectedThread.title}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 text-sm text-gray-600">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Avatar className="h-6 w-6">
|
||||||
|
{selectedThread.authorAvatar && <AvatarImage src={selectedThread.authorAvatar} />}
|
||||||
|
<AvatarFallback>{selectedThread.author.charAt(0)}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<span>{selectedThread.author}</span>
|
||||||
|
</div>
|
||||||
|
<span>•</span>
|
||||||
|
<span>{formatDate(selectedThread.createdAt)}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>{selectedThread.replies} replies</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedThread.tags && (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{selectedThread.tags.map((tag) => (
|
||||||
|
<Badge key={tag} variant="secondary" className="text-xs">
|
||||||
|
{tag}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Posts */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
{mockPosts.map((post, index) => (
|
||||||
|
<Card key={post.id} className="bg-white border">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Avatar className="h-10 w-10">
|
||||||
|
{post.authorAvatar && <AvatarImage src={post.authorAvatar} />}
|
||||||
|
<AvatarFallback>{post.author.charAt(0)}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
|
||||||
|
<div className="flex-1 space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium">{post.author}</span>
|
||||||
|
<span className="text-sm text-gray-600">{formatDate(post.createdAt)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{post.canEdit && (
|
||||||
|
<Button variant="ghost" size="sm">
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button variant="ghost" size="sm">
|
||||||
|
<Flag className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="prose prose-sm max-w-none"
|
||||||
|
dangerouslySetInnerHTML={{ __html: post.bodyHtml }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 pt-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className={`flex items-center gap-2 ${post.liked ? 'text-[var(--color-brand-primary)]' : ''}`}
|
||||||
|
>
|
||||||
|
<ThumbsUp className="h-4 w-4" />
|
||||||
|
{post.likes}
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="sm" className="flex items-center gap-2">
|
||||||
|
<Reply className="h-4 w-4" />
|
||||||
|
Reply
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Reply Form */}
|
||||||
|
<Card className="bg-white border">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="font-medium">Add a reply</h3>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Share your thoughts..."
|
||||||
|
value={replyText}
|
||||||
|
onChange={(e) => setReplyText(e.target.value)}
|
||||||
|
rows={4}
|
||||||
|
className="bg-white"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button variant="outline" onClick={() => setReplyText('')}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleReply}
|
||||||
|
disabled={!replyText.trim()}
|
||||||
|
className="bg-[var(--color-brand-primary)] hover:bg-[var(--color-brand-primary)]/90 text-white"
|
||||||
|
>
|
||||||
|
Post Reply
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 space-y-6 bg-white min-h-screen">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-medium">Discussion Forums</h1>
|
||||||
|
<p className="text-gray-600">Connect and learn with your cohort</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{user.persona === 'corporate' && (
|
||||||
|
<Select value={selectedCohort} onValueChange={setSelectedCohort}>
|
||||||
|
<SelectTrigger className="w-48 bg-white">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="cohort1">Tech Solutions Cohort</SelectItem>
|
||||||
|
<SelectItem value="cohort2">Leadership Program 2024</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Dialog open={newThreadOpen} onOpenChange={setNewThreadOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button className="bg-[var(--color-brand-primary)] hover:bg-[var(--color-brand-primary)]/90 text-white">
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
New Thread
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Create New Discussion Thread</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium">Title</label>
|
||||||
|
<Input
|
||||||
|
placeholder="What would you like to discuss?"
|
||||||
|
value={newThreadTitle}
|
||||||
|
onChange={(e) => setNewThreadTitle(e.target.value)}
|
||||||
|
maxLength={120}
|
||||||
|
className="bg-white"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-600 mt-1">
|
||||||
|
{newThreadTitle.length}/120 characters
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium">Description</label>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Provide more details about your topic..."
|
||||||
|
value={newThreadBody}
|
||||||
|
onChange={(e) => setNewThreadBody(e.target.value)}
|
||||||
|
rows={6}
|
||||||
|
className="bg-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button variant="outline" onClick={() => setNewThreadOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleCreateThread}
|
||||||
|
disabled={!newThreadTitle.trim() || !newThreadBody.trim()}
|
||||||
|
className="bg-[var(--color-brand-primary)] hover:bg-[var(--color-brand-primary)]/90 text-white"
|
||||||
|
>
|
||||||
|
Create Thread
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Threads List */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{mockThreads.map((thread) => (
|
||||||
|
<Card
|
||||||
|
key={thread.id}
|
||||||
|
className="bg-white border hover:shadow-md transition-shadow cursor-pointer"
|
||||||
|
onClick={() => handleThreadClick(thread)}
|
||||||
|
>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Avatar className="h-10 w-10">
|
||||||
|
{thread.authorAvatar && <AvatarImage src={thread.authorAvatar} />}
|
||||||
|
<AvatarFallback>{thread.author.charAt(0)}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
|
||||||
|
<div className="flex-1 space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{thread.pinned && <Pin className="h-4 w-4 text-[var(--color-brand-accent)]" />}
|
||||||
|
<h3 className="font-medium hover:text-[var(--color-brand-primary)]">
|
||||||
|
{thread.title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-gray-600 line-clamp-2">
|
||||||
|
{thread.excerpt}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4 text-xs text-gray-500">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<User className="h-3 w-3" />
|
||||||
|
{thread.author}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Calendar className="h-3 w-3" />
|
||||||
|
{formatDate(thread.createdAt)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<MessageCircle className="h-3 w-3" />
|
||||||
|
{thread.replies} replies
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{thread.tags && (
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{thread.tags.slice(0, 2).map((tag) => (
|
||||||
|
<Badge key={tag} variant="secondary" className="text-xs">
|
||||||
|
{tag}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
{thread.tags.length > 2 && (
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
+{thread.tags.length - 2}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{mockThreads.length === 0 && (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<MessageCircle className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-2">No discussions yet</h3>
|
||||||
|
<p className="text-gray-600 mb-4">Be the first to start a conversation with your cohort.</p>
|
||||||
|
<Button
|
||||||
|
onClick={() => setNewThreadOpen(true)}
|
||||||
|
className="bg-[var(--color-brand-primary)] hover:bg-[var(--color-brand-primary)]/90 text-white"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
Start a Discussion
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
|
||||||
import { Badge } from './ui/badge';
|
|
||||||
import { Button } from './ui/button';
|
|
||||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
|
||||||
import { navigate } from './Router';
|
|
||||||
import {
|
|
||||||
Star,
|
|
||||||
Users,
|
|
||||||
MapPin,
|
|
||||||
Calendar,
|
|
||||||
Clock,
|
|
||||||
ArrowRight,
|
|
||||||
TrendingUp
|
|
||||||
} from 'lucide-react';
|
|
||||||
|
|
||||||
interface FacilityShowcaseCardProps {
|
|
||||||
facility: {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
capacity: string;
|
|
||||||
area: string;
|
|
||||||
hourlyPrice: number;
|
|
||||||
dayPrice: number;
|
|
||||||
rating: number;
|
|
||||||
bookings: number;
|
|
||||||
availability: 'available' | 'limited' | 'unavailable';
|
|
||||||
category: string;
|
|
||||||
features: string[];
|
|
||||||
image: string;
|
|
||||||
analyticsData?: {
|
|
||||||
occupancy: string;
|
|
||||||
satisfaction: string;
|
|
||||||
rebookRate: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FacilityShowcaseCard({ facility }: FacilityShowcaseCardProps) {
|
|
||||||
const formatPrice = (price: number) => {
|
|
||||||
return new Intl.NumberFormat('en-IN', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: 'INR',
|
|
||||||
maximumFractionDigits: 0
|
|
||||||
}).format(price);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAvailabilityColor = (availability: string) => {
|
|
||||||
switch (availability) {
|
|
||||||
case 'available': return 'text-success';
|
|
||||||
case 'limited': return 'text-yellow-600';
|
|
||||||
case 'unavailable': return 'text-destructive';
|
|
||||||
default: return 'text-muted-foreground';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAvailabilityText = (availability: string) => {
|
|
||||||
switch (availability) {
|
|
||||||
case 'available': return 'Available';
|
|
||||||
case 'limited': return 'Limited';
|
|
||||||
case 'unavailable': return 'Fully Booked';
|
|
||||||
default: return 'Unknown';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const analyticsData = facility.analyticsData || {
|
|
||||||
occupancy: '17.8K',
|
|
||||||
satisfaction: '1.3%',
|
|
||||||
rebookRate: '25.4'
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="max-w-sm overflow-hidden hover:shadow-lg transition-all duration-300 group bg-white">
|
|
||||||
{/* Header with badges and rating */}
|
|
||||||
<div className="relative p-4 pb-2">
|
|
||||||
<div className="flex items-center justify-between mb-3">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Badge variant="secondary" className="text-xs">
|
|
||||||
{facility.category}
|
|
||||||
</Badge>
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className={`text-xs ${getAvailabilityColor(facility.availability)}`}
|
|
||||||
>
|
|
||||||
{getAvailabilityText(facility.availability)}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1 bg-amber-50 px-2 py-1 rounded-full">
|
|
||||||
<Star className="w-3 h-3 fill-amber-400 text-amber-400" />
|
|
||||||
<span className="text-xs font-medium text-amber-700">{facility.rating}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Analytics Chart Placeholder */}
|
|
||||||
<div className="bg-slate-50 rounded-lg p-3 mb-4">
|
|
||||||
<div className="grid grid-cols-4 gap-3 mb-3">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-xs text-muted-foreground mb-1">Total Book</div>
|
|
||||||
<div className="text-sm font-semibold text-blue-600">223</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-xs text-muted-foreground mb-1">This Month</div>
|
|
||||||
<div className="text-sm font-semibold text-blue-600">{analyticsData.occupancy}</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-xs text-muted-foreground mb-1">Analytics</div>
|
|
||||||
<div className="text-sm font-semibold text-green-600">{analyticsData.satisfaction}</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-xs text-muted-foreground mb-1">Client Sat</div>
|
|
||||||
<div className="text-sm font-semibold text-purple-600">{analyticsData.rebookRate}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Chart visualization */}
|
|
||||||
<div className="h-16 bg-gradient-to-r from-blue-100 to-purple-100 rounded flex items-end justify-center relative overflow-hidden">
|
|
||||||
<svg className="w-full h-full" viewBox="0 0 200 40">
|
|
||||||
<polyline
|
|
||||||
fill="none"
|
|
||||||
stroke="#3b82f6"
|
|
||||||
strokeWidth="2"
|
|
||||||
points="10,30 30,25 50,28 70,20 90,22 110,15 130,18 150,12 170,16 190,10"
|
|
||||||
/>
|
|
||||||
<polyline
|
|
||||||
fill="none"
|
|
||||||
stroke="#8b5cf6"
|
|
||||||
strokeWidth="2"
|
|
||||||
points="10,35 30,32 50,35 70,28 90,30 110,25 130,28 150,22 170,25 190,20"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<div className="absolute bottom-1 left-2 text-xs text-muted-foreground">Jan 23</div>
|
|
||||||
<div className="absolute bottom-1 right-2 text-xs text-muted-foreground">Dec 24</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Facility Information */}
|
|
||||||
<CardHeader className="pt-0 pb-3">
|
|
||||||
<CardTitle className="text-xl font-bold group-hover:text-primary transition-colors">
|
|
||||||
{facility.name}
|
|
||||||
</CardTitle>
|
|
||||||
<p className="text-sm text-muted-foreground line-clamp-2">
|
|
||||||
{facility.description}
|
|
||||||
</p>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
{/* Stats Grid */}
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
|
||||||
<div className="flex items-center gap-2 text-sm">
|
|
||||||
<Users className="w-4 h-4 text-muted-foreground" />
|
|
||||||
<span>{facility.capacity}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-sm">
|
|
||||||
<MapPin className="w-4 h-4 text-muted-foreground" />
|
|
||||||
<span>{facility.area}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-sm">
|
|
||||||
<Calendar className="w-4 h-4 text-muted-foreground" />
|
|
||||||
<span>{facility.bookings} bookings</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-sm">
|
|
||||||
<Clock className="w-4 h-4 text-muted-foreground" />
|
|
||||||
<span>Hourly/Daily</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Key Features */}
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2 text-sm">Key Features</h4>
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{facility.features.slice(0, 3).map((feature, idx) => (
|
|
||||||
<Badge key={idx} variant="secondary" className="text-xs">
|
|
||||||
{feature}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
{facility.features.length > 3 && (
|
|
||||||
<Badge variant="outline" className="text-xs">
|
|
||||||
+{facility.features.length - 3} more
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Pricing */}
|
|
||||||
<div className="border-t pt-4">
|
|
||||||
<div className="mb-3">
|
|
||||||
<div className="text-xs text-muted-foreground">Starting from</div>
|
|
||||||
<div className="text-lg font-bold text-primary">
|
|
||||||
{formatPrice(facility.hourlyPrice)}/hour
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-muted-foreground">
|
|
||||||
{formatPrice(facility.dayPrice)}/day
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
className="w-full bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
|
|
||||||
onClick={() => navigate(`/facility/${facility.id}`)}
|
|
||||||
disabled={facility.availability === 'unavailable'}
|
|
||||||
>
|
|
||||||
{facility.availability === 'unavailable' ? 'Fully Booked' : 'View Details'}
|
|
||||||
{facility.availability !== 'unavailable' && (
|
|
||||||
<ArrowRight className="w-4 h-4 ml-2" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Button } from './ui/button';
|
|
||||||
import klcLogo from 'figma:asset/c2d0a01da274cef655bbdfb1b11ff3e9993ea278.png';
|
|
||||||
import { navigate } from './Router';
|
|
||||||
import {
|
|
||||||
Mail,
|
|
||||||
Phone,
|
|
||||||
MapPin,
|
|
||||||
Linkedin,
|
|
||||||
Instagram
|
|
||||||
} from 'lucide-react';
|
|
||||||
|
|
||||||
// Custom X (formerly Twitter) icon component
|
|
||||||
const XIcon = ({ className }: { className?: string }) => (
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
className={className}
|
|
||||||
fill="currentColor"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
const footerSections = [
|
|
||||||
{
|
|
||||||
title: 'Services',
|
|
||||||
links: [
|
|
||||||
{ name: 'All Services', href: '/services' },
|
|
||||||
{ name: 'Leadership Development', href: '/services/leadership-development' },
|
|
||||||
{ name: 'Management Development', href: '/services/management-development' },
|
|
||||||
{ name: 'Executive Coaching', href: '/services/executive-coaching' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Learning',
|
|
||||||
links: [
|
|
||||||
{ name: 'Articles', href: '/learning/articles' },
|
|
||||||
{ name: 'Blogs', href: '/learning/blogs' },
|
|
||||||
{ name: 'Webcasts', href: '/learning/webcasts' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Facilities',
|
|
||||||
links: [
|
|
||||||
{ name: 'Virtual Tour', href: '/facility-tour' },
|
|
||||||
{ name: 'Learning Facility', href: '/services/learning-facility' },
|
|
||||||
{ name: 'Boarding & Lodging Facility', href: '/facility/boarding-lodging' },
|
|
||||||
{ name: 'Recreation Facility', href: '/facility/recreation' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Support',
|
|
||||||
links: [
|
|
||||||
{ name: 'Contact Us', href: '/contact' },
|
|
||||||
{ name: 'Login', href: '/login' },
|
|
||||||
{ name: 'Sign Up', href: '/signup' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const socialLinks = [
|
|
||||||
{ name: 'LinkedIn', href: '#', icon: Linkedin },
|
|
||||||
{ name: 'X', href: '#', icon: XIcon },
|
|
||||||
{ name: 'Instagram', href: '#', icon: Instagram }
|
|
||||||
];
|
|
||||||
|
|
||||||
export function Footer() {
|
|
||||||
return (
|
|
||||||
<footer className="bg-background border-t">
|
|
||||||
{/* Main Footer Content */}
|
|
||||||
<div className="container mx-auto px-4 lg:px-8 py-16">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-8 lg:gap-12">
|
|
||||||
{/* Brand Section */}
|
|
||||||
<div className="lg:col-span-2 space-y-6">
|
|
||||||
<div className="flex items-start">
|
|
||||||
<img
|
|
||||||
src={klcLogo}
|
|
||||||
alt="Kautilya Leadership Centre"
|
|
||||||
className="h-16 w-auto"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground leading-relaxed">
|
|
||||||
Empowering leaders and organizations worldwide through innovative leadership
|
|
||||||
development programs, cutting-edge research, and transformational learning experiences.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Contact Info */}
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center gap-3 text-sm">
|
|
||||||
<MapPin className="w-4 h-4 text-primary flex-shrink-0" />
|
|
||||||
<span>123 Leadership Avenue, New Delhi, India 110001</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3 text-sm">
|
|
||||||
<Phone className="w-4 h-4 text-primary flex-shrink-0" />
|
|
||||||
<span>+91 11 4567 8900</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3 text-sm">
|
|
||||||
<Mail className="w-4 h-4 text-primary flex-shrink-0" />
|
|
||||||
<span>info@klc.edu.in</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Social Links */}
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{socialLinks.map((social) => {
|
|
||||||
const Icon = social.icon;
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
key={social.name}
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="w-10 h-10 p-0"
|
|
||||||
asChild
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href={social.href}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
aria-label={`Follow us on ${social.name}`}
|
|
||||||
>
|
|
||||||
<Icon className="w-4 h-4" />
|
|
||||||
</a>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer Links */}
|
|
||||||
{footerSections.map((section) => (
|
|
||||||
<div key={section.title} className="space-y-4">
|
|
||||||
<h4 className="font-semibold text-foreground">{section.title}</h4>
|
|
||||||
<ul className="space-y-3">
|
|
||||||
{section.links.map((link) => (
|
|
||||||
<li key={link.name}>
|
|
||||||
<button
|
|
||||||
onClick={() => navigate(link.href)}
|
|
||||||
className="text-sm text-muted-foreground hover:text-primary transition-colors text-left"
|
|
||||||
>
|
|
||||||
{link.name}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bottom Section */}
|
|
||||||
<div className="border-t">
|
|
||||||
<div className="container mx-auto px-4 lg:px-8 py-6">
|
|
||||||
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
|
|
||||||
<div className="flex flex-col md:flex-row items-center gap-4 text-sm text-muted-foreground">
|
|
||||||
<span>© 2025 Kautilya Leadership Centre. All rights reserved.</span>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<button
|
|
||||||
onClick={() => navigate('/privacy')}
|
|
||||||
className="hover:text-primary transition-colors"
|
|
||||||
>
|
|
||||||
Privacy Policy
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => navigate('/terms')}
|
|
||||||
className="hover:text-primary transition-colors"
|
|
||||||
>
|
|
||||||
Terms of Service
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
|
||||||
<span>Accredited by:</span>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-8 h-6 bg-muted rounded flex items-center justify-center">
|
|
||||||
<span className="text-xs font-bold">ISO</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-8 h-6 bg-muted rounded flex items-center justify-center">
|
|
||||||
<span className="text-xs font-bold">NAAC</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
359
src/components/GlobalSearch.tsx
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Search, FileText, Video, BookOpen, MessageCircle, X } from 'lucide-react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
import { Input } from './ui/input';
|
||||||
|
import { Badge } from './ui/badge';
|
||||||
|
import { Checkbox } from './ui/checkbox';
|
||||||
|
import { Label } from './ui/label';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog';
|
||||||
|
|
||||||
|
interface SearchResult {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
type: 'course' | 'webcast' | 'blog' | 'article' | 'forum';
|
||||||
|
summary?: string;
|
||||||
|
url: string;
|
||||||
|
theme?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SearchFacets {
|
||||||
|
type: string[];
|
||||||
|
theme: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock data
|
||||||
|
const mockResults: SearchResult[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
title: 'Strategic Leadership in Digital Age',
|
||||||
|
type: 'course',
|
||||||
|
summary: 'Comprehensive course covering leadership principles for digital transformation, including change management strategies and team engagement techniques.',
|
||||||
|
url: '/courses/strategic-leadership-digital',
|
||||||
|
theme: 'Leadership'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
title: 'Future of Work Webcast Series',
|
||||||
|
type: 'webcast',
|
||||||
|
summary: 'Expert panel discussion on emerging trends in workplace dynamics, remote leadership, and organizational resilience in the post-pandemic era.',
|
||||||
|
url: '/webcasts/future-of-work',
|
||||||
|
theme: 'Digital Transformation'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
title: 'Building High-Performance Teams',
|
||||||
|
type: 'blog',
|
||||||
|
summary: 'Key insights on team dynamics, psychological safety, and performance optimization drawn from recent organizational psychology research.',
|
||||||
|
url: '/blog/high-performance-teams',
|
||||||
|
theme: 'Team Building'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
title: 'Emotional Intelligence for Leaders',
|
||||||
|
type: 'article',
|
||||||
|
summary: 'Research-backed strategies for developing emotional intelligence capabilities and applying them in leadership contexts for better outcomes.',
|
||||||
|
url: '/articles/emotional-intelligence-leaders',
|
||||||
|
theme: 'Leadership'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
title: 'Best practices for remote team leadership',
|
||||||
|
type: 'forum',
|
||||||
|
url: '/forums/threads/remote-leadership-practices',
|
||||||
|
theme: 'Leadership'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const allTypes = ['course', 'webcast', 'blog', 'article', 'forum'];
|
||||||
|
const allThemes = ['Leadership', 'Digital Transformation', 'Team Building', 'Strategy', 'Innovation'];
|
||||||
|
|
||||||
|
interface GlobalSearchProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onOpenAIMentor?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GlobalSearch({ isOpen, onClose, onOpenAIMentor }: GlobalSearchProps) {
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
const [results, setResults] = useState<SearchResult[]>([]);
|
||||||
|
const [facets, setFacets] = useState<SearchFacets>({ type: [], theme: [] });
|
||||||
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
|
const [hasSearched, setHasSearched] = useState(false);
|
||||||
|
|
||||||
|
const handleSearch = async () => {
|
||||||
|
if (!query.trim()) return;
|
||||||
|
|
||||||
|
setIsSearching(true);
|
||||||
|
setHasSearched(true);
|
||||||
|
|
||||||
|
// Simulate API call with enhanced search
|
||||||
|
setTimeout(() => {
|
||||||
|
let filteredResults = mockResults.filter(result =>
|
||||||
|
result.title.toLowerCase().includes(query.toLowerCase()) ||
|
||||||
|
(result.summary && result.summary.toLowerCase().includes(query.toLowerCase()))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Apply facet filters
|
||||||
|
if (facets.type.length > 0) {
|
||||||
|
filteredResults = filteredResults.filter(result =>
|
||||||
|
facets.type.includes(result.type)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (facets.theme.length > 0) {
|
||||||
|
filteredResults = filteredResults.filter(result =>
|
||||||
|
result.theme && facets.theme.includes(result.theme)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate AI-generated summaries for relevant items
|
||||||
|
filteredResults = filteredResults.map(result => ({
|
||||||
|
...result,
|
||||||
|
summary: result.summary || generateAISummary(result.title, result.type)
|
||||||
|
}));
|
||||||
|
|
||||||
|
setResults(filteredResults);
|
||||||
|
setIsSearching(false);
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateAISummary = (title: string, type: string) => {
|
||||||
|
// Mock AI summary generation based on content type
|
||||||
|
const summaries = {
|
||||||
|
course: `Comprehensive ${title.toLowerCase()} curriculum with practical exercises and real-world applications. Includes assessments and certification upon completion.`,
|
||||||
|
webcast: `Live expert discussion on ${title.toLowerCase()}. Interactive session with Q&A and downloadable resources for participants.`,
|
||||||
|
blog: `Insightful article exploring ${title.toLowerCase()}. Evidence-based analysis with actionable takeaways for immediate implementation.`,
|
||||||
|
article: `Research-backed insights on ${title.toLowerCase()}. Data-driven recommendations from industry experts and thought leaders.`,
|
||||||
|
forum: `Community discussion thread about ${title.toLowerCase()}. Peer-to-peer learning with shared experiences and best practices.`
|
||||||
|
};
|
||||||
|
return summaries[type as keyof typeof summaries] || `Relevant content about ${title.toLowerCase()}.`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleFacet = (facetType: 'type' | 'theme', value: string) => {
|
||||||
|
setFacets(prev => ({
|
||||||
|
...prev,
|
||||||
|
[facetType]: prev[facetType].includes(value)
|
||||||
|
? prev[facetType].filter(item => item !== value)
|
||||||
|
: [...prev[facetType], value]
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Re-run search with new facets
|
||||||
|
if (hasSearched) {
|
||||||
|
handleSearch();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTypeIcon = (type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'course': return <BookOpen className="h-4 w-4" />;
|
||||||
|
case 'webcast': return <Video className="h-4 w-4" />;
|
||||||
|
case 'blog': return <FileText className="h-4 w-4" />;
|
||||||
|
case 'article': return <FileText className="h-4 w-4" />;
|
||||||
|
case 'forum': return <MessageCircle className="h-4 w-4" />;
|
||||||
|
default: return <FileText className="h-4 w-4" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearFilters = () => {
|
||||||
|
setFacets({ type: [], theme: [] });
|
||||||
|
if (hasSearched) {
|
||||||
|
handleSearch();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[80vh] p-0 bg-white">
|
||||||
|
<DialogHeader className="px-6 py-4 border-b">
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Search className="h-5 w-5" />
|
||||||
|
Global Search
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="flex h-[calc(80vh-120px)]">
|
||||||
|
{/* Search and Filters Sidebar */}
|
||||||
|
<div className="w-80 border-r bg-gray-50 p-4 space-y-4">
|
||||||
|
{/* Search Input */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search courses, articles, discussions..."
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') handleSearch();
|
||||||
|
if (e.key === '/') e.preventDefault();
|
||||||
|
}}
|
||||||
|
className="pl-10 bg-white"
|
||||||
|
maxLength={200}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-600">{query.length}/200 characters</p>
|
||||||
|
<Button
|
||||||
|
onClick={handleSearch}
|
||||||
|
disabled={!query.trim() || isSearching}
|
||||||
|
className="w-full bg-[var(--color-brand-primary)] hover:bg-[var(--color-brand-primary)]/90 text-white"
|
||||||
|
>
|
||||||
|
{isSearching ? 'Searching...' : 'Search'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Facet Filters */}
|
||||||
|
{hasSearched && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="font-medium">Filters</h3>
|
||||||
|
{(facets.type.length > 0 || facets.theme.length > 0) && (
|
||||||
|
<Button variant="ghost" size="sm" onClick={clearFilters}>
|
||||||
|
Clear all
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Type Filter */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="text-sm font-medium">Type</h4>
|
||||||
|
{allTypes.map((type) => (
|
||||||
|
<div key={type} className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id={`type-${type}`}
|
||||||
|
checked={facets.type.includes(type)}
|
||||||
|
onCheckedChange={() => toggleFacet('type', type)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor={`type-${type}`} className="text-sm capitalize">
|
||||||
|
{type}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Theme Filter */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="text-sm font-medium">Theme</h4>
|
||||||
|
{allThemes.map((theme) => (
|
||||||
|
<div key={theme} className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id={`theme-${theme}`}
|
||||||
|
checked={facets.theme.includes(theme)}
|
||||||
|
onCheckedChange={() => toggleFacet('theme', theme)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor={`theme-${theme}`} className="text-sm">
|
||||||
|
{theme}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Results Area */}
|
||||||
|
<div className="flex-1 p-6">
|
||||||
|
{!hasSearched && (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<Search className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||||
|
Search across all your learning resources
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Find courses, articles, discussions, and more with AI-powered search
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isSearching && (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[var(--color-brand-primary)] mx-auto mb-4"></div>
|
||||||
|
<p className="text-gray-600">Searching...</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasSearched && !isSearching && results.length === 0 && (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<Search className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||||
|
No results found
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 mb-4">
|
||||||
|
Try adjusting your search terms or filters
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
onClick={onOpenAIMentor}
|
||||||
|
variant="outline"
|
||||||
|
className="border-[var(--color-brand-primary)] text-[var(--color-brand-primary)]"
|
||||||
|
>
|
||||||
|
Open AI Mentor for help
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasSearched && !isSearching && results.length > 0 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="font-medium">
|
||||||
|
{results.length} result{results.length !== 1 ? 's' : ''} for "{query}"
|
||||||
|
</h3>
|
||||||
|
{(facets.type.length > 0 || facets.theme.length > 0) && (
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{facets.type.map(type => (
|
||||||
|
<Badge key={type} variant="secondary" className="text-xs">
|
||||||
|
{type}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
{facets.theme.map(theme => (
|
||||||
|
<Badge key={theme} variant="secondary" className="text-xs">
|
||||||
|
{theme}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{results.map((result) => (
|
||||||
|
<Card key={result.id} className="hover:shadow-md transition-shadow cursor-pointer bg-white border">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="text-[var(--color-brand-primary)] mt-1">
|
||||||
|
{getTypeIcon(result.type)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h4 className="font-medium hover:text-[var(--color-brand-primary)]">
|
||||||
|
{result.title}
|
||||||
|
</h4>
|
||||||
|
<Badge variant="outline" className="text-xs capitalize">
|
||||||
|
{result.type}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
{result.summary && (
|
||||||
|
<p className="text-sm text-gray-600 line-clamp-2">
|
||||||
|
{result.summary}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs text-gray-500">{result.url}</span>
|
||||||
|
{result.theme && (
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{result.theme}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Button } from './ui/button';
|
|
||||||
import { ArrowRight, ChevronLeft, ChevronRight } from 'lucide-react';
|
|
||||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
|
||||||
import heroBannerImage from 'figma:asset/1bb9c22c86c0892d4716564b7135835f04869298.png';
|
|
||||||
|
|
||||||
export function HeroBanner() {
|
|
||||||
const handleEnrollNow = () => {
|
|
||||||
// Navigate to webinars page
|
|
||||||
window.location.href = '/webinars?view=individual';
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBuildPipeline = () => {
|
|
||||||
// Navigate to programs catalogue or individual dashboard
|
|
||||||
window.location.href = '/dashboard?view=individual';
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative w-full">
|
|
||||||
{/* Top Announcement Bar */}
|
|
||||||
<div className="bg-[#F8C301] text-[#26231A] py-3 px-4">
|
|
||||||
<div className="container mx-auto px-4 lg:px-8">
|
|
||||||
<div className="flex items-center justify-center gap-4 text-center">
|
|
||||||
<span className="text-base font-medium">
|
|
||||||
Join Our Upcoming Leadership Webinars - Transform Your Leadership Journey
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
onClick={handleEnrollNow}
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="text-[#26231A] hover:bg-[#26231A]/10 font-medium text-base h-auto py-1 px-3"
|
|
||||||
>
|
|
||||||
Enroll Now
|
|
||||||
<ArrowRight className="ml-2 h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Hero Section */}
|
|
||||||
<div className="relative min-h-[80vh] flex items-center justify-center overflow-hidden">
|
|
||||||
{/* Background Image */}
|
|
||||||
<div className="absolute inset-0 z-0">
|
|
||||||
<ImageWithFallback
|
|
||||||
src={heroBannerImage}
|
|
||||||
alt="Leadership workshop with diverse team members collaborating with colorful sticky notes on a wall"
|
|
||||||
className="w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
{/* Overlay for better text readability */}
|
|
||||||
<div className="absolute inset-0 bg-black/40"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Hero Content */}
|
|
||||||
<div className="relative z-10 container mx-auto px-4 lg:px-8 pt-20 pb-32">
|
|
||||||
<div className="max-w-4xl">
|
|
||||||
<div className="text-white space-y-8">
|
|
||||||
{/* Main Heading */}
|
|
||||||
<h1 className="text-5xl lg:text-6xl font-bold leading-tight">
|
|
||||||
Empowering Future-Ready Leaders
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
{/* Subtext */}
|
|
||||||
<p className="text-xl lg:text-2xl text-white/90 leading-relaxed max-w-2xl">
|
|
||||||
Build confidence, agility, and clarity for today's complex challenges.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* CTA Button */}
|
|
||||||
<div className="pt-4">
|
|
||||||
<Button
|
|
||||||
onClick={handleBuildPipeline}
|
|
||||||
size="lg"
|
|
||||||
className="bg-[#04045B] hover:bg-[#04045B]/90 text-white text-lg px-8 py-4 min-h-[60px] font-medium"
|
|
||||||
>
|
|
||||||
Build Your Leadership Pipeline
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bottom Feature Navigation */}
|
|
||||||
<div className="absolute bottom-0 left-0 right-0 z-10">
|
|
||||||
<div className="container mx-auto px-4 lg:px-8">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 py-12">
|
|
||||||
{/* Feature 01 */}
|
|
||||||
<div className="text-white space-y-4">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="text-[#F8C301] text-xl font-bold">01</div>
|
|
||||||
<div className="h-px bg-[#F8C301] flex-1"></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold mb-2">Leadership Is Learning. We Teach It Right.</h3>
|
|
||||||
<p className="text-white/80 text-base leading-relaxed">
|
|
||||||
Master proven methodologies and frameworks that transform managers into exceptional leaders.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Feature 02 */}
|
|
||||||
<div className="text-white space-y-4">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="text-[#F8C301] text-xl font-bold">02</div>
|
|
||||||
<div className="h-px bg-[#F8C301] flex-1"></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold mb-2">Turn Managers Into Impactful Leaders</h3>
|
|
||||||
<p className="text-white/80 text-base leading-relaxed">
|
|
||||||
Develop strategic thinking, emotional intelligence, and decision-making capabilities.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Feature 03 */}
|
|
||||||
<div className="text-white space-y-4">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="text-[#F8C301] text-xl font-bold">03</div>
|
|
||||||
<div className="h-px bg-[#F8C301] flex-1"></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold mb-2">Struggling with Managerial Gaps?</h3>
|
|
||||||
<p className="text-white/80 text-base leading-relaxed">
|
|
||||||
Bridge the gap between individual contribution and effective team leadership.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Navigation Controls */}
|
|
||||||
<div className="absolute bottom-6 right-6 flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="w-12 h-12 rounded-full bg-white/10 hover:bg-white/20 text-white border border-white/20"
|
|
||||||
aria-label="Previous slide"
|
|
||||||
>
|
|
||||||
<ChevronLeft className="h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="w-12 h-12 rounded-full bg-white/10 hover:bg-white/20 text-white border border-white/20"
|
|
||||||
aria-label="Next slide"
|
|
||||||
>
|
|
||||||
<ChevronRight className="h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Button } from './ui/button';
|
|
||||||
import { ArrowRight, ChevronLeft, ChevronRight } from 'lucide-react';
|
|
||||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
|
||||||
import heroBannerImage from 'figma:asset/1bb9c22c86c0892d4716564b7135835f04869298.png';
|
|
||||||
|
|
||||||
interface HeroSectionProps {
|
|
||||||
showAnnouncementBar?: boolean;
|
|
||||||
announcementText?: string;
|
|
||||||
announcementCTA?: string;
|
|
||||||
title?: string;
|
|
||||||
subtitle?: string;
|
|
||||||
ctaText?: string;
|
|
||||||
onAnnouncementClick?: () => void;
|
|
||||||
onCTAClick?: () => void;
|
|
||||||
showFeatures?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function HeroSection({
|
|
||||||
showAnnouncementBar = true,
|
|
||||||
announcementText = "Join Our Upcoming Leadership Webinars - Transform Your Leadership Journey",
|
|
||||||
announcementCTA = "Enroll Now",
|
|
||||||
title = "Empowering Future-Ready Leaders",
|
|
||||||
subtitle = "Build confidence, agility, and clarity for today's complex challenges.",
|
|
||||||
ctaText = "Build Your Leadership Pipeline",
|
|
||||||
onAnnouncementClick,
|
|
||||||
onCTAClick,
|
|
||||||
showFeatures = true
|
|
||||||
}: HeroSectionProps) {
|
|
||||||
|
|
||||||
const handleAnnouncementClick = () => {
|
|
||||||
if (onAnnouncementClick) {
|
|
||||||
onAnnouncementClick();
|
|
||||||
} else {
|
|
||||||
window.location.href = '/webinars?view=individual';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCTAClick = () => {
|
|
||||||
if (onCTAClick) {
|
|
||||||
onCTAClick();
|
|
||||||
} else {
|
|
||||||
window.location.href = '/dashboard?view=individual';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative w-full">
|
|
||||||
{/* Top Announcement Bar */}
|
|
||||||
{showAnnouncementBar && (
|
|
||||||
<div className="bg-[#F8C301] text-[#26231A] py-3 px-4">
|
|
||||||
<div className="container mx-auto px-4 lg:px-8">
|
|
||||||
<div className="flex items-center justify-center gap-4 text-center">
|
|
||||||
<span className="text-base font-medium">
|
|
||||||
{announcementText}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
onClick={handleAnnouncementClick}
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="text-[#26231A] hover:bg-[#26231A]/10 font-medium text-base h-auto py-1 px-3"
|
|
||||||
>
|
|
||||||
{announcementCTA}
|
|
||||||
<ArrowRight className="ml-2 h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Main Hero Section */}
|
|
||||||
<div className="relative min-h-[80vh] flex items-center justify-center overflow-hidden">
|
|
||||||
{/* Background Image */}
|
|
||||||
<div className="absolute inset-0 z-0">
|
|
||||||
<ImageWithFallback
|
|
||||||
src={heroBannerImage}
|
|
||||||
alt="Leadership workshop with diverse team members collaborating with colorful sticky notes on a wall"
|
|
||||||
className="w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
{/* Overlay for better text readability */}
|
|
||||||
<div className="absolute inset-0 bg-black/40"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Hero Content */}
|
|
||||||
<div className="relative z-10 container mx-auto px-4 lg:px-8 pt-20 pb-32">
|
|
||||||
<div className="max-w-4xl">
|
|
||||||
<div className="text-white space-y-8">
|
|
||||||
{/* Main Heading */}
|
|
||||||
<h1 className="text-5xl lg:text-6xl font-bold leading-tight">
|
|
||||||
{title}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
{/* Subtext */}
|
|
||||||
<p className="text-xl lg:text-2xl text-white/90 leading-relaxed max-w-2xl">
|
|
||||||
{subtitle}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* CTA Button */}
|
|
||||||
<div className="pt-4">
|
|
||||||
<Button
|
|
||||||
onClick={handleCTAClick}
|
|
||||||
size="lg"
|
|
||||||
className="bg-[#04045B] hover:bg-[#04045B]/90 text-white text-lg px-8 py-4 min-h-[60px] font-medium"
|
|
||||||
>
|
|
||||||
{ctaText}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bottom Feature Navigation */}
|
|
||||||
{showFeatures && (
|
|
||||||
<div className="absolute bottom-0 left-0 right-0 z-10">
|
|
||||||
<div className="container mx-auto px-4 lg:px-8">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 py-12">
|
|
||||||
{/* Feature 01 */}
|
|
||||||
<div className="text-white space-y-4">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="text-[#F8C301] text-xl font-bold">01</div>
|
|
||||||
<div className="h-px bg-[#F8C301] flex-1"></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold mb-2">Leadership Is Learning. We Teach It Right.</h3>
|
|
||||||
<p className="text-white/80 text-base leading-relaxed">
|
|
||||||
Master proven methodologies and frameworks that transform managers into exceptional leaders.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Feature 02 */}
|
|
||||||
<div className="text-white space-y-4">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="text-[#F8C301] text-xl font-bold">02</div>
|
|
||||||
<div className="h-px bg-[#F8C301] flex-1"></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold mb-2">Turn Managers Into Impactful Leaders</h3>
|
|
||||||
<p className="text-white/80 text-base leading-relaxed">
|
|
||||||
Develop strategic thinking, emotional intelligence, and decision-making capabilities.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Feature 03 */}
|
|
||||||
<div className="text-white space-y-4">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="text-[#F8C301] text-xl font-bold">03</div>
|
|
||||||
<div className="h-px bg-[#F8C301] flex-1"></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold mb-2">Struggling with Managerial Gaps?</h3>
|
|
||||||
<p className="text-white/80 text-base leading-relaxed">
|
|
||||||
Bridge the gap between individual contribution and effective team leadership.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Navigation Controls */}
|
|
||||||
<div className="absolute bottom-6 right-6 flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="w-12 h-12 rounded-full bg-white/10 hover:bg-white/20 text-white border border-white/20"
|
|
||||||
aria-label="Previous slide"
|
|
||||||
>
|
|
||||||
<ChevronLeft className="h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="w-12 h-12 rounded-full bg-white/10 hover:bg-white/20 text-white border border-white/20"
|
|
||||||
aria-label="Next slide"
|
|
||||||
>
|
|
||||||
<ChevronRight className="h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
245
src/components/Leaderboard.tsx
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||||
|
import { Badge } from './ui/badge';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
||||||
|
import { Progress } from './ui/progress';
|
||||||
|
import { Trophy, Medal, Award, Crown, Star, Target } from 'lucide-react';
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
persona: 'corporate' | 'individual';
|
||||||
|
orgName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LeaderboardEntry {
|
||||||
|
rank: number;
|
||||||
|
userName: string;
|
||||||
|
exp: number;
|
||||||
|
milestone: number;
|
||||||
|
timeToMilestoneSec: number;
|
||||||
|
badges?: number;
|
||||||
|
avatar?: string;
|
||||||
|
isCurrentUser?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock data
|
||||||
|
const mockCurrentUser = {
|
||||||
|
rank: 7,
|
||||||
|
exp: 2450,
|
||||||
|
milestone: 3000,
|
||||||
|
timeToMilestoneSec: 86400 * 5, // 5 days
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockLeaderboard: LeaderboardEntry[] = [
|
||||||
|
{ rank: 1, userName: 'Rajesh Kumar', exp: 4850, milestone: 5000, timeToMilestoneSec: 86400 * 2, badges: 12, avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=64&h=64&fit=crop&crop=face' },
|
||||||
|
{ rank: 2, userName: 'Priya Sharma', exp: 4200, milestone: 5000, timeToMilestoneSec: 86400 * 8, badges: 10, avatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=64&h=64&fit=crop&crop=face' },
|
||||||
|
{ rank: 3, userName: 'Amit Singh', exp: 3950, milestone: 4000, timeToMilestoneSec: 86400 * 1, badges: 11, avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=64&h=64&fit=crop&crop=face' },
|
||||||
|
{ rank: 4, userName: 'Sneha Patel', exp: 3600, milestone: 4000, timeToMilestoneSec: 86400 * 4, badges: 9 },
|
||||||
|
{ rank: 5, userName: 'Vikram Rao', exp: 3250, milestone: 4000, timeToMilestoneSec: 86400 * 7, badges: 8 },
|
||||||
|
{ rank: 6, userName: 'Kavya Iyer', exp: 2800, milestone: 3000, timeToMilestoneSec: 86400 * 3, badges: 7 },
|
||||||
|
{ rank: 7, userName: 'You', exp: 2450, milestone: 3000, timeToMilestoneSec: 86400 * 5, badges: 6, isCurrentUser: true },
|
||||||
|
{ rank: 8, userName: 'Arjun Mehta', exp: 2200, milestone: 3000, timeToMilestoneSec: 86400 * 10, badges: 5 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockBadges = [
|
||||||
|
{ id: '1', name: 'First Course', iconUrl: '🎯', description: 'Complete your first course', earnedAt: '2024-01-01' },
|
||||||
|
{ id: '2', name: 'Quick Learner', iconUrl: '⚡', description: 'Complete a course in under 2 days', earnedAt: '2024-01-08' },
|
||||||
|
{ id: '3', name: 'Consistency Master', iconUrl: '🔥', description: 'Maintain a 14-day learning streak', earnedAt: '2024-01-15' },
|
||||||
|
{ id: '4', name: 'Knowledge Seeker', iconUrl: '📚', description: 'Complete 5 courses', earnedAt: '2024-01-20' },
|
||||||
|
{ id: '5', name: 'Mentor Material', iconUrl: '🧠', description: 'Help 10 peers through AI mentor', earnedAt: '2024-01-25' },
|
||||||
|
{ id: '6', name: 'Discussion Leader', iconUrl: '💬', description: 'Start 5 discussion threads', earnedAt: '2024-01-30' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export function Leaderboard({ user }: { user: User }) {
|
||||||
|
const [scope, setScope] = useState<string>(user.persona === 'corporate' ? 'cohort' : 'global');
|
||||||
|
|
||||||
|
const getRankIcon = (rank: number) => {
|
||||||
|
switch (rank) {
|
||||||
|
case 1: return <Crown className="h-5 w-5 text-yellow-500" />;
|
||||||
|
case 2: return <Medal className="h-5 w-5 text-gray-400" />;
|
||||||
|
case 3: return <Award className="h-5 w-5 text-amber-600" />;
|
||||||
|
default: return <span className="text-sm font-medium w-5 text-center">#{rank}</span>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTimeToMilestone = (seconds: number) => {
|
||||||
|
const days = Math.floor(seconds / 86400);
|
||||||
|
if (days === 0) return 'Today';
|
||||||
|
if (days === 1) return '1 day';
|
||||||
|
return `${days} days`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getScopeLabel = () => {
|
||||||
|
switch (scope) {
|
||||||
|
case 'cohort': return 'Cohort Leaderboard';
|
||||||
|
case 'org': return 'Organization Leaderboard';
|
||||||
|
case 'global': return 'Global Leaderboard';
|
||||||
|
default: return 'Leaderboard';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const progressToNextMilestone = (mockCurrentUser.exp / mockCurrentUser.milestone) * 100;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 space-y-6 bg-white min-h-screen">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-medium">Leaderboard</h1>
|
||||||
|
<p className="text-gray-600">Track your progress and compete with peers</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{user.persona === 'corporate' && (
|
||||||
|
<Select value={scope} onValueChange={setScope}>
|
||||||
|
<SelectTrigger className="w-48 bg-white">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="cohort">Cohort</SelectItem>
|
||||||
|
<SelectItem value="org">Organization</SelectItem>
|
||||||
|
<SelectItem value="global">Global</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Current User Progress */}
|
||||||
|
<Card className="bg-gradient-to-r from-[var(--color-brand-primary)] to-blue-600 text-white border-0">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-blue-100">Your Rank</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{getRankIcon(mockCurrentUser.rank)}
|
||||||
|
<span className="text-2xl font-bold">#{mockCurrentUser.rank}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-blue-100">Experience Points</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<span className="text-2xl font-bold">{mockCurrentUser.exp.toLocaleString()} XP</span>
|
||||||
|
<Progress
|
||||||
|
value={progressToNextMilestone}
|
||||||
|
className="bg-blue-400/30 h-2"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-blue-100">
|
||||||
|
{mockCurrentUser.milestone - mockCurrentUser.exp} XP to next milestone
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-blue-100">Time to Milestone</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Target className="h-5 w-5 text-blue-200" />
|
||||||
|
<span className="text-xl font-bold">
|
||||||
|
{formatTimeToMilestone(mockCurrentUser.timeToMilestoneSec)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Badges Section */}
|
||||||
|
<Card className="bg-white border">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Star className="h-5 w-5 text-[var(--color-brand-accent)]" />
|
||||||
|
Your Badges ({mockBadges.length})
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||||
|
{mockBadges.map((badge) => (
|
||||||
|
<div key={badge.id} className="text-center space-y-2 p-3 rounded-lg bg-gray-50">
|
||||||
|
<div className="text-2xl">{badge.iconUrl}</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-sm">{badge.name}</p>
|
||||||
|
<p className="text-xs text-gray-600">{badge.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Leaderboard */}
|
||||||
|
<Card className="bg-white border">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{getScopeLabel()}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{mockLeaderboard.slice(0, 20).map((entry) => (
|
||||||
|
<div
|
||||||
|
key={entry.rank}
|
||||||
|
className={`flex items-center gap-4 p-4 rounded-lg transition-colors ${
|
||||||
|
entry.isCurrentUser
|
||||||
|
? 'bg-[var(--color-brand-primary)]/10 border border-[var(--color-brand-primary)]/20'
|
||||||
|
: 'bg-gray-50 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center w-8">
|
||||||
|
{getRankIcon(entry.rank)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Avatar className="h-10 w-10">
|
||||||
|
{entry.avatar && <AvatarImage src={entry.avatar} alt={entry.userName} />}
|
||||||
|
<AvatarFallback>{entry.userName.charAt(0)}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className={`font-medium ${entry.isCurrentUser ? 'text-[var(--color-brand-primary)]' : ''}`}>
|
||||||
|
{entry.userName}
|
||||||
|
{entry.isCurrentUser && <span className="ml-2 text-sm text-gray-600">(You)</span>}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
{formatTimeToMilestone(entry.timeToMilestoneSec)} to milestone
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-right space-y-1">
|
||||||
|
<p className="font-bold text-lg">{entry.exp.toLocaleString()}</p>
|
||||||
|
<p className="text-xs text-gray-600">XP</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{entry.badges && (
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{entry.badges} badges
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Current user position if outside top 20 */}
|
||||||
|
{mockCurrentUser.rank > 20 && (
|
||||||
|
<div className="mt-6 pt-4 border-t">
|
||||||
|
<div className="flex items-center gap-4 p-4 rounded-lg bg-[var(--color-brand-primary)]/10 border border-[var(--color-brand-primary)]/20">
|
||||||
|
<div className="flex items-center justify-center w-8">
|
||||||
|
<span className="text-sm font-medium">#{mockCurrentUser.rank}</span>
|
||||||
|
</div>
|
||||||
|
<Avatar className="h-10 w-10">
|
||||||
|
<AvatarFallback>You</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="font-medium text-[var(--color-brand-primary)]">You</p>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
{formatTimeToMilestone(mockCurrentUser.timeToMilestoneSec)} to milestone
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="font-bold text-lg">{mockCurrentUser.exp.toLocaleString()}</p>
|
||||||
|
<p className="text-xs text-gray-600">XP</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
441
src/components/Library.tsx
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Search,
|
||||||
|
Filter,
|
||||||
|
Grid3X3,
|
||||||
|
List,
|
||||||
|
Clock,
|
||||||
|
Star,
|
||||||
|
Users,
|
||||||
|
Play,
|
||||||
|
Book,
|
||||||
|
Video,
|
||||||
|
FileText,
|
||||||
|
Headphones,
|
||||||
|
Download
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
import { Input } from './ui/input';
|
||||||
|
import { Badge } from './ui/badge';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue
|
||||||
|
} from './ui/select';
|
||||||
|
import { Checkbox } from './ui/checkbox';
|
||||||
|
import { Separator } from './ui/separator';
|
||||||
|
import { AspectRatio } from './ui/aspect-ratio';
|
||||||
|
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||||
|
import { useAppContext } from './AppShell';
|
||||||
|
|
||||||
|
// Mock data for library resources
|
||||||
|
const mockLibraryData = {
|
||||||
|
courses: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
title: 'Strategic Leadership Development',
|
||||||
|
programName: 'Executive Leadership Programme',
|
||||||
|
type: 'course',
|
||||||
|
level: 'Advanced',
|
||||||
|
duration: '8 weeks',
|
||||||
|
rating: 4.8,
|
||||||
|
ratingCount: 156,
|
||||||
|
currentLearners: 89,
|
||||||
|
progress: 65,
|
||||||
|
instructor: 'Dr. Rajesh Kumar',
|
||||||
|
thumbnail: 'https://images.unsplash.com/photo-1552664730-d307ca884978?w=400',
|
||||||
|
status: 'in-progress',
|
||||||
|
tags: ['Leadership', 'Strategy', 'Management']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
title: 'Financial Management for Leaders',
|
||||||
|
programName: 'Leadership Foundation Programme',
|
||||||
|
type: 'course',
|
||||||
|
level: 'Intermediate',
|
||||||
|
duration: '6 weeks',
|
||||||
|
rating: 4.6,
|
||||||
|
ratingCount: 203,
|
||||||
|
currentLearners: 67,
|
||||||
|
progress: 0,
|
||||||
|
instructor: 'Prof. Priya Sharma',
|
||||||
|
thumbnail: 'https://images.unsplash.com/photo-1554224155-6726b3ff858f?w=400',
|
||||||
|
status: 'available',
|
||||||
|
tags: ['Finance', 'Leadership', 'Analytics']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
title: 'Team Building Essentials',
|
||||||
|
programName: 'Management Skills Programme',
|
||||||
|
type: 'course',
|
||||||
|
level: 'Beginner',
|
||||||
|
duration: '4 weeks',
|
||||||
|
rating: 4.9,
|
||||||
|
ratingCount: 89,
|
||||||
|
currentLearners: 134,
|
||||||
|
progress: 100,
|
||||||
|
instructor: 'Mr. Arjun Mehta',
|
||||||
|
thumbnail: 'https://images.unsplash.com/photo-1522202176988-66273c2fd55f?w=400',
|
||||||
|
status: 'completed',
|
||||||
|
tags: ['Team Building', 'Communication', 'Leadership']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
webinars: [
|
||||||
|
{
|
||||||
|
id: 'w1',
|
||||||
|
title: 'Future of Leadership in Digital Age',
|
||||||
|
type: 'webinar',
|
||||||
|
date: '2024-09-15T14:00:00Z',
|
||||||
|
duration: '90 minutes',
|
||||||
|
speaker: 'Dr. Anita Singh',
|
||||||
|
attendees: 245,
|
||||||
|
status: 'upcoming',
|
||||||
|
thumbnail: 'https://images.unsplash.com/photo-1560472354-b33ff0c44a43?w=400',
|
||||||
|
tags: ['Digital Transformation', 'Future Trends']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
resources: [
|
||||||
|
{
|
||||||
|
id: 'r1',
|
||||||
|
title: 'Leadership Assessment Framework',
|
||||||
|
type: 'document',
|
||||||
|
fileType: 'PDF',
|
||||||
|
size: '2.3 MB',
|
||||||
|
downloads: 456,
|
||||||
|
thumbnail: 'https://images.unsplash.com/photo-1586953208448-b95a79798f07?w=400',
|
||||||
|
tags: ['Assessment', 'Framework']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'r2',
|
||||||
|
title: 'Decision Making Podcast Series',
|
||||||
|
type: 'audio',
|
||||||
|
duration: '45 minutes',
|
||||||
|
episodes: 8,
|
||||||
|
thumbnail: 'https://images.unsplash.com/photo-1478737270239-2f02b77fc618?w=400',
|
||||||
|
tags: ['Decision Making', 'Podcast']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Library() {
|
||||||
|
const { user } = useAppContext();
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
||||||
|
const [selectedFilters, setSelectedFilters] = useState({
|
||||||
|
type: 'all',
|
||||||
|
level: 'all',
|
||||||
|
status: 'all',
|
||||||
|
tags: [] as string[]
|
||||||
|
});
|
||||||
|
|
||||||
|
const allResources = [
|
||||||
|
...mockLibraryData.courses,
|
||||||
|
...mockLibraryData.webinars,
|
||||||
|
...mockLibraryData.resources
|
||||||
|
];
|
||||||
|
|
||||||
|
const getResourceIcon = (type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'course': return Book;
|
||||||
|
case 'webinar': return Video;
|
||||||
|
case 'document': return FileText;
|
||||||
|
case 'audio': return Headphones;
|
||||||
|
default: return Book;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'completed': return 'bg-green-100 text-green-800';
|
||||||
|
case 'in-progress': return 'bg-blue-100 text-blue-800';
|
||||||
|
case 'available': return 'bg-gray-100 text-gray-800';
|
||||||
|
case 'upcoming': return 'bg-yellow-100 text-yellow-800';
|
||||||
|
default: return 'bg-gray-100 text-gray-800';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ResourceCard = ({ resource }: { resource: any }) => {
|
||||||
|
const Icon = getResourceIcon(resource.type);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="overflow-hidden hover:shadow-md transition-shadow">
|
||||||
|
<div className="relative">
|
||||||
|
<AspectRatio ratio={16 / 9}>
|
||||||
|
<ImageWithFallback
|
||||||
|
src={resource.thumbnail}
|
||||||
|
alt={resource.title}
|
||||||
|
className="object-cover w-full h-full"
|
||||||
|
/>
|
||||||
|
</AspectRatio>
|
||||||
|
<div className="absolute top-2 left-2">
|
||||||
|
<Badge variant="secondary" className="capitalize">
|
||||||
|
<Icon className="h-3 w-3 mr-1" />
|
||||||
|
{resource.type}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
{resource.status && (
|
||||||
|
<div className="absolute top-2 right-2">
|
||||||
|
<Badge className={`capitalize ${getStatusColor(resource.status)}`}>
|
||||||
|
{resource.status.replace('-', ' ')}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CardContent className="p-4 space-y-3">
|
||||||
|
{resource.programName && (
|
||||||
|
<p className="text-xs text-muted-foreground">{resource.programName}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<h3 className="font-medium line-clamp-2">{resource.title}</h3>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{resource.tags?.slice(0, 2).map((tag: string) => (
|
||||||
|
<Badge key={tag} variant="outline" className="text-xs">
|
||||||
|
{tag}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
||||||
|
{resource.duration && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Clock className="h-3 w-3" />
|
||||||
|
{resource.duration}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{resource.rating && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Star className="h-3 w-3 fill-yellow-400 text-yellow-400" />
|
||||||
|
{resource.rating} ({resource.ratingCount})
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{resource.currentLearners && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Users className="h-3 w-3" />
|
||||||
|
{resource.currentLearners}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{resource.progress !== undefined && resource.progress > 0 && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex justify-between text-xs">
|
||||||
|
<span>Progress</span>
|
||||||
|
<span>{resource.progress}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-gray-200 rounded-full h-1.5">
|
||||||
|
<div
|
||||||
|
className="bg-[#04045B] h-1.5 rounded-full"
|
||||||
|
style={{ width: `${resource.progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-2 pt-2">
|
||||||
|
{resource.status === 'completed' ? (
|
||||||
|
<Button variant="outline" size="sm" className="flex-1">
|
||||||
|
Recap Course
|
||||||
|
</Button>
|
||||||
|
) : resource.type === 'course' ? (
|
||||||
|
<Button size="sm" className="flex-1">
|
||||||
|
<Play className="h-3 w-3 mr-1" />
|
||||||
|
{resource.progress > 0 ? 'Continue' : 'Start Course'}
|
||||||
|
</Button>
|
||||||
|
) : resource.type === 'document' ? (
|
||||||
|
<Button size="sm" className="flex-1">
|
||||||
|
<Download className="h-3 w-3 mr-1" />
|
||||||
|
Download
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button size="sm" className="flex-1">
|
||||||
|
View
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-medium">Library</h1>
|
||||||
|
<p className="text-muted-foreground">Explore courses, webinars, and resources</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant={viewMode === 'grid' ? 'default' : 'outline'}
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setViewMode('grid')}
|
||||||
|
>
|
||||||
|
<Grid3X3 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={viewMode === 'list' ? 'default' : 'outline'}
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setViewMode('list')}
|
||||||
|
>
|
||||||
|
<List className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-6">
|
||||||
|
{/* Filters Sidebar */}
|
||||||
|
<div className="w-64 space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">Filters</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{/* Search */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Search</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search library..."
|
||||||
|
className="pl-10"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* Resource Type */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Type</label>
|
||||||
|
<Select value={selectedFilters.type} onValueChange={(value) =>
|
||||||
|
setSelectedFilters(prev => ({ ...prev, type: value }))
|
||||||
|
}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All Types</SelectItem>
|
||||||
|
<SelectItem value="course">Courses</SelectItem>
|
||||||
|
<SelectItem value="webinar">Webinars</SelectItem>
|
||||||
|
<SelectItem value="document">Documents</SelectItem>
|
||||||
|
<SelectItem value="audio">Audio</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Level */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Level</label>
|
||||||
|
<Select value={selectedFilters.level} onValueChange={(value) =>
|
||||||
|
setSelectedFilters(prev => ({ ...prev, level: value }))
|
||||||
|
}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All Levels</SelectItem>
|
||||||
|
<SelectItem value="beginner">Beginner</SelectItem>
|
||||||
|
<SelectItem value="intermediate">Intermediate</SelectItem>
|
||||||
|
<SelectItem value="advanced">Advanced</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Status</label>
|
||||||
|
<Select value={selectedFilters.status} onValueChange={(value) =>
|
||||||
|
setSelectedFilters(prev => ({ ...prev, status: value }))
|
||||||
|
}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All Status</SelectItem>
|
||||||
|
<SelectItem value="available">Available</SelectItem>
|
||||||
|
<SelectItem value="in-progress">In Progress</SelectItem>
|
||||||
|
<SelectItem value="completed">Completed</SelectItem>
|
||||||
|
<SelectItem value="upcoming">Upcoming</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Popular Tags */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Popular Tags</label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{['Leadership', 'Strategy', 'Finance', 'Communication', 'Team Building'].map((tag) => (
|
||||||
|
<div key={tag} className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id={tag}
|
||||||
|
checked={selectedFilters.tags.includes(tag)}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
if (checked) {
|
||||||
|
setSelectedFilters(prev => ({
|
||||||
|
...prev,
|
||||||
|
tags: [...prev.tags, tag]
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
setSelectedFilters(prev => ({
|
||||||
|
...prev,
|
||||||
|
tags: prev.tags.filter(t => t !== tag)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<label htmlFor={tag} className="text-sm">{tag}</label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="flex-1 space-y-6">
|
||||||
|
{/* Results Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Showing {allResources.length} results
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Select defaultValue="relevance">
|
||||||
|
<SelectTrigger className="w-48">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="relevance">Sort by Relevance</SelectItem>
|
||||||
|
<SelectItem value="newest">Newest First</SelectItem>
|
||||||
|
<SelectItem value="rating">Highest Rated</SelectItem>
|
||||||
|
<SelectItem value="popular">Most Popular</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Resource Grid */}
|
||||||
|
<div className={`grid gap-6 ${
|
||||||
|
viewMode === 'grid'
|
||||||
|
? 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3'
|
||||||
|
: 'grid-cols-1'
|
||||||
|
}`}>
|
||||||
|
{allResources.map((resource) => (
|
||||||
|
<ResourceCard key={resource.id} resource={resource} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
1188
src/components/MyCourses.tsx
Normal file
@@ -1,709 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { Button } from './ui/button';
|
|
||||||
import { Input } from './ui/input';
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
|
||||||
import { Badge } from './ui/badge';
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
} from './ui/dropdown-menu';
|
|
||||||
import {
|
|
||||||
Sheet,
|
|
||||||
SheetContent,
|
|
||||||
SheetHeader,
|
|
||||||
SheetTitle,
|
|
||||||
SheetTrigger,
|
|
||||||
} from './ui/sheet';
|
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible';
|
|
||||||
import { navigate } from './Router';
|
|
||||||
import { useAuth } from './AuthContext';
|
|
||||||
import klcLogo from 'figma:asset/209958db0c439ec78be82ab4f3e335a6aed5de89.png';
|
|
||||||
import exampleImage from 'figma:asset/6cae567b6bf6a44cb03b767e4308c4c705340d08.png';
|
|
||||||
import {
|
|
||||||
Menu,
|
|
||||||
ChevronDown,
|
|
||||||
ChevronRight,
|
|
||||||
ShoppingCart,
|
|
||||||
Search,
|
|
||||||
Building2,
|
|
||||||
User,
|
|
||||||
Settings,
|
|
||||||
LogOut,
|
|
||||||
LayoutDashboard,
|
|
||||||
Users,
|
|
||||||
Target,
|
|
||||||
Award,
|
|
||||||
Lightbulb,
|
|
||||||
GraduationCap,
|
|
||||||
BookOpen,
|
|
||||||
Video,
|
|
||||||
FileText,
|
|
||||||
Eye,
|
|
||||||
Heart,
|
|
||||||
MapPin,
|
|
||||||
Calendar,
|
|
||||||
Play,
|
|
||||||
Home,
|
|
||||||
Check,
|
|
||||||
ArrowRight
|
|
||||||
} from 'lucide-react';
|
|
||||||
|
|
||||||
interface NavigationProps {
|
|
||||||
currentPage?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Navigation({ currentPage }: NavigationProps) {
|
|
||||||
const [isScrolled, setIsScrolled] = useState(false);
|
|
||||||
const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
|
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
|
||||||
const [expandedMobileSection, setExpandedMobileSection] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const { user, login, signOut, isAuthenticated } = useAuth();
|
|
||||||
|
|
||||||
// Determine user type from URL or user data
|
|
||||||
const getQueryParam = (param: string) => {
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
return urlParams.get(param);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isIndividualUser = getQueryParam('view') === 'individual' ||
|
|
||||||
(!getQueryParam('view') && currentPage?.includes('/dashboard')) ||
|
|
||||||
(!getQueryParam('view') && currentPage?.includes('/library')) ||
|
|
||||||
(!getQueryParam('view') && currentPage?.includes('/course')) ||
|
|
||||||
(!getQueryParam('view') && currentPage?.includes('/settings')) ||
|
|
||||||
(!getQueryParam('view') && currentPage?.includes('/surveys')) ||
|
|
||||||
(!getQueryParam('view') && currentPage?.includes('/webinars')) ||
|
|
||||||
(!getQueryParam('view') && currentPage?.includes('/leaderboard'));
|
|
||||||
|
|
||||||
const isCorporateUser = getQueryParam('view') === 'corporate';
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleScroll = () => {
|
|
||||||
setIsScrolled(window.scrollY > 10);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('scroll', handleScroll);
|
|
||||||
return () => window.removeEventListener('scroll', handleScroll);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
const target = event.target as Element;
|
|
||||||
if (!target.closest('[data-dropdown]')) {
|
|
||||||
setActiveDropdown(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('click', handleClickOutside);
|
|
||||||
return () => document.removeEventListener('click', handleClickOutside);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDropdownToggle = (dropdown: string) => {
|
|
||||||
setActiveDropdown(activeDropdown === dropdown ? null : dropdown);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMobileToggle = (section: string) => {
|
|
||||||
setExpandedMobileSection(expandedMobileSection === section ? null : section);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLogin = () => {
|
|
||||||
navigate('/auth'); // Route to login selection page
|
|
||||||
setIsMobileMenuOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSignup = () => {
|
|
||||||
navigate('/signup');
|
|
||||||
setIsMobileMenuOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLogout = () => {
|
|
||||||
signOut();
|
|
||||||
navigate('/');
|
|
||||||
setActiveDropdown(null);
|
|
||||||
setIsMobileMenuOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAccountSignIn = (accountType: 'individual' | 'corporate') => {
|
|
||||||
// Navigate to appropriate sign-in page for the account type
|
|
||||||
if (accountType === 'individual') {
|
|
||||||
navigate('/login');
|
|
||||||
} else {
|
|
||||||
navigate('/corporate/login');
|
|
||||||
}
|
|
||||||
setActiveDropdown(null);
|
|
||||||
setIsMobileMenuOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const navigationItems = [
|
|
||||||
{
|
|
||||||
title: 'About Us',
|
|
||||||
href: '/about-us/our-vision',
|
|
||||||
items: [
|
|
||||||
{ title: 'Our Vision', href: '/about-us/our-vision', icon: Eye },
|
|
||||||
{ title: 'Our Team', href: '/about-us/our-team', icon: Users },
|
|
||||||
{ title: 'Our Impact', href: '/about-us/our-impact', icon: Target },
|
|
||||||
{ title: 'Our Expertise', href: '/about-us/our-expertise', icon: Award }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Programmes',
|
|
||||||
href: '/programmes',
|
|
||||||
items: [
|
|
||||||
{ title: 'Programme Catalogue', href: '/programmes', icon: BookOpen },
|
|
||||||
{ title: 'Executive Leadership', href: '/programmes/executive-leadership', icon: Award },
|
|
||||||
{ title: 'Team Leadership', href: '/programmes/team-leadership', icon: Users },
|
|
||||||
{ title: 'Innovation Leadership', href: '/programmes/innovation-leadership', icon: Lightbulb },
|
|
||||||
{ title: 'Leadership Online', href: '/programmes/leadership-online', icon: Play }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Services',
|
|
||||||
href: '/services/leadership-development',
|
|
||||||
items: [
|
|
||||||
{ title: 'Leadership Development', href: '/services/leadership-development', icon: Target },
|
|
||||||
{ title: 'Management Development', href: '/services/management-development', icon: Users },
|
|
||||||
{ title: 'Executive Coaching', href: '/services/executive-coaching', icon: Award },
|
|
||||||
{ title: 'Culture & Competence', href: '/services/culture-competence', icon: Heart },
|
|
||||||
{ title: 'Consulting', href: '/services/consulting', icon: Lightbulb },
|
|
||||||
{ title: 'Learning Facility', href: '/services/learning-facility', icon: MapPin }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Learning',
|
|
||||||
href: '/learning/articles',
|
|
||||||
items: [
|
|
||||||
{ title: 'Articles', href: '/learning/articles', icon: FileText },
|
|
||||||
{ title: 'Blog', href: '/learning/blog', icon: BookOpen },
|
|
||||||
{ title: 'Resources', href: '/learning/resources', icon: BookOpen },
|
|
||||||
{ title: 'Webinars', href: '/individual-webinars', icon: Video }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const learnerMenuItems = [
|
|
||||||
{
|
|
||||||
title: 'Dashboard',
|
|
||||||
href: isIndividualUser ? '/dashboard?view=individual' : '/dashboard?view=corporate',
|
|
||||||
icon: LayoutDashboard,
|
|
||||||
description: isIndividualUser ? 'Your learning overview' : 'Team management hub'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Library',
|
|
||||||
href: isIndividualUser ? '/library?view=individual' : '/library?view=corporate',
|
|
||||||
icon: BookOpen,
|
|
||||||
description: isIndividualUser ? 'Browse courses' : 'Assigned courses'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Course Timeline',
|
|
||||||
href: isIndividualUser ? '/course?view=individual' : '/course?view=corporate',
|
|
||||||
icon: Calendar,
|
|
||||||
description: isIndividualUser ? 'Your learning path' : 'Team progress'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Surveys & Assessments',
|
|
||||||
href: isIndividualUser ? '/surveys?view=individual' : '/surveys?view=corporate',
|
|
||||||
icon: FileText,
|
|
||||||
description: isIndividualUser ? 'Complete assessments' : 'Team evaluations'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Live Webinars',
|
|
||||||
href: isIndividualUser ? '/webinars?view=individual' : '/webinars?view=corporate',
|
|
||||||
icon: Video,
|
|
||||||
description: isIndividualUser ? 'Join sessions' : 'Corporate events'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Leaderboard',
|
|
||||||
href: isIndividualUser ? '/leaderboard?view=individual' : '/leaderboard?view=corporate',
|
|
||||||
icon: Award,
|
|
||||||
description: isIndividualUser ? 'Your achievements' : 'Team rankings'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Settings',
|
|
||||||
href: isIndividualUser ? '/settings?view=individual' : '/settings?view=corporate',
|
|
||||||
icon: Settings,
|
|
||||||
description: isIndividualUser ? 'Account preferences' : 'Admin settings'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// Mock data for demonstration - replace with actual user data
|
|
||||||
const currentUser = {
|
|
||||||
name: 'Priya Sharma',
|
|
||||||
email: 'priya.sharma@example.com',
|
|
||||||
avatar: "https://images.unsplash.com/photo-1494790108755-2616b612b786?w=150&h=150&fit=crop&crop=face",
|
|
||||||
organization: 'TechCorp Inc.',
|
|
||||||
role: 'Marketing Team Member'
|
|
||||||
};
|
|
||||||
|
|
||||||
const availableAccounts = [
|
|
||||||
{
|
|
||||||
type: 'individual',
|
|
||||||
isActive: isIndividualUser,
|
|
||||||
title: 'Personal Learning',
|
|
||||||
subtitle: 'Access your individual learning portal',
|
|
||||||
icon: User,
|
|
||||||
user: currentUser
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'corporate',
|
|
||||||
isActive: isCorporateUser,
|
|
||||||
title: 'Corporate Learning',
|
|
||||||
subtitle: 'Enterprise team development portal',
|
|
||||||
icon: Building2,
|
|
||||||
user: currentUser,
|
|
||||||
organization: 'TechCorp Inc.'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<nav className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
|
|
||||||
isScrolled ? 'bg-white/95 backdrop-blur-md shadow-sm' : 'bg-white'
|
|
||||||
}`}>
|
|
||||||
<div className="w-full px-4 lg:px-8">
|
|
||||||
<div className="flex items-center justify-between h-[70px]">
|
|
||||||
{/* Logo */}
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<button
|
|
||||||
onClick={() => navigate('/')}
|
|
||||||
className="flex items-center space-x-2 focus:outline-none focus:ring-2 focus:ring-primary rounded-lg p-1 hover:bg-gray-50 transition-colors"
|
|
||||||
aria-label="Go to KLC homepage"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={klcLogo}
|
|
||||||
alt="Kautilya Leadership Centre"
|
|
||||||
className="h-12 w-auto object-contain"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Desktop Navigation */}
|
|
||||||
<div className="hidden lg:flex items-center space-x-8">
|
|
||||||
{navigationItems.map((item) => (
|
|
||||||
<div key={item.title} className="relative" data-dropdown>
|
|
||||||
<button
|
|
||||||
onClick={() => handleDropdownToggle(item.title)}
|
|
||||||
className="flex items-center space-x-1 text-[16px] text-foreground hover:text-primary transition-colors py-2 px-3 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-primary"
|
|
||||||
aria-expanded={activeDropdown === item.title}
|
|
||||||
aria-haspopup="true"
|
|
||||||
>
|
|
||||||
<span>{item.title}</span>
|
|
||||||
<ChevronDown className={`h-4 w-4 transition-transform ${
|
|
||||||
activeDropdown === item.title ? 'rotate-180' : ''
|
|
||||||
}`} />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{activeDropdown === item.title && (
|
|
||||||
<div className="absolute top-full left-0 mt-2 w-64 bg-white rounded-lg shadow-lg border border-gray-200 py-2 z-50">
|
|
||||||
{item.items.map((subItem) => {
|
|
||||||
const IconComponent = subItem.icon;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={subItem.title}
|
|
||||||
onClick={() => {
|
|
||||||
navigate(subItem.href);
|
|
||||||
setActiveDropdown(null);
|
|
||||||
}}
|
|
||||||
className="w-full flex items-center space-x-3 px-4 py-3 text-[16px] text-gray-700 hover:bg-gray-50 hover:text-primary transition-colors text-left focus:outline-none focus:ring-2 focus:ring-primary"
|
|
||||||
>
|
|
||||||
<IconComponent className="h-4 w-4 flex-shrink-0" />
|
|
||||||
<span>{subItem.title}</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => navigate('/contact')}
|
|
||||||
className="text-[16px] text-foreground hover:text-primary transition-colors py-2 px-3 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-primary"
|
|
||||||
>
|
|
||||||
Contact
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Section - Desktop */}
|
|
||||||
<div className="hidden lg:flex items-center space-x-4">
|
|
||||||
{!isAuthenticated ? (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={handleLogin}
|
|
||||||
className="text-[16px] min-h-[44px]"
|
|
||||||
>
|
|
||||||
Sign In
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleSignup}
|
|
||||||
className="text-[16px] min-h-[44px] bg-primary hover:bg-primary/90 text-primary-foreground"
|
|
||||||
>
|
|
||||||
Get Started
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
{/* Redesigned User Profile Dropdown */}
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="flex items-center gap-3 h-auto p-2 hover:bg-gray-50 transition-all duration-200 rounded-lg min-h-[44px]"
|
|
||||||
aria-label="Open user menu"
|
|
||||||
>
|
|
||||||
<Avatar className="h-8 w-8">
|
|
||||||
<AvatarImage
|
|
||||||
src={currentUser.avatar}
|
|
||||||
alt={currentUser.name}
|
|
||||||
/>
|
|
||||||
<AvatarFallback className="bg-primary/10 text-primary text-sm">
|
|
||||||
{currentUser.name.split(' ').map(n => n[0]).join('')}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div className="flex flex-col items-start min-w-0">
|
|
||||||
<span className="text-[16px] font-medium text-gray-900 truncate">
|
|
||||||
{currentUser.name}
|
|
||||||
</span>
|
|
||||||
<span className="text-[14px] text-gray-600 truncate">
|
|
||||||
{isIndividualUser ? 'Individual Account' : 'Corporate Account'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<ChevronDown className="h-4 w-4 text-gray-500 flex-shrink-0" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent className="w-80 p-0" align="end" forceMount>
|
|
||||||
{/* Header Section */}
|
|
||||||
<div className="p-4 border-b border-gray-100 bg-gray-50">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Avatar className="h-12 w-12">
|
|
||||||
<AvatarImage
|
|
||||||
src={currentUser.avatar}
|
|
||||||
alt={currentUser.name}
|
|
||||||
/>
|
|
||||||
<AvatarFallback className="bg-primary/10 text-primary text-lg">
|
|
||||||
{currentUser.name.split(' ').map(n => n[0]).join('')}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<p className="text-[16px] font-semibold text-gray-900 truncate">
|
|
||||||
{currentUser.name}
|
|
||||||
</p>
|
|
||||||
<ChevronDown className="h-4 w-4 text-gray-400 flex-shrink-0" />
|
|
||||||
</div>
|
|
||||||
<p className="text-[14px] text-gray-600 truncate">
|
|
||||||
{isIndividualUser ? 'Individual Account' : 'Corporate Account'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Account Switching Section */}
|
|
||||||
<div className="p-4 border-b border-gray-100">
|
|
||||||
<h4 className="text-[14px] font-medium text-gray-900 mb-3">Switch Account</h4>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{availableAccounts.map((account) => {
|
|
||||||
const IconComponent = account.icon;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={account.type}
|
|
||||||
className={`flex items-center gap-3 p-3 rounded-lg border transition-all duration-200 ${
|
|
||||||
account.isActive
|
|
||||||
? 'bg-green-50 border-green-200'
|
|
||||||
: 'bg-gray-50 border-gray-200 hover:bg-gray-100 cursor-pointer'
|
|
||||||
}`}
|
|
||||||
onClick={() => !account.isActive && handleAccountSignIn(account.type as 'individual' | 'corporate')}
|
|
||||||
>
|
|
||||||
<div className="relative">
|
|
||||||
{account.type === 'individual' ? (
|
|
||||||
<Avatar className="h-8 w-8">
|
|
||||||
<AvatarImage
|
|
||||||
src={currentUser.avatar}
|
|
||||||
alt={currentUser.name}
|
|
||||||
/>
|
|
||||||
<AvatarFallback className="bg-blue-100 text-blue-700 text-sm">
|
|
||||||
<User className="h-4 w-4" />
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
) : (
|
|
||||||
<div className="h-8 w-8 bg-purple-100 rounded-full flex items-center justify-center">
|
|
||||||
<Building2 className="h-4 w-4 text-purple-700" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{account.isActive && (
|
|
||||||
<div className="absolute -top-1 -right-1 w-4 h-4 bg-green-500 rounded-full flex items-center justify-center">
|
|
||||||
<Check className="h-2.5 w-2.5 text-white" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p className="text-[14px] font-medium text-gray-900 truncate">
|
|
||||||
{account.title}
|
|
||||||
</p>
|
|
||||||
<p className="text-[14px] text-gray-600 truncate">
|
|
||||||
{account.subtitle}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{!account.isActive && (
|
|
||||||
<ArrowRight className="h-4 w-4 text-gray-400 flex-shrink-0" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
|
|
||||||
{/* Settings and Logout */}
|
|
||||||
<div className="p-2">
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="flex items-center gap-3 px-3 py-2 cursor-pointer rounded-md hover:bg-gray-50 focus:bg-gray-50 min-h-[44px]"
|
|
||||||
onClick={() => navigate(learnerMenuItems[learnerMenuItems.length - 1].href)}
|
|
||||||
>
|
|
||||||
<Settings className="h-5 w-5 text-gray-500" />
|
|
||||||
<span className="text-[16px] font-medium text-gray-900">Settings</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="flex items-center gap-3 px-3 py-2 cursor-pointer rounded-md text-red-600 hover:bg-red-50 focus:bg-red-50 min-h-[44px]"
|
|
||||||
onClick={handleLogout}
|
|
||||||
>
|
|
||||||
<LogOut className="h-5 w-5" />
|
|
||||||
<span className="text-[16px] font-medium">Sign Out</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mobile Menu Button */}
|
|
||||||
<div className="lg:hidden">
|
|
||||||
<Sheet open={isMobileMenuOpen} onOpenChange={setIsMobileMenuOpen}>
|
|
||||||
<SheetTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-10 w-10"
|
|
||||||
aria-label="Open mobile menu"
|
|
||||||
>
|
|
||||||
<Menu className="h-6 w-6" />
|
|
||||||
</Button>
|
|
||||||
</SheetTrigger>
|
|
||||||
<SheetContent side="right" className="w-full sm:w-96 p-0">
|
|
||||||
<SheetHeader className="p-6 border-b border-gray-200">
|
|
||||||
<SheetTitle className="text-left flex items-center gap-3">
|
|
||||||
<img
|
|
||||||
src={klcLogo}
|
|
||||||
alt="KLC"
|
|
||||||
className="h-8 w-auto object-contain"
|
|
||||||
/>
|
|
||||||
<span className="text-lg font-semibold">Menu</span>
|
|
||||||
</SheetTitle>
|
|
||||||
</SheetHeader>
|
|
||||||
|
|
||||||
<div className="flex flex-col h-full">
|
|
||||||
{/* User Section for Mobile */}
|
|
||||||
{isAuthenticated && (
|
|
||||||
<div className="p-4 border-b border-gray-200">
|
|
||||||
<div className="flex items-center gap-3 mb-4">
|
|
||||||
<Avatar className="h-12 w-12">
|
|
||||||
<AvatarImage
|
|
||||||
src={currentUser.avatar}
|
|
||||||
alt={currentUser.name}
|
|
||||||
/>
|
|
||||||
<AvatarFallback className="bg-primary/10 text-primary">
|
|
||||||
{currentUser.name.split(' ').map(n => n[0]).join('')}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p className="text-[16px] font-semibold text-gray-900 truncate">
|
|
||||||
{currentUser.name}
|
|
||||||
</p>
|
|
||||||
<p className="text-[14px] text-gray-600 truncate">
|
|
||||||
{isIndividualUser ? 'Individual Account' : 'Corporate Account'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mobile Account Switching */}
|
|
||||||
<div className="space-y-3">
|
|
||||||
<h4 className="text-[14px] font-medium text-gray-900">Switch Account</h4>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{availableAccounts.map((account) => (
|
|
||||||
<button
|
|
||||||
key={account.type}
|
|
||||||
onClick={() => !account.isActive && handleAccountSignIn(account.type as 'individual' | 'corporate')}
|
|
||||||
disabled={account.isActive}
|
|
||||||
className={`w-full flex items-center gap-3 p-3 rounded-lg border text-left transition-all duration-200 ${
|
|
||||||
account.isActive
|
|
||||||
? 'bg-green-50 border-green-200'
|
|
||||||
: 'bg-gray-50 border-gray-200 hover:bg-gray-100'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="relative">
|
|
||||||
{account.type === 'individual' ? (
|
|
||||||
<Avatar className="h-8 w-8">
|
|
||||||
<AvatarImage
|
|
||||||
src={currentUser.avatar}
|
|
||||||
alt={currentUser.name}
|
|
||||||
/>
|
|
||||||
<AvatarFallback className="bg-blue-100 text-blue-700 text-sm">
|
|
||||||
<User className="h-4 w-4" />
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
) : (
|
|
||||||
<div className="h-8 w-8 bg-purple-100 rounded-full flex items-center justify-center">
|
|
||||||
<Building2 className="h-4 w-4 text-purple-700" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{account.isActive && (
|
|
||||||
<div className="absolute -top-1 -right-1 w-4 h-4 bg-green-500 rounded-full flex items-center justify-center">
|
|
||||||
<Check className="h-2.5 w-2.5 text-white" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p className="text-[14px] font-medium text-gray-900 truncate">
|
|
||||||
{account.title}
|
|
||||||
</p>
|
|
||||||
<p className="text-[14px] text-gray-600 truncate">
|
|
||||||
{account.subtitle}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{!account.isActive && (
|
|
||||||
<ArrowRight className="h-4 w-4 text-gray-400 flex-shrink-0" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Navigation Items */}
|
|
||||||
<div className="flex-1 overflow-y-auto py-4">
|
|
||||||
{/* Learner Portal Items (if authenticated) */}
|
|
||||||
{isAuthenticated && (isIndividualUser || isCorporateUser) && (
|
|
||||||
<div className="px-4 mb-6">
|
|
||||||
<h3 className="text-[14px] font-medium text-gray-900 mb-3">
|
|
||||||
{isIndividualUser ? 'Personal Learning' : 'Corporate Learning'}
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-1">
|
|
||||||
{learnerMenuItems.map((item) => {
|
|
||||||
const IconComponent = item.icon;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={item.title}
|
|
||||||
onClick={() => {
|
|
||||||
navigate(item.href);
|
|
||||||
setIsMobileMenuOpen(false);
|
|
||||||
}}
|
|
||||||
className="w-full flex items-center gap-3 px-3 py-2 text-left text-[16px] text-gray-700 hover:bg-gray-50 hover:text-primary rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-primary min-h-[44px]"
|
|
||||||
>
|
|
||||||
<IconComponent className="h-5 w-5 flex-shrink-0" />
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="font-medium">{item.title}</div>
|
|
||||||
<div className="text-[14px] text-gray-500 truncate">{item.description}</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Public Navigation Items */}
|
|
||||||
<div className="px-4">
|
|
||||||
{navigationItems.map((item) => (
|
|
||||||
<Collapsible key={item.title}>
|
|
||||||
<CollapsibleTrigger
|
|
||||||
onClick={() => handleMobileToggle(item.title)}
|
|
||||||
className="w-full flex items-center justify-between p-3 text-[16px] text-gray-900 hover:bg-gray-50 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-primary min-h-[44px]"
|
|
||||||
>
|
|
||||||
<span className="font-medium">{item.title}</span>
|
|
||||||
<ChevronRight className={`h-4 w-4 transition-transform ${
|
|
||||||
expandedMobileSection === item.title ? 'rotate-90' : ''
|
|
||||||
}`} />
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
<CollapsibleContent className="px-4 pb-2">
|
|
||||||
<div className="space-y-1">
|
|
||||||
{item.items.map((subItem) => {
|
|
||||||
const IconComponent = subItem.icon;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={subItem.title}
|
|
||||||
onClick={() => {
|
|
||||||
navigate(subItem.href);
|
|
||||||
setIsMobileMenuOpen(false);
|
|
||||||
}}
|
|
||||||
className="w-full flex items-center gap-3 px-3 py-2 text-left text-[16px] text-gray-600 hover:bg-gray-50 hover:text-primary rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-primary min-h-[44px]"
|
|
||||||
>
|
|
||||||
<IconComponent className="h-4 w-4 flex-shrink-0" />
|
|
||||||
<span>{subItem.title}</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</CollapsibleContent>
|
|
||||||
</Collapsible>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
navigate('/contact');
|
|
||||||
setIsMobileMenuOpen(false);
|
|
||||||
}}
|
|
||||||
className="w-full flex items-center p-3 text-[16px] text-gray-900 hover:bg-gray-50 rounded-lg transition-colors text-left focus:outline-none focus:ring-2 focus:ring-primary min-h-[44px]"
|
|
||||||
>
|
|
||||||
<span className="font-medium">Contact</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mobile Authentication Actions */}
|
|
||||||
{!isAuthenticated && (
|
|
||||||
<div className="p-4 border-t border-gray-200 space-y-2">
|
|
||||||
<Button
|
|
||||||
onClick={handleLogin}
|
|
||||||
variant="outline"
|
|
||||||
className="w-full text-[16px] min-h-[44px]"
|
|
||||||
>
|
|
||||||
Sign In
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleSignup}
|
|
||||||
className="w-full text-[16px] min-h-[44px] bg-primary hover:bg-primary/90 text-primary-foreground"
|
|
||||||
>
|
|
||||||
Get Started
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Mobile Logout */}
|
|
||||||
{isAuthenticated && (
|
|
||||||
<div className="p-4 border-t border-gray-200">
|
|
||||||
<Button
|
|
||||||
onClick={handleLogout}
|
|
||||||
variant="outline"
|
|
||||||
className="w-full text-[16px] min-h-[44px] text-red-600 border-red-200 hover:bg-red-50"
|
|
||||||
>
|
|
||||||
<LogOut className="h-4 w-4 mr-2" />
|
|
||||||
Sign Out
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</SheetContent>
|
|
||||||
</Sheet>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
401
src/components/Notes.tsx
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Search,
|
||||||
|
Plus,
|
||||||
|
Edit,
|
||||||
|
Trash2,
|
||||||
|
ExternalLink,
|
||||||
|
Calendar,
|
||||||
|
Book,
|
||||||
|
StickyNote,
|
||||||
|
Filter
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
import { Input } from './ui/input';
|
||||||
|
import { Badge } from './ui/badge';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue
|
||||||
|
} from './ui/select';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger
|
||||||
|
} from './ui/dialog';
|
||||||
|
import { Textarea } from './ui/textarea';
|
||||||
|
import { Separator } from './ui/separator';
|
||||||
|
import { useAppContext } from './AppShell';
|
||||||
|
|
||||||
|
// Mock data for notes
|
||||||
|
const mockNotesData = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
courseId: 'course-1',
|
||||||
|
courseTitle: 'Strategic Leadership Development',
|
||||||
|
moduleId: 'module-3',
|
||||||
|
moduleTitle: 'Decision Making Framework',
|
||||||
|
lessonId: 'lesson-2',
|
||||||
|
lessonTitle: 'Risk Assessment Strategies',
|
||||||
|
content: 'Key insight: Risk assessment should consider both quantitative metrics and qualitative factors. The SWOT analysis framework is particularly useful for strategic decisions.\n\nRemember to:\n- Involve stakeholders in the assessment process\n- Document assumptions clearly\n- Review decisions periodically',
|
||||||
|
createdAt: '2024-09-02T14:30:00Z',
|
||||||
|
updatedAt: '2024-09-02T14:30:00Z',
|
||||||
|
tags: ['risk-assessment', 'decision-making', 'strategy']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
courseId: 'course-1',
|
||||||
|
courseTitle: 'Strategic Leadership Development',
|
||||||
|
moduleId: 'module-2',
|
||||||
|
moduleTitle: 'Leadership Communication',
|
||||||
|
lessonId: 'lesson-4',
|
||||||
|
lessonTitle: 'Effective Presentation Skills',
|
||||||
|
content: 'Presentation framework:\n1. Hook - grab attention in first 30 seconds\n2. Overview - tell them what you\'ll tell them\n3. Body - 3 main points maximum\n4. Summary - tell them what you told them\n5. Call to action - what do you want them to do?\n\nPractice with the "rule of 3" - people remember things in threes.',
|
||||||
|
createdAt: '2024-08-28T10:15:00Z',
|
||||||
|
updatedAt: '2024-08-28T10:15:00Z',
|
||||||
|
tags: ['communication', 'presentations', 'leadership']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
courseId: 'course-2',
|
||||||
|
courseTitle: 'Team Building Essentials',
|
||||||
|
moduleId: 'module-1',
|
||||||
|
moduleTitle: 'Understanding Team Dynamics',
|
||||||
|
lessonId: 'lesson-1',
|
||||||
|
lessonTitle: 'Tuckman\'s Team Development Model',
|
||||||
|
content: 'The 4 stages of team development:\n\n1. FORMING - Team members are polite, excited, anxious\n2. STORMING - Conflicts arise, different opinions emerge\n3. NORMING - Team establishes norms and ways of working\n4. PERFORMING - Team works effectively towards goals\n\nAs a leader, my role changes in each stage. In forming, I need to provide clear direction. In storming, I mediate conflicts.',
|
||||||
|
createdAt: '2024-08-25T16:45:00Z',
|
||||||
|
updatedAt: '2024-08-25T16:45:00Z',
|
||||||
|
tags: ['team-development', 'tuckman-model', 'leadership']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
courseId: 'course-2',
|
||||||
|
courseTitle: 'Team Building Essentials',
|
||||||
|
moduleId: 'module-2',
|
||||||
|
moduleTitle: 'Conflict Resolution',
|
||||||
|
lessonId: 'lesson-3',
|
||||||
|
lessonTitle: 'Mediation Techniques',
|
||||||
|
content: 'Thomas-Kilmann Conflict Mode Instrument - 5 approaches:\n\n• Competing (assertive, uncooperative)\n• Accommodating (unassertive, cooperative)\n• Avoiding (unassertive, uncooperative)\n• Collaborating (assertive, cooperative) ← BEST for most situations\n• Compromising (moderate assertive & cooperative)\n\nNeed to practice active listening more in conflicts.',
|
||||||
|
createdAt: '2024-08-20T11:20:00Z',
|
||||||
|
updatedAt: '2024-08-22T09:30:00Z',
|
||||||
|
tags: ['conflict-resolution', 'mediation', 'thomas-kilmann']
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export function Notes() {
|
||||||
|
const { user } = useAppContext();
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [selectedCourse, setSelectedCourse] = useState('all');
|
||||||
|
const [sortBy, setSortBy] = useState('newest');
|
||||||
|
const [editingNote, setEditingNote] = useState<any>(null);
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
const formatDate = (dateStr: string) => {
|
||||||
|
return new Date(dateStr).toLocaleDateString('en-IN', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
timeZone: 'Asia/Kolkata'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUniqueCoursesFromNotes = () => {
|
||||||
|
const courses = mockNotesData.reduce((acc, note) => {
|
||||||
|
if (!acc.find(c => c.id === note.courseId)) {
|
||||||
|
acc.push({
|
||||||
|
id: note.courseId,
|
||||||
|
title: note.courseTitle
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, [] as any[]);
|
||||||
|
return courses;
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredNotes = mockNotesData.filter(note => {
|
||||||
|
const matchesSearch = searchQuery === '' ||
|
||||||
|
note.content.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
note.courseTitle.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
note.moduleTitle.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
note.lessonTitle.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
note.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase()));
|
||||||
|
|
||||||
|
const matchesCourse = selectedCourse === 'all' || note.courseId === selectedCourse;
|
||||||
|
|
||||||
|
return matchesSearch && matchesCourse;
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortedNotes = [...filteredNotes].sort((a, b) => {
|
||||||
|
switch (sortBy) {
|
||||||
|
case 'newest':
|
||||||
|
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
|
||||||
|
case 'oldest':
|
||||||
|
return new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime();
|
||||||
|
case 'course':
|
||||||
|
return a.courseTitle.localeCompare(b.courseTitle);
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const NoteCard = ({ note }: { note: any }) => {
|
||||||
|
const previewText = note.content.length > 200
|
||||||
|
? note.content.substring(0, 200) + '...'
|
||||||
|
: note.content;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="hover:shadow-md transition-shadow">
|
||||||
|
<CardContent className="p-4 space-y-3">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1 space-y-2">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<Book className="h-3 w-3" />
|
||||||
|
<span>{note.courseTitle}</span>
|
||||||
|
<span>›</span>
|
||||||
|
<span>{note.moduleTitle}</span>
|
||||||
|
<span>›</span>
|
||||||
|
<span>{note.lessonTitle}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||||
|
<Calendar className="h-3 w-3" />
|
||||||
|
{note.updatedAt !== note.createdAt ? 'Updated' : 'Created'} {formatDate(note.updatedAt)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm whitespace-pre-line">{previewText}</p>
|
||||||
|
|
||||||
|
{note.tags && note.tags.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{note.tags.map((tag: string) => (
|
||||||
|
<Badge key={tag} variant="outline" className="text-xs">
|
||||||
|
{tag}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 ml-4">
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
// TODO: Open lesson
|
||||||
|
}}
|
||||||
|
className="h-8 w-8"
|
||||||
|
>
|
||||||
|
<ExternalLink className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingNote(note);
|
||||||
|
setIsDialogOpen(true);
|
||||||
|
}}
|
||||||
|
className="h-8 w-8"
|
||||||
|
>
|
||||||
|
<Edit className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
// TODO: Delete note
|
||||||
|
}}
|
||||||
|
className="h-8 w-8 text-destructive hover:text-destructive"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-medium">My Notes</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Centralized view of all your learning notes
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button onClick={() => setEditingNote(null)}>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
Add Note
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{editingNote ? 'Edit Note' : 'Add New Note'}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{editingNote ? 'Make changes to your note below.' : 'Create a new note to capture important insights from your learning.'}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* TODO: Add note editor form */}
|
||||||
|
<Textarea
|
||||||
|
placeholder="Enter your note..."
|
||||||
|
className="min-h-[200px]"
|
||||||
|
defaultValue={editingNote?.content || ''}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => setIsDialogOpen(false)}>
|
||||||
|
{editingNote ? 'Save Changes' : 'Save Note'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters and Search */}
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4">
|
||||||
|
<div className="relative flex-1 max-w-md">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search notes..."
|
||||||
|
className="pl-10"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Select value={selectedCourse} onValueChange={setSelectedCourse}>
|
||||||
|
<SelectTrigger className="w-48">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All Courses</SelectItem>
|
||||||
|
{getUniqueCoursesFromNotes().map((course) => (
|
||||||
|
<SelectItem key={course.id} value={course.id}>
|
||||||
|
{course.title}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Select value={sortBy} onValueChange={setSortBy}>
|
||||||
|
<SelectTrigger className="w-40">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="newest">Newest First</SelectItem>
|
||||||
|
<SelectItem value="oldest">Oldest First</SelectItem>
|
||||||
|
<SelectItem value="course">By Course</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-[#04045B] flex items-center justify-center text-white">
|
||||||
|
<StickyNote className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl font-bold">{mockNotesData.length}</p>
|
||||||
|
<p className="text-sm text-muted-foreground">Total Notes</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-[#F8C301] flex items-center justify-center text-[#04045B]">
|
||||||
|
<Book className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl font-bold">{getUniqueCoursesFromNotes().length}</p>
|
||||||
|
<p className="text-sm text-muted-foreground">Courses</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-green-500 flex items-center justify-center text-white">
|
||||||
|
<Calendar className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl font-bold">
|
||||||
|
{new Set(mockNotesData.map(n => n.createdAt.split('T')[0])).size}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">Days Active</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Results */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Showing {sortedNotes.length} of {mockNotesData.length} notes
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{sortedNotes.length === 0 ? (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-8 text-center">
|
||||||
|
<StickyNote className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
|
||||||
|
<h3 className="font-medium mb-2">No notes found</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
{searchQuery || selectedCourse !== 'all'
|
||||||
|
? 'Try adjusting your search or filters'
|
||||||
|
: 'Start taking notes during your lessons'
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
{(!searchQuery && selectedCourse === 'all') && (
|
||||||
|
<Button>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
Add Your First Note
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{sortedNotes.map((note) => (
|
||||||
|
<NoteCard key={note.id} note={note} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
367
src/components/Notifications.tsx
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Bell,
|
||||||
|
Filter,
|
||||||
|
Mail,
|
||||||
|
CheckCheck,
|
||||||
|
Calendar,
|
||||||
|
BookOpen,
|
||||||
|
MessageSquare,
|
||||||
|
AlertTriangle,
|
||||||
|
CreditCard,
|
||||||
|
Clock,
|
||||||
|
Search
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
import { Input } from './ui/input';
|
||||||
|
import { Badge } from './ui/badge';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue
|
||||||
|
} from './ui/select';
|
||||||
|
import { Checkbox } from './ui/checkbox';
|
||||||
|
import { Separator } from './ui/separator';
|
||||||
|
import { useAppContext } from './AppShell';
|
||||||
|
|
||||||
|
// Mock notifications data
|
||||||
|
const mockNotifications = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
type: 'module-due',
|
||||||
|
title: 'Module Due This Week',
|
||||||
|
message: 'Module 3: Decision Making Framework is due in 2 days',
|
||||||
|
timestamp: '2024-09-03T10:30:00Z',
|
||||||
|
read: false,
|
||||||
|
courseTitle: 'Strategic Leadership Development',
|
||||||
|
priority: 'high'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
type: 'new-article',
|
||||||
|
title: 'New Research Paper Available',
|
||||||
|
message: 'New article: "The Future of Remote Leadership" has been added to your library',
|
||||||
|
timestamp: '2024-09-03T09:15:00Z',
|
||||||
|
read: false,
|
||||||
|
priority: 'medium'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
type: 'discussion',
|
||||||
|
title: 'New Discussion Reply',
|
||||||
|
message: 'Dr. Kumar replied to your comment in "Risk Assessment Strategies" discussion',
|
||||||
|
timestamp: '2024-09-02T16:45:00Z',
|
||||||
|
read: true,
|
||||||
|
courseTitle: 'Strategic Leadership Development',
|
||||||
|
priority: 'low'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
type: 'course-assigned',
|
||||||
|
title: 'New Course Assigned',
|
||||||
|
message: 'Your organization has enrolled you in "Financial Management for Leaders"',
|
||||||
|
timestamp: '2024-09-02T14:20:00Z',
|
||||||
|
read: false,
|
||||||
|
courseTitle: 'Financial Management for Leaders',
|
||||||
|
priority: 'high'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
type: 'overdue',
|
||||||
|
title: 'Assignment Overdue',
|
||||||
|
message: 'Leadership Assessment for Module 2 is now overdue',
|
||||||
|
timestamp: '2024-09-01T18:00:00Z',
|
||||||
|
read: true,
|
||||||
|
courseTitle: 'Strategic Leadership Development',
|
||||||
|
priority: 'high'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '6',
|
||||||
|
type: 'webinar',
|
||||||
|
title: 'Webinar Reminder',
|
||||||
|
message: 'Webinar "Future of Leadership in Digital Age" starts in 1 hour',
|
||||||
|
timestamp: '2024-09-01T13:00:00Z',
|
||||||
|
read: true,
|
||||||
|
priority: 'medium'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export function Notifications() {
|
||||||
|
const { user } = useAppContext();
|
||||||
|
const [notifications, setNotifications] = useState(mockNotifications);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [filterType, setFilterType] = useState('all');
|
||||||
|
const [selectedNotifications, setSelectedNotifications] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const getNotificationIcon = (type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'module-due':
|
||||||
|
case 'overdue':
|
||||||
|
return Calendar;
|
||||||
|
case 'new-article':
|
||||||
|
return BookOpen;
|
||||||
|
case 'discussion':
|
||||||
|
return MessageSquare;
|
||||||
|
case 'course-assigned':
|
||||||
|
case 'course-purchased':
|
||||||
|
return CreditCard;
|
||||||
|
case 'webinar':
|
||||||
|
return Clock;
|
||||||
|
default:
|
||||||
|
return Bell;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNotificationColor = (type: string, priority: string) => {
|
||||||
|
if (priority === 'high') return 'border-l-red-500';
|
||||||
|
if (priority === 'medium') return 'border-l-yellow-500';
|
||||||
|
return 'border-l-blue-500';
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (timestamp: string) => {
|
||||||
|
const now = new Date();
|
||||||
|
const notificationTime = new Date(timestamp);
|
||||||
|
const diffInHours = (now.getTime() - notificationTime.getTime()) / (1000 * 60 * 60);
|
||||||
|
|
||||||
|
if (diffInHours < 1) {
|
||||||
|
const diffInMinutes = Math.floor(diffInHours * 60);
|
||||||
|
return `${diffInMinutes}m ago`;
|
||||||
|
} else if (diffInHours < 24) {
|
||||||
|
return `${Math.floor(diffInHours)}h ago`;
|
||||||
|
} else {
|
||||||
|
const diffInDays = Math.floor(diffInHours / 24);
|
||||||
|
return `${diffInDays}d ago`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredNotifications = notifications.filter(notification => {
|
||||||
|
const matchesSearch = searchQuery === '' ||
|
||||||
|
notification.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
notification.message.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
notification.courseTitle?.toLowerCase().includes(searchQuery.toLowerCase());
|
||||||
|
|
||||||
|
const matchesType = filterType === 'all' ||
|
||||||
|
(filterType === 'unread' && !notification.read) ||
|
||||||
|
(filterType === 'overdue' && notification.type === 'overdue') ||
|
||||||
|
(filterType === 'this-week' && ['module-due'].includes(notification.type)) ||
|
||||||
|
(filterType === 'this-month' && ['module-due', 'course-assigned'].includes(notification.type)) ||
|
||||||
|
notification.type === filterType;
|
||||||
|
|
||||||
|
return matchesSearch && matchesType;
|
||||||
|
});
|
||||||
|
|
||||||
|
const markAsRead = (id: string) => {
|
||||||
|
setNotifications(prev =>
|
||||||
|
prev.map(notification =>
|
||||||
|
notification.id === id
|
||||||
|
? { ...notification, read: true }
|
||||||
|
: notification
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const markAllAsRead = () => {
|
||||||
|
setNotifications(prev =>
|
||||||
|
prev.map(notification => ({ ...notification, read: true }))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleNotificationSelection = (id: string) => {
|
||||||
|
setSelectedNotifications(prev =>
|
||||||
|
prev.includes(id)
|
||||||
|
? prev.filter(nId => nId !== id)
|
||||||
|
: [...prev, id]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const markSelectedAsRead = () => {
|
||||||
|
setNotifications(prev =>
|
||||||
|
prev.map(notification =>
|
||||||
|
selectedNotifications.includes(notification.id)
|
||||||
|
? { ...notification, read: true }
|
||||||
|
: notification
|
||||||
|
)
|
||||||
|
);
|
||||||
|
setSelectedNotifications([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const unreadCount = notifications.filter(n => !n.read).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-medium">Notifications</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Stay updated with your learning progress and activities
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant={unreadCount > 0 ? "destructive" : "secondary"}>
|
||||||
|
{unreadCount} unread
|
||||||
|
</Badge>
|
||||||
|
<Button variant="outline" onClick={markAllAsRead}>
|
||||||
|
<CheckCheck className="h-4 w-4 mr-2" />
|
||||||
|
Mark all as read
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters */}
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4">
|
||||||
|
<div className="relative flex-1 max-w-md">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search notifications..."
|
||||||
|
className="pl-10"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Select value={filterType} onValueChange={setFilterType}>
|
||||||
|
<SelectTrigger className="w-48">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All Notifications</SelectItem>
|
||||||
|
<SelectItem value="unread">Unread</SelectItem>
|
||||||
|
<SelectItem value="new-article">New Articles</SelectItem>
|
||||||
|
<SelectItem value="discussion">Discussions</SelectItem>
|
||||||
|
<SelectItem value="this-week">Due This Week</SelectItem>
|
||||||
|
<SelectItem value="this-month">Due This Month</SelectItem>
|
||||||
|
<SelectItem value="overdue">Overdue</SelectItem>
|
||||||
|
<SelectItem value="course-assigned">Course Assigned</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
{selectedNotifications.length > 0 && (
|
||||||
|
<Button variant="outline" onClick={markSelectedAsRead}>
|
||||||
|
Mark Selected as Read ({selectedNotifications.length})
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Notifications List */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
{filteredNotifications.length === 0 ? (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-8 text-center">
|
||||||
|
<Bell className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
|
||||||
|
<h3 className="font-medium mb-2">No notifications found</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{searchQuery || filterType !== 'all'
|
||||||
|
? 'Try adjusting your search or filters'
|
||||||
|
: 'You\'re all caught up!'
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
filteredNotifications.map((notification) => {
|
||||||
|
const Icon = getNotificationIcon(notification.type);
|
||||||
|
const isSelected = selectedNotifications.includes(notification.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={notification.id}
|
||||||
|
className={`transition-all hover:shadow-sm border-l-4 ${
|
||||||
|
getNotificationColor(notification.type, notification.priority)
|
||||||
|
} ${notification.read ? 'opacity-60' : ''} ${
|
||||||
|
isSelected ? 'bg-blue-50' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Checkbox
|
||||||
|
checked={isSelected}
|
||||||
|
onCheckedChange={() => toggleNotificationSelection(notification.id)}
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${
|
||||||
|
notification.priority === 'high' ? 'bg-red-100 text-red-600' :
|
||||||
|
notification.priority === 'medium' ? 'bg-yellow-100 text-yellow-600' :
|
||||||
|
'bg-blue-100 text-blue-600'
|
||||||
|
}`}>
|
||||||
|
<Icon className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 space-y-1">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<h3 className={`font-medium ${!notification.read ? 'text-foreground' : 'text-muted-foreground'}`}>
|
||||||
|
{notification.title}
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{formatTime(notification.timestamp)}
|
||||||
|
</span>
|
||||||
|
{!notification.read && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => markAsRead(notification.id)}
|
||||||
|
className="h-6 px-2"
|
||||||
|
>
|
||||||
|
<Mail className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{notification.message}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{notification.courseTitle && (
|
||||||
|
<div className="flex items-center gap-1 mt-1">
|
||||||
|
<BookOpen className="h-3 w-3 text-muted-foreground" />
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{notification.courseTitle}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between pt-2">
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={`text-xs capitalize ${
|
||||||
|
notification.priority === 'high' ? 'border-red-200 text-red-700' :
|
||||||
|
notification.priority === 'medium' ? 'border-yellow-200 text-yellow-700' :
|
||||||
|
'border-blue-200 text-blue-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{notification.type.replace('-', ' ')}
|
||||||
|
</Badge>
|
||||||
|
|
||||||
|
{!notification.read && (
|
||||||
|
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Load More */}
|
||||||
|
{filteredNotifications.length > 0 && (
|
||||||
|
<div className="text-center">
|
||||||
|
<Button variant="outline">
|
||||||
|
Load More Notifications
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
|
|
||||||
import { Button } from "./ui/button";
|
|
||||||
import { Badge } from "./ui/badge";
|
|
||||||
import { ChevronRight, RotateCcw, Wand2 } from "lucide-react";
|
|
||||||
import { navigate } from './Router';
|
|
||||||
|
|
||||||
const wizardSteps = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
question: "What's your current role?",
|
|
||||||
options: ["Individual Contributor", "Team Lead", "Manager", "Senior Manager", "Director", "VP/C-Level"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
question: "Which industry describes your organization?",
|
|
||||||
options: ["Technology", "Financial Services", "Healthcare", "Manufacturing", "Consulting", "Other"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
question: "What's your primary development goal?",
|
|
||||||
options: ["Strategic Thinking", "Team Leadership", "Communication", "Digital Transformation", "Innovation", "Change Management"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
question: "How much leadership experience do you have?",
|
|
||||||
options: ["0-2 years", "3-5 years", "6-10 years", "10+ years"]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
export function ProgrammeWizard() {
|
|
||||||
const [currentStep, setCurrentStep] = useState(0);
|
|
||||||
const [answers, setAnswers] = useState<string[]>([]);
|
|
||||||
const [isCompleted, setIsCompleted] = useState(false);
|
|
||||||
|
|
||||||
const handleAnswer = (answer: string) => {
|
|
||||||
const newAnswers = [...answers, answer];
|
|
||||||
setAnswers(newAnswers);
|
|
||||||
|
|
||||||
if (currentStep < wizardSteps.length - 1) {
|
|
||||||
setCurrentStep(currentStep + 1);
|
|
||||||
} else {
|
|
||||||
setIsCompleted(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const reset = () => {
|
|
||||||
setCurrentStep(0);
|
|
||||||
setAnswers([]);
|
|
||||||
setIsCompleted(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRecommendation = () => {
|
|
||||||
// Simple recommendation logic based on answers
|
|
||||||
const [role, industry, goal, experience] = answers;
|
|
||||||
if (goal === "Strategic Thinking" && experience === "10+ years") {
|
|
||||||
return {
|
|
||||||
title: "Strategic Leadership Mastery",
|
|
||||||
reason: "Perfect for senior leaders focused on strategic thinking",
|
|
||||||
slug: "strategic-leadership"
|
|
||||||
};
|
|
||||||
} else if (goal === "Communication") {
|
|
||||||
return {
|
|
||||||
title: "Executive Leadership Program",
|
|
||||||
reason: "Ideal for developing communication and executive presence",
|
|
||||||
slug: "exec-leadership-program"
|
|
||||||
};
|
|
||||||
} else if (role === "Individual Contributor" || role === "Team Lead") {
|
|
||||||
return {
|
|
||||||
title: "Emerging Leaders Program",
|
|
||||||
reason: "Great foundation for emerging leaders",
|
|
||||||
slug: "emerging-leaders"
|
|
||||||
};
|
|
||||||
} else if (goal === "Innovation" || goal === "Digital Transformation") {
|
|
||||||
return {
|
|
||||||
title: "Innovation Leadership",
|
|
||||||
reason: "Perfect for driving innovation and digital transformation",
|
|
||||||
slug: "innovation-leadership"
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
title: "Team Leadership Intensive",
|
|
||||||
reason: "Comprehensive programme for effective team leadership",
|
|
||||||
slug: "team-leadership-intensive"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isCompleted) {
|
|
||||||
const recommendation = getRecommendation();
|
|
||||||
return (
|
|
||||||
<Card className="bg-primary/5 border-primary">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2 text-foreground">
|
|
||||||
<Wand2 className="w-5 h-5 text-primary" />
|
|
||||||
Your Recommended Programme
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="bg-white p-6 rounded-lg border shadow-sm">
|
|
||||||
<h3 className="text-xl text-foreground mb-2">{recommendation.title}</h3>
|
|
||||||
<p className="text-muted-foreground mb-6">{recommendation.reason}</p>
|
|
||||||
<div className="flex flex-col sm:flex-row gap-3">
|
|
||||||
<Button
|
|
||||||
className="bg-primary hover:bg-primary/90 text-primary-foreground"
|
|
||||||
onClick={() => navigate(`/programme/${recommendation.slug}`)}
|
|
||||||
>
|
|
||||||
View Programme Details
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" onClick={reset}>
|
|
||||||
<RotateCcw className="w-4 h-4 mr-2" />
|
|
||||||
Try Again
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="bg-white border border-primary/20 shadow-lg">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2 text-foreground">
|
|
||||||
<Wand2 className="w-5 h-5 text-primary" />
|
|
||||||
Need Help Choosing?
|
|
||||||
</CardTitle>
|
|
||||||
<div className="flex gap-1 mt-4">
|
|
||||||
{wizardSteps.map((_, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={`h-2 flex-1 rounded ${
|
|
||||||
index <= currentStep ? "bg-primary" : "bg-secondary/30"
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg mb-6 text-foreground">
|
|
||||||
{wizardSteps[currentStep].question}
|
|
||||||
</h3>
|
|
||||||
<div className="grid grid-cols-1 gap-3">
|
|
||||||
{wizardSteps[currentStep].options.map((option) => (
|
|
||||||
<Button
|
|
||||||
key={option}
|
|
||||||
variant="outline"
|
|
||||||
className="justify-between hover:bg-primary hover:text-primary-foreground hover:border-primary text-left p-4 h-auto"
|
|
||||||
onClick={() => handleAnswer(option)}
|
|
||||||
>
|
|
||||||
<span>{option}</span>
|
|
||||||
<ChevronRight className="w-4 h-4 flex-shrink-0" />
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{answers.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-muted-foreground mb-3">Your answers:</p>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{answers.map((answer, index) => (
|
|
||||||
<Badge key={index} variant="secondary" className="bg-primary/10 text-foreground">
|
|
||||||
{answer}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
376
src/components/ReportsAndCertificates.tsx
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Download,
|
||||||
|
Share2,
|
||||||
|
Eye,
|
||||||
|
FileText,
|
||||||
|
Award,
|
||||||
|
Calendar,
|
||||||
|
ExternalLink,
|
||||||
|
CheckCircle,
|
||||||
|
Clock,
|
||||||
|
AlertCircle
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
import { Badge } from './ui/badge';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
|
||||||
|
import { Separator } from './ui/separator';
|
||||||
|
import { useAppContext } from './AppShell';
|
||||||
|
|
||||||
|
// Mock data for reports and certificates
|
||||||
|
const mockReportsData = {
|
||||||
|
certificates: [
|
||||||
|
{
|
||||||
|
id: 'cert-1',
|
||||||
|
title: 'Strategic Leadership Development Certificate',
|
||||||
|
courseTitle: 'Strategic Leadership Development',
|
||||||
|
issuedOn: '2024-08-15T00:00:00Z',
|
||||||
|
status: 'ready',
|
||||||
|
downloadUrl: '#',
|
||||||
|
shareUrl: '#',
|
||||||
|
thumbnail: 'https://images.unsplash.com/photo-1586953208448-b95a79798f07?w=400',
|
||||||
|
credentialId: 'KLC-SLD-2024-001'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cert-2',
|
||||||
|
title: 'Team Building Essentials Certificate',
|
||||||
|
courseTitle: 'Team Building Essentials',
|
||||||
|
issuedOn: '2024-07-20T00:00:00Z',
|
||||||
|
status: 'ready',
|
||||||
|
downloadUrl: '#',
|
||||||
|
shareUrl: '#',
|
||||||
|
thumbnail: 'https://images.unsplash.com/photo-1586953208448-b95a79798f07?w=400',
|
||||||
|
credentialId: 'KLC-TBE-2024-002'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cert-3',
|
||||||
|
title: 'Financial Management for Leaders Certificate',
|
||||||
|
courseTitle: 'Financial Management for Leaders',
|
||||||
|
issuedOn: null,
|
||||||
|
status: 'in-progress',
|
||||||
|
downloadUrl: null,
|
||||||
|
shareUrl: null,
|
||||||
|
thumbnail: 'https://images.unsplash.com/photo-1586953208448-b95a79798f07?w=400',
|
||||||
|
credentialId: null,
|
||||||
|
expectedDate: '2024-10-15T00:00:00Z'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
leadershipReports: [
|
||||||
|
{
|
||||||
|
id: 'report-1',
|
||||||
|
title: 'Leadership Assessment Report - Q3 2024',
|
||||||
|
type: 'assessment',
|
||||||
|
courseId: 'course-1',
|
||||||
|
courseTitle: 'Strategic Leadership Development',
|
||||||
|
createdAt: '2024-08-20T00:00:00Z',
|
||||||
|
status: 'ready',
|
||||||
|
pdfUrl: '#',
|
||||||
|
summary: 'Comprehensive analysis of leadership competencies including strategic thinking, decision making, and team management capabilities.',
|
||||||
|
keyInsights: [
|
||||||
|
'Strong strategic thinking capabilities',
|
||||||
|
'Opportunity for improvement in delegation',
|
||||||
|
'Excellent communication skills'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'report-2',
|
||||||
|
title: '360-Degree Feedback Report',
|
||||||
|
type: '360-feedback',
|
||||||
|
courseId: 'course-1',
|
||||||
|
courseTitle: 'Strategic Leadership Development',
|
||||||
|
createdAt: '2024-08-25T00:00:00Z',
|
||||||
|
status: 'ready',
|
||||||
|
pdfUrl: '#',
|
||||||
|
summary: 'Multi-perspective evaluation including self-assessment, peer feedback, and supervisor input on leadership effectiveness.',
|
||||||
|
keyInsights: [
|
||||||
|
'High emotional intelligence scores',
|
||||||
|
'Consistent feedback on visionary leadership',
|
||||||
|
'Development area: conflict resolution'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'report-3',
|
||||||
|
title: 'Leadership Style Analysis',
|
||||||
|
type: 'profiler',
|
||||||
|
courseId: null,
|
||||||
|
courseTitle: null,
|
||||||
|
createdAt: '2024-09-01T00:00:00Z',
|
||||||
|
status: 'in-progress',
|
||||||
|
pdfUrl: null,
|
||||||
|
summary: 'Analysis of predominant leadership style and recommendations for situational leadership adaptation.',
|
||||||
|
expectedDate: '2024-09-10T00:00:00Z'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ReportsAndCertificates() {
|
||||||
|
const { user } = useAppContext();
|
||||||
|
const [activeTab, setActiveTab] = useState('certificates');
|
||||||
|
|
||||||
|
const formatDate = (dateStr: string) => {
|
||||||
|
return new Date(dateStr).toLocaleDateString('en-IN', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
timeZone: 'Asia/Kolkata'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusIcon = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'ready': return CheckCircle;
|
||||||
|
case 'in-progress': return Clock;
|
||||||
|
case 'eligible': return AlertCircle;
|
||||||
|
default: return Clock;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'ready': return 'bg-green-100 text-green-800';
|
||||||
|
case 'in-progress': return 'bg-blue-100 text-blue-800';
|
||||||
|
case 'eligible': return 'bg-yellow-100 text-yellow-800';
|
||||||
|
default: return 'bg-gray-100 text-gray-800';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const CertificateCard = ({ certificate }: { certificate: any }) => {
|
||||||
|
const StatusIcon = getStatusIcon(certificate.status);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="overflow-hidden">
|
||||||
|
<div className="flex">
|
||||||
|
<div className="w-32 h-32 bg-gradient-to-br from-[#04045B] to-[#F8C301] flex items-center justify-center text-white">
|
||||||
|
<Award className="h-12 w-12" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 p-4">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<h3 className="font-medium">{certificate.title}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">{certificate.courseTitle}</p>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<StatusIcon className="h-4 w-4" />
|
||||||
|
<Badge className={`capitalize ${getStatusColor(certificate.status)}`}>
|
||||||
|
{certificate.status.replace('-', ' ')}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{certificate.issuedOn && (
|
||||||
|
<div className="flex items-center gap-1 text-sm text-muted-foreground">
|
||||||
|
<Calendar className="h-3 w-3" />
|
||||||
|
Issued on {formatDate(certificate.issuedOn)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{certificate.expectedDate && (
|
||||||
|
<div className="flex items-center gap-1 text-sm text-muted-foreground">
|
||||||
|
<Calendar className="h-3 w-3" />
|
||||||
|
Expected {formatDate(certificate.expectedDate)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{certificate.credentialId && (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
ID: {certificate.credentialId}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{certificate.status === 'ready' && (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button size="sm" variant="outline">
|
||||||
|
<Eye className="h-3 w-3 mr-1" />
|
||||||
|
View
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="outline">
|
||||||
|
<Download className="h-3 w-3 mr-1" />
|
||||||
|
Download
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="outline">
|
||||||
|
<Share2 className="h-3 w-3 mr-1" />
|
||||||
|
Share
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ReportCard = ({ report }: { report: any }) => {
|
||||||
|
const StatusIcon = getStatusIcon(report.status);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1 space-y-3">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-[#04045B] flex items-center justify-center text-white">
|
||||||
|
<FileText className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 space-y-1">
|
||||||
|
<h3 className="font-medium">{report.title}</h3>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<StatusIcon className="h-4 w-4" />
|
||||||
|
<Badge className={`capitalize ${getStatusColor(report.status)}`}>
|
||||||
|
{report.status.replace('-', ' ')}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" className="capitalize">
|
||||||
|
{report.type.replace('-', ' ')}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{report.courseTitle && (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Related to: {report.courseTitle}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1 text-sm text-muted-foreground">
|
||||||
|
<Calendar className="h-3 w-3" />
|
||||||
|
{report.status === 'ready' ? 'Generated' : 'Expected'} on {
|
||||||
|
formatDate(report.status === 'ready' ? report.createdAt : report.expectedDate)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-muted-foreground">{report.summary}</p>
|
||||||
|
|
||||||
|
{report.keyInsights && report.keyInsights.length > 0 && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm font-medium">Key Insights:</p>
|
||||||
|
<ul className="text-sm text-muted-foreground space-y-1">
|
||||||
|
{report.keyInsights.map((insight: string, index: number) => (
|
||||||
|
<li key={index} className="flex items-start gap-2">
|
||||||
|
<span className="w-1 h-1 bg-muted-foreground rounded-full mt-2 flex-shrink-0" />
|
||||||
|
{insight}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2 ml-4">
|
||||||
|
{report.status === 'ready' && (
|
||||||
|
<>
|
||||||
|
<Button size="sm" variant="outline">
|
||||||
|
<Eye className="h-3 w-3 mr-1" />
|
||||||
|
View Report
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="outline">
|
||||||
|
<Download className="h-3 w-3 mr-1" />
|
||||||
|
Download PDF
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{report.courseId && (
|
||||||
|
<Button size="sm" variant="ghost">
|
||||||
|
<ExternalLink className="h-3 w-3 mr-1" />
|
||||||
|
View Course
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-medium">Reports & Certificates</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Access your leadership reports and download certificates
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
|
||||||
|
<TabsList className="grid w-full max-w-md grid-cols-2">
|
||||||
|
<TabsTrigger value="certificates" className="flex items-center gap-2">
|
||||||
|
<Award className="h-4 w-4" />
|
||||||
|
Certificates
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="reports" className="flex items-center gap-2">
|
||||||
|
<FileText className="h-4 w-4" />
|
||||||
|
Reports
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
{/* Certificates Tab */}
|
||||||
|
<TabsContent value="certificates" className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-medium">Your Certificates</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{mockReportsData.certificates.filter(c => c.status === 'ready').length} ready, {' '}
|
||||||
|
{mockReportsData.certificates.filter(c => c.status === 'in-progress').length} in progress
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{mockReportsData.certificates.map((certificate) => (
|
||||||
|
<CertificateCard key={certificate.id} certificate={certificate} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{mockReportsData.certificates.length === 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-8 text-center">
|
||||||
|
<Award className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
|
||||||
|
<h3 className="font-medium mb-2">No certificates yet</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Complete courses to earn certificates
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* Reports Tab */}
|
||||||
|
<TabsContent value="reports" className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-medium">Leadership Reports</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{mockReportsData.leadershipReports.filter(r => r.status === 'ready').length} ready, {' '}
|
||||||
|
{mockReportsData.leadershipReports.filter(r => r.status === 'in-progress').length} in progress
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{mockReportsData.leadershipReports.map((report) => (
|
||||||
|
<ReportCard key={report.id} report={report} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{mockReportsData.leadershipReports.length === 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-8 text-center">
|
||||||
|
<FileText className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
|
||||||
|
<h3 className="font-medium mb-2">No reports yet</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Complete assessments and profilers to generate reports
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
import { Routes, Route, Navigate, useSearchParams } from "react-router-dom";
|
|
||||||
|
|
||||||
// Import all page components
|
|
||||||
import { HomePage } from "../pages/HomePage";
|
|
||||||
import Dashboard from "../pages/learner/Dashboard";
|
|
||||||
import { CorporateDashboard } from "../pages/learner/CorporateDashboard";
|
|
||||||
import { Library } from "../pages/learner/Library";
|
|
||||||
import { CourseTimeline } from "../pages/learner/CourseTimeline";
|
|
||||||
import { Settings } from "../pages/Settings";
|
|
||||||
import { Surveys } from "../pages/Surveys";
|
|
||||||
import { CorporateWebinars } from "../pages/CorporateWebinars";
|
|
||||||
import { IndividualWebinars } from "../pages/IndividualWebinars";
|
|
||||||
import { Leaderboard } from "../pages/Leaderboard";
|
|
||||||
import { CorporateLeaderboard } from "../pages/CorporateLeaderboard";
|
|
||||||
import { LoginSelection } from "../pages/LoginSelection";
|
|
||||||
import { Login } from "../pages/Login";
|
|
||||||
import { Signup } from "../pages/Signup";
|
|
||||||
import { CorporateAuth } from "../pages/CorporateAuth";
|
|
||||||
import { CorporateLogin } from "../pages/CorporateLogin";
|
|
||||||
import { CorporateSignup } from "../pages/CorporateSignup";
|
|
||||||
import { ForgotPassword } from "../pages/ForgotPassword";
|
|
||||||
import { EmailVerification } from "../pages/EmailVerification";
|
|
||||||
import { Contact } from "../pages/Contact";
|
|
||||||
import { AboutKLC } from "../pages/AboutKLC";
|
|
||||||
import { OurVision } from "../pages/OurVision";
|
|
||||||
import { OurTeam } from "../pages/OurTeam";
|
|
||||||
import { OurImpact } from "../pages/OurImpact";
|
|
||||||
import { OurExpertise } from "../pages/OurExpertise";
|
|
||||||
import { ProgrammeCatalogue } from "../pages/ProgrammeCatalogue";
|
|
||||||
import { ProgrammeDetail } from "../pages/ProgrammeDetail";
|
|
||||||
import { ExecutiveLeadership } from "../pages/ExecutiveLeadership";
|
|
||||||
import { TeamLeadership } from "../pages/TeamLeadership";
|
|
||||||
import { InnovationLeadership } from "../pages/InnovationLeadership";
|
|
||||||
import { LeadershipOnline } from "../pages/LeadershipOnline";
|
|
||||||
import { LeadershipDevelopment } from "../pages/services/LeadershipDevelopment";
|
|
||||||
import { ManagementDevelopment } from "../pages/services/ManagementDevelopment";
|
|
||||||
import { ExecutiveCoaching } from "../pages/services/ExecutiveCoaching";
|
|
||||||
import { CultureCompetence } from "../pages/services/CultureCompetence";
|
|
||||||
import { Consulting } from "../pages/services/Consulting";
|
|
||||||
import { LearningFacility } from "../pages/services/LearningFacility";
|
|
||||||
import { Articles } from "../pages/Articles";
|
|
||||||
import { BlogListing } from "../pages/BlogListing";
|
|
||||||
import { BlogDetail } from "../pages/BlogDetail";
|
|
||||||
import { Resources } from "../pages/Resources";
|
|
||||||
import { WebinarListing } from "../pages/WebinarListing";
|
|
||||||
import { WebinarDetail } from "../pages/WebinarDetail";
|
|
||||||
import { FacilityDetail } from "../pages/FacilityDetail";
|
|
||||||
import { FacilityBooking } from "../pages/FacilityBooking";
|
|
||||||
import { FacilityTour } from "../pages/FacilityTour";
|
|
||||||
import { Cart } from "../pages/Cart";
|
|
||||||
import { Checkout } from "../pages/Checkout";
|
|
||||||
import { OrderConfirmation } from "../pages/OrderConfirmation";
|
|
||||||
import { OrderFailed } from "../pages/OrderFailed";
|
|
||||||
import { MyCohort } from "../pages/MyCohort";
|
|
||||||
import { FAQ } from "../pages/FAQ";
|
|
||||||
import { Privacy } from "../pages/Privacy";
|
|
||||||
import { Terms } from "../pages/Terms";
|
|
||||||
import { NotFound } from "../pages/NotFound";
|
|
||||||
|
|
||||||
function DashboardRoute() {
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
const view = searchParams.get("view");
|
|
||||||
return view === "corporate" ? (
|
|
||||||
<CorporateDashboard />
|
|
||||||
) : (
|
|
||||||
<Dashboard userType="individual" />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function WebinarsRoute() {
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
const view = searchParams.get("view");
|
|
||||||
return view === "corporate" ? <CorporateWebinars /> : <IndividualWebinars />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function LeaderboardRoute() {
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
const view = searchParams.get("view");
|
|
||||||
return view === "corporate" ? <CorporateLeaderboard /> : <Leaderboard />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Router() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen">
|
|
||||||
<Routes>
|
|
||||||
{/* Main */}
|
|
||||||
<Route path="/" element={<HomePage />} />
|
|
||||||
|
|
||||||
{/* Auth */}
|
|
||||||
<Route path="/auth" element={<LoginSelection />} />
|
|
||||||
<Route path="/login-selection" element={<LoginSelection />} />
|
|
||||||
<Route path="/login" element={<Login />} />
|
|
||||||
<Route path="/signup" element={<Signup />} />
|
|
||||||
<Route path="/corporate/auth" element={<CorporateAuth />} />
|
|
||||||
<Route path="/corporate/login" element={<CorporateLogin />} />
|
|
||||||
<Route path="/corporate/signup" element={<CorporateSignup />} />
|
|
||||||
<Route path="/forgot-password" element={<ForgotPassword />} />
|
|
||||||
<Route path="/email-verification" element={<EmailVerification />} />
|
|
||||||
|
|
||||||
{/* Learner Portal */}
|
|
||||||
<Route path="/dashboard" element={<DashboardRoute />} />
|
|
||||||
<Route path="/library" element={<Library />} />
|
|
||||||
<Route path="/course" element={<CourseTimeline />} />
|
|
||||||
<Route path="/settings" element={<Settings />} />
|
|
||||||
<Route path="/surveys" element={<Surveys userType="individual" />} />
|
|
||||||
<Route path="/webinars" element={<WebinarsRoute />} />
|
|
||||||
<Route path="/leaderboard" element={<LeaderboardRoute />} />
|
|
||||||
|
|
||||||
{/* Other Pages */}
|
|
||||||
<Route path="/individual-webinars" element={<IndividualWebinars />} />
|
|
||||||
<Route path="/contact" element={<Contact />} />
|
|
||||||
<Route path="/about-klc" element={<AboutKLC />} />
|
|
||||||
<Route path="/about-us/our-vision" element={<OurVision />} />
|
|
||||||
<Route path="/about-us/our-team" element={<OurTeam />} />
|
|
||||||
<Route path="/about-us/our-impact" element={<OurImpact />} />
|
|
||||||
<Route path="/about-us/our-expertise" element={<OurExpertise />} />
|
|
||||||
|
|
||||||
{/* Programmes */}
|
|
||||||
<Route path="/programmes" element={<ProgrammeCatalogue />} />
|
|
||||||
<Route path="/programmes/detail" element={<ProgrammeDetail />} />
|
|
||||||
<Route
|
|
||||||
path="/programmes/executive-leadership"
|
|
||||||
element={<ExecutiveLeadership />}
|
|
||||||
/>
|
|
||||||
<Route path="/programmes/team-leadership" element={<TeamLeadership />} />
|
|
||||||
<Route
|
|
||||||
path="/programmes/innovation-leadership"
|
|
||||||
element={<InnovationLeadership />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/programmes/leadership-online"
|
|
||||||
element={<LeadershipOnline />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Services */}
|
|
||||||
<Route
|
|
||||||
path="/services/leadership-development"
|
|
||||||
element={<LeadershipDevelopment />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/services/management-development"
|
|
||||||
element={<ManagementDevelopment />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/services/executive-coaching"
|
|
||||||
element={<ExecutiveCoaching />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/services/culture-competence"
|
|
||||||
element={<CultureCompetence />}
|
|
||||||
/>
|
|
||||||
<Route path="/services/consulting" element={<Consulting />} />
|
|
||||||
<Route path="/services/learning-facility" element={<LearningFacility />} />
|
|
||||||
|
|
||||||
{/* Learning */}
|
|
||||||
<Route path="/learning/articles" element={<Articles />} />
|
|
||||||
<Route path="/learning/blog" element={<BlogListing />} />
|
|
||||||
<Route path="/learning/blog/detail" element={<BlogDetail />} />
|
|
||||||
<Route path="/learning/resources" element={<Resources />} />
|
|
||||||
|
|
||||||
{/* Webinars */}
|
|
||||||
<Route path="/webinars/listing" element={<WebinarListing />} />
|
|
||||||
<Route path="/webinars/detail" element={<WebinarDetail />} />
|
|
||||||
|
|
||||||
{/* Facilities */}
|
|
||||||
<Route path="/facilities/detail" element={<FacilityDetail />} />
|
|
||||||
<Route path="/facilities/booking" element={<FacilityBooking />} />
|
|
||||||
<Route path="/facilities/tour" element={<FacilityTour />} />
|
|
||||||
|
|
||||||
{/* E-commerce */}
|
|
||||||
<Route path="/cart" element={<Cart />} />
|
|
||||||
<Route path="/checkout" element={<Checkout />} />
|
|
||||||
<Route path="/order-confirmation" element={<OrderConfirmation />} />
|
|
||||||
<Route path="/order-failed" element={<OrderFailed />} />
|
|
||||||
|
|
||||||
{/* Extra Features */}
|
|
||||||
<Route path="/my-cohort" element={<MyCohort />} />
|
|
||||||
|
|
||||||
{/* Legal */}
|
|
||||||
<Route path="/faq" element={<FAQ />} />
|
|
||||||
<Route path="/privacy" element={<Privacy />} />
|
|
||||||
<Route path="/terms" element={<Terms />} />
|
|
||||||
|
|
||||||
{/* Legacy Redirects */}
|
|
||||||
<Route
|
|
||||||
path="/corporate/dashboard"
|
|
||||||
element={<Navigate to="/dashboard?view=corporate" replace />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/corporate/library"
|
|
||||||
element={<Navigate to="/library?view=corporate" replace />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/corporate/course"
|
|
||||||
element={<Navigate to="/course?view=corporate" replace />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/corporate/settings"
|
|
||||||
element={<Navigate to="/settings?view=corporate" replace />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/corporate/surveys"
|
|
||||||
element={<Navigate to="/surveys?view=corporate" replace />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/corporate/webinars"
|
|
||||||
element={<Navigate to="/webinars?view=corporate" replace />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/corporate/leaderboard"
|
|
||||||
element={<Navigate to="/leaderboard?view=corporate" replace />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Catch-all */}
|
|
||||||
<Route path="*" element={<NotFound />} />
|
|
||||||
</Routes>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
467
src/components/Settings.tsx
Normal file
@@ -0,0 +1,467 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
import { Input } from './ui/input';
|
||||||
|
import { Label } from './ui/label';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
||||||
|
import { Switch } from './ui/switch';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
||||||
|
import { Badge } from './ui/badge';
|
||||||
|
import { Separator } from './ui/separator';
|
||||||
|
import {
|
||||||
|
User,
|
||||||
|
Shield,
|
||||||
|
Settings as SettingsIcon,
|
||||||
|
Building2,
|
||||||
|
Bell,
|
||||||
|
Globe,
|
||||||
|
Eye,
|
||||||
|
Smartphone,
|
||||||
|
Mail,
|
||||||
|
Lock
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName?: string;
|
||||||
|
email: string;
|
||||||
|
persona: 'corporate' | 'individual';
|
||||||
|
orgName?: string;
|
||||||
|
avatar?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SettingsProps {
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Settings({ user }: SettingsProps) {
|
||||||
|
const [twoFactorEnabled, setTwoFactorEnabled] = useState(false);
|
||||||
|
const [emailNotifications, setEmailNotifications] = useState(true);
|
||||||
|
const [pushNotifications, setPushNotifications] = useState(true);
|
||||||
|
const [weeklyDigest, setWeeklyDigest] = useState(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 space-y-6 bg-white min-h-screen">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-medium">Settings</h1>
|
||||||
|
<p className="text-muted-foreground">Manage your account settings and preferences</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs defaultValue="profile" className="space-y-6">
|
||||||
|
<TabsList className="grid w-full grid-cols-4">
|
||||||
|
<TabsTrigger value="profile" className="flex items-center gap-2">
|
||||||
|
<User className="h-4 w-4" />
|
||||||
|
Profile
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="security" className="flex items-center gap-2">
|
||||||
|
<Shield className="h-4 w-4" />
|
||||||
|
Security
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="preferences" className="flex items-center gap-2">
|
||||||
|
<SettingsIcon className="h-4 w-4" />
|
||||||
|
Preferences
|
||||||
|
</TabsTrigger>
|
||||||
|
{user.persona === 'corporate' && (
|
||||||
|
<TabsTrigger value="organization" className="flex items-center gap-2">
|
||||||
|
<Building2 className="h-4 w-4" />
|
||||||
|
Organization
|
||||||
|
</TabsTrigger>
|
||||||
|
)}
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
{/* Profile Tab */}
|
||||||
|
<TabsContent value="profile">
|
||||||
|
<div className="grid gap-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Profile Information</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div className="flex items-center gap-6">
|
||||||
|
<Avatar className="h-20 w-20">
|
||||||
|
<AvatarImage src={user.avatar} />
|
||||||
|
<AvatarFallback className="text-lg">
|
||||||
|
{user.firstName.charAt(0)}{user.lastName?.charAt(0) || ''}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Button variant="outline">Change Photo</Button>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
JPG, PNG or GIF. Max file size 2MB.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="firstName">First Name</Label>
|
||||||
|
<Input id="firstName" defaultValue={user.firstName} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="lastName">Last Name</Label>
|
||||||
|
<Input id="lastName" defaultValue={user.lastName || ''} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="email">Email</Label>
|
||||||
|
<Input id="email" type="email" defaultValue={user.email} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="bio">Bio</Label>
|
||||||
|
<textarea
|
||||||
|
id="bio"
|
||||||
|
className="w-full min-h-[100px] px-3 py-2 border border-input rounded-md text-sm"
|
||||||
|
placeholder="Tell us about yourself..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant="secondary" className="bg-[var(--color-brand-primary)] text-white">
|
||||||
|
{user.persona === 'corporate' ? 'Corporate Learner' : 'Individual Learner'}
|
||||||
|
</Badge>
|
||||||
|
{user.orgName && (
|
||||||
|
<Badge variant="outline">{user.orgName}</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button className="bg-[var(--color-brand-primary)] hover:bg-[var(--color-brand-primary)]/90 text-white">
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* Security Tab */}
|
||||||
|
<TabsContent value="security">
|
||||||
|
<div className="grid gap-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Password</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="currentPassword">Current Password</Label>
|
||||||
|
<Input id="currentPassword" type="password" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="newPassword">New Password</Label>
|
||||||
|
<Input id="newPassword" type="password" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="confirmPassword">Confirm New Password</Label>
|
||||||
|
<Input id="confirmPassword" type="password" />
|
||||||
|
</div>
|
||||||
|
<Button variant="outline">Update Password</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Two-Factor Authentication</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm font-medium">Enable 2FA</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Add an extra layer of security to your account
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={twoFactorEnabled}
|
||||||
|
onCheckedChange={setTwoFactorEnabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{twoFactorEnabled && (
|
||||||
|
<div className="space-y-4 pt-4 border-t">
|
||||||
|
<div className="flex items-center gap-3 p-4 border rounded-lg">
|
||||||
|
<Smartphone className="h-5 w-5 text-muted-foreground" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm font-medium">Authenticator App</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Use an app like Google Authenticator or Authy
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" size="sm">Setup</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Active Sessions</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between p-4 border rounded-lg">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-green-100 flex items-center justify-center">
|
||||||
|
<Globe className="h-5 w-5 text-green-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">Current session</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Chrome on macOS • Mumbai, India
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge variant="secondary" className="bg-green-100 text-green-800">
|
||||||
|
Active
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button variant="destructive" size="sm">
|
||||||
|
Sign out all other sessions
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* Preferences Tab */}
|
||||||
|
<TabsContent value="preferences">
|
||||||
|
<div className="grid gap-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Notifications</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Mail className="h-4 w-4" />
|
||||||
|
<p className="text-sm font-medium">Email Notifications</p>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Receive notifications about course updates and deadlines
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={emailNotifications}
|
||||||
|
onCheckedChange={setEmailNotifications}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Bell className="h-4 w-4" />
|
||||||
|
<p className="text-sm font-medium">Push Notifications</p>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Get notified about important updates and reminders
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={pushNotifications}
|
||||||
|
onCheckedChange={setPushNotifications}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm font-medium">Weekly Learning Digest</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Summary of your learning progress and recommendations
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={weeklyDigest}
|
||||||
|
onCheckedChange={setWeeklyDigest}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Learning Preferences</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Preferred Learning Time</Label>
|
||||||
|
<Select defaultValue="morning">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="morning">Morning (6 AM - 12 PM)</SelectItem>
|
||||||
|
<SelectItem value="afternoon">Afternoon (12 PM - 6 PM)</SelectItem>
|
||||||
|
<SelectItem value="evening">Evening (6 PM - 10 PM)</SelectItem>
|
||||||
|
<SelectItem value="late">Late Night (10 PM - 12 AM)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Language</Label>
|
||||||
|
<Select defaultValue="en">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="en">English</SelectItem>
|
||||||
|
<SelectItem value="hi">Hindi</SelectItem>
|
||||||
|
<SelectItem value="es">Spanish</SelectItem>
|
||||||
|
<SelectItem value="fr">French</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Time Zone</Label>
|
||||||
|
<Select defaultValue="ist">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="ist">India Standard Time (GMT+5:30)</SelectItem>
|
||||||
|
<SelectItem value="utc">UTC (GMT+0)</SelectItem>
|
||||||
|
<SelectItem value="pst">Pacific Time (GMT-8)</SelectItem>
|
||||||
|
<SelectItem value="est">Eastern Time (GMT-5)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Accessibility</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
<p className="text-sm font-medium">High Contrast Mode</p>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Improve visibility with higher contrast colors
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Font Size</Label>
|
||||||
|
<Select defaultValue="medium">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="small">Small</SelectItem>
|
||||||
|
<SelectItem value="medium">Medium</SelectItem>
|
||||||
|
<SelectItem value="large">Large</SelectItem>
|
||||||
|
<SelectItem value="extra-large">Extra Large</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* Organization Tab (Corporate only) */}
|
||||||
|
{user.persona === 'corporate' && (
|
||||||
|
<TabsContent value="organization">
|
||||||
|
<div className="grid gap-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Organization Details</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Organization Name</Label>
|
||||||
|
<Input defaultValue={user.orgName} disabled />
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Contact your HR administrator to update organization details
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Employee ID</Label>
|
||||||
|
<Input defaultValue="EMP001234" disabled />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Department</Label>
|
||||||
|
<Input defaultValue="Engineering" disabled />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Manager</Label>
|
||||||
|
<Input defaultValue="Sarah Johnson" disabled />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Learning Permissions</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">Access to All Programmes</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Can access all corporate learning programmes
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Badge variant="secondary" className="bg-green-100 text-green-800">
|
||||||
|
Enabled
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">Manager Reporting</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Learning progress is shared with manager
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Badge variant="secondary" className="bg-green-100 text-green-800">
|
||||||
|
Enabled
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">Certification Authority</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Can issue official company certificates
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Badge variant="secondary" className="bg-yellow-100 text-yellow-800">
|
||||||
|
Limited
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
For changes to permissions, please contact your HR administrator or submit a request through the HR portal.
|
||||||
|
</p>
|
||||||
|
<Button variant="outline" className="mt-2">
|
||||||
|
Contact HR
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Card, CardContent, CardHeader } from '../ui/card';
|
|
||||||
import { Button } from '../ui/button';
|
|
||||||
import { Badge } from '../ui/badge';
|
|
||||||
import { Progress } from '../ui/progress';
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar';
|
|
||||||
import {
|
|
||||||
Star,
|
|
||||||
Clock,
|
|
||||||
Users,
|
|
||||||
Play,
|
|
||||||
CheckCircle,
|
|
||||||
Calendar,
|
|
||||||
Award,
|
|
||||||
Eye,
|
|
||||||
Heart,
|
|
||||||
Share2,
|
|
||||||
Video,
|
|
||||||
FileText,
|
|
||||||
Headphones,
|
|
||||||
Monitor,
|
|
||||||
AlertCircle
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { Course } from '../../pages/learner/data/libraryData';
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
interface CourseCardProps {
|
|
||||||
course: Course;
|
|
||||||
userType: 'individual' | 'corporate';
|
|
||||||
onEnroll?: (courseId: string) => void;
|
|
||||||
onContinue?: (courseId: string) => void;
|
|
||||||
onBookmark?: (courseId: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTypeIcon = (type: string) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'video': return Video;
|
|
||||||
case 'article': return FileText;
|
|
||||||
case 'audio': return Headphones;
|
|
||||||
case 'interactive': return Monitor;
|
|
||||||
default: return Video;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPriorityColor = (priority?: string) => {
|
|
||||||
switch (priority) {
|
|
||||||
case 'high': return 'text-destructive bg-destructive/10';
|
|
||||||
case 'medium': return 'text-orange-600 bg-orange-100';
|
|
||||||
case 'low': return 'text-success bg-success/10';
|
|
||||||
default: return '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getLevelColor = (level: string) => {
|
|
||||||
switch (level) {
|
|
||||||
case 'Beginner': return 'text-success bg-success/10';
|
|
||||||
case 'Intermediate': return 'text-primary bg-primary/10';
|
|
||||||
case 'Advanced': return 'text-destructive bg-destructive/10';
|
|
||||||
default: return 'text-muted-foreground bg-muted';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export function CourseCard({ course, userType, onEnroll, onContinue, onBookmark }: CourseCardProps) {
|
|
||||||
const TypeIcon = getTypeIcon(course.type);
|
|
||||||
const isOverdue = course.deadline && new Date(course.deadline) < new Date();
|
|
||||||
|
|
||||||
// Navigate to course details page with proper query parameters
|
|
||||||
const handleCourseNavigation = () => {
|
|
||||||
navigate(`/course?view=${userType}&courseId=${course.id}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className={`group hover:shadow-lg transition-all duration-300 ${
|
|
||||||
course.isFeatured ? 'ring-2 ring-primary/20' : ''
|
|
||||||
} ${isOverdue ? 'border-destructive/20 bg-destructive/5' : ''}`}>
|
|
||||||
{/* Course Thumbnail */}
|
|
||||||
<div className="relative overflow-hidden rounded-t-lg">
|
|
||||||
<img
|
|
||||||
src={course.thumbnail}
|
|
||||||
alt={course.title}
|
|
||||||
className="w-full h-48 object-cover group-hover:scale-105 transition-transform duration-300"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Overlay Badges */}
|
|
||||||
<div className="absolute top-3 left-3 flex flex-wrap gap-2">
|
|
||||||
{course.isFeatured && (
|
|
||||||
<Badge className="bg-primary text-primary-foreground text-xs">
|
|
||||||
Featured
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{course.isPremium && (
|
|
||||||
<Badge variant="secondary" className="text-xs">
|
|
||||||
Premium
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{course.organizationAssigned && userType === 'corporate' && (
|
|
||||||
<Badge variant="outline" className="text-xs bg-background/90">
|
|
||||||
Assigned
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Priority Badge for Corporate */}
|
|
||||||
{userType === 'corporate' && course.priority && (
|
|
||||||
<div className="absolute top-3 right-3">
|
|
||||||
<Badge variant="outline" className={`text-xs ${getPriorityColor(course.priority)}`}>
|
|
||||||
{course.priority.toUpperCase()}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Type Icon */}
|
|
||||||
<div className="absolute bottom-3 left-3">
|
|
||||||
<div className="bg-background/90 backdrop-blur-sm p-2 rounded-full">
|
|
||||||
<TypeIcon className="h-4 w-4 text-primary" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Course Status */}
|
|
||||||
<div className="absolute bottom-3 right-3">
|
|
||||||
{course.status === 'completed' && (
|
|
||||||
<div className="bg-success/90 backdrop-blur-sm p-2 rounded-full">
|
|
||||||
<CheckCircle className="h-4 w-4 text-white" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{course.status === 'bookmarked' && (
|
|
||||||
<div className="bg-orange-500/90 backdrop-blur-sm p-2 rounded-full">
|
|
||||||
<Heart className="h-4 w-4 text-white fill-current" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CardHeader className="pb-3">
|
|
||||||
<div className="flex items-start justify-between gap-3">
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<h3 className="text-lg font-semibold line-clamp-2 group-hover:text-primary transition-colors">
|
|
||||||
{course.title}
|
|
||||||
</h3>
|
|
||||||
<p className="text-base text-muted-foreground mt-2 line-clamp-2">
|
|
||||||
{course.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Instructor Info */}
|
|
||||||
<div className="flex items-center gap-3 mt-3">
|
|
||||||
<Avatar className="h-8 w-8">
|
|
||||||
<AvatarImage src={course.instructor.avatar} />
|
|
||||||
<AvatarFallback className="text-xs">
|
|
||||||
{course.instructor.name.split(' ').map(n => n[0]).join('')}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p className="text-base font-medium">{course.instructor.name}</p>
|
|
||||||
<p className="text-base text-muted-foreground">{course.instructor.title}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Course Metadata */}
|
|
||||||
<div className="flex items-center gap-4 mt-3 text-base text-muted-foreground">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Clock className="h-4 w-4" />
|
|
||||||
<span>{course.duration}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Eye className="h-4 w-4" />
|
|
||||||
<span>{course.lessonsCount} lessons</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Users className="h-4 w-4" />
|
|
||||||
<span>{course.enrolledCount.toLocaleString()}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Rating */}
|
|
||||||
<div className="flex items-center gap-2 mt-2">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
{[...Array(5)].map((_, i) => (
|
|
||||||
<Star
|
|
||||||
key={i}
|
|
||||||
className={`h-4 w-4 ${
|
|
||||||
i < Math.floor(course.rating)
|
|
||||||
? 'text-yellow-500 fill-current'
|
|
||||||
: 'text-muted-foreground'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<span className="text-base font-medium">{course.rating}</span>
|
|
||||||
<span className="text-base text-muted-foreground">
|
|
||||||
({course.completionRate}% completion rate)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Level and Category */}
|
|
||||||
<div className="flex items-center gap-2 mt-2">
|
|
||||||
<Badge variant="outline" className={`text-xs ${getLevelColor(course.level)}`}>
|
|
||||||
{course.level}
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="secondary" className="text-xs">
|
|
||||||
{course.category}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tags */}
|
|
||||||
<div className="flex flex-wrap gap-1 mt-2">
|
|
||||||
{course.tags.slice(0, 3).map((tag) => (
|
|
||||||
<Badge key={tag} variant="outline" className="text-xs">
|
|
||||||
{tag}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
{course.tags.length > 3 && (
|
|
||||||
<Badge variant="outline" className="text-xs">
|
|
||||||
+{course.tags.length - 3} more
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<CardContent className="pt-0">
|
|
||||||
{/* Progress Bar for In-Progress Courses */}
|
|
||||||
{course.status === 'in-progress' && course.progress !== undefined && (
|
|
||||||
<div className="space-y-2 mb-4">
|
|
||||||
<div className="flex justify-between text-base">
|
|
||||||
<span>Progress</span>
|
|
||||||
<span>{course.progress}%</span>
|
|
||||||
</div>
|
|
||||||
<Progress value={course.progress} className="h-2" />
|
|
||||||
{course.lastAccessed && (
|
|
||||||
<p className="text-base text-muted-foreground">
|
|
||||||
Last accessed: {course.lastAccessed}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Deadline Warning for Corporate */}
|
|
||||||
{userType === 'corporate' && course.deadline && (
|
|
||||||
<div className={`p-3 rounded-lg mb-4 ${
|
|
||||||
isOverdue
|
|
||||||
? 'bg-destructive/5 border border-destructive/20'
|
|
||||||
: 'bg-orange-50 border border-orange-200'
|
|
||||||
}`}>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<AlertCircle className={`h-4 w-4 ${isOverdue ? 'text-destructive' : 'text-orange-600'}`} />
|
|
||||||
<span className={`text-base font-medium ${isOverdue ? 'text-destructive' : 'text-orange-600'}`}>
|
|
||||||
{isOverdue ? 'Overdue' : 'Due'}: {new Date(course.deadline).toLocaleDateString()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Action Buttons */}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{course.status === 'not-started' && (
|
|
||||||
<Button
|
|
||||||
onClick={handleCourseNavigation}
|
|
||||||
className="flex-1 text-base min-h-[44px]"
|
|
||||||
>
|
|
||||||
<Play className="h-4 w-4 mr-2" />
|
|
||||||
Start Course
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{course.status === 'in-progress' && (
|
|
||||||
<Button
|
|
||||||
onClick={handleCourseNavigation}
|
|
||||||
className="flex-1 text-base min-h-[44px]"
|
|
||||||
>
|
|
||||||
<Play className="h-4 w-4 mr-2" />
|
|
||||||
Continue Learning
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{course.status === 'completed' && (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleCourseNavigation}
|
|
||||||
className="flex-1 text-base min-h-[44px]"
|
|
||||||
>
|
|
||||||
<Eye className="h-4 w-4 mr-2" />
|
|
||||||
Review Course
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{course.status === 'bookmarked' && (
|
|
||||||
<Button
|
|
||||||
onClick={handleCourseNavigation}
|
|
||||||
className="flex-1 text-base min-h-[44px]"
|
|
||||||
>
|
|
||||||
<Play className="h-4 w-4 mr-2" />
|
|
||||||
Start Course
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Secondary Actions */}
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => onBookmark?.(course.id)}
|
|
||||||
className="min-h-[44px] min-w-[44px]"
|
|
||||||
aria-label="Bookmark course"
|
|
||||||
>
|
|
||||||
<Heart className={`h-4 w-4 ${course.status === 'bookmarked' ? 'fill-current text-red-500' : ''}`} />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
className="min-h-[44px] min-w-[44px]"
|
|
||||||
aria-label="Share course"
|
|
||||||
>
|
|
||||||
<Share2 className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{course.certificate && course.status === 'completed' && (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
className="min-h-[44px] min-w-[44px]"
|
|
||||||
aria-label="Download certificate"
|
|
||||||
>
|
|
||||||
<Award className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,268 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Button } from '../ui/button';
|
|
||||||
import { Badge } from '../ui/badge';
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar';
|
|
||||||
import {
|
|
||||||
Star,
|
|
||||||
Clock,
|
|
||||||
Users,
|
|
||||||
Play,
|
|
||||||
CheckCircle,
|
|
||||||
Heart,
|
|
||||||
MoreHorizontal,
|
|
||||||
Award,
|
|
||||||
BookOpen,
|
|
||||||
Eye,
|
|
||||||
Calendar,
|
|
||||||
Target
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { Course } from '../../pages/learner/data/libraryData';
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
interface CourseListItemProps {
|
|
||||||
course: Course;
|
|
||||||
userType: 'individual' | 'corporate';
|
|
||||||
onEnroll?: (courseId: string) => void;
|
|
||||||
onContinue?: (courseId: string) => void;
|
|
||||||
onBookmark?: (courseId: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'completed': return 'text-success bg-success/10';
|
|
||||||
case 'in-progress': return 'text-primary bg-primary/10';
|
|
||||||
case 'bookmarked': return 'text-orange-600 bg-orange-100';
|
|
||||||
case 'not-started': return 'text-muted-foreground bg-muted/50';
|
|
||||||
default: return 'text-muted-foreground bg-muted/50';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCourseIcon = (category: string) => {
|
|
||||||
switch (category) {
|
|
||||||
case 'Leadership': return '👑';
|
|
||||||
case 'Personal Development': return '🧠';
|
|
||||||
case 'Team Management': return '👥';
|
|
||||||
case 'Digital Leadership': return '💻';
|
|
||||||
case 'Communication': return '💬';
|
|
||||||
case 'Crisis Management': return '⚡';
|
|
||||||
default: return '📚';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCourseGradient = (category: string) => {
|
|
||||||
switch (category) {
|
|
||||||
case 'Leadership': return 'bg-gradient-to-br from-blue-500 via-purple-500 to-pink-500';
|
|
||||||
case 'Personal Development': return 'bg-gradient-to-br from-green-400 via-blue-500 to-purple-600';
|
|
||||||
case 'Team Management': return 'bg-gradient-to-br from-orange-400 via-pink-500 to-red-500';
|
|
||||||
case 'Digital Leadership': return 'bg-gradient-to-br from-cyan-400 via-blue-500 to-indigo-600';
|
|
||||||
case 'Communication': return 'bg-gradient-to-br from-yellow-400 via-orange-500 to-red-500';
|
|
||||||
case 'Crisis Management': return 'bg-gradient-to-br from-red-400 via-pink-500 to-purple-600';
|
|
||||||
default: return 'bg-gradient-to-br from-gray-400 via-gray-500 to-gray-600';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export function CourseListItem({ course, userType, onEnroll, onContinue, onBookmark }: CourseListItemProps) {
|
|
||||||
const isOverdue = course.deadline && new Date(course.deadline) < new Date();
|
|
||||||
|
|
||||||
// Navigate to course details page with proper query parameters
|
|
||||||
const handleCourseNavigation = () => {
|
|
||||||
navigate(`/course?view=${userType}&courseId=${course.id}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-start gap-4 p-4 bg-background border border-border rounded-lg hover:shadow-md transition-all duration-200">
|
|
||||||
{/* Course Thumbnail */}
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<div className={`w-16 h-16 rounded-lg ${getCourseGradient(course.category)} flex items-center justify-center text-2xl relative overflow-hidden`}>
|
|
||||||
<span className="relative z-10">{getCourseIcon(course.category)}</span>
|
|
||||||
<div className="absolute inset-0 bg-black/10" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Course Content */}
|
|
||||||
<div className="flex-1 min-w-0 space-y-2">
|
|
||||||
{/* Title and Badges */}
|
|
||||||
<div className="flex items-start justify-between gap-3">
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<h3
|
|
||||||
className="text-lg font-semibold text-foreground line-clamp-1 hover:text-primary transition-colors cursor-pointer"
|
|
||||||
onClick={handleCourseNavigation}
|
|
||||||
>
|
|
||||||
{course.title}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 flex-shrink-0">
|
|
||||||
{course.isPremium && (
|
|
||||||
<Badge variant="secondary" className="text-xs bg-yellow-100 text-yellow-800">
|
|
||||||
Premium
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{course.isFeatured && (
|
|
||||||
<Badge className="text-xs bg-primary text-primary-foreground">
|
|
||||||
Featured
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
<Badge variant="outline" className={`text-xs ${getStatusColor(course.status)}`}>
|
|
||||||
{course.status === 'not-started' ? 'Available' : course.status.replace('-', ' ').toUpperCase()}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
<p className="text-base text-muted-foreground line-clamp-2 leading-relaxed">
|
|
||||||
{course.description}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Course Metadata */}
|
|
||||||
<div className="flex items-center gap-4 text-base text-muted-foreground">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Clock className="h-4 w-4" />
|
|
||||||
<span>{course.duration}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<BookOpen className="h-4 w-4" />
|
|
||||||
<span>{course.lessonsCount} lessons</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Users className="h-4 w-4" />
|
|
||||||
<span>{course.enrolledCount.toLocaleString()}</span>
|
|
||||||
</div>
|
|
||||||
<Badge variant="outline" className="text-xs">
|
|
||||||
{course.level}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Progress Bar for In-Progress Courses */}
|
|
||||||
{course.status === 'in-progress' && course.progress !== undefined && (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="flex justify-between text-base">
|
|
||||||
<span className="text-muted-foreground">Progress</span>
|
|
||||||
<span className="font-medium">{course.progress}%</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-full bg-muted rounded-full h-2">
|
|
||||||
<div
|
|
||||||
className="bg-primary h-2 rounded-full transition-all duration-300"
|
|
||||||
style={{ width: `${course.progress}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Deadline Warning for Corporate */}
|
|
||||||
{userType === 'corporate' && course.deadline && (
|
|
||||||
<div className={`p-2 rounded-md text-base flex items-center gap-2 ${
|
|
||||||
isOverdue
|
|
||||||
? 'bg-destructive/10 text-destructive'
|
|
||||||
: 'bg-orange-50 text-orange-700'
|
|
||||||
}`}>
|
|
||||||
<Calendar className="h-4 w-4" />
|
|
||||||
<span className="font-medium">
|
|
||||||
{isOverdue ? 'Overdue' : 'Due'}: {new Date(course.deadline).toLocaleDateString()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Instructor and Rating Row */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-base text-muted-foreground">Created by:</span>
|
|
||||||
<Avatar className="w-6 h-6">
|
|
||||||
<AvatarImage src={course.instructor.avatar} />
|
|
||||||
<AvatarFallback className="text-xs">
|
|
||||||
{course.instructor.name.split(' ').map(n => n[0]).join('')}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<span className="text-base font-medium text-foreground">{course.instructor.name}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{/* Rating */}
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
{[...Array(5)].map((_, i) => (
|
|
||||||
<Star
|
|
||||||
key={i}
|
|
||||||
className={`h-4 w-4 ${
|
|
||||||
i < Math.floor(course.rating)
|
|
||||||
? 'text-yellow-500 fill-current'
|
|
||||||
: 'text-muted-foreground'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<span className="text-base font-medium ml-1">{course.rating}</span>
|
|
||||||
<span className="text-base text-muted-foreground">
|
|
||||||
({course.completionRate}%)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Action Buttons */}
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
{course.status === 'not-started' && (
|
|
||||||
<Button
|
|
||||||
onClick={handleCourseNavigation}
|
|
||||||
size="sm"
|
|
||||||
className="text-base min-h-[36px]"
|
|
||||||
>
|
|
||||||
<Play className="h-4 w-4 mr-2" />
|
|
||||||
Start
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{course.status === 'in-progress' && (
|
|
||||||
<Button
|
|
||||||
onClick={handleCourseNavigation}
|
|
||||||
size="sm"
|
|
||||||
className="text-base min-h-[36px]"
|
|
||||||
>
|
|
||||||
<Play className="h-4 w-4 mr-2" />
|
|
||||||
Continue
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{course.status === 'completed' && (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleCourseNavigation}
|
|
||||||
size="sm"
|
|
||||||
className="text-base min-h-[36px]"
|
|
||||||
>
|
|
||||||
<Eye className="h-4 w-4 mr-2" />
|
|
||||||
Review
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{course.status === 'bookmarked' && (
|
|
||||||
<Button
|
|
||||||
onClick={handleCourseNavigation}
|
|
||||||
size="sm"
|
|
||||||
className="text-base min-h-[36px]"
|
|
||||||
>
|
|
||||||
<Play className="h-4 w-4 mr-2" />
|
|
||||||
Start
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => onBookmark?.(course.id)}
|
|
||||||
className="min-h-[36px] min-w-[36px] hover:bg-muted"
|
|
||||||
aria-label="Bookmark course"
|
|
||||||
>
|
|
||||||
<Heart className={`h-4 w-4 ${course.status === 'bookmarked' ? 'fill-current text-red-500' : 'text-muted-foreground'}`} />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="min-h-[36px] min-w-[36px] hover:bg-muted"
|
|
||||||
aria-label="More options"
|
|
||||||
>
|
|
||||||
<MoreHorizontal className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,299 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Button } from '../ui/button';
|
|
||||||
import { Badge } from '../ui/badge';
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar';
|
|
||||||
import {
|
|
||||||
Star,
|
|
||||||
Clock,
|
|
||||||
Users,
|
|
||||||
Play,
|
|
||||||
CheckCircle,
|
|
||||||
Heart,
|
|
||||||
BookOpen,
|
|
||||||
ChevronRight,
|
|
||||||
MoreHorizontal
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { Course } from '../../pages/learner/data/libraryData';
|
|
||||||
import { ImageWithFallback } from '../figma/ImageWithFallback';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
interface HorizontalCourseCardProps {
|
|
||||||
course: Course;
|
|
||||||
userType: 'individual' | 'corporate';
|
|
||||||
onEnroll?: (courseId: string) => void;
|
|
||||||
onContinue?: (courseId: string) => void;
|
|
||||||
onBookmark?: (courseId: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Course images based on course category
|
|
||||||
const getCourseImage = (category: string) => {
|
|
||||||
const patterns = {
|
|
||||||
'Leadership': (
|
|
||||||
<div className="absolute inset-0 overflow-hidden">
|
|
||||||
<div className="absolute inset-0 opacity-100">
|
|
||||||
<ImageWithFallback
|
|
||||||
src="https://images.unsplash.com/photo-1658198420916-951923730cdd?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxsZWFkZXJzaGlwJTIwYm9va3N8ZW58MXx8fHwxNzU1ODQzNDIxfDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral"
|
|
||||||
alt=""
|
|
||||||
className="w-full h-full object-cover object-right"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
'Personal Development': (
|
|
||||||
<div className="absolute inset-0 overflow-hidden">
|
|
||||||
<div className="absolute inset-0 opacity-100">
|
|
||||||
<ImageWithFallback
|
|
||||||
src="https://images.unsplash.com/photo-1668092547893-6402c0387885?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxidXNpbmVzcyUyMGVkdWNhdGlvbiUyMGxlYXJuaW5nfGVufDF8fHx8MTc1NTg0MzQwOHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral"
|
|
||||||
alt=""
|
|
||||||
className="w-full h-full object-cover object-center"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
'Team Management': (
|
|
||||||
<div className="absolute inset-0 overflow-hidden">
|
|
||||||
<div className="absolute inset-0 opacity-100">
|
|
||||||
<ImageWithFallback
|
|
||||||
src="https://images.unsplash.com/photo-1668092547893-6402c0387885?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHhidXNpbmVzcyUyMGVkdWNhdGlvbiUyMGxlYXJuaW5nfGVufDF8fHx8MTc1NTg0MzQwOHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral"
|
|
||||||
alt=""
|
|
||||||
className="w-full h-full object-cover object-center"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
'Digital Leadership': (
|
|
||||||
<div className="absolute inset-0 overflow-hidden">
|
|
||||||
<div className="absolute inset-0 opacity-100">
|
|
||||||
<ImageWithFallback
|
|
||||||
src="https://images.unsplash.com/photo-1588912914078-2fe5224fd8b8?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxvbmxpbmUlMjBjb3Vyc2UlMjBsYXB0b3B8ZW58MXx8fHwxNzU1NzIwMTYyfDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral"
|
|
||||||
alt=""
|
|
||||||
className="w-full h-full object-cover object-center"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
'Communication': (
|
|
||||||
<div className="absolute inset-0 overflow-hidden">
|
|
||||||
<div className="absolute inset-0 opacity-100">
|
|
||||||
<ImageWithFallback
|
|
||||||
src="https://images.unsplash.com/photo-1668092547893-6402c0387885?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxidXNpbmVzcyUyMGVkdWNhdGlvbiUyMGxlYXJuaW5nfGVufDF8fHx8MTc1NTg0MzQwOHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral"
|
|
||||||
alt=""
|
|
||||||
className="w-full h-full object-cover object-center"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
return patterns[category as keyof typeof patterns] || patterns.Leadership;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function HorizontalCourseCard({ course, userType, onEnroll, onContinue, onBookmark }: HorizontalCourseCardProps) {
|
|
||||||
const courseImage = getCourseImage(course.category);
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
|
|
||||||
// Navigate to course details page with proper query parameters
|
|
||||||
const handleCourseNavigation = () => {
|
|
||||||
navigate(`/course?view=${userType}&courseId=${course.id}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-background border border-border rounded-lg overflow-hidden hover:shadow-lg transition-all duration-200"
|
|
||||||
style={{ height: '260px', minHeight: '260px' }}>
|
|
||||||
<div className="flex h-full">
|
|
||||||
{/* Left Image Section - 50% width, full height */}
|
|
||||||
<div className="w-1/2 relative overflow-hidden">
|
|
||||||
{/* Course image overlay */}
|
|
||||||
{courseImage}
|
|
||||||
|
|
||||||
{/* Status indicators - Positioned better for larger image area */}
|
|
||||||
<div className="absolute top-3 left-3 flex flex-wrap gap-1.5 z-10 max-w-[70%]">
|
|
||||||
{course.isFeatured && (
|
|
||||||
<Badge className="text-sm bg-white/90 text-foreground hover:bg-white font-medium">
|
|
||||||
Featured
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{course.isPremium && (
|
|
||||||
<Badge variant="secondary" className="text-sm bg-[#F8C301]/90 text-[#26231A] font-medium">
|
|
||||||
Premium
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Course status indicator */}
|
|
||||||
<div className="absolute top-3 right-3 z-10">
|
|
||||||
{course.status === 'completed' && (
|
|
||||||
<div className="bg-success/90 backdrop-blur-sm p-1.5 rounded-full">
|
|
||||||
<CheckCircle className="h-4 w-4 text-white" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{course.status === 'bookmarked' && (
|
|
||||||
<div className="bg-[#F8C301]/90 backdrop-blur-sm p-1.5 rounded-full">
|
|
||||||
<Heart className="h-4 w-4 text-[#26231A] fill-current" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Content Section - 50% width with increased spacing */}
|
|
||||||
<div className="w-1/2 p-5 flex flex-col justify-between">
|
|
||||||
{/* Top Content - Title and Description stay with original spacing */}
|
|
||||||
<div>
|
|
||||||
{/* Course Title - Increased to text-xl (20px) as requested */}
|
|
||||||
<h3 className="text-xl font-semibold text-foreground line-clamp-2 leading-tight mb-2">
|
|
||||||
{course.title}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
{/* Description with 8px spacing from title as requested */}
|
|
||||||
<p className="text-sm text-muted-foreground line-clamp-2 leading-relaxed mb-4">
|
|
||||||
{course.description}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Elements after description - Increased vertical spacing to 12px (space-y-3) */}
|
|
||||||
<div className="space-y-3">
|
|
||||||
{/* Created by section - Updated font size for Avatar text */}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Avatar className="w-5 h-5 flex-shrink-0">
|
|
||||||
<AvatarImage src={course.instructor.avatar} />
|
|
||||||
<AvatarFallback className="text-sm">
|
|
||||||
{course.instructor.name.split(' ').map(n => n[0]).join('')}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<span className="text-sm text-muted-foreground">Created by:</span>
|
|
||||||
<span className="text-sm font-medium text-foreground truncate">{course.instructor.name}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Badges below created by - Updated to text-sm (14px minimum) with brand colors */}
|
|
||||||
<div className="flex flex-wrap gap-1.5">
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className={`text-sm font-medium ${
|
|
||||||
course.level === 'Beginner' ? 'border-success text-success bg-success/5' :
|
|
||||||
course.level === 'Intermediate' ? 'border-primary text-primary bg-primary/5' :
|
|
||||||
course.level === 'Advanced' ? 'border-[#26231A] text-[#26231A] bg-[#26231A]/5' :
|
|
||||||
'border-[#F8C301] text-[#26231A] bg-[#F8C301]/10'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{course.level}
|
|
||||||
</Badge>
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className={`text-sm font-medium ${
|
|
||||||
course.category === 'Leadership' ? 'border-primary text-primary bg-primary/5' :
|
|
||||||
course.category === 'Digital Leadership' ? 'border-[#F8C301] text-[#26231A] bg-[#F8C301]/10' :
|
|
||||||
course.category === 'Strategy' ? 'border-[#26231A] text-[#26231A] bg-[#26231A]/5' :
|
|
||||||
'border-[#F8C301] text-[#26231A] bg-[#F8C301]/10'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{course.category}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Combined lessons, duration, and rating section - All in one container */}
|
|
||||||
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
|
||||||
{/* Left side: Duration and Lessons */}
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Clock className="h-3.5 w-3.5 flex-shrink-0" />
|
|
||||||
<span>{course.duration}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<BookOpen className="h-3.5 w-3.5 flex-shrink-0" />
|
|
||||||
<span>{course.lessonsCount} lessons</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right side: Rating stars */}
|
|
||||||
<div className="flex items-center gap-1 flex-shrink-0">
|
|
||||||
{[...Array(5)].map((_, i) => (
|
|
||||||
<Star
|
|
||||||
key={i}
|
|
||||||
className={`h-3.5 w-3.5 ${
|
|
||||||
i < Math.floor(course.rating)
|
|
||||||
? 'text-[#F8C301] fill-current'
|
|
||||||
: 'text-muted-foreground'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<span className="text-sm font-medium ml-1 text-foreground">{course.rating}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bottom Section - CTA buttons only with reduced spacing */}
|
|
||||||
<div className="mt-auto">
|
|
||||||
{/* CTA buttons below everything else - wider for consistency */}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{course.status === 'not-started' && (
|
|
||||||
<Button
|
|
||||||
onClick={handleCourseNavigation}
|
|
||||||
size="sm"
|
|
||||||
className="flex-1 text-base font-medium min-h-[36px]"
|
|
||||||
>
|
|
||||||
Start Course
|
|
||||||
<ChevronRight className="h-4 w-4 ml-1" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{course.status === 'in-progress' && (
|
|
||||||
<Button
|
|
||||||
onClick={handleCourseNavigation}
|
|
||||||
size="sm"
|
|
||||||
className="flex-1 text-base font-medium min-h-[36px]"
|
|
||||||
>
|
|
||||||
Continue
|
|
||||||
<Play className="h-4 w-4 ml-1" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{course.status === 'completed' && (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleCourseNavigation}
|
|
||||||
size="sm"
|
|
||||||
className="flex-1 text-base font-medium min-h-[36px]"
|
|
||||||
>
|
|
||||||
Review
|
|
||||||
<ChevronRight className="h-4 w-4 ml-1" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{course.status === 'bookmarked' && (
|
|
||||||
<Button
|
|
||||||
onClick={handleCourseNavigation}
|
|
||||||
size="sm"
|
|
||||||
className="flex-1 text-base font-medium min-h-[36px]"
|
|
||||||
>
|
|
||||||
Start Course
|
|
||||||
<ChevronRight className="h-4 w-4 ml-1" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Secondary action buttons - optimized sizing */}
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => onBookmark?.(course.id)}
|
|
||||||
className="min-h-[36px] min-w-[36px] hover:bg-muted flex-shrink-0"
|
|
||||||
aria-label="Bookmark course"
|
|
||||||
>
|
|
||||||
<Heart className={`h-4 w-4 ${course.status === 'bookmarked' ? 'fill-current text-[#F8C301]' : 'text-muted-foreground'}`} />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="min-h-[36px] min-w-[36px] hover:bg-muted flex-shrink-0"
|
|
||||||
aria-label="More options"
|
|
||||||
>
|
|
||||||
<MoreHorizontal className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,547 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { Button } from '../ui/button';
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar';
|
|
||||||
import { Badge } from '../ui/badge';
|
|
||||||
import { Sheet, SheetContent, SheetTrigger } from '../ui/sheet';
|
|
||||||
import { ScrollArea } from '../ui/scroll-area';
|
|
||||||
import { Separator } from '../ui/separator';
|
|
||||||
import logo from "../../assets/klc-logo.png"
|
|
||||||
import {
|
|
||||||
Menu,
|
|
||||||
Search,
|
|
||||||
Bell,
|
|
||||||
ChevronDown,
|
|
||||||
Home,
|
|
||||||
BookOpen,
|
|
||||||
User,
|
|
||||||
BarChart3,
|
|
||||||
MessageSquare,
|
|
||||||
Calendar,
|
|
||||||
Trophy,
|
|
||||||
Building2,
|
|
||||||
Users,
|
|
||||||
X,
|
|
||||||
Settings
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
interface LearnerLayoutProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
currentPage?: string;
|
|
||||||
userType?: 'individual' | 'corporate';
|
|
||||||
user?: {
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
avatar?: string;
|
|
||||||
organization?: string;
|
|
||||||
orgLogo?: string;
|
|
||||||
role?: string;
|
|
||||||
cohort?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NotificationPanelProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
notifications: Array<{
|
|
||||||
id: string;
|
|
||||||
type: 'info' | 'warning' | 'success' | 'error';
|
|
||||||
title: string;
|
|
||||||
message: string;
|
|
||||||
time: string;
|
|
||||||
read: boolean;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function NotificationPanel({ isOpen, onClose, notifications }: NotificationPanelProps) {
|
|
||||||
const unreadCount = notifications.filter(n => !n.read).length;
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
if (!isOpen) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 z-50 lg:relative lg:inset-auto">
|
|
||||||
{/* Mobile overlay */}
|
|
||||||
<div className="lg:hidden fixed inset-0 bg-black/50" onClick={onClose} />
|
|
||||||
|
|
||||||
{/* Panel */}
|
|
||||||
<div className="fixed right-0 top-0 h-full w-80 bg-card border-l border-border shadow-xl lg:absolute lg:top-full lg:right-0 lg:h-auto lg:max-h-96 lg:rounded-lg lg:border lg:shadow-lg">
|
|
||||||
<div className="flex items-center justify-between p-4 border-b border-border">
|
|
||||||
<h3 className="font-semibold text-lg">
|
|
||||||
Notifications {unreadCount > 0 && <Badge variant="secondary" className="ml-2">{unreadCount}</Badge>}
|
|
||||||
</h3>
|
|
||||||
<Button variant="ghost" size="sm" onClick={onClose}>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ScrollArea className="h-80 lg:h-64">
|
|
||||||
<div className="p-4 space-y-3">
|
|
||||||
{notifications.length === 0 ? (
|
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
|
||||||
<Bell className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
|
||||||
<p>No notifications</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
notifications.map((notification) => (
|
|
||||||
<div
|
|
||||||
key={notification.id}
|
|
||||||
className={`p-3 rounded-lg border ${notification.read ? 'bg-muted/50' : 'bg-background'} hover:bg-muted/70 transition-colors cursor-pointer`}
|
|
||||||
>
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<div className={`w-2 h-2 rounded-full mt-2 flex-shrink-0 ${notification.type === 'info' ? 'bg-blue-500' :
|
|
||||||
notification.type === 'success' ? 'bg-success' :
|
|
||||||
notification.type === 'warning' ? 'bg-yellow-500' :
|
|
||||||
'bg-destructive'
|
|
||||||
}`} />
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p className="font-medium text-lg text-foreground">{notification.title}</p>
|
|
||||||
<p className="text-lg text-muted-foreground mt-1">{notification.message}</p>
|
|
||||||
<p className="text-sm text-muted-foreground mt-2">{notification.time}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
|
|
||||||
{notifications.length > 0 && (
|
|
||||||
<div className="p-4 border-t border-border">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="w-full text-lg min-h-[44px]"
|
|
||||||
onClick={() => navigate('/notifications')}
|
|
||||||
>
|
|
||||||
View All Notifications
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function LearnerLayout({ children, currentPage, userType = 'individual', user }: LearnerLayoutProps) {
|
|
||||||
// Get current path if not provided
|
|
||||||
const currentPath = currentPage || window.location.pathname;
|
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
||||||
const [notificationsOpen, setNotificationsOpen] = useState(false);
|
|
||||||
const [searchValue, setSearchValue] = useState('');
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
|
|
||||||
// Get current view parameter directly from URL
|
|
||||||
const getCurrentView = () => {
|
|
||||||
const searchParams = new URLSearchParams(window.location.search);
|
|
||||||
return searchParams.get('view');
|
|
||||||
};
|
|
||||||
|
|
||||||
const [currentView, setCurrentView] = useState(getCurrentView);
|
|
||||||
|
|
||||||
// Update URL params when location changes
|
|
||||||
useEffect(() => {
|
|
||||||
const handleLocationChange = () => {
|
|
||||||
setCurrentView(getCurrentView());
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('popstate', handleLocationChange);
|
|
||||||
|
|
||||||
// Listen for programmatic navigation changes
|
|
||||||
const originalPushState = window.history.pushState;
|
|
||||||
const originalReplaceState = window.history.replaceState;
|
|
||||||
|
|
||||||
window.history.pushState = function (...args) {
|
|
||||||
originalPushState.apply(window.history, args);
|
|
||||||
setTimeout(handleLocationChange, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.history.replaceState = function (...args) {
|
|
||||||
originalReplaceState.apply(window.history, args);
|
|
||||||
setTimeout(handleLocationChange, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('popstate', handleLocationChange);
|
|
||||||
window.history.pushState = originalPushState;
|
|
||||||
window.history.replaceState = originalReplaceState;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Mock notifications
|
|
||||||
const notifications = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
type: 'info' as const,
|
|
||||||
title: 'New Course Available',
|
|
||||||
message: 'Strategic Leadership Foundations is now available in your library',
|
|
||||||
time: '2 hours ago',
|
|
||||||
read: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
type: 'warning' as const,
|
|
||||||
title: 'Assignment Due Soon',
|
|
||||||
message: 'Leadership Assessment due in 3 days',
|
|
||||||
time: '1 day ago',
|
|
||||||
read: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
type: 'success' as const,
|
|
||||||
title: 'Certificate Earned',
|
|
||||||
message: 'Congratulations! You completed Management Essentials',
|
|
||||||
time: '3 days ago',
|
|
||||||
read: true
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const unreadCount = notifications.filter(n => !n.read).length;
|
|
||||||
|
|
||||||
// Navigation items with simplified active state logic
|
|
||||||
const navigationItems = [
|
|
||||||
{
|
|
||||||
name: 'Dashboard',
|
|
||||||
icon: Home,
|
|
||||||
href: `/dashboard?view=${userType}`,
|
|
||||||
active: currentPath === '/dashboard' && (currentView === userType || (!currentView && userType === 'individual'))
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Library',
|
|
||||||
icon: BookOpen,
|
|
||||||
href: `/library?view=${userType}`,
|
|
||||||
active: currentPath === '/library' && (currentView === userType || (!currentView && userType === 'individual'))
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Surveys',
|
|
||||||
icon: MessageSquare,
|
|
||||||
href: `/surveys?view=${userType}`,
|
|
||||||
active: currentPath === '/surveys' && (currentView === userType || (!currentView && userType === 'individual'))
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Webinars',
|
|
||||||
icon: Calendar,
|
|
||||||
href: `/webinars?view=${userType}`,
|
|
||||||
active: (currentPath === '/webinars' || currentPath === '/individual-webinars') && (currentView === userType || (!currentView && userType === 'individual'))
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Leaderboard',
|
|
||||||
icon: Trophy,
|
|
||||||
href: `/leaderboard?view=${userType}`,
|
|
||||||
active: currentPath === '/leaderboard' && (currentView === userType || (!currentView && userType === 'individual'))
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Settings',
|
|
||||||
icon: Settings,
|
|
||||||
href: `/settings?view=${userType}`,
|
|
||||||
active: currentPath?.startsWith('/settings') && (currentView === userType || (!currentView && userType === 'individual'))
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const Sidebar = ({ className = "" }: { className?: string }) => (
|
|
||||||
<div className={`flex flex-col h-full bg-brand-navy ${className}`}>
|
|
||||||
{/* Logo */}
|
|
||||||
<div className="p-6 border-b border-white/20">
|
|
||||||
{/* <div className="flex items-center gap-3">
|
|
||||||
<div className="w-8 h-8 bg-brand-gold rounded-lg flex items-center justify-center">
|
|
||||||
<span className="text-brand-gold-foreground font-bold text-sm">KLC</span>
|
|
||||||
</div>
|
|
||||||
<span className="font-semibold text-white text-lg">Learning Portal</span>
|
|
||||||
</div> */}
|
|
||||||
<img
|
|
||||||
src={logo}
|
|
||||||
alt="Logo"
|
|
||||||
className="h-6 md:h-8 lg:h-10 w-auto object-contain"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Navigation */}
|
|
||||||
<ScrollArea className="flex-1 px-4 py-6">
|
|
||||||
<nav className="space-y-2">
|
|
||||||
{navigationItems.map((item) => {
|
|
||||||
const Icon = item.icon;
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
key={item.name}
|
|
||||||
variant={item.active ? "secondary" : "ghost"}
|
|
||||||
className={`w-full justify-start text-lg h-10 min-h-[44px] ${item.active
|
|
||||||
? 'bg-brand-gold text-brand-gold-foreground hover:bg-brand-gold/90'
|
|
||||||
: 'text-white/80 hover:bg-white/10 hover:text-white'
|
|
||||||
}`}
|
|
||||||
onClick={() => {
|
|
||||||
navigate(item.href);
|
|
||||||
setSidebarOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon className="mr-3 h-4 w-4" />
|
|
||||||
{item.name}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-background">
|
|
||||||
{/* Mobile Header */}
|
|
||||||
<header className="lg:hidden border-b border-border bg-card/95 backdrop-blur supports-[backdrop-filter]:bg-card/60">
|
|
||||||
<div className="flex items-center justify-between p-4">
|
|
||||||
<Sheet open={sidebarOpen} onOpenChange={setSidebarOpen}>
|
|
||||||
<SheetTrigger asChild>
|
|
||||||
<Button variant="ghost" size="icon">
|
|
||||||
<Menu className="h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
</SheetTrigger>
|
|
||||||
<SheetContent side="left" className="p-0 w-80">
|
|
||||||
<Sidebar />
|
|
||||||
</SheetContent>
|
|
||||||
</Sheet>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{/* Corporate org badge */}
|
|
||||||
{userType === 'corporate' && user?.organization && (
|
|
||||||
<div className="flex items-center gap-2 px-3 py-1 bg-muted rounded-full">
|
|
||||||
{user.orgLogo && (
|
|
||||||
<img src={user.orgLogo} alt={user.organization} className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
<span className="text-sm font-medium">{user.organization}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Notifications */}
|
|
||||||
<div className="relative">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => setNotificationsOpen(!notificationsOpen)}
|
|
||||||
className="relative"
|
|
||||||
>
|
|
||||||
<Bell className="h-5 w-5" />
|
|
||||||
{unreadCount > 0 && (
|
|
||||||
<Badge className="absolute -top-1 -right-1 h-5 w-5 p-0 text-xs flex items-center justify-center">
|
|
||||||
{unreadCount}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<NotificationPanel
|
|
||||||
isOpen={notificationsOpen}
|
|
||||||
onClose={() => setNotificationsOpen(false)}
|
|
||||||
notifications={notifications}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* User menu */}
|
|
||||||
<Avatar className="h-8 w-8">
|
|
||||||
<AvatarImage src={user?.avatar} />
|
|
||||||
<AvatarFallback className="text-sm">
|
|
||||||
{user?.name?.split(' ').map(n => n[0]).join('') || 'U'}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div className="flex">
|
|
||||||
{/* Desktop Sidebar - Reduced width from 256px to 240px */}
|
|
||||||
<div className="hidden lg:block w-60 border-r border-border">
|
|
||||||
<div className="fixed w-60 h-full">
|
|
||||||
<Sidebar />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Content Area - Optimized for wider content */}
|
|
||||||
<div className="flex-1 lg:ml-0">
|
|
||||||
{/* Desktop Header */}
|
|
||||||
<header className="hidden lg:block border-b border-border bg-card/95 backdrop-blur supports-[backdrop-filter]:bg-card/60">
|
|
||||||
<div className="flex items-center justify-end px-4 py-4">
|
|
||||||
{/* <div className="flex-1 max-w-sm">
|
|
||||||
<div className="relative">
|
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Search courses, resources..."
|
|
||||||
value={searchValue}
|
|
||||||
onChange={(e) => setSearchValue(e.target.value)}
|
|
||||||
className="w-full pl-10 pr-4 py-2 text-lg border border-border rounded-lg bg-background focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div> */}
|
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{/* Corporate features */}
|
|
||||||
{userType === 'corporate' && (
|
|
||||||
<>
|
|
||||||
{/* Organization badge */}
|
|
||||||
{user?.organization && (
|
|
||||||
<div className="flex items-center gap-2 px-3 py-1 bg-muted rounded-full">
|
|
||||||
{user.orgLogo && (
|
|
||||||
<img src={user.orgLogo} alt={user.organization} className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
<span className="text-lg font-medium">{user.organization}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Cohort reminder */}
|
|
||||||
{user?.cohort && (
|
|
||||||
<Badge variant="outline" className="text-sm">
|
|
||||||
<Users className="w-3 h-3 mr-1" />
|
|
||||||
{user.cohort}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Notifications */}
|
|
||||||
<div className="relative">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => setNotificationsOpen(!notificationsOpen)}
|
|
||||||
className="relative"
|
|
||||||
>
|
|
||||||
<Bell className="h-5 w-5" />
|
|
||||||
{unreadCount > 0 && (
|
|
||||||
<Badge className="absolute -top-1 -right-1 h-5 w-5 p-0 text-xs flex items-center justify-center">
|
|
||||||
{unreadCount}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<NotificationPanel
|
|
||||||
isOpen={notificationsOpen}
|
|
||||||
onClose={() => setNotificationsOpen(false)}
|
|
||||||
notifications={notifications}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* User menu */}
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Avatar className="h-8 w-8">
|
|
||||||
<AvatarImage src={user?.avatar} />
|
|
||||||
<AvatarFallback className="text-base">
|
|
||||||
{user?.name?.split(' ').map(n => n[0]).join('') || 'U'}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div className="hidden xl:block">
|
|
||||||
<p className="text-lg font-medium">{user?.name || 'Priya Sharma'}</p>
|
|
||||||
<p className="text-sm text-muted-foreground">{user?.email || 'priya.sharma@example.com'}</p>
|
|
||||||
</div>
|
|
||||||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* Page Content - Remove default padding, let pages control their own spacing */}
|
|
||||||
<main
|
|
||||||
className="flex-1 min-h-screen bg-background"
|
|
||||||
role="main"
|
|
||||||
id="main-content"
|
|
||||||
tabIndex={-1}
|
|
||||||
>
|
|
||||||
{/* Content wrapper with consistent spacing and accessibility */}
|
|
||||||
<div className="w-full min-h-full">
|
|
||||||
{/* Skip to main content anchor for screen readers */}
|
|
||||||
<a
|
|
||||||
href="#learner-content"
|
|
||||||
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 bg-[#04045B] text-white px-4 py-2 rounded-lg text-base font-medium focus:outline-none focus:ring-2 focus:ring-[#F8C301] focus:ring-offset-2 transition-all duration-200"
|
|
||||||
>
|
|
||||||
Skip to learner content
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{/* Main learner content area */}
|
|
||||||
<div
|
|
||||||
id="learner-content"
|
|
||||||
className="w-full"
|
|
||||||
role="region"
|
|
||||||
aria-label="Learner portal content"
|
|
||||||
>
|
|
||||||
{/* Content with proper spacing and structure */}
|
|
||||||
<div className="relative">
|
|
||||||
{/* Background pattern for visual enhancement */}
|
|
||||||
<div className="absolute inset-0 opacity-[0.02] pointer-events-none">
|
|
||||||
<div className="w-full h-full bg-gradient-to-br from-[#04045B]/5 via-transparent to-[#F8C301]/5"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main content with proper spacing */}
|
|
||||||
<div className="relative z-10">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Live region for dynamic content announcements */}
|
|
||||||
<div
|
|
||||||
id="learner-live-region"
|
|
||||||
aria-live="polite"
|
|
||||||
aria-atomic="true"
|
|
||||||
className="sr-only"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
{/* Status region for form validation and success messages */}
|
|
||||||
<div
|
|
||||||
id="learner-status-region"
|
|
||||||
aria-live="assertive"
|
|
||||||
aria-atomic="true"
|
|
||||||
className="sr-only"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Back to top functionality for long content */}
|
|
||||||
<div className="fixed bottom-6 left-6 z-40">
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
document.getElementById('main-content')?.scrollIntoView({
|
|
||||||
behavior: 'smooth',
|
|
||||||
block: 'start'
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className="sr-only focus:not-sr-only bg-[#04045B] hover:bg-[#04045B]/90 text-white p-3 rounded-full shadow-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-[#F8C301] focus:ring-offset-2"
|
|
||||||
aria-label="Back to top of page"
|
|
||||||
title="Back to top"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="w-5 h-5"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M5 10l7-7m0 0l7 7m-7-7v18"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Progress indicator for course content */}
|
|
||||||
<div
|
|
||||||
id="learner-progress-indicator"
|
|
||||||
className="fixed top-[70px] left-0 right-0 h-1 bg-gray-200 opacity-0 transition-opacity duration-200 z-40"
|
|
||||||
role="progressbar"
|
|
||||||
aria-label="Page loading progress"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="h-full bg-gradient-to-r from-[#04045B] to-[#F8C301] transition-all duration-300 ease-out"
|
|
||||||
style={{ width: '0%' }}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,346 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Card, CardContent } from '../ui/card';
|
|
||||||
import { Button } from '../ui/button';
|
|
||||||
import { Input } from '../ui/input';
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
|
||||||
import { Badge } from '../ui/badge';
|
|
||||||
import {
|
|
||||||
Search,
|
|
||||||
Filter,
|
|
||||||
X,
|
|
||||||
Globe,
|
|
||||||
Video,
|
|
||||||
FileText,
|
|
||||||
Headphones,
|
|
||||||
Monitor
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { categories, levels, courseTypes, sortOptions } from '../../pages/learner/data/libraryData';
|
|
||||||
|
|
||||||
interface LibraryFiltersProps {
|
|
||||||
searchQuery: string;
|
|
||||||
setSearchQuery: (query: string) => void;
|
|
||||||
selectedCategory: string;
|
|
||||||
setSelectedCategory: (category: string) => void;
|
|
||||||
selectedLevel: string;
|
|
||||||
setSelectedLevel: (level: string) => void;
|
|
||||||
selectedType: string;
|
|
||||||
setSelectedType: (type: string) => void;
|
|
||||||
sortBy: string;
|
|
||||||
setSortBy: (sort: string) => void;
|
|
||||||
showBookmarkedOnly: boolean;
|
|
||||||
setShowBookmarkedOnly: (show: boolean) => void;
|
|
||||||
showInProgressOnly: boolean;
|
|
||||||
setShowInProgressOnly: (show: boolean) => void;
|
|
||||||
userType: 'individual' | 'corporate';
|
|
||||||
totalCourses: number;
|
|
||||||
filteredCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTypeIcon = (iconName: string) => {
|
|
||||||
switch (iconName) {
|
|
||||||
case 'Globe': return Globe;
|
|
||||||
case 'Video': return Video;
|
|
||||||
case 'FileText': return FileText;
|
|
||||||
case 'Headphones': return Headphones;
|
|
||||||
case 'Monitor': return Monitor;
|
|
||||||
default: return Globe;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export function LibraryFilters({
|
|
||||||
searchQuery,
|
|
||||||
setSearchQuery,
|
|
||||||
selectedCategory,
|
|
||||||
setSelectedCategory,
|
|
||||||
selectedLevel,
|
|
||||||
setSelectedLevel,
|
|
||||||
selectedType,
|
|
||||||
setSelectedType,
|
|
||||||
sortBy,
|
|
||||||
setSortBy,
|
|
||||||
showBookmarkedOnly,
|
|
||||||
setShowBookmarkedOnly,
|
|
||||||
showInProgressOnly,
|
|
||||||
setShowInProgressOnly,
|
|
||||||
userType,
|
|
||||||
totalCourses,
|
|
||||||
filteredCount
|
|
||||||
}: LibraryFiltersProps) {
|
|
||||||
const hasActiveFilters =
|
|
||||||
selectedCategory !== 'All Categories' ||
|
|
||||||
selectedLevel !== 'All Levels' ||
|
|
||||||
selectedType !== 'all' ||
|
|
||||||
showBookmarkedOnly ||
|
|
||||||
showInProgressOnly ||
|
|
||||||
searchQuery.length > 0;
|
|
||||||
|
|
||||||
const clearAllFilters = () => {
|
|
||||||
setSearchQuery('');
|
|
||||||
setSelectedCategory('All Categories');
|
|
||||||
setSelectedLevel('All Levels');
|
|
||||||
setSelectedType('all');
|
|
||||||
setShowBookmarkedOnly(false);
|
|
||||||
setShowInProgressOnly(false);
|
|
||||||
setSortBy('featured');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-6">
|
|
||||||
{/* Search Bar */}
|
|
||||||
<div className="relative mb-6">
|
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-5 w-5" />
|
|
||||||
<Input
|
|
||||||
placeholder="Search courses, instructors, topics..."
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
className="pl-10 text-base min-h-[44px]"
|
|
||||||
/>
|
|
||||||
{searchQuery && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setSearchQuery('')}
|
|
||||||
className="absolute right-2 top-1/2 transform -translate-y-1/2 h-7 w-7 p-0"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filter Controls */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
|
||||||
{/* Category Filter */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-base font-medium">Category</label>
|
|
||||||
<Select value={selectedCategory} onValueChange={setSelectedCategory}>
|
|
||||||
<SelectTrigger className="text-base min-h-[44px]">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{categories.map((category) => (
|
|
||||||
<SelectItem key={category} value={category} className="text-base">
|
|
||||||
{category}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Level Filter */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-base font-medium">Level</label>
|
|
||||||
<Select value={selectedLevel} onValueChange={setSelectedLevel}>
|
|
||||||
<SelectTrigger className="text-base min-h-[44px]">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{levels.map((level) => (
|
|
||||||
<SelectItem key={level} value={level} className="text-base">
|
|
||||||
{level}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Type Filter */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-base font-medium">Content Type</label>
|
|
||||||
<Select value={selectedType} onValueChange={setSelectedType}>
|
|
||||||
<SelectTrigger className="text-base min-h-[44px]">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{courseTypes.map((type) => {
|
|
||||||
const Icon = getTypeIcon(type.icon);
|
|
||||||
return (
|
|
||||||
<SelectItem key={type.value} value={type.value} className="text-base">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Icon className="h-4 w-4" />
|
|
||||||
{type.label}
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Sort Options */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-base font-medium">Sort By</label>
|
|
||||||
<Select value={sortBy} onValueChange={setSortBy}>
|
|
||||||
<SelectTrigger className="text-base min-h-[44px]">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{sortOptions.map((option) => (
|
|
||||||
<SelectItem key={option.value} value={option.value} className="text-base">
|
|
||||||
{option.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Quick Filter Buttons */}
|
|
||||||
<div className="flex flex-wrap gap-3 mb-6">
|
|
||||||
<Button
|
|
||||||
variant={showInProgressOnly ? "default" : "outline"}
|
|
||||||
onClick={() => setShowInProgressOnly(!showInProgressOnly)}
|
|
||||||
className={`text-base min-h-[40px] ${
|
|
||||||
showInProgressOnly
|
|
||||||
? 'bg-[#26231A] hover:bg-[#26231A]/90 text-white border-[#26231A]'
|
|
||||||
: 'border-[#26231A]/30 text-[#26231A] hover:bg-[#26231A]/10'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Filter className="h-4 w-4 mr-2" />
|
|
||||||
Continue Learning
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant={showBookmarkedOnly ? "default" : "outline"}
|
|
||||||
onClick={() => setShowBookmarkedOnly(!showBookmarkedOnly)}
|
|
||||||
className={`text-base min-h-[40px] ${
|
|
||||||
showBookmarkedOnly
|
|
||||||
? 'bg-[#F8C301] hover:bg-[#F8C301]/90 text-[#26231A] border-[#F8C301]'
|
|
||||||
: 'border-[#F8C301]/30 text-[#26231A] hover:bg-[#F8C301]/10'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Filter className="h-4 w-4 mr-2" />
|
|
||||||
Bookmarked
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{userType === 'corporate' && (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="text-base min-h-[40px] border-primary/30 text-primary hover:bg-primary/10"
|
|
||||||
>
|
|
||||||
<Filter className="h-4 w-4 mr-2" />
|
|
||||||
Assigned Courses
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Active Filters Display */}
|
|
||||||
{hasActiveFilters && (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-base font-medium">Active Filters:</span>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={clearAllFilters}
|
|
||||||
className="text-base h-auto p-1"
|
|
||||||
>
|
|
||||||
Clear All
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{searchQuery && (
|
|
||||||
<Badge variant="secondary" className="flex items-center gap-1 text-base px-3 py-1">
|
|
||||||
Search: "{searchQuery}"
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setSearchQuery('')}
|
|
||||||
className="h-4 w-4 p-0 hover:bg-transparent"
|
|
||||||
>
|
|
||||||
<X className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedCategory !== 'All Categories' && (
|
|
||||||
<Badge variant="secondary" className="flex items-center gap-1 text-base px-3 py-1">
|
|
||||||
{selectedCategory}
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setSelectedCategory('All Categories')}
|
|
||||||
className="h-4 w-4 p-0 hover:bg-transparent"
|
|
||||||
>
|
|
||||||
<X className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedLevel !== 'All Levels' && (
|
|
||||||
<Badge variant="secondary" className="flex items-center gap-1 text-base px-3 py-1">
|
|
||||||
{selectedLevel}
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setSelectedLevel('All Levels')}
|
|
||||||
className="h-4 w-4 p-0 hover:bg-transparent"
|
|
||||||
>
|
|
||||||
<X className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedType !== 'all' && (
|
|
||||||
<Badge variant="secondary" className="flex items-center gap-1 text-base px-3 py-1">
|
|
||||||
{courseTypes.find(t => t.value === selectedType)?.label}
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setSelectedType('all')}
|
|
||||||
className="h-4 w-4 p-0 hover:bg-transparent"
|
|
||||||
>
|
|
||||||
<X className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showBookmarkedOnly && (
|
|
||||||
<Badge variant="secondary" className="flex items-center gap-1 text-base px-3 py-1">
|
|
||||||
Bookmarked Only
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setShowBookmarkedOnly(false)}
|
|
||||||
className="h-4 w-4 p-0 hover:bg-transparent"
|
|
||||||
>
|
|
||||||
<X className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showInProgressOnly && (
|
|
||||||
<Badge variant="secondary" className="flex items-center gap-1 text-base px-3 py-1">
|
|
||||||
In Progress Only
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setShowInProgressOnly(false)}
|
|
||||||
className="h-4 w-4 p-0 hover:bg-transparent"
|
|
||||||
>
|
|
||||||
<X className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Results Summary */}
|
|
||||||
<div className="mt-4 pt-4 border-t border-border">
|
|
||||||
<p className="text-base text-muted-foreground">
|
|
||||||
Showing {filteredCount} of {totalCourses} courses
|
|
||||||
{hasActiveFilters && filteredCount !== totalCourses && (
|
|
||||||
<span className="ml-2 text-primary font-medium">
|
|
||||||
({totalCourses - filteredCount} filtered out)
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,595 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
|
|
||||||
import { Progress } from '../ui/progress';
|
|
||||||
import { Badge } from '../ui/badge';
|
|
||||||
import { Button } from '../ui/button';
|
|
||||||
import {
|
|
||||||
BookOpen,
|
|
||||||
Clock,
|
|
||||||
Trophy,
|
|
||||||
Target,
|
|
||||||
TrendingUp,
|
|
||||||
Award,
|
|
||||||
CheckCircle,
|
|
||||||
Calendar,
|
|
||||||
Users,
|
|
||||||
Star,
|
|
||||||
Play,
|
|
||||||
Bookmark,
|
|
||||||
Search,
|
|
||||||
Zap,
|
|
||||||
ArrowRight,
|
|
||||||
BarChart3,
|
|
||||||
Brain,
|
|
||||||
Sparkles,
|
|
||||||
Flame,
|
|
||||||
ChevronRight,
|
|
||||||
Gauge,
|
|
||||||
Activity,
|
|
||||||
ChevronLeft,
|
|
||||||
ChevronDown,
|
|
||||||
MoreHorizontal
|
|
||||||
} from 'lucide-react';
|
|
||||||
import {
|
|
||||||
BarChart,
|
|
||||||
Bar,
|
|
||||||
XAxis,
|
|
||||||
YAxis,
|
|
||||||
CartesianGrid,
|
|
||||||
ResponsiveContainer,
|
|
||||||
Tooltip,
|
|
||||||
PieChart,
|
|
||||||
Pie,
|
|
||||||
Cell,
|
|
||||||
Legend
|
|
||||||
} from 'recharts';
|
|
||||||
import { Course } from '../../pages/learner/data/libraryData';
|
|
||||||
|
|
||||||
interface LibraryStatsProps {
|
|
||||||
courses: Course[];
|
|
||||||
userType: 'individual' | 'corporate';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function LibraryStats({ courses, userType }: LibraryStatsProps) {
|
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
|
||||||
const [selectedTimeRange, setSelectedTimeRange] = useState('December');
|
|
||||||
|
|
||||||
// Calculate statistics
|
|
||||||
const stats = {
|
|
||||||
totalCourses: courses.length,
|
|
||||||
completedCourses: courses.filter(c => c.status === 'completed').length,
|
|
||||||
inProgressCourses: courses.filter(c => c.status === 'in-progress').length,
|
|
||||||
bookmarkedCourses: courses.filter(c => c.status === 'bookmarked').length,
|
|
||||||
assignedCourses: userType === 'corporate' ? courses.filter(c => c.organizationAssigned).length : 0,
|
|
||||||
overdueCourses: userType === 'corporate' ? courses.filter(c => c.status === 'not-started').length : undefined,
|
|
||||||
completionRate: courses.length > 0 ? (courses.filter(c => c.status === 'completed').length / courses.length) * 100 : 0,
|
|
||||||
averageRating: courses.length > 0 ? courses.reduce((sum, c) => sum + c.rating, 0) / courses.length : 0,
|
|
||||||
totalHours: courses.reduce((sum, c) => sum + parseFloat(c.duration.replace(/[^\d.]/g, '')), 0),
|
|
||||||
certificatesEarned: courses.filter(c => c.status === 'completed').length
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const timer = setTimeout(() => setIsVisible(true), 100);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Performance chart data (bar chart showing progress over time)
|
|
||||||
const performanceData = [
|
|
||||||
{ name: 'Jan', value: 1, color: '#04045B' },
|
|
||||||
{ name: 'Feb', value: 2, color: '#04045B' },
|
|
||||||
{ name: 'Mar', value: 4, color: '#04045B' },
|
|
||||||
{ name: 'Apr', value: 6, color: '#04045B' },
|
|
||||||
{ name: 'May', value: 7, color: '#04045B' },
|
|
||||||
{ name: 'Jun', value: 8, color: '#04045B' }
|
|
||||||
];
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Time learning data (stacked bar chart)
|
|
||||||
const timeLearningData = [
|
|
||||||
{ date: 'Apr 18', performance: 2, consistency: 1, unknown: 0.5 },
|
|
||||||
{ date: 'Apr 19', performance: 3, consistency: 2, unknown: 1 },
|
|
||||||
{ date: 'Apr 20', performance: 1.5, consistency: 2.5, unknown: 0.5 },
|
|
||||||
{ date: 'Apr 21', performance: 4, consistency: 1, unknown: 1 },
|
|
||||||
{ date: 'Apr 22', performance: 2, consistency: 3, unknown: 0.5 },
|
|
||||||
{ date: 'Apr 23', performance: 3.5, consistency: 1.5, unknown: 1 },
|
|
||||||
{ date: 'Apr 24', performance: 2.5, consistency: 2, unknown: 1.5 }
|
|
||||||
];
|
|
||||||
|
|
||||||
// Course list with progress
|
|
||||||
const courseList = [
|
|
||||||
{ name: 'Introduction to Strategic Leadership', lessons: '24/30 Lessons', progress: 80, color: '#04045B' },
|
|
||||||
{ name: 'English for Effective Communication', lessons: '18/25 Lessons', progress: 72, color: '#10B981' },
|
|
||||||
{ name: 'Introduction to Team Management', lessons: '14/20 Lessons', progress: 70, color: '#F8C301' },
|
|
||||||
{ name: 'Introduction to Digital Leadership', lessons: '8/15 Lessons', progress: 53, color: '#6366F1' }
|
|
||||||
];
|
|
||||||
|
|
||||||
// Daily activity timeline
|
|
||||||
const dailyActivities = [
|
|
||||||
{ time: '07:00', course: 'Introduction to Strategic Leadership', type: 'Google Meeting', color: '#F8C301', instructor: 'A' },
|
|
||||||
{ time: '08:00', course: '', type: '', color: '', instructor: '' },
|
|
||||||
{ time: '09:00', course: 'English for Effective Communication', type: 'Google Meeting', color: '#3B82F6', instructor: 'E' },
|
|
||||||
{ time: '10:00', course: '', type: '', color: '', instructor: '' },
|
|
||||||
{ time: '11:00', course: '', type: '', color: '', instructor: '' },
|
|
||||||
{ time: '12:00', course: 'Introduction to Digital Leadership', type: 'Google Meeting', color: '#6366F1', instructor: 'I' },
|
|
||||||
{ time: '01:00', course: '', type: '', color: '', instructor: '' }
|
|
||||||
];
|
|
||||||
|
|
||||||
// Month navigation functionality
|
|
||||||
const months = [
|
|
||||||
'January', 'February', 'March', 'April', 'May', 'June',
|
|
||||||
'July', 'August', 'September', 'October', 'November', 'December'
|
|
||||||
];
|
|
||||||
|
|
||||||
const navigateMonth = (direction: 'prev' | 'next') => {
|
|
||||||
const currentIndex = months.findIndex(month => month === selectedTimeRange);
|
|
||||||
let newIndex;
|
|
||||||
|
|
||||||
if (direction === 'prev') {
|
|
||||||
newIndex = currentIndex <= 0 ? months.length - 1 : currentIndex - 1;
|
|
||||||
} else {
|
|
||||||
newIndex = currentIndex >= months.length - 1 ? 0 : currentIndex + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedTimeRange(months[newIndex]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleWheel = (e: React.WheelEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
// Throttle wheel events to prevent rapid scrolling
|
|
||||||
const now = Date.now();
|
|
||||||
const lastWheelTime = (e.currentTarget as any)._lastWheelTime || 0;
|
|
||||||
|
|
||||||
if (now - lastWheelTime < 200) return; // 200ms throttle
|
|
||||||
|
|
||||||
(e.currentTarget as any)._lastWheelTime = now;
|
|
||||||
|
|
||||||
if (e.deltaY > 0) {
|
|
||||||
navigateMonth('next');
|
|
||||||
} else {
|
|
||||||
navigateMonth('prev');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`space-y-4 transition-all duration-500 ${isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'}`}>
|
|
||||||
{/* Dashboard Grid Layout - Matching Reference Image */}
|
|
||||||
<div className="grid grid-cols-12 gap-4">
|
|
||||||
|
|
||||||
{/* Top Row */}
|
|
||||||
|
|
||||||
{/* Simplified Performance Chart */}
|
|
||||||
<div className="col-span-12 lg:col-span-6">
|
|
||||||
<Card className="border-0 shadow-md bg-white h-full">
|
|
||||||
<CardHeader className="pb-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<CardTitle className="text-[16px] font-semibold text-[#111827]">
|
|
||||||
Performance
|
|
||||||
</CardTitle>
|
|
||||||
<MoreHorizontal className="h-4 w-4 text-[#6B7280]" />
|
|
||||||
</div>
|
|
||||||
<div className="text-[14px] text-[#6B7280]">
|
|
||||||
{stats.completedCourses} Course Completed
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<CardContent className="pt-0 pb-4">
|
|
||||||
<ResponsiveContainer width="100%" height={140}>
|
|
||||||
<BarChart data={performanceData} margin={{ top: 15, right: 15, left: 15, bottom: 15 }}>
|
|
||||||
<defs>
|
|
||||||
<filter id="performanceShadow" x="-50%" y="-50%" width="200%" height="200%">
|
|
||||||
<feDropShadow dx="0" dy="2" stdDeviation="2" floodColor="#04045B" floodOpacity="0.3"/>
|
|
||||||
</filter>
|
|
||||||
</defs>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#E5E7EB" opacity={0.4} />
|
|
||||||
<Bar
|
|
||||||
dataKey="value"
|
|
||||||
fill="#04045B"
|
|
||||||
radius={[4, 4, 0, 0]}
|
|
||||||
maxBarSize={24}
|
|
||||||
filter="url(#performanceShadow)"
|
|
||||||
className="hover:opacity-80 transition-opacity duration-200"
|
|
||||||
/>
|
|
||||||
<XAxis
|
|
||||||
dataKey="name"
|
|
||||||
axisLine={false}
|
|
||||||
tickLine={false}
|
|
||||||
tick={{ fontSize: 13, fill: '#6B7280', fontWeight: 500 }}
|
|
||||||
/>
|
|
||||||
<YAxis
|
|
||||||
axisLine={false}
|
|
||||||
tickLine={false}
|
|
||||||
tick={{ fontSize: 11, fill: '#9CA3AF' }}
|
|
||||||
width={25}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
cursor={false}
|
|
||||||
contentStyle={{
|
|
||||||
backgroundColor: 'white',
|
|
||||||
border: '1px solid #E5E7EB',
|
|
||||||
borderRadius: '8px',
|
|
||||||
fontSize: '14px',
|
|
||||||
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
|
||||||
padding: '12px'
|
|
||||||
}}
|
|
||||||
formatter={(value) => [`${value} courses`, 'Completed']}
|
|
||||||
labelStyle={{ color: '#111827', fontWeight: 600, marginBottom: '4px' }}
|
|
||||||
/>
|
|
||||||
</BarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Simplified Time Learning Chart */}
|
|
||||||
<div className="col-span-12 lg:col-span-6">
|
|
||||||
<Card className="border-0 shadow-md bg-white h-full">
|
|
||||||
<CardHeader className="pb-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<CardTitle className="text-[16px] font-semibold text-[#111827] mb-1">
|
|
||||||
Time spend on learning
|
|
||||||
</CardTitle>
|
|
||||||
<div className="text-[14px] text-[#6B7280]">
|
|
||||||
4 Course Completed
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div
|
|
||||||
className="flex items-center gap-2 cursor-pointer select-none hover:bg-gray-50 rounded-lg px-3 py-2 transition-colors duration-200"
|
|
||||||
onWheel={handleWheel}
|
|
||||||
title="Scroll to change month or click arrows"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
onClick={() => navigateMonth('prev')}
|
|
||||||
className="h-4 w-4 text-[#6B7280] hover:text-[#04045B] transition-colors duration-200 flex items-center justify-center"
|
|
||||||
aria-label="Previous month"
|
|
||||||
>
|
|
||||||
<ChevronLeft className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
<span className="text-[14px] font-medium text-[#111827] min-w-[70px] text-center transition-all duration-200">
|
|
||||||
{selectedTimeRange}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
onClick={() => navigateMonth('next')}
|
|
||||||
className="h-4 w-4 text-[#6B7280] hover:text-[#04045B] transition-colors duration-200 flex items-center justify-center"
|
|
||||||
aria-label="Next month"
|
|
||||||
>
|
|
||||||
<ChevronRight className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<CardContent className="pt-0 pb-4">
|
|
||||||
<div className="mb-4">
|
|
||||||
<ResponsiveContainer width="100%" height={120}>
|
|
||||||
<BarChart data={timeLearningData} margin={{ top: 10, right: 10, left: 10, bottom: 10 }}>
|
|
||||||
<defs>
|
|
||||||
<filter id="stackShadow" x="-20%" y="-20%" width="140%" height="140%">
|
|
||||||
<feDropShadow dx="0" dy="1" stdDeviation="1" floodColor="#000000" floodOpacity="0.2"/>
|
|
||||||
</filter>
|
|
||||||
</defs>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#E5E7EB" opacity={0.3} />
|
|
||||||
<Bar
|
|
||||||
dataKey="performance"
|
|
||||||
stackId="a"
|
|
||||||
fill="#04045B"
|
|
||||||
radius={[0, 0, 0, 0]}
|
|
||||||
maxBarSize={28}
|
|
||||||
filter="url(#stackShadow)"
|
|
||||||
/>
|
|
||||||
<Bar
|
|
||||||
dataKey="consistency"
|
|
||||||
stackId="a"
|
|
||||||
fill="#6366F1"
|
|
||||||
radius={[0, 0, 0, 0]}
|
|
||||||
maxBarSize={28}
|
|
||||||
/>
|
|
||||||
<Bar
|
|
||||||
dataKey="unknown"
|
|
||||||
stackId="a"
|
|
||||||
fill="#E5E7EB"
|
|
||||||
radius={[3, 3, 0, 0]}
|
|
||||||
maxBarSize={28}
|
|
||||||
/>
|
|
||||||
<XAxis
|
|
||||||
dataKey="date"
|
|
||||||
axisLine={false}
|
|
||||||
tickLine={false}
|
|
||||||
tick={{ fontSize: 12, fill: '#6B7280', fontWeight: 500 }}
|
|
||||||
/>
|
|
||||||
<YAxis
|
|
||||||
axisLine={false}
|
|
||||||
tickLine={false}
|
|
||||||
tick={{ fontSize: 10, fill: '#9CA3AF' }}
|
|
||||||
width={25}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
cursor={false}
|
|
||||||
contentStyle={{
|
|
||||||
backgroundColor: 'white',
|
|
||||||
border: '1px solid #E5E7EB',
|
|
||||||
borderRadius: '8px',
|
|
||||||
fontSize: '14px',
|
|
||||||
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
|
||||||
padding: '12px'
|
|
||||||
}}
|
|
||||||
labelStyle={{ color: '#111827', fontWeight: 600, marginBottom: '4px' }}
|
|
||||||
/>
|
|
||||||
</BarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Enhanced Legend */}
|
|
||||||
<div className="flex items-center justify-center gap-6 text-[14px]">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-3 h-3 rounded-sm bg-[#04045B] shadow-sm"></div>
|
|
||||||
<span className="text-[#6B7280] font-medium">Performance</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-3 h-3 rounded-sm bg-[#6366F1] shadow-sm"></div>
|
|
||||||
<span className="text-[#6B7280] font-medium">Consistency</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-3 h-3 rounded-sm bg-[#E5E7EB] shadow-sm"></div>
|
|
||||||
<span className="text-[#6B7280] font-medium">Other</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bottom Row */}
|
|
||||||
|
|
||||||
{/* Enhanced Your Courses (7 columns) */}
|
|
||||||
<div className="col-span-12 lg:col-span-7">
|
|
||||||
<Card className="border-0 shadow-md bg-white h-full">
|
|
||||||
<CardHeader className="pb-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<CardTitle className="text-[16px] font-semibold text-[#111827]">
|
|
||||||
Your Courses
|
|
||||||
</CardTitle>
|
|
||||||
<div className="text-[14px] text-[#6B7280]">
|
|
||||||
{courseList.length} Course{courseList.length !== 1 ? 's' : ''} • {courseList.filter(c => c.progress === 100).length} Completed
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="text-[14px] text-[#6B7280] hover:text-[#04045B] h-auto p-2 rounded-lg transition-colors"
|
|
||||||
onClick={() => window.location.href = '/library?view=individual'}
|
|
||||||
>
|
|
||||||
<span className="mr-1">View All</span>
|
|
||||||
<ArrowRight className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<CardContent className="pt-0 pb-4">
|
|
||||||
<div className="space-y-3">
|
|
||||||
{courseList.map((course, index) => {
|
|
||||||
const isCompleted = course.progress === 100;
|
|
||||||
const isInProgress = course.progress > 0 && course.progress < 100;
|
|
||||||
const isNotStarted = course.progress === 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="group relative flex items-center gap-4 p-3 hover:bg-gray-50 hover:shadow-sm rounded-lg transition-all duration-200 cursor-pointer border border-transparent hover:border-gray-200"
|
|
||||||
onClick={() => window.location.href = '/course?view=individual'}
|
|
||||||
>
|
|
||||||
{/* Enhanced Course Avatar */}
|
|
||||||
<div className="relative flex-shrink-0">
|
|
||||||
<div
|
|
||||||
className="w-8 h-8 rounded-lg flex items-center justify-center text-white text-[14px] font-semibold shadow-sm"
|
|
||||||
style={{ backgroundColor: course.color }}
|
|
||||||
>
|
|
||||||
{course.name.charAt(0)}
|
|
||||||
</div>
|
|
||||||
{/* Status Indicator */}
|
|
||||||
{isCompleted && (
|
|
||||||
<div className="absolute -top-1 -right-1 w-3 h-3 bg-[#21A36A] rounded-full flex items-center justify-center">
|
|
||||||
<CheckCircle className="w-2 h-2 text-white" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{isInProgress && (
|
|
||||||
<div className="absolute -top-1 -right-1 w-3 h-3 bg-[#F8C301] rounded-full animate-pulse"></div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Course Information */}
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-start justify-between gap-2 mb-1">
|
|
||||||
<h4 className="text-[16px] font-medium text-[#111827] truncate group-hover:text-[#04045B] transition-colors">
|
|
||||||
{course.name}
|
|
||||||
</h4>
|
|
||||||
<div className="flex items-center gap-1 flex-shrink-0">
|
|
||||||
<span className="text-[14px] font-medium text-[#111827]">
|
|
||||||
{course.progress}%
|
|
||||||
</span>
|
|
||||||
{isCompleted && (
|
|
||||||
<Badge variant="outline" className="text-[12px] bg-[#21A36A]/10 text-[#21A36A] border-[#21A36A]/20 px-2 py-0.5">
|
|
||||||
Complete
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-4 mb-2">
|
|
||||||
<span className="text-[14px] text-[#6B7280]">
|
|
||||||
{course.lessons}
|
|
||||||
</span>
|
|
||||||
<span className="text-[14px] text-[#6B7280]">
|
|
||||||
• {isCompleted ? 'Completed' : isInProgress ? 'In Progress' : 'Not Started'}
|
|
||||||
</span>
|
|
||||||
{isInProgress && (
|
|
||||||
<span className="text-[14px] text-[#F8C301] font-medium">
|
|
||||||
• Continue Learning
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Enhanced Progress Bar */}
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="flex-1 bg-gray-100 rounded-full h-2 overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="h-2 rounded-full transition-all duration-700 ease-out relative"
|
|
||||||
style={{
|
|
||||||
width: `${course.progress}%`,
|
|
||||||
backgroundColor: course.color
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Progress bar shine effect */}
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent animate-pulse"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Action Button */}
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
{isCompleted ? (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="h-8 px-3 text-[14px] border-[#21A36A]/20 text-[#21A36A] hover:bg-[#21A36A]/10"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
window.location.href = '/course?view=individual';
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Award className="w-3 h-3 mr-1" />
|
|
||||||
Review
|
|
||||||
</Button>
|
|
||||||
) : isInProgress ? (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="h-8 px-3 text-[14px] border-[#F8C301]/30 text-[#04045B] hover:bg-[#F8C301]/10"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
window.location.href = '/course?view=individual';
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Play className="w-3 h-3 mr-1 stroke-[#04045B]" />
|
|
||||||
Continue
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="h-8 px-3 text-[14px] border-[#04045B]/20 text-[#04045B] hover:bg-[#04045B]/10"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
window.location.href = '/course?view=individual';
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<BookOpen className="w-3 h-3 mr-1" />
|
|
||||||
Start
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Hover Arrow */}
|
|
||||||
<div className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
|
|
||||||
<ArrowRight className="h-4 w-4 text-[#6B7280]" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* Quick Action Footer */}
|
|
||||||
<div className="pt-2 mt-4 border-t border-gray-100">
|
|
||||||
<div className="flex items-center justify-between text-[14px]">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-2 h-2 bg-[#21A36A] rounded-full"></div>
|
|
||||||
<span className="text-[#6B7280]">Completed</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-2 h-2 bg-[#F8C301] rounded-full"></div>
|
|
||||||
<span className="text-[#6B7280]">In Progress</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-2 h-2 bg-gray-300 rounded-full"></div>
|
|
||||||
<span className="text-[#6B7280]">Not Started</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="text-[14px] text-[#6B7280] hover:text-[#04045B] h-auto p-1"
|
|
||||||
onClick={() => window.location.href = '/library?view=individual'}
|
|
||||||
>
|
|
||||||
<Target className="w-3 h-3 mr-1" />
|
|
||||||
Browse Library
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Daily Activity (5 columns) */}
|
|
||||||
<div className="col-span-12 lg:col-span-5">
|
|
||||||
<Card className="border-0 shadow-md bg-white h-full">
|
|
||||||
<CardHeader className="pb-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<CardTitle className="text-[16px] font-semibold text-[#111827]">
|
|
||||||
Daily activity
|
|
||||||
</CardTitle>
|
|
||||||
<div className="text-[14px] text-[#6B7280]">
|
|
||||||
Today Apr 24
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<CardContent className="pt-0 pb-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
{dailyActivities.map((activity, index) => (
|
|
||||||
<div key={index} className="flex items-center gap-3">
|
|
||||||
<div className="text-[12px] text-[#6B7280] w-10 text-right">
|
|
||||||
{activity.time}
|
|
||||||
</div>
|
|
||||||
<div className="w-px h-8 bg-gray-200 relative">
|
|
||||||
{activity.course && (
|
|
||||||
<div className="absolute -left-1 top-1/2 transform -translate-y-1/2 w-2 h-2 rounded-full" style={{ backgroundColor: activity.color }}></div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
{activity.course ? (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="text-[14px] font-medium text-[#111827] truncate">
|
|
||||||
{activity.course}
|
|
||||||
</div>
|
|
||||||
<div className="text-[12px] text-[#6B7280]">
|
|
||||||
{activity.type}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-6 h-6 rounded-full flex items-center justify-center text-white text-[10px] font-semibold flex-shrink-0" style={{ backgroundColor: activity.color }}>
|
|
||||||
{activity.instructor}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="h-8"></div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,419 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
|
|
||||||
import { Button } from '../ui/button';
|
|
||||||
import { Badge } from '../ui/badge';
|
|
||||||
import {
|
|
||||||
Calendar,
|
|
||||||
Clock,
|
|
||||||
Users,
|
|
||||||
Video,
|
|
||||||
ChevronLeft,
|
|
||||||
ChevronRight,
|
|
||||||
CalendarDays,
|
|
||||||
MapPin,
|
|
||||||
Globe,
|
|
||||||
ExternalLink
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
interface UpcomingEventsProps {
|
|
||||||
userType: 'individual' | 'corporate';
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Event {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
date: string;
|
|
||||||
time: string;
|
|
||||||
duration: string;
|
|
||||||
type: 'webinar' | 'workshop' | 'assessment' | 'meeting';
|
|
||||||
status: 'upcoming' | 'today' | 'live';
|
|
||||||
instructor: string;
|
|
||||||
category: string;
|
|
||||||
location: string;
|
|
||||||
attendees?: number;
|
|
||||||
maxAttendees?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function UpcomingEvents({ userType }: UpcomingEventsProps) {
|
|
||||||
const [currentMonth, setCurrentMonth] = useState(new Date());
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
|
|
||||||
// Sample events data
|
|
||||||
const events: Event[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
title: 'Strategic Leadership in Digital Transformation',
|
|
||||||
date: '2024-12-28',
|
|
||||||
time: '2:00 PM',
|
|
||||||
duration: '90 min',
|
|
||||||
type: 'webinar',
|
|
||||||
status: 'live',
|
|
||||||
instructor: 'Dr. Rajesh Kumar',
|
|
||||||
category: 'Leadership',
|
|
||||||
location: 'online',
|
|
||||||
attendees: 145,
|
|
||||||
maxAttendees: 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
title: 'Team Management Best Practices',
|
|
||||||
date: '2024-12-28',
|
|
||||||
time: '4:00 PM',
|
|
||||||
duration: '60 min',
|
|
||||||
type: 'workshop',
|
|
||||||
status: 'today',
|
|
||||||
instructor: 'Sarah Mitchell',
|
|
||||||
category: 'Team Management',
|
|
||||||
location: 'online',
|
|
||||||
attendees: 89,
|
|
||||||
maxAttendees: 150
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
title: 'Leadership Assessment Review',
|
|
||||||
date: '2024-12-30',
|
|
||||||
time: '10:00 AM',
|
|
||||||
duration: '45 min',
|
|
||||||
type: 'assessment',
|
|
||||||
status: 'upcoming',
|
|
||||||
instructor: 'Prof. Michael Chen',
|
|
||||||
category: 'Assessment',
|
|
||||||
location: 'Conference Room A',
|
|
||||||
attendees: 12,
|
|
||||||
maxAttendees: 15
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
title: 'Digital Leadership Fundamentals',
|
|
||||||
date: '2025-01-02',
|
|
||||||
time: '11:00 AM',
|
|
||||||
duration: '2 hours',
|
|
||||||
type: 'webinar',
|
|
||||||
status: 'upcoming',
|
|
||||||
instructor: 'Dr. Lisa Anderson',
|
|
||||||
category: 'Digital Leadership',
|
|
||||||
location: 'online',
|
|
||||||
attendees: 78,
|
|
||||||
maxAttendees: 200
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const monthNames = [
|
|
||||||
'January', 'February', 'March', 'April', 'May', 'June',
|
|
||||||
'July', 'August', 'September', 'October', 'November', 'December'
|
|
||||||
];
|
|
||||||
|
|
||||||
const navigateMonth = (direction: 'prev' | 'next') => {
|
|
||||||
setCurrentMonth(prevDate => {
|
|
||||||
const newDate = new Date(prevDate);
|
|
||||||
if (direction === 'prev') {
|
|
||||||
newDate.setMonth(newDate.getMonth() - 1);
|
|
||||||
} else {
|
|
||||||
newDate.setMonth(newDate.getMonth() + 1);
|
|
||||||
}
|
|
||||||
return newDate;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDaysInMonth = (date: Date) => {
|
|
||||||
return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFirstDayOfMonth = (date: Date) => {
|
|
||||||
return new Date(date.getFullYear(), date.getMonth(), 1).getDay();
|
|
||||||
};
|
|
||||||
|
|
||||||
const isToday = (date: Date, day: number) => {
|
|
||||||
const today = new Date();
|
|
||||||
return date.getFullYear() === today.getFullYear() &&
|
|
||||||
date.getMonth() === today.getMonth() &&
|
|
||||||
day === today.getDate();
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasEvent = (date: Date, day: number) => {
|
|
||||||
const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
||||||
return events.some(event => event.date === dateStr);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderCalendar = () => {
|
|
||||||
const daysInMonth = getDaysInMonth(currentMonth);
|
|
||||||
const firstDay = getFirstDayOfMonth(currentMonth);
|
|
||||||
const days = [];
|
|
||||||
|
|
||||||
// Empty cells for days before the first day of the month
|
|
||||||
for (let i = 0; i < firstDay; i++) {
|
|
||||||
days.push(
|
|
||||||
<div key={`empty-${i}`} className="h-8"></div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Days of the month
|
|
||||||
for (let day = 1; day <= daysInMonth; day++) {
|
|
||||||
const today = isToday(currentMonth, day);
|
|
||||||
const eventDay = hasEvent(currentMonth, day);
|
|
||||||
|
|
||||||
days.push(
|
|
||||||
<div
|
|
||||||
key={day}
|
|
||||||
className={`h-8 flex items-center justify-center text-[14px] font-medium rounded-md cursor-pointer transition-colors ${
|
|
||||||
today
|
|
||||||
? 'bg-[#F8C301] text-[#26231A] font-semibold'
|
|
||||||
: eventDay
|
|
||||||
? 'bg-[#04045B]/10 text-[#04045B] hover:bg-[#04045B]/20'
|
|
||||||
: 'text-[#6B7280] hover:bg-gray-100'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{day}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return days;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filter events for current month
|
|
||||||
const currentMonthEvents = events.filter(event => {
|
|
||||||
const eventDate = new Date(event.date);
|
|
||||||
return eventDate.getMonth() === currentMonth.getMonth() &&
|
|
||||||
eventDate.getFullYear() === currentMonth.getFullYear();
|
|
||||||
}).slice(0, 3); // Show only first 3 events
|
|
||||||
|
|
||||||
const getEventIcon = (type: string) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'webinar':
|
|
||||||
return Video;
|
|
||||||
case 'workshop':
|
|
||||||
return Users;
|
|
||||||
case 'assessment':
|
|
||||||
return CalendarDays;
|
|
||||||
case 'meeting':
|
|
||||||
return Calendar;
|
|
||||||
default:
|
|
||||||
return Calendar;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getEventColor = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'live':
|
|
||||||
return 'bg-red-50 border-red-200 hover:bg-red-100';
|
|
||||||
case 'today':
|
|
||||||
return 'bg-amber-50 border-amber-200 hover:bg-amber-100';
|
|
||||||
default:
|
|
||||||
return 'bg-gray-50 border-gray-200 hover:bg-gray-100';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Mini Calendar - Distinct White Card */}
|
|
||||||
<Card className="bg-white border-gray-200 shadow-lg hover:shadow-xl transition-all duration-300 rounded-lg">
|
|
||||||
<CardHeader className="pb-4 border-b border-gray-100">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<CardTitle className="text-[16px] font-semibold text-[#111827]">
|
|
||||||
{monthNames[currentMonth.getMonth()]} {currentMonth.getFullYear()}
|
|
||||||
</CardTitle>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => navigateMonth('prev')}
|
|
||||||
className="h-8 w-8 text-[#6B7280] hover:text-[#111827] hover:bg-gray-100 rounded-md"
|
|
||||||
aria-label="Previous month"
|
|
||||||
>
|
|
||||||
<ChevronLeft className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => navigateMonth('next')}
|
|
||||||
className="h-8 w-8 text-[#6B7280] hover:text-[#111827] hover:bg-gray-100 rounded-md"
|
|
||||||
aria-label="Next month"
|
|
||||||
>
|
|
||||||
<ChevronRight className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="pt-4">
|
|
||||||
<div className="grid grid-cols-7 gap-1 mb-3">
|
|
||||||
{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(day => (
|
|
||||||
<div key={day} className="h-8 flex items-center justify-center text-[12px] font-medium text-[#6B7280]">
|
|
||||||
{day}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-7 gap-1">
|
|
||||||
{renderCalendar()}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Upcoming Events - Distinct White Card */}
|
|
||||||
<Card className="bg-white border-gray-200 shadow-lg hover:shadow-xl transition-all duration-300 rounded-lg">
|
|
||||||
<CardHeader className="pb-4 border-b border-gray-100">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<CardTitle className="text-[16px] font-semibold flex items-center gap-2 text-[#111827]">
|
|
||||||
<div className="w-5 h-5 rounded-md bg-[#04045B] flex items-center justify-center">
|
|
||||||
<Calendar className="h-3 w-3 text-white" />
|
|
||||||
</div>
|
|
||||||
Upcoming Events
|
|
||||||
</CardTitle>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => navigate(`/webinars?view=${userType}`)}
|
|
||||||
className="text-[14px] text-[#6B7280] hover:text-[#111827] hover:bg-gray-100 rounded-md h-8 px-2"
|
|
||||||
>
|
|
||||||
View All
|
|
||||||
<ExternalLink className="h-3 w-3 ml-1" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="pt-4 space-y-3">
|
|
||||||
{currentMonthEvents.length === 0 ? (
|
|
||||||
<div className="text-center py-8">
|
|
||||||
<div className="w-12 h-12 rounded-lg bg-gray-100 flex items-center justify-center mx-auto mb-3">
|
|
||||||
<CalendarDays className="h-6 w-6 text-[#6B7280]" />
|
|
||||||
</div>
|
|
||||||
<p className="text-[14px] text-[#6B7280] font-medium">No events scheduled for this month</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
currentMonthEvents.map((event) => {
|
|
||||||
const Icon = getEventIcon(event.type);
|
|
||||||
const eventDate = new Date(event.date);
|
|
||||||
const isToday = eventDate.toDateString() === new Date().toDateString();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={event.id}
|
|
||||||
className={`p-3 rounded-lg border transition-all duration-300 cursor-pointer hover:shadow-md ${getEventColor(event.status)}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<div className={`p-2 rounded-full flex-shrink-0 ${
|
|
||||||
event.status === 'live'
|
|
||||||
? 'bg-red-100 text-red-600'
|
|
||||||
: event.status === 'today'
|
|
||||||
? 'bg-amber-100 text-amber-700'
|
|
||||||
: 'bg-[#04045B]/10 text-[#04045B]'
|
|
||||||
}`}>
|
|
||||||
<Icon className="h-4 w-4" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-start justify-between gap-2 mb-1">
|
|
||||||
<h4 className="text-[14px] font-semibold line-clamp-2 text-[#111827]">{event.title}</h4>
|
|
||||||
{event.status === 'live' && (
|
|
||||||
<Badge variant="destructive" className="text-[10px] flex-shrink-0 font-semibold">
|
|
||||||
LIVE
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{isToday && event.status !== 'live' && (
|
|
||||||
<Badge className="text-[10px] flex-shrink-0 bg-[#F8C301] text-[#26231A] hover:bg-[#F8C301]/90 font-semibold">
|
|
||||||
Today
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-1 text-[12px] text-[#6B7280]">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Clock className="h-3 w-3" />
|
|
||||||
<span className="font-medium">{event.time} • {event.duration}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{event.location === 'online' ? (
|
|
||||||
<>
|
|
||||||
<Globe className="h-3 w-3" />
|
|
||||||
<span className="font-medium">Online</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<MapPin className="h-3 w-3" />
|
|
||||||
<span className="line-clamp-1 font-medium">{event.location}</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{event.attendees && event.maxAttendees && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Users className="h-3 w-3" />
|
|
||||||
<span className="font-medium">{event.attendees}/{event.maxAttendees} attending</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="text-[12px]">
|
|
||||||
<span className="font-semibold text-[#111827]">{event.instructor}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between mt-3">
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className={`text-[10px] font-medium border ${
|
|
||||||
event.category === 'Leadership' ? 'border-[#04045B]/30 text-[#04045B] bg-[#04045B]/5' :
|
|
||||||
event.category === 'Digital Leadership' ? 'border-[#F8C301]/30 text-[#8A6A00] bg-[#F8C301]/10' :
|
|
||||||
event.category === 'Team Management' ? 'border-[#10B981]/30 text-[#10B981] bg-[#10B981]/5' :
|
|
||||||
'border-[#6B7280]/30 text-[#6B7280] bg-[#6B7280]/5'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{event.category}
|
|
||||||
</Badge>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant={event.status === 'live' ? 'destructive' : event.status === 'today' ? 'default' : 'outline'}
|
|
||||||
className="text-[12px] font-semibold min-h-[28px] px-3 rounded-md"
|
|
||||||
onClick={() => navigate(`/webinars?view=${userType}`)}
|
|
||||||
>
|
|
||||||
{event.status === 'live' ? 'Join Now' : 'View Details'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Quick Actions - Distinct White Card */}
|
|
||||||
<Card className="bg-white border-gray-200 shadow-lg hover:shadow-xl transition-all duration-300 rounded-lg">
|
|
||||||
<CardHeader className="pb-4 border-b border-gray-100">
|
|
||||||
<CardTitle className="text-[16px] font-semibold text-[#111827]">Quick Actions</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="pt-4 space-y-3">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="w-full justify-start text-[14px] font-medium min-h-[40px] bg-gray-50 border-gray-200 hover:bg-gray-100 transition-colors duration-200 rounded-md"
|
|
||||||
onClick={() => navigate(`/webinars?view=${userType}`)}
|
|
||||||
>
|
|
||||||
<Video className="h-4 w-4 mr-3" />
|
|
||||||
Browse All Webinars
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="w-full justify-start text-[14px] font-medium min-h-[40px] bg-gray-50 border-gray-200 hover:bg-gray-100 transition-colors duration-200 rounded-md"
|
|
||||||
onClick={() => navigate(`/surveys?view=${userType}`)}
|
|
||||||
>
|
|
||||||
<CalendarDays className="h-4 w-4 mr-3" />
|
|
||||||
Schedule Assessment
|
|
||||||
</Button>
|
|
||||||
{userType === 'corporate' && (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="w-full justify-start text-[14px] font-medium min-h-[40px] bg-gray-50 border-gray-200 hover:bg-gray-100 transition-colors duration-200 rounded-md"
|
|
||||||
onClick={() => navigate(`/dashboard?view=${userType}`)}
|
|
||||||
>
|
|
||||||
<Users className="h-4 w-4 mr-3" />
|
|
||||||
Team Calendar
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,362 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Card, CardContent } from '../ui/card';
|
|
||||||
import { TrendingUp, Target, Users, Brain, CheckCircle } from 'lucide-react';
|
|
||||||
|
|
||||||
// Icon mapping for dynamic rendering
|
|
||||||
const iconMap = {
|
|
||||||
Target, Users, CheckCircle, Brain, TrendingUp
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
// What We Offer Section - Card-based Layout
|
|
||||||
interface OfferingItem {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
icon: keyof typeof iconMap;
|
|
||||||
features: string[];
|
|
||||||
highlighted?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EnhancedOfferingsProps {
|
|
||||||
title: string;
|
|
||||||
subtitle: string;
|
|
||||||
offerings: OfferingItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EnhancedOfferings({ title, subtitle, offerings }: EnhancedOfferingsProps) {
|
|
||||||
return (
|
|
||||||
<section className="py-16 lg:py-24 bg-background">
|
|
||||||
<div className="container mx-auto px-4 lg:px-8">
|
|
||||||
{/* Header Section */}
|
|
||||||
<div className="text-center mb-16 lg:mb-20 max-w-4xl mx-auto">
|
|
||||||
<div className="text-base font-medium text-muted-foreground uppercase tracking-wider mb-4">
|
|
||||||
SERVICES
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 className="text-3xl lg:text-4xl xl:text-5xl font-bold text-foreground mb-6 leading-tight">
|
|
||||||
{title}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<p className="text-base lg:text-lg text-muted-foreground leading-relaxed max-w-2xl mx-auto">
|
|
||||||
{subtitle}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Card Grid Layout */}
|
|
||||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 lg:gap-8 max-w-7xl mx-auto">
|
|
||||||
{offerings.map((offering, index) => {
|
|
||||||
const Icon = iconMap[offering.icon];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
key={index}
|
|
||||||
className={`group h-full border transition-all duration-300 hover:shadow-lg ${
|
|
||||||
offering.highlighted
|
|
||||||
? 'border-primary/30 bg-primary/[0.02] hover:border-primary/50'
|
|
||||||
: 'border-border hover:border-primary/30'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<CardContent className="p-6 h-full flex flex-col">
|
|
||||||
{/* Icon */}
|
|
||||||
<div className="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center mb-4 group-hover:bg-primary/15 transition-colors duration-300">
|
|
||||||
<Icon className="w-6 h-6 text-primary" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Title */}
|
|
||||||
<h3 className="text-lg lg:text-xl font-bold text-foreground mb-3 leading-tight group-hover:text-primary transition-colors duration-300">
|
|
||||||
{offering.title}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
<p className="text-base text-muted-foreground leading-relaxed mb-6 flex-grow">
|
|
||||||
{offering.description}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Features List */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
{offering.features.slice(0, 3).map((feature, featureIndex) => (
|
|
||||||
<div key={featureIndex} className="flex items-start gap-2">
|
|
||||||
<CheckCircle className="w-4 h-4 text-success flex-shrink-0 mt-0.5" />
|
|
||||||
<span className="text-base text-muted-foreground leading-relaxed">
|
|
||||||
{feature}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Who It's For Section - Card-based Layout
|
|
||||||
interface AudienceItem {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
icon: keyof typeof iconMap;
|
|
||||||
characteristics: string[];
|
|
||||||
highlight?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EnhancedAudienceProps {
|
|
||||||
title: string;
|
|
||||||
subtitle: string;
|
|
||||||
audiences: AudienceItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EnhancedAudience({ title, subtitle, audiences }: EnhancedAudienceProps) {
|
|
||||||
return (
|
|
||||||
<section className="py-16 lg:py-24 bg-muted/30">
|
|
||||||
<div className="container mx-auto px-4 lg:px-8">
|
|
||||||
{/* Header Section */}
|
|
||||||
<div className="text-center mb-16 lg:mb-20 max-w-4xl mx-auto">
|
|
||||||
<div className="text-base font-medium text-muted-foreground uppercase tracking-wider mb-4">
|
|
||||||
TARGET AUDIENCE
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 className="text-3xl lg:text-4xl xl:text-5xl font-bold text-foreground mb-6 leading-tight">
|
|
||||||
{title}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<p className="text-base lg:text-lg text-muted-foreground leading-relaxed max-w-2xl mx-auto">
|
|
||||||
{subtitle}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Card Grid Layout */}
|
|
||||||
<div className="grid lg:grid-cols-3 gap-6 lg:gap-8 max-w-6xl mx-auto">
|
|
||||||
{audiences.map((audience, index) => {
|
|
||||||
const Icon = iconMap[audience.icon];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
key={index}
|
|
||||||
className="group h-full border border-border hover:border-primary/30 hover:shadow-lg transition-all duration-300 bg-background"
|
|
||||||
>
|
|
||||||
<CardContent className="p-6 h-full flex flex-col">
|
|
||||||
{/* Icon */}
|
|
||||||
<div className="w-16 h-16 bg-primary/10 rounded-xl flex items-center justify-center mb-6 group-hover:bg-primary/15 transition-colors duration-300">
|
|
||||||
<Icon className="w-8 h-8 text-primary" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Title */}
|
|
||||||
<h3 className="text-xl lg:text-2xl font-bold text-foreground mb-4 leading-tight group-hover:text-primary transition-colors duration-300">
|
|
||||||
{audience.title}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
<p className="text-base text-muted-foreground leading-relaxed mb-6 flex-grow">
|
|
||||||
{audience.description}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Highlight */}
|
|
||||||
{audience.highlight && (
|
|
||||||
<div className="bg-secondary/10 rounded-lg p-4 mb-6">
|
|
||||||
<p className="text-base font-medium text-secondary-foreground">
|
|
||||||
{audience.highlight}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Characteristics */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
{audience.characteristics.map((characteristic, charIndex) => (
|
|
||||||
<div key={charIndex} className="flex items-start gap-2">
|
|
||||||
<CheckCircle className="w-4 h-4 text-success flex-shrink-0 mt-0.5" />
|
|
||||||
<span className="text-base text-muted-foreground leading-relaxed">
|
|
||||||
{characteristic}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expected Outcomes Section - Card-based Layout
|
|
||||||
interface OutcomeItem {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
icon: keyof typeof iconMap;
|
|
||||||
metrics: string;
|
|
||||||
percentage?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EnhancedOutcomesProps {
|
|
||||||
title: string;
|
|
||||||
subtitle: string;
|
|
||||||
outcomes: OutcomeItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EnhancedOutcomes({ title, subtitle, outcomes }: EnhancedOutcomesProps) {
|
|
||||||
return (
|
|
||||||
<section className="py-16 lg:py-24 bg-background">
|
|
||||||
<div className="container mx-auto px-4 lg:px-8">
|
|
||||||
{/* Header Section */}
|
|
||||||
<div className="text-center mb-16 lg:mb-20 max-w-4xl mx-auto">
|
|
||||||
<div className="text-base font-medium text-muted-foreground uppercase tracking-wider mb-4">
|
|
||||||
EXPECTED RESULTS
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 className="text-3xl lg:text-4xl xl:text-5xl font-bold text-foreground mb-6 leading-tight">
|
|
||||||
{title}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<p className="text-base lg:text-lg text-muted-foreground leading-relaxed max-w-2xl mx-auto">
|
|
||||||
{subtitle}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Card Grid Layout */}
|
|
||||||
<div className="grid lg:grid-cols-3 gap-6 lg:gap-8 max-w-6xl mx-auto">
|
|
||||||
{outcomes.map((outcome, index) => {
|
|
||||||
const Icon = iconMap[outcome.icon];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
key={index}
|
|
||||||
className="group h-full border border-border hover:border-primary/30 hover:shadow-lg transition-all duration-300"
|
|
||||||
>
|
|
||||||
<CardContent className="p-6 h-full flex flex-col">
|
|
||||||
{/* Header with Icon and Percentage */}
|
|
||||||
<div className="flex items-center justify-between mb-6">
|
|
||||||
<div className="w-16 h-16 bg-primary/10 rounded-xl flex items-center justify-center group-hover:bg-primary/15 transition-colors duration-300">
|
|
||||||
<Icon className="w-8 h-8 text-primary" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{outcome.percentage && (
|
|
||||||
<div className="text-right">
|
|
||||||
<div className="text-2xl lg:text-3xl font-bold text-success">
|
|
||||||
{outcome.percentage}%
|
|
||||||
</div>
|
|
||||||
<div className="text-base text-muted-foreground">Success Rate</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Title */}
|
|
||||||
<h3 className="text-xl lg:text-2xl font-bold text-foreground mb-4 leading-tight group-hover:text-primary transition-colors duration-300">
|
|
||||||
{outcome.title}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
<p className="text-base text-muted-foreground leading-relaxed mb-6 flex-grow">
|
|
||||||
{outcome.description}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Key Metric */}
|
|
||||||
<div className="border-t border-border/50 pt-4">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<TrendingUp className="w-4 h-4 text-success" />
|
|
||||||
<span className="text-base font-medium text-success">Key Metric</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-base font-semibold text-foreground">
|
|
||||||
{outcome.metrics}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Our Approach Section - Card-based Layout
|
|
||||||
interface ApproachStep {
|
|
||||||
step: string;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
details: string[];
|
|
||||||
icon: keyof typeof iconMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EnhancedApproachProps {
|
|
||||||
title: string;
|
|
||||||
subtitle: string;
|
|
||||||
methodologyNote: string;
|
|
||||||
methodologyIcon: keyof typeof iconMap;
|
|
||||||
steps: ApproachStep[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EnhancedApproach({ title, subtitle, methodologyNote, methodologyIcon, steps }: EnhancedApproachProps) {
|
|
||||||
return (
|
|
||||||
<section className="py-16 lg:py-24 bg-muted/30">
|
|
||||||
<div className="container mx-auto px-4 lg:px-8">
|
|
||||||
{/* Header Section */}
|
|
||||||
<div className="text-center mb-16 lg:mb-20 max-w-4xl mx-auto">
|
|
||||||
<div className="text-base font-medium text-muted-foreground uppercase tracking-wider mb-4">
|
|
||||||
METHODOLOGY
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 className="text-3xl lg:text-4xl xl:text-5xl font-bold text-foreground mb-6 leading-tight">
|
|
||||||
{title}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<p className="text-base lg:text-lg text-muted-foreground leading-relaxed max-w-2xl mx-auto mb-8">
|
|
||||||
{subtitle}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Methodology Note */}
|
|
||||||
<div className="inline-flex items-center gap-3 px-6 py-3 bg-primary/5 rounded-lg border border-primary/10">
|
|
||||||
<span className="text-base font-medium text-primary">{methodologyNote}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Card Grid Layout */}
|
|
||||||
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8 max-w-7xl mx-auto">
|
|
||||||
{steps.map((step, index) => {
|
|
||||||
const Icon = iconMap[step.icon];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
key={index}
|
|
||||||
className="group h-full border border-border hover:border-primary/30 hover:shadow-lg transition-all duration-300 bg-background"
|
|
||||||
>
|
|
||||||
<CardContent className="p-6 h-full flex flex-col">
|
|
||||||
{/* Icon */}
|
|
||||||
<div className="w-16 h-16 bg-primary/10 rounded-xl flex items-center justify-center mb-6 group-hover:bg-primary/15 transition-colors duration-300">
|
|
||||||
<Icon className="w-8 h-8 text-primary" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Title */}
|
|
||||||
<h3 className="text-xl lg:text-2xl font-bold text-foreground mb-4 leading-tight group-hover:text-primary transition-colors duration-300">
|
|
||||||
{step.title}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
<p className="text-base text-muted-foreground leading-relaxed mb-6 flex-grow">
|
|
||||||
{step.description}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Details List */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
{step.details.map((detail, detailIndex) => (
|
|
||||||
<div key={detailIndex} className="flex items-start gap-2">
|
|
||||||
<CheckCircle className="w-4 h-4 text-success flex-shrink-0 mt-0.5" />
|
|
||||||
<span className="text-base text-muted-foreground leading-relaxed">
|
|
||||||
{detail}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -45,14 +45,13 @@ const Button = React.forwardRef<
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
ref={ref}
|
|
||||||
data-slot="button"
|
data-slot="button"
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
Button.displayName = "Button";
|
Button.displayName = "Button";
|
||||||
|
|
||||||
export { Button, buttonVariants };
|
export { Button, buttonVariants };
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
|
|||||||
<div
|
<div
|
||||||
data-slot="card"
|
data-slot="card"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-lg border",
|
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ function Dialog({
|
|||||||
const DialogTrigger = React.forwardRef<
|
const DialogTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Trigger>,
|
React.ElementRef<typeof DialogPrimitive.Trigger>,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Trigger>
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Trigger>
|
||||||
>((props, ref) => (
|
>(({ ...props }, ref) => {
|
||||||
<DialogPrimitive.Trigger ref={ref} data-slot="dialog-trigger" {...props} />
|
return <DialogPrimitive.Trigger ref={ref} data-slot="dialog-trigger" {...props} />;
|
||||||
))
|
});
|
||||||
DialogTrigger.displayName = DialogPrimitive.Trigger.displayName
|
DialogTrigger.displayName = "DialogTrigger";
|
||||||
|
|
||||||
function DialogPortal({
|
function DialogPortal({
|
||||||
...props
|
...props
|
||||||
@@ -26,18 +26,17 @@ function DialogPortal({
|
|||||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DialogClose = React.forwardRef<
|
function DialogClose({
|
||||||
React.ElementRef<typeof DialogPrimitive.Close>,
|
...props
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Close>
|
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||||
>((props, ref) => (
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||||
<DialogPrimitive.Close ref={ref} data-slot="dialog-close" {...props} />
|
}
|
||||||
))
|
|
||||||
DialogClose.displayName = DialogPrimitive.Close.displayName
|
|
||||||
|
|
||||||
const DialogOverlay = React.forwardRef<
|
const DialogOverlay = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
ref={ref}
|
ref={ref}
|
||||||
data-slot="dialog-overlay"
|
data-slot="dialog-overlay"
|
||||||
@@ -47,13 +46,15 @@ const DialogOverlay = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
);
|
||||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
});
|
||||||
|
DialogOverlay.displayName = "DialogOverlay";
|
||||||
|
|
||||||
const DialogContent = React.forwardRef<
|
const DialogContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||||
>(({ className, children, ...props }, ref) => (
|
>(({ className, children, ...props }, ref) => {
|
||||||
|
return (
|
||||||
<DialogPortal data-slot="dialog-portal">
|
<DialogPortal data-slot="dialog-portal">
|
||||||
<DialogOverlay />
|
<DialogOverlay />
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
@@ -72,8 +73,9 @@ const DialogContent = React.forwardRef<
|
|||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
))
|
);
|
||||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
});
|
||||||
|
DialogContent.displayName = "DialogContent";
|
||||||
|
|
||||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
@@ -98,31 +100,31 @@ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const DialogTitle = React.forwardRef<
|
function DialogTitle({
|
||||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||||
|
return (
|
||||||
<DialogPrimitive.Title
|
<DialogPrimitive.Title
|
||||||
ref={ref}
|
|
||||||
data-slot="dialog-title"
|
data-slot="dialog-title"
|
||||||
className={cn("text-xl leading-tight font-semibold", className)}
|
className={cn("text-lg leading-none font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
);
|
||||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
}
|
||||||
|
|
||||||
const DialogDescription = React.forwardRef<
|
function DialogDescription({
|
||||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||||
|
return (
|
||||||
<DialogPrimitive.Description
|
<DialogPrimitive.Description
|
||||||
ref={ref}
|
|
||||||
data-slot="dialog-description"
|
data-slot="dialog-description"
|
||||||
className={cn("text-muted-foreground text-base", className)}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
);
|
||||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
|||||||
@@ -20,18 +20,16 @@ function DropdownMenuPortal({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const DropdownMenuTrigger = React.forwardRef<
|
function DropdownMenuTrigger({
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Trigger>,
|
...props
|
||||||
React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||||
>(({ ...props }, ref) => (
|
return (
|
||||||
<DropdownMenuPrimitive.Trigger
|
<DropdownMenuPrimitive.Trigger
|
||||||
ref={ref}
|
|
||||||
data-slot="dropdown-menu-trigger"
|
data-slot="dropdown-menu-trigger"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
);
|
||||||
|
}
|
||||||
DropdownMenuTrigger.displayName = "DropdownMenuTrigger";
|
|
||||||
|
|
||||||
function DropdownMenuContent({
|
function DropdownMenuContent({
|
||||||
className,
|
className,
|
||||||
@@ -61,15 +59,17 @@ function DropdownMenuGroup({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const DropdownMenuItem = React.forwardRef<
|
function DropdownMenuItem({
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
className,
|
||||||
React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
inset,
|
||||||
|
variant = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||||
inset?: boolean;
|
inset?: boolean;
|
||||||
variant?: "default" | "destructive";
|
variant?: "default" | "destructive";
|
||||||
}
|
}) {
|
||||||
>(({ className, inset, variant = "default", ...props }, ref) => (
|
return (
|
||||||
<DropdownMenuPrimitive.Item
|
<DropdownMenuPrimitive.Item
|
||||||
ref={ref}
|
|
||||||
data-slot="dropdown-menu-item"
|
data-slot="dropdown-menu-item"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
@@ -79,9 +79,8 @@ const DropdownMenuItem = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
);
|
||||||
|
}
|
||||||
DropdownMenuItem.displayName = "DropdownMenuItem";
|
|
||||||
|
|
||||||
function DropdownMenuCheckboxItem({
|
function DropdownMenuCheckboxItem({
|
||||||
className,
|
className,
|
||||||
|
|||||||
@@ -6,38 +6,46 @@ import { XIcon } from "lucide-react@0.487.0";
|
|||||||
|
|
||||||
import { cn } from "./utils";
|
import { cn } from "./utils";
|
||||||
|
|
||||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
const Sheet = React.forwardRef<
|
||||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
|
React.ElementRef<typeof SheetPrimitive.Root>,
|
||||||
}
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Root>
|
||||||
|
>(({ ...props }, ref) => {
|
||||||
|
return <SheetPrimitive.Root data-slot="sheet" ref={ref} {...props} />;
|
||||||
|
});
|
||||||
|
Sheet.displayName = "Sheet";
|
||||||
|
|
||||||
const SheetTrigger = React.forwardRef<
|
const SheetTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof SheetPrimitive.Trigger>,
|
React.ElementRef<typeof SheetPrimitive.Trigger>,
|
||||||
React.ComponentProps<typeof SheetPrimitive.Trigger>
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Trigger>
|
||||||
>(({ ...props }, ref) => (
|
>(({ ...props }, ref) => {
|
||||||
<SheetPrimitive.Trigger ref={ref} data-slot="sheet-trigger" {...props} />
|
return <SheetPrimitive.Trigger data-slot="sheet-trigger" ref={ref} {...props} />;
|
||||||
));
|
});
|
||||||
|
|
||||||
SheetTrigger.displayName = "SheetTrigger";
|
SheetTrigger.displayName = "SheetTrigger";
|
||||||
|
|
||||||
function SheetClose({
|
const SheetClose = React.forwardRef<
|
||||||
...props
|
React.ElementRef<typeof SheetPrimitive.Close>,
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Close>
|
||||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
|
>(({ ...props }, ref) => {
|
||||||
}
|
return <SheetPrimitive.Close data-slot="sheet-close" ref={ref} {...props} />;
|
||||||
|
});
|
||||||
|
SheetClose.displayName = "SheetClose";
|
||||||
|
|
||||||
function SheetPortal({
|
const SheetPortal = React.forwardRef<
|
||||||
...props
|
React.ElementRef<typeof SheetPrimitive.Portal>,
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Portal>
|
||||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
|
>(({ ...props }, ref) => {
|
||||||
}
|
return <SheetPrimitive.Portal data-slot="sheet-portal" ref={ref} {...props} />;
|
||||||
|
});
|
||||||
|
SheetPortal.displayName = "SheetPortal";
|
||||||
|
|
||||||
function SheetOverlay({
|
const SheetOverlay = React.forwardRef<
|
||||||
className,
|
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||||
...props
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
>(({ className, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<SheetPrimitive.Overlay
|
<SheetPrimitive.Overlay
|
||||||
data-slot="sheet-overlay"
|
data-slot="sheet-overlay"
|
||||||
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
className,
|
className,
|
||||||
@@ -45,21 +53,21 @@ function SheetOverlay({
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
SheetOverlay.displayName = "SheetOverlay";
|
||||||
|
|
||||||
function SheetContent({
|
const SheetContent = React.forwardRef<
|
||||||
className,
|
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||||
children,
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content> & {
|
||||||
side = "right",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
|
||||||
side?: "top" | "right" | "bottom" | "left";
|
side?: "top" | "right" | "bottom" | "left";
|
||||||
}) {
|
}
|
||||||
|
>(({ className, children, side = "right", ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<SheetPortal>
|
<SheetPortal>
|
||||||
<SheetOverlay />
|
<SheetOverlay />
|
||||||
<SheetPrimitive.Content
|
<SheetPrimitive.Content
|
||||||
data-slot="sheet-content"
|
data-slot="sheet-content"
|
||||||
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||||
side === "right" &&
|
side === "right" &&
|
||||||
@@ -82,53 +90,68 @@ function SheetContent({
|
|||||||
</SheetPrimitive.Content>
|
</SheetPrimitive.Content>
|
||||||
</SheetPortal>
|
</SheetPortal>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
SheetContent.displayName = "SheetContent";
|
||||||
|
|
||||||
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
const SheetHeader = React.forwardRef<
|
||||||
|
React.ElementRef<"div">,
|
||||||
|
React.ComponentPropsWithoutRef<"div">
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="sheet-header"
|
data-slot="sheet-header"
|
||||||
|
ref={ref}
|
||||||
className={cn("flex flex-col gap-1.5 p-4", className)}
|
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
SheetHeader.displayName = "SheetHeader";
|
||||||
|
|
||||||
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
const SheetFooter = React.forwardRef<
|
||||||
|
React.ElementRef<"div">,
|
||||||
|
React.ComponentPropsWithoutRef<"div">
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="sheet-footer"
|
data-slot="sheet-footer"
|
||||||
|
ref={ref}
|
||||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
SheetFooter.displayName = "SheetFooter";
|
||||||
|
|
||||||
function SheetTitle({
|
const SheetTitle = React.forwardRef<
|
||||||
className,
|
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||||
...props
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
>(({ className, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<SheetPrimitive.Title
|
<SheetPrimitive.Title
|
||||||
data-slot="sheet-title"
|
data-slot="sheet-title"
|
||||||
|
ref={ref}
|
||||||
className={cn("text-foreground font-semibold", className)}
|
className={cn("text-foreground font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
SheetTitle.displayName = "SheetTitle";
|
||||||
|
|
||||||
function SheetDescription({
|
const SheetDescription = React.forwardRef<
|
||||||
className,
|
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||||
...props
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
>(({ className, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<SheetPrimitive.Description
|
<SheetPrimitive.Description
|
||||||
data-slot="sheet-description"
|
data-slot="sheet-description"
|
||||||
|
ref={ref}
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
SheetDescription.displayName = "SheetDescription";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Sheet,
|
Sheet,
|
||||||
|
|||||||
29
src/global.d.ts
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
declare module '*.webp' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.png' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.jpg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.jpeg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.svg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.gif' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
61
src/guidelines/Guidelines.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
**Add your own guidelines here**
|
||||||
|
<!--
|
||||||
|
|
||||||
|
System Guidelines
|
||||||
|
|
||||||
|
Use this file to provide the AI with rules and guidelines you want it to follow.
|
||||||
|
This template outlines a few examples of things you can add. You can add your own sections and format it to suit your needs
|
||||||
|
|
||||||
|
TIP: More context isn't always better. It can confuse the LLM. Try and add the most important rules you need
|
||||||
|
|
||||||
|
# General guidelines
|
||||||
|
|
||||||
|
Any general rules you want the AI to follow.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
* Only use absolute positioning when necessary. Opt for responsive and well structured layouts that use flexbox and grid by default
|
||||||
|
* Refactor code as you go to keep code clean
|
||||||
|
* Keep file sizes small and put helper functions and components in their own files.
|
||||||
|
|
||||||
|
--------------
|
||||||
|
|
||||||
|
# Design system guidelines
|
||||||
|
Rules for how the AI should make generations look like your company's design system
|
||||||
|
|
||||||
|
Additionally, if you select a design system to use in the prompt box, you can reference
|
||||||
|
your design system's components, tokens, variables and components.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
* Use a base font-size of 14px
|
||||||
|
* Date formats should always be in the format “Jun 10”
|
||||||
|
* The bottom toolbar should only ever have a maximum of 4 items
|
||||||
|
* Never use the floating action button with the bottom toolbar
|
||||||
|
* Chips should always come in sets of 3 or more
|
||||||
|
* Don't use a dropdown if there are 2 or fewer options
|
||||||
|
|
||||||
|
You can also create sub sections and add more specific details
|
||||||
|
For example:
|
||||||
|
|
||||||
|
|
||||||
|
## Button
|
||||||
|
The Button component is a fundamental interactive element in our design system, designed to trigger actions or navigate
|
||||||
|
users through the application. It provides visual feedback and clear affordances to enhance user experience.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
Buttons should be used for important actions that users need to take, such as form submissions, confirming choices,
|
||||||
|
or initiating processes. They communicate interactivity and should have clear, action-oriented labels.
|
||||||
|
|
||||||
|
### Variants
|
||||||
|
* Primary Button
|
||||||
|
* Purpose : Used for the main action in a section or page
|
||||||
|
* Visual Style : Bold, filled with the primary brand color
|
||||||
|
* Usage : One primary button per section to guide users toward the most important action
|
||||||
|
* Secondary Button
|
||||||
|
* Purpose : Used for alternative or supporting actions
|
||||||
|
* Visual Style : Outlined with the primary color, transparent background
|
||||||
|
* Usage : Can appear alongside a primary button for less important actions
|
||||||
|
* Tertiary Button
|
||||||
|
* Purpose : Used for the least important actions
|
||||||
|
* Visual Style : Text-only with no border, using primary color
|
||||||
|
* Usage : For actions that should be available but not emphasized
|
||||||
|
-->
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import svgPaths from "./svg-l99du6w2o3";
|
|
||||||
|
|
||||||
export default function CurrentLearningHeaderIcon() {
|
|
||||||
return (
|
|
||||||
<div className="relative size-full" data-name="current learning header icon">
|
|
||||||
<svg className="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 24 24">
|
|
||||||
<g id="current learning header icon">
|
|
||||||
<path
|
|
||||||
d={svgPaths.p3fb52080}
|
|
||||||
fill="white"
|
|
||||||
strokeWidth="2.5"
|
|
||||||
id="Vector"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import imgImage40 from "figma:asset/2824e18f6ed39b8e82cf8a9fc215648cde48d2f4.png";
|
|
||||||
import imgImage41 from "figma:asset/f7fa2dab6765df7645e62459459afe9a6ff4959b.png";
|
|
||||||
import imgImage42 from "figma:asset/50c9ddeeb90128ebffbfbfe2dea36d09c03b5335.png";
|
|
||||||
import imgImage44 from "figma:asset/fc9194a6dac9bc6614c0646ed0b66177408ca5e6.png";
|
|
||||||
import imgImage46 from "figma:asset/ae07aac2d7927002260d7261da0eee0c09a8352f.png";
|
|
||||||
import imgImage37 from "figma:asset/624ce058c9c961b32643853cf5c692afe9d3ed60.png";
|
|
||||||
import imgImage7 from "figma:asset/468d85c60825612022ad15f5afa770440bd885e1.png";
|
|
||||||
|
|
||||||
function Frame4() {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="absolute box-border content-stretch flex flex-row gap-[42px] items-center justify-start p-0 top-[552px] translate-x-[-50%]"
|
|
||||||
style={{ left: "calc(50% - 1.242px)" }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="bg-center bg-cover bg-no-repeat h-[38.935px] shrink-0 w-[158.733px]"
|
|
||||||
data-name="image 40"
|
|
||||||
style={{ backgroundImage: `url('${imgImage40}')` }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="bg-center bg-cover bg-no-repeat h-[38.935px] shrink-0 w-[88.84px]"
|
|
||||||
data-name="image 41"
|
|
||||||
style={{ backgroundImage: `url('${imgImage41}')` }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="bg-center bg-cover bg-no-repeat h-[38.935px] shrink-0 w-[112.561px]"
|
|
||||||
data-name="image 42"
|
|
||||||
style={{ backgroundImage: `url('${imgImage42}')` }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="bg-center bg-cover bg-no-repeat h-[38.935px] shrink-0 w-[156.737px]"
|
|
||||||
data-name="image 44"
|
|
||||||
style={{ backgroundImage: `url('${imgImage44}')` }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="bg-center bg-cover bg-no-repeat h-[38.935px] shrink-0 w-[114.572px]"
|
|
||||||
data-name="image 46"
|
|
||||||
style={{ backgroundImage: `url('${imgImage46}')` }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="bg-center bg-cover bg-no-repeat h-[38.935px] shrink-0 w-[121.325px]"
|
|
||||||
data-name="image 37"
|
|
||||||
style={{ backgroundImage: `url('${imgImage37}')` }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="bg-center bg-cover bg-no-repeat h-[38.935px] shrink-0 w-[114.572px]"
|
|
||||||
data-name="image 47"
|
|
||||||
style={{ backgroundImage: `url('${imgImage46}')` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Group39900() {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="absolute contents top-[552px] translate-x-[-50%]"
|
|
||||||
style={{ left: "calc(50% - 1.242px)" }}
|
|
||||||
>
|
|
||||||
<Frame4 />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Frame5() {
|
|
||||||
return (
|
|
||||||
<div className="absolute bg-[#f3c200] box-border content-stretch flex flex-row gap-2 h-12 items-center justify-center left-1/2 px-12 py-[13px] rounded-[5px] top-[644px] translate-x-[-50%] w-[322px]">
|
|
||||||
<div className="font-['Source_Sans_Pro:Regular',_sans-serif] leading-[0] not-italic relative shrink-0 text-[#000000] text-[20px] text-left text-nowrap">
|
|
||||||
<p className="block leading-[normal] whitespace-pre">
|
|
||||||
Explore Programs
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Desktop4() {
|
|
||||||
return (
|
|
||||||
<div className="bg-[#f9f9f9] relative size-full" data-name="Desktop - 4">
|
|
||||||
<div
|
|
||||||
className="absolute bg-[#02004094] bg-size-[auto,100%_121.93%] bg-top-left h-[791px] left-1/2 top-0 translate-x-[-50%] w-[1446.11px]"
|
|
||||||
data-name="image 7"
|
|
||||||
style={{ backgroundImage: `url('${imgImage7}')` }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="absolute font-['Source_Sans_Pro:Bold',_sans-serif] leading-[0] not-italic text-[#ffffff] text-[70px] text-left text-nowrap top-[218.32px]"
|
|
||||||
style={{ left: "calc(50% - 310px)" }}
|
|
||||||
>
|
|
||||||
<p className="block leading-[normal] whitespace-pre">
|
|
||||||
Discover Leadership
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="absolute font-['Source_Sans_Pro:Regular',_sans-serif] leading-[0] not-italic text-[#ffffff] text-[22px] text-left text-nowrap top-[329.72px]"
|
|
||||||
style={{ left: "calc(50% - 582.5px)" }}
|
|
||||||
>
|
|
||||||
<p className="block leading-[normal] whitespace-pre">{`Leadership Development | Management Development | Culture Competence | Consulting | Executive Coaching | Learning Facility `}</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="absolute font-['Source_Sans_Pro:Regular',_sans-serif] leading-[0] not-italic text-[#ffffff] text-[18px] text-left text-nowrap top-[502.5px]"
|
|
||||||
style={{ left: "calc(50% - 40px)" }}
|
|
||||||
>
|
|
||||||
<p className="block leading-[normal] whitespace-pre">Trusted By</p>
|
|
||||||
</div>
|
|
||||||
<Group39900 />
|
|
||||||
<Frame5 />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import imgRectangle5 from "figma:asset/468d85c60825612022ad15f5afa770440bd885e1.png";
|
|
||||||
|
|
||||||
export default function Desktop6() {
|
|
||||||
return (
|
|
||||||
<div className="bg-[#f9f9f9] relative size-full" data-name="Desktop - 6">
|
|
||||||
<div className="absolute font-['Source_Sans_Pro:SemiBold',_sans-serif] leading-[0] left-[50.18px] not-italic text-[#000000] text-[35px] text-left top-[63.45px] w-[481.603px]">
|
|
||||||
<p className="block leading-[normal]">
|
|
||||||
We Provide Superior Leadership Services
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="absolute font-['Source_Sans_Pro:Regular',_sans-serif] leading-[0] left-[52.73px] not-italic text-[#707070] text-[18px] text-left top-[163.21px] w-[494.016px]">
|
|
||||||
<p className="block leading-[normal]">
|
|
||||||
Our comprehensive approach combines traditional leadership development
|
|
||||||
with innovative methodologies, supported by world-class facilities and
|
|
||||||
cutting-edge technology to deliver exceptional results.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="absolute bg-no-repeat bg-size-[117.37%_114.37%] bg-top h-[227.197px] left-[654.23px] rounded-[20px] top-[63.45px] w-[331.952px]"
|
|
||||||
style={{ backgroundImage: `url('${imgRectangle5}')` }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="absolute bg-center bg-cover bg-no-repeat h-[265.29px] left-[50.18px] rounded-[20px] top-[383.16px] w-[405.625px]"
|
|
||||||
style={{ backgroundImage: `url('${imgRectangle5}')` }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="absolute bg-center bg-cover bg-no-repeat h-[259.848px] left-[1021.55px] rounded-[20px] top-[63.45px] w-[331.952px]"
|
|
||||||
style={{ backgroundImage: `url('${imgRectangle5}')` }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="absolute bg-center bg-cover bg-no-repeat h-[265.29px] left-[499.03px] rounded-[20px] top-[383.16px] w-[405.625px]"
|
|
||||||
style={{ backgroundImage: `url('${imgRectangle5}')` }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="absolute bg-center bg-cover bg-no-repeat h-[265.29px] left-[947.88px] rounded-[20px] top-[383.16px] w-[405.625px]"
|
|
||||||
style={{ backgroundImage: `url('${imgRectangle5}')` }}
|
|
||||||
/>
|
|
||||||
<div className="absolute bg-[#ffffff] h-[149.651px] left-[682.8px] rounded-bl-[5px] rounded-br-[5px] shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] top-[192.69px] w-[291.139px]" />
|
|
||||||
<div className="absolute bg-[#ffffff] h-[149.651px] left-[70.59px] rounded-bl-[5px] rounded-br-[5px] shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] top-[545.05px] w-[357.801px]" />
|
|
||||||
<div className="absolute bg-[#ffffff] h-[149.651px] left-[522.78px] rounded-bl-[5px] rounded-br-[5px] shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] top-[545.05px] w-[357.801px]" />
|
|
||||||
<div className="absolute bg-[#ffffff] h-[149.651px] left-[973.93px] rounded-bl-[5px] rounded-br-[5px] shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] top-[545.05px] w-[357.801px]" />
|
|
||||||
<div className="absolute bg-[#ffffff] h-[149.651px] left-[1041.96px] rounded-bl-[5px] rounded-br-[5px] shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] top-[192.69px] w-[291.139px]" />
|
|
||||||
<div className="absolute font-['Source_Sans_Pro:SemiBold',_sans-serif] leading-[0] left-[705.92px] not-italic text-[#000000] text-[20px] text-left top-[207.21px] w-[481.603px]">
|
|
||||||
<p className="block leading-[normal]">Heading</p>
|
|
||||||
</div>
|
|
||||||
<div className="absolute font-['Source_Sans_Pro:Regular',_sans-serif] leading-[0] left-[705.92px] not-italic text-[#707070] text-[14px] text-left top-[237.21px] w-[494.016px]">
|
|
||||||
<p className="block leading-[normal]">subheading</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import imgImage36 from "figma:asset/6bdf8056f51bbdc6dd9dab9044a6579a254bd02c.png";
|
|
||||||
import imgImage39 from "figma:asset/037c4659b7b0bf15b1dfdcd4868cb42e8257e838.png";
|
|
||||||
import imgImage43 from "figma:asset/c57ec1f4466f68e607139a3cd6d52f7e2f372408.png";
|
|
||||||
import imgImage45 from "figma:asset/4833274f0a593cd31fdefe553b70bb016de281af.png";
|
|
||||||
import imgImage38 from "figma:asset/d5bab6ea4f3d8cef3b0425c45cfee7faea19fdbc.png";
|
|
||||||
import imgImage47 from "figma:asset/e8fad960112d5eba554c3969d08891ebe4d4b9c7.png";
|
|
||||||
|
|
||||||
function Group39901() {
|
|
||||||
return (
|
|
||||||
<div className="absolute contents left-0 top-0">
|
|
||||||
<div
|
|
||||||
className="absolute bg-center bg-cover bg-no-repeat h-[54.621px] left-0 top-0 w-[302px]"
|
|
||||||
data-name="image 36"
|
|
||||||
style={{ backgroundImage: `url('${imgImage36}')` }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="absolute bg-center bg-cover bg-no-repeat h-[54px] left-[837.887px] top-0 w-[172.038px]"
|
|
||||||
data-name="image 39"
|
|
||||||
style={{ backgroundImage: `url('${imgImage39}')` }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="absolute bg-center bg-cover bg-no-repeat h-[54px] left-[1292.92px] top-0 w-[206.942px]"
|
|
||||||
data-name="image 43"
|
|
||||||
style={{ backgroundImage: `url('${imgImage43}')` }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="absolute bg-center bg-cover bg-no-repeat h-[54px] left-[362px] top-0 w-[210.74px]"
|
|
||||||
data-name="image 45"
|
|
||||||
style={{ backgroundImage: `url('${imgImage45}')` }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="absolute bg-center bg-cover bg-no-repeat h-[54px] left-[632.74px] top-0 w-[145.146px]"
|
|
||||||
data-name="image 38"
|
|
||||||
style={{ backgroundImage: `url('${imgImage38}')` }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="absolute bg-center bg-cover bg-no-repeat h-[54px] left-[1069.92px] top-0 w-[163.293px]"
|
|
||||||
data-name="image 47"
|
|
||||||
style={{ backgroundImage: `url('${imgImage47}')` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Frame1597884933() {
|
|
||||||
return (
|
|
||||||
<div className="relative size-full">
|
|
||||||
<Group39901 />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import imgBrandLogoWhiteBackground1 from "figma:asset/3a97bc3c43824d72250953bd1d41ece20112a45a.png";
|
|
||||||
|
|
||||||
export default function Logo() {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="box-border content-stretch flex flex-row gap-2 items-center justify-start p-0 relative size-full"
|
|
||||||
data-name="logo"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="basis-0 bg-center bg-contain bg-no-repeat grow h-full min-h-px min-w-px shrink-0"
|
|
||||||
data-name="brand-logo-white background 1"
|
|
||||||
style={{ backgroundImage: `url('${imgBrandLogoWhiteBackground1}')` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export default {
|
|
||||||
p3fb52080: "M12.4789 3.26119C12.1804 3.09839 11.8196 3.09839 11.5211 3.26119L2.61015 8.12174C1.91514 8.50083 1.91532 9.49885 2.61045 9.8777L4.47854 10.8958C4.79998 11.071 5 11.4078 5 11.7739V16.5865C5 16.9524 5.19981 17.289 5.52097 17.4643L11.521 20.7386C11.8195 20.9015 12.1805 20.9015 12.479 20.7386L18.479 17.4643C18.8002 17.289 19 16.9524 19 16.5865V11.7739C19 11.4078 19.2 11.071 19.5215 10.8958V10.8958C20.1878 10.5326 21 11.015 21 11.7739V16C21 16.5523 21.4477 17 22 17V17C22.5523 17 23 16.5523 23 16V9.59363C23 9.22769 22.8001 8.89097 22.4789 8.71574L12.4789 3.26119ZM17.2105 8.1221C17.9054 8.50112 17.9054 9.49888 17.2105 9.8779L12.4789 12.4588C12.1804 12.6216 11.8196 12.6216 11.5211 12.4588L6.78947 9.87789C6.09461 9.49888 6.09461 8.50112 6.78948 8.1221L11.5211 5.54119C11.8196 5.37839 12.1804 5.37839 12.4789 5.54119L17.2105 8.1221ZM17 15.4056C17 15.772 16.7997 16.109 16.4779 16.284L12.4779 18.46C12.1799 18.6221 11.8201 18.6221 11.5221 18.46L7.52213 16.284C7.20032 16.109 7 15.772 7 15.4056V13.9554C7 13.1961 7.81284 12.7138 8.47922 13.0777L11.5208 14.7383C11.8195 14.9014 12.1806 14.9014 12.4792 14.7383L15.5208 13.0777C16.1872 12.7138 17 13.1961 17 13.9553V15.4056Z",
|
|
||||||
}
|
|
||||||
4196
src/index.css
Normal file
15
src/main.tsx
@@ -1,10 +1,7 @@
|
|||||||
import { createRoot } from "react-dom/client";
|
|
||||||
import { BrowserRouter } from "react-router-dom";
|
|
||||||
import App from "./App.tsx";
|
|
||||||
import "./styles/globals.css";
|
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
import { createRoot } from "react-dom/client";
|
||||||
<BrowserRouter>
|
import App from "./App.tsx";
|
||||||
<App />
|
import "./index.css";
|
||||||
</BrowserRouter>
|
|
||||||
);
|
createRoot(document.getElementById("root")!).render(<App />);
|
||||||
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import React, { useEffect } from 'react';
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
export function AboutKLC() {
|
|
||||||
useEffect(() => {
|
|
||||||
// This is a deprecated page, redirect to home
|
|
||||||
navigate('/');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import React, { useEffect } from 'react';
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
export function Articles() {
|
|
||||||
useEffect(() => {
|
|
||||||
// This is a deprecated page, redirect to home
|
|
||||||
navigate('/');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import React, { useEffect } from 'react';
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
export function BlogDetail() {
|
|
||||||
useEffect(() => {
|
|
||||||
// This is a deprecated page, redirect to home
|
|
||||||
navigate('/');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import React, { useEffect } from 'react';
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
export function BlogListing() {
|
|
||||||
useEffect(() => {
|
|
||||||
// This is a deprecated page, redirect to home
|
|
||||||
navigate('/');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import React, { useEffect } from 'react';
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
export function Cart() {
|
|
||||||
useEffect(() => {
|
|
||||||
// This is a deprecated page, redirect to home
|
|
||||||
navigate('/');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import React, { useEffect } from 'react';
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
export function Checkout() {
|
|
||||||
useEffect(() => {
|
|
||||||
// This is a deprecated page, redirect to home
|
|
||||||
navigate('/');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { Button } from '../components/ui/button';
|
|
||||||
import { Input } from '../components/ui/input';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card';
|
|
||||||
import { ArrowLeft, Mail, Phone, MapPin, Clock, Send } from 'lucide-react';
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
export function Contact() {
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
name: '',
|
|
||||||
email: '',
|
|
||||||
subject: '',
|
|
||||||
message: ''
|
|
||||||
});
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Simulate form submission
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
alert('Thank you for your message! We\'ll get back to you within 24 hours.');
|
|
||||||
setFormData({ name: '', email: '', subject: '', message: '' });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Form submission failed:', error);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBackNavigation = () => {
|
|
||||||
navigate('/');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-white">
|
|
||||||
{/* Header Section */}
|
|
||||||
<div className="bg-primary text-primary-foreground pt-24 pb-12">
|
|
||||||
<div className="container mx-auto px-4 lg:px-8">
|
|
||||||
<div className="max-w-4xl mx-auto">
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={handleBackNavigation}
|
|
||||||
className="text-primary-foreground hover:bg-primary-foreground/10 min-h-[44px] min-w-[44px] mt-1"
|
|
||||||
aria-label="Go back to home page"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="h-6 w-6" />
|
|
||||||
</Button>
|
|
||||||
<div className="flex-1">
|
|
||||||
<h1 className="text-[36px] mb-3 font-bold">Contact Us</h1>
|
|
||||||
<p className="text-[18px] text-primary-foreground/90 leading-relaxed">
|
|
||||||
Get in touch with our team for support, inquiries, or to learn more about our leadership programs.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Content */}
|
|
||||||
<div className="container mx-auto px-4 lg:px-8 -mt-8">
|
|
||||||
<div className="max-w-6xl mx-auto">
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
||||||
|
|
||||||
{/* Contact Form */}
|
|
||||||
<div className="lg:col-span-2">
|
|
||||||
<Card className="bg-white shadow-xl border-0">
|
|
||||||
<CardHeader className="pb-6">
|
|
||||||
<CardTitle className="text-[24px] font-bold text-gray-900">
|
|
||||||
Send us a Message
|
|
||||||
</CardTitle>
|
|
||||||
<p className="text-[16px] text-gray-600">
|
|
||||||
We'd love to hear from you. Fill out the form below and we'll get back to you as soon as possible.
|
|
||||||
</p>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<CardContent>
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div>
|
|
||||||
<label htmlFor="name" className="block text-[16px] font-medium text-gray-900 mb-2">
|
|
||||||
Full Name
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
type="text"
|
|
||||||
value={formData.name}
|
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
|
||||||
className="text-[16px] min-h-[44px]"
|
|
||||||
placeholder="Your full name"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="email" className="block text-[16px] font-medium text-gray-900 mb-2">
|
|
||||||
Email Address
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
value={formData.email}
|
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
|
|
||||||
className="text-[16px] min-h-[44px]"
|
|
||||||
placeholder="your@email.com"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="subject" className="block text-[16px] font-medium text-gray-900 mb-2">
|
|
||||||
Subject
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id="subject"
|
|
||||||
type="text"
|
|
||||||
value={formData.subject}
|
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, subject: e.target.value }))}
|
|
||||||
className="text-[16px] min-h-[44px]"
|
|
||||||
placeholder="What can we help you with?"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="message" className="block text-[16px] font-medium text-gray-900 mb-2">
|
|
||||||
Message
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
id="message"
|
|
||||||
value={formData.message}
|
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, message: e.target.value }))}
|
|
||||||
rows={6}
|
|
||||||
className="w-full px-4 py-3 text-[16px] border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent resize-none"
|
|
||||||
placeholder="Tell us more about your inquiry..."
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
disabled={isLoading}
|
|
||||||
className="w-full text-[16px] min-h-[48px] bg-primary hover:bg-primary/90"
|
|
||||||
>
|
|
||||||
<Send className="h-4 w-4 mr-2" />
|
|
||||||
{isLoading ? 'Sending Message...' : 'Send Message'}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Contact Information */}
|
|
||||||
<div className="space-y-6">
|
|
||||||
<Card className="bg-white shadow-xl border-0">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-[20px] font-bold text-gray-900">
|
|
||||||
Get in Touch
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-6">
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<div className="w-10 h-10 bg-primary/10 rounded-full flex items-center justify-center flex-shrink-0">
|
|
||||||
<Mail className="h-5 w-5 text-primary" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-[16px] font-semibold text-gray-900 mb-1">Email Us</h3>
|
|
||||||
<p className="text-[16px] text-gray-600">support@klc.edu</p>
|
|
||||||
<p className="text-[16px] text-gray-600">enterprise@klc.edu</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<div className="w-10 h-10 bg-primary/10 rounded-full flex items-center justify-center flex-shrink-0">
|
|
||||||
<Phone className="h-5 w-5 text-primary" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-[16px] font-semibold text-gray-900 mb-1">Call Us</h3>
|
|
||||||
<p className="text-[16px] text-gray-600">+1 (555) 123-4567</p>
|
|
||||||
<p className="text-[14px] text-gray-500">Mon-Fri, 9 AM - 6 PM EST</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<div className="w-10 h-10 bg-primary/10 rounded-full flex items-center justify-center flex-shrink-0">
|
|
||||||
<MapPin className="h-5 w-5 text-primary" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-[16px] font-semibold text-gray-900 mb-1">Visit Us</h3>
|
|
||||||
<p className="text-[16px] text-gray-600">
|
|
||||||
123 Leadership Way<br />
|
|
||||||
Suite 100<br />
|
|
||||||
Boston, MA 02101
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<div className="w-10 h-10 bg-primary/10 rounded-full flex items-center justify-center flex-shrink-0">
|
|
||||||
<Clock className="h-5 w-5 text-primary" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-[16px] font-semibold text-gray-900 mb-1">Business Hours</h3>
|
|
||||||
<p className="text-[16px] text-gray-600">
|
|
||||||
Monday - Friday: 9:00 AM - 6:00 PM<br />
|
|
||||||
Saturday: 10:00 AM - 4:00 PM<br />
|
|
||||||
Sunday: Closed
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="bg-gradient-to-br from-primary to-primary/90 text-primary-foreground shadow-xl border-0">
|
|
||||||
<CardContent className="p-6">
|
|
||||||
<h3 className="text-[18px] font-semibold mb-3">Need Immediate Help?</h3>
|
|
||||||
<p className="text-[16px] mb-4 opacity-90">
|
|
||||||
Our support team is standing by to assist you with any questions about our programs or platform.
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
className="w-full text-[16px] min-h-[44px] bg-white/10 hover:bg-white/20 text-white border-white/20"
|
|
||||||
onClick={() => window.open('mailto:support@klc.edu', '_blank')}
|
|
||||||
>
|
|
||||||
Email Support Now
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Button } from '../components/ui/button';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card';
|
|
||||||
import { ArrowLeft, Building2, Users, Shield, BarChart3 } from 'lucide-react';
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
export function CorporateAuth() {
|
|
||||||
const handleBackNavigation = () => {
|
|
||||||
navigate('/auth');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLogin = () => {
|
|
||||||
navigate('/corporate/login');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSignup = () => {
|
|
||||||
navigate('/corporate/signup');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-white">
|
|
||||||
{/* Header Section */}
|
|
||||||
<div className="bg-primary text-primary-foreground pt-24 pb-12">
|
|
||||||
<div className="container mx-auto px-4 lg:px-8">
|
|
||||||
<div className="max-w-4xl mx-auto">
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={handleBackNavigation}
|
|
||||||
className="text-primary-foreground hover:bg-primary-foreground/10 min-h-[44px] min-w-[44px] mt-1"
|
|
||||||
aria-label="Go back to account selection"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="h-6 w-6" />
|
|
||||||
</Button>
|
|
||||||
<div className="flex-1">
|
|
||||||
<h1 className="text-[36px] mb-3 font-bold">Corporate Learning Portal</h1>
|
|
||||||
<p className="text-[18px] text-primary-foreground/90 leading-relaxed">
|
|
||||||
Access your organization's leadership development programs and team management tools.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Content */}
|
|
||||||
<div className="container mx-auto px-4 lg:px-8 -mt-8">
|
|
||||||
<div className="max-w-2xl mx-auto">
|
|
||||||
<Card className="bg-white shadow-xl border-0">
|
|
||||||
<CardHeader className="pb-6 text-center">
|
|
||||||
<div className="w-16 h-16 bg-purple-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
||||||
<Building2 className="h-8 w-8 text-purple-700" />
|
|
||||||
</div>
|
|
||||||
<CardTitle className="text-[24px] font-bold text-gray-900">
|
|
||||||
Enterprise Learning Access
|
|
||||||
</CardTitle>
|
|
||||||
<p className="text-[16px] text-gray-600">
|
|
||||||
Sign in to your corporate learning account or request access from your administrator
|
|
||||||
</p>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<CardContent className="space-y-8">
|
|
||||||
{/* Features Overview */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<div className="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center flex-shrink-0">
|
|
||||||
<Users className="h-5 w-5 text-purple-700" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-[16px] font-semibold text-gray-900 mb-1">Team Collaboration</h3>
|
|
||||||
<p className="text-[14px] text-gray-600">Work together on learning objectives and track team progress</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<div className="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center flex-shrink-0">
|
|
||||||
<BarChart3 className="h-5 w-5 text-purple-700" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-[16px] font-semibold text-gray-900 mb-1">Advanced Analytics</h3>
|
|
||||||
<p className="text-[14px] text-gray-600">Comprehensive insights into organizational learning performance</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<div className="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center flex-shrink-0">
|
|
||||||
<Shield className="h-5 w-5 text-purple-700" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-[16px] font-semibold text-gray-900 mb-1">Enterprise Security</h3>
|
|
||||||
<p className="text-[14px] text-gray-600">Secure access controls and data protection for your organization</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<div className="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center flex-shrink-0">
|
|
||||||
<Building2 className="h-5 w-5 text-purple-700" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-[16px] font-semibold text-gray-900 mb-1">Custom Programs</h3>
|
|
||||||
<p className="text-[14px] text-gray-600">Tailored learning paths aligned with your business objectives</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Action Buttons */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<Button
|
|
||||||
onClick={handleLogin}
|
|
||||||
className="w-full text-[16px] min-h-[48px] bg-purple-600 hover:bg-purple-700 text-white"
|
|
||||||
>
|
|
||||||
Sign In to Corporate Account
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute inset-0 flex items-center">
|
|
||||||
<span className="w-full border-t border-gray-300" />
|
|
||||||
</div>
|
|
||||||
<div className="relative flex justify-center text-[14px]">
|
|
||||||
<span className="bg-white px-2 text-gray-500">New to corporate learning?</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={handleSignup}
|
|
||||||
variant="outline"
|
|
||||||
className="w-full text-[16px] min-h-[48px] border-purple-600 text-purple-600 hover:bg-purple-50"
|
|
||||||
>
|
|
||||||
Request Corporate Access
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Support Information */}
|
|
||||||
<div className="bg-purple-50 rounded-lg p-6">
|
|
||||||
<h3 className="text-[16px] font-semibold text-purple-900 mb-3">Need Help Getting Started?</h3>
|
|
||||||
<p className="text-[14px] text-purple-700 mb-4">
|
|
||||||
Contact your organization's learning administrator or reach out to our enterprise support team for assistance with account setup and access.
|
|
||||||
</p>
|
|
||||||
<div className="flex flex-col sm:flex-row gap-3">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => navigate('/contact')}
|
|
||||||
className="text-[14px] border-purple-600 text-purple-600 hover:bg-purple-100"
|
|
||||||
>
|
|
||||||
Contact Support
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => window.open('mailto:enterprise@klc.edu', '_blank')}
|
|
||||||
className="text-[14px] border-purple-600 text-purple-600 hover:bg-purple-100"
|
|
||||||
>
|
|
||||||
Email Enterprise Team
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,793 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { LearnerLayout } from '../components/learner/LearnerLayout';
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card';
|
|
||||||
import { Button } from '../components/ui/button';
|
|
||||||
import { Input } from '../components/ui/input';
|
|
||||||
import { Badge } from '../components/ui/badge';
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '../components/ui/avatar';
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/ui/tabs';
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../components/ui/select';
|
|
||||||
import { Progress } from '../components/ui/progress';
|
|
||||||
import { Separator } from '../components/ui/separator';
|
|
||||||
import { motion } from 'motion/react';
|
|
||||||
import {
|
|
||||||
ArrowLeft,
|
|
||||||
Trophy,
|
|
||||||
Medal,
|
|
||||||
Award,
|
|
||||||
Target,
|
|
||||||
TrendingUp,
|
|
||||||
Users,
|
|
||||||
Download,
|
|
||||||
Filter,
|
|
||||||
Search,
|
|
||||||
Crown,
|
|
||||||
Star,
|
|
||||||
Gift,
|
|
||||||
Zap,
|
|
||||||
ChevronRight,
|
|
||||||
Calendar,
|
|
||||||
Clock,
|
|
||||||
BookOpen,
|
|
||||||
CheckCircle,
|
|
||||||
BarChart3,
|
|
||||||
PieChart,
|
|
||||||
ArrowUp,
|
|
||||||
ArrowDown,
|
|
||||||
Minus,
|
|
||||||
Settings,
|
|
||||||
Flag,
|
|
||||||
Sparkles
|
|
||||||
} from 'lucide-react';
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
// Mock data for leaderboard
|
|
||||||
const mockLeaderboard = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
rank: 1,
|
|
||||||
name: "Alexandra Chen",
|
|
||||||
title: "Senior Director",
|
|
||||||
department: "Operations",
|
|
||||||
avatar: "https://images.unsplash.com/photo-1494790108755-2616b612b786?w=150&h=150&fit=crop&crop=face",
|
|
||||||
totalPoints: 2847,
|
|
||||||
monthlyPoints: 485,
|
|
||||||
coursesCompleted: 12,
|
|
||||||
streakDays: 28,
|
|
||||||
level: "Expert",
|
|
||||||
progress: 85,
|
|
||||||
trend: "up",
|
|
||||||
badges: ["Leadership Excellence", "Team Builder", "Innovation Champion"],
|
|
||||||
lastActive: "2 hours ago",
|
|
||||||
completionRate: 94
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
rank: 2,
|
|
||||||
name: "Michael Rodriguez",
|
|
||||||
title: "VP Engineering",
|
|
||||||
department: "Technology",
|
|
||||||
avatar: "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face",
|
|
||||||
totalPoints: 2756,
|
|
||||||
monthlyPoints: 423,
|
|
||||||
coursesCompleted: 11,
|
|
||||||
streakDays: 22,
|
|
||||||
level: "Expert",
|
|
||||||
progress: 76,
|
|
||||||
trend: "up",
|
|
||||||
badges: ["Tech Leader", "Mentor", "Strategic Thinker"],
|
|
||||||
lastActive: "4 hours ago",
|
|
||||||
completionRate: 89
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
rank: 3,
|
|
||||||
name: "Sarah Johnson",
|
|
||||||
title: "Director of Sales",
|
|
||||||
department: "Sales",
|
|
||||||
avatar: "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&crop=face",
|
|
||||||
totalPoints: 2634,
|
|
||||||
monthlyPoints: 398,
|
|
||||||
coursesCompleted: 10,
|
|
||||||
streakDays: 19,
|
|
||||||
level: "Advanced",
|
|
||||||
progress: 68,
|
|
||||||
trend: "up",
|
|
||||||
badges: ["Sales Champion", "Customer Focus", "Results Driver"],
|
|
||||||
lastActive: "1 hour ago",
|
|
||||||
completionRate: 91
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
rank: 4,
|
|
||||||
name: "David Kim",
|
|
||||||
title: "Marketing Manager",
|
|
||||||
department: "Marketing",
|
|
||||||
avatar: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face",
|
|
||||||
totalPoints: 2521,
|
|
||||||
monthlyPoints: 367,
|
|
||||||
coursesCompleted: 9,
|
|
||||||
streakDays: 15,
|
|
||||||
level: "Advanced",
|
|
||||||
progress: 52,
|
|
||||||
trend: "stable",
|
|
||||||
badges: ["Creative Leader", "Brand Builder", "Analytics Pro"],
|
|
||||||
lastActive: "6 hours ago",
|
|
||||||
completionRate: 87
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
rank: 5,
|
|
||||||
name: "Emily Watson",
|
|
||||||
title: "HR Director",
|
|
||||||
department: "Human Resources",
|
|
||||||
avatar: "https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=150&h=150&fit=crop&crop=face",
|
|
||||||
totalPoints: 2398,
|
|
||||||
monthlyPoints: 334,
|
|
||||||
coursesCompleted: 8,
|
|
||||||
streakDays: 12,
|
|
||||||
level: "Advanced",
|
|
||||||
progress: 45,
|
|
||||||
trend: "down",
|
|
||||||
badges: ["People Leader", "Culture Champion", "Diversity Advocate"],
|
|
||||||
lastActive: "3 hours ago",
|
|
||||||
completionRate: 92
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
rank: 6,
|
|
||||||
name: "James Thompson",
|
|
||||||
title: "Finance Manager",
|
|
||||||
department: "Finance",
|
|
||||||
avatar: "https://images.unsplash.com/photo-1556157382-97eda2d62296?w=150&h=150&fit=crop&crop=face",
|
|
||||||
totalPoints: 2267,
|
|
||||||
monthlyPoints: 298,
|
|
||||||
coursesCompleted: 7,
|
|
||||||
streakDays: 8,
|
|
||||||
level: "Intermediate",
|
|
||||||
progress: 34,
|
|
||||||
trend: "up",
|
|
||||||
badges: ["Financial Acumen", "Risk Manager", "Process Optimizer"],
|
|
||||||
lastActive: "5 hours ago",
|
|
||||||
completionRate: 85
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// Animated number counter component
|
|
||||||
const AnimatedNumber = ({ value, duration = 1000 }: { value: number; duration?: number }) => {
|
|
||||||
const [displayValue, setDisplayValue] = useState(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let startTime: number;
|
|
||||||
let animationFrame: number;
|
|
||||||
|
|
||||||
const animate = (timestamp: number) => {
|
|
||||||
if (!startTime) startTime = timestamp;
|
|
||||||
const progress = Math.min((timestamp - startTime) / duration, 1);
|
|
||||||
|
|
||||||
// Easing function for smooth animation
|
|
||||||
const easeOut = 1 - Math.pow(1 - progress, 3);
|
|
||||||
setDisplayValue(Math.floor(value * easeOut));
|
|
||||||
|
|
||||||
if (progress < 1) {
|
|
||||||
animationFrame = requestAnimationFrame(animate);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
animationFrame = requestAnimationFrame(animate);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (animationFrame) {
|
|
||||||
cancelAnimationFrame(animationFrame);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [value, duration]);
|
|
||||||
|
|
||||||
return <span>{displayValue.toLocaleString()}</span>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function CorporateLeaderboard() {
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
|
||||||
const [selectedDepartment, setSelectedDepartment] = useState('all');
|
|
||||||
const [selectedLevel, setSelectedLevel] = useState('all');
|
|
||||||
const [sortBy, setSortBy] = useState('rank');
|
|
||||||
const [timeFilter, setTimeFilter] = useState('month');
|
|
||||||
|
|
||||||
// Mock corporate user data matching dashboard pattern
|
|
||||||
const user = {
|
|
||||||
name: "Sarah Johnson",
|
|
||||||
email: "sarah.johnson@company.com",
|
|
||||||
avatar: "https://images.unsplash.com/photo-1494790108755-2616b612b786?w=150&h=150&fit=crop&crop=face",
|
|
||||||
organization: "TCS",
|
|
||||||
orgLogo: "https://images.unsplash.com/photo-1560179707-f14e90ef3623?w=32&h=32&fit=crop",
|
|
||||||
role: "Senior Manager",
|
|
||||||
cohort: "Leadership 2024"
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRankIcon = (rank: number) => {
|
|
||||||
switch (rank) {
|
|
||||||
case 1:
|
|
||||||
return <Trophy className="h-8 w-8 text-[#F8C301]" />;
|
|
||||||
case 2:
|
|
||||||
return <Medal className="h-8 w-8 text-primary" />;
|
|
||||||
case 3:
|
|
||||||
return <Award className="h-8 w-8 text-[#26231A]" />;
|
|
||||||
default:
|
|
||||||
return <Target className="h-8 w-8 text-muted-foreground" />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTrendIcon = (trend: string) => {
|
|
||||||
switch (trend) {
|
|
||||||
case 'up':
|
|
||||||
return <ArrowUp className="h-4 w-4 text-success" />;
|
|
||||||
case 'down':
|
|
||||||
return <ArrowDown className="h-4 w-4 text-destructive" />;
|
|
||||||
default:
|
|
||||||
return <Minus className="h-4 w-4 text-muted-foreground" />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filter leaderboard data
|
|
||||||
const filteredLeaderboard = mockLeaderboard.filter(user => {
|
|
||||||
const matchesSearch = user.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
||||||
user.department.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
||||||
user.title.toLowerCase().includes(searchQuery.toLowerCase());
|
|
||||||
|
|
||||||
const matchesDepartment = selectedDepartment === 'all' || user.department === selectedDepartment;
|
|
||||||
const matchesLevel = selectedLevel === 'all' || user.level === selectedLevel;
|
|
||||||
|
|
||||||
return matchesSearch && matchesDepartment && matchesLevel;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get unique departments and levels for filters
|
|
||||||
const departments = Array.from(new Set(mockLeaderboard.map(user => user.department)));
|
|
||||||
const levels = Array.from(new Set(mockLeaderboard.map(user => user.level)));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LearnerLayout currentPage="/leaderboard" userType="corporate" user={user}>
|
|
||||||
<div className="p-6 space-y-8">
|
|
||||||
{/* KPI Cards - Matching Dashboard Design with Brand Colors */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
||||||
<Card className="hover:shadow-lg transition-shadow duration-200">
|
|
||||||
<CardContent className="p-6 text-center">
|
|
||||||
<div className="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-3">
|
|
||||||
<Users className="h-6 w-6 text-primary" />
|
|
||||||
</div>
|
|
||||||
<p className="text-2xl font-bold text-foreground mb-1">128</p>
|
|
||||||
<p className="text-base text-muted-foreground">Active Learners</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="hover:shadow-lg transition-shadow duration-200">
|
|
||||||
<CardContent className="p-6 text-center">
|
|
||||||
<div className="w-12 h-12 bg-success/10 rounded-full flex items-center justify-center mx-auto mb-3">
|
|
||||||
<BookOpen className="h-6 w-6 text-success" />
|
|
||||||
</div>
|
|
||||||
<p className="text-2xl font-bold text-foreground mb-1">1,247</p>
|
|
||||||
<p className="text-base text-muted-foreground">Courses Completed</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="hover:shadow-lg transition-shadow duration-200">
|
|
||||||
<CardContent className="p-6 text-center">
|
|
||||||
<div className="w-12 h-12 bg-[#F8C301]/10 rounded-full flex items-center justify-center mx-auto mb-3">
|
|
||||||
<Trophy className="h-6 w-6 text-[#26231A]" />
|
|
||||||
</div>
|
|
||||||
<p className="text-2xl font-bold text-foreground mb-1">89%</p>
|
|
||||||
<p className="text-base text-muted-foreground">Avg Completion</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="hover:shadow-lg transition-shadow duration-200">
|
|
||||||
<CardContent className="p-6 text-center">
|
|
||||||
<div className="w-12 h-12 bg-[#26231A]/10 rounded-full flex items-center justify-center mx-auto mb-3">
|
|
||||||
<TrendingUp className="h-6 w-6 text-[#26231A]" />
|
|
||||||
</div>
|
|
||||||
<p className="text-2xl font-bold text-foreground mb-1">+23%</p>
|
|
||||||
<p className="text-base text-muted-foreground">This Month</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Time Period Filter - Enhanced Design */}
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-6">
|
|
||||||
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-xl font-semibold mb-1 text-foreground">Performance Tracking</h2>
|
|
||||||
<p className="text-base text-muted-foreground">View leaderboard data across different time periods</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
variant={timeFilter === 'week' ? 'default' : 'outline'}
|
|
||||||
onClick={() => setTimeFilter('week')}
|
|
||||||
className="text-base h-11"
|
|
||||||
>
|
|
||||||
This Week
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={timeFilter === 'month' ? 'default' : 'outline'}
|
|
||||||
onClick={() => setTimeFilter('month')}
|
|
||||||
className="text-base h-11"
|
|
||||||
>
|
|
||||||
This Month
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={timeFilter === 'quarter' ? 'default' : 'outline'}
|
|
||||||
onClick={() => setTimeFilter('quarter')}
|
|
||||||
className="text-base h-11"
|
|
||||||
>
|
|
||||||
This Quarter
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Top Performers - Enhanced with KLC Branding */}
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5 }}
|
|
||||||
>
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-10 h-10 bg-[#F8C301]/10 rounded-full flex items-center justify-center">
|
|
||||||
<Crown className="h-5 w-5 text-[#26231A]" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<CardTitle className="text-xl text-foreground">Top Performers This Month</CardTitle>
|
|
||||||
<CardDescription className="text-base">
|
|
||||||
Celebrating our highest achievers and their outstanding dedication
|
|
||||||
</CardDescription>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="p-6">
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
||||||
{filteredLeaderboard.slice(0, 3).map((user, index) => {
|
|
||||||
const isFirst = index === 0;
|
|
||||||
const isSecond = index === 1;
|
|
||||||
const isThird = index === 2;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
key={user.id}
|
|
||||||
initial={{ opacity: 0, y: 30, scale: 0.95 }}
|
|
||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
||||||
transition={{
|
|
||||||
duration: 0.6,
|
|
||||||
delay: index * 0.15,
|
|
||||||
ease: "easeOut"
|
|
||||||
}}
|
|
||||||
whileHover={{
|
|
||||||
scale: 1.02,
|
|
||||||
y: -5,
|
|
||||||
transition: { duration: 0.2 }
|
|
||||||
}}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<Card
|
|
||||||
className={`relative overflow-hidden transition-all duration-300 hover:shadow-xl cursor-pointer group ${
|
|
||||||
isFirst ? 'border-2 border-[#F8C301] bg-gradient-to-br from-[#F8C301]/5 to-[#F8C301]/10' :
|
|
||||||
isSecond ? 'border-2 border-primary bg-gradient-to-br from-primary/5 to-primary/10' :
|
|
||||||
'border-2 border-[#26231A] bg-gradient-to-br from-[#26231A]/5 to-[#26231A]/10'
|
|
||||||
} ${isFirst ? 'lg:scale-105 lg:-translate-y-2' : ''}`}
|
|
||||||
>
|
|
||||||
{/* Subtle Metallic Shimmer Effect for Top 3 */}
|
|
||||||
<div
|
|
||||||
className={`absolute inset-0 opacity-0 group-hover:opacity-60 transition-opacity duration-1000 pointer-events-none shimmer-effect ${
|
|
||||||
isFirst ? 'shimmer-gold' : isSecond ? 'shimmer-silver' : 'shimmer-bronze'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Rank Ribbon with KLC Colors */}
|
|
||||||
<motion.div
|
|
||||||
className={`absolute top-0 right-0 px-3 py-1 rounded-bl-lg text-white text-sm font-medium ${
|
|
||||||
isFirst ? 'bg-[#F8C301] text-[#26231A]' :
|
|
||||||
isSecond ? 'bg-primary text-white' :
|
|
||||||
'bg-[#26231A] text-white'
|
|
||||||
}`}
|
|
||||||
initial={{ x: 20, opacity: 0 }}
|
|
||||||
animate={{ x: 0, opacity: 1 }}
|
|
||||||
transition={{ delay: index * 0.15 + 0.3 }}
|
|
||||||
>
|
|
||||||
#{user.rank}
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Winner Crown for First Place */}
|
|
||||||
{isFirst && (
|
|
||||||
<motion.div
|
|
||||||
className="absolute top-4 left-4"
|
|
||||||
initial={{ rotate: -10, scale: 0 }}
|
|
||||||
animate={{
|
|
||||||
rotate: 0,
|
|
||||||
scale: 1,
|
|
||||||
y: [0, -2, 0],
|
|
||||||
rotateZ: [0, 2, -2, 0]
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
delay: 0.5,
|
|
||||||
type: "spring",
|
|
||||||
stiffness: 200,
|
|
||||||
damping: 10,
|
|
||||||
y: {
|
|
||||||
duration: 2,
|
|
||||||
repeat: Infinity,
|
|
||||||
ease: "easeInOut"
|
|
||||||
},
|
|
||||||
rotateZ: {
|
|
||||||
duration: 4,
|
|
||||||
repeat: Infinity,
|
|
||||||
ease: "easeInOut"
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Crown className="h-6 w-6 text-[#F8C301]" />
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<CardContent className="p-8 text-center">
|
|
||||||
{/* Rank Icon */}
|
|
||||||
<motion.div
|
|
||||||
className="flex justify-center mb-6"
|
|
||||||
initial={{ scale: 0, rotate: -180 }}
|
|
||||||
animate={{ scale: 1, rotate: 0 }}
|
|
||||||
transition={{
|
|
||||||
delay: index * 0.15 + 0.2,
|
|
||||||
type: "spring",
|
|
||||||
stiffness: 150
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className={`p-4 rounded-full ${
|
|
||||||
isFirst ? 'bg-[#F8C301]/10' : isSecond ? 'bg-primary/10' : 'bg-[#26231A]/10'
|
|
||||||
}`}>
|
|
||||||
{getRankIcon(user.rank)}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* User Avatar with Enhanced Styling */}
|
|
||||||
<motion.div
|
|
||||||
className="relative mb-6"
|
|
||||||
initial={{ scale: 0 }}
|
|
||||||
animate={{ scale: 1 }}
|
|
||||||
transition={{
|
|
||||||
delay: index * 0.15 + 0.4,
|
|
||||||
type: "spring",
|
|
||||||
stiffness: 200
|
|
||||||
}}
|
|
||||||
whileHover={{ scale: 1.05 }}
|
|
||||||
>
|
|
||||||
<Avatar className="w-24 h-24 mx-auto border-4 border-white shadow-lg">
|
|
||||||
<AvatarImage src={user.avatar} alt={user.name} />
|
|
||||||
<AvatarFallback className="text-xl">
|
|
||||||
{user.name.split(' ').map(n => n[0]).join('')}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
{/* Trend Indicator */}
|
|
||||||
<motion.div
|
|
||||||
className="absolute -bottom-2 -right-2 p-1 bg-white rounded-full shadow-md"
|
|
||||||
initial={{ scale: 0 }}
|
|
||||||
animate={{
|
|
||||||
scale: [1, 1.1, 1]
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
delay: index * 0.15 + 0.6,
|
|
||||||
scale: {
|
|
||||||
duration: 2,
|
|
||||||
repeat: Infinity,
|
|
||||||
ease: "easeInOut"
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{getTrendIcon(user.trend)}
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* User Information */}
|
|
||||||
<motion.div
|
|
||||||
className="mb-6"
|
|
||||||
initial={{ opacity: 0, y: 10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: index * 0.15 + 0.5 }}
|
|
||||||
>
|
|
||||||
<h3 className="text-xl mb-2 text-foreground">{user.name}</h3>
|
|
||||||
<p className="text-base text-muted-foreground mb-1">{user.title}</p>
|
|
||||||
<p className="text-base text-muted-foreground">{user.department}</p>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Performance Stats */}
|
|
||||||
<motion.div
|
|
||||||
className="grid grid-cols-3 gap-4 mb-6 p-4 bg-white/50 rounded-lg backdrop-blur-sm"
|
|
||||||
initial={{ opacity: 0, scale: 0.9 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
transition={{ delay: index * 0.15 + 0.7 }}
|
|
||||||
>
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-2xl font-bold text-foreground">
|
|
||||||
<AnimatedNumber value={user.totalPoints} duration={1500} />
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-muted-foreground">Points</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-center border-x border-border/50">
|
|
||||||
<p className="text-2xl font-bold text-foreground">
|
|
||||||
<AnimatedNumber value={user.coursesCompleted} duration={1200} />
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-muted-foreground">Courses</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-2xl font-bold text-foreground">
|
|
||||||
<AnimatedNumber value={user.streakDays} duration={1000} />
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-muted-foreground">Day Streak</p>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Level Progress */}
|
|
||||||
<motion.div
|
|
||||||
className="mb-6"
|
|
||||||
initial={{ opacity: 0, x: -20 }}
|
|
||||||
animate={{ opacity: 1, x: 0 }}
|
|
||||||
transition={{ delay: index * 0.15 + 0.8 }}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<span className="text-base text-muted-foreground">Level Progress</span>
|
|
||||||
<Badge variant="outline" className="text-sm font-medium">
|
|
||||||
{user.level}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<Progress value={user.progress} className="h-2" />
|
|
||||||
<p className="text-sm text-muted-foreground mt-1">{user.progress}% to next level</p>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Achievement Badges */}
|
|
||||||
<motion.div
|
|
||||||
className="space-y-3"
|
|
||||||
initial={{ opacity: 0, y: 10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: index * 0.15 + 0.9 }}
|
|
||||||
>
|
|
||||||
<p className="text-base text-muted-foreground">Recent Achievements</p>
|
|
||||||
<div className="flex flex-wrap justify-center gap-2">
|
|
||||||
{user.badges.slice(0, 3).map((badge, badgeIndex) => (
|
|
||||||
<motion.div
|
|
||||||
key={badge}
|
|
||||||
initial={{ opacity: 0, scale: 0.8 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
transition={{
|
|
||||||
delay: index * 0.15 + 1 + (badgeIndex * 0.1)
|
|
||||||
}}
|
|
||||||
whileHover={{ scale: 1.05 }}
|
|
||||||
>
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className={`text-sm px-3 py-1 ${
|
|
||||||
badgeIndex === 0 ? 'border-[#F8C301]/30 bg-[#F8C301]/5 text-[#26231A]' :
|
|
||||||
badgeIndex === 1 ? 'border-primary/30 bg-primary/5 text-primary' :
|
|
||||||
'border-success/30 bg-success/5 text-success'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{badge}
|
|
||||||
</Badge>
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Monthly Performance Indicator */}
|
|
||||||
<motion.div
|
|
||||||
className="mt-6 pt-4 border-t border-border/50"
|
|
||||||
initial={{ opacity: 0, y: 10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: index * 0.15 + 1.1 }}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-center gap-2">
|
|
||||||
<TrendingUp className="h-4 w-4 text-success" />
|
|
||||||
<span className="text-base text-success font-medium">
|
|
||||||
+<AnimatedNumber value={user.monthlyPoints} duration={1800} /> this month
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Search and Filters - Matching Dashboard Design */}
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-6">
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* Search Bar */}
|
|
||||||
<div className="relative">
|
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
||||||
<Input
|
|
||||||
placeholder="Search by name, department, or title..."
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
className="pl-10 text-base h-11"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filters Row */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
||||||
<Select value={selectedDepartment} onValueChange={setSelectedDepartment}>
|
|
||||||
<SelectTrigger className="text-base h-11">
|
|
||||||
<SelectValue placeholder="All Departments" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">All Departments</SelectItem>
|
|
||||||
{departments.map(dept => (
|
|
||||||
<SelectItem key={dept} value={dept}>{dept}</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<Select value={selectedLevel} onValueChange={setSelectedLevel}>
|
|
||||||
<SelectTrigger className="text-base h-11">
|
|
||||||
<SelectValue placeholder="All Levels" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">All Levels</SelectItem>
|
|
||||||
{levels.map(level => (
|
|
||||||
<SelectItem key={level} value={level}>{level}</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<Select value={sortBy} onValueChange={setSortBy}>
|
|
||||||
<SelectTrigger className="text-base h-11">
|
|
||||||
<SelectValue placeholder="Sort By" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="rank">Rank</SelectItem>
|
|
||||||
<SelectItem value="points">Total Points</SelectItem>
|
|
||||||
<SelectItem value="courses">Courses Completed</SelectItem>
|
|
||||||
<SelectItem value="streak">Learning Streak</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="text-base h-11"
|
|
||||||
onClick={() => {
|
|
||||||
setSearchQuery('');
|
|
||||||
setSelectedDepartment('all');
|
|
||||||
setSelectedLevel('all');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Clear Filters
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Full Leaderboard Table - Enhanced Design */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-10 h-10 bg-primary/10 rounded-full flex items-center justify-center">
|
|
||||||
<Trophy className="h-5 w-5 text-primary" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<CardTitle className="text-xl text-foreground">Complete Leaderboard</CardTitle>
|
|
||||||
<p className="text-base text-muted-foreground mt-1">
|
|
||||||
View all team members and their performance metrics
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Badge variant="outline" className="text-base">
|
|
||||||
{filteredLeaderboard.length} {filteredLeaderboard.length === 1 ? 'learner' : 'learners'}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<CardContent className="p-6">
|
|
||||||
<div className="space-y-4 !mt-0 !pt-0">
|
|
||||||
{filteredLeaderboard.map((user, index) => (
|
|
||||||
<motion.div
|
|
||||||
key={user.id}
|
|
||||||
initial={{ opacity: 0, x: -20 }}
|
|
||||||
animate={{ opacity: 1, x: 0 }}
|
|
||||||
transition={{ delay: index * 0.05 }}
|
|
||||||
className="group cursor-pointer p-4 rounded-lg border border-border hover:border-primary/30 hover:bg-muted/30 transition-all duration-200"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
{/* Rank */}
|
|
||||||
<div className="flex items-center justify-center w-12 h-12 rounded-full bg-muted flex-shrink-0">
|
|
||||||
<div className="flex items-center justify-center w-8 h-8">
|
|
||||||
{user.rank <= 3 ? getRankIcon(user.rank) : (
|
|
||||||
<span className="text-lg font-bold text-foreground">#{user.rank}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Avatar and User Info */}
|
|
||||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
|
||||||
<Avatar className="w-10 h-10 flex-shrink-0">
|
|
||||||
<AvatarImage src={user.avatar} alt={user.name} />
|
|
||||||
<AvatarFallback className="text-sm bg-primary/10 text-primary">
|
|
||||||
{user.name.split(' ').map(n => n[0]).join('')}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<p className="text-base font-medium text-foreground group-hover:text-primary transition-colors truncate">
|
|
||||||
{user.name}
|
|
||||||
</p>
|
|
||||||
{getTrendIcon(user.trend)}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
||||||
<span>{user.title}</span>
|
|
||||||
<span>•</span>
|
|
||||||
<span>{user.department}</span>
|
|
||||||
<span>•</span>
|
|
||||||
<Badge variant="outline" className="text-sm">
|
|
||||||
{user.level}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Performance Stats */}
|
|
||||||
<div className="hidden md:flex items-center gap-6 text-sm">
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="font-bold text-foreground">{user.totalPoints.toLocaleString()}</p>
|
|
||||||
<p className="text-muted-foreground">Points</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="font-bold text-foreground">{user.coursesCompleted}</p>
|
|
||||||
<p className="text-muted-foreground">Courses</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="font-bold text-foreground">{user.streakDays}</p>
|
|
||||||
<p className="text-muted-foreground">Streak</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="font-bold text-foreground">{user.completionRate}%</p>
|
|
||||||
<p className="text-muted-foreground">Complete</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Action */}
|
|
||||||
<ChevronRight className="h-4 w-4 text-muted-foreground group-hover:text-primary group-hover:translate-x-1 transition-all flex-shrink-0" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mobile Stats */}
|
|
||||||
<div className="md:hidden mt-4 grid grid-cols-4 gap-3 text-sm">
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="font-bold text-foreground">{user.totalPoints.toLocaleString()}</p>
|
|
||||||
<p className="text-muted-foreground">Points</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="font-bold text-foreground">{user.coursesCompleted}</p>
|
|
||||||
<p className="text-muted-foreground">Courses</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="font-bold text-foreground">{user.streakDays}</p>
|
|
||||||
<p className="text-muted-foreground">Streak</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="font-bold text-foreground">{user.completionRate}%</p>
|
|
||||||
<p className="text-muted-foreground">Complete</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</LearnerLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,847 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { Navigation } from '../components/Navigation';
|
|
||||||
import { Footer } from '../components/Footer';
|
|
||||||
import { AIChatbot } from '../components/AIChatbot';
|
|
||||||
const navigate = useNavigate();
|
|
||||||
import { Button } from '../components/ui/button';
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card';
|
|
||||||
import { Input } from '../components/ui/input';
|
|
||||||
import { Label } from '../components/ui/label';
|
|
||||||
import { Checkbox } from '../components/ui/checkbox';
|
|
||||||
import { Alert, AlertDescription } from '../components/ui/alert';
|
|
||||||
import { Badge } from '../components/ui/badge';
|
|
||||||
import { Progress } from '../components/ui/progress';
|
|
||||||
import {
|
|
||||||
Eye,
|
|
||||||
EyeOff,
|
|
||||||
Mail,
|
|
||||||
User,
|
|
||||||
Lock,
|
|
||||||
ArrowRight,
|
|
||||||
CheckCircle,
|
|
||||||
AlertCircle,
|
|
||||||
BookOpen,
|
|
||||||
Award,
|
|
||||||
Users,
|
|
||||||
TrendingUp,
|
|
||||||
RefreshCw,
|
|
||||||
Check,
|
|
||||||
X,
|
|
||||||
BarChart3,
|
|
||||||
GraduationCap,
|
|
||||||
Smartphone,
|
|
||||||
Target,
|
|
||||||
Star
|
|
||||||
} from 'lucide-react';
|
|
||||||
|
|
||||||
interface SignInFormData {
|
|
||||||
workEmail: string;
|
|
||||||
password: string;
|
|
||||||
rememberMe: boolean;
|
|
||||||
otp: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SignUpFormData {
|
|
||||||
employeeId: string;
|
|
||||||
workEmail: string;
|
|
||||||
fullName: string;
|
|
||||||
department: string;
|
|
||||||
password: string;
|
|
||||||
confirmPassword: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PasswordStrength {
|
|
||||||
score: number;
|
|
||||||
label: string;
|
|
||||||
color: string;
|
|
||||||
criteria: {
|
|
||||||
length: boolean;
|
|
||||||
lowercase: boolean;
|
|
||||||
uppercase: boolean;
|
|
||||||
number: boolean;
|
|
||||||
symbol: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const learnerBenefits = [
|
|
||||||
{
|
|
||||||
icon: BookOpen,
|
|
||||||
title: 'Personal Learning Path',
|
|
||||||
description: 'Customized development journey based on your role and goals'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Award,
|
|
||||||
title: 'Skill Certifications',
|
|
||||||
description: 'Earn industry-recognized certificates and digital badges'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Users,
|
|
||||||
title: 'Peer Learning Network',
|
|
||||||
description: 'Connect with colleagues and share knowledge across teams'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: BarChart3,
|
|
||||||
title: 'Progress Tracking',
|
|
||||||
description: 'Monitor your learning progress and skill development'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const learnerStats = [
|
|
||||||
{
|
|
||||||
icon: GraduationCap,
|
|
||||||
metric: '95%',
|
|
||||||
label: 'Course Completion'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Target,
|
|
||||||
metric: '4.8/5',
|
|
||||||
label: 'Learner Rating'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: TrendingUp,
|
|
||||||
metric: '78%',
|
|
||||||
label: 'Career Growth'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Star,
|
|
||||||
metric: '90%',
|
|
||||||
label: 'Skill Improvement'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function CorporateLearnerLogin() {
|
|
||||||
const [isSignUp, setIsSignUp] = useState(false);
|
|
||||||
const [signInData, setSignInData] = useState<SignInFormData>({
|
|
||||||
workEmail: '',
|
|
||||||
password: '',
|
|
||||||
rememberMe: true,
|
|
||||||
otp: ''
|
|
||||||
});
|
|
||||||
const [signUpData, setSignUpData] = useState<SignUpFormData>({
|
|
||||||
employeeId: '',
|
|
||||||
workEmail: '',
|
|
||||||
fullName: '',
|
|
||||||
department: '',
|
|
||||||
password: '',
|
|
||||||
confirmPassword: ''
|
|
||||||
});
|
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
|
||||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
||||||
const [passwordStrength, setPasswordStrength] = useState<PasswordStrength>({
|
|
||||||
score: 0,
|
|
||||||
label: 'Very Weak',
|
|
||||||
color: 'bg-destructive',
|
|
||||||
criteria: {
|
|
||||||
length: false,
|
|
||||||
lowercase: false,
|
|
||||||
uppercase: false,
|
|
||||||
number: false,
|
|
||||||
symbol: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const [requiresMFA, setRequiresMFA] = useState(false);
|
|
||||||
const [isFormShaking, setIsFormShaking] = useState(false);
|
|
||||||
const [accountSuspended, setAccountSuspended] = useState(false);
|
|
||||||
const [showEmailVerification, setShowEmailVerification] = useState(false);
|
|
||||||
const [maskedEmail, setMaskedEmail] = useState('');
|
|
||||||
const [resendCooldown, setResendCooldown] = useState(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
document.title = isSignUp
|
|
||||||
? 'Join Your Learning Journey - Corporate Learner | KLC'
|
|
||||||
: 'Welcome Back, Learner - Corporate Access | KLC';
|
|
||||||
}, [isSignUp]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (resendCooldown > 0) {
|
|
||||||
const timer = setTimeout(() => setResendCooldown(resendCooldown - 1), 1000);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}
|
|
||||||
}, [resendCooldown]);
|
|
||||||
|
|
||||||
const calculatePasswordStrength = (password: string): PasswordStrength => {
|
|
||||||
const criteria = {
|
|
||||||
length: password.length >= 8,
|
|
||||||
lowercase: /[a-z]/.test(password),
|
|
||||||
uppercase: /[A-Z]/.test(password),
|
|
||||||
number: /\d/.test(password),
|
|
||||||
symbol: /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)
|
|
||||||
};
|
|
||||||
|
|
||||||
const score = Object.values(criteria).filter(Boolean).length;
|
|
||||||
|
|
||||||
let label = 'Very Weak';
|
|
||||||
let color = 'bg-destructive';
|
|
||||||
|
|
||||||
if (score >= 4) {
|
|
||||||
label = 'Strong';
|
|
||||||
color = 'bg-success';
|
|
||||||
} else if (score >= 3) {
|
|
||||||
label = 'Medium';
|
|
||||||
color = 'bg-secondary';
|
|
||||||
} else if (score >= 2) {
|
|
||||||
label = 'Weak';
|
|
||||||
color = 'bg-orange-500';
|
|
||||||
}
|
|
||||||
|
|
||||||
return { score, label, color, criteria };
|
|
||||||
};
|
|
||||||
|
|
||||||
const maskEmail = (email: string): string => {
|
|
||||||
const [username, domain] = email.split('@');
|
|
||||||
if (username.length <= 3) {
|
|
||||||
return `${username[0]}***@${domain}`;
|
|
||||||
}
|
|
||||||
return `${username.substring(0, 2)}***${username.slice(-1)}@${domain}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateSignInForm = (): boolean => {
|
|
||||||
const newErrors: Record<string, string> = {};
|
|
||||||
|
|
||||||
if (!signInData.workEmail) {
|
|
||||||
newErrors.workEmail = 'Work email is required';
|
|
||||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(signInData.workEmail)) {
|
|
||||||
newErrors.workEmail = 'Please enter a valid email address';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!signInData.password) {
|
|
||||||
newErrors.password = 'Password is required';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requiresMFA && !signInData.otp) {
|
|
||||||
newErrors.otp = 'Verification code is required';
|
|
||||||
} else if (requiresMFA && signInData.otp && !/^\d{6}$/.test(signInData.otp)) {
|
|
||||||
newErrors.otp = 'Please enter a 6-digit verification code';
|
|
||||||
}
|
|
||||||
|
|
||||||
setErrors(newErrors);
|
|
||||||
return Object.keys(newErrors).length === 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateSignUpForm = (): boolean => {
|
|
||||||
const newErrors: Record<string, string> = {};
|
|
||||||
|
|
||||||
if (!signUpData.employeeId) {
|
|
||||||
newErrors.employeeId = 'Employee ID is required';
|
|
||||||
} else if (signUpData.employeeId.length < 3) {
|
|
||||||
newErrors.employeeId = 'Employee ID must be at least 3 characters';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!signUpData.workEmail) {
|
|
||||||
newErrors.workEmail = 'Work email is required';
|
|
||||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(signUpData.workEmail)) {
|
|
||||||
newErrors.workEmail = 'Please enter a valid work email address';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!signUpData.fullName) {
|
|
||||||
newErrors.fullName = 'Full name is required';
|
|
||||||
} else if (signUpData.fullName.trim().length < 2) {
|
|
||||||
newErrors.fullName = 'Full name must be at least 2 characters';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!signUpData.department) {
|
|
||||||
newErrors.department = 'Department is required';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!signUpData.password) {
|
|
||||||
newErrors.password = 'Password is required';
|
|
||||||
} else if (passwordStrength.score < 3) {
|
|
||||||
newErrors.password = 'Password must be stronger (at least 3 criteria met)';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!signUpData.confirmPassword) {
|
|
||||||
newErrors.confirmPassword = 'Please confirm your password';
|
|
||||||
} else if (signUpData.password !== signUpData.confirmPassword) {
|
|
||||||
newErrors.confirmPassword = 'Passwords do not match';
|
|
||||||
}
|
|
||||||
|
|
||||||
setErrors(newErrors);
|
|
||||||
return Object.keys(newErrors).length === 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSignInInputChange = (field: keyof SignInFormData, value: any) => {
|
|
||||||
setSignInData(prev => ({ ...prev, [field]: value }));
|
|
||||||
if (errors[field]) {
|
|
||||||
setErrors(prev => ({ ...prev, [field]: '' }));
|
|
||||||
}
|
|
||||||
if (accountSuspended) {
|
|
||||||
setAccountSuspended(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSignUpInputChange = (field: keyof SignUpFormData, value: string) => {
|
|
||||||
setSignUpData(prev => ({ ...prev, [field]: value }));
|
|
||||||
|
|
||||||
if (errors[field]) {
|
|
||||||
setErrors(prev => ({ ...prev, [field]: '' }));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (field === 'password') {
|
|
||||||
setPasswordStrength(calculatePasswordStrength(value));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const triggerFormShake = () => {
|
|
||||||
setIsFormShaking(true);
|
|
||||||
setTimeout(() => setIsFormShaking(false), 500);
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleFormMode = () => {
|
|
||||||
setIsSignUp(!isSignUp);
|
|
||||||
setErrors({});
|
|
||||||
setRequiresMFA(false);
|
|
||||||
setAccountSuspended(false);
|
|
||||||
setIsFormShaking(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSignInSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!validateSignInForm()) {
|
|
||||||
triggerFormShake();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSubmitting(true);
|
|
||||||
setAccountSuspended(false);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
||||||
|
|
||||||
if (signInData.workEmail === 'suspended@company.com') {
|
|
||||||
setAccountSuspended(true);
|
|
||||||
setIsSubmitting(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (signInData.workEmail === 'mfa@company.com' && signInData.password === 'learner123' && !requiresMFA) {
|
|
||||||
setRequiresMFA(true);
|
|
||||||
setIsSubmitting(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (signInData.workEmail === 'learner@company.com' && signInData.password === 'learner123') {
|
|
||||||
const redirectUrl = sessionStorage.getItem('loginRedirect') || '/corporate/dashboard';
|
|
||||||
sessionStorage.removeItem('loginRedirect');
|
|
||||||
navigate(redirectUrl);
|
|
||||||
} else if (requiresMFA && signInData.otp === '123456') {
|
|
||||||
const redirectUrl = sessionStorage.getItem('loginRedirect') || '/corporate/dashboard';
|
|
||||||
sessionStorage.removeItem('loginRedirect');
|
|
||||||
navigate(redirectUrl);
|
|
||||||
} else {
|
|
||||||
setErrors({ submit: 'Email or password incorrect' });
|
|
||||||
triggerFormShake();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setErrors({ submit: 'Something went wrong. Please try again.' });
|
|
||||||
triggerFormShake();
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSignUpSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!validateSignUpForm()) return;
|
|
||||||
|
|
||||||
setIsSubmitting(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
||||||
setMaskedEmail(maskEmail(signUpData.workEmail));
|
|
||||||
setShowEmailVerification(true);
|
|
||||||
} catch (error) {
|
|
||||||
setErrors(prev => ({ ...prev, submit: 'Something went wrong. Please try again.' }));
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleResendEmail = async () => {
|
|
||||||
if (resendCooldown > 0) return;
|
|
||||||
setResendCooldown(60);
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleVerifyEmail = (token: string) => {
|
|
||||||
navigate('/corporate/dashboard');
|
|
||||||
};
|
|
||||||
|
|
||||||
if (showEmailVerification) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-background">
|
|
||||||
<Navigation currentPage="/signin/corporate-learner" />
|
|
||||||
|
|
||||||
<main className="pt-40 pb-16">
|
|
||||||
<div className="container mx-auto px-4 lg:px-8">
|
|
||||||
<div className="max-w-2xl mx-auto text-center">
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-12">
|
|
||||||
<div className="w-16 h-16 bg-secondary/10 rounded-full flex items-center justify-center mx-auto mb-6">
|
|
||||||
<GraduationCap className="w-8 h-8 text-secondary" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1 className="text-2xl mb-4">Check Your Inbox</h1>
|
|
||||||
<p className="text-base text-muted-foreground mb-6">
|
|
||||||
We've sent a verification email to <strong>{maskedEmail}</strong>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="text-base text-muted-foreground mb-8">
|
|
||||||
Click the verification link to activate your learner account and start your development journey.
|
|
||||||
The link will expire in 30 minutes.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<Button
|
|
||||||
onClick={handleResendEmail}
|
|
||||||
variant="outline"
|
|
||||||
disabled={resendCooldown > 0}
|
|
||||||
className="text-base min-h-[44px]"
|
|
||||||
>
|
|
||||||
<RefreshCw className="w-4 h-4 mr-2" />
|
|
||||||
{resendCooldown > 0 ? `Resend in ${resendCooldown}s` : 'Resend Email'}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Need help getting started? Contact your HR team or email us at{' '}
|
|
||||||
<a href="mailto:learners@klc.edu.in" className="text-primary hover:underline">
|
|
||||||
learners@klc.edu.in
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-8 p-4 bg-muted/50 rounded-lg">
|
|
||||||
<p className="text-sm text-muted-foreground mb-2">Demo Mode:</p>
|
|
||||||
<Button
|
|
||||||
onClick={() => handleVerifyEmail('demo-token')}
|
|
||||||
size="sm"
|
|
||||||
className="text-sm min-h-[44px]"
|
|
||||||
>
|
|
||||||
Simulate Email Verification
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<Footer />
|
|
||||||
<AIChatbot />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-background">
|
|
||||||
<Navigation currentPage="/signin/corporate-learner" />
|
|
||||||
|
|
||||||
<main className="pt-40 pb-16">
|
|
||||||
<div className="container mx-auto px-4 lg:px-8">
|
|
||||||
<div className="max-w-6xl mx-auto">
|
|
||||||
<div className="grid lg:grid-cols-2 gap-12 items-center">
|
|
||||||
<div className="order-2 lg:order-1">
|
|
||||||
<div className="space-y-8">
|
|
||||||
<div>
|
|
||||||
<Badge variant="secondary" className="mb-4 text-base min-h-[44px] px-4 py-2">
|
|
||||||
<GraduationCap className="w-4 h-4 mr-2" />
|
|
||||||
Corporate Learner Access
|
|
||||||
</Badge>
|
|
||||||
<h1 className="text-3xl lg:text-4xl mb-6 leading-tight">
|
|
||||||
{isSignUp ? 'Start Your Learning Journey' : 'Welcome Back to Your Learning'}
|
|
||||||
</h1>
|
|
||||||
<p className="text-base text-muted-foreground leading-relaxed">
|
|
||||||
{isSignUp
|
|
||||||
? 'Join your organization\'s learning community and unlock your potential with personalized development programs, skill certifications, and peer collaboration.'
|
|
||||||
: 'Continue your professional development journey with access to your learning dashboard, progress tracking, and new skill-building opportunities.'
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
{(isSignUp ? learnerBenefits : learnerBenefits.slice(0, 3)).map((benefit) => {
|
|
||||||
const Icon = benefit.icon;
|
|
||||||
return (
|
|
||||||
<div key={benefit.title} className="flex items-start gap-4">
|
|
||||||
<div className="w-12 h-12 bg-secondary/10 rounded-lg flex items-center justify-center flex-shrink-0">
|
|
||||||
<Icon className="w-6 h-6 text-secondary" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-base font-semibold mb-1">{benefit.title}</h3>
|
|
||||||
<p className="text-base text-muted-foreground">{benefit.description}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-8 border-t border-border/50">
|
|
||||||
<div className="mb-6">
|
|
||||||
<h3 className="text-lg font-semibold text-foreground mb-2">Learning Excellence</h3>
|
|
||||||
<p className="text-base text-muted-foreground">Join thousands of successful learners</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-6">
|
|
||||||
{learnerStats.map((stat) => {
|
|
||||||
const Icon = stat.icon;
|
|
||||||
return (
|
|
||||||
<div key={stat.label} className="group">
|
|
||||||
<div className="bg-gradient-to-br from-secondary/5 to-primary/5 rounded-xl p-4 border border-border/50 hover:border-secondary/20 transition-all duration-200">
|
|
||||||
<div className="flex items-center gap-3 mb-3">
|
|
||||||
<div className="w-10 h-10 bg-secondary/10 rounded-lg flex items-center justify-center flex-shrink-0 group-hover:bg-secondary/15 transition-colors duration-200">
|
|
||||||
<Icon className="w-5 h-5 text-secondary" />
|
|
||||||
</div>
|
|
||||||
<div className="text-2xl font-bold text-secondary leading-none">{stat.metric}</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-base font-medium text-foreground/80">{stat.label}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="order-1 lg:order-2">
|
|
||||||
<Card className={`w-full transition-all duration-300 ${isFormShaking ? 'animate-pulse' : ''}`}>
|
|
||||||
<CardHeader className="text-center">
|
|
||||||
<CardTitle className="text-2xl">
|
|
||||||
{isSignUp ? 'Join Your Learning Community' : 'Welcome Back, Learner'}
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription className="text-base min-h-[44px] flex items-center justify-center">
|
|
||||||
{isSignUp
|
|
||||||
? 'Get started with your personalized learning journey'
|
|
||||||
: requiresMFA
|
|
||||||
? 'Enter the verification code sent to your device'
|
|
||||||
: 'Access your learning dashboard and continue growing'
|
|
||||||
}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<CardContent>
|
|
||||||
<form onSubmit={isSignUp ? handleSignUpSubmit : handleSignInSubmit} className="space-y-6">
|
|
||||||
{accountSuspended && (
|
|
||||||
<Alert variant="destructive">
|
|
||||||
<AlertCircle className="h-4 w-4" />
|
|
||||||
<AlertDescription className="text-base">
|
|
||||||
Your account access has been temporarily suspended. Please contact your HR team or email learners@klc.edu.in for assistance.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{errors.submit && (
|
|
||||||
<Alert variant="destructive">
|
|
||||||
<AlertCircle className="h-4 w-4" />
|
|
||||||
<AlertDescription className="text-base">{errors.submit}</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isSignUp && (
|
|
||||||
<>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="employeeId" className="text-base">Employee ID *</Label>
|
|
||||||
<div className="relative">
|
|
||||||
<User className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
|
|
||||||
<Input
|
|
||||||
id="employeeId"
|
|
||||||
type="text"
|
|
||||||
value={signUpData.employeeId}
|
|
||||||
onChange={(e) => handleSignUpInputChange('employeeId', e.target.value)}
|
|
||||||
placeholder="Your employee ID"
|
|
||||||
className={`pl-10 text-base min-h-[44px] ${errors.employeeId ? 'border-destructive' : ''}`}
|
|
||||||
autoComplete="username"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{errors.employeeId && (
|
|
||||||
<p className="text-sm text-destructive flex items-center gap-1">
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
{errors.employeeId}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="fullName" className="text-base">Full Name *</Label>
|
|
||||||
<div className="relative">
|
|
||||||
<User className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
|
|
||||||
<Input
|
|
||||||
id="fullName"
|
|
||||||
type="text"
|
|
||||||
value={signUpData.fullName}
|
|
||||||
onChange={(e) => handleSignUpInputChange('fullName', e.target.value)}
|
|
||||||
placeholder="Your full name"
|
|
||||||
className={`pl-10 text-base min-h-[44px] ${errors.fullName ? 'border-destructive' : ''}`}
|
|
||||||
autoComplete="name"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{errors.fullName && (
|
|
||||||
<p className="text-sm text-destructive flex items-center gap-1">
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
{errors.fullName}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="department" className="text-base">Department *</Label>
|
|
||||||
<div className="relative">
|
|
||||||
<Users className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
|
|
||||||
<Input
|
|
||||||
id="department"
|
|
||||||
type="text"
|
|
||||||
value={signUpData.department}
|
|
||||||
onChange={(e) => handleSignUpInputChange('department', e.target.value)}
|
|
||||||
placeholder="Your department"
|
|
||||||
className={`pl-10 text-base min-h-[44px] ${errors.department ? 'border-destructive' : ''}`}
|
|
||||||
autoComplete="organization-title"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{errors.department && (
|
|
||||||
<p className="text-sm text-destructive flex items-center gap-1">
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
{errors.department}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(!requiresMFA || isSignUp) && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="workEmail" className="text-base">Work Email *</Label>
|
|
||||||
<div className="relative">
|
|
||||||
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
|
|
||||||
<Input
|
|
||||||
id="workEmail"
|
|
||||||
type="email"
|
|
||||||
value={isSignUp ? signUpData.workEmail : signInData.workEmail}
|
|
||||||
onChange={(e) => isSignUp
|
|
||||||
? handleSignUpInputChange('workEmail', e.target.value)
|
|
||||||
: handleSignInInputChange('workEmail', e.target.value)
|
|
||||||
}
|
|
||||||
placeholder={isSignUp ? "you@company.com" : "your.work@company.com"}
|
|
||||||
className={`pl-10 pr-10 text-base min-h-[44px] ${errors.workEmail ? 'border-destructive' : ''}`}
|
|
||||||
autoComplete="email"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{errors.workEmail && (
|
|
||||||
<p className="text-sm text-destructive flex items-center gap-1">
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
{errors.workEmail}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(!requiresMFA || isSignUp) && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="password" className="text-base">Password *</Label>
|
|
||||||
<div className="relative">
|
|
||||||
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
|
|
||||||
<Input
|
|
||||||
id="password"
|
|
||||||
type={showPassword ? "text" : "password"}
|
|
||||||
value={isSignUp ? signUpData.password : signInData.password}
|
|
||||||
onChange={(e) => isSignUp
|
|
||||||
? handleSignUpInputChange('password', e.target.value)
|
|
||||||
: handleSignInInputChange('password', e.target.value)
|
|
||||||
}
|
|
||||||
placeholder="Enter your password"
|
|
||||||
className={`pl-10 pr-10 text-base min-h-[44px] ${errors.password ? 'border-destructive' : ''}`}
|
|
||||||
autoComplete={isSignUp ? "new-password" : "current-password"}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent min-h-[44px] min-w-[44px]"
|
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
|
||||||
>
|
|
||||||
{showPassword ? (
|
|
||||||
<EyeOff className="w-4 h-4 text-muted-foreground" />
|
|
||||||
) : (
|
|
||||||
<Eye className="w-4 h-4 text-muted-foreground" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{errors.password && (
|
|
||||||
<p className="text-sm text-destructive flex items-center gap-1">
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
{errors.password}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isSignUp && signUpData.password && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-sm text-muted-foreground">Password strength:</span>
|
|
||||||
<span className={`text-sm font-medium ${
|
|
||||||
passwordStrength.score >= 4 ? 'text-success' :
|
|
||||||
passwordStrength.score >= 3 ? 'text-secondary' :
|
|
||||||
passwordStrength.score >= 2 ? 'text-orange-500' : 'text-destructive'
|
|
||||||
}`}>
|
|
||||||
{passwordStrength.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Progress
|
|
||||||
value={(passwordStrength.score / 5) * 100}
|
|
||||||
className="h-2"
|
|
||||||
/>
|
|
||||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
|
||||||
{Object.entries(passwordStrength.criteria).map(([key, met]) => (
|
|
||||||
<div key={key} className={`flex items-center gap-1 ${met ? 'text-success' : 'text-muted-foreground'}`}>
|
|
||||||
{met ? <Check className="w-3 h-3" /> : <X className="w-3 h-3" />}
|
|
||||||
<span>
|
|
||||||
{key === 'length' ? '8+ chars' :
|
|
||||||
key === 'lowercase' ? 'lowercase' :
|
|
||||||
key === 'uppercase' ? 'uppercase' :
|
|
||||||
key === 'number' ? 'number' : 'symbol'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isSignUp && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="confirmPassword" className="text-base">Confirm Password *</Label>
|
|
||||||
<div className="relative">
|
|
||||||
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
|
|
||||||
<Input
|
|
||||||
id="confirmPassword"
|
|
||||||
type={showConfirmPassword ? "text" : "password"}
|
|
||||||
value={signUpData.confirmPassword}
|
|
||||||
onChange={(e) => handleSignUpInputChange('confirmPassword', e.target.value)}
|
|
||||||
placeholder="Confirm your password"
|
|
||||||
className={`pl-10 pr-10 text-base min-h-[44px] ${errors.confirmPassword ? 'border-destructive' : ''}`}
|
|
||||||
autoComplete="new-password"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent min-h-[44px] min-w-[44px]"
|
|
||||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
|
||||||
>
|
|
||||||
{showConfirmPassword ? (
|
|
||||||
<EyeOff className="w-4 h-4 text-muted-foreground" />
|
|
||||||
) : (
|
|
||||||
<Eye className="w-4 h-4 text-muted-foreground" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{errors.confirmPassword && (
|
|
||||||
<p className="text-sm text-destructive flex items-center gap-1">
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
{errors.confirmPassword}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{requiresMFA && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="otp" className="text-base">Verification Code *</Label>
|
|
||||||
<div className="relative">
|
|
||||||
<Smartphone className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
|
|
||||||
<Input
|
|
||||||
id="otp"
|
|
||||||
type="text"
|
|
||||||
value={signInData.otp}
|
|
||||||
onChange={(e) => handleSignInInputChange('otp', e.target.value.replace(/\D/g, '').slice(0, 6))}
|
|
||||||
placeholder="000000"
|
|
||||||
className={`pl-10 text-base min-h-[44px] text-center tracking-wider ${errors.otp ? 'border-destructive' : ''}`}
|
|
||||||
autoComplete="one-time-code"
|
|
||||||
maxLength={6}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{errors.otp && (
|
|
||||||
<p className="text-sm text-destructive flex items-center gap-1">
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
{errors.otp}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Enter the 6-digit code sent to your registered device
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isSignUp && !requiresMFA && (
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Checkbox
|
|
||||||
id="rememberMe"
|
|
||||||
checked={signInData.rememberMe}
|
|
||||||
onCheckedChange={(checked) => handleSignInInputChange('rememberMe', checked)}
|
|
||||||
className="min-h-[20px] min-w-[20px]"
|
|
||||||
/>
|
|
||||||
<Label htmlFor="rememberMe" className="text-base cursor-pointer">
|
|
||||||
Keep me signed in
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
className="w-full text-base min-h-[44px] font-medium bg-secondary hover:bg-secondary/90 text-secondary-foreground"
|
|
||||||
size="lg"
|
|
||||||
>
|
|
||||||
{isSubmitting ? (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-4 h-4 border-2 border-secondary-foreground/30 border-t-secondary-foreground rounded-full animate-spin" />
|
|
||||||
<span>
|
|
||||||
{isSignUp ? 'Creating Account...' : requiresMFA ? 'Verifying...' : 'Signing In...'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span>
|
|
||||||
{isSignUp ? 'Create My Account' : requiresMFA ? 'Verify & Sign In' : 'Access My Learning'}
|
|
||||||
</span>
|
|
||||||
<ArrowRight className="w-4 h-4" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="text-center pt-4 border-t border-border/20">
|
|
||||||
<p className="text-base text-muted-foreground mb-3">
|
|
||||||
{isSignUp ? 'Already have an account?' : 'New to your organization\'s learning platform?'}
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={toggleFormMode}
|
|
||||||
className="text-base min-h-[44px] text-secondary hover:text-secondary/80 hover:bg-secondary/10"
|
|
||||||
>
|
|
||||||
{isSignUp ? 'Sign in to existing account' : 'Create new learner account'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div className="mt-8 p-4 bg-muted/50 rounded-lg border border-border/50">
|
|
||||||
<p className="text-sm font-medium text-foreground mb-2">Demo Mode - Try these credentials:</p>
|
|
||||||
<div className="space-y-1 text-sm text-muted-foreground">
|
|
||||||
<p><strong>Email:</strong> learner@company.com</p>
|
|
||||||
<p><strong>Password:</strong> learner123</p>
|
|
||||||
<p className="text-xs">For MFA demo, use: mfa@company.com (code: 123456)</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<Footer />
|
|
||||||
<AIChatbot />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { Button } from '../components/ui/button';
|
|
||||||
import { Input } from '../components/ui/input';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card';
|
|
||||||
import { ArrowLeft, Eye, EyeOff, Mail, Lock, Building2 } from 'lucide-react';
|
|
||||||
const navigate = useNavigate();
|
|
||||||
import { useAuth } from '../components/AuthContext';
|
|
||||||
|
|
||||||
export function CorporateLogin() {
|
|
||||||
const [email, setEmail] = useState('');
|
|
||||||
const [password, setPassword] = useState('');
|
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const { login } = useAuth();
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Simulate corporate login and go directly to corporate dashboard
|
|
||||||
await login(email || 'corporate@klc.edu', password || 'demo');
|
|
||||||
navigate('/dashboard?view=corporate');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Corporate login failed:', error);
|
|
||||||
// Even if login fails, still navigate to dashboard for ease of use
|
|
||||||
navigate('/dashboard?view=corporate');
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDirectLogin = () => {
|
|
||||||
// Direct navigation to corporate dashboard without form validation
|
|
||||||
setIsLoading(true);
|
|
||||||
login('corporate@klc.edu', 'demo').then(() => {
|
|
||||||
navigate('/dashboard?view=corporate');
|
|
||||||
}).catch(() => {
|
|
||||||
navigate('/dashboard?view=corporate');
|
|
||||||
}).finally(() => {
|
|
||||||
setIsLoading(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBackNavigation = () => {
|
|
||||||
navigate('/corporate/auth');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-white">
|
|
||||||
{/* Header Section */}
|
|
||||||
<div className="bg-primary text-primary-foreground pt-24 pb-12">
|
|
||||||
<div className="container mx-auto px-4 lg:px-8">
|
|
||||||
<div className="max-w-4xl mx-auto">
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={handleBackNavigation}
|
|
||||||
className="text-primary-foreground hover:bg-primary-foreground/10 min-h-[44px] min-w-[44px] mt-1"
|
|
||||||
aria-label="Go back to corporate auth"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="h-6 w-6" />
|
|
||||||
</Button>
|
|
||||||
<div className="flex-1">
|
|
||||||
<h1 className="text-[36px] mb-3 font-bold">Corporate Learning Portal</h1>
|
|
||||||
<p className="text-[18px] text-primary-foreground/90 leading-relaxed">
|
|
||||||
Sign in to access your organization's learning programs and team management tools.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Content */}
|
|
||||||
<div className="container mx-auto px-4 lg:px-8 -mt-8">
|
|
||||||
<div className="max-w-md mx-auto">
|
|
||||||
<Card className="bg-white shadow-xl border-0">
|
|
||||||
<CardHeader className="pb-6 text-center">
|
|
||||||
<div className="w-16 h-16 bg-purple-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
||||||
<Building2 className="h-8 w-8 text-purple-700" />
|
|
||||||
</div>
|
|
||||||
<CardTitle className="text-[24px] font-bold text-gray-900">
|
|
||||||
Welcome Back
|
|
||||||
</CardTitle>
|
|
||||||
<p className="text-[16px] text-gray-600">
|
|
||||||
Sign in to your corporate learning account
|
|
||||||
</p>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<CardContent>
|
|
||||||
{/* Quick Access Button */}
|
|
||||||
<div className="mb-6">
|
|
||||||
<Button
|
|
||||||
onClick={handleDirectLogin}
|
|
||||||
disabled={isLoading}
|
|
||||||
className="w-full text-[16px] min-h-[48px] bg-purple-600 hover:bg-purple-700 text-white"
|
|
||||||
>
|
|
||||||
{isLoading ? 'Accessing Dashboard...' : 'Go to Corporate Dashboard'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative mb-6">
|
|
||||||
<div className="absolute inset-0 flex items-center">
|
|
||||||
<span className="w-full border-t border-gray-300" />
|
|
||||||
</div>
|
|
||||||
<div className="relative flex justify-center text-[14px]">
|
|
||||||
<span className="bg-white px-2 text-gray-500">Or enter credentials</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label htmlFor="email" className="block text-[16px] font-medium text-gray-900 mb-2">
|
|
||||||
Corporate Email Address
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" />
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
className="pl-10 text-[16px] min-h-[44px]"
|
|
||||||
placeholder="Enter your corporate email (optional)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="password" className="block text-[16px] font-medium text-gray-900 mb-2">
|
|
||||||
Password
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" />
|
|
||||||
<Input
|
|
||||||
id="password"
|
|
||||||
type={showPassword ? 'text' : 'password'}
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
className="pl-10 pr-12 text-[16px] min-h-[44px]"
|
|
||||||
placeholder="Enter your password (optional)"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
|
||||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
|
||||||
>
|
|
||||||
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<input
|
|
||||||
id="remember-me"
|
|
||||||
name="remember-me"
|
|
||||||
type="checkbox"
|
|
||||||
className="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded"
|
|
||||||
/>
|
|
||||||
<label htmlFor="remember-me" className="ml-2 block text-[16px] text-gray-900">
|
|
||||||
Remember me
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => navigate('/forgot-password')}
|
|
||||||
className="text-[16px] text-primary hover:text-primary/80 font-medium"
|
|
||||||
>
|
|
||||||
Forgot password?
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
disabled={isLoading}
|
|
||||||
className="w-full text-[16px] min-h-[48px] bg-purple-600 hover:bg-purple-700"
|
|
||||||
>
|
|
||||||
{isLoading ? 'Signing In...' : 'Sign In with Credentials'}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div className="mt-6 text-center">
|
|
||||||
<p className="text-[16px] text-gray-600">
|
|
||||||
Need corporate access?{' '}
|
|
||||||
<button
|
|
||||||
onClick={() => navigate('/corporate/signup')}
|
|
||||||
className="text-primary hover:text-primary/80 font-medium"
|
|
||||||
>
|
|
||||||
Contact your administrator
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { Button } from '../components/ui/button';
|
|
||||||
import { Input } from '../components/ui/input';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card';
|
|
||||||
import { ArrowLeft, Mail, User, Building2, Phone, Users } from 'lucide-react';
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
export function CorporateSignup() {
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
firstName: '',
|
|
||||||
lastName: '',
|
|
||||||
email: '',
|
|
||||||
company: '',
|
|
||||||
jobTitle: '',
|
|
||||||
phone: '',
|
|
||||||
teamSize: '',
|
|
||||||
message: ''
|
|
||||||
});
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Simulate request submission
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
navigate('/email-verification');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Request failed:', error);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBackNavigation = () => {
|
|
||||||
navigate('/corporate/auth');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-white">
|
|
||||||
{/* Header Section */}
|
|
||||||
<div className="bg-primary text-primary-foreground pt-24 pb-12">
|
|
||||||
<div className="container mx-auto px-4 lg:px-8">
|
|
||||||
<div className="max-w-4xl mx-auto">
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={handleBackNavigation}
|
|
||||||
className="text-primary-foreground hover:bg-primary-foreground/10 min-h-[44px] min-w-[44px] mt-1"
|
|
||||||
aria-label="Go back to corporate auth"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="h-6 w-6" />
|
|
||||||
</Button>
|
|
||||||
<div className="flex-1">
|
|
||||||
<h1 className="text-[36px] mb-3 font-bold">Request Corporate Access</h1>
|
|
||||||
<p className="text-[18px] text-primary-foreground/90 leading-relaxed">
|
|
||||||
Get your organization started with KLC's enterprise learning solutions.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Content */}
|
|
||||||
<div className="container mx-auto px-4 lg:px-8 -mt-8">
|
|
||||||
<div className="max-w-2xl mx-auto">
|
|
||||||
<Card className="bg-white shadow-xl border-0">
|
|
||||||
<CardHeader className="pb-6 text-center">
|
|
||||||
<div className="w-16 h-16 bg-purple-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
||||||
<Building2 className="h-8 w-8 text-purple-700" />
|
|
||||||
</div>
|
|
||||||
<CardTitle className="text-[24px] font-bold text-gray-900">
|
|
||||||
Enterprise Learning Request
|
|
||||||
</CardTitle>
|
|
||||||
<p className="text-[16px] text-gray-600">
|
|
||||||
Fill out this form and our enterprise team will contact you within 24 hours
|
|
||||||
</p>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<CardContent>
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div>
|
|
||||||
<label htmlFor="firstName" className="block text-[16px] font-medium text-gray-900 mb-2">
|
|
||||||
First Name
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<User className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" />
|
|
||||||
<Input
|
|
||||||
id="firstName"
|
|
||||||
type="text"
|
|
||||||
value={formData.firstName}
|
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, firstName: e.target.value }))}
|
|
||||||
className="pl-10 text-[16px] min-h-[44px]"
|
|
||||||
placeholder="First name"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="lastName" className="block text-[16px] font-medium text-gray-900 mb-2">
|
|
||||||
Last Name
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id="lastName"
|
|
||||||
type="text"
|
|
||||||
value={formData.lastName}
|
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, lastName: e.target.value }))}
|
|
||||||
className="text-[16px] min-h-[44px]"
|
|
||||||
placeholder="Last name"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="email" className="block text-[16px] font-medium text-gray-900 mb-2">
|
|
||||||
Work Email Address
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" />
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
value={formData.email}
|
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
|
|
||||||
className="pl-10 text-[16px] min-h-[44px]"
|
|
||||||
placeholder="Enter your work email"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div>
|
|
||||||
<label htmlFor="company" className="block text-[16px] font-medium text-gray-900 mb-2">
|
|
||||||
Company Name
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<Building2 className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" />
|
|
||||||
<Input
|
|
||||||
id="company"
|
|
||||||
type="text"
|
|
||||||
value={formData.company}
|
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, company: e.target.value }))}
|
|
||||||
className="pl-10 text-[16px] min-h-[44px]"
|
|
||||||
placeholder="Your company"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="jobTitle" className="block text-[16px] font-medium text-gray-900 mb-2">
|
|
||||||
Job Title
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id="jobTitle"
|
|
||||||
type="text"
|
|
||||||
value={formData.jobTitle}
|
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, jobTitle: e.target.value }))}
|
|
||||||
className="text-[16px] min-h-[44px]"
|
|
||||||
placeholder="Your role"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div>
|
|
||||||
<label htmlFor="phone" className="block text-[16px] font-medium text-gray-900 mb-2">
|
|
||||||
Phone Number
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<Phone className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" />
|
|
||||||
<Input
|
|
||||||
id="phone"
|
|
||||||
type="tel"
|
|
||||||
value={formData.phone}
|
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, phone: e.target.value }))}
|
|
||||||
className="pl-10 text-[16px] min-h-[44px]"
|
|
||||||
placeholder="Your phone number"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="teamSize" className="block text-[16px] font-medium text-gray-900 mb-2">
|
|
||||||
Team Size
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<Users className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" />
|
|
||||||
<select
|
|
||||||
id="teamSize"
|
|
||||||
value={formData.teamSize}
|
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, teamSize: e.target.value }))}
|
|
||||||
className="w-full pl-10 pr-4 py-3 text-[16px] border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<option value="">Select team size</option>
|
|
||||||
<option value="1-10">1-10 employees</option>
|
|
||||||
<option value="11-50">11-50 employees</option>
|
|
||||||
<option value="51-200">51-200 employees</option>
|
|
||||||
<option value="201-1000">201-1000 employees</option>
|
|
||||||
<option value="1000+">1000+ employees</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="message" className="block text-[16px] font-medium text-gray-900 mb-2">
|
|
||||||
Tell us about your learning needs
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
id="message"
|
|
||||||
value={formData.message}
|
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, message: e.target.value }))}
|
|
||||||
rows={4}
|
|
||||||
className="w-full px-4 py-3 text-[16px] border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent resize-none"
|
|
||||||
placeholder="Describe your organization's leadership development goals and any specific requirements..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
disabled={isLoading}
|
|
||||||
className="w-full text-[16px] min-h-[48px] bg-purple-600 hover:bg-purple-700"
|
|
||||||
>
|
|
||||||
{isLoading ? 'Submitting Request...' : 'Request Enterprise Access'}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div className="mt-6 bg-purple-50 rounded-lg p-4">
|
|
||||||
<p className="text-[14px] text-purple-700">
|
|
||||||
<strong>What happens next?</strong> Our enterprise team will review your request and contact you within 24 hours to discuss your organization's learning needs and provide a customized solution.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,686 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { LearnerLayout } from '../components/learner/LearnerLayout';
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card';
|
|
||||||
import { Button } from '../components/ui/button';
|
|
||||||
import { Input } from '../components/ui/input';
|
|
||||||
import { Badge } from '../components/ui/badge';
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '../components/ui/avatar';
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/ui/tabs';
|
|
||||||
import { Progress } from '../components/ui/progress';
|
|
||||||
import { Alert, AlertDescription } from '../components/ui/alert';
|
|
||||||
import { Separator } from '../components/ui/separator';
|
|
||||||
import { ScrollArea } from '../components/ui/scroll-area';
|
|
||||||
import {
|
|
||||||
ArrowLeft,
|
|
||||||
Calendar,
|
|
||||||
Clock,
|
|
||||||
Users,
|
|
||||||
MapPin,
|
|
||||||
Play,
|
|
||||||
Send,
|
|
||||||
MessageSquare,
|
|
||||||
UserCheck,
|
|
||||||
Eye,
|
|
||||||
Download,
|
|
||||||
AlertTriangle,
|
|
||||||
Star,
|
|
||||||
BookOpen,
|
|
||||||
Video,
|
|
||||||
Mic,
|
|
||||||
MicOff,
|
|
||||||
Camera,
|
|
||||||
CameraOff,
|
|
||||||
Settings,
|
|
||||||
Share2,
|
|
||||||
Heart,
|
|
||||||
Bookmark,
|
|
||||||
ChevronRight,
|
|
||||||
CheckCircle,
|
|
||||||
XCircle,
|
|
||||||
Target,
|
|
||||||
Award,
|
|
||||||
TrendingUp,
|
|
||||||
AlertCircle,
|
|
||||||
Info,
|
|
||||||
Tag,
|
|
||||||
ArrowRight,
|
|
||||||
MoreHorizontal,
|
|
||||||
Search
|
|
||||||
} from 'lucide-react';
|
|
||||||
// const navigate = useNavigate();
|
|
||||||
import { ImageWithFallback } from '../components/figma/ImageWithFallback';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
const navigate = useNavigate();
|
|
||||||
// Mock data for webinars
|
|
||||||
const mockWebinars = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: "Strategic Leadership in Digital Transformation",
|
|
||||||
presenter: "Dr. Sarah Mitchell",
|
|
||||||
presenterImage: "https://images.unsplash.com/photo-1494790108755-2616b612b786?w=150&h=150&fit=crop&crop=face",
|
|
||||||
date: "2024-02-15",
|
|
||||||
time: "10:00 AM EST",
|
|
||||||
duration: 90,
|
|
||||||
description: "Learn how to lead your organization through digital transformation initiatives while maintaining team cohesion and strategic focus. This comprehensive session covers change management strategies and technology adoption frameworks.",
|
|
||||||
category: "Leadership",
|
|
||||||
level: "Advanced",
|
|
||||||
registeredCount: 142,
|
|
||||||
capacity: 200,
|
|
||||||
status: "upcoming",
|
|
||||||
attendanceRequired: true,
|
|
||||||
allocatedSeat: "VIP-001",
|
|
||||||
tags: ["Digital Transformation", "Strategy", "Change Management"],
|
|
||||||
hrActions: [
|
|
||||||
{ title: "Complete pre-webinar assessment", deadline: "Feb 13" },
|
|
||||||
{ title: "Submit team feedback form", deadline: "Feb 17" }
|
|
||||||
],
|
|
||||||
thumbnail: "https://images.unsplash.com/photo-1551434678-e076c223a692?w=400&h=250&fit=crop"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: "Building Resilient Teams in Remote Work",
|
|
||||||
presenter: "Michael Rodriguez",
|
|
||||||
presenterImage: "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face",
|
|
||||||
date: "2024-02-20",
|
|
||||||
time: "2:00 PM EST",
|
|
||||||
duration: 60,
|
|
||||||
description: "Explore practical strategies for maintaining team resilience, productivity, and engagement in distributed work environments. Learn to foster collaboration and maintain team culture remotely.",
|
|
||||||
category: "Team Management",
|
|
||||||
level: "Intermediate",
|
|
||||||
registeredCount: 89,
|
|
||||||
capacity: 150,
|
|
||||||
status: "upcoming",
|
|
||||||
attendanceRequired: false,
|
|
||||||
allocatedSeat: "STD-045",
|
|
||||||
tags: ["Remote Work", "Team Building", "Productivity"],
|
|
||||||
hrActions: [],
|
|
||||||
thumbnail: "https://images.unsplash.com/photo-1552664730-d307ca884978?w=400&h=250&fit=crop"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: "Innovation Leadership: Fostering Creative Culture",
|
|
||||||
presenter: "Dr. Emily Chen",
|
|
||||||
presenterImage: "https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=150&h=150&fit=crop&crop=face",
|
|
||||||
date: "2024-02-22",
|
|
||||||
time: "11:00 AM EST",
|
|
||||||
duration: 75,
|
|
||||||
description: "Discover how to create an organizational culture that encourages innovation, creativity, and continuous improvement. Learn practical techniques for driving innovation at every level.",
|
|
||||||
category: "Innovation",
|
|
||||||
level: "Intermediate",
|
|
||||||
registeredCount: 76,
|
|
||||||
capacity: 120,
|
|
||||||
status: "upcoming",
|
|
||||||
attendanceRequired: true,
|
|
||||||
allocatedSeat: "VIP-012",
|
|
||||||
tags: ["Innovation", "Culture", "Creativity"],
|
|
||||||
hrActions: [
|
|
||||||
{ title: "Innovation readiness survey", deadline: "Feb 20" }
|
|
||||||
],
|
|
||||||
thumbnail: "https://images.unsplash.com/photo-1560472354-b33ff0c44a43?w=400&h=250&fit=crop"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
title: "Securing Infrastructure-as-Code from Tampering & Misconfigurations",
|
|
||||||
presenter: "David Kim",
|
|
||||||
presenterImage: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face",
|
|
||||||
date: "2024-02-25",
|
|
||||||
time: "3:00 PM EST",
|
|
||||||
duration: 90,
|
|
||||||
description: "Moving applications and development to the cloud has delivered both operational benefits at scale. Faster release cycles and microservices architecture have improved developer productivity and application resilience.",
|
|
||||||
category: "Technology",
|
|
||||||
level: "Advanced",
|
|
||||||
registeredCount: 94,
|
|
||||||
capacity: 180,
|
|
||||||
status: "upcoming",
|
|
||||||
attendanceRequired: false,
|
|
||||||
allocatedSeat: "STD-067",
|
|
||||||
tags: ["Security", "Infrastructure", "DevOps"],
|
|
||||||
hrActions: [],
|
|
||||||
thumbnail: "https://images.unsplash.com/photo-1558494949-ef010cbdcc31?w=400&h=250&fit=crop"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
title: "Executive Decision Making Under Pressure",
|
|
||||||
presenter: "Jennifer Wilson",
|
|
||||||
presenterImage: "https://images.unsplash.com/photo-1580489944761-15a19d654956?w=150&h=150&fit=crop&crop=face",
|
|
||||||
date: "2024-02-28",
|
|
||||||
time: "1:00 PM EST",
|
|
||||||
duration: 120,
|
|
||||||
description: "Learn proven frameworks for making critical decisions in high-pressure situations. Develop skills in risk assessment, stakeholder analysis, and crisis management to lead with confidence.",
|
|
||||||
category: "Leadership",
|
|
||||||
level: "Expert",
|
|
||||||
registeredCount: 156,
|
|
||||||
capacity: 200,
|
|
||||||
status: "upcoming",
|
|
||||||
attendanceRequired: true,
|
|
||||||
allocatedSeat: "VIP-008",
|
|
||||||
tags: ["Decision Making", "Crisis Management", "Risk Assessment"],
|
|
||||||
hrActions: [
|
|
||||||
{ title: "Pre-session crisis simulation", deadline: "Feb 26" }
|
|
||||||
],
|
|
||||||
thumbnail: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=250&fit=crop"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
title: "Sustainable Leadership Practices",
|
|
||||||
presenter: "Dr. Maria Santos",
|
|
||||||
presenterImage: "https://images.unsplash.com/photo-1559548331-f72fd4d48f9b?w=150&h=150&fit=crop&crop=face",
|
|
||||||
date: "2024-03-05",
|
|
||||||
time: "10:30 AM EST",
|
|
||||||
duration: 105,
|
|
||||||
description: "Learn how to integrate sustainability into your leadership approach. Explore environmental, social, and governance (ESG) principles for modern leaders and create lasting positive impact.",
|
|
||||||
category: "Sustainability",
|
|
||||||
level: "Intermediate",
|
|
||||||
registeredCount: 112,
|
|
||||||
capacity: 160,
|
|
||||||
status: "upcoming",
|
|
||||||
attendanceRequired: false,
|
|
||||||
allocatedSeat: "STD-089",
|
|
||||||
tags: ["ESG", "Sustainability", "Social Responsibility"],
|
|
||||||
hrActions: [],
|
|
||||||
thumbnail: "https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=400&h=250&fit=crop"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// Enhanced badge components with KLC brand colors
|
|
||||||
function RequiredBadge() {
|
|
||||||
return (
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="text-sm font-medium px-3 py-1.5 h-auto flex items-center bg-destructive/5 text-destructive border-destructive/30 hover:bg-destructive/10 transition-colors"
|
|
||||||
>
|
|
||||||
<AlertCircle className="h-3.5 w-3.5 mr-1.5" />
|
|
||||||
Required
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CategoryBadge({ children }: { children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="text-sm font-medium px-2 py-1 bg-primary/5 text-primary border-primary/30"
|
|
||||||
>
|
|
||||||
<Info className="h-3 w-3 mr-1" />
|
|
||||||
{children}
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TagBadge({ children }: { children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="text-sm px-2 py-1 bg-muted/50 text-muted-foreground border-border"
|
|
||||||
>
|
|
||||||
<Tag className="h-3 w-3 mr-1" />
|
|
||||||
{children}
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function LevelBadge({ level }: { level: string }) {
|
|
||||||
const getLevelStyle = (level: string) => {
|
|
||||||
switch (level.toLowerCase()) {
|
|
||||||
case 'beginner':
|
|
||||||
return 'bg-success/10 text-success border-success/30';
|
|
||||||
case 'intermediate':
|
|
||||||
return 'bg-[#F8C301]/10 text-[#26231A] border-[#F8C301]/30';
|
|
||||||
case 'advanced':
|
|
||||||
return 'bg-primary/10 text-primary border-primary/30';
|
|
||||||
case 'expert':
|
|
||||||
return 'bg-[#26231A]/10 text-[#26231A] border-[#26231A]/30';
|
|
||||||
default:
|
|
||||||
return 'bg-muted/50 text-muted-foreground border-border';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className={`text-sm font-medium px-2 py-1 ${getLevelStyle(level)}`}
|
|
||||||
>
|
|
||||||
{level} Level
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to truncate text at word boundaries
|
|
||||||
function truncateText(text: string, maxLength: number): { truncated: string; needsTruncation: boolean } {
|
|
||||||
if (text.length <= maxLength) {
|
|
||||||
return { truncated: text, needsTruncation: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the last space before maxLength to avoid cutting words
|
|
||||||
let truncatedText = text.substring(0, maxLength);
|
|
||||||
const lastSpaceIndex = truncatedText.lastIndexOf(' ');
|
|
||||||
|
|
||||||
if (lastSpaceIndex > maxLength * 0.8) { // Only use word boundary if it's not too far back
|
|
||||||
truncatedText = text.substring(0, lastSpaceIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { truncated: truncatedText, needsTruncation: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enhanced WebinarCard component with KLC branding and consistent design
|
|
||||||
function WebinarCard({ webinar }: { webinar: typeof mockWebinars[0] }) {
|
|
||||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false);
|
|
||||||
|
|
||||||
const handleJoinWebinar = () => {
|
|
||||||
navigate(`/webinar-detail?id=${webinar.id}&view=corporate`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleDescription = (e: React.MouseEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsDescriptionExpanded(!isDescriptionExpanded);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Truncate description for collapsed state
|
|
||||||
const maxDescriptionLength = 140;
|
|
||||||
const { truncated: truncatedDescription, needsTruncation } = truncateText(webinar.description, maxDescriptionLength);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="group hover:shadow-lg transition-all duration-200 cursor-pointer border-border hover:border-primary/30 bg-card h-full flex flex-col overflow-hidden">
|
|
||||||
{/* Course Image with Brand Gradient Overlay */}
|
|
||||||
<div className="relative h-48 overflow-hidden">
|
|
||||||
<ImageWithFallback
|
|
||||||
src={webinar.thumbnail}
|
|
||||||
alt={webinar.title}
|
|
||||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-200"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Brand Gradient Overlay */}
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-[#04045B]/80 via-[#04045B]/60 to-transparent" />
|
|
||||||
|
|
||||||
{/* Geometric Brand Elements */}
|
|
||||||
<div className="absolute inset-0">
|
|
||||||
<div className="absolute top-0 right-0 w-24 h-24 bg-[#F8C301]/20 transform rotate-45 translate-x-8 -translate-y-8" />
|
|
||||||
<div className="absolute bottom-0 left-0 w-32 h-32 bg-[#26231A]/20 transform -rotate-12 -translate-x-12 translate-y-12" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Play Button with KLC Brand Colors */}
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
|
||||||
<button
|
|
||||||
onClick={handleJoinWebinar}
|
|
||||||
className="w-16 h-16 bg-white rounded-full flex items-center justify-center shadow-lg hover:scale-105 transition-transform duration-200 group border-2 border-primary/20"
|
|
||||||
aria-label="Watch webinar"
|
|
||||||
>
|
|
||||||
<Play className="h-6 w-6 text-primary ml-1" fill="currentColor" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Status Indicators */}
|
|
||||||
<div className="absolute top-3 left-3 flex gap-2">
|
|
||||||
{webinar.attendanceRequired && <RequiredBadge />}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Category Badge */}
|
|
||||||
<div className="absolute top-3 right-3">
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="bg-white/90 backdrop-blur-sm border-white/30 text-[#26231A] text-sm font-medium"
|
|
||||||
>
|
|
||||||
{webinar.category}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content Section */}
|
|
||||||
<div className="flex-1 flex flex-col px-6 pb-6">
|
|
||||||
{/* Webinar Type Badge */}
|
|
||||||
<div className="flex items-center gap-2 mb-4">
|
|
||||||
<div className="w-6 h-6 bg-primary/10 rounded-full flex items-center justify-center">
|
|
||||||
<Video className="h-3 w-3 text-primary" />
|
|
||||||
</div>
|
|
||||||
<span className="text-muted-foreground text-sm font-medium uppercase tracking-wide">WEBINAR</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Title */}
|
|
||||||
<div className="mb-3">
|
|
||||||
<h4 className="text-lg font-semibold text-foreground group-hover:text-primary transition-colors line-clamp-2 leading-tight">
|
|
||||||
{webinar.title}
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Description with Proper Spacing */}
|
|
||||||
<div className="mb-4">
|
|
||||||
<p className="text-base text-muted-foreground leading-relaxed">
|
|
||||||
{isDescriptionExpanded ? (
|
|
||||||
<>
|
|
||||||
{webinar.description}
|
|
||||||
{' '}
|
|
||||||
<button
|
|
||||||
onClick={toggleDescription}
|
|
||||||
className="text-primary hover:text-primary/80 font-medium transition-colors cursor-pointer underline decoration-dotted underline-offset-2"
|
|
||||||
aria-label="Show less description"
|
|
||||||
>
|
|
||||||
Show less
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{truncatedDescription}
|
|
||||||
{needsTruncation && (
|
|
||||||
<>
|
|
||||||
{' '}
|
|
||||||
<button
|
|
||||||
onClick={toggleDescription}
|
|
||||||
className="text-primary hover:text-primary/80 font-medium transition-colors cursor-pointer"
|
|
||||||
aria-label="Show more description"
|
|
||||||
>
|
|
||||||
...more
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Presenter Info */}
|
|
||||||
<div className="flex items-center gap-3 mb-4">
|
|
||||||
<Avatar className="w-8 h-8">
|
|
||||||
<AvatarImage src={webinar.presenterImage} alt={webinar.presenter} />
|
|
||||||
<AvatarFallback className="text-sm bg-primary/10 text-primary">
|
|
||||||
{webinar.presenter.split(' ').map(n => n[0]).join('')}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div>
|
|
||||||
<p className="text-base font-medium text-foreground">{webinar.presenter}</p>
|
|
||||||
<p className="text-sm text-muted-foreground">Expert Presenter</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Metadata - Improved Spacing and Typography */}
|
|
||||||
<div className="space-y-3 mb-4">
|
|
||||||
<div className="flex items-center justify-between pt-3 border-t border-border">
|
|
||||||
<div className="flex items-center gap-2 text-base text-muted-foreground">
|
|
||||||
<Calendar className="h-4 w-4" />
|
|
||||||
<span>{new Date(webinar.date).toLocaleDateString()}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-base text-muted-foreground">
|
|
||||||
<Clock className="h-4 w-4" />
|
|
||||||
<span>{webinar.duration}min</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2 text-base text-muted-foreground">
|
|
||||||
<Users className="h-4 w-4" />
|
|
||||||
<span>{webinar.registeredCount}/{webinar.capacity}</span>
|
|
||||||
</div>
|
|
||||||
<LevelBadge level={webinar.level} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* HR Actions */}
|
|
||||||
{webinar.hrActions.length > 0 && (
|
|
||||||
<div className="mb-4 p-3 bg-destructive/5 border border-destructive/20 rounded-lg">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<AlertTriangle className="h-4 w-4 text-destructive" />
|
|
||||||
<p className="text-base font-medium text-destructive">Action Required:</p>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
{webinar.hrActions.map((action, index) => (
|
|
||||||
<p key={index} className="text-sm text-muted-foreground">
|
|
||||||
{action.title} (Due: {action.deadline})
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* CTA Button - Auto-positioned at bottom */}
|
|
||||||
<div className="mt-auto">
|
|
||||||
<Button
|
|
||||||
onClick={handleJoinWebinar}
|
|
||||||
className="w-full text-base font-medium h-11 group"
|
|
||||||
>
|
|
||||||
Join Webinar
|
|
||||||
<ArrowRight className="h-4 w-4 ml-2 group-hover:translate-x-1 transition-transform duration-200" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CorporateWebinars() {
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
|
||||||
const [selectedCategory, setSelectedCategory] = useState('all');
|
|
||||||
const [selectedStatus, setSelectedStatus] = useState('all');
|
|
||||||
|
|
||||||
// Mock corporate user data matching dashboard pattern
|
|
||||||
const user = {
|
|
||||||
name: "Sarah Johnson",
|
|
||||||
email: "sarah.johnson@company.com",
|
|
||||||
avatar: "https://images.unsplash.com/photo-1494790108755-2616b612b786?w=150&h=150&fit=crop&crop=face",
|
|
||||||
organization: "TCS",
|
|
||||||
orgLogo: "https://images.unsplash.com/photo-1560179707-f14e90ef3623?w=32&h=32&fit=crop",
|
|
||||||
role: "Senior Manager",
|
|
||||||
cohort: "Leadership 2024"
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filter webinars based on search and filters
|
|
||||||
const filteredWebinars = mockWebinars.filter(webinar => {
|
|
||||||
const matchesSearch = webinar.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
||||||
webinar.presenter.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
||||||
webinar.description.toLowerCase().includes(searchQuery.toLowerCase());
|
|
||||||
|
|
||||||
const matchesCategory = selectedCategory === 'all' || webinar.category === selectedCategory;
|
|
||||||
const matchesStatus = selectedStatus === 'all' || webinar.status === selectedStatus;
|
|
||||||
|
|
||||||
return matchesSearch && matchesCategory && matchesStatus;
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LearnerLayout currentPage="/webinars" userType="corporate" user={user}>
|
|
||||||
<div className="p-6 space-y-8">
|
|
||||||
{/* KPI Cards - Matching Dashboard Design */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
||||||
<Card className="hover:shadow-lg transition-shadow duration-200">
|
|
||||||
<CardContent className="p-6 text-center">
|
|
||||||
<div className="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-3">
|
|
||||||
<Video className="h-6 w-6 text-primary" />
|
|
||||||
</div>
|
|
||||||
<p className="text-2xl font-bold text-foreground mb-1">12</p>
|
|
||||||
<p className="text-base text-muted-foreground">Upcoming Sessions</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="hover:shadow-lg transition-shadow duration-200">
|
|
||||||
<CardContent className="p-6 text-center">
|
|
||||||
<div className="w-12 h-12 bg-success/10 rounded-full flex items-center justify-center mx-auto mb-3">
|
|
||||||
<CheckCircle className="h-6 w-6 text-success" />
|
|
||||||
</div>
|
|
||||||
<p className="text-2xl font-bold text-foreground mb-1">8</p>
|
|
||||||
<p className="text-base text-muted-foreground">Completed</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="hover:shadow-lg transition-shadow duration-200">
|
|
||||||
<CardContent className="p-6 text-center">
|
|
||||||
<div className="w-12 h-12 bg-[#F8C301]/10 rounded-full flex items-center justify-center mx-auto mb-3">
|
|
||||||
<Users className="h-6 w-6 text-[#26231A]" />
|
|
||||||
</div>
|
|
||||||
<p className="text-2xl font-bold text-foreground mb-1">347</p>
|
|
||||||
<p className="text-base text-muted-foreground">Total Participants</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="hover:shadow-lg transition-shadow duration-200">
|
|
||||||
<CardContent className="p-6 text-center">
|
|
||||||
<div className="w-12 h-12 bg-[#26231A]/10 rounded-full flex items-center justify-center mx-auto mb-3">
|
|
||||||
<TrendingUp className="h-6 w-6 text-[#26231A]" />
|
|
||||||
</div>
|
|
||||||
<p className="text-2xl font-bold text-foreground mb-1">94%</p>
|
|
||||||
<p className="text-base text-muted-foreground">Attendance Rate</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Search and Filters */}
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-6">
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* Search Bar */}
|
|
||||||
<div className="relative">
|
|
||||||
<Input
|
|
||||||
placeholder="Search webinars by title, presenter, or description..."
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
className="pl-10 text-base min-h-[44px]"
|
|
||||||
/>
|
|
||||||
<div className="absolute left-3 top-1/2 transform -translate-y-1/2">
|
|
||||||
<Search className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filters */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
||||||
<select
|
|
||||||
value={selectedCategory}
|
|
||||||
onChange={(e) => setSelectedCategory(e.target.value)}
|
|
||||||
className="px-3 py-2 border border-border rounded-md text-base min-h-[44px] bg-background"
|
|
||||||
>
|
|
||||||
<option value="all">All Categories</option>
|
|
||||||
<option value="Leadership">Leadership</option>
|
|
||||||
<option value="Team Management">Team Management</option>
|
|
||||||
<option value="Innovation">Innovation</option>
|
|
||||||
<option value="Technology">Technology</option>
|
|
||||||
<option value="Sustainability">Sustainability</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select
|
|
||||||
value={selectedStatus}
|
|
||||||
onChange={(e) => setSelectedStatus(e.target.value)}
|
|
||||||
className="px-3 py-2 border border-border rounded-md text-base min-h-[44px] bg-background"
|
|
||||||
>
|
|
||||||
<option value="all">All Status</option>
|
|
||||||
<option value="upcoming">Upcoming</option>
|
|
||||||
<option value="live">Live Now</option>
|
|
||||||
<option value="completed">Completed</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="text-base min-h-[44px]"
|
|
||||||
onClick={() => {
|
|
||||||
setSearchQuery('');
|
|
||||||
setSelectedCategory('all');
|
|
||||||
setSelectedStatus('all');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Clear Filters
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Webinar Grid */}
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h2 className="text-2xl font-semibold text-foreground">Corporate Webinars</h2>
|
|
||||||
<Badge variant="outline" className="text-base">
|
|
||||||
{filteredWebinars.length} {filteredWebinars.length === 1 ? 'webinar' : 'webinars'}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{filteredWebinars.length > 0 ? (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
||||||
{filteredWebinars.map((webinar) => (
|
|
||||||
<WebinarCard key={webinar.id} webinar={webinar} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-12 text-center">
|
|
||||||
<div className="w-16 h-16 bg-muted/50 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
||||||
<Video className="h-8 w-8 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-xl font-semibold mb-2 text-foreground">No webinars found</h3>
|
|
||||||
<p className="text-base text-muted-foreground mb-6">
|
|
||||||
Try adjusting your search terms or filters to find webinars.
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
setSearchQuery('');
|
|
||||||
setSelectedCategory('all');
|
|
||||||
setSelectedStatus('all');
|
|
||||||
}}
|
|
||||||
className="text-base min-h-[44px]"
|
|
||||||
>
|
|
||||||
Clear All Filters
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* This Week's Schedule */}
|
|
||||||
<Card className="h-full flex flex-col">
|
|
||||||
<CardHeader className="pb-4">
|
|
||||||
<CardTitle className="text-lg font-semibold flex items-center gap-2">
|
|
||||||
<Calendar className="h-5 w-5 text-primary" />
|
|
||||||
This Week's Schedule
|
|
||||||
</CardTitle>
|
|
||||||
<p className="text-base text-muted-foreground">
|
|
||||||
Quick overview of your upcoming webinar commitments
|
|
||||||
</p>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<CardContent className="flex-1">
|
|
||||||
<div className="space-y-4">
|
|
||||||
{mockWebinars.slice(0, 3).map((webinar) => (
|
|
||||||
<div
|
|
||||||
key={webinar.id}
|
|
||||||
className="group cursor-pointer p-4 rounded-lg border border-border hover:border-primary/30 hover:bg-muted/30 transition-all duration-200"
|
|
||||||
onClick={() => navigate(`/webinar-detail?id=${webinar.id}&view=corporate`)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-3 flex-1">
|
|
||||||
<div className="w-8 h-8 bg-primary/10 rounded-full flex items-center justify-center flex-shrink-0">
|
|
||||||
<Video className="h-4 w-4 text-primary" />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-center gap-2 mb-1">
|
|
||||||
<p className="text-base font-medium text-foreground group-hover:text-primary transition-colors truncate">
|
|
||||||
{webinar.title}
|
|
||||||
</p>
|
|
||||||
{webinar.attendanceRequired && (
|
|
||||||
<Badge variant="destructive" className="text-sm flex-shrink-0">
|
|
||||||
<AlertCircle className="h-3 w-3 mr-1" />
|
|
||||||
Required
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{new Date(webinar.date).toLocaleDateString()} at {webinar.time} • {webinar.duration}min
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ArrowRight className="h-4 w-4 text-muted-foreground group-hover:text-primary group-hover:translate-x-1 transition-all flex-shrink-0 ml-2" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => navigate('/webinars?view=corporate')}
|
|
||||||
className="w-full text-base h-11"
|
|
||||||
>
|
|
||||||
View All Webinars
|
|
||||||
<Calendar className="h-4 w-4 ml-2" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</LearnerLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Button } from '../components/ui/button';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card';
|
|
||||||
import { Mail, CheckCircle, RefreshCw } from 'lucide-react';
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
export function EmailVerification() {
|
|
||||||
const handleResendEmail = () => {
|
|
||||||
// Simulate resending verification email
|
|
||||||
console.log('Resending verification email...');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-white flex items-center justify-center px-4">
|
|
||||||
<Card className="w-full max-w-md bg-white shadow-xl border-0">
|
|
||||||
<CardHeader className="pb-6 text-center">
|
|
||||||
<div className="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
||||||
<Mail className="h-8 w-8 text-blue-600" />
|
|
||||||
</div>
|
|
||||||
<CardTitle className="text-[24px] font-bold text-gray-900">
|
|
||||||
Verify Your Email
|
|
||||||
</CardTitle>
|
|
||||||
<p className="text-[16px] text-gray-600">
|
|
||||||
We've sent a verification link to your email address
|
|
||||||
</p>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<CardContent className="space-y-6">
|
|
||||||
<div className="bg-blue-50 rounded-lg p-4">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<CheckCircle className="h-5 w-5 text-blue-600 flex-shrink-0 mt-0.5" />
|
|
||||||
<div>
|
|
||||||
<h3 className="text-[16px] font-semibold text-blue-900 mb-1">
|
|
||||||
Check Your Inbox
|
|
||||||
</h3>
|
|
||||||
<p className="text-[14px] text-blue-700">
|
|
||||||
Click the verification link in your email to activate your account. The link will expire in 24 hours.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<p className="text-[16px] text-gray-600 text-center">
|
|
||||||
Don't see the email? Check your spam folder or request a new one.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={handleResendEmail}
|
|
||||||
variant="outline"
|
|
||||||
className="w-full text-[16px] min-h-[44px] border-blue-600 text-blue-600 hover:bg-blue-50"
|
|
||||||
>
|
|
||||||
<RefreshCw className="h-4 w-4 mr-2" />
|
|
||||||
Resend Verification Email
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={() => navigate('/auth')}
|
|
||||||
className="w-full text-[16px] min-h-[44px] bg-primary hover:bg-primary/90"
|
|
||||||
>
|
|
||||||
Back to Sign In
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-[14px] text-gray-500">
|
|
||||||
Need help? Contact our{' '}
|
|
||||||
<button
|
|
||||||
onClick={() => navigate('/contact')}
|
|
||||||
className="text-primary hover:text-primary/80 font-medium"
|
|
||||||
>
|
|
||||||
support team
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import React, { useEffect } from 'react';
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
export function ExecutiveLeadership() {
|
|
||||||
useEffect(() => {
|
|
||||||
// This is a deprecated page, redirect to home
|
|
||||||
navigate('/');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Button } from '../components/ui/button';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card';
|
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../components/ui/collapsible';
|
|
||||||
import { ArrowLeft, ChevronDown, HelpCircle, Mail, Phone } from 'lucide-react';
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
export function FAQ() {
|
|
||||||
const handleBackNavigation = () => {
|
|
||||||
navigate('/');
|
|
||||||
};
|
|
||||||
|
|
||||||
const faqData = [
|
|
||||||
{
|
|
||||||
question: "How do I access my learning account?",
|
|
||||||
answer: "Click 'Sign In' on the homepage and choose between Individual Learning or Corporate Learning. If you don't have an account, you can create one during the sign-in process."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: "What's the difference between Individual and Corporate learning?",
|
|
||||||
answer: "Individual Learning is for personal professional development with self-paced courses and individual progress tracking. Corporate Learning includes team collaboration, organizational analytics, and enterprise-specific content."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: "How do I reset my password?",
|
|
||||||
answer: "On the sign-in page, click 'Forgot Password' and enter your email address. We'll send you instructions to reset your password within a few minutes."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: "Can I switch between Individual and Corporate accounts?",
|
|
||||||
answer: "Yes, once logged in, you can switch between account types using the profile menu in the top-right corner. You'll need to sign in separately for each account type."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: "How do I join a live webinar?",
|
|
||||||
answer: "Navigate to the Webinars section in your learning portal. You'll see upcoming live sessions with join links that become active 15 minutes before the session starts."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: "Where can I find my course certificates?",
|
|
||||||
answer: "Completed course certificates are available in your Dashboard under the 'Achievements' section, or you can access them through your Account Settings."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: "How does the corporate learning program work?",
|
|
||||||
answer: "Corporate programs are customized for your organization with assigned courses, team collaboration tools, progress tracking, and detailed analytics for administrators."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: "What technical requirements do I need?",
|
|
||||||
answer: "You need a modern web browser (Chrome, Firefox, Safari, or Edge), stable internet connection, and speakers/headphones for video content. No special software installation is required."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: "How do I contact support?",
|
|
||||||
answer: "You can reach our support team through the Contact page, email us at support@klc.edu, or use the KLC Assistant chat available on every page."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: "Are there mobile apps available?",
|
|
||||||
answer: "Currently, our learning platform is web-based and fully responsive, working seamlessly on mobile devices through your browser. Native mobile apps are planned for future release."
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-white">
|
|
||||||
{/* Header Section */}
|
|
||||||
<div className="bg-primary text-primary-foreground pt-24 pb-12">
|
|
||||||
<div className="container mx-auto px-4 lg:px-8">
|
|
||||||
<div className="max-w-4xl mx-auto">
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={handleBackNavigation}
|
|
||||||
className="text-primary-foreground hover:bg-primary-foreground/10 min-h-[44px] min-w-[44px] mt-1"
|
|
||||||
aria-label="Go back to home page"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="h-6 w-6" />
|
|
||||||
</Button>
|
|
||||||
<div className="flex-1">
|
|
||||||
<h1 className="text-[36px] mb-3 font-bold">Frequently Asked Questions</h1>
|
|
||||||
<p className="text-[18px] text-primary-foreground/90 leading-relaxed">
|
|
||||||
Find answers to common questions about our learning platform and programs.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Content */}
|
|
||||||
<div className="container mx-auto px-4 lg:px-8 -mt-8">
|
|
||||||
<div className="max-w-4xl mx-auto">
|
|
||||||
<Card className="bg-white shadow-xl border-0">
|
|
||||||
<CardHeader className="pb-6">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center">
|
|
||||||
<HelpCircle className="h-6 w-6 text-primary" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<CardTitle className="text-[24px] font-bold text-gray-900">
|
|
||||||
How can we help you?
|
|
||||||
</CardTitle>
|
|
||||||
<p className="text-[16px] text-gray-600">
|
|
||||||
Browse through our most commonly asked questions below
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{faqData.map((faq, index) => (
|
|
||||||
<Collapsible key={index}>
|
|
||||||
<CollapsibleTrigger className="flex w-full items-center justify-between rounded-lg border border-gray-200 p-4 text-left hover:bg-gray-50 transition-colors">
|
|
||||||
<span className="text-[16px] font-medium text-gray-900 pr-4">
|
|
||||||
{faq.question}
|
|
||||||
</span>
|
|
||||||
<ChevronDown className="h-5 w-5 shrink-0 text-gray-500 transition-transform duration-200 group-data-[state=open]:rotate-180" />
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
<CollapsibleContent className="px-4 pb-4 pt-2">
|
|
||||||
<p className="text-[16px] text-gray-600 leading-relaxed">
|
|
||||||
{faq.answer}
|
|
||||||
</p>
|
|
||||||
</CollapsibleContent>
|
|
||||||
</Collapsible>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Contact Support Section */}
|
|
||||||
<div className="mt-8 p-6 bg-gradient-to-r from-primary/5 to-primary/10 rounded-lg">
|
|
||||||
<h3 className="text-[18px] font-semibold text-gray-900 mb-3">
|
|
||||||
Still have questions?
|
|
||||||
</h3>
|
|
||||||
<p className="text-[16px] text-gray-600 mb-4">
|
|
||||||
Our support team is here to help you get the most out of your learning experience.
|
|
||||||
</p>
|
|
||||||
<div className="flex flex-col sm:flex-row gap-3">
|
|
||||||
<Button
|
|
||||||
onClick={() => navigate('/contact')}
|
|
||||||
className="text-[16px] min-h-[44px] bg-primary hover:bg-primary/90"
|
|
||||||
>
|
|
||||||
<Mail className="w-4 h-4 mr-2" />
|
|
||||||
Contact Support
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => window.open('mailto:support@klc.edu', '_blank')}
|
|
||||||
className="text-[16px] min-h-[44px] border-primary text-primary hover:bg-primary/10"
|
|
||||||
>
|
|
||||||
<Phone className="w-4 h-4 mr-2" />
|
|
||||||
Email Us Directly
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import React, { useEffect } from 'react';
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
export function FacilityBooking() {
|
|
||||||
useEffect(() => {
|
|
||||||
// This is a deprecated page, redirect to home
|
|
||||||
navigate('/');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import React, { useEffect } from 'react';
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
export function FacilityDetail() {
|
|
||||||
useEffect(() => {
|
|
||||||
// This is a deprecated page, redirect to home
|
|
||||||
navigate('/');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import React, { useEffect } from 'react';
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
export function FacilityTour() {
|
|
||||||
useEffect(() => {
|
|
||||||
// This is a deprecated page, redirect to home
|
|
||||||
navigate('/');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||