Initial commit: setup project

This commit is contained in:
priyanshuvish
2025-08-28 14:02:49 +05:30
commit f485f346eb
176 changed files with 32770 additions and 0 deletions

23
.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# Dependencies
node_modules/
# Build output
dist/
build/
# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Environment variables
.env
.env.local
.env.*.local
# IDE/editor settings
.vscode/
.idea/
.DS_Store
Thumbs.db

11
README.md Normal file
View File

@@ -0,0 +1,11 @@
# (Success) v 02 KLC Website with Learner's Learning 0.1
This is a code bundle for (Success) v 02 KLC Website with Learner's Learning 0.1. The original project is available at https://www.figma.com/design/EsJMRo8s3zDQtHwPpj5TVk/-Success--v-02-KLC-Website-with-Learner-s-Learning-0.1.
## Running the code
Run `npm i` to install the dependencies.
Run `npm run dev` to start the development server.

15
index.html Normal file
View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>(Success) v 02 KLC Website with Learner's Learning 0.1</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

4094
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

62
package.json Normal file
View File

@@ -0,0 +1,62 @@
{
"name": "(Success) v 02 KLC Website with Learner's Learning 0.1",
"version": "0.1.0",
"private": true,
"dependencies": {
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-alert-dialog": "^1.1.6",
"@radix-ui/react-aspect-ratio": "^1.1.2",
"@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-collapsible": "^1.1.3",
"@radix-ui/react-context-menu": "^2.2.6",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-hover-card": "^1.1.6",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-menubar": "^1.1.6",
"@radix-ui/react-navigation-menu": "^1.2.5",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-radio-group": "^1.2.3",
"@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slider": "^1.2.3",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toggle": "^1.1.2",
"@radix-ui/react-toggle-group": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@tailwindcss/postcss": "^4.1.12",
"class-variance-authority": "^0.7.1",
"clsx": "*",
"cmdk": "^1.1.1",
"embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.487.0",
"motion": "*",
"next-themes": "^0.4.6",
"postcss": "^8.5.6",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.55.0",
"react-resizable-panels": "^2.1.7",
"recharts": "^2.15.2",
"sonner": "^2.0.3",
"tailwind-merge": "*",
"tailwindcss": "^4.1.12",
"vaul": "^1.1.2"
},
"devDependencies": {
"@types/node": "^20.10.0",
"@vitejs/plugin-react-swc": "^3.10.2",
"vite": "^5.4.19"
},
"scripts": {
"dev": "vite",
"build": "vite build"
}
}

5
postcss.config.mjs Normal file
View File

@@ -0,0 +1,5 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
}
}

View File

@@ -0,0 +1,13 @@
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>
);
}

View File

@@ -0,0 +1,3 @@
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",
}

1187
src/App.tsx Normal file

File diff suppressed because it is too large Load Diff

3
src/Attributions.md Normal file
View File

@@ -0,0 +1,3 @@
This Figma Make file includes components from [shadcn/ui](https://ui.shadcn.com/) used under [MIT license](https://github.com/shadcn-ui/ui/blob/main/LICENSE.md).
This Figma Make file includes photos from [Unsplash](https://unsplash.com) used under [license](https://unsplash.com/license).

140
src/Guidelines.md Normal file
View File

@@ -0,0 +1,140 @@
# 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';
import { navigateTo } from '../components/Router';
```
### Standard Navigation Handler
```tsx
const handleBackNavigation = () => {
// Choose appropriate navigation based on page context
navigateTo('/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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 899 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,337 @@
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>
)}
</>
);
}

View File

@@ -0,0 +1,92 @@
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;
}

View File

@@ -0,0 +1,2 @@
// CTABanner component has been removed
// This file is left empty to prevent import errors

View File

@@ -0,0 +1,358 @@
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>
);
}

View File

@@ -0,0 +1,212 @@
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 { navigateTo } 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={() => navigateTo(`/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>
);
}

190
src/components/Footer.tsx Normal file
View File

@@ -0,0 +1,190 @@
import React from 'react';
import { Button } from './ui/button';
import klcLogo from 'figma:asset/c2d0a01da274cef655bbdfb1b11ff3e9993ea278.png';
import { navigateTo } 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={() => navigateTo(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>&copy; 2025 Kautilya Leadership Centre. All rights reserved.</span>
<div className="flex items-center gap-4">
<button
onClick={() => navigateTo('/privacy')}
className="hover:text-primary transition-colors"
>
Privacy Policy
</button>
<button
onClick={() => navigateTo('/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>
);
}

View File

@@ -0,0 +1,152 @@
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>
);
}

View File

@@ -0,0 +1,185 @@
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>
);
}

View File

@@ -0,0 +1,709 @@
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 { navigateTo } 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 = () => {
navigateTo('/auth'); // Route to login selection page
setIsMobileMenuOpen(false);
};
const handleSignup = () => {
navigateTo('/signup');
setIsMobileMenuOpen(false);
};
const handleLogout = () => {
signOut();
navigateTo('/');
setActiveDropdown(null);
setIsMobileMenuOpen(false);
};
const handleAccountSignIn = (accountType: 'individual' | 'corporate') => {
// Navigate to appropriate sign-in page for the account type
if (accountType === 'individual') {
navigateTo('/login');
} else {
navigateTo('/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={() => navigateTo('/')}
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={() => {
navigateTo(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={() => navigateTo('/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={() => navigateTo(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={() => {
navigateTo(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={() => {
navigateTo(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={() => {
navigateTo('/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>
);
}

View File

@@ -0,0 +1,173 @@
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 { navigateTo } 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={() => navigateTo(`/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>
);
}

306
src/components/Router.tsx Normal file
View File

@@ -0,0 +1,306 @@
import React from 'react';
// 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';
// Router function to navigate programmatically
export function navigateTo(path: string) {
window.history.pushState({}, '', path);
window.dispatchEvent(new PopStateEvent('popstate'));
}
// Get query parameter helper
function getQueryParam(param: string): string | null {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get(param);
}
// Router component
export function Router() {
const [currentPath, setCurrentPath] = React.useState(window.location.pathname);
React.useEffect(() => {
const handlePopState = () => {
setCurrentPath(window.location.pathname);
};
window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
}, []);
// Handle legacy route redirects
React.useEffect(() => {
// Redirect old corporate routes to new query parameter format
const legacyRedirects: Record<string, string> = {
'/corporate/dashboard': '/dashboard?view=corporate',
'/corporate/library': '/library?view=corporate',
'/corporate/course': '/course?view=corporate',
'/corporate/settings': '/settings?view=corporate',
'/corporate/surveys': '/surveys?view=corporate',
'/corporate/webinars': '/webinars?view=corporate',
'/corporate/leaderboard': '/leaderboard?view=corporate'
};
if (legacyRedirects[currentPath]) {
navigateTo(legacyRedirects[currentPath]);
return;
}
// Redirect deprecated service pages to home
const deprecatedServiceRoutes = [
'/services',
'/services/leadership-development',
'/services/management-development',
'/services/executive-coaching',
'/services/culture-competence',
'/services/consulting',
'/services/learning-facility'
];
if (deprecatedServiceRoutes.includes(currentPath)) {
navigateTo('/');
return;
}
// Redirect deprecated about pages to home
const deprecatedAboutRoutes = [
'/about-us/our-vision',
'/about-us/our-team',
'/about-us/our-impact',
'/about-us/our-expertise'
];
if (deprecatedAboutRoutes.includes(currentPath)) {
navigateTo('/');
return;
}
// Redirect deprecated learning pages to home
const deprecatedLearningRoutes = [
'/learning/articles',
'/learning/blog',
'/learning/resources'
];
if (deprecatedLearningRoutes.includes(currentPath)) {
navigateTo('/');
return;
}
}, [currentPath]);
// Route matching logic
const renderRoute = () => {
const path = currentPath;
const viewParam = getQueryParam('view');
switch (path) {
case '/':
return <HomePage />;
// Authentication routes
case '/auth':
case '/login-selection':
return <LoginSelection />;
case '/login':
return <Login />;
case '/signup':
return <Signup />;
case '/corporate/auth':
return <CorporateAuth />;
case '/corporate/login':
return <CorporateLogin />;
case '/corporate/signup':
return <CorporateSignup />;
case '/forgot-password':
return <ForgotPassword />;
case '/email-verification':
return <EmailVerification />;
// Learner portal routes with query parameter support
case '/dashboard':
// Route to appropriate dashboard based on view parameter
if (viewParam === 'corporate') {
return <CorporateDashboard />;
}
return <Dashboard userType="individual" />;
case '/library':
return <Library />;
case '/course':
return <CourseTimeline />;
case '/settings':
return <Settings />;
case '/surveys':
return <Surveys />;
case '/webinars':
// Route to appropriate webinar page based on view parameter
if (viewParam === 'corporate') {
return <CorporateWebinars />;
}
return <IndividualWebinars />;
case '/leaderboard':
// Route to appropriate leaderboard based on view parameter
if (viewParam === 'corporate') {
return <CorporateLeaderboard />;
}
return <Leaderboard />;
// Individual webinars (accessible without authentication)
case '/individual-webinars':
return <IndividualWebinars />;
// Contact and support
case '/contact':
return <Contact />;
// About pages (deprecated - redirect to home)
case '/about-klc':
return <AboutKLC />;
case '/about-us/our-vision':
return <OurVision />;
case '/about-us/our-team':
return <OurTeam />;
case '/about-us/our-impact':
return <OurImpact />;
case '/about-us/our-expertise':
return <OurExpertise />;
// Programme pages (deprecated - redirect to home)
case '/programmes':
return <ProgrammeCatalogue />;
case '/programmes/detail':
return <ProgrammeDetail />;
case '/programmes/executive-leadership':
return <ExecutiveLeadership />;
case '/programmes/team-leadership':
return <TeamLeadership />;
case '/programmes/innovation-leadership':
return <InnovationLeadership />;
case '/programmes/leadership-online':
return <LeadershipOnline />;
// Service pages (deprecated - redirect to home)
case '/services/leadership-development':
return <LeadershipDevelopment />;
case '/services/management-development':
return <ManagementDevelopment />;
case '/services/executive-coaching':
return <ExecutiveCoaching />;
case '/services/culture-competence':
return <CultureCompetence />;
case '/services/consulting':
return <Consulting />;
case '/services/learning-facility':
return <LearningFacility />;
// Learning pages (deprecated - redirect to home)
case '/learning/articles':
return <Articles />;
case '/learning/blog':
return <BlogListing />;
case '/learning/blog/detail':
return <BlogDetail />;
case '/learning/resources':
return <Resources />;
// Webinar pages (deprecated - redirect to home)
case '/webinars/listing':
return <WebinarListing />;
case '/webinars/detail':
return <WebinarDetail />;
// Facility pages (deprecated - redirect to home)
case '/facilities/detail':
return <FacilityDetail />;
case '/facilities/booking':
return <FacilityBooking />;
case '/facilities/tour':
return <FacilityTour />;
// E-commerce pages (deprecated - redirect to home)
case '/cart':
return <Cart />;
case '/checkout':
return <Checkout />;
case '/order-confirmation':
return <OrderConfirmation />;
case '/order-failed':
return <OrderFailed />;
// Additional learner features (deprecated - redirect to home)
case '/my-cohort':
return <MyCohort />;
// Legal and support pages
case '/faq':
return <FAQ />;
case '/privacy':
return <Privacy />;
case '/terms':
return <Terms />;
// 404 page
default:
return <NotFound />;
}
};
return (
<div className="min-h-screen">
{renderRoute()}
</div>
);
}

View File

@@ -0,0 +1,27 @@
import React, { useState } from 'react'
const ERROR_IMG_SRC =
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODgiIGhlaWdodD0iODgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBvcGFjaXR5PSIuMyIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIzLjciPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjU2IiBoZWlnaHQ9IjU2IiByeD0iNiIvPjxwYXRoIGQ9Im0xNiA1OCAxNi0xOCAzMiAzMiIvPjxjaXJjbGUgY3g9IjUzIiBjeT0iMzUiIHI9IjciLz48L3N2Zz4KCg=='
export function ImageWithFallback(props: React.ImgHTMLAttributes<HTMLImageElement>) {
const [didError, setDidError] = useState(false)
const handleError = () => {
setDidError(true)
}
const { src, alt, style, className, ...rest } = props
return didError ? (
<div
className={`inline-block bg-gray-100 text-center align-middle ${className ?? ''}`}
style={style}
>
<div className="flex items-center justify-center w-full h-full">
<img src={ERROR_IMG_SRC} alt="Error loading image" {...rest} data-original-url={src} />
</div>
</div>
) : (
<img src={src} alt={alt} className={className} style={style} {...rest} onError={handleError} />
)
}

View File

@@ -0,0 +1,331 @@
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';
import { navigateTo } from '../Router';
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 = () => {
navigateTo(`/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>
);
}

View File

@@ -0,0 +1,268 @@
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';
import { navigateTo } from '../Router';
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 = () => {
navigateTo(`/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>
);
}

View File

@@ -0,0 +1,297 @@
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 { navigateTo } from '../Router';
import { ImageWithFallback } from '../figma/ImageWithFallback';
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);
// Navigate to course details page with proper query parameters
const handleCourseNavigation = () => {
navigateTo(`/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>
);
}

View File

@@ -0,0 +1,541 @@
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 {
Menu,
Search,
Bell,
ChevronDown,
Home,
BookOpen,
User,
BarChart3,
MessageSquare,
Calendar,
Trophy,
Building2,
Users,
X,
Settings
} from 'lucide-react';
import { navigateTo } from '../Router';
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;
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={() => navigateTo('/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('');
// 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>
</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={() => {
navigateTo(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-between px-4 py-4">
{/* Search - Reduced max width for more space */}
<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>
);
}

View File

@@ -0,0 +1,346 @@
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>
);
}

View File

@@ -0,0 +1,595 @@
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>
);
}

View File

@@ -0,0 +1,417 @@
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 { navigateTo } from '../Router';
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());
// 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={() => navigateTo(`/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={() => navigateTo(`/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={() => navigateTo(`/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={() => navigateTo(`/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={() => navigateTo(`/dashboard?view=${userType}`)}
>
<Users className="h-4 w-4 mr-3" />
Team Calendar
</Button>
)}
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,362 @@
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>
);
}

View File

@@ -0,0 +1,66 @@
"use client";
import * as React from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion@1.2.3";
import { ChevronDownIcon } from "lucide-react@0.487.0";
import { cn } from "./utils";
function Accordion({
...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
}
function AccordionItem({
className,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn("border-b last:border-b-0", className)}
{...props}
/>
);
}
function AccordionTrigger({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className,
)}
{...props}
>
{children}
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
);
}
function AccordionContent({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...props}
>
<div className={cn("pt-0 pb-4", className)}>{children}</div>
</AccordionPrimitive.Content>
);
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

View File

@@ -0,0 +1,157 @@
"use client";
import * as React from "react";
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog@1.1.6";
import { cn } from "./utils";
import { buttonVariants } from "./button";
function AlertDialog({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
}
function AlertDialogTrigger({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
);
}
function AlertDialogPortal({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
);
}
function AlertDialogOverlay({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
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",
className,
)}
{...props}
/>
);
}
function AlertDialogContent({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className,
)}
{...props}
/>
</AlertDialogPortal>
);
}
function AlertDialogHeader({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
);
}
function AlertDialogFooter({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className,
)}
{...props}
/>
);
}
function AlertDialogTitle({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn("text-lg font-semibold", className)}
{...props}
/>
);
}
function AlertDialogDescription({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
function AlertDialogAction({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
return (
<AlertDialogPrimitive.Action
className={cn(buttonVariants(), className)}
{...props}
/>
);
}
function AlertDialogCancel({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return (
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: "outline" }), className)}
{...props}
/>
);
}
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
};

View File

@@ -0,0 +1,66 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority@0.7.1";
import { cn } from "./utils";
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
},
},
defaultVariants: {
variant: "default",
},
},
);
function Alert({
className,
variant,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return (
<div
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
);
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-title"
className={cn(
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
className,
)}
{...props}
/>
);
}
function AlertDescription({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-description"
className={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className,
)}
{...props}
/>
);
}
export { Alert, AlertTitle, AlertDescription };

View File

@@ -0,0 +1,11 @@
"use client";
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio@1.1.2";
function AspectRatio({
...props
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />;
}
export { AspectRatio };

View File

@@ -0,0 +1,53 @@
"use client";
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar@1.1.3";
import { cn } from "./utils";
function Avatar({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
"relative flex size-10 shrink-0 overflow-hidden rounded-full",
className,
)}
{...props}
/>
);
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
{...props}
/>
);
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted flex size-full items-center justify-center rounded-full",
className,
)}
{...props}
/>
);
}
export { Avatar, AvatarImage, AvatarFallback };

View File

@@ -0,0 +1,46 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot@1.1.2";
import { cva, type VariantProps } from "class-variance-authority@0.7.1";
import { cn } from "./utils";
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span";
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
);
}
export { Badge, badgeVariants };

View File

@@ -0,0 +1,109 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot@1.1.2";
import { ChevronRight, MoreHorizontal } from "lucide-react@0.487.0";
import { cn } from "./utils";
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
}
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
return (
<ol
data-slot="breadcrumb-list"
className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
className,
)}
{...props}
/>
);
}
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-item"
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
);
}
function BreadcrumbLink({
asChild,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "a";
return (
<Comp
data-slot="breadcrumb-link"
className={cn("hover:text-foreground transition-colors", className)}
{...props}
/>
);
}
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
className={cn("text-foreground font-normal", className)}
{...props}
/>
);
}
function BreadcrumbSeparator({
children,
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
);
}
function BreadcrumbEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="size-4" />
<span className="sr-only">More</span>
</span>
);
}
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
};

View File

@@ -0,0 +1,58 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot@1.1.2";
import { cva, type VariantProps } from "class-variance-authority@0.7.1";
import { cn } from "./utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background text-foreground hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9 rounded-md",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
const Button = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}
>(({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
ref={ref}
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
});
Button.displayName = "Button";
export { Button, buttonVariants };

View File

@@ -0,0 +1,75 @@
"use client";
import * as React from "react";
import { ChevronLeft, ChevronRight } from "lucide-react@0.487.0";
import { DayPicker } from "react-day-picker@8.10.1";
import { cn } from "./utils";
import { buttonVariants } from "./button";
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: React.ComponentProps<typeof DayPicker>) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row gap-2",
month: "flex flex-col gap-4",
caption: "flex justify-center pt-1 relative items-center w-full",
caption_label: "text-sm font-medium",
nav: "flex items-center gap-1",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-x-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: cn(
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md",
props.mode === "range"
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
: "[&:has([aria-selected])]:rounded-md",
),
day: cn(
buttonVariants({ variant: "ghost" }),
"size-8 p-0 font-normal aria-selected:opacity-100",
),
day_range_start:
"day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
day_range_end:
"day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground aria-selected:text-muted-foreground",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ className, ...props }) => (
<ChevronLeft className={cn("size-4", className)} {...props} />
),
IconRight: ({ className, ...props }) => (
<ChevronRight className={cn("size-4", className)} {...props} />
),
}}
{...props}
/>
);
}
export { Calendar };

View File

@@ -0,0 +1,92 @@
import * as React from "react";
import { cn } from "./utils";
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-lg border",
className,
)}
{...props}
/>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 pt-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className,
)}
{...props}
/>
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<h4
data-slot="card-title"
className={cn("leading-none", className)}
{...props}
/>
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<p
data-slot="card-description"
className={cn("text-muted-foreground", className)}
{...props}
/>
);
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className,
)}
{...props}
/>
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6 [&:last-child]:pb-6", className)}
{...props}
/>
);
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 pb-6 [.border-t]:pt-6", className)}
{...props}
/>
);
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
};

View File

@@ -0,0 +1,241 @@
"use client";
import * as React from "react";
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react@8.6.0";
import { ArrowLeft, ArrowRight } from "lucide-react@0.487.0";
import { cn } from "./utils";
import { Button } from "./button";
type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
type CarouselOptions = UseCarouselParameters[0];
type CarouselPlugin = UseCarouselParameters[1];
type CarouselProps = {
opts?: CarouselOptions;
plugins?: CarouselPlugin;
orientation?: "horizontal" | "vertical";
setApi?: (api: CarouselApi) => void;
};
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
api: ReturnType<typeof useEmblaCarousel>[1];
scrollPrev: () => void;
scrollNext: () => void;
canScrollPrev: boolean;
canScrollNext: boolean;
} & CarouselProps;
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
function useCarousel() {
const context = React.useContext(CarouselContext);
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />");
}
return context;
}
function Carousel({
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
}: React.ComponentProps<"div"> & CarouselProps) {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins,
);
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
const [canScrollNext, setCanScrollNext] = React.useState(false);
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) return;
setCanScrollPrev(api.canScrollPrev());
setCanScrollNext(api.canScrollNext());
}, []);
const scrollPrev = React.useCallback(() => {
api?.scrollPrev();
}, [api]);
const scrollNext = React.useCallback(() => {
api?.scrollNext();
}, [api]);
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault();
scrollPrev();
} else if (event.key === "ArrowRight") {
event.preventDefault();
scrollNext();
}
},
[scrollPrev, scrollNext],
);
React.useEffect(() => {
if (!api || !setApi) return;
setApi(api);
}, [api, setApi]);
React.useEffect(() => {
if (!api) return;
onSelect(api);
api.on("reInit", onSelect);
api.on("select", onSelect);
return () => {
api?.off("select", onSelect);
};
}, [api, onSelect]);
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
data-slot="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
);
}
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
const { carouselRef, orientation } = useCarousel();
return (
<div
ref={carouselRef}
className="overflow-hidden"
data-slot="carousel-content"
>
<div
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className,
)}
{...props}
/>
</div>
);
}
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
const { orientation } = useCarousel();
return (
<div
role="group"
aria-roledescription="slide"
data-slot="carousel-item"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className,
)}
{...props}
/>
);
}
function CarouselPrevious({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return (
<Button
data-slot="carousel-previous"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -left-12 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft />
<span className="sr-only">Previous slide</span>
</Button>
);
}
function CarouselNext({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollNext, canScrollNext } = useCarousel();
return (
<Button
data-slot="carousel-next"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -right-12 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight />
<span className="sr-only">Next slide</span>
</Button>
);
}
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
};

353
src/components/ui/chart.tsx Normal file
View File

@@ -0,0 +1,353 @@
"use client";
import * as React from "react";
import * as RechartsPrimitive from "recharts@2.15.2";
import { cn } from "./utils";
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const;
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode;
icon?: React.ComponentType;
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
);
};
type ChartContextProps = {
config: ChartConfig;
};
const ChartContext = React.createContext<ChartContextProps | null>(null);
function useChart() {
const context = React.useContext(ChartContext);
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />");
}
return context;
}
function ChartContainer({
id,
className,
children,
config,
...props
}: React.ComponentProps<"div"> & {
config: ChartConfig;
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"];
}) {
const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
return (
<ChartContext.Provider value={{ config }}>
<div
data-slot="chart"
data-chart={chartId}
className={cn(
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
className,
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
);
}
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([, config]) => config.theme || config.color,
);
if (!colorConfig.length) {
return null;
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color;
return color ? ` --color-${key}: ${color};` : null;
})
.join("\n")}
}
`,
)
.join("\n"),
}}
/>
);
};
const ChartTooltip = RechartsPrimitive.Tooltip;
function ChartTooltipContent({
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean;
hideIndicator?: boolean;
indicator?: "line" | "dot" | "dashed";
nameKey?: string;
labelKey?: string;
}) {
const { config } = useChart();
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null;
}
const [item] = payload;
const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label;
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
);
}
if (!value) {
return null;
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
]);
if (!active || !payload?.length) {
return null;
}
const nestLabel = payload.length === 1 && indicator !== "dot";
return (
<div
className={cn(
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
className,
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color;
return (
<div
key={item.dataKey}
className={cn(
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
indicator === "dot" && "items-center",
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
},
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center",
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
);
})}
</div>
</div>
);
}
const ChartLegend = RechartsPrimitive.Legend;
function ChartLegendContent({
className,
hideIcon = false,
payload,
verticalAlign = "bottom",
nameKey,
}: React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean;
nameKey?: string;
}) {
const { config } = useChart();
if (!payload?.length) {
return null;
}
return (
<div
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className,
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
return (
<div
key={item.value}
className={cn(
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3",
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
);
})}
</div>
);
}
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string,
) {
if (typeof payload !== "object" || payload === null) {
return undefined;
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined;
let configLabelKey: string = key;
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string;
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string;
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config];
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
};

View File

@@ -0,0 +1,32 @@
"use client";
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox@1.1.4";
import { CheckIcon } from "lucide-react@0.487.0";
import { cn } from "./utils";
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border bg-input-background dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="flex items-center justify-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
);
}
export { Checkbox };

View File

@@ -0,0 +1,33 @@
"use client";
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible@1.1.3";
function Collapsible({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
}
function CollapsibleTrigger({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
return (
<CollapsiblePrimitive.CollapsibleTrigger
data-slot="collapsible-trigger"
{...props}
/>
);
}
function CollapsibleContent({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
return (
<CollapsiblePrimitive.CollapsibleContent
data-slot="collapsible-content"
{...props}
/>
);
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent };

View File

@@ -0,0 +1,177 @@
"use client";
import * as React from "react";
import { Command as CommandPrimitive } from "cmdk@1.1.1";
import { SearchIcon } from "lucide-react@0.487.0";
import { cn } from "./utils";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "./dialog";
function Command({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot="command"
className={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className,
)}
{...props}
/>
);
}
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
children,
...props
}: React.ComponentProps<typeof Dialog> & {
title?: string;
description?: string;
}) {
return (
<Dialog {...props}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent className="overflow-hidden p-0">
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
);
}
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div
data-slot="command-input-wrapper"
className="flex h-9 items-center gap-2 border-b px-3"
>
<SearchIcon className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
</div>
);
}
function CommandList({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot="command-list"
className={cn(
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
className,
)}
{...props}
/>
);
}
function CommandEmpty({
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className="py-6 text-center text-sm"
{...props}
/>
);
}
function CommandGroup({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
className,
)}
{...props}
/>
);
}
function CommandSeparator({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("bg-border -mx-1 h-px", className)}
{...props}
/>
);
}
function CommandItem({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function CommandShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className,
)}
{...props}
/>
);
}
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};

View File

@@ -0,0 +1,252 @@
"use client";
import * as React from "react";
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu@2.2.6";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react@0.487.0";
import { cn } from "./utils";
function ContextMenu({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
}
function ContextMenuTrigger({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
return (
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
);
}
function ContextMenuGroup({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
return (
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
);
}
function ContextMenuPortal({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
return (
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
);
}
function ContextMenuSub({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
}
function ContextMenuRadioGroup({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
return (
<ContextMenuPrimitive.RadioGroup
data-slot="context-menu-radio-group"
{...props}
/>
);
}
function ContextMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean;
}) {
return (
<ContextMenuPrimitive.SubTrigger
data-slot="context-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</ContextMenuPrimitive.SubTrigger>
);
}
function ContextMenuSubContent({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
return (
<ContextMenuPrimitive.SubContent
data-slot="context-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className,
)}
{...props}
/>
);
}
function ContextMenuContent({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
return (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
data-slot="context-menu-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className,
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
);
}
function ContextMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<ContextMenuPrimitive.Item
data-slot="context-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function ContextMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
return (
<ContextMenuPrimitive.CheckboxItem
data-slot="context-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
);
}
function ContextMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
return (
<ContextMenuPrimitive.RadioItem
data-slot="context-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
);
}
function ContextMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
inset?: boolean;
}) {
return (
<ContextMenuPrimitive.Label
data-slot="context-menu-label"
data-inset={inset}
className={cn(
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className,
)}
{...props}
/>
);
}
function ContextMenuSeparator({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
return (
<ContextMenuPrimitive.Separator
data-slot="context-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function ContextMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="context-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className,
)}
{...props}
/>
);
}
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
};

View File

@@ -0,0 +1,138 @@
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog@1.1.6";
import { XIcon } from "lucide-react@0.487.0";
import { cn } from "./utils";
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
}
const DialogTrigger = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Trigger>
>((props, ref) => (
<DialogPrimitive.Trigger ref={ref} data-slot="dialog-trigger" {...props} />
))
DialogTrigger.displayName = DialogPrimitive.Trigger.displayName
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
}
const DialogClose = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Close>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Close>
>((props, ref) => (
<DialogPrimitive.Close ref={ref} data-slot="dialog-close" {...props} />
))
DialogClose.displayName = DialogPrimitive.Close.displayName
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
data-slot="dialog-overlay"
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",
className,
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
);
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className,
)}
{...props}
/>
);
}
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
data-slot="dialog-title"
className={cn("text-xl leading-tight font-semibold", className)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
data-slot="dialog-description"
className={cn("text-muted-foreground text-base", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
};

View File

@@ -0,0 +1,132 @@
"use client";
import * as React from "react";
import { Drawer as DrawerPrimitive } from "vaul@1.1.2";
import { cn } from "./utils";
function Drawer({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
}
function DrawerTrigger({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
}
function DrawerPortal({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
}
function DrawerClose({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
}
function DrawerOverlay({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
return (
<DrawerPrimitive.Overlay
data-slot="drawer-overlay"
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",
className,
)}
{...props}
/>
);
}
function DrawerContent({
className,
children,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
return (
<DrawerPortal data-slot="drawer-portal">
<DrawerOverlay />
<DrawerPrimitive.Content
data-slot="drawer-content"
className={cn(
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
className,
)}
{...props}
>
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
);
}
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-header"
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
);
}
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
);
}
function DrawerTitle({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
return (
<DrawerPrimitive.Title
data-slot="drawer-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
);
}
function DrawerDescription({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return (
<DrawerPrimitive.Description
data-slot="drawer-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
};

View File

@@ -0,0 +1,258 @@
"use client";
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu@2.1.6";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react@0.487.0";
import { cn } from "./utils";
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
);
}
const DropdownMenuTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Trigger>,
React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>
>(({ ...props }, ref) => (
<DropdownMenuPrimitive.Trigger
ref={ref}
data-slot="dropdown-menu-trigger"
{...props}
/>
));
DropdownMenuTrigger.displayName = "DropdownMenuTrigger";
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
);
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
);
}
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
variant?: "default" | "destructive";
}
>(({ className, inset, variant = "default", ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
));
DropdownMenuItem.displayName = "DropdownMenuItem";
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
);
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
);
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
);
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className,
)}
{...props}
/>
);
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className,
)}
{...props}
/>
);
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
);
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className,
)}
{...props}
/>
);
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
};

168
src/components/ui/form.tsx Normal file
View File

@@ -0,0 +1,168 @@
"use client";
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label@2.1.2";
import { Slot } from "@radix-ui/react-slot@1.1.2";
import {
Controller,
FormProvider,
useFormContext,
useFormState,
type ControllerProps,
type FieldPath,
type FieldValues,
} from "react-hook-form@7.55.0";
import { cn } from "./utils";
import { Label } from "./label";
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue,
);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState } = useFormContext();
const formState = useFormState({ name: fieldContext.name });
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>");
}
const { id } = itemContext;
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
};
type FormItemContextValue = {
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue,
);
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div
data-slot="form-item"
className={cn("grid gap-2", className)}
{...props}
/>
</FormItemContext.Provider>
);
}
function FormLabel({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField();
return (
<Label
data-slot="form-label"
data-error={!!error}
className={cn("data-[error=true]:text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
);
}
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return (
<Slot
data-slot="form-control"
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
);
}
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
const { formDescriptionId } = useFormField();
return (
<p
data-slot="form-description"
id={formDescriptionId}
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message ?? "") : props.children;
if (!body) {
return null;
}
return (
<p
data-slot="form-message"
id={formMessageId}
className={cn("text-destructive text-sm", className)}
{...props}
>
{body}
</p>
);
}
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
};

View File

@@ -0,0 +1,44 @@
"use client";
import * as React from "react";
import * as HoverCardPrimitive from "@radix-ui/react-hover-card@1.1.6";
import { cn } from "./utils";
function HoverCard({
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
}
function HoverCardTrigger({
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
return (
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
);
}
function HoverCardContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
return (
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
<HoverCardPrimitive.Content
data-slot="hover-card-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className,
)}
{...props}
/>
</HoverCardPrimitive.Portal>
);
}
export { HoverCard, HoverCardTrigger, HoverCardContent };

View File

@@ -0,0 +1,77 @@
"use client";
import * as React from "react";
import { OTPInput, OTPInputContext } from "input-otp@1.4.2";
import { MinusIcon } from "lucide-react@0.487.0";
import { cn } from "./utils";
function InputOTP({
className,
containerClassName,
...props
}: React.ComponentProps<typeof OTPInput> & {
containerClassName?: string;
}) {
return (
<OTPInput
data-slot="input-otp"
containerClassName={cn(
"flex items-center gap-2 has-disabled:opacity-50",
containerClassName,
)}
className={cn("disabled:cursor-not-allowed", className)}
{...props}
/>
);
}
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-otp-group"
className={cn("flex items-center gap-1", className)}
{...props}
/>
);
}
function InputOTPSlot({
index,
className,
...props
}: React.ComponentProps<"div"> & {
index: number;
}) {
const inputOTPContext = React.useContext(OTPInputContext);
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
return (
<div
data-slot="input-otp-slot"
data-active={isActive}
className={cn(
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm bg-input-background transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
className,
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
</div>
)}
</div>
);
}
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
return (
<div data-slot="input-otp-separator" role="separator" {...props}>
<MinusIcon />
</div>
);
}
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };

View File

@@ -0,0 +1,21 @@
import * as React from "react";
import { cn } from "./utils";
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base bg-input-background transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className,
)}
{...props}
/>
);
}
export { Input };

View File

@@ -0,0 +1,24 @@
"use client";
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label@2.1.2";
import { cn } from "./utils";
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className,
)}
{...props}
/>
);
}
export { Label };

View File

@@ -0,0 +1,276 @@
"use client";
import * as React from "react";
import * as MenubarPrimitive from "@radix-ui/react-menubar@1.1.6";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react@0.487.0";
import { cn } from "./utils";
function Menubar({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Root>) {
return (
<MenubarPrimitive.Root
data-slot="menubar"
className={cn(
"bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs",
className,
)}
{...props}
/>
);
}
function MenubarMenu({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />;
}
function MenubarGroup({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Group>) {
return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />;
}
function MenubarPortal({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />;
}
function MenubarRadioGroup({
...props
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
return (
<MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
);
}
function MenubarTrigger({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {
return (
<MenubarPrimitive.Trigger
data-slot="menubar-trigger"
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none",
className,
)}
{...props}
/>
);
}
function MenubarContent({
className,
align = "start",
alignOffset = -4,
sideOffset = 8,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Content>) {
return (
<MenubarPortal>
<MenubarPrimitive.Content
data-slot="menubar-content"
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
className,
)}
{...props}
/>
</MenubarPortal>
);
}
function MenubarItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof MenubarPrimitive.Item> & {
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<MenubarPrimitive.Item
data-slot="menubar-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function MenubarCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
return (
<MenubarPrimitive.CheckboxItem
data-slot="menubar-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
);
}
function MenubarRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) {
return (
<MenubarPrimitive.RadioItem
data-slot="menubar-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
);
}
function MenubarLabel({
className,
inset,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Label> & {
inset?: boolean;
}) {
return (
<MenubarPrimitive.Label
data-slot="menubar-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className,
)}
{...props}
/>
);
}
function MenubarSeparator({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Separator>) {
return (
<MenubarPrimitive.Separator
data-slot="menubar-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function MenubarShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="menubar-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className,
)}
{...props}
/>
);
}
function MenubarSub({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />;
}
function MenubarSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean;
}) {
return (
<MenubarPrimitive.SubTrigger
data-slot="menubar-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
);
}
function MenubarSubContent({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
return (
<MenubarPrimitive.SubContent
data-slot="menubar-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className,
)}
{...props}
/>
);
}
export {
Menubar,
MenubarPortal,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarGroup,
MenubarSeparator,
MenubarLabel,
MenubarItem,
MenubarShortcut,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarSub,
MenubarSubTrigger,
MenubarSubContent,
};

View File

@@ -0,0 +1,168 @@
import * as React from "react";
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu@1.2.5";
import { cva } from "class-variance-authority@0.7.1";
import { ChevronDownIcon } from "lucide-react@0.487.0";
import { cn } from "./utils";
function NavigationMenu({
className,
children,
viewport = true,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
viewport?: boolean;
}) {
return (
<NavigationMenuPrimitive.Root
data-slot="navigation-menu"
data-viewport={viewport}
className={cn(
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
className,
)}
{...props}
>
{children}
{viewport && <NavigationMenuViewport />}
</NavigationMenuPrimitive.Root>
);
}
function NavigationMenuList({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
return (
<NavigationMenuPrimitive.List
data-slot="navigation-menu-list"
className={cn(
"group flex flex-1 list-none items-center justify-center gap-1",
className,
)}
{...props}
/>
);
}
function NavigationMenuItem({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
return (
<NavigationMenuPrimitive.Item
data-slot="navigation-menu-item"
className={cn("relative", className)}
{...props}
/>
);
}
const navigationMenuTriggerStyle = cva(
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1",
);
function NavigationMenuTrigger({
className,
children,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
return (
<NavigationMenuPrimitive.Trigger
data-slot="navigation-menu-trigger"
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDownIcon
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
);
}
function NavigationMenuContent({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
return (
<NavigationMenuPrimitive.Content
data-slot="navigation-menu-content"
className={cn(
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
className,
)}
{...props}
/>
);
}
function NavigationMenuViewport({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
return (
<div
className={cn(
"absolute top-full left-0 isolate z-50 flex justify-center",
)}
>
<NavigationMenuPrimitive.Viewport
data-slot="navigation-menu-viewport"
className={cn(
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
className,
)}
{...props}
/>
</div>
);
}
function NavigationMenuLink({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
return (
<NavigationMenuPrimitive.Link
data-slot="navigation-menu-link"
className={cn(
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function NavigationMenuIndicator({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
return (
<NavigationMenuPrimitive.Indicator
data-slot="navigation-menu-indicator"
className={cn(
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
className,
)}
{...props}
>
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
</NavigationMenuPrimitive.Indicator>
);
}
export {
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
navigationMenuTriggerStyle,
};

View File

@@ -0,0 +1,127 @@
import * as React from "react";
import {
ChevronLeftIcon,
ChevronRightIcon,
MoreHorizontalIcon,
} from "lucide-react@0.487.0";
import { cn } from "./utils";
import { Button, buttonVariants } from "./button";
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
return (
<nav
role="navigation"
aria-label="pagination"
data-slot="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
);
}
function PaginationContent({
className,
...props
}: React.ComponentProps<"ul">) {
return (
<ul
data-slot="pagination-content"
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
);
}
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
return <li data-slot="pagination-item" {...props} />;
}
type PaginationLinkProps = {
isActive?: boolean;
} & Pick<React.ComponentProps<typeof Button>, "size"> &
React.ComponentProps<"a">;
function PaginationLink({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) {
return (
<a
aria-current={isActive ? "page" : undefined}
data-slot="pagination-link"
data-active={isActive}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className,
)}
{...props}
/>
);
}
function PaginationPrevious({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
{...props}
>
<ChevronLeftIcon />
<span className="hidden sm:block">Previous</span>
</PaginationLink>
);
}
function PaginationNext({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
{...props}
>
<span className="hidden sm:block">Next</span>
<ChevronRightIcon />
</PaginationLink>
);
}
function PaginationEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
aria-hidden
data-slot="pagination-ellipsis"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontalIcon className="size-4" />
<span className="sr-only">More pages</span>
</span>
);
}
export {
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
};

View File

@@ -0,0 +1,48 @@
"use client";
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover@1.1.6";
import { cn } from "./utils";
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
}
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
);
}
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };

View File

@@ -0,0 +1,31 @@
"use client";
import * as React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress@1.1.2";
import { cn } from "./utils";
function Progress({
className,
value,
...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
className,
)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
);
}
export { Progress };

View File

@@ -0,0 +1,45 @@
"use client";
import * as React from "react";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group@1.2.3";
import { CircleIcon } from "lucide-react@0.487.0";
import { cn } from "./utils";
function RadioGroup({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
return (
<RadioGroupPrimitive.Root
data-slot="radio-group"
className={cn("grid gap-3", className)}
{...props}
/>
);
}
function RadioGroupItem({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
return (
<RadioGroupPrimitive.Item
data-slot="radio-group-item"
className={cn(
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<RadioGroupPrimitive.Indicator
data-slot="radio-group-indicator"
className="relative flex items-center justify-center"
>
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
);
}
export { RadioGroup, RadioGroupItem };

View File

@@ -0,0 +1,56 @@
"use client";
import * as React from "react";
import { GripVerticalIcon } from "lucide-react@0.487.0";
import * as ResizablePrimitive from "react-resizable-panels@2.1.7";
import { cn } from "./utils";
function ResizablePanelGroup({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
return (
<ResizablePrimitive.PanelGroup
data-slot="resizable-panel-group"
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className,
)}
{...props}
/>
);
}
function ResizablePanel({
...props
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />;
}
function ResizableHandle({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean;
}) {
return (
<ResizablePrimitive.PanelResizeHandle
data-slot="resizable-handle"
className={cn(
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className,
)}
{...props}
>
{withHandle && (
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
<GripVerticalIcon className="size-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
);
}
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };

View File

@@ -0,0 +1,58 @@
"use client";
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area@1.2.3";
import { cn } from "./utils";
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
);
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
);
}
export { ScrollArea, ScrollBar };

View File

@@ -0,0 +1,189 @@
"use client";
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select@2.1.6";
import {
CheckIcon,
ChevronDownIcon,
ChevronUpIcon,
} from "lucide-react@0.487.0";
import { cn } from "./utils";
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />;
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default";
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-full items-center justify-between gap-2 rounded-md border bg-input-background px-3 py-2 text-sm whitespace-nowrap transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
}
function SelectContent({
className,
children,
position = "popper",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
);
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
);
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className,
)}
{...props}
>
<span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
);
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
);
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
};

View File

@@ -0,0 +1,28 @@
"use client";
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator@1.1.2";
import { cn } from "./utils";
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator-root"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className,
)}
{...props}
/>
);
}
export { Separator };

142
src/components/ui/sheet.tsx Normal file
View File

@@ -0,0 +1,142 @@
"use client";
import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog@1.1.6";
import { XIcon } from "lucide-react@0.487.0";
import { cn } from "./utils";
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
}
const SheetTrigger = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Trigger>,
React.ComponentProps<typeof SheetPrimitive.Trigger>
>(({ ...props }, ref) => (
<SheetPrimitive.Trigger ref={ref} data-slot="sheet-trigger" {...props} />
));
SheetTrigger.displayName = "SheetTrigger";
function SheetClose({
...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
}
function SheetPortal({
...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
}
function SheetOverlay({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
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",
className,
)}
{...props}
/>
);
}
function SheetContent({
className,
children,
side = "right",
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left";
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
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",
side === "right" &&
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
side === "left" &&
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
side === "top" &&
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
className,
)}
{...props}
>
{children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<XIcon className="size-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
);
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
);
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
);
}
function SheetTitle({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
);
}
function SheetDescription({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
};

View File

@@ -0,0 +1,726 @@
"use client";
import * as React from "react";
import { Slot } from "@radix-ui/react-slot@1.1.2";
import { VariantProps, cva } from "class-variance-authority@0.7.1";
import { PanelLeftIcon } from "lucide-react@0.487.0";
import { useIsMobile } from "./use-mobile";
import { cn } from "./utils";
import { Button } from "./button";
import { Input } from "./input";
import { Separator } from "./separator";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "./sheet";
import { Skeleton } from "./skeleton";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "./tooltip";
const SIDEBAR_COOKIE_NAME = "sidebar_state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = "16rem";
const SIDEBAR_WIDTH_MOBILE = "18rem";
const SIDEBAR_WIDTH_ICON = "3rem";
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
type SidebarContextProps = {
state: "expanded" | "collapsed";
open: boolean;
setOpen: (open: boolean) => void;
openMobile: boolean;
setOpenMobile: (open: boolean) => void;
isMobile: boolean;
toggleSidebar: () => void;
};
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
function useSidebar() {
const context = React.useContext(SidebarContext);
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.");
}
return context;
}
function SidebarProvider({
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
}: React.ComponentProps<"div"> & {
defaultOpen?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}) {
const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false);
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen);
const open = openProp ?? _open;
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value;
if (setOpenProp) {
setOpenProp(openState);
} else {
_setOpen(openState);
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
[setOpenProp, open],
);
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
}, [isMobile, setOpen, setOpenMobile]);
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault();
toggleSidebar();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [toggleSidebar]);
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed";
const contextValue = React.useMemo<SidebarContextProps>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
);
return (
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
data-slot="sidebar-wrapper"
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn(
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
className,
)}
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
);
}
function Sidebar({
side = "left",
variant = "sidebar",
collapsible = "offcanvas",
className,
children,
...props
}: React.ComponentProps<"div"> & {
side?: "left" | "right";
variant?: "sidebar" | "floating" | "inset";
collapsible?: "offcanvas" | "icon" | "none";
}) {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
if (collapsible === "none") {
return (
<div
data-slot="sidebar"
className={cn(
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
className,
)}
{...props}
>
{children}
</div>
);
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
data-sidebar="sidebar"
data-slot="sidebar"
data-mobile="true"
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
>
<SheetHeader className="sr-only">
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
);
}
return (
<div
className="group peer text-sidebar-foreground hidden md:block"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
data-slot="sidebar"
>
{/* This is what handles the sidebar gap on desktop */}
<div
data-slot="sidebar-gap"
className={cn(
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
)}
/>
<div
data-slot="sidebar-container"
className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
className,
)}
{...props}
>
<div
data-sidebar="sidebar"
data-slot="sidebar-inner"
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
>
{children}
</div>
</div>
</div>
);
}
function SidebarTrigger({
className,
onClick,
...props
}: React.ComponentProps<typeof Button>) {
const { toggleSidebar } = useSidebar();
return (
<Button
data-sidebar="trigger"
data-slot="sidebar-trigger"
variant="ghost"
size="icon"
className={cn("size-7", className)}
onClick={(event) => {
onClick?.(event);
toggleSidebar();
}}
{...props}
>
<PanelLeftIcon />
<span className="sr-only">Toggle Sidebar</span>
</Button>
);
}
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
const { toggleSidebar } = useSidebar();
return (
<button
data-sidebar="rail"
data-slot="sidebar-rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className,
)}
{...props}
/>
);
}
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
return (
<main
data-slot="sidebar-inset"
className={cn(
"bg-background relative flex w-full flex-1 flex-col",
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
className,
)}
{...props}
/>
);
}
function SidebarInput({
className,
...props
}: React.ComponentProps<typeof Input>) {
return (
<Input
data-slot="sidebar-input"
data-sidebar="input"
className={cn("bg-background h-8 w-full shadow-none", className)}
{...props}
/>
);
}
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-header"
data-sidebar="header"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
);
}
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-footer"
data-sidebar="footer"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
);
}
function SidebarSeparator({
className,
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="sidebar-separator"
data-sidebar="separator"
className={cn("bg-sidebar-border mx-2 w-auto", className)}
{...props}
/>
);
}
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-content"
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className,
)}
{...props}
/>
);
}
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group"
data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
);
}
function SidebarGroupLabel({
className,
asChild = false,
...props
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div";
return (
<Comp
data-slot="sidebar-group-label"
data-sidebar="group-label"
className={cn(
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className,
)}
{...props}
/>
);
}
function SidebarGroupAction({
className,
asChild = false,
...props
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="sidebar-group-action"
data-sidebar="group-action"
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
);
}
function SidebarGroupContent({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group-content"
data-sidebar="group-content"
className={cn("w-full text-sm", className)}
{...props}
/>
);
}
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu"
data-sidebar="menu"
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props}
/>
);
}
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-item"
data-sidebar="menu-item"
className={cn("group/menu-item relative", className)}
{...props}
/>
);
}
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function SidebarMenuButton({
asChild = false,
isActive = false,
variant = "default",
size = "default",
tooltip,
className,
...props
}: React.ComponentProps<"button"> & {
asChild?: boolean;
isActive?: boolean;
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
} & VariantProps<typeof sidebarMenuButtonVariants>) {
const Comp = asChild ? Slot : "button";
const { isMobile, state } = useSidebar();
const button = (
<Comp
data-slot="sidebar-menu-button"
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
);
if (!tooltip) {
return button;
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
};
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent
side="right"
align="center"
hidden={state !== "collapsed" || isMobile}
{...tooltip}
/>
</Tooltip>
);
}
function SidebarMenuAction({
className,
asChild = false,
showOnHover = false,
...props
}: React.ComponentProps<"button"> & {
asChild?: boolean;
showOnHover?: boolean;
}) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="sidebar-menu-action"
data-sidebar="menu-action"
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
className,
)}
{...props}
/>
);
}
function SidebarMenuBadge({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-menu-badge"
data-sidebar="menu-badge"
className={cn(
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
);
}
function SidebarMenuSkeleton({
className,
showIcon = false,
...props
}: React.ComponentProps<"div"> & {
showIcon?: boolean;
}) {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`;
}, []);
return (
<div
data-slot="sidebar-menu-skeleton"
data-sidebar="menu-skeleton"
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...props}
>
{showIcon && (
<Skeleton
className="size-4 rounded-md"
data-sidebar="menu-skeleton-icon"
/>
)}
<Skeleton
className="h-4 max-w-(--skeleton-width) flex-1"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
} as React.CSSProperties
}
/>
</div>
);
}
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu-sub"
data-sidebar="menu-sub"
className={cn(
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
);
}
function SidebarMenuSubItem({
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-sub-item"
data-sidebar="menu-sub-item"
className={cn("group/menu-sub-item relative", className)}
{...props}
/>
);
}
function SidebarMenuSubButton({
asChild = false,
size = "md",
isActive = false,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean;
size?: "sm" | "md";
isActive?: boolean;
}) {
const Comp = asChild ? Slot : "a";
return (
<Comp
data-slot="sidebar-menu-sub-button"
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
);
}
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
};

View File

@@ -0,0 +1,13 @@
import { cn } from "./utils";
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn("bg-accent animate-pulse rounded-md", className)}
{...props}
/>
);
}
export { Skeleton };

View File

@@ -0,0 +1,63 @@
"use client";
import * as React from "react";
import * as SliderPrimitive from "@radix-ui/react-slider@1.2.3";
import { cn } from "./utils";
function Slider({
className,
defaultValue,
value,
min = 0,
max = 100,
...props
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
const _values = React.useMemo(
() =>
Array.isArray(value)
? value
: Array.isArray(defaultValue)
? defaultValue
: [min, max],
[value, defaultValue, min, max],
);
return (
<SliderPrimitive.Root
data-slot="slider"
defaultValue={defaultValue}
value={value}
min={min}
max={max}
className={cn(
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
className,
)}
{...props}
>
<SliderPrimitive.Track
data-slot="slider-track"
className={cn(
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-4 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5",
)}
>
<SliderPrimitive.Range
data-slot="slider-range"
className={cn(
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full",
)}
/>
</SliderPrimitive.Track>
{Array.from({ length: _values.length }, (_, index) => (
<SliderPrimitive.Thumb
data-slot="slider-thumb"
key={index}
className="border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
/>
))}
</SliderPrimitive.Root>
);
}
export { Slider };

View File

@@ -0,0 +1,25 @@
"use client";
import { useTheme } from "next-themes@0.4.6";
import { Toaster as Sonner, ToasterProps } from "sonner@2.0.3";
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme();
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
} as React.CSSProperties
}
{...props}
/>
);
};
export { Toaster };

View File

@@ -0,0 +1,31 @@
"use client";
import * as React from "react";
import * as SwitchPrimitive from "@radix-ui/react-switch@1.1.3";
import { cn } from "./utils";
function Switch({
className,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-switch-background focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-card dark:data-[state=unchecked]:bg-card-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitive.Root>
);
}
export { Switch };

116
src/components/ui/table.tsx Normal file
View File

@@ -0,0 +1,116 @@
"use client";
import * as React from "react";
import { cn } from "./utils";
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
);
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
{...props}
/>
);
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
);
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
className,
)}
{...props}
/>
);
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className,
)}
{...props}
/>
);
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className,
)}
{...props}
/>
);
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className,
)}
{...props}
/>
);
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props}
/>
);
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
};

View File

@@ -0,0 +1,66 @@
"use client";
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs@1.1.3";
import { cn } from "./utils";
function Tabs({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
);
}
function TabsList({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-xl p-[3px] flex",
className,
)}
{...props}
/>
);
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-card dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-xl border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
/>
);
}
export { Tabs, TabsList, TabsTrigger, TabsContent };

View File

@@ -0,0 +1,18 @@
import * as React from "react";
import { cn } from "./utils";
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"resize-none border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-input-background px-3 py-2 text-base transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
{...props}
/>
);
}
export { Textarea };

View File

@@ -0,0 +1,73 @@
"use client";
import * as React from "react";
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group@1.1.2";
import { type VariantProps } from "class-variance-authority@0.7.1";
import { cn } from "./utils";
import { toggleVariants } from "./toggle";
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
>({
size: "default",
variant: "default",
});
function ToggleGroup({
className,
variant,
size,
children,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>) {
return (
<ToggleGroupPrimitive.Root
data-slot="toggle-group"
data-variant={variant}
data-size={size}
className={cn(
"group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs",
className,
)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
);
}
function ToggleGroupItem({
className,
children,
variant,
size,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>) {
const context = React.useContext(ToggleGroupContext);
return (
<ToggleGroupPrimitive.Item
data-slot="toggle-group-item"
data-variant={context.variant || variant}
data-size={context.size || size}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
"min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
className,
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
);
}
export { ToggleGroup, ToggleGroupItem };

Some files were not shown because too many files have changed in this diff Show More