first commit

This commit is contained in:
priyanshuvish
2025-09-05 19:24:43 +05:30
commit cca922e8dd
86 changed files with 20564 additions and 0 deletions

32
.gitignore vendored Normal file
View File

@@ -0,0 +1,32 @@
# Node modules
node_modules/
# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Environment files
.env
.env.local
.env.*.local
# Build output
dist/
build/
.next/
out/
# IDE files
.vscode/
.idea/
*.swp
# Mac / Linux / Windows system files
.DS_Store
Thumbs.db
# Temporary
*.tmp

11
README.md Normal file
View File

@@ -0,0 +1,11 @@
# (PR) KLC Learners Portal
This is a code bundle for (PR) KLC Learners Portal. The original project is available at https://www.figma.com/design/skyhldFw1yua0kljiJ4Iba/-PR--KLC-Learners-Portal.
## 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>(PR) KLC Learners Portal</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

4179
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

63
package.json Normal file
View File

@@ -0,0 +1,63 @@
{
"name": "(PR) KLC Learners Portal",
"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.13",
"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",
"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.13",
"vaul": "^1.1.2"
},
"devDependencies": {
"@types/node": "^20.10.0",
"@types/react": "^19.1.12",
"@types/react-dom": "^19.1.9",
"@vitejs/plugin-react-swc": "^3.10.2",
"vite": "6.3.5"
},
"scripts": {
"dev": "vite",
"build": "vite build"
}
}

5
postcss.config.mjs Normal file
View File

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

150
src/App.tsx Normal file
View File

@@ -0,0 +1,150 @@
import React, { useState } from 'react';
import { AppShell } from './components/AppShell';
import { MyCourses } from './components/MyCourses';
import { Leaderboard } from './components/Leaderboard';
import { ReportsAndCertificates } from './components/ReportsAndCertificates';
import { Notes } from './components/Notes';
import { AIMentor } from './components/AIMentor';
import { Blog } from './components/Blog';
import { DiscussionForums } from './components/DiscussionForums';
import { Notifications } from './components/Notifications';
import { Settings } from './components/Settings';
import { CourseDetailPage } from './components/CourseDetailPage';
import { ProgramDetailPage } from './components/ProgramDetailPage';
import { WebinarDetailPage } from './components/WebinarDetailPage';
import { OfflineVideoDetailPage } from './components/OfflineVideoDetailPage';
import { DocumentDetailPage } from './components/DocumentDetailPage';
// Mock user data - in real app this would come from authentication
const mockUser = {
id: '1',
firstName: 'Priya',
lastName: 'Sharma',
email: 'priya.sharma@techsolutions.com',
persona: 'corporate' as const,
orgName: 'Tech Solutions Pvt Ltd',
canSwitchMode: true,
canSwitchAccount: true,
avatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=64&h=64&fit=crop&crop=face'
};
export default function App() {
const [currentPage, setCurrentPage] = useState('my-courses');
const [selectedContentId, setSelectedContentId] = useState<string | null>(null);
const [contentType, setContentType] = useState<'course' | 'program' | 'webinar' | 'video' | 'document'>('course');
const handleNavigateToContent = (contentId: string, type: 'course' | 'program' | 'webinar' | 'video' | 'document') => {
setSelectedContentId(contentId);
setContentType(type);
setCurrentPage(`${type}-detail`);
};
const handleNavigateBack = () => {
setCurrentPage('my-courses');
setSelectedContentId(null);
};
const renderCurrentPage = () => {
switch (currentPage) {
case 'my-courses':
return <MyCourses user={mockUser} onNavigateToContent={handleNavigateToContent} />;
case 'course-detail':
return selectedContentId ? (
<CourseDetailPage
courseId={selectedContentId}
onNavigateBack={handleNavigateBack}
/>
) : (
<MyCourses user={mockUser} onNavigateToContent={handleNavigateToContent} />
);
case 'program-detail':
return selectedContentId ? (
<ProgramDetailPage
programId={selectedContentId}
onNavigateBack={handleNavigateBack}
/>
) : (
<MyCourses user={mockUser} onNavigateToContent={handleNavigateToContent} />
);
case 'webinar-detail':
return selectedContentId ? (
<WebinarDetailPage
webinarId={selectedContentId}
onNavigateBack={handleNavigateBack}
/>
) : (
<MyCourses user={mockUser} onNavigateToContent={handleNavigateToContent} />
);
case 'video-detail':
return selectedContentId ? (
<OfflineVideoDetailPage
videoId={selectedContentId}
onNavigateBack={handleNavigateBack}
/>
) : (
<MyCourses user={mockUser} onNavigateToContent={handleNavigateToContent} />
);
case 'document-detail':
return selectedContentId ? (
<DocumentDetailPage
documentId={selectedContentId}
onNavigateBack={handleNavigateBack}
/>
) : (
<MyCourses user={mockUser} onNavigateToContent={handleNavigateToContent} />
);
case 'leaderboard':
return <Leaderboard user={mockUser} />;
case 'reports-certificates':
return <ReportsAndCertificates />;
case 'notes':
return <Notes />;
case 'blog':
return <Blog />;
case 'forums':
return <DiscussionForums user={mockUser} />;
case 'notifications':
return <Notifications />;
case 'settings':
return <Settings user={mockUser} />;
default:
return <MyCourses user={mockUser} onNavigateToContent={handleNavigateToContent} />;
}
};
return (
<div className="min-h-screen bg-white">
<AppShell
currentPage={currentPage}
user={mockUser}
onNavigate={setCurrentPage}
>
{renderCurrentPage()}
</AppShell>
{/* AI Mentor FAB - Always visible */}
<AIMentor />
</div>
);
}
// Placeholder component for unimplemented pages
function PlaceholderPage({ title, description }: { title: string; description: string }) {
return (
<div className="p-6 space-y-6">
<div>
<h1 className="text-3xl font-medium">{title}</h1>
<p className="text-muted-foreground">{description}</p>
</div>
<div className="border-2 border-dashed border-border rounded-lg p-12 text-center">
<h2 className="text-xl font-medium text-muted-foreground mb-2">
{title} Coming Soon
</h2>
<p className="text-muted-foreground">
This feature is currently under development and will be available soon.
</p>
</div>
</div>
);
}

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).

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

437
src/components/AIMentor.tsx Normal file
View File

@@ -0,0 +1,437 @@
import React, { useState } from 'react';
import {
Send,
Bot,
User,
Lightbulb,
BookOpen,
Target,
TrendingUp,
MessageCircle,
X,
Youtube,
FileText,
Headphones,
ExternalLink,
Upload,
Edit3
} from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Badge } from './ui/badge';
import { ScrollArea } from './ui/scroll-area';
import { Separator } from './ui/separator';
import { Sheet, SheetContent, SheetHeader, SheetTitle } from './ui/sheet';
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
import { Textarea } from './ui/textarea';
// Mock conversation data
const mockConversation = [
{
id: '1',
type: 'assistant',
content: "Hello! I'm your KLC Assistant. I'm here to help you with your learning journey. How can I assist you today?",
timestamp: '2024-09-03T09:00:00Z'
},
{
id: '2',
type: 'user',
content: "What should I do next in my learning path?",
timestamp: '2024-09-03T09:01:00Z'
},
{
id: '3',
type: 'assistant',
content: "Based on your current progress in the Strategic Leadership Development course, I recommend:\n\n1. **Complete Module 3, Lesson 2** - You're 65% through 'Risk Assessment Strategies'\n2. **Review your notes** - You have 4 notes that could benefit from additional practice\n3. **Join the upcoming webinar** - 'Future of Leadership in Digital Age' on Sep 15th\n\nWould you like me to explain any specific concept from your current lesson?",
timestamp: '2024-09-03T09:02:00Z'
}
];
const quickChips = [
{ id: '1', text: "What should I do next?", icon: Target },
{ id: '2', text: "Explain my Module 3 gap", icon: BookOpen },
{ id: '3', text: "Review my progress", icon: TrendingUp },
{ id: '4', text: "Find YouTube videos", icon: Youtube },
{ id: '5', text: "Get feedback on my notes", icon: Edit3 },
{ id: '6', text: "Leadership challenge help", icon: Lightbulb }
];
const mockRecommendations = [
{
id: '1',
type: 'youtube',
title: 'KLC Leadership Interview: Digital Transformation',
description: 'CEO discusses modern leadership challenges in digital transformation',
url: 'https://youtube.com/watch?v=example1',
thumbnail: 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=400&h=200&fit=crop'
},
{
id: '2',
type: 'article',
title: 'Research: Emotional Intelligence in Leadership',
description: 'Latest findings on EQ impact on team performance and organizational outcomes',
url: 'https://klc.edu/research/emotional-intelligence-2024',
thumbnail: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=200&fit=crop'
},
{
id: '3',
type: 'podcast',
title: 'Future Leaders Podcast: Episode 24',
description: 'Discussion on adaptive leadership strategies with industry experts',
url: 'https://podcasts.klc.edu/future-leaders-24',
thumbnail: 'https://images.unsplash.com/photo-1535015853489-4c0e8b7cd34e?w=400&h=200&fit=crop'
}
];
export function AIMentor() {
const [messages, setMessages] = useState(mockConversation);
const [inputValue, setInputValue] = useState('');
const [isTyping, setIsTyping] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [activeTab, setActiveTab] = useState('chat');
const [notesFeedback, setNotesFeedback] = useState('');
const handleSendMessage = async () => {
if (!inputValue.trim()) return;
const newUserMessage = {
id: Date.now().toString(),
type: 'user' as const,
content: inputValue,
timestamp: new Date().toISOString()
};
setMessages(prev => [...prev, newUserMessage]);
setInputValue('');
setIsTyping(true);
// Enhanced AI response with recommendations
setTimeout(() => {
let responseContent = "I understand your question. Let me provide you with a helpful response based on your learning context and progress.";
// Add contextual recommendations based on query
if (inputValue.toLowerCase().includes('youtube') || inputValue.toLowerCase().includes('video')) {
responseContent += "\n\nHere are some relevant KLC YouTube interviews I'd recommend:\n• Digital Transformation Leadership with Sarah Chen\n• Emotional Intelligence Masterclass with Dr. Kumar\n• Crisis Leadership Strategies - Executive Panel";
} else if (inputValue.toLowerCase().includes('research') || inputValue.toLowerCase().includes('article')) {
responseContent += "\n\nBased on your current learning path, here are relevant research articles:\n• 'Adaptive Leadership in Remote Teams' - Harvard Business Review\n• 'Data-Driven Decision Making for Leaders' - KLC Research\n• 'Building Psychological Safety' - Stanford Leadership Institute";
} else if (inputValue.toLowerCase().includes('leadership challenge')) {
responseContent += "\n\nFor leadership challenges, I recommend:\n1. Review Module 3 content on conflict resolution\n2. Practice scenario planning with the KLC Leadership Simulator\n3. Connect with mentor Dr. Patel for personalized guidance\n\nWould you like me to schedule a mentoring session?";
}
const aiResponse = {
id: (Date.now() + 1).toString(),
type: 'assistant' as const,
content: responseContent,
timestamp: new Date().toISOString()
};
setMessages(prev => [...prev, aiResponse]);
setIsTyping(false);
}, 2000);
};
const handleNotesFeedback = async () => {
if (!notesFeedback.trim()) return;
setIsTyping(true);
// Simulate AI feedback processing
setTimeout(() => {
const feedbackResponse = {
id: Date.now().toString(),
type: 'assistant' as const,
content: `I've analyzed your notes. Here's my feedback:\n\n**Strengths:**\n• Clear structure and logical flow\n• Good use of real-world examples\n• Well-connected to course concepts\n\n**Suggestions:**\n• Consider adding more specific metrics/data\n• Link to Module 4 content on implementation\n• Try the STAR method for case study format\n\n**Learning Resources:**\n• KLC Note-Taking Best Practices Guide\n• Strategic Thinking Framework Workshop\n\nWould you like me to suggest related courses based on your notes?`,
timestamp: new Date().toISOString()
};
setMessages(prev => [...prev, feedbackResponse]);
setNotesFeedback('');
setIsTyping(false);
setActiveTab('chat');
}, 3000);
};
const handleChipClick = (chipText: string) => {
setInputValue(chipText);
};
const formatTime = (timestamp: string) => {
return new Date(timestamp).toLocaleTimeString('en-IN', {
hour: '2-digit',
minute: '2-digit',
timeZone: 'Asia/Kolkata'
});
};
return (
<>
{/* Floating Action Button */}
<div className="fixed bottom-6 right-6 z-50">
<Button
onClick={() => setIsOpen(true)}
className="w-14 h-14 rounded-full bg-[var(--color-brand-primary)] hover:bg-[var(--color-brand-primary)]/90 text-white shadow-lg hover:shadow-xl transition-all duration-200"
size="icon"
>
<Bot className="h-6 w-6" />
</Button>
</div>
{/* Enhanced Chat Panel with Tabs */}
<Sheet open={isOpen} onOpenChange={setIsOpen}>
<SheetContent side="right" className="w-[400px] sm:w-[500px] p-0 bg-white">
<div className="flex flex-col h-full">
{/* Header */}
<SheetHeader className="px-6 py-4 border-b bg-[var(--color-brand-primary)] text-white">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center">
<Bot className="h-5 w-5 text-white" />
</div>
<div>
<SheetTitle className="text-white text-lg">KLC Assistant</SheetTitle>
<p className="text-blue-100 text-sm">Strategic Leadership Development - Module 3</p>
</div>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => setIsOpen(false)}
className="text-white hover:bg-white/20"
>
<X className="h-4 w-4" />
</Button>
</div>
</SheetHeader>
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col">
<TabsList className="mx-6 my-4 grid w-full grid-cols-3">
<TabsTrigger value="chat" className="text-xs">Chat</TabsTrigger>
<TabsTrigger value="resources" className="text-xs">Resources</TabsTrigger>
<TabsTrigger value="feedback" className="text-xs">Feedback</TabsTrigger>
</TabsList>
<TabsContent value="chat" className="flex-1 flex flex-col m-0">
{/* Quick Action Chips */}
<div className="px-6 py-4 border-b bg-gray-50">
<p className="text-sm font-medium mb-3">Quick questions:</p>
<div className="flex flex-wrap gap-2">
{quickChips.map((chip) => {
const Icon = chip.icon;
return (
<Button
key={chip.id}
variant="outline"
size="sm"
className="text-xs h-8"
onClick={() => handleChipClick(chip.text)}
>
<Icon className="h-3 w-3 mr-1" />
{chip.text}
</Button>
);
})}
</div>
</div>
{/* Messages Area */}
<ScrollArea className="flex-1 px-6 py-4">
<div className="space-y-4">
{messages.map((message) => (
<div
key={message.id}
className={`flex gap-3 ${
message.type === 'user' ? 'justify-end' : 'justify-start'
}`}
>
{message.type === 'assistant' && (
<div className="w-8 h-8 rounded-full bg-[var(--color-brand-primary)] flex items-center justify-center flex-shrink-0">
<Bot className="h-4 w-4 text-white" />
</div>
)}
<div
className={`max-w-[80%] p-3 rounded-lg ${
message.type === 'user'
? 'bg-[var(--color-brand-primary)] text-white'
: 'bg-gray-100'
}`}
>
<p className="whitespace-pre-line text-sm">{message.content}</p>
<p className={`text-xs mt-1 ${
message.type === 'user' ? 'text-blue-100' : 'text-gray-600'
}`}>
{formatTime(message.timestamp)}
</p>
</div>
{message.type === 'user' && (
<div className="w-8 h-8 rounded-full bg-[var(--color-brand-accent)] flex items-center justify-center flex-shrink-0">
<User className="h-4 w-4 text-[var(--color-brand-black)]" />
</div>
)}
</div>
))}
{isTyping && (
<div className="flex gap-3 justify-start">
<div className="w-8 h-8 rounded-full bg-[var(--color-brand-primary)] flex items-center justify-center">
<Bot className="h-4 w-4 text-white" />
</div>
<div className="bg-gray-100 p-3 rounded-lg">
<div className="flex gap-1">
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
</div>
</div>
</div>
)}
</div>
</ScrollArea>
{/* Input Area */}
<div className="px-6 py-4 border-t bg-white">
<div className="flex gap-2">
<Input
placeholder="Ask me anything..."
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleSendMessage();
}
if (e.key === 'Escape') {
setIsOpen(false);
}
}}
className="flex-1 bg-white"
/>
<Button
onClick={handleSendMessage}
disabled={!inputValue.trim() || isTyping}
className="bg-[var(--color-brand-primary)] hover:bg-[var(--color-brand-primary)]/90 text-white"
>
<Send className="h-4 w-4" />
</Button>
</div>
<p className="text-xs text-gray-600 mt-2">
Press Enter to send, Esc to close
</p>
</div>
</TabsContent>
<TabsContent value="resources" className="flex-1 m-0">
<ScrollArea className="h-full px-6 py-4">
<div className="space-y-4">
<div>
<h3 className="font-medium mb-3">Recommended for You</h3>
<div className="space-y-3">
{mockRecommendations.map((rec) => (
<Card key={rec.id} className="bg-white border">
<CardContent className="p-3">
<div className="flex gap-3">
<img src={rec.thumbnail} alt={rec.title} className="w-16 h-12 object-cover rounded" />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
{rec.type === 'youtube' && <Youtube className="h-3 w-3 text-red-500" />}
{rec.type === 'article' && <FileText className="h-3 w-3 text-blue-500" />}
{rec.type === 'podcast' && <Headphones className="h-3 w-3 text-green-500" />}
<Badge variant="outline" className="text-xs">
{rec.type}
</Badge>
</div>
<h4 className="font-medium text-sm line-clamp-2 mb-1">{rec.title}</h4>
<p className="text-xs text-gray-600 line-clamp-2 mb-2">{rec.description}</p>
<Button size="sm" variant="outline" className="text-xs h-6">
<ExternalLink className="h-3 w-3 mr-1" />
View
</Button>
</div>
</div>
</CardContent>
</Card>
))}
</div>
</div>
<div>
<h3 className="font-medium mb-3">External References</h3>
<div className="space-y-2 text-sm">
<div className="flex items-center gap-2 p-2 bg-gray-50 rounded">
<FileText className="h-4 w-4 text-gray-500" />
<div className="flex-1">
<p className="font-medium">Harvard Business Review</p>
<p className="text-xs text-gray-600">Leadership in Crisis Management</p>
</div>
<Button size="sm" variant="ghost" className="text-xs h-6">
<ExternalLink className="h-3 w-3" />
</Button>
</div>
<div className="flex items-center gap-2 p-2 bg-gray-50 rounded">
<Youtube className="h-4 w-4 text-red-500" />
<div className="flex-1">
<p className="font-medium">MIT Leadership Center</p>
<p className="text-xs text-gray-600">Digital Leadership Strategies</p>
</div>
<Button size="sm" variant="ghost" className="text-xs h-6">
<ExternalLink className="h-3 w-3" />
</Button>
</div>
</div>
</div>
</div>
</ScrollArea>
</TabsContent>
<TabsContent value="feedback" className="flex-1 m-0">
<div className="flex flex-col h-full">
<div className="px-6 py-4 border-b">
<h3 className="font-medium mb-2">Get AI Feedback</h3>
<p className="text-sm text-gray-600">
Upload your notes, case studies, or learning reflections for personalized feedback.
</p>
</div>
<div className="flex-1 px-6 py-4">
<div className="space-y-4">
<div>
<label className="text-sm font-medium mb-2 block">Your Notes/Case Study</label>
<Textarea
placeholder="Paste your notes, case study, or reflection here..."
value={notesFeedback}
onChange={(e) => setNotesFeedback(e.target.value)}
className="min-h-[200px] bg-white"
/>
</div>
<div className="flex gap-2">
<Button
onClick={handleNotesFeedback}
disabled={!notesFeedback.trim() || isTyping}
className="flex-1 bg-[var(--color-brand-primary)] hover:bg-[var(--color-brand-primary)]/90 text-white"
>
Get AI Feedback
</Button>
<Button
variant="outline"
size="icon"
title="Upload file"
>
<Upload className="h-4 w-4" />
</Button>
</div>
<div className="text-xs text-gray-600 space-y-1">
<p> Get suggestions for improvement</p>
<p> Receive learning resource recommendations</p>
<p> Connect insights to KLC framework</p>
</div>
</div>
</div>
</div>
</TabsContent>
</Tabs>
</div>
</SheetContent>
</Sheet>
</>
);
}

325
src/components/AppShell.tsx Normal file
View File

@@ -0,0 +1,325 @@
import React, { useState, useContext, createContext, useEffect } from 'react';
import {
Bell,
Search,
Menu,
ChevronLeft,
ChevronRight,
BookOpen,
Library,
Award,
Trophy,
StickyNote,
Bot,
PenTool,
Settings,
User,
LogOut,
Building2,
UserCircle2,
ChevronDown,
MessageCircle,
GraduationCap
} from 'lucide-react';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Badge } from './ui/badge';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger
} from './ui/dropdown-menu';
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
import { Separator } from './ui/separator';
import { ImageWithFallback } from './figma/ImageWithFallback';
import { GlobalSearch } from './GlobalSearch';
import { ProfileSwitchDropdown } from './ProfileSwitchDropdown';
import klcLogo from 'figma:asset/1e150e43f238df3e08fcbf5d8f4899c233264e9f.png';
// Persona and user context
interface User {
id: string;
firstName: string;
lastName: string;
email: string;
persona: 'corporate' | 'individual';
orgName?: string;
canSwitchMode?: boolean;
canSwitchAccount?: boolean;
avatar?: string;
}
interface AppContextType {
user: User;
currentMode: 'corporate' | 'hr' | 'individual';
currentAccount: 'corporate' | 'personal';
sidebarCollapsed: boolean;
setSidebarCollapsed: (collapsed: boolean) => void;
switchMode: (mode: 'corporate' | 'hr') => void;
switchAccount: (account: 'corporate' | 'personal') => void;
}
const AppContext = createContext<AppContextType | null>(null);
export const useAppContext = () => {
const context = useContext(AppContext);
if (!context) throw new Error('useAppContext must be used within AppShell');
return context;
};
interface AppShellProps {
children: React.ReactNode;
currentPage: string;
user: User;
onNavigate?: (page: string) => void;
}
export function AppShell({ children, currentPage, user, onNavigate }: AppShellProps) {
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [currentMode, setCurrentMode] = useState<'corporate' | 'hr' | 'individual'>(
user.persona === 'corporate' ? 'corporate' : 'individual'
);
const [currentAccount, setCurrentAccount] = useState<'corporate' | 'personal'>('corporate');
const [searchQuery, setSearchQuery] = useState('');
const [searchOpen, setSearchOpen] = useState(false);
const switchMode = (mode: 'corporate' | 'hr') => {
setCurrentMode(mode);
};
const switchAccount = (account: 'corporate' | 'personal') => {
setCurrentAccount(account);
};
const contextValue: AppContextType = {
user,
currentMode,
currentAccount,
sidebarCollapsed,
setSidebarCollapsed,
switchMode,
switchAccount
};
const navItems = [
{
id: 'my-courses',
label: 'My Courses',
icon: GraduationCap,
href: '/my-courses',
available: ['corporate', 'individual']
},
{
id: 'leaderboard',
label: 'Leaderboard',
icon: Trophy,
href: '/leaderboard',
available: ['corporate', 'individual']
},
{
id: 'reports-certificates',
label: 'Reports & Certificates',
icon: Award,
href: '/reports-certificates',
available: ['corporate', 'individual']
},
{
id: 'notes',
label: 'Notes',
icon: StickyNote,
href: '/notes',
available: ['corporate', 'individual']
},
{
id: 'blog',
label: 'Blog',
icon: PenTool,
href: '/blog',
available: ['corporate', 'individual']
},
{
id: 'forums',
label: 'Discussion Forums',
icon: MessageCircle,
href: '/forums',
available: ['corporate', 'individual']
},
{
id: 'settings',
label: 'Settings',
icon: Settings,
href: '/settings',
available: ['corporate', 'individual']
}
];
const handleSearch = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === '/') {
e.preventDefault();
setSearchOpen(true);
}
};
const handleSearchClick = () => {
setSearchOpen(true);
};
// Global keyboard shortcut
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === '/' && !searchOpen) {
e.preventDefault();
setSearchOpen(true);
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [searchOpen]);
return (
<AppContext.Provider value={contextValue}>
<div className="min-h-screen bg-white">
{/* Top Bar */}
<header className="h-16 border-b border-border bg-white flex items-center px-4 sticky top-0 z-50">
<div className="flex items-center gap-4 flex-1">
{/* Logo */}
<div className="flex items-center gap-3">
<ImageWithFallback
src={klcLogo}
alt="Kautilya Leadership Centre (KLC)"
className="h-8 w-auto"
/>
</div>
{/* Universal Search */}
<div className="relative max-w-md flex-1">
<Button
variant="outline"
className="w-full justify-start text-left font-normal bg-white"
onClick={handleSearchClick}
>
<Search className="mr-2 h-4 w-4 text-muted-foreground" />
<span className="text-muted-foreground">Search... (Press '/' to focus)</span>
<kbd className="ml-auto pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
/
</kbd>
</Button>
</div>
</div>
<div className="flex items-center gap-4">
{/* Notifications */}
<Button
variant="ghost"
size="icon"
className="relative"
onClick={() => onNavigate?.('notifications')}
>
<Bell className="h-5 w-5" />
<Badge className="absolute -top-1 -right-1 h-5 w-5 flex items-center justify-center p-0 text-xs bg-[var(--color-brand-accent)] text-[var(--color-brand-black)]">
3
</Badge>
</Button>
{/* User Menu */}
<ProfileSwitchDropdown
user={user}
currentMode={currentMode as 'corporate' | 'hr'}
currentAccount={currentAccount}
onModeSwitch={switchMode}
onAccountSwitch={switchAccount}
onNavigate={onNavigate || (() => {})}
onSignOut={() => {
// Handle sign out logic here
console.log('User signed out');
}}
/>
</div>
</header>
<div className="flex">
{/* Sidebar */}
<aside
className={`${
sidebarCollapsed ? 'w-16' : 'w-64'
} transition-all duration-200 border-r border-border bg-sidebar h-[calc(100vh-4rem)] sticky top-16 overflow-hidden`}
>
{/* Sidebar Toggle */}
<div className="flex items-center justify-between p-4 border-b border-sidebar-border">
{!sidebarCollapsed && (
<span className="font-medium text-sidebar-foreground">Navigation</span>
)}
<Button
variant="ghost"
size="icon"
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
className="h-8 w-8"
>
{sidebarCollapsed ? (
<ChevronRight className="h-4 w-4" />
) : (
<ChevronLeft className="h-4 w-4" />
)}
</Button>
</div>
{/* Navigation Items */}
<nav className="p-2">
{navItems.map((item) => {
const Icon = item.icon;
const isActive = currentPage === item.id;
return (
<div key={item.id} className="mb-1">
<Button
variant="ghost"
className={`w-full justify-start h-10 px-3 transition-colors ${
sidebarCollapsed ? 'px-2' : 'px-3'
} ${
isActive
? 'bg-primary/10 text-primary border border-primary/20 hover:bg-primary/15'
: 'hover:bg-muted'
}`}
title={sidebarCollapsed ? item.label : undefined}
onClick={() => onNavigate?.(item.id)}
>
<Icon className={`h-4 w-4 ${sidebarCollapsed ? '' : 'mr-3'}`} />
{!sidebarCollapsed && (
<span className="flex-1 text-left">{item.label}</span>
)}
{!sidebarCollapsed && item.badge && (
<Badge variant="secondary" className="ml-auto text-xs">
{item.badge}
</Badge>
)}
</Button>
</div>
);
})}
</nav>
</aside>
{/* Main Content */}
<main className="flex-1 overflow-x-hidden">
{children}
</main>
</div>
{/* Global Search Modal */}
<GlobalSearch
isOpen={searchOpen}
onClose={() => setSearchOpen(false)}
onOpenAIMentor={() => {
setSearchOpen(false);
// This would trigger AI Mentor opening
}}
/>
</div>
</AppContext.Provider>
);
}

590
src/components/Blog.tsx Normal file
View File

@@ -0,0 +1,590 @@
import React, { useState } from 'react';
import {
Plus,
Edit,
Eye,
Trash2,
Clock,
CheckCircle,
XCircle,
AlertCircle,
Upload,
Image,
Video,
BookOpen,
TrendingUp,
Users
} from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Textarea } from './ui/textarea';
import { Badge } from './ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger
} from './ui/dialog';
import { Label } from './ui/label';
import { useAppContext } from './AppShell';
// Mock blog submissions data
const mockBlogData = [
{
id: '1',
title: 'The Future of Remote Leadership: Lessons from the Pandemic',
excerpt: 'Exploring how the COVID-19 pandemic transformed leadership approaches and what lessons we can carry forward...',
content: 'Full article content would be here...',
status: 'published',
submittedAt: '2024-08-15T10:00:00Z',
publishedAt: '2024-08-18T14:00:00Z',
views: 1247,
tags: ['Remote Leadership', 'Digital Transformation', 'Crisis Management']
},
{
id: '2',
title: 'Building Resilient Teams in Uncertain Times',
excerpt: 'Strategies for developing team resilience and maintaining high performance during periods of uncertainty...',
content: 'Full article content would be here...',
status: 'under-review',
submittedAt: '2024-09-01T15:30:00Z',
tags: ['Team Building', 'Resilience', 'Leadership']
},
{
id: '3',
title: 'Emotional Intelligence in Modern Leadership',
excerpt: 'Why EQ matters more than ever in today\'s complex business environment and how to develop it...',
content: 'Full article content would be here...',
status: 'rejected',
submittedAt: '2024-08-20T09:15:00Z',
rejectedAt: '2024-08-22T11:00:00Z',
rejectionReason: 'Content needs more original research and citations. Please add specific examples and data to support your claims.',
tags: ['Emotional Intelligence', 'Leadership Development']
},
{
id: '4',
title: 'Sustainable Leadership Practices',
excerpt: 'Draft exploring how leaders can integrate sustainability into their decision-making processes...',
content: 'Full article content would be here...',
status: 'draft',
updatedAt: '2024-09-02T16:45:00Z',
tags: ['Sustainability', 'Strategic Leadership']
}
];
export function Blog() {
const { user } = useAppContext();
const [activeTab, setActiveTab] = useState('my-posts');
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingPost, setEditingPost] = useState<any>(null);
const [formData, setFormData] = useState({
title: '',
excerpt: '',
content: '',
tags: ''
});
const getStatusIcon = (status: string) => {
switch (status) {
case 'published': return CheckCircle;
case 'under-review': return Clock;
case 'rejected': return XCircle;
case 'draft': return Edit;
default: return AlertCircle;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'published': return 'bg-green-100 text-green-800';
case 'under-review': return 'bg-blue-100 text-blue-800';
case 'rejected': return 'bg-red-100 text-red-800';
case 'draft': return 'bg-gray-100 text-gray-800';
default: return 'bg-gray-100 text-gray-800';
}
};
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString('en-IN', {
day: '2-digit',
month: 'short',
year: 'numeric',
timeZone: 'Asia/Kolkata'
});
};
const handleSubmit = () => {
// TODO: Submit blog post
console.log('Submitting:', formData);
setIsDialogOpen(false);
setFormData({ title: '', excerpt: '', content: '', tags: '' });
setEditingPost(null);
};
const BlogPostCard = ({ post }: { post: any }) => {
const StatusIcon = getStatusIcon(post.status);
return (
<Card className="group hover:shadow-lg transition-all duration-300 border border-gray-200 bg-white">
<CardContent className="p-6">
<div className="space-y-5">
{/* Header */}
<div className="flex items-start justify-between gap-4">
<div className="flex items-start gap-3 flex-1">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-[#04045B] to-[#030344] flex items-center justify-center flex-shrink-0">
<BookOpen className="h-5 w-5 text-white" />
</div>
<div className="space-y-2 flex-1">
<div className="flex items-center gap-2">
<StatusIcon className={`h-4 w-4 ${
post.status === 'published' ? 'text-green-600' :
post.status === 'under-review' ? 'text-blue-600' :
post.status === 'rejected' ? 'text-red-600' : 'text-gray-600'
}`} />
<Badge className={`capitalize font-medium ${getStatusColor(post.status)} border-0`}>
{post.status.replace('-', ' ')}
</Badge>
</div>
<h3 className="text-lg font-semibold text-gray-900 leading-tight">{post.title}</h3>
</div>
</div>
{/* Action Buttons */}
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => {
setEditingPost(post);
setFormData({
title: post.title,
excerpt: post.excerpt,
content: post.content,
tags: post.tags.join(', ')
});
setIsDialogOpen(true);
}}
className="gap-2"
>
<Edit className="h-4 w-4" />
Edit
</Button>
{post.status === 'published' && (
<Button
size="sm"
variant="outline"
className="gap-2"
>
<Eye className="h-4 w-4" />
View
</Button>
)}
<Button
size="sm"
variant="outline"
className="gap-2 text-red-600 hover:text-red-700 hover:border-red-200 hover:bg-red-50"
>
<Trash2 className="h-4 w-4" />
Delete
</Button>
</div>
</div>
{/* Excerpt */}
<div className="bg-gray-50 rounded-lg p-4 border border-gray-100">
<p className="text-sm text-gray-700 leading-relaxed line-clamp-2">
{post.excerpt}
</p>
</div>
{/* Tags */}
{post.tags && post.tags.length > 0 && (
<div className="flex flex-wrap gap-2">
{post.tags.map((tag: string) => (
<Badge
key={tag}
variant="outline"
className="text-xs font-medium bg-[#F8C301]/10 text-[#04045B] border-[#F8C301]/30"
>
#{tag}
</Badge>
))}
</div>
)}
{/* Metadata */}
<div className="flex items-center gap-4 text-sm text-gray-600 bg-blue-50 p-3 rounded-lg border border-blue-100">
<div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-gray-400" />
<span>Submitted {formatDate(post.submittedAt)}</span>
</div>
{post.publishedAt && (
<>
<span className="text-gray-400"></span>
<div className="flex items-center gap-2">
<CheckCircle className="h-4 w-4 text-green-500" />
<span>Published {formatDate(post.publishedAt)}</span>
</div>
</>
)}
{post.views && (
<>
<span className="text-gray-400"></span>
<div className="flex items-center gap-2">
<Eye className="h-4 w-4 text-blue-500" />
<span>{post.views} views</span>
</div>
</>
)}
</div>
{/* Rejection Reason */}
{post.rejectionReason && (
<div className="bg-red-50 border border-red-200 p-4 rounded-lg">
<div className="flex items-start gap-3">
<XCircle className="h-4 w-4 text-red-600 mt-0.5 flex-shrink-0" />
<div className="space-y-1">
<p className="text-sm font-semibold text-red-800">Rejection Reason:</p>
<p className="text-sm text-red-700 leading-relaxed">{post.rejectionReason}</p>
</div>
</div>
</div>
)}
</div>
</CardContent>
</Card>
);
};
return (
<div className="p-6 space-y-8 bg-white min-h-screen">
{/* Header */}
<div className="flex items-start justify-between">
<div className="space-y-2">
<h1 className="text-3xl font-semibold text-gray-900">Leadership Blog</h1>
<p className="text-gray-600">
Share your leadership insights and experiences with the community
</p>
</div>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button
onClick={() => {
setEditingPost(null);
setFormData({ title: '', excerpt: '', content: '', tags: '' });
}}
className="bg-[#04045B] hover:bg-[#030344] text-white px-6 py-2.5"
>
<Plus className="h-4 w-4 mr-2" />
Write Article
</Button>
</DialogTrigger>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>
{editingPost ? 'Edit Article' : 'Write New Article'}
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="title">Title</Label>
<Input
id="title"
placeholder="Enter article title..."
value={formData.title}
onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))}
/>
</div>
<div className="space-y-2">
<Label htmlFor="excerpt">Excerpt</Label>
<Textarea
id="excerpt"
placeholder="Brief summary of your article..."
className="h-20"
value={formData.excerpt}
onChange={(e) => setFormData(prev => ({ ...prev, excerpt: e.target.value }))}
/>
</div>
<div className="space-y-2">
<Label htmlFor="content">Content</Label>
<div className="border rounded-lg">
<div className="flex items-center gap-2 p-2 border-b bg-muted">
<Button size="sm" variant="ghost">
<Image className="h-4 w-4" />
</Button>
<Button size="sm" variant="ghost">
<Video className="h-4 w-4" />
</Button>
<Button size="sm" variant="ghost">
<Upload className="h-4 w-4" />
</Button>
<span className="text-xs text-muted-foreground ml-auto">
Markdown supported
</span>
</div>
<Textarea
id="content"
placeholder="Write your article content here. You can use markdown formatting and embed images/videos..."
className="min-h-[300px] border-0 resize-none focus-visible:ring-0"
value={formData.content}
onChange={(e) => setFormData(prev => ({ ...prev, content: e.target.value }))}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="tags">Tags</Label>
<Input
id="tags"
placeholder="Leadership, Strategy, Team Building (comma separated)"
value={formData.tags}
onChange={(e) => setFormData(prev => ({ ...prev, tags: e.target.value }))}
/>
</div>
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
<h4 className="font-medium text-blue-800 mb-2">Submission Guidelines</h4>
<ul className="text-sm text-blue-700 space-y-1">
<li> Articles are reviewed by our editorial team before publication</li>
<li> Focus on original insights and practical leadership advice</li>
<li> Include specific examples and actionable takeaways</li>
<li> Maintain professional tone and cite sources when needed</li>
</ul>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>
Cancel
</Button>
<Button variant="outline">
Save Draft
</Button>
<Button onClick={handleSubmit}>
{editingPost ? 'Update Article' : 'Submit for Review'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<Card className="hover:shadow-lg transition-all duration-300 border-0 shadow-sm bg-gradient-to-br from-white to-green-50/30">
<CardContent className="p-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-green-500 to-green-600 flex items-center justify-center text-white shadow-lg">
<CheckCircle className="h-6 w-6" />
</div>
<div>
<p className="text-2xl font-semibold text-gray-900">{mockBlogData.filter(p => p.status === 'published').length}</p>
<p className="text-sm text-gray-600 font-medium">Published</p>
</div>
</div>
</CardContent>
</Card>
<Card className="hover:shadow-lg transition-all duration-300 border-0 shadow-sm bg-gradient-to-br from-white to-blue-50/30">
<CardContent className="p-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center text-white shadow-lg">
<Clock className="h-6 w-6" />
</div>
<div>
<p className="text-2xl font-semibold text-gray-900">{mockBlogData.filter(p => p.status === 'under-review').length}</p>
<p className="text-sm text-gray-600 font-medium">Under Review</p>
</div>
</div>
</CardContent>
</Card>
<Card className="hover:shadow-lg transition-all duration-300 border-0 shadow-sm bg-gradient-to-br from-white to-gray-50/30">
<CardContent className="p-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-gray-500 to-gray-600 flex items-center justify-center text-white shadow-lg">
<Edit className="h-6 w-6" />
</div>
<div>
<p className="text-2xl font-semibold text-gray-900">{mockBlogData.filter(p => p.status === 'draft').length}</p>
<p className="text-sm text-gray-600 font-medium">Drafts</p>
</div>
</div>
</CardContent>
</Card>
<Card className="hover:shadow-lg transition-all duration-300 border-0 shadow-sm bg-gradient-to-br from-white to-yellow-50/30">
<CardContent className="p-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-[#F8C301] to-yellow-500 flex items-center justify-center text-[#04045B] shadow-lg">
<TrendingUp className="h-6 w-6" />
</div>
<div>
<p className="text-2xl font-semibold text-gray-900">
{mockBlogData.reduce((sum, post) => sum + (post.views || 0), 0)}
</p>
<p className="text-sm text-gray-600 font-medium">Total Views</p>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-8">
<TabsList className="grid w-full max-w-lg grid-cols-2 bg-gray-50/80 border border-gray-200 p-1 rounded-xl shadow-sm h-auto">
<TabsTrigger
value="my-posts"
className="flex items-center gap-3 px-6 py-3 rounded-lg font-medium transition-all data-[state=active]:bg-white data-[state=active]:text-[#04045B] data-[state=active]:shadow-md data-[state=active]:border data-[state=active]:border-[#04045B]/10"
>
<BookOpen className="h-4 w-4" />
My Submissions
</TabsTrigger>
<TabsTrigger
value="guidelines"
className="flex items-center gap-3 px-6 py-3 rounded-lg font-medium transition-all data-[state=active]:bg-white data-[state=active]:text-[#04045B] data-[state=active]:shadow-md data-[state=active]:border data-[state=active]:border-[#04045B]/10"
>
<Users className="h-4 w-4" />
Guidelines
</TabsTrigger>
</TabsList>
<TabsContent value="my-posts" className="space-y-6">
{mockBlogData.length === 0 ? (
<Card className="border-0 shadow-lg">
<CardContent className="p-12 text-center">
<div className="w-20 h-20 mx-auto mb-6 bg-gradient-to-br from-[#04045B]/10 to-blue-50 rounded-full flex items-center justify-center">
<BookOpen className="h-10 w-10 text-[#04045B]" />
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">No articles yet</h3>
<p className="text-gray-600 mb-6 max-w-md mx-auto">
Start sharing your leadership insights with the community
</p>
<Button className="bg-[#04045B] hover:bg-[#030344] text-white px-6 py-2.5">
<Plus className="h-4 w-4 mr-2" />
Write Your First Article
</Button>
</CardContent>
</Card>
) : (
<div className="space-y-5">
{mockBlogData.map((post) => (
<BlogPostCard key={post.id} post={post} />
))}
</div>
)}
</TabsContent>
<TabsContent value="guidelines">
<Card className="border-0 shadow-lg">
<CardHeader className="pb-6">
<CardTitle className="text-xl font-semibold text-gray-900 flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-[#04045B] to-[#030344] flex items-center justify-center">
<Users className="h-4 w-4 text-white" />
</div>
Article Submission Guidelines
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid gap-6">
<div className="bg-white p-6 rounded-lg border border-gray-100 shadow-sm">
<div className="flex items-center gap-3 mb-4">
<div className="w-2 h-2 bg-[#04045B] rounded-full"></div>
<h3 className="text-lg font-semibold text-gray-900">Content Requirements</h3>
</div>
<ul className="space-y-3 text-sm text-gray-700">
<li className="flex items-start gap-3">
<span className="w-1.5 h-1.5 bg-[#F8C301] rounded-full mt-2 flex-shrink-0" />
<span className="font-medium">Minimum 800 words, maximum 3000 words</span>
</li>
<li className="flex items-start gap-3">
<span className="w-1.5 h-1.5 bg-[#F8C301] rounded-full mt-2 flex-shrink-0" />
<span className="font-medium">Original content with practical leadership insights</span>
</li>
<li className="flex items-start gap-3">
<span className="w-1.5 h-1.5 bg-[#F8C301] rounded-full mt-2 flex-shrink-0" />
<span className="font-medium">Include specific examples and actionable advice</span>
</li>
<li className="flex items-start gap-3">
<span className="w-1.5 h-1.5 bg-[#F8C301] rounded-full mt-2 flex-shrink-0" />
<span className="font-medium">Cite sources and provide references where applicable</span>
</li>
</ul>
</div>
<div className="bg-white p-6 rounded-lg border border-gray-100 shadow-sm">
<div className="flex items-center gap-3 mb-4">
<div className="w-2 h-2 bg-[#04045B] rounded-full"></div>
<h3 className="text-lg font-semibold text-gray-900">Review Process</h3>
</div>
<ul className="space-y-3 text-sm text-gray-700">
<li className="flex items-start gap-3">
<span className="w-1.5 h-1.5 bg-[#F8C301] rounded-full mt-2 flex-shrink-0" />
<span className="font-medium">Articles are reviewed within 5-7 business days</span>
</li>
<li className="flex items-start gap-3">
<span className="w-1.5 h-1.5 bg-[#F8C301] rounded-full mt-2 flex-shrink-0" />
<span className="font-medium">Editorial team may suggest revisions</span>
</li>
<li className="flex items-start gap-3">
<span className="w-1.5 h-1.5 bg-[#F8C301] rounded-full mt-2 flex-shrink-0" />
<span className="font-medium">Approved articles are published on the KLC website</span>
</li>
<li className="flex items-start gap-3">
<span className="w-1.5 h-1.5 bg-[#F8C301] rounded-full mt-2 flex-shrink-0" />
<span className="font-medium">Authors are notified of publication via email</span>
</li>
</ul>
</div>
<div className="bg-white p-6 rounded-lg border border-gray-100 shadow-sm">
<div className="flex items-center gap-3 mb-4">
<div className="w-2 h-2 bg-[#04045B] rounded-full"></div>
<h3 className="text-lg font-semibold text-gray-900">Topics We're Looking For</h3>
</div>
<ul className="space-y-3 text-sm text-gray-700">
<li className="flex items-start gap-3">
<span className="w-1.5 h-1.5 bg-[#F8C301] rounded-full mt-2 flex-shrink-0" />
<span className="font-medium">Strategic leadership and vision</span>
</li>
<li className="flex items-start gap-3">
<span className="w-1.5 h-1.5 bg-[#F8C301] rounded-full mt-2 flex-shrink-0" />
<span className="font-medium">Team building and management</span>
</li>
<li className="flex items-start gap-3">
<span className="w-1.5 h-1.5 bg-[#F8C301] rounded-full mt-2 flex-shrink-0" />
<span className="font-medium">Change management and innovation</span>
</li>
<li className="flex items-start gap-3">
<span className="w-1.5 h-1.5 bg-[#F8C301] rounded-full mt-2 flex-shrink-0" />
<span className="font-medium">Digital transformation leadership</span>
</li>
<li className="flex items-start gap-3">
<span className="w-1.5 h-1.5 bg-[#F8C301] rounded-full mt-2 flex-shrink-0" />
<span className="font-medium">Emotional intelligence and soft skills</span>
</li>
<li className="flex items-start gap-3">
<span className="w-1.5 h-1.5 bg-[#F8C301] rounded-full mt-2 flex-shrink-0" />
<span className="font-medium">Industry-specific leadership challenges</span>
</li>
</ul>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
}

View File

@@ -0,0 +1,881 @@
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
import { Button } from './ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
import { Progress } from './ui/progress';
import { LeadershipProfiler } from './LeadershipProfiler';
import {
ChevronRight,
ChevronDown,
ChevronUp,
CheckCircle,
Check,
Play,
Pause,
Volume2,
Maximize,
Calendar,
Clock,
BookOpen,
FileText,
Video,
Target,
User,
MessageSquare,
StickyNote,
Info,
BarChart3,
Users,
Globe,
GraduationCap,
Award,
Smartphone
} from 'lucide-react';
// Data interfaces
interface CourseData {
id: string;
title: string;
subtitle: string;
week: number;
totalWeeks: number;
progress: number;
category: string;
description: string;
instructor: {
name: string;
title: string;
bio: string;
avatar: string;
};
learningOutcomes: string[];
modules: Module[];
thumbnail: string;
}
interface Module {
id: string;
title: string;
lessons: Lesson[];
isExpanded: boolean;
isCompleted: boolean;
}
interface Lesson {
id: string;
title: string;
duration: string;
isCompleted: boolean;
isActive: boolean;
type: 'video' | 'quiz' | 'reading' | 'assignment' | 'profiler' | 'lab';
videoUrl?: string;
content: string;
subtitle?: string;
}
// Mock course database
const courseDatabase: Record<string, CourseData> = {
'course-1': {
id: 'course-1',
title: 'Building High-Performance Teams',
subtitle: 'Understanding Communication Styles',
week: 2,
totalWeeks: 6,
progress: 67,
category: 'Leadership Development',
description: 'Discover the secrets of creating and managing teams that consistently deliver exceptional results. Learn team dynamics, conflict resolution, motivation strategies, and performance optimization techniques used by world-class leaders.',
instructor: {
name: 'Dr. Emily Chen',
title: 'Organizational Psychology Expert',
bio: 'Dr. Chen has over 15 years of experience in organizational psychology and team development, having worked with Fortune 500 companies to build high-performing teams.',
avatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=100&h=100&fit=crop'
},
learningOutcomes: [
'Design and structure high-performance team environments that maximize productivity',
'Master team dynamics and psychological safety principles for optimal collaboration',
'Implement effective conflict resolution and performance management strategies',
'Develop leadership skills that inspire and motivate team members to achieve excellence'
],
thumbnail: 'https://images.unsplash.com/photo-1573164713714-d95e436ab8d6?w=800&h=450&fit=crop',
modules: [
{
id: 'module-1',
title: 'Understanding Communication Styles',
isExpanded: true,
isCompleted: true,
lessons: [
{
id: 'lesson-1',
title: 'Situational Communication',
duration: '4 min',
isCompleted: true,
isActive: false,
type: 'video',
content: 'Learn the fundamentals of situational communication.'
},
{
id: 'lesson-2',
title: 'Communication Styles',
duration: '5 min',
isCompleted: false,
isActive: true,
type: 'video',
content: 'Understanding different communication styles in practice.',
subtitle: 'Currently watching'
},
{
id: 'lesson-profiler-1',
title: 'Leadership Profiler',
duration: '',
isCompleted: false,
isActive: false,
type: 'profiler',
content: 'Interactive leadership assessment to identify your leadership style and strengths.'
},
{
id: 'lesson-profiler-2',
title: 'Assessment',
duration: '15 min',
isCompleted: false,
isActive: false,
type: 'profiler',
content: 'Interactive profiler to assess your learning and application of communication styles.',
subtitle: 'Interactive profiler'
},
{
id: 'lesson-lab-1',
title: 'Hands-on Lab: Assess your Communication Style',
duration: '15 min',
isCompleted: false,
isActive: false,
type: 'lab',
content: 'Practical lab exercise to assess and practice your communication style.'
}
]
},
{
id: 'module-2',
title: 'Communication Challenges and Styles',
isExpanded: false,
isCompleted: false,
lessons: [
{
id: 'lesson-5',
title: 'Expert Viewpoints: Communication Styles',
duration: '12 min',
isCompleted: false,
isActive: false,
type: 'video',
content: 'Expert insights on effective communication styles.'
},
{
id: 'lesson-6',
title: 'Trust and Communication Styles',
duration: '8 min',
isCompleted: false,
isActive: false,
type: 'video',
content: 'Building trust through effective communication.'
}
]
},
{
id: 'module-3',
title: 'Communication Styles and the Team',
isExpanded: false,
isCompleted: false,
lessons: [
{
id: 'lesson-7',
title: 'Team Communication Dynamics',
duration: '10 min',
isCompleted: false,
isActive: false,
type: 'video',
content: 'Understanding team communication patterns.'
},
{
id: 'lesson-8',
title: 'Building Effective Team Communication',
duration: '12 min',
isCompleted: false,
isActive: false,
type: 'video',
content: 'Strategies for building effective team communication.'
}
]
}
]
},
'course-2': {
id: 'course-2',
title: 'Workshop: Team Building Essentials',
subtitle: 'Hands-on Team Development',
week: 1,
totalWeeks: 4,
progress: 0,
category: 'Team Management',
description: 'Hands-on workshop for building high-performance teams. Learn practical techniques for team formation, collaboration, and performance optimization.',
instructor: {
name: 'Michael Rodriguez',
title: 'Team Development Specialist',
bio: 'Michael is a certified team development facilitator with 12+ years of experience helping organizations build cohesive, high-performing teams.',
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=100&h=100&fit=crop'
},
learningOutcomes: [
'Facilitate effective team building exercises and activities',
'Identify and resolve common team conflicts and dynamics',
'Create psychological safety and trust within team environments',
'Measure and improve team performance using proven frameworks'
],
thumbnail: 'https://images.unsplash.com/photo-1522202176988-66273c2fd55f?w=800&h=450&fit=crop',
modules: [
{
id: 'team-foundations',
title: 'Team Formation Fundamentals',
isExpanded: true,
isCompleted: false,
lessons: [
{
id: 'tf-1',
title: 'Stages of Team Development',
duration: '12 min',
isCompleted: false,
isActive: true,
type: 'video',
content: 'Understanding the five stages of team development.'
},
{
id: 'tf-2',
title: 'Team Roles and Responsibilities',
duration: '15 min',
isCompleted: false,
isActive: false,
type: 'video',
content: 'Defining clear roles and responsibilities for team success.'
}
]
}
]
},
'prog-1': {
id: 'prog-1',
title: 'Leadership Excellence Program',
subtitle: 'Executive Leadership Track',
week: 3,
totalWeeks: 12,
progress: 65,
category: 'Leadership Development',
description: 'Comprehensive leadership development program for senior executives. Master advanced leadership techniques and strategic thinking.',
instructor: {
name: 'Dr. Patricia Wilson',
title: 'Executive Leadership Expert',
bio: 'Dr. Wilson is a renowned leadership consultant with 20+ years of experience developing C-level executives.',
avatar: 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=100&h=100&fit=crop'
},
learningOutcomes: [
'Develop advanced strategic leadership capabilities',
'Master executive decision-making frameworks',
'Build and lead high-performance leadership teams',
'Execute organizational transformation initiatives'
],
thumbnail: 'https://images.unsplash.com/photo-1552664730-d307ca884978?w=800&h=450&fit=crop',
modules: [
{
id: 'leadership-foundations',
title: 'Leadership Foundations',
isExpanded: true,
isCompleted: true,
lessons: [
{
id: 'lf-1',
title: 'Leadership Principles',
duration: '20 min',
isCompleted: true,
isActive: false,
type: 'video',
content: 'Core principles of effective leadership.'
},
{
id: 'lf-2',
title: 'Strategic Thinking',
duration: '35 min',
isCompleted: false,
isActive: true,
type: 'video',
content: 'Advanced frameworks for strategic thinking.'
}
]
}
]
}
};
interface CourseDetailPageProps {
courseId: string;
onNavigateBack: () => void;
}
export function CourseDetailPage({ courseId, onNavigateBack }: CourseDetailPageProps) {
const [courseData] = useState<CourseData>(courseDatabase[courseId] || courseDatabase['course-1']);
// Find the first active lesson or default to the first lesson
const getInitialActiveLesson = () => {
for (const module of courseData.modules) {
for (const lesson of module.lessons) {
if (lesson.isActive) return lesson.id;
}
}
return courseData.modules[0]?.lessons[0]?.id || 'lesson-2';
};
const [activeLesson, setActiveLesson] = useState(getInitialActiveLesson());
const [isPlaying, setIsPlaying] = useState(false);
const [activeTab, setActiveTab] = useState('overview');
const [modules, setModules] = useState(courseData.modules);
const [showProfiler, setShowProfiler] = useState(false);
const toggleModule = (moduleId: string) => {
setModules(prev => prev.map(module =>
module.id === moduleId
? { ...module, isExpanded: !module.isExpanded }
: module
));
};
const selectLesson = (lessonId: string) => {
setActiveLesson(lessonId);
// Check if selected lesson is a profiler type
let selectedLesson = null;
for (const module of modules) {
const lesson = module.lessons.find(l => l.id === lessonId);
if (lesson) {
selectedLesson = lesson;
break;
}
}
if (selectedLesson?.type === 'profiler') {
setShowProfiler(true);
} else {
setShowProfiler(false);
}
};
const getActiveLesson = () => {
for (const module of modules) {
const lesson = module.lessons.find(l => l.id === activeLesson);
if (lesson) return lesson;
}
return modules[0].lessons[0];
};
const currentLesson = getActiveLesson();
const getLessonIcon = (lesson: Lesson) => {
switch (lesson.type) {
case 'video':
return <Video className="h-4 w-4" />;
case 'quiz':
return <Target className="h-4 w-4" />;
case 'reading':
return <BookOpen className="h-4 w-4" />;
case 'assignment':
return <FileText className="h-4 w-4" />;
case 'profiler':
return <User className="h-4 w-4" />;
case 'lab':
return <Target className="h-4 w-4" />;
default:
return <Video className="h-4 w-4" />;
}
};
const getLessonIndicatorColor = (lesson: Lesson) => {
if (lesson.isCompleted) {
return 'bg-[#21A36A]';
}
if (lesson.id === activeLesson) {
return 'bg-[#0066CC]'; // Blue for currently watching
}
if (lesson.type === 'profiler') {
return 'bg-[#8B5CF6]'; // Purple for profiler/assessment
}
return 'border-2 border-gray-300'; // Default gray
};
// Check if current active lesson is a profiler
React.useEffect(() => {
const currentLesson = getActiveLesson();
if (currentLesson?.type === 'profiler') {
setShowProfiler(true);
}
}, [activeLesson]);
// If profiler is active, show profiler interface
if (showProfiler) {
return (
<LeadershipProfiler
onBack={() => {
setShowProfiler(false);
// Stay on the course page instead of navigating back
}}
/>
);
}
return (
<div className="h-full bg-background">
{/* Full Width Headers */}
{/* Breadcrumb Navigation - Full Width */}
<div className="bg-background border-b border-border py-3">
<div className="px-6">
<nav className="flex items-center gap-2 text-[14px] text-muted-foreground">
<button
onClick={onNavigateBack}
className="hover:text-[var(--color-brand-primary)] transition-colors"
>
Library
</button>
<ChevronRight className="h-3 w-3" />
<span>{courseData.category}</span>
<ChevronRight className="h-3 w-3" />
<span className="text-foreground font-medium">{courseData.title}</span>
</nav>
</div>
</div>
{/* Course Header - Full Width */}
<div className="bg-background border-b border-border py-6">
<div className="px-6">
<div className="flex items-start justify-between">
<div className="flex-1">
<h1 className="text-[28px] font-bold leading-[34px] tracking-[-1px] mb-2 text-gray-900">
{courseData.title}
</h1>
<p className="text-[16px] text-muted-foreground mb-0">
Week {courseData.week} of {courseData.totalWeeks} {courseData.subtitle}
</p>
</div>
<div className="flex items-center gap-3">
<div className="text-right">
<p className="text-[14px] text-muted-foreground">Progress</p>
<p className="text-[24px] font-bold text-[var(--color-brand-primary)]">{courseData.progress}%</p>
</div>
<div className="w-12 h-12 rounded-full bg-[var(--color-brand-primary)] flex items-center justify-center relative">
<div
className="w-8 h-8 rounded-full bg-[var(--color-brand-accent)]"
style={{
clipPath: `inset(0 ${100 - courseData.progress}% 0 0)`
}}
/>
</div>
</div>
</div>
</div>
</div>
{/* Content Layout with Sidebar */}
<div className="flex h-full">
{/* Left Sidebar - Course Materials (300px fixed) */}
<aside className="w-[300px] flex-shrink-0 bg-white border-r border-border">
<div className="h-full overflow-y-auto">
<div className="p-6">
<h3 className="text-[16px] font-semibold text-foreground mb-6">Course Material</h3>
<div className="space-y-4">
{modules.map((module, moduleIndex) => (
<div key={module.id} className="mb-6">
<div
className="flex items-center justify-between mb-3 cursor-pointer hover:bg-gray-50 p-2 rounded-lg"
onClick={() => toggleModule(module.id)}
>
<div className="flex items-center gap-2">
{module.isCompleted && (
<div className="w-5 h-5 rounded-full bg-[#21A36A] flex items-center justify-center">
<Check className="h-3 w-3 text-white" />
</div>
)}
<h4 className="text-[16px] font-medium text-foreground">
Module {moduleIndex + 1}
</h4>
</div>
{module.isExpanded ? (
<ChevronUp className="h-4 w-4 text-muted-foreground" />
) : (
<ChevronDown className="h-4 w-4 text-muted-foreground" />
)}
</div>
<div className="ml-7 mb-2">
<p className="text-[14px] text-foreground">{module.title}</p>
</div>
{module.isExpanded && (
<div className="space-y-2 ml-4">
{module.lessons.map((lesson) => (
<div
key={lesson.id}
className={`flex items-center gap-3 p-3 rounded-lg cursor-pointer transition-colors ${
lesson.id === activeLesson
? 'bg-[#E3F2FD] border-l-3 border-[#0066CC]'
: 'hover:bg-gray-50'
}`}
onClick={() => selectLesson(lesson.id)}
>
<div className={`w-6 h-6 rounded-full flex items-center justify-center ${getLessonIndicatorColor(lesson)}`}>
{lesson.isCompleted ? (
<Check className="h-3 w-3 text-white" />
) : lesson.id === activeLesson ? (
<Play className="h-3 w-3 text-white" />
) : lesson.type === 'profiler' ? (
<div className="w-2 h-2 rounded-full bg-white" />
) : (
<div className="w-2 h-2 rounded-full bg-gray-300" />
)}
</div>
<div className="flex-1">
<p className={`text-[14px] ${
lesson.id === activeLesson
? 'font-medium text-[#0066CC]'
: 'text-foreground'
}`}>
{lesson.title}
</p>
<div className="flex items-center gap-1 text-[12px] text-muted-foreground">
{lesson.duration && (
<>
<span>{lesson.duration}</span>
{lesson.subtitle && <span> {lesson.subtitle}</span>}
</>
)}
{!lesson.duration && lesson.subtitle && (
<span>{lesson.subtitle}</span>
)}
</div>
</div>
</div>
))}
</div>
)}
</div>
))}
{/* Additional Course Sections */}
<div className="mt-8 pt-4 border-t border-border space-y-3">
<div className="flex items-center gap-3 p-3 rounded-lg cursor-pointer hover:bg-gray-50 transition-colors">
<div className="w-6 h-6 flex items-center justify-center">
<MessageSquare className="h-4 w-4 text-muted-foreground" />
</div>
<span className="text-[14px] text-foreground">Discussion Forums</span>
</div>
<div className="flex items-center gap-3 p-3 rounded-lg cursor-pointer hover:bg-gray-50 transition-colors">
<div className="w-6 h-6 flex items-center justify-center">
<StickyNote className="h-4 w-4 text-muted-foreground" />
</div>
<span className="text-[14px] text-foreground">Notes</span>
</div>
<div className="flex items-center gap-3 p-3 rounded-lg cursor-pointer hover:bg-gray-50 transition-colors">
<div className="w-6 h-6 flex items-center justify-center">
<Info className="h-4 w-4 text-muted-foreground" />
</div>
<span className="text-[14px] text-foreground">Course Info</span>
</div>
<div className="flex items-center justify-between p-3 rounded-lg cursor-pointer hover:bg-gray-50 transition-colors">
<div className="flex items-center gap-3">
<div className="w-6 h-6 flex items-center justify-center">
<BarChart3 className="h-4 w-4 text-muted-foreground" />
</div>
<span className="text-[14px] text-foreground">Surveys & Assessments</span>
</div>
<ChevronRight className="h-3 w-3 text-muted-foreground" />
</div>
</div>
</div>
</div>
</div>
</aside>
{/* Main Content (flexible) */}
<main className="flex-1 min-w-0">
{/* Main Content */}
<div className="p-6">
{/* Module Header */}
<div className="flex items-center gap-3 mb-6">
<div className="w-8 h-8 rounded-full bg-[var(--color-brand-primary)]/10 flex items-center justify-center">
<span className="text-[14px] font-medium text-[var(--color-brand-primary)]">1</span>
</div>
<div>
<p className="text-[14px] text-muted-foreground">Module 1</p>
<h2 className="text-[20px] font-semibold text-foreground">{modules[0]?.title || 'Leadership'}</h2>
</div>
</div>
{/* Video Player */}
<div className="relative w-full aspect-video bg-black rounded-lg overflow-hidden mb-6">
<img
src={courseData.thumbnail}
alt={courseData.title}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 flex items-center justify-center">
<button
onClick={() => setIsPlaying(!isPlaying)}
className="w-16 h-16 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center hover:bg-white/30 transition-colors"
>
{isPlaying ? (
<Pause className="h-8 w-8 text-white" />
) : (
<Play className="h-8 w-8 text-white ml-1" />
)}
</button>
</div>
<div className="absolute bottom-4 left-4 right-4 flex items-center justify-between text-white text-[14px]">
<span>0:00 / {currentLesson?.duration || '8 min'}</span>
<div className="flex items-center gap-2">
<Volume2 className="h-4 w-4" />
<Maximize className="h-4 w-4" />
</div>
</div>
</div>
{/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-2 bg-gray-100 rounded-full p-1 mb-6">
<TabsTrigger
value="overview"
className="rounded-full text-[16px] font-medium data-[state=active]:bg-white data-[state=active]:text-[var(--color-brand-primary)]"
>
Overview
</TabsTrigger>
<TabsTrigger
value="notes"
className="rounded-full text-[16px] font-medium data-[state=active]:bg-white data-[state=active]:text-[var(--color-brand-primary)]"
>
Notes
</TabsTrigger>
</TabsList>
<TabsContent value="overview">
<div className="space-y-6">
{/* About this course */}
<div>
<h3 className="text-[18px] font-semibold text-foreground mb-3">About this course</h3>
<p className="text-[14px] text-muted-foreground leading-relaxed">
{courseData.description}
</p>
</div>
{/* What you'll learn */}
<div>
<h3 className="text-[18px] font-semibold text-foreground mb-3">What you'll learn</h3>
<div className="space-y-2">
{courseData.learningOutcomes.map((outcome, index) => (
<div key={index} className="flex items-start gap-3">
<div className="w-4 h-4 rounded-full bg-[#21A36A] flex items-center justify-center flex-shrink-0 mt-0.5">
<Check className="h-2.5 w-2.5 text-white" />
</div>
<p className="text-[14px] text-foreground">
{outcome}
</p>
</div>
))}
</div>
</div>
{/* Instructor */}
<div>
<h3 className="text-[18px] font-semibold text-foreground mb-3">Instructor</h3>
<div className="flex items-start gap-3">
<img
src={courseData.instructor.avatar}
alt={courseData.instructor.name}
className="w-12 h-12 rounded-full object-cover"
/>
<div>
<h4 className="text-[16px] font-semibold text-foreground">{courseData.instructor.name}</h4>
<p className="text-[14px] text-[var(--color-brand-primary)] mb-1">{courseData.instructor.title}</p>
<p className="text-[14px] text-muted-foreground">{courseData.instructor.bio}</p>
</div>
</div>
</div>
{/* Course Summary */}
<div>
<h3 className="text-[18px] font-semibold text-foreground mb-4">Course Summary</h3>
<div className="bg-muted/30 rounded-lg p-4">
<div className="grid grid-cols-2 gap-4">
{/* Left Column */}
<div className="space-y-4">
<div className="flex items-center gap-3">
<Target className="h-4 w-4 text-muted-foreground" />
<div>
<span className="text-[14px] font-medium text-foreground">Skill Level:</span>
<span className="text-[14px] text-muted-foreground ml-1">Intermediate</span>
</div>
</div>
<div className="flex items-center gap-3">
<Globe className="h-4 w-4 text-muted-foreground" />
<div>
<span className="text-[14px] font-medium text-foreground">Languages:</span>
<span className="text-[14px] text-muted-foreground ml-1">EN, ES, DE, FR, JP</span>
</div>
</div>
<div className="flex items-center gap-3">
<BookOpen className="h-4 w-4 text-muted-foreground" />
<div>
<span className="text-[14px] font-medium text-foreground">Lectures:</span>
<span className="text-[14px] text-muted-foreground ml-1">28</span>
</div>
</div>
<div className="flex items-center gap-3">
<Award className="h-4 w-4 text-muted-foreground" />
<div>
<span className="text-[14px] font-medium text-foreground">Certification:</span>
<span className="text-[14px] text-muted-foreground ml-1">Yes</span>
</div>
</div>
</div>
{/* Right Column */}
<div className="space-y-4">
<div className="flex items-center gap-3">
<Users className="h-4 w-4 text-muted-foreground" />
<div>
<span className="text-[14px] font-medium text-foreground">Students:</span>
<span className="text-[14px] text-muted-foreground ml-1">12,350</span>
</div>
</div>
<div className="flex items-center gap-3">
<FileText className="h-4 w-4 text-muted-foreground" />
<div>
<span className="text-[14px] font-medium text-foreground">Captions:</span>
<span className="text-[14px] text-muted-foreground ml-1">Yes</span>
</div>
</div>
<div className="flex items-center gap-3">
<Clock className="h-4 w-4 text-muted-foreground" />
<div>
<span className="text-[14px] font-medium text-foreground">Duration:</span>
<span className="text-[14px] text-muted-foreground ml-1">6.5h</span>
</div>
</div>
<div className="flex items-center gap-3">
<Smartphone className="h-4 w-4 text-muted-foreground" />
<div>
<span className="text-[14px] font-medium text-foreground">App Support:</span>
<span className="text-[14px] text-muted-foreground ml-1">Yes</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="notes">
<div className="space-y-4">
<div className="text-center py-8">
<BookOpen className="h-12 w-12 text-muted-foreground mx-auto mb-3" />
<h3 className="text-[18px] font-semibold mb-2">No notes yet</h3>
<p className="text-[16px] text-muted-foreground mb-4">
Start taking notes as you watch the course to keep track of important insights and key concepts.
</p>
<Button className="text-[16px] min-h-[44px] bg-[var(--color-brand-primary)] hover:bg-[var(--color-brand-primary)]/90">
Add your first note
</Button>
</div>
</div>
</TabsContent>
</Tabs>
</div>
</main>
{/* Right Sidebar - Learning Plan (300px fixed) */}
<aside className="w-[300px] flex-shrink-0 bg-white border-l border-border">
<div className="h-full overflow-y-auto">
<div className="p-6">
<h3 className="text-[18px] font-semibold text-foreground mb-4">Learning plan</h3>
<p className="text-[14px] text-muted-foreground mb-6">
I'm committed to learning 1 day a week on Coursera.
</p>
{/* This week */}
<div className="mb-6">
<div className="flex items-center justify-between mb-4">
<h4 className="text-[16px] font-medium text-foreground">This week</h4>
<span className="text-[14px] font-medium text-muted-foreground">Week {courseData.week}</span>
</div>
<div className="mb-4">
<p className="text-[14px] text-muted-foreground mb-2">Your next deadline</p>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded-full border-2 border-[var(--color-brand-primary)] flex items-center justify-center">
<div className="w-2 h-2 bg-[var(--color-brand-primary)] rounded-full" />
</div>
<div>
<p className="text-[14px] font-medium text-foreground">Peer-graded Assignment</p>
<p className="text-[12px] text-muted-foreground">Due in 7 days</p>
</div>
</div>
</div>
</div>
{/* Course timeline */}
<div>
<h4 className="text-[16px] font-medium text-foreground mb-4">Course timeline</h4>
<div className="space-y-4">
{/* Start date */}
<div className="flex items-start gap-3">
<div className="w-5 h-5 rounded-full bg-[#21A36A] flex items-center justify-center flex-shrink-0 mt-0.5">
<Check className="h-3 w-3 text-white" />
</div>
<div>
<p className="text-[14px] font-medium text-foreground">Start date: February 6, 2024</p>
<p className="text-[12px] text-muted-foreground">Your goal deadline</p>
</div>
</div>
{/* Current assignment */}
<div className="flex items-start gap-3">
<div className="w-5 h-5 rounded-full bg-[var(--color-brand-primary)] flex items-center justify-center flex-shrink-0 mt-0.5">
<span className="text-[12px] font-medium text-white">2</span>
</div>
<div>
<p className="text-[14px] font-medium text-foreground">Peer-graded Assignment</p>
<p className="text-[12px] text-muted-foreground">Due in 3 days</p>
</div>
</div>
{/* End date */}
<div className="flex items-start gap-3">
<div className="w-5 h-5 flex items-center justify-center flex-shrink-0 mt-0.5">
<Calendar className="h-4 w-4 text-muted-foreground" />
</div>
<div>
<p className="text-[14px] font-medium text-foreground">Estimated end date: October 3, 2024</p>
</div>
</div>
</div>
</div>
</div>
</div>
</aside>
</div>
</div>
);
}

View File

@@ -0,0 +1,410 @@
import React, { useState } from 'react';
import {
Play,
Edit,
TrendingUp,
Users,
Clock,
Target,
Flame,
Star,
Award,
ChevronRight,
Calendar,
BarChart3
} from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
import { Button } from './ui/button';
import { Badge } from './ui/badge';
import { Progress } from './ui/progress';
import { Separator } from './ui/separator';
import { useAppContext } from './AppShell';
import {
PieChart,
Pie,
Cell,
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer
} from 'recharts';
// Mock data - in real app this would come from API
const mockDashboardData = {
currentCourse: {
id: '1',
title: 'Strategic Leadership Development',
moduleTitle: 'Module 3: Decision Making Framework',
lessonTitle: 'Lesson 2: Risk Assessment Strategies',
dueAt: '2024-09-10T18:00:00Z',
progressPct: 65
},
weeklyConsistency: {
start: '2024-09-01',
end: '2024-09-07',
points: [
{ date: '2024-09-01', hours: 2.5, day: 'Mon' },
{ date: '2024-09-02', hours: 1.8, day: 'Tue' },
{ date: '2024-09-03', hours: 3.2, day: 'Wed' },
{ date: '2024-09-04', hours: 0, day: 'Thu' },
{ date: '2024-09-05', hours: 2.1, day: 'Fri' },
{ date: '2024-09-06', hours: 1.5, day: 'Sat' },
{ date: '2024-09-07', hours: 0.8, day: 'Sun' }
]
},
goals: {
daysOfWeek: [1, 2, 3, 4, 5], // Mon-Fri
minutesPerDay: 120
},
streaks: {
loginStreakDays: 12,
goalAlignedDays: 8
},
badges: [
{ id: '1', name: 'Early Bird', earnedAt: '2024-08-15', iconUrl: '', desc: 'Complete 5 lessons before 9 AM' },
{ id: '2', name: 'Consistent Learner', earnedAt: '2024-08-20', iconUrl: '', desc: '7-day learning streak' },
{ id: '3', name: 'Discussion Champion', earnedAt: '2024-08-25', iconUrl: '', desc: 'Participate in 10 discussions' }
],
leaderboardGlance: {
rank: 23,
total: 156,
xp: 2450,
scope: 'org'
},
recommendations: [
{
courseId: '101',
title: 'Advanced Communication Skills',
level: 'Intermediate',
trendingSkill: 'Leadership',
currentLearnersCount: 89,
price: 299
},
{
courseId: '102',
title: 'Financial Management for Leaders',
level: 'Advanced',
trendingSkill: 'Finance',
currentLearnersCount: 67,
price: 399
},
{
courseId: '103',
title: 'Team Building Essentials',
level: 'Beginner',
trendingSkill: 'Management',
currentLearnersCount: 134,
price: 199
}
]
};
export function Dashboard() {
const { user, currentMode } = useAppContext();
const [dateRange, setDateRange] = useState({ start: '2024-09-01', end: '2024-09-07' });
const data = mockDashboardData;
// Donut chart data for module progress
const moduleProgressData = [
{ name: 'Completed', value: data.currentCourse.progressPct, fill: '#04045B' },
{ name: 'Remaining', value: 100 - data.currentCourse.progressPct, fill: '#C0C0C0' }
];
const formatTime = (dateStr: string) => {
return new Date(dateStr).toLocaleString('en-IN', {
day: '2-digit',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
timeZone: 'Asia/Kolkata'
});
};
const totalWeeklyHours = data.weeklyConsistency.points.reduce((sum, point) => sum + point.hours, 0);
return (
<div className="p-6 space-y-6">
{/* Header Greeting */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<h1 className="text-3xl">Welcome back, {user.firstName}</h1>
<span className="text-2xl">👋</span>
</div>
<div className="flex flex-wrap gap-2">
<Badge variant="secondary" className="capitalize">
{user.persona} Learner
</Badge>
{user.orgName && (
<Badge variant="outline">
{user.orgName}
</Badge>
)}
<Badge variant="outline">
Asia/Kolkata (GMT+5:30)
</Badge>
</div>
<div className="flex gap-4">
<Button>
<Play className="h-4 w-4 mr-2" />
Resume last activity
</Button>
<Button variant="outline">
<Calendar className="h-4 w-4 mr-2" />
View course timeline
</Button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
{/* Left Column */}
<div className="lg:col-span-8 space-y-6">
{/* Module Progress */}
<Card>
<CardHeader>
<CardTitle>Module Progress</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-6">
<div className="relative w-24 h-24">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={moduleProgressData}
cx="50%"
cy="50%"
innerRadius={25}
outerRadius={40}
startAngle={90}
endAngle={-270}
dataKey="value"
>
{moduleProgressData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.fill} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-sm font-medium">{data.currentCourse.progressPct}%</span>
</div>
</div>
<div className="flex-1 space-y-1">
<h3 className="font-medium">{data.currentCourse.moduleTitle}</h3>
<p className="text-sm text-muted-foreground">{data.currentCourse.lessonTitle}</p>
<p className="text-sm">
<span className="text-muted-foreground">Due: </span>
{formatTime(data.currentCourse.dueAt)}
</p>
<Button size="sm" className="mt-2">
Continue Learning
<ChevronRight className="h-4 w-4 ml-1" />
</Button>
</div>
</div>
</CardContent>
</Card>
{/* Weekly Consistency Chart */}
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>Weekly Consistency Chart</CardTitle>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<BarChart3 className="h-4 w-4" />
{totalWeeklyHours.toFixed(1)}h this week
</div>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={200}>
<BarChart data={data.weeklyConsistency.points}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="day" />
<YAxis />
<Tooltip
formatter={(value) => [`${value}h`, 'Hours Spent']}
labelFormatter={(label) => `${label}`}
/>
<Bar dataKey="hours" fill="#04045B" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
{/* Continue Learning */}
<Card>
<CardHeader>
<CardTitle>Continue Learning</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between p-4 border rounded-lg">
<div className="space-y-1">
<p className="text-sm text-muted-foreground">Strategic Leadership Programme Advanced Leadership Module 3</p>
<h3 className="font-medium">{data.currentCourse.lessonTitle}</h3>
<Progress value={data.currentCourse.progressPct} className="w-32" />
</div>
<Button>
<Play className="h-4 w-4 mr-2" />
Resume
</Button>
</div>
</CardContent>
</Card>
{/* Recommendations */}
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>Based on your activity, here are our course recommendations</CardTitle>
<Button variant="outline" size="sm">
<Edit className="h-4 w-4 mr-2" />
Edit recommendations
</Button>
</CardHeader>
<CardContent className="space-y-4">
{data.recommendations.map((course) => (
<div key={course.courseId} className="flex items-center justify-between p-4 border rounded-lg">
<div className="space-y-2">
<div className="flex items-center gap-2">
<h3 className="font-medium">{course.title}</h3>
<Badge variant="secondary">{course.level}</Badge>
<Badge variant="outline" className="bg-[#F8C301] text-black">
{course.trendingSkill}
</Badge>
</div>
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<div className="flex items-center gap-1">
<Users className="h-4 w-4" />
{course.currentLearnersCount} current learners
</div>
</div>
</div>
<div className="flex items-center gap-2">
{user.persona === 'individual' && (
<span className="font-medium">{course.price}</span>
)}
<Button>
{user.persona === 'individual' ? 'Enrol' : 'View details'}
</Button>
</div>
</div>
))}
</CardContent>
</Card>
</div>
{/* Right Column */}
<div className="lg:col-span-4 space-y-6">
{/* Learning Goals & Streaks */}
<Card>
<CardHeader>
<CardTitle>Learning Goals & Streaks</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-1">
<p className="text-sm text-muted-foreground">Weekly Goal</p>
<p className="font-medium">{data.goals.minutesPerDay} min/day, 5 days/week</p>
</div>
<Button variant="outline" size="sm">
<Edit className="h-4 w-4 mr-2" />
Edit My Goal
</Button>
</div>
<Separator />
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Flame className="h-5 w-5 text-orange-500" />
<span className="font-medium">Login Streak</span>
</div>
<span className="text-2xl font-bold">{data.streaks.loginStreakDays}</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Target className="h-5 w-5 text-green-500" />
<span className="font-medium">Goal Aligned Days</span>
</div>
<span className="text-2xl font-bold">{data.streaks.goalAlignedDays}</span>
</div>
</div>
</CardContent>
</Card>
{/* Badges */}
<Card>
<CardHeader>
<CardTitle>Badges</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{data.badges.map((badge) => (
<div key={badge.id} className="flex items-center gap-3 p-2 rounded-lg border">
<div className="w-10 h-10 rounded-full bg-[#F8C301] flex items-center justify-center">
<Award className="h-5 w-5 text-[#04045B]" />
</div>
<div className="flex-1">
<p className="font-medium text-sm">{badge.name}</p>
<p className="text-xs text-muted-foreground">{badge.desc}</p>
</div>
</div>
))}
</CardContent>
</Card>
{/* Global Leaderboard Glance */}
<Card>
<CardHeader>
<CardTitle>Leaderboard</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="text-center space-y-2">
<div className="text-3xl font-bold">#{data.leaderboardGlance.rank}</div>
<p className="text-sm text-muted-foreground">
out of {data.leaderboardGlance.total} {data.leaderboardGlance.scope === 'org' ? 'colleagues' : 'learners'}
</p>
<div className="flex items-center justify-center gap-1">
<Star className="h-4 w-4 text-[#F8C301]" />
<span className="font-medium">{data.leaderboardGlance.xp} XP</span>
</div>
</div>
<Button variant="outline" className="w-full">
View Full Leaderboard
</Button>
</CardContent>
</Card>
{/* What other leaders are training for */}
<Card>
<CardHeader>
<CardTitle>What other leaders are training for</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{data.recommendations.slice(0, 2).map((course) => (
<div key={course.courseId} className="p-3 border rounded-lg space-y-2">
<h3 className="font-medium text-sm">{course.title}</h3>
<div className="flex items-center justify-between text-xs text-muted-foreground">
<div className="flex items-center gap-1">
<Users className="h-3 w-3" />
{course.currentLearnersCount} learners
</div>
<Badge variant="outline" className="text-xs">
{course.trendingSkill}
</Badge>
</div>
</div>
))}
</CardContent>
</Card>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,472 @@
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
import { Badge } from './ui/badge';
import { Button } from './ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import { Input } from './ui/input';
import { Textarea } from './ui/textarea';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from './ui/dialog';
import { MessageCircle, Plus, User, Calendar, Pin, Flag, ThumbsUp, Reply } from 'lucide-react';
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
interface User {
persona: 'corporate' | 'individual';
firstName: string;
}
interface Thread {
id: string;
title: string;
author: string;
authorAvatar?: string;
createdAt: string;
lastReplyAt: string;
replies: number;
pinned?: boolean;
tags?: string[];
excerpt: string;
}
interface Post {
id: string;
author: string;
authorAvatar?: string;
createdAt: string;
bodyHtml: string;
canEdit: boolean;
likes: number;
liked: boolean;
}
// Mock data
const mockThreads: Thread[] = [
{
id: '1',
title: 'Best practices for remote team leadership',
author: 'Priya Sharma',
authorAvatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=64&h=64&fit=crop&crop=face',
createdAt: '2024-01-15T10:30:00Z',
lastReplyAt: '2024-01-16T14:20:00Z',
replies: 12,
pinned: true,
tags: ['leadership', 'remote-work'],
excerpt: 'Looking for insights on managing distributed teams effectively...'
},
{
id: '2',
title: 'Digital transformation challenges in traditional industries',
author: 'Rajesh Kumar',
authorAvatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=64&h=64&fit=crop&crop=face',
createdAt: '2024-01-14T09:15:00Z',
lastReplyAt: '2024-01-16T11:45:00Z',
replies: 8,
tags: ['digital-transformation', 'strategy'],
excerpt: 'How do we overcome resistance to change in established organizations?'
},
{
id: '3',
title: 'Effective feedback techniques for managers',
author: 'Sneha Patel',
createdAt: '2024-01-13T16:20:00Z',
lastReplyAt: '2024-01-15T13:10:00Z',
replies: 15,
tags: ['feedback', 'management'],
excerpt: 'Share your experiences with giving constructive feedback...'
}
];
const mockPosts: Post[] = [
{
id: '1',
author: 'Priya Sharma',
authorAvatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=64&h=64&fit=crop&crop=face',
createdAt: '2024-01-15T10:30:00Z',
bodyHtml: '<p>I have been leading a remote team for the past 2 years and wanted to share some insights while also learning from your experiences.</p><p><strong>Key challenges I have faced:</strong></p><ul><li>Maintaining team cohesion across time zones</li><li>Ensuring clear communication without over-communicating</li><li>Building trust with team members I rarely see in person</li></ul><p>What strategies have worked best for you?</p>',
canEdit: true,
likes: 8,
liked: false
},
{
id: '2',
author: 'Amit Singh',
authorAvatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=64&h=64&fit=crop&crop=face',
createdAt: '2024-01-15T14:45:00Z',
bodyHtml: '<p>Great topic, Priya! I have found that regular one-on-ones are crucial. Here is what works for me:</p><p>📅 <strong>Weekly 30-min check-ins</strong> - not just about work, but also personal connection</p><p>🎯 <strong>Clear goal setting</strong> - using OKRs to align everyone</p><p>🛠️ <strong>Right tools</strong> - Slack for quick comms, Zoom for face-to-face, Notion for documentation</p><p>The key is being intentional about relationship building, not just task management.</p>',
canEdit: false,
likes: 12,
liked: true
}
];
export function DiscussionForums({ user }: { user: User }) {
const [selectedCohort, setSelectedCohort] = useState('cohort1');
const [currentView, setCurrentView] = useState<'threads' | 'thread'>('threads');
const [selectedThread, setSelectedThread] = useState<Thread | null>(null);
const [newThreadOpen, setNewThreadOpen] = useState(false);
const [newThreadTitle, setNewThreadTitle] = useState('');
const [newThreadBody, setNewThreadBody] = useState('');
const [replyText, setReplyText] = useState('');
const formatDate = (dateString: string) => {
const date = new Date(dateString);
const now = new Date();
const diffTime = now.getTime() - date.getTime();
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
if (diffDays === 0) return 'Today';
if (diffDays === 1) return 'Yesterday';
if (diffDays < 7) return `${diffDays} days ago`;
return date.toLocaleDateString();
};
const handleThreadClick = (thread: Thread) => {
setSelectedThread(thread);
setCurrentView('thread');
};
const handleCreateThread = () => {
// Mock thread creation
console.log('Creating thread:', { title: newThreadTitle, body: newThreadBody });
setNewThreadOpen(false);
setNewThreadTitle('');
setNewThreadBody('');
};
const handleReply = () => {
// Mock reply creation
console.log('Creating reply:', replyText);
setReplyText('');
};
if (currentView === 'thread' && selectedThread) {
return (
<div className="p-6 space-y-6 bg-white min-h-screen">
{/* Thread Header */}
<div className="space-y-4">
<Button
variant="outline"
onClick={() => setCurrentView('threads')}
className="mb-4"
>
Back to Forums
</Button>
<div className="space-y-2">
<div className="flex items-center gap-2">
{selectedThread.pinned && <Pin className="h-4 w-4 text-[var(--color-brand-accent)]" />}
<h1 className="text-2xl font-medium">{selectedThread.title}</h1>
</div>
<div className="flex items-center gap-4 text-sm text-gray-600">
<div className="flex items-center gap-2">
<Avatar className="h-6 w-6">
{selectedThread.authorAvatar && <AvatarImage src={selectedThread.authorAvatar} />}
<AvatarFallback>{selectedThread.author.charAt(0)}</AvatarFallback>
</Avatar>
<span>{selectedThread.author}</span>
</div>
<span></span>
<span>{formatDate(selectedThread.createdAt)}</span>
<span></span>
<span>{selectedThread.replies} replies</span>
</div>
{selectedThread.tags && (
<div className="flex gap-2">
{selectedThread.tags.map((tag) => (
<Badge key={tag} variant="secondary" className="text-xs">
{tag}
</Badge>
))}
</div>
)}
</div>
</div>
{/* Posts */}
<div className="space-y-5">
{mockPosts.map((post, index) => (
<Card key={post.id} className="border border-gray-200 bg-white">
<CardContent className="p-6">
<div className="space-y-4">
{/* Header */}
<div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-3">
<Avatar className="h-10 w-10">
{post.authorAvatar && <AvatarImage src={post.authorAvatar} />}
<AvatarFallback>{post.author.charAt(0)}</AvatarFallback>
</Avatar>
<div className="space-y-1">
<span className="font-semibold text-gray-900">{post.author}</span>
<div className="flex items-center gap-2 text-sm text-gray-600">
<Calendar className="h-4 w-4 text-gray-400" />
<span>{formatDate(post.createdAt)}</span>
</div>
</div>
</div>
<div className="flex items-center gap-2">
{post.canEdit && (
<Button variant="outline" size="sm" className="gap-2">
<Flag className="h-4 w-4" />
Edit
</Button>
)}
<Button variant="outline" size="sm" className="gap-2">
<Flag className="h-4 w-4" />
Report
</Button>
</div>
</div>
{/* Content */}
<div className="bg-gray-50 rounded-lg p-4 border border-gray-100">
<div
className="prose prose-sm max-w-none text-gray-700"
dangerouslySetInnerHTML={{ __html: post.bodyHtml }}
/>
</div>
{/* Actions */}
<div className="flex items-center gap-4 pt-2">
<Button
variant="outline"
size="sm"
className={`gap-2 ${post.liked ? 'bg-[#04045B]/5 text-[#04045B] border-[#04045B]/20' : ''}`}
>
<ThumbsUp className="h-4 w-4" />
{post.likes} Likes
</Button>
<Button variant="outline" size="sm" className="gap-2">
<Reply className="h-4 w-4" />
Reply
</Button>
</div>
</div>
</CardContent>
</Card>
))}
</div>
{/* Reply Form */}
<Card className="border border-gray-200 bg-white">
<CardContent className="p-6">
<div className="space-y-4">
<div className="flex items-center gap-3">
<div className="w-2 h-2 bg-[#04045B] rounded-full"></div>
<h3 className="text-lg font-semibold text-gray-900">Add a reply</h3>
</div>
<div className="bg-gray-50 rounded-lg p-4 border border-gray-100">
<Textarea
placeholder="Share your thoughts and contribute to the discussion..."
value={replyText}
onChange={(e) => setReplyText(e.target.value)}
rows={4}
className="bg-white border-gray-200 resize-none"
/>
</div>
<div className="flex justify-end gap-3">
<Button variant="outline" onClick={() => setReplyText('')} className="gap-2">
Cancel
</Button>
<Button
onClick={handleReply}
disabled={!replyText.trim()}
className="gap-2 bg-[#04045B] hover:bg-[#030344] text-white"
>
<Reply className="h-4 w-4" />
Post Reply
</Button>
</div>
</div>
</CardContent>
</Card>
</div>
);
}
return (
<div className="p-6 space-y-6 bg-white min-h-screen">
{/* Header */}
<div className="space-y-2">
<h1 className="text-3xl font-semibold text-gray-900">Discussion Forums</h1>
<p className="text-gray-600">Connect and learn with your cohort</p>
</div>
{/* Controls */}
<div className="flex items-center justify-between bg-gradient-to-r from-gray-50 to-blue-50/30 p-4 rounded-xl border border-gray-100">
<div className="flex items-center gap-4">
{user.persona === 'corporate' && (
<Select value={selectedCohort} onValueChange={setSelectedCohort}>
<SelectTrigger className="w-56 bg-white border-gray-200 shadow-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="cohort1">Tech Solutions Cohort</SelectItem>
<SelectItem value="cohort2">Leadership Program 2024</SelectItem>
</SelectContent>
</Select>
)}
</div>
<Dialog open={newThreadOpen} onOpenChange={setNewThreadOpen}>
<DialogTrigger asChild>
<Button className="gap-2 bg-[#04045B] hover:bg-[#030344] text-white px-6 py-2.5">
<Plus className="h-4 w-4" />
New Thread
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Create New Discussion Thread</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<label className="text-sm font-medium">Title</label>
<Input
placeholder="What would you like to discuss?"
value={newThreadTitle}
onChange={(e) => setNewThreadTitle(e.target.value)}
maxLength={120}
className="bg-white"
/>
<p className="text-xs text-gray-600 mt-1">
{newThreadTitle.length}/120 characters
</p>
</div>
<div>
<label className="text-sm font-medium">Description</label>
<Textarea
placeholder="Provide more details about your topic..."
value={newThreadBody}
onChange={(e) => setNewThreadBody(e.target.value)}
rows={6}
className="bg-white"
/>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setNewThreadOpen(false)}>
Cancel
</Button>
<Button
onClick={handleCreateThread}
disabled={!newThreadTitle.trim() || !newThreadBody.trim()}
className="bg-[#04045B] hover:bg-[#030344] text-white"
>
Create Thread
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
{/* Threads List */}
<div className="space-y-5">
{mockThreads.map((thread) => (
<Card
key={thread.id}
className="hover:shadow-lg transition-all duration-300 border border-gray-200 bg-white cursor-pointer group"
onClick={() => handleThreadClick(thread)}
>
<CardContent className="p-6">
<div className="space-y-4">
{/* Header with author and pin */}
<div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-3 flex-1">
<Avatar className="h-10 w-10 flex-shrink-0">
{thread.authorAvatar && <AvatarImage src={thread.authorAvatar} />}
<AvatarFallback>{thread.author.charAt(0)}</AvatarFallback>
</Avatar>
<div className="space-y-1 flex-1">
<div className="flex items-center gap-2">
{thread.pinned && <Pin className="h-4 w-4 text-[#F8C301]" />}
<h3 className="text-lg font-semibold text-gray-900 group-hover:text-[#04045B] transition-colors leading-tight">
{thread.title}
</h3>
</div>
<p className="text-sm text-gray-600">by {thread.author}</p>
</div>
</div>
<div className="text-right text-sm text-gray-500">
<div className="flex items-center gap-1">
<Calendar className="h-4 w-4" />
<span>{formatDate(thread.lastReplyAt)}</span>
</div>
</div>
</div>
{/* Thread excerpt */}
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100">
<p className="text-sm text-gray-700 leading-relaxed line-clamp-2">
{thread.excerpt}
</p>
</div>
{/* Tags and metadata */}
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-4 text-sm text-gray-600">
<div className="flex items-center gap-2">
<MessageCircle className="h-4 w-4 text-gray-400" />
<span>{thread.replies} replies</span>
</div>
</div>
{thread.tags && (
<div className="flex gap-2">
{thread.tags.slice(0, 2).map((tag) => (
<Badge
key={tag}
variant="outline"
className="text-xs font-medium bg-[#F8C301]/10 text-[#04045B] border-[#F8C301]/30"
>
#{tag}
</Badge>
))}
{thread.tags.length > 2 && (
<Badge
variant="outline"
className="text-xs font-medium bg-gray-100 text-gray-600 border-gray-200"
>
+{thread.tags.length - 2} more
</Badge>
)}
</div>
)}
</div>
</div>
</CardContent>
</Card>
))}
</div>
{mockThreads.length === 0 && (
<Card className="border-0 shadow-lg">
<CardContent className="p-12 text-center">
<div className="w-20 h-20 mx-auto mb-6 bg-gradient-to-br from-[#04045B]/10 to-blue-50 rounded-full flex items-center justify-center">
<MessageCircle className="h-10 w-10 text-[#04045B]" />
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">No discussions yet</h3>
<p className="text-gray-600 mb-6 max-w-md mx-auto">
Be the first to start a conversation with your cohort and share your leadership insights.
</p>
<Button
onClick={() => setNewThreadOpen(true)}
className="gap-2 bg-[#04045B] hover:bg-[#030344] text-white px-6 py-2.5"
>
<Plus className="h-4 w-4" />
Start a Discussion
</Button>
</CardContent>
</Card>
)}
</div>
);
}

View File

@@ -0,0 +1,557 @@
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
import { Button } from './ui/button';
import { Badge } from './ui/badge';
import { Progress } from './ui/progress';
import {
ChevronRight,
Download,
Share,
BookmarkPlus,
Clock,
FileText,
Eye,
Search,
ZoomIn,
ZoomOut,
RotateCw,
Printer,
MessageSquare,
Highlighter,
StickyNote,
ChevronLeft,
ChevronUp,
ChevronDown
} from 'lucide-react';
// Mock document data
interface DocumentData {
id: string;
title: string;
subtitle: string;
description: string;
category: string;
author: {
name: string;
title: string;
bio: string;
avatar: string;
};
fileType: string;
fileSize: string;
pageCount: number;
downloadUrl: string;
isDownloaded: boolean;
lastRead: string;
progress: number;
currentPage: number;
tableOfContents: DocumentSection[];
tags: string[];
language: string;
publishedDate: string;
}
interface DocumentSection {
id: string;
title: string;
page: number;
subsections?: DocumentSection[];
}
const documentDatabase: Record<string, DocumentData> = {
'document-1': {
id: 'document-1',
title: 'Leadership in the Digital Age',
subtitle: 'A Comprehensive Guide to Modern Leadership Strategies',
description: 'This comprehensive guide explores the evolving landscape of leadership in our digital world. Learn how to navigate technological disruption, lead remote teams, and build resilient organizations that thrive in uncertainty.',
category: 'Leadership Development',
author: {
name: 'Dr. Marcus Thompson',
title: 'Digital Transformation Expert',
bio: 'Dr. Thompson is a leading expert in digital leadership and organizational transformation, with over 20 years of experience consulting for global enterprises.',
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=100&h=100&fit=crop'
},
fileType: 'PDF',
fileSize: '2.8 MB',
pageCount: 142,
downloadUrl: 'https://example.com/leadership-digital-age.pdf',
isDownloaded: true,
lastRead: '2024-03-15T14:30:00Z',
progress: 45,
currentPage: 64,
language: 'English',
publishedDate: '2024-01-15',
tags: ['Leadership', 'Digital Transformation', 'Remote Work', 'Change Management'],
tableOfContents: [
{
id: 'chapter-1',
title: 'Introduction to Digital Leadership',
page: 1,
subsections: [
{ id: 'section-1-1', title: 'Defining Digital Leadership', page: 3 },
{ id: 'section-1-2', title: 'The Digital Leadership Mindset', page: 8 }
]
},
{
id: 'chapter-2',
title: 'Leading Remote and Hybrid Teams',
page: 15,
subsections: [
{ id: 'section-2-1', title: 'Building Trust in Virtual Environments', page: 17 },
{ id: 'section-2-2', title: 'Communication Strategies for Remote Teams', page: 25 },
{ id: 'section-2-3', title: 'Performance Management in Digital Workspaces', page: 33 }
]
},
{
id: 'chapter-3',
title: 'Digital Transformation Leadership',
page: 42,
subsections: [
{ id: 'section-3-1', title: 'Driving Organizational Change', page: 44 },
{ id: 'section-3-2', title: 'Technology Adoption Strategies', page: 52 }
]
},
{
id: 'chapter-4',
title: 'Building Resilient Organizations',
page: 65,
subsections: [
{ id: 'section-4-1', title: 'Crisis Leadership in Digital Times', page: 67 },
{ id: 'section-4-2', title: 'Future-Proofing Your Organization', page: 75 }
]
},
{
id: 'chapter-5',
title: 'The Future of Leadership',
page: 85,
subsections: [
{ id: 'section-5-1', title: 'Emerging Leadership Trends', page: 87 },
{ id: 'section-5-2', title: 'Preparing for Tomorrow\'s Challenges', page: 95 }
]
}
]
}
};
interface DocumentDetailPageProps {
documentId: string;
onNavigateBack: () => void;
}
export function DocumentDetailPage({ documentId, onNavigateBack }: DocumentDetailPageProps) {
const [documentData] = useState<DocumentData>(documentDatabase[documentId] || documentDatabase['document-1']);
const [selectedSection, setSelectedSection] = useState<string | null>(null);
const [expandedSections, setExpandedSections] = useState<string[]>(['chapter-1']);
const [viewMode, setViewMode] = useState<'reader' | 'viewer'>('reader');
const [zoomLevel, setZoomLevel] = useState(100);
const formatLastRead = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
const formatPublishedDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
};
const toggleSection = (sectionId: string) => {
setExpandedSections(prev =>
prev.includes(sectionId)
? prev.filter(id => id !== sectionId)
: [...prev, sectionId]
);
};
const jumpToPage = (page: number) => {
// In a real app, this would navigate to the specific page
console.log(`Jumping to page ${page}`);
};
const handleDownload = () => {
// In a real app, this would trigger the download
console.log('Downloading document...');
};
return (
<div className="h-full bg-background">
{/* Breadcrumb Navigation - Full Width */}
<div className="bg-background border-b border-border py-3">
<div className="px-6">
<nav className="flex items-center gap-2 text-[14px] text-muted-foreground">
<button
onClick={onNavigateBack}
className="hover:text-[var(--color-brand-primary)] transition-colors"
>
Library
</button>
<ChevronRight className="h-3 w-3" />
<span>{documentData.category}</span>
<ChevronRight className="h-3 w-3" />
<span className="text-foreground font-medium">Documents</span>
<ChevronRight className="h-3 w-3" />
<span className="text-foreground font-medium">{documentData.title}</span>
</nav>
</div>
</div>
{/* Document Header - Full Width */}
<div className="bg-background border-b border-border py-6">
<div className="px-6">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<Badge variant="secondary" className="bg-[var(--color-brand-primary)]/10 text-[var(--color-brand-primary)] border-[var(--color-brand-primary)]/20">
<FileText className="h-3 w-3 mr-1" />
{documentData.fileType}
</Badge>
<Badge variant="outline" className="text-[12px]">
{documentData.pageCount} pages
</Badge>
{documentData.isDownloaded && (
<Badge className="bg-green-100 text-green-800 border-green-200">
Downloaded
</Badge>
)}
</div>
<h1 className="text-[28px] font-bold leading-[34px] tracking-[-1px] mb-2 text-gray-900">
{documentData.title}
</h1>
<p className="text-[16px] text-muted-foreground mb-3">
{documentData.subtitle}
</p>
<div className="flex items-center gap-4 text-[14px] text-muted-foreground">
<span>By {documentData.author.name}</span>
<span></span>
<span>Published {formatPublishedDate(documentData.publishedDate)}</span>
<span></span>
<span>{documentData.fileSize}</span>
{documentData.lastRead && (
<>
<span></span>
<span>Last read: {formatLastRead(documentData.lastRead)}</span>
</>
)}
</div>
</div>
<div className="flex items-center gap-3">
<Button variant="outline">
<Eye className="h-4 w-4 mr-2" />
Read Online
</Button>
<Button onClick={handleDownload}>
<Download className="h-4 w-4 mr-2" />
Download PDF
</Button>
</div>
</div>
</div>
</div>
{/* Content Layout */}
<div className="flex h-full">
{/* Left Sidebar - Table of Contents */}
<aside className="w-[300px] flex-shrink-0 bg-white border-r border-border">
<div className="h-full overflow-y-auto">
<div className="p-6">
<h3 className="text-[16px] font-semibold text-foreground mb-6">Table of Contents</h3>
<div className="space-y-2">
{documentData.tableOfContents.map((section) => (
<div key={section.id}>
<div
className={`flex items-center justify-between p-2 rounded-lg cursor-pointer transition-colors ${
selectedSection === section.id
? 'bg-[var(--color-brand-primary)]/5 border border-[var(--color-brand-primary)]/20'
: 'hover:bg-gray-50'
}`}
onClick={() => {
setSelectedSection(section.id);
jumpToPage(section.page);
if (section.subsections) {
toggleSection(section.id);
}
}}
>
<div className="flex items-center gap-2 flex-1">
<span className="text-[14px] font-medium text-foreground">
{section.title}
</span>
</div>
<div className="flex items-center gap-2">
<span className="text-[12px] text-muted-foreground">p.{section.page}</span>
{section.subsections && (
expandedSections.includes(section.id) ? (
<ChevronUp className="h-3 w-3 text-muted-foreground" />
) : (
<ChevronDown className="h-3 w-3 text-muted-foreground" />
)
)}
</div>
</div>
{section.subsections && expandedSections.includes(section.id) && (
<div className="ml-4 mt-2 space-y-1">
{section.subsections.map((subsection) => (
<div
key={subsection.id}
className="flex items-center justify-between p-2 rounded cursor-pointer hover:bg-gray-50"
onClick={() => {
setSelectedSection(subsection.id);
jumpToPage(subsection.page);
}}
>
<span className="text-[13px] text-muted-foreground">
{subsection.title}
</span>
<span className="text-[11px] text-muted-foreground">
p.{subsection.page}
</span>
</div>
))}
</div>
)}
</div>
))}
</div>
{/* Document Tools */}
<div className="mt-8 pt-6 border-t border-border">
<h4 className="text-[14px] font-medium text-foreground mb-4">Document Tools</h4>
<div className="space-y-2">
<Button variant="outline" size="sm" className="w-full justify-start">
<Search className="h-4 w-4 mr-2" />
Search Document
</Button>
<Button variant="outline" size="sm" className="w-full justify-start">
<Highlighter className="h-4 w-4 mr-2" />
Highlight Text
</Button>
<Button variant="outline" size="sm" className="w-full justify-start">
<StickyNote className="h-4 w-4 mr-2" />
Add Notes
</Button>
<Button variant="outline" size="sm" className="w-full justify-start">
<Printer className="h-4 w-4 mr-2" />
Print
</Button>
</div>
</div>
</div>
</div>
</aside>
{/* Main Content - Document Viewer */}
<main className="flex-1 min-w-0">
<div className="h-full flex flex-col">
{/* Document Toolbar */}
<div className="flex items-center justify-between p-4 border-b border-border bg-gray-50">
<div className="flex items-center gap-4">
<span className="text-[14px] text-muted-foreground">
Page {documentData.currentPage} of {documentData.pageCount}
</span>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setZoomLevel(Math.max(50, zoomLevel - 10))}
>
<ZoomOut className="h-4 w-4" />
</Button>
<span className="text-[14px] text-muted-foreground min-w-[60px] text-center">
{zoomLevel}%
</span>
<Button
variant="outline"
size="sm"
onClick={() => setZoomLevel(Math.min(200, zoomLevel + 10))}
>
<ZoomIn className="h-4 w-4" />
</Button>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm">
<RotateCw className="h-4 w-4" />
</Button>
<Button variant="outline" size="sm">
<Share className="h-4 w-4" />
</Button>
<Button variant="outline" size="sm">
<BookmarkPlus className="h-4 w-4" />
</Button>
</div>
</div>
{/* Document Content Area */}
<div className="flex-1 p-6 overflow-auto bg-gray-100">
<div className="max-w-4xl mx-auto">
{/* Document Preview/Reader */}
<div
className="bg-white shadow-lg rounded-lg overflow-hidden"
style={{ transform: `scale(${zoomLevel / 100})`, transformOrigin: 'top center' }}
>
<div className="aspect-[8.5/11] p-8 bg-white">
<div className="h-full border border-gray-200 p-6">
<h1 className="text-[24px] font-bold mb-4 text-center">
Leadership in the Digital Age
</h1>
<h2 className="text-[18px] font-medium mb-6 text-center text-muted-foreground">
Chapter 3: Digital Transformation Leadership
</h2>
<div className="space-y-4 text-[14px] leading-relaxed">
<p>
In today's rapidly evolving business landscape, leaders must navigate
unprecedented technological disruption while maintaining organizational
cohesion and purpose. Digital transformation is not merely about
implementing new technologies; it's about fundamentally reimagining
how organizations operate, deliver value, and engage with stakeholders.
</p>
<p>
Successful digital leaders understand that transformation begins with
mindset. They cultivate a culture of continuous learning, encourage
experimentation, and view failure as a stepping stone to innovation.
This chapter explores the core competencies required to lead
organizations through digital transformation successfully.
</p>
<h3 className="text-[16px] font-semibold mt-6 mb-3">
Key Principles of Digital Leadership
</h3>
<p>
Digital leadership requires a unique blend of traditional leadership
skills and digital-age competencies. Leaders must be comfortable
with ambiguity, data-driven decision making, and rapid iteration cycles.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
{/* Right Sidebar - Document Info & Progress */}
<aside className="w-[300px] flex-shrink-0 bg-white border-l border-border">
<div className="h-full overflow-y-auto">
<div className="p-6">
<h3 className="text-[18px] font-semibold text-foreground mb-4">Reading progress</h3>
{/* Reading Progress */}
<div className="mb-6">
<div className="flex items-center justify-between mb-3">
<span className="text-[14px] text-foreground">Pages read</span>
<span className="text-[14px] font-medium text-[var(--color-brand-primary)]">
{Math.round(documentData.pageCount * documentData.progress / 100)} / {documentData.pageCount}
</span>
</div>
<Progress value={documentData.progress} className="h-2 mb-3" />
<p className="text-[12px] text-muted-foreground">
{documentData.progress}% completed
</p>
</div>
{/* Document Information */}
<div className="mb-6">
<h4 className="text-[16px] font-medium text-foreground mb-4">Document Details</h4>
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-[14px] text-muted-foreground">Format</span>
<span className="text-[14px] font-medium">{documentData.fileType}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[14px] text-muted-foreground">File Size</span>
<span className="text-[14px] font-medium">{documentData.fileSize}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[14px] text-muted-foreground">Pages</span>
<span className="text-[14px] font-medium">{documentData.pageCount}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[14px] text-muted-foreground">Language</span>
<span className="text-[14px] font-medium">{documentData.language}</span>
</div>
</div>
</div>
{/* Tags */}
<div className="mb-6">
<h4 className="text-[16px] font-medium text-foreground mb-3">Topics</h4>
<div className="flex flex-wrap gap-2">
{documentData.tags.map((tag, index) => (
<Badge key={index} variant="outline" className="text-[12px]">
{tag}
</Badge>
))}
</div>
</div>
{/* Author Information */}
<div className="mb-6">
<h4 className="text-[16px] font-medium text-foreground mb-3">Author</h4>
<div className="flex items-start gap-3">
<img
src={documentData.author.avatar}
alt={documentData.author.name}
className="w-12 h-12 rounded-full object-cover"
/>
<div>
<h5 className="text-[14px] font-medium text-foreground">{documentData.author.name}</h5>
<p className="text-[13px] text-[var(--color-brand-primary)] mb-1">{documentData.author.title}</p>
<p className="text-[12px] text-muted-foreground">{documentData.author.bio}</p>
</div>
</div>
</div>
{/* Actions */}
<div>
<h4 className="text-[16px] font-medium text-foreground mb-3">Actions</h4>
<div className="space-y-2">
<Button variant="outline" size="sm" className="w-full justify-start">
<Download className="h-4 w-4 mr-2" />
Download PDF
</Button>
<Button variant="outline" size="sm" className="w-full justify-start">
<Share className="h-4 w-4 mr-2" />
Share Document
</Button>
<Button variant="outline" size="sm" className="w-full justify-start">
<MessageSquare className="h-4 w-4 mr-2" />
Discuss Document
</Button>
<Button variant="outline" size="sm" className="w-full justify-start">
<BookmarkPlus className="h-4 w-4 mr-2" />
Save to Library
</Button>
</div>
</div>
</div>
</div>
</aside>
</div>
</div>
);
}

View File

@@ -0,0 +1,509 @@
import React, { useState, useEffect } from 'react';
import { Search, FileText, Video, BookOpen, MessageCircle, X, Loader2, Brain } from 'lucide-react';
import { Card, CardContent } from './ui/card';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Badge } from './ui/badge';
import { Checkbox } from './ui/checkbox';
import { Label } from './ui/label';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from './ui/dialog';
interface SearchResult {
id: string;
title: string;
type: 'Course' | 'Webcast' | 'Blog' | 'Article' | 'Forum';
theme: 'Leadership' | 'Digital Transformation' | 'Team Building' | 'Strategy' | 'Innovation' | 'Project Management';
summary?: string;
url: string;
}
interface GlobalSearchProps {
isOpen: boolean;
onClose: () => void;
}
// Mock dataset
const mockData: SearchResult[] = [
{
id: '1',
title: 'Advanced Leadership Development Program',
type: 'Course',
theme: 'Leadership',
summary: 'Comprehensive leadership training covering strategic thinking, team management, and executive decision-making skills.',
url: '/courses/advanced-leadership-development'
},
{
id: '2',
title: 'Digital Transformation Strategies',
type: 'Webcast',
theme: 'Digital Transformation',
summary: 'Learn how to lead your organization through digital transformation with proven methodologies and real-world case studies.',
url: '/webcasts/digital-transformation-strategies'
},
{
id: '3',
title: 'Building High-Performance Teams',
type: 'Article',
theme: 'Team Building',
summary: 'Essential techniques for creating cohesive, productive teams that deliver exceptional results in challenging environments.',
url: '/articles/building-high-performance-teams'
},
{
id: '4',
title: 'Strategic Planning Best Practices',
type: 'Blog',
theme: 'Strategy',
summary: 'A comprehensive guide to developing and implementing strategic plans that drive organizational success.',
url: '/blog/strategic-planning-best-practices'
},
{
id: '5',
title: 'Innovation in the Workplace',
type: 'Forum',
theme: 'Innovation',
url: '/forums/innovation-workplace'
},
{
id: '6',
title: 'Project Management Fundamentals',
type: 'Course',
theme: 'Project Management',
summary: 'Master the core principles of project management including planning, execution, monitoring, and closure.',
url: '/courses/project-management-fundamentals'
},
{
id: '7',
title: 'Communication Skills for Leaders',
type: 'Webcast',
theme: 'Leadership',
summary: 'Develop powerful communication skills that inspire teams and drive organizational change.',
url: '/webcasts/communication-skills-leaders'
},
{
id: '8',
title: 'Team Dynamics and Collaboration',
type: 'Article',
theme: 'Team Building',
summary: 'Understanding team dynamics and fostering collaboration in diverse, multi-generational workplaces.',
url: '/articles/team-dynamics-collaboration'
}
];
const searchSuggestions = [
'leadership development',
'project management',
'team building',
'digital transformation',
'communication skills',
'strategic planning'
];
const typeOptions = ['Course', 'Webcast', 'Blog', 'Article', 'Forum'];
const themeOptions = ['Leadership', 'Digital Transformation', 'Team Building', 'Strategy', 'Innovation', 'Project Management'];
export function GlobalSearch({ isOpen, onClose }: GlobalSearchProps) {
const [searchQuery, setSearchQuery] = useState('');
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
const [isSearching, setIsSearching] = useState(false);
const [hasSearched, setHasSearched] = useState(false);
const [selectedTypes, setSelectedTypes] = useState<string[]>([]);
const [selectedThemes, setSelectedThemes] = useState<string[]>([]);
const characterCount = searchQuery.length;
const maxCharacters = 200;
// Generate placeholder summary for items without one
const generatePlaceholderSummary = (type: string): string => {
const summaries = {
Course: 'Interactive learning experience with comprehensive modules and practical exercises.',
Webcast: 'Live streaming session with expert insights and interactive Q&A opportunities.',
Blog: 'Thought-provoking article with actionable insights and industry perspectives.',
Article: 'In-depth analysis with research-backed insights and practical recommendations.',
Forum: 'Community discussion forum for sharing experiences and best practices.'
};
return summaries[type as keyof typeof summaries] || 'Comprehensive learning resource with valuable insights.';
};
// Get icon by content type
const getTypeIcon = (type: string) => {
switch (type) {
case 'Course':
return <BookOpen className="h-5 w-5" />;
case 'Webcast':
return <Video className="h-5 w-5" />;
case 'Blog':
case 'Article':
return <FileText className="h-5 w-5" />;
case 'Forum':
return <MessageCircle className="h-5 w-5" />;
default:
return <FileText className="h-5 w-5" />;
}
};
// Filter and search logic
const performSearch = async (query: string) => {
setIsSearching(true);
// Simulate API delay
await new Promise(resolve => setTimeout(resolve, 1000));
let results = mockData;
// Filter by search query
if (query.trim()) {
const lowercaseQuery = query.toLowerCase();
results = results.filter(item =>
item.title.toLowerCase().includes(lowercaseQuery) ||
(item.summary && item.summary.toLowerCase().includes(lowercaseQuery))
);
}
// Filter by selected types
if (selectedTypes.length > 0) {
results = results.filter(item => selectedTypes.includes(item.type));
}
// Filter by selected themes
if (selectedThemes.length > 0) {
results = results.filter(item => selectedThemes.includes(item.theme));
}
setSearchResults(results);
setIsSearching(false);
setHasSearched(true);
};
const handleSearch = () => {
performSearch(searchQuery);
};
const handleSuggestionClick = (suggestion: string) => {
setSearchQuery(suggestion);
performSearch(suggestion);
};
const handleTypeChange = (type: string, checked: boolean) => {
if (checked) {
setSelectedTypes([...selectedTypes, type]);
} else {
setSelectedTypes(selectedTypes.filter(t => t !== type));
}
};
const handleThemeChange = (theme: string, checked: boolean) => {
if (checked) {
setSelectedThemes([...selectedThemes, theme]);
} else {
setSelectedThemes(selectedThemes.filter(t => t !== theme));
}
};
const clearAllFilters = () => {
setSelectedTypes([]);
setSelectedThemes([]);
};
const hasActiveFilters = selectedTypes.length > 0 || selectedThemes.length > 0;
// Re-search when filters change
useEffect(() => {
if (hasSearched) {
performSearch(searchQuery);
}
}, [selectedTypes, selectedThemes]);
// Reset state when dialog closes
useEffect(() => {
if (!isOpen) {
setSearchQuery('');
setSearchResults([]);
setIsSearching(false);
setHasSearched(false);
setSelectedTypes([]);
setSelectedThemes([]);
}
}, [isOpen]);
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="!max-w-[800px] !w-[800px] max-h-[90vh] p-0 bg-white overflow-hidden sm:!max-w-[800px]" style={{maxWidth: '800px', width: '800px'}}>
<DialogHeader className="px-6 py-4 border-b flex-shrink-0">
<DialogTitle className="flex items-center gap-2">
<Search className="h-5 w-5" />
Global Search
</DialogTitle>
<DialogDescription>
Search across all your learning resources including courses, articles, discussions, and more with AI-powered search capabilities.
</DialogDescription>
</DialogHeader>
<div className="flex h-[calc(90vh-100px)] overflow-hidden">
{/* Search and Filters Sidebar */}
<div className="w-64 border-r bg-gray-50 flex-shrink-0 overflow-y-auto custom-scrollbar">
<div className="p-4 space-y-6">
{/* Search Input */}
<div className="space-y-3">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="Search resources..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
className="pl-10 bg-white"
maxLength={maxCharacters}
/>
</div>
<div className="flex items-center justify-between text-xs text-gray-500">
<span>{characterCount}/{maxCharacters} characters</span>
</div>
<Button
onClick={handleSearch}
className="w-full bg-[var(--color-brand-primary)] hover:bg-[var(--color-brand-primary)]/90"
disabled={isSearching}
>
{isSearching ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Searching...
</>
) : (
<>
<Search className="h-4 w-4 mr-2" />
Search
</>
)}
</Button>
</div>
{/* Search Suggestions */}
{!hasSearched && (
<div className="space-y-3">
<h4 className="font-medium text-sm">Suggested searches:</h4>
<div className="space-y-2">
{searchSuggestions.map((suggestion) => (
<button
key={suggestion}
onClick={() => handleSuggestionClick(suggestion)}
className="text-left text-sm text-gray-600 hover:text-[var(--color-brand-primary)] hover:underline block w-full"
>
"{suggestion}"
</button>
))}
</div>
</div>
)}
{/* Filters - Only show after search */}
{hasSearched && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h4 className="font-medium text-sm">Filters</h4>
{hasActiveFilters && (
<Button
variant="ghost"
size="sm"
onClick={clearAllFilters}
className="text-xs h-auto p-1 text-gray-500 hover:text-gray-700"
>
Clear All
</Button>
)}
</div>
{/* Type Filters */}
<div className="space-y-3">
<h5 className="font-medium text-xs text-gray-700 uppercase tracking-wider">Type</h5>
<div className="space-y-2">
{typeOptions.map((type) => (
<div key={type} className="flex items-center space-x-2">
<Checkbox
id={`type-${type}`}
checked={selectedTypes.includes(type)}
onCheckedChange={(checked) => handleTypeChange(type, checked as boolean)}
/>
<Label htmlFor={`type-${type}`} className="text-sm cursor-pointer">
{type}
</Label>
</div>
))}
</div>
</div>
{/* Theme Filters */}
<div className="space-y-3">
<h5 className="font-medium text-xs text-gray-700 uppercase tracking-wider">Theme</h5>
<div className="space-y-2">
{themeOptions.map((theme) => (
<div key={theme} className="flex items-center space-x-2">
<Checkbox
id={`theme-${theme}`}
checked={selectedThemes.includes(theme)}
onCheckedChange={(checked) => handleThemeChange(theme, checked as boolean)}
/>
<Label htmlFor={`theme-${theme}`} className="text-sm cursor-pointer">
{theme}
</Label>
</div>
))}
</div>
</div>
</div>
)}
</div>
</div>
{/* Results Area */}
<div className="flex-1 min-w-0 overflow-y-auto custom-scrollbar">
<div className="p-6">
{!hasSearched ? (
/* Placeholder State */
<div className="flex flex-col items-center justify-center h-full text-center space-y-4 py-20">
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center">
<Search className="h-8 w-8 text-gray-400" />
</div>
<div className="space-y-2">
<h3 className="text-lg font-medium text-gray-900">Search Learning Resources</h3>
<p className="text-gray-500 max-w-md">
Find courses, articles, webcasts, and discussions across your learning library.
Use the search bar or try one of the suggested searches to get started.
</p>
</div>
</div>
) : isSearching ? (
/* Loading State */
<div className="flex flex-col items-center justify-center h-full text-center space-y-4 py-20">
<Loader2 className="h-8 w-8 animate-spin text-[var(--color-brand-primary)]" />
<div className="space-y-2">
<h3 className="text-lg font-medium text-gray-900">Searching...</h3>
<p className="text-gray-500">
Finding the best resources for your query
</p>
</div>
</div>
) : searchResults.length === 0 ? (
/* No Results State */
<div className="flex flex-col items-center justify-center h-full text-center space-y-4 py-20">
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center">
<Search className="h-8 w-8 text-gray-400" />
</div>
<div className="space-y-2">
<h3 className="text-lg font-medium text-gray-900">No results found</h3>
<p className="text-gray-500 max-w-md">
We couldn't find any resources matching your search. Try adjusting your search terms or filters.
</p>
</div>
<Button
className="bg-[var(--color-brand-accent)] hover:bg-[var(--color-brand-accent)]/90 text-black mt-4"
>
<Brain className="h-4 w-4 mr-2" />
Open AI Mentor
</Button>
</div>
) : (
/* Results State */
<div className="space-y-6">
{/* Results Header */}
<div className="space-y-3">
<h3 className="text-lg font-medium">
{searchResults.length} result{searchResults.length !== 1 ? 's' : ''}
{searchQuery && ` for "${searchQuery}"`}
</h3>
{/* Active Filters */}
{hasActiveFilters && (
<div className="flex flex-wrap gap-2">
{selectedTypes.map((type) => (
<Badge key={type} variant="secondary" className="text-xs">
Type: {type}
<button
onClick={() => handleTypeChange(type, false)}
className="ml-1 hover:text-red-600"
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
{selectedThemes.map((theme) => (
<Badge key={theme} variant="secondary" className="text-xs">
Theme: {theme}
<button
onClick={() => handleThemeChange(theme, false)}
className="ml-1 hover:text-red-600"
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
</div>
)}
</div>
{/* Results List */}
<div className="space-y-6">
{searchResults.map((result) => (
<Card key={result.id} className="hover:shadow-lg transition-all duration-200 cursor-pointer bg-white border hover:border-[var(--color-brand-primary)]/20">
<CardContent className="p-6">
<div className="flex items-start gap-4">
<div className="text-[var(--color-brand-primary)] mt-1 flex-shrink-0">
{getTypeIcon(result.type)}
</div>
<div className="flex-1 min-w-0 space-y-3">
<div className="flex items-start justify-between gap-4">
<h4
className="font-medium text-lg hover:text-[var(--color-brand-primary)] leading-relaxed flex-1 min-w-0"
style={{
wordWrap: 'break-word',
overflowWrap: 'break-word'
}}
>
{result.title}
</h4>
<div className="flex items-center gap-2 flex-shrink-0">
<Badge variant="outline" className="text-xs capitalize">
{result.type}
</Badge>
<Badge variant="secondary" className="text-xs">
{result.theme}
</Badge>
</div>
</div>
<div className="space-y-2">
<p
className="text-gray-600 leading-relaxed min-w-0"
style={{
wordWrap: 'break-word',
overflowWrap: 'break-word'
}}
>
{result.summary || generatePlaceholderSummary(result.type)}
</p>
<div className="pt-2 border-t border-gray-100">
<span
className="text-sm text-gray-500 block min-w-0"
style={{
wordWrap: 'break-word',
overflowWrap: 'break-word'
}}
>
{result.url}
</span>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
))}
</div>
</div>
)}
</div>
</div>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,246 @@
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
import { Badge } from './ui/badge';
import { Button } from './ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import { Progress } from './ui/progress';
import { Trophy, Medal, Award, Crown, Star, Target } from 'lucide-react';
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
interface User {
persona: 'corporate' | 'individual';
orgName?: string;
}
interface LeaderboardEntry {
rank: number;
userName: string;
exp: number;
milestone: number;
timeToMilestoneSec: number;
badges?: number;
avatar?: string;
isCurrentUser?: boolean;
}
// Mock data
const mockCurrentUser = {
rank: 7,
exp: 2450,
milestone: 3000,
timeToMilestoneSec: 86400 * 5, // 5 days
};
const mockLeaderboard: LeaderboardEntry[] = [
{ rank: 1, userName: 'Rajesh Kumar', exp: 4850, milestone: 5000, timeToMilestoneSec: 86400 * 2, badges: 12, avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=64&h=64&fit=crop&crop=face' },
{ rank: 2, userName: 'Priya Sharma', exp: 4200, milestone: 5000, timeToMilestoneSec: 86400 * 8, badges: 10, avatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=64&h=64&fit=crop&crop=face' },
{ rank: 3, userName: 'Amit Singh', exp: 3950, milestone: 4000, timeToMilestoneSec: 86400 * 1, badges: 11, avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=64&h=64&fit=crop&crop=face' },
{ rank: 4, userName: 'Sneha Patel', exp: 3600, milestone: 4000, timeToMilestoneSec: 86400 * 4, badges: 9 },
{ rank: 5, userName: 'Vikram Rao', exp: 3250, milestone: 4000, timeToMilestoneSec: 86400 * 7, badges: 8 },
{ rank: 6, userName: 'Kavya Iyer', exp: 2800, milestone: 3000, timeToMilestoneSec: 86400 * 3, badges: 7 },
{ rank: 7, userName: 'You', exp: 2450, milestone: 3000, timeToMilestoneSec: 86400 * 5, badges: 6, isCurrentUser: true },
{ rank: 8, userName: 'Arjun Mehta', exp: 2200, milestone: 3000, timeToMilestoneSec: 86400 * 10, badges: 5 },
];
const mockBadges = [
{ id: '1', name: 'First Course', iconUrl: '🎯', description: 'Complete your first course', earnedAt: '2024-01-01' },
{ id: '2', name: 'Quick Learner', iconUrl: '⚡', description: 'Complete a course in under 2 days', earnedAt: '2024-01-08' },
{ id: '3', name: 'Consistency Master', iconUrl: '🔥', description: 'Maintain a 14-day learning streak', earnedAt: '2024-01-15' },
{ id: '4', name: 'Knowledge Seeker', iconUrl: '📚', description: 'Complete 5 courses', earnedAt: '2024-01-20' },
{ id: '5', name: 'Mentor Material', iconUrl: '🧠', description: 'Help 10 peers through AI mentor', earnedAt: '2024-01-25' },
{ id: '6', name: 'Discussion Leader', iconUrl: '💬', description: 'Start 5 discussion threads', earnedAt: '2024-01-30' }
];
export function Leaderboard({ user }: { user: User }) {
const [scope, setScope] = useState<string>(user.persona === 'corporate' ? 'cohort' : 'global');
const getRankIcon = (rank: number) => {
switch (rank) {
case 1: return <Crown className="h-5 w-5 text-yellow-500" />;
case 2: return <Medal className="h-5 w-5 text-gray-400" />;
case 3: return <Award className="h-5 w-5 text-amber-600" />;
default: return <span className="text-sm font-medium w-5 text-center">#{rank}</span>;
}
};
const formatTimeToMilestone = (seconds: number) => {
const days = Math.floor(seconds / 86400);
if (days === 0) return 'Today';
if (days === 1) return '1 day';
return `${days} days`;
};
const getScopeLabel = () => {
switch (scope) {
case 'cohort': return 'Cohort Leaderboard';
case 'org': return 'Organization Leaderboard';
case 'global': return 'Global Leaderboard';
default: return 'Leaderboard';
}
};
const progressToNextMilestone = (mockCurrentUser.exp / mockCurrentUser.milestone) * 100;
return (
<div className="p-6 space-y-6 bg-white min-h-screen">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-medium">Leaderboard</h1>
<p className="text-gray-600">Track your progress and compete with peers</p>
</div>
{user.persona === 'corporate' && (
<Select value={scope} onValueChange={setScope}>
<SelectTrigger className="w-48 bg-white">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="cohort">Cohort</SelectItem>
<SelectItem value="org">Organization</SelectItem>
<SelectItem value="global">Global</SelectItem>
</SelectContent>
</Select>
)}
</div>
{/* Current User Progress */}
<Card className="bg-gradient-to-r from-[var(--color-brand-primary)] to-blue-600 text-white border-0">
<CardContent className="p-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="space-y-2">
<p className="text-blue-100">Your Rank</p>
<div className="flex items-center gap-2">
{getRankIcon(mockCurrentUser.rank)}
<span className="text-2xl font-bold">#{mockCurrentUser.rank}</span>
</div>
</div>
<div className="space-y-2">
<p className="text-blue-100">Experience Points</p>
<div className="space-y-2">
<span className="text-2xl font-bold">{mockCurrentUser.exp.toLocaleString()} XP</span>
<Progress
value={progressToNextMilestone}
className="bg-blue-400/30 h-2"
indicatorClassName="bg-[#F8C301]"
/>
<p className="text-xs text-blue-100">
{mockCurrentUser.milestone - mockCurrentUser.exp} XP to next milestone
</p>
</div>
</div>
<div className="space-y-2">
<p className="text-blue-100">Time to Milestone</p>
<div className="flex items-center gap-2">
<Target className="h-5 w-5 text-blue-200" />
<span className="text-xl font-bold">
{formatTimeToMilestone(mockCurrentUser.timeToMilestoneSec)}
</span>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Badges Section */}
<Card className="bg-white border">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Star className="h-5 w-5 text-[var(--color-brand-accent)]" />
Your Badges ({mockBadges.length})
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
{mockBadges.map((badge) => (
<div key={badge.id} className="text-center space-y-2 p-3 rounded-lg bg-gray-50">
<div className="text-2xl">{badge.iconUrl}</div>
<div>
<p className="font-medium text-sm">{badge.name}</p>
<p className="text-xs text-gray-600">{badge.description}</p>
</div>
</div>
))}
</div>
</CardContent>
</Card>
{/* Leaderboard */}
<Card className="bg-white border">
<CardHeader>
<CardTitle>{getScopeLabel()}</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{mockLeaderboard.slice(0, 20).map((entry) => (
<div
key={entry.rank}
className={`flex items-center gap-4 p-4 rounded-lg transition-colors ${
entry.isCurrentUser
? 'bg-[var(--color-brand-primary)]/10 border border-[var(--color-brand-primary)]/20'
: 'bg-gray-50 hover:bg-gray-100'
}`}
>
<div className="flex items-center justify-center w-8">
{getRankIcon(entry.rank)}
</div>
<Avatar className="h-10 w-10">
{entry.avatar && <AvatarImage src={entry.avatar} alt={entry.userName} />}
<AvatarFallback>{entry.userName.charAt(0)}</AvatarFallback>
</Avatar>
<div className="flex-1">
<p className={`font-medium ${entry.isCurrentUser ? 'text-[var(--color-brand-primary)]' : ''}`}>
{entry.userName}
{entry.isCurrentUser && <span className="ml-2 text-sm text-gray-600">(You)</span>}
</p>
<p className="text-sm text-gray-600">
{formatTimeToMilestone(entry.timeToMilestoneSec)} to milestone
</p>
</div>
<div className="text-right space-y-1">
<p className="font-bold text-lg">{entry.exp.toLocaleString()}</p>
<p className="text-xs text-gray-600">XP</p>
</div>
{entry.badges && (
<Badge variant="secondary" className="text-xs">
{entry.badges} badges
</Badge>
)}
</div>
))}
</div>
{/* Current user position if outside top 20 */}
{mockCurrentUser.rank > 20 && (
<div className="mt-6 pt-4 border-t">
<div className="flex items-center gap-4 p-4 rounded-lg bg-[var(--color-brand-primary)]/10 border border-[var(--color-brand-primary)]/20">
<div className="flex items-center justify-center w-8">
<span className="text-sm font-medium">#{mockCurrentUser.rank}</span>
</div>
<Avatar className="h-10 w-10">
<AvatarFallback>You</AvatarFallback>
</Avatar>
<div className="flex-1">
<p className="font-medium text-[var(--color-brand-primary)]">You</p>
<p className="text-sm text-gray-600">
{formatTimeToMilestone(mockCurrentUser.timeToMilestoneSec)} to milestone
</p>
</div>
<div className="text-right">
<p className="font-bold text-lg">{mockCurrentUser.exp.toLocaleString()}</p>
<p className="text-xs text-gray-600">XP</p>
</div>
</div>
</div>
)}
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,226 @@
import React, { useState } from 'react';
import { Button } from './ui/button';
import { Progress } from './ui/progress';
import { Textarea } from './ui/textarea';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import exampleImage from 'figma:asset/6b17aafb4d0b31099f8eec7b69e7d0a8b29ad00f.png';
interface Question {
id: number;
title: string;
description: string;
prompt: string;
minWords: number;
}
interface LeadershipProfilerProps {
onBack: () => void;
}
const profilerQuestions: Question[] = [
{
id: 1,
title: "Think about leadership in the context of understanding. What kind of understanding do you consider to be essential for effective leadership?",
description: "Consider areas like team dynamics, market trends, organizational culture, stakeholder needs, and personal strengths.",
prompt: "Write about your understanding of essential leadership areas...",
minWords: 50
},
{
id: 2,
title: "Reflect on a time when you had to lead through uncertainty. How did your thinking approach help or hinder your leadership effectiveness?",
description: "Think about your decision-making process, how you communicated with your team, and what you learned from the experience.",
prompt: "Describe your experience leading through uncertainty...",
minWords: 50
},
{
id: 3,
title: "What cognitive biases do you think most commonly affect leadership decisions, and how do you work to mitigate them?",
description: "Consider confirmation bias, anchoring bias, availability heuristic, and other thinking patterns that might influence leadership choices.",
prompt: "Discuss cognitive biases in leadership and your mitigation strategies...",
minWords: 50
},
{
id: 4,
title: "How do you balance analytical thinking with intuitive decision-making in your leadership approach?",
description: "Reflect on situations where data-driven analysis was crucial versus times when gut instinct guided your decisions.",
prompt: "Explain your balance between analytical and intuitive leadership...",
minWords: 50
}
];
export function LeadershipProfiler({ onBack }: LeadershipProfilerProps) {
const [currentStep, setCurrentStep] = useState(1);
const [responses, setResponses] = useState<Record<number, string>>({});
const [currentResponse, setCurrentResponse] = useState('');
const currentQuestion = profilerQuestions[currentStep - 1];
const totalSteps = profilerQuestions.length;
const progressPercentage = (currentStep / totalSteps) * 100;
const getWordCount = (text: string) => {
return text.trim().split(/\s+/).filter(word => word.length > 0).length;
};
const getCharacterCount = (text: string) => {
return text.length;
};
const handleNext = () => {
// Save current response
setResponses(prev => ({
...prev,
[currentStep]: currentResponse
}));
if (currentStep < totalSteps) {
setCurrentStep(currentStep + 1);
setCurrentResponse(responses[currentStep + 1] || '');
} else {
// Completed all questions
alert('Profiler completed! Your responses have been saved.');
// Return to course content instead of navigating away
onBack();
}
};
const handlePrevious = () => {
if (currentStep > 1) {
// Save current response
setResponses(prev => ({
...prev,
[currentStep]: currentResponse
}));
setCurrentStep(currentStep - 1);
setCurrentResponse(responses[currentStep - 1] || '');
}
};
// Load saved response when component mounts or step changes
React.useEffect(() => {
setCurrentResponse(responses[currentStep] || '');
}, [currentStep, responses]);
const wordCount = getWordCount(currentResponse);
const characterCount = getCharacterCount(currentResponse);
const isMinimumMet = wordCount >= currentQuestion.minWords;
return (
<div className="h-full bg-background">
{/* Header Section */}
<div className="bg-[var(--color-brand-primary)] text-white">
<div className="px-6 py-4">
<div className="mb-4">
<h1 className="text-[24px] font-semibold">
Thinking Orientation Reflection Exercise
</h1>
</div>
<p className="text-[16px] text-white/90 mb-6 leading-relaxed">
Based on your learning and current thinking state reflect on the questions and think of your responses
in the context of thinking with the understanding that they will be used for generating insights.
</p>
<div className="flex items-center justify-between">
<span className="text-[16px]">Step {currentStep} of {totalSteps}</span>
<span className="text-[16px]">{Math.round(progressPercentage)}% Complete</span>
</div>
<div className="mt-3">
<Progress
value={progressPercentage}
className="h-2 bg-white/20"
indicatorClassName="bg-white"
/>
</div>
</div>
</div>
{/* Content Section */}
<div className="flex-1 px-6 py-8">
<div className="max-w-4xl mx-auto">
{/* Question Card */}
<div className="bg-white rounded-lg border border-border shadow-sm mb-8">
<div className="p-6">
<div className="flex items-start gap-4 mb-6">
<div className="w-8 h-8 rounded-full bg-[var(--color-brand-primary)] flex items-center justify-center flex-shrink-0">
<span className="text-white text-[14px] font-medium">Q{currentQuestion.id}</span>
</div>
<div className="flex-1">
<h2 className="text-[20px] font-medium text-foreground mb-3 leading-relaxed">
{currentQuestion.title}
</h2>
<p className="text-[16px] text-muted-foreground">
{currentQuestion.description}
</p>
</div>
</div>
<div className="space-y-4">
<Textarea
placeholder={currentQuestion.prompt}
value={currentResponse}
onChange={(e) => setCurrentResponse(e.target.value)}
className="min-h-[200px] resize-none text-[16px] leading-relaxed"
/>
<div className="flex items-center justify-between text-[14px]">
<div className="flex items-center gap-4">
<span className={wordCount >= currentQuestion.minWords ? 'text-[#21A36A]' : 'text-[#F59E0B]'}>
Word count: {wordCount} {wordCount < currentQuestion.minWords && `(minimum ${currentQuestion.minWords} words)`}
</span>
</div>
<span className="text-muted-foreground">
Character count: {characterCount}
</span>
</div>
</div>
</div>
</div>
{/* Navigation */}
<div className="flex items-center justify-between">
<Button
variant="ghost"
onClick={handlePrevious}
disabled={currentStep === 1}
className="flex items-center gap-2 text-muted-foreground hover:text-foreground"
>
<ChevronLeft className="h-4 w-4" />
Previous
</Button>
<div className="flex items-center gap-2">
<span className="text-[14px] text-muted-foreground">
Step {currentStep} of {totalSteps}
</span>
<div className="flex gap-1">
{Array.from({ length: totalSteps }, (_, i) => (
<div
key={i}
className={`w-2 h-2 rounded-full ${
i + 1 === currentStep
? 'bg-[var(--color-brand-primary)]'
: i + 1 < currentStep
? 'bg-[#21A36A]'
: 'bg-gray-300'
}`}
/>
))}
</div>
</div>
<Button
onClick={handleNext}
disabled={!isMinimumMet}
className="flex items-center gap-2 bg-[#8B7BE8] hover:bg-[#7A6AE6] text-white"
>
{currentStep === totalSteps ? 'Complete' : 'Next'}
{currentStep < totalSteps && <ChevronRight className="h-4 w-4" />}
</Button>
</div>
</div>
</div>
</div>
);
}

441
src/components/Library.tsx Normal file
View File

@@ -0,0 +1,441 @@
import React, { useState } from 'react';
import {
Search,
Filter,
Grid3X3,
List,
Clock,
Star,
Users,
Play,
Book,
Video,
FileText,
Headphones,
Download
} from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Badge } from './ui/badge';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from './ui/select';
import { Checkbox } from './ui/checkbox';
import { Separator } from './ui/separator';
import { AspectRatio } from './ui/aspect-ratio';
import { ImageWithFallback } from './figma/ImageWithFallback';
import { useAppContext } from './AppShell';
// Mock data for library resources
const mockLibraryData = {
courses: [
{
id: '1',
title: 'Strategic Leadership Development',
programName: 'Executive Leadership Programme',
type: 'course',
level: 'Advanced',
duration: '8 weeks',
rating: 4.8,
ratingCount: 156,
currentLearners: 89,
progress: 65,
instructor: 'Dr. Rajesh Kumar',
thumbnail: 'https://images.unsplash.com/photo-1552664730-d307ca884978?w=400',
status: 'in-progress',
tags: ['Leadership', 'Strategy', 'Management']
},
{
id: '2',
title: 'Financial Management for Leaders',
programName: 'Leadership Foundation Programme',
type: 'course',
level: 'Intermediate',
duration: '6 weeks',
rating: 4.6,
ratingCount: 203,
currentLearners: 67,
progress: 0,
instructor: 'Prof. Priya Sharma',
thumbnail: 'https://images.unsplash.com/photo-1554224155-6726b3ff858f?w=400',
status: 'available',
tags: ['Finance', 'Leadership', 'Analytics']
},
{
id: '3',
title: 'Team Building Essentials',
programName: 'Management Skills Programme',
type: 'course',
level: 'Beginner',
duration: '4 weeks',
rating: 4.9,
ratingCount: 89,
currentLearners: 134,
progress: 100,
instructor: 'Mr. Arjun Mehta',
thumbnail: 'https://images.unsplash.com/photo-1522202176988-66273c2fd55f?w=400',
status: 'completed',
tags: ['Team Building', 'Communication', 'Leadership']
}
],
webinars: [
{
id: 'w1',
title: 'Future of Leadership in Digital Age',
type: 'webinar',
date: '2024-09-15T14:00:00Z',
duration: '90 minutes',
speaker: 'Dr. Anita Singh',
attendees: 245,
status: 'upcoming',
thumbnail: 'https://images.unsplash.com/photo-1560472354-b33ff0c44a43?w=400',
tags: ['Digital Transformation', 'Future Trends']
}
],
resources: [
{
id: 'r1',
title: 'Leadership Assessment Framework',
type: 'document',
fileType: 'PDF',
size: '2.3 MB',
downloads: 456,
thumbnail: 'https://images.unsplash.com/photo-1586953208448-b95a79798f07?w=400',
tags: ['Assessment', 'Framework']
},
{
id: 'r2',
title: 'Decision Making Podcast Series',
type: 'audio',
duration: '45 minutes',
episodes: 8,
thumbnail: 'https://images.unsplash.com/photo-1478737270239-2f02b77fc618?w=400',
tags: ['Decision Making', 'Podcast']
}
]
};
export function Library() {
const { user } = useAppContext();
const [searchQuery, setSearchQuery] = useState('');
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
const [selectedFilters, setSelectedFilters] = useState({
type: 'all',
level: 'all',
status: 'all',
tags: [] as string[]
});
const allResources = [
...mockLibraryData.courses,
...mockLibraryData.webinars,
...mockLibraryData.resources
];
const getResourceIcon = (type: string) => {
switch (type) {
case 'course': return Book;
case 'webinar': return Video;
case 'document': return FileText;
case 'audio': return Headphones;
default: return Book;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'completed': return 'bg-green-100 text-green-800';
case 'in-progress': return 'bg-blue-100 text-blue-800';
case 'available': return 'bg-gray-100 text-gray-800';
case 'upcoming': return 'bg-yellow-100 text-yellow-800';
default: return 'bg-gray-100 text-gray-800';
}
};
const ResourceCard = ({ resource }: { resource: any }) => {
const Icon = getResourceIcon(resource.type);
return (
<Card className="overflow-hidden hover:shadow-md transition-shadow">
<div className="relative">
<AspectRatio ratio={16 / 9}>
<ImageWithFallback
src={resource.thumbnail}
alt={resource.title}
className="object-cover w-full h-full"
/>
</AspectRatio>
<div className="absolute top-2 left-2">
<Badge variant="secondary" className="capitalize">
<Icon className="h-3 w-3 mr-1" />
{resource.type}
</Badge>
</div>
{resource.status && (
<div className="absolute top-2 right-2">
<Badge className={`capitalize ${getStatusColor(resource.status)}`}>
{resource.status.replace('-', ' ')}
</Badge>
</div>
)}
</div>
<CardContent className="p-4 space-y-3">
{resource.programName && (
<p className="text-xs text-muted-foreground">{resource.programName}</p>
)}
<h3 className="font-medium line-clamp-2">{resource.title}</h3>
<div className="flex flex-wrap gap-1">
{resource.tags?.slice(0, 2).map((tag: string) => (
<Badge key={tag} variant="outline" className="text-xs">
{tag}
</Badge>
))}
</div>
<div className="flex items-center justify-between text-sm text-muted-foreground">
{resource.duration && (
<div className="flex items-center gap-1">
<Clock className="h-3 w-3" />
{resource.duration}
</div>
)}
{resource.rating && (
<div className="flex items-center gap-1">
<Star className="h-3 w-3 fill-yellow-400 text-yellow-400" />
{resource.rating} ({resource.ratingCount})
</div>
)}
{resource.currentLearners && (
<div className="flex items-center gap-1">
<Users className="h-3 w-3" />
{resource.currentLearners}
</div>
)}
</div>
{resource.progress !== undefined && resource.progress > 0 && (
<div className="space-y-1">
<div className="flex justify-between text-xs">
<span>Progress</span>
<span>{resource.progress}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div
className="bg-[#04045B] h-1.5 rounded-full"
style={{ width: `${resource.progress}%` }}
/>
</div>
</div>
)}
<div className="flex gap-2 pt-2">
{resource.status === 'completed' ? (
<Button variant="outline" size="sm" className="flex-1">
Recap Course
</Button>
) : resource.type === 'course' ? (
<Button size="sm" className="flex-1">
<Play className="h-3 w-3 mr-1" />
{resource.progress > 0 ? 'Continue' : 'Start Course'}
</Button>
) : resource.type === 'document' ? (
<Button size="sm" className="flex-1">
<Download className="h-3 w-3 mr-1" />
Download
</Button>
) : (
<Button size="sm" className="flex-1">
View
</Button>
)}
</div>
</CardContent>
</Card>
);
};
return (
<div className="p-6 space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-medium">Library</h1>
<p className="text-muted-foreground">Explore courses, webinars, and resources</p>
</div>
<div className="flex items-center gap-2">
<Button
variant={viewMode === 'grid' ? 'default' : 'outline'}
size="icon"
onClick={() => setViewMode('grid')}
>
<Grid3X3 className="h-4 w-4" />
</Button>
<Button
variant={viewMode === 'list' ? 'default' : 'outline'}
size="icon"
onClick={() => setViewMode('list')}
>
<List className="h-4 w-4" />
</Button>
</div>
</div>
<div className="flex gap-6">
{/* Filters Sidebar */}
<div className="w-64 space-y-6">
<Card>
<CardHeader>
<CardTitle className="text-lg">Filters</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Search */}
<div className="space-y-2">
<label className="text-sm font-medium">Search</label>
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search library..."
className="pl-10"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
</div>
<Separator />
{/* Resource Type */}
<div className="space-y-2">
<label className="text-sm font-medium">Type</label>
<Select value={selectedFilters.type} onValueChange={(value) =>
setSelectedFilters(prev => ({ ...prev, type: value }))
}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Types</SelectItem>
<SelectItem value="course">Courses</SelectItem>
<SelectItem value="webinar">Webinars</SelectItem>
<SelectItem value="document">Documents</SelectItem>
<SelectItem value="audio">Audio</SelectItem>
</SelectContent>
</Select>
</div>
{/* Level */}
<div className="space-y-2">
<label className="text-sm font-medium">Level</label>
<Select value={selectedFilters.level} onValueChange={(value) =>
setSelectedFilters(prev => ({ ...prev, level: value }))
}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Levels</SelectItem>
<SelectItem value="beginner">Beginner</SelectItem>
<SelectItem value="intermediate">Intermediate</SelectItem>
<SelectItem value="advanced">Advanced</SelectItem>
</SelectContent>
</Select>
</div>
{/* Status */}
<div className="space-y-2">
<label className="text-sm font-medium">Status</label>
<Select value={selectedFilters.status} onValueChange={(value) =>
setSelectedFilters(prev => ({ ...prev, status: value }))
}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="available">Available</SelectItem>
<SelectItem value="in-progress">In Progress</SelectItem>
<SelectItem value="completed">Completed</SelectItem>
<SelectItem value="upcoming">Upcoming</SelectItem>
</SelectContent>
</Select>
</div>
{/* Popular Tags */}
<div className="space-y-2">
<label className="text-sm font-medium">Popular Tags</label>
<div className="space-y-2">
{['Leadership', 'Strategy', 'Finance', 'Communication', 'Team Building'].map((tag) => (
<div key={tag} className="flex items-center space-x-2">
<Checkbox
id={tag}
checked={selectedFilters.tags.includes(tag)}
onCheckedChange={(checked) => {
if (checked) {
setSelectedFilters(prev => ({
...prev,
tags: [...prev.tags, tag]
}));
} else {
setSelectedFilters(prev => ({
...prev,
tags: prev.tags.filter(t => t !== tag)
}));
}
}}
/>
<label htmlFor={tag} className="text-sm">{tag}</label>
</div>
))}
</div>
</div>
</CardContent>
</Card>
</div>
{/* Main Content */}
<div className="flex-1 space-y-6">
{/* Results Header */}
<div className="flex items-center justify-between">
<p className="text-muted-foreground">
Showing {allResources.length} results
</p>
<Select defaultValue="relevance">
<SelectTrigger className="w-48">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="relevance">Sort by Relevance</SelectItem>
<SelectItem value="newest">Newest First</SelectItem>
<SelectItem value="rating">Highest Rated</SelectItem>
<SelectItem value="popular">Most Popular</SelectItem>
</SelectContent>
</Select>
</div>
{/* Resource Grid */}
<div className={`grid gap-6 ${
viewMode === 'grid'
? 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3'
: 'grid-cols-1'
}`}>
{allResources.map((resource) => (
<ResourceCard key={resource.id} resource={resource} />
))}
</div>
</div>
</div>
</div>
);
}

1866
src/components/MyCourses.tsx Normal file

File diff suppressed because it is too large Load Diff

387
src/components/Notes.tsx Normal file
View File

@@ -0,0 +1,387 @@
import React, { useState } from 'react';
import {
Search,
Plus,
Edit,
Trash2,
ExternalLink,
Calendar,
Book,
StickyNote,
Filter
} from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Badge } from './ui/badge';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from './ui/select';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger
} from './ui/dialog';
import { Textarea } from './ui/textarea';
import { Separator } from './ui/separator';
import { useAppContext } from './AppShell';
// Mock data for notes
const mockNotesData = [
{
id: '1',
courseId: 'course-1',
courseTitle: 'Strategic Leadership Development',
moduleId: 'module-3',
moduleTitle: 'Decision Making Framework',
lessonId: 'lesson-2',
lessonTitle: 'Risk Assessment Strategies',
content: 'Key insight: Risk assessment should consider both quantitative metrics and qualitative factors. The SWOT analysis framework is particularly useful for strategic decisions.\n\nRemember to:\n- Involve stakeholders in the assessment process\n- Document assumptions clearly\n- Review decisions periodically',
createdAt: '2024-09-02T14:30:00Z',
updatedAt: '2024-09-02T14:30:00Z',
tags: ['risk-assessment', 'decision-making', 'strategy']
},
{
id: '2',
courseId: 'course-1',
courseTitle: 'Strategic Leadership Development',
moduleId: 'module-2',
moduleTitle: 'Leadership Communication',
lessonId: 'lesson-4',
lessonTitle: 'Effective Presentation Skills',
content: 'Presentation framework:\n1. Hook - grab attention in first 30 seconds\n2. Overview - tell them what you\'ll tell them\n3. Body - 3 main points maximum\n4. Summary - tell them what you told them\n5. Call to action - what do you want them to do?\n\nPractice with the "rule of 3" - people remember things in threes.',
createdAt: '2024-08-28T10:15:00Z',
updatedAt: '2024-08-28T10:15:00Z',
tags: ['communication', 'presentations', 'leadership']
},
{
id: '3',
courseId: 'course-2',
courseTitle: 'Team Building Essentials',
moduleId: 'module-1',
moduleTitle: 'Understanding Team Dynamics',
lessonId: 'lesson-1',
lessonTitle: 'Tuckman\'s Team Development Model',
content: 'The 4 stages of team development:\n\n1. FORMING - Team members are polite, excited, anxious\n2. STORMING - Conflicts arise, different opinions emerge\n3. NORMING - Team establishes norms and ways of working\n4. PERFORMING - Team works effectively towards goals\n\nAs a leader, my role changes in each stage. In forming, I need to provide clear direction. In storming, I mediate conflicts.',
createdAt: '2024-08-25T16:45:00Z',
updatedAt: '2024-08-25T16:45:00Z',
tags: ['team-development', 'tuckman-model', 'leadership']
},
{
id: '4',
courseId: 'course-2',
courseTitle: 'Team Building Essentials',
moduleId: 'module-2',
moduleTitle: 'Conflict Resolution',
lessonId: 'lesson-3',
lessonTitle: 'Mediation Techniques',
content: 'Thomas-Kilmann Conflict Mode Instrument - 5 approaches:\n\n• Competing (assertive, uncooperative)\n• Accommodating (unassertive, cooperative)\n• Avoiding (unassertive, uncooperative)\n• Collaborating (assertive, cooperative) ← BEST for most situations\n• Compromising (moderate assertive & cooperative)\n\nNeed to practice active listening more in conflicts.',
createdAt: '2024-08-20T11:20:00Z',
updatedAt: '2024-08-22T09:30:00Z',
tags: ['conflict-resolution', 'mediation', 'thomas-kilmann']
}
];
export function Notes() {
const { user } = useAppContext();
const [searchQuery, setSearchQuery] = useState('');
const [selectedCourse, setSelectedCourse] = useState('all');
const [sortBy, setSortBy] = useState('newest');
const [editingNote, setEditingNote] = useState<any>(null);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString('en-IN', {
day: '2-digit',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
timeZone: 'Asia/Kolkata'
});
};
const getUniqueCoursesFromNotes = () => {
const courses = mockNotesData.reduce((acc, note) => {
if (!acc.find(c => c.id === note.courseId)) {
acc.push({
id: note.courseId,
title: note.courseTitle
});
}
return acc;
}, [] as any[]);
return courses;
};
const filteredNotes = mockNotesData.filter(note => {
const matchesSearch = searchQuery === '' ||
note.content.toLowerCase().includes(searchQuery.toLowerCase()) ||
note.courseTitle.toLowerCase().includes(searchQuery.toLowerCase()) ||
note.moduleTitle.toLowerCase().includes(searchQuery.toLowerCase()) ||
note.lessonTitle.toLowerCase().includes(searchQuery.toLowerCase()) ||
note.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase()));
const matchesCourse = selectedCourse === 'all' || note.courseId === selectedCourse;
return matchesSearch && matchesCourse;
});
const sortedNotes = [...filteredNotes].sort((a, b) => {
switch (sortBy) {
case 'newest':
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
case 'oldest':
return new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime();
case 'course':
return a.courseTitle.localeCompare(b.courseTitle);
default:
return 0;
}
});
const NoteCard = ({ note }: { note: any }) => {
const previewText = note.content.length > 200
? note.content.substring(0, 200) + '...'
: note.content;
return (
<Card className="group hover:shadow-lg transition-all duration-300 border border-gray-200 bg-white">
<CardContent className="p-6">
<div className="space-y-5">
{/* Header with course path */}
<div className="flex items-start justify-between gap-4">
<div className="flex items-start gap-3 flex-1">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-[#04045B] to-[#030344] flex items-center justify-center flex-shrink-0">
<Book className="h-5 w-5 text-white" />
</div>
<div className="space-y-2 flex-1">
<div className="bg-blue-50 rounded-lg p-3 border border-blue-100">
<div className="flex items-center gap-2 text-sm text-gray-700">
<span className="font-medium text-[#04045B]">{note.courseTitle}</span>
<span className="text-gray-400"></span>
<span className="text-gray-600">{note.moduleTitle}</span>
<span className="text-gray-400"></span>
<span className="text-gray-600">{note.lessonTitle}</span>
</div>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<Calendar className="h-4 w-4 text-gray-400" />
<span>
{note.updatedAt !== note.createdAt ? 'Updated' : 'Created'} on {formatDate(note.updatedAt)}
</span>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => {
// TODO: Open lesson
}}
className="gap-2"
>
<ExternalLink className="h-4 w-4" />
Open
</Button>
<Button
size="sm"
variant="outline"
onClick={() => {
setEditingNote(note);
setIsDialogOpen(true);
}}
className="gap-2"
>
<Edit className="h-4 w-4" />
Edit
</Button>
<Button
size="sm"
variant="outline"
onClick={() => {
// TODO: Delete note
}}
className="gap-2 text-red-600 hover:text-red-700 hover:border-red-200 hover:bg-red-50"
>
<Trash2 className="h-4 w-4" />
Delete
</Button>
</div>
</div>
{/* Note Content */}
<div className="bg-gray-50 rounded-lg p-4 border border-gray-100">
<p className="text-sm leading-relaxed text-gray-700 whitespace-pre-line">
{previewText}
</p>
</div>
{/* Tags */}
{note.tags && note.tags.length > 0 && (
<div className="flex flex-wrap gap-2">
{note.tags.map((tag: string) => (
<Badge
key={tag}
variant="outline"
className="text-xs font-medium bg-[#F8C301]/10 text-[#04045B] border-[#F8C301]/30"
>
#{tag}
</Badge>
))}
</div>
)}
</div>
</CardContent>
</Card>
);
};
return (
<div className="p-6 space-y-8 bg-white min-h-screen">
{/* Header */}
<div className="space-y-2">
<h1 className="text-3xl font-semibold text-gray-900">My Notes</h1>
<p className="text-gray-600">
Centralized view of all your learning notes and insights
</p>
</div>
{/* Note Editor Dialog */}
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>
{editingNote ? 'Edit Note' : 'Add New Note'}
</DialogTitle>
<DialogDescription>
{editingNote ? 'Make changes to your note below.' : 'Create a new note to capture important insights from your learning.'}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* TODO: Add note editor form */}
<Textarea
placeholder="Enter your note..."
className="min-h-[200px]"
defaultValue={editingNote?.content || ''}
/>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>
Cancel
</Button>
<Button onClick={() => setIsDialogOpen(false)}>
{editingNote ? 'Save Changes' : 'Save Note'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* Filters and Search */}
<div className="bg-gradient-to-r from-gray-50 to-blue-50/30 p-6 rounded-2xl border border-gray-100 shadow-sm">
<div className="flex flex-col sm:flex-row gap-4">
<div className="relative flex-1 max-w-lg">
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input
placeholder="Search notes, courses, or tags..."
className="pl-12 h-12 bg-white border-gray-200 shadow-sm focus:ring-2 focus:ring-[#04045B]/20 focus:border-[#04045B] transition-all"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<div className="flex gap-3">
<Select value={selectedCourse} onValueChange={setSelectedCourse}>
<SelectTrigger className="w-52 h-12 bg-white border-gray-200 shadow-sm">
<Filter className="h-4 w-4 mr-2 text-gray-400" />
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Courses</SelectItem>
{getUniqueCoursesFromNotes().map((course) => (
<SelectItem key={course.id} value={course.id}>
{course.title}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="w-44 h-12 bg-white border-gray-200 shadow-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="newest">Newest First</SelectItem>
<SelectItem value="oldest">Oldest First</SelectItem>
<SelectItem value="course">By Course</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
{/* Stats */}
{/* Results */}
<div className="space-y-6">
<div className="flex items-center justify-between bg-white p-4 rounded-lg border border-gray-100 shadow-sm">
<div className="flex items-center gap-3">
<div className="w-2 h-2 bg-[#04045B] rounded-full"></div>
<p className="text-gray-700 font-medium">
Showing {sortedNotes.length} of {mockNotesData.length} notes
</p>
</div>
{sortedNotes.length > 0 && (
<div className="text-sm text-gray-500">
{sortBy === 'newest' ? 'Most recent first' :
sortBy === 'oldest' ? 'Oldest first' :
'Grouped by course'}
</div>
)}
</div>
{sortedNotes.length === 0 ? (
<Card className="border-0 shadow-lg">
<CardContent className="p-12 text-center">
<div className="w-20 h-20 mx-auto mb-6 bg-gradient-to-br from-[#04045B]/10 to-blue-50 rounded-full flex items-center justify-center">
<StickyNote className="h-10 w-10 text-[#04045B]" />
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">No notes found</h3>
<p className="text-gray-600 mb-6 max-w-md mx-auto">
{searchQuery || selectedCourse !== 'all'
? 'Try adjusting your search criteria or filters to find more notes'
: 'Start taking notes during your lessons to capture important insights'
}
</p>
{(!searchQuery && selectedCourse === 'all') && (
<Button className="bg-[#04045B] hover:bg-[#030344] text-white px-6 py-2.5">
<Plus className="h-4 w-4 mr-2" />
Add Your First Note
</Button>
)}
</CardContent>
</Card>
) : (
<div className="space-y-5">
{sortedNotes.map((note) => (
<NoteCard key={note.id} note={note} />
))}
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,367 @@
import React, { useState } from 'react';
import {
Bell,
Filter,
Mail,
CheckCheck,
Calendar,
BookOpen,
MessageSquare,
AlertTriangle,
CreditCard,
Clock,
Search
} from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Badge } from './ui/badge';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from './ui/select';
import { Checkbox } from './ui/checkbox';
import { Separator } from './ui/separator';
import { useAppContext } from './AppShell';
// Mock notifications data
const mockNotifications = [
{
id: '1',
type: 'module-due',
title: 'Module Due This Week',
message: 'Module 3: Decision Making Framework is due in 2 days',
timestamp: '2024-09-03T10:30:00Z',
read: false,
courseTitle: 'Strategic Leadership Development',
priority: 'high'
},
{
id: '2',
type: 'new-article',
title: 'New Research Paper Available',
message: 'New article: "The Future of Remote Leadership" has been added to your library',
timestamp: '2024-09-03T09:15:00Z',
read: false,
priority: 'medium'
},
{
id: '3',
type: 'discussion',
title: 'New Discussion Reply',
message: 'Dr. Kumar replied to your comment in "Risk Assessment Strategies" discussion',
timestamp: '2024-09-02T16:45:00Z',
read: true,
courseTitle: 'Strategic Leadership Development',
priority: 'low'
},
{
id: '4',
type: 'course-assigned',
title: 'New Course Assigned',
message: 'Your organization has enrolled you in "Financial Management for Leaders"',
timestamp: '2024-09-02T14:20:00Z',
read: false,
courseTitle: 'Financial Management for Leaders',
priority: 'high'
},
{
id: '5',
type: 'overdue',
title: 'Assignment Overdue',
message: 'Leadership Assessment for Module 2 is now overdue',
timestamp: '2024-09-01T18:00:00Z',
read: true,
courseTitle: 'Strategic Leadership Development',
priority: 'high'
},
{
id: '6',
type: 'webinar',
title: 'Webinar Reminder',
message: 'Webinar "Future of Leadership in Digital Age" starts in 1 hour',
timestamp: '2024-09-01T13:00:00Z',
read: true,
priority: 'medium'
}
];
export function Notifications() {
const { user } = useAppContext();
const [notifications, setNotifications] = useState(mockNotifications);
const [searchQuery, setSearchQuery] = useState('');
const [filterType, setFilterType] = useState('all');
const [selectedNotifications, setSelectedNotifications] = useState<string[]>([]);
const getNotificationIcon = (type: string) => {
switch (type) {
case 'module-due':
case 'overdue':
return Calendar;
case 'new-article':
return BookOpen;
case 'discussion':
return MessageSquare;
case 'course-assigned':
case 'course-purchased':
return CreditCard;
case 'webinar':
return Clock;
default:
return Bell;
}
};
const getNotificationColor = (type: string, priority: string) => {
if (priority === 'high') return 'border-l-red-500';
if (priority === 'medium') return 'border-l-yellow-500';
return 'border-l-blue-500';
};
const formatTime = (timestamp: string) => {
const now = new Date();
const notificationTime = new Date(timestamp);
const diffInHours = (now.getTime() - notificationTime.getTime()) / (1000 * 60 * 60);
if (diffInHours < 1) {
const diffInMinutes = Math.floor(diffInHours * 60);
return `${diffInMinutes}m ago`;
} else if (diffInHours < 24) {
return `${Math.floor(diffInHours)}h ago`;
} else {
const diffInDays = Math.floor(diffInHours / 24);
return `${diffInDays}d ago`;
}
};
const filteredNotifications = notifications.filter(notification => {
const matchesSearch = searchQuery === '' ||
notification.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
notification.message.toLowerCase().includes(searchQuery.toLowerCase()) ||
notification.courseTitle?.toLowerCase().includes(searchQuery.toLowerCase());
const matchesType = filterType === 'all' ||
(filterType === 'unread' && !notification.read) ||
(filterType === 'overdue' && notification.type === 'overdue') ||
(filterType === 'this-week' && ['module-due'].includes(notification.type)) ||
(filterType === 'this-month' && ['module-due', 'course-assigned'].includes(notification.type)) ||
notification.type === filterType;
return matchesSearch && matchesType;
});
const markAsRead = (id: string) => {
setNotifications(prev =>
prev.map(notification =>
notification.id === id
? { ...notification, read: true }
: notification
)
);
};
const markAllAsRead = () => {
setNotifications(prev =>
prev.map(notification => ({ ...notification, read: true }))
);
};
const toggleNotificationSelection = (id: string) => {
setSelectedNotifications(prev =>
prev.includes(id)
? prev.filter(nId => nId !== id)
: [...prev, id]
);
};
const markSelectedAsRead = () => {
setNotifications(prev =>
prev.map(notification =>
selectedNotifications.includes(notification.id)
? { ...notification, read: true }
: notification
)
);
setSelectedNotifications([]);
};
const unreadCount = notifications.filter(n => !n.read).length;
return (
<div className="p-6 space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-medium">Notifications</h1>
<p className="text-muted-foreground">
Stay updated with your learning progress and activities
</p>
</div>
<div className="flex items-center gap-2">
<Badge variant={unreadCount > 0 ? "destructive" : "secondary"}>
{unreadCount} unread
</Badge>
<Button variant="outline" onClick={markAllAsRead}>
<CheckCheck className="h-4 w-4 mr-2" />
Mark all as read
</Button>
</div>
</div>
{/* Filters */}
<div className="flex flex-col sm:flex-row gap-4">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search notifications..."
className="pl-10"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<div className="flex gap-2">
<Select value={filterType} onValueChange={setFilterType}>
<SelectTrigger className="w-48">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Notifications</SelectItem>
<SelectItem value="unread">Unread</SelectItem>
<SelectItem value="new-article">New Articles</SelectItem>
<SelectItem value="discussion">Discussions</SelectItem>
<SelectItem value="this-week">Due This Week</SelectItem>
<SelectItem value="this-month">Due This Month</SelectItem>
<SelectItem value="overdue">Overdue</SelectItem>
<SelectItem value="course-assigned">Course Assigned</SelectItem>
</SelectContent>
</Select>
{selectedNotifications.length > 0 && (
<Button variant="outline" onClick={markSelectedAsRead}>
Mark Selected as Read ({selectedNotifications.length})
</Button>
)}
</div>
</div>
{/* Notifications List */}
<div className="space-y-2">
{filteredNotifications.length === 0 ? (
<Card>
<CardContent className="p-8 text-center">
<Bell className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
<h3 className="font-medium mb-2">No notifications found</h3>
<p className="text-sm text-muted-foreground">
{searchQuery || filterType !== 'all'
? 'Try adjusting your search or filters'
: 'You\'re all caught up!'
}
</p>
</CardContent>
</Card>
) : (
filteredNotifications.map((notification) => {
const Icon = getNotificationIcon(notification.type);
const isSelected = selectedNotifications.includes(notification.id);
return (
<Card
key={notification.id}
className={`transition-all hover:shadow-sm border-l-4 ${
getNotificationColor(notification.type, notification.priority)
} ${notification.read ? 'opacity-60' : ''} ${
isSelected ? 'bg-blue-50' : ''
}`}
>
<CardContent className="p-4">
<div className="flex items-start gap-3">
<Checkbox
checked={isSelected}
onCheckedChange={() => toggleNotificationSelection(notification.id)}
className="mt-1"
/>
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${
notification.priority === 'high' ? 'bg-red-100 text-red-600' :
notification.priority === 'medium' ? 'bg-yellow-100 text-yellow-600' :
'bg-blue-100 text-blue-600'
}`}>
<Icon className="h-5 w-5" />
</div>
<div className="flex-1 space-y-1">
<div className="flex items-start justify-between">
<h3 className={`font-medium ${!notification.read ? 'text-foreground' : 'text-muted-foreground'}`}>
{notification.title}
</h3>
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">
{formatTime(notification.timestamp)}
</span>
{!notification.read && (
<Button
size="sm"
variant="ghost"
onClick={() => markAsRead(notification.id)}
className="h-6 px-2"
>
<Mail className="h-3 w-3" />
</Button>
)}
</div>
</div>
<p className="text-sm text-muted-foreground">
{notification.message}
</p>
{notification.courseTitle && (
<div className="flex items-center gap-1 mt-1">
<BookOpen className="h-3 w-3 text-muted-foreground" />
<span className="text-xs text-muted-foreground">
{notification.courseTitle}
</span>
</div>
)}
<div className="flex items-center justify-between pt-2">
<Badge
variant="outline"
className={`text-xs capitalize ${
notification.priority === 'high' ? 'border-red-200 text-red-700' :
notification.priority === 'medium' ? 'border-yellow-200 text-yellow-700' :
'border-blue-200 text-blue-700'
}`}
>
{notification.type.replace('-', ' ')}
</Badge>
{!notification.read && (
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
)}
</div>
</div>
</div>
</CardContent>
</Card>
);
})
)}
</div>
{/* Load More */}
{filteredNotifications.length > 0 && (
<div className="text-center">
<Button variant="outline">
Load More Notifications
</Button>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,522 @@
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
import { Button } from './ui/button';
import { Badge } from './ui/badge';
import { Progress } from './ui/progress';
import {
ChevronRight,
Play,
Pause,
Volume2,
Maximize,
Download,
Share,
BookmarkPlus,
Clock,
Video,
FileVideo,
SkipBack,
SkipForward,
Settings,
Subtitles,
Wifi,
WifiOff
} from 'lucide-react';
// Mock offline video data
interface OfflineVideoData {
id: string;
title: string;
subtitle: string;
description: string;
duration: string;
category: string;
instructor: {
name: string;
title: string;
bio: string;
avatar: string;
};
videoUrl: string;
downloadUrl: string;
fileSize: string;
resolution: string;
isDownloaded: boolean;
downloadProgress: number;
progress: number;
lastWatched: string;
thumbnail: string;
chapters: VideoChapter[];
transcriptAvailable: boolean;
subtitlesAvailable: boolean;
}
interface VideoChapter {
id: string;
title: string;
startTime: string;
duration: string;
isCompleted: boolean;
}
const videoDatabase: Record<string, OfflineVideoData> = {
'video-1': {
id: 'video-1',
title: 'Effective Communication Masterclass',
subtitle: 'Essential Skills for Professional Success',
description: 'Master the art of effective communication with this comprehensive video tutorial. Learn proven techniques for clear messaging, active listening, and persuasive presentation that will transform your professional interactions.',
duration: '45 minutes',
category: 'Communication Skills',
instructor: {
name: 'Jennifer Martinez',
title: 'Communication Expert',
bio: 'Jennifer is a renowned communication consultant with 15+ years of experience training executives and teams at Fortune 500 companies.',
avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=100&h=100&fit=crop'
},
videoUrl: 'https://example.com/video.mp4',
downloadUrl: 'https://example.com/download/video.mp4',
fileSize: '850 MB',
resolution: '1080p',
isDownloaded: true,
downloadProgress: 100,
progress: 67,
lastWatched: '2024-03-15T10:30:00Z',
thumbnail: 'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=800&h=450&fit=crop',
transcriptAvailable: true,
subtitlesAvailable: true,
chapters: [
{
id: 'chapter-1',
title: 'Introduction to Effective Communication',
startTime: '0:00',
duration: '8 min',
isCompleted: true
},
{
id: 'chapter-2',
title: 'Verbal Communication Techniques',
startTime: '8:00',
duration: '12 min',
isCompleted: true
},
{
id: 'chapter-3',
title: 'Non-Verbal Communication',
startTime: '20:00',
duration: '10 min',
isCompleted: true
},
{
id: 'chapter-4',
title: 'Active Listening Skills',
startTime: '30:00',
duration: '8 min',
isCompleted: false
},
{
id: 'chapter-5',
title: 'Presentation Techniques',
startTime: '38:00',
duration: '7 min',
isCompleted: false
}
]
}
};
interface OfflineVideoDetailPageProps {
videoId: string;
onNavigateBack: () => void;
}
export function OfflineVideoDetailPage({ videoId, onNavigateBack }: OfflineVideoDetailPageProps) {
const [videoData] = useState<OfflineVideoData>(videoDatabase[videoId] || videoDatabase['video-1']);
const [isPlaying, setIsPlaying] = useState(false);
const [showTranscript, setShowTranscript] = useState(false);
const [selectedChapter, setSelectedChapter] = useState<string | null>(null);
const formatLastWatched = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
const handleDownload = () => {
// In a real app, this would trigger the download
console.log('Downloading video...');
};
const jumpToChapter = (startTime: string) => {
// In a real app, this would seek to the specific time
console.log(`Jumping to ${startTime}`);
};
return (
<div className="h-full bg-background">
{/* Breadcrumb Navigation - Full Width */}
<div className="bg-background border-b border-border py-3">
<div className="px-6">
<nav className="flex items-center gap-2 text-[14px] text-muted-foreground">
<button
onClick={onNavigateBack}
className="hover:text-[var(--color-brand-primary)] transition-colors"
>
Library
</button>
<ChevronRight className="h-3 w-3" />
<span>{videoData.category}</span>
<ChevronRight className="h-3 w-3" />
<span className="text-foreground font-medium">Offline Videos</span>
<ChevronRight className="h-3 w-3" />
<span className="text-foreground font-medium">{videoData.title}</span>
</nav>
</div>
</div>
{/* Video Header - Full Width */}
<div className="bg-background border-b border-border py-6">
<div className="px-6">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<Badge variant="secondary" className="bg-[var(--color-brand-primary)]/10 text-[var(--color-brand-primary)] border-[var(--color-brand-primary)]/20">
<FileVideo className="h-3 w-3 mr-1" />
Offline Video
</Badge>
<Badge variant="outline" className="text-[12px]">
{videoData.resolution}
</Badge>
{videoData.isDownloaded && (
<Badge className="bg-green-100 text-green-800 border-green-200">
<WifiOff className="h-3 w-3 mr-1" />
Downloaded
</Badge>
)}
</div>
<h1 className="text-[28px] font-bold leading-[34px] tracking-[-1px] mb-2 text-gray-900">
{videoData.title}
</h1>
<p className="text-[16px] text-muted-foreground mb-3">
{videoData.subtitle}
</p>
<div className="flex items-center gap-4 text-[14px] text-muted-foreground">
<div className="flex items-center gap-1">
<Clock className="h-4 w-4" />
<span>{videoData.duration}</span>
</div>
<div className="flex items-center gap-1">
<Video className="h-4 w-4" />
<span>{videoData.fileSize}</span>
</div>
{videoData.lastWatched && (
<span>Last watched: {formatLastWatched(videoData.lastWatched)}</span>
)}
</div>
</div>
<div className="flex items-center gap-3">
{!videoData.isDownloaded ? (
<Button onClick={handleDownload}>
<Download className="h-4 w-4 mr-2" />
Download ({videoData.fileSize})
</Button>
) : (
<Button>
<Play className="h-4 w-4 mr-2" />
Resume Watching
</Button>
)}
</div>
</div>
</div>
</div>
{/* Content Layout */}
<div className="flex h-full">
{/* Left Sidebar - Video Chapters */}
<aside className="w-[300px] flex-shrink-0 bg-white border-r border-border">
<div className="h-full overflow-y-auto">
<div className="p-6">
<h3 className="text-[16px] font-semibold text-foreground mb-6">Video Chapters</h3>
<div className="space-y-3">
{videoData.chapters.map((chapter, index) => (
<div
key={chapter.id}
className={`p-3 rounded-lg cursor-pointer transition-colors border ${
selectedChapter === chapter.id
? 'bg-[var(--color-brand-primary)]/5 border-[var(--color-brand-primary)]'
: 'border-border hover:bg-gray-50'
}`}
onClick={() => {
setSelectedChapter(chapter.id);
jumpToChapter(chapter.startTime);
}}
>
<div className="flex items-start gap-3">
<div className={`w-6 h-6 rounded-full flex items-center justify-center flex-shrink-0 ${
chapter.isCompleted ? 'bg-[#21A36A]' : 'bg-gray-200'
}`}>
{chapter.isCompleted ? (
<Video className="h-3 w-3 text-white" />
) : (
<span className="text-[12px] font-medium text-gray-600">{index + 1}</span>
)}
</div>
<div className="flex-1">
<p className="text-[14px] font-medium text-foreground">{chapter.title}</p>
<div className="flex items-center gap-2 text-[12px] text-muted-foreground">
<span>{chapter.startTime}</span>
<span></span>
<span>{chapter.duration}</span>
</div>
</div>
</div>
</div>
))}
</div>
{/* Video Controls */}
<div className="mt-8 pt-6 border-t border-border">
<h4 className="text-[14px] font-medium text-foreground mb-4">Video Options</h4>
<div className="space-y-2">
<Button
variant="outline"
size="sm"
className="w-full justify-start"
onClick={() => setShowTranscript(!showTranscript)}
>
<Subtitles className="h-4 w-4 mr-2" />
{showTranscript ? 'Hide' : 'Show'} Transcript
</Button>
<Button variant="outline" size="sm" className="w-full justify-start">
<Settings className="h-4 w-4 mr-2" />
Video Settings
</Button>
<Button variant="outline" size="sm" className="w-full justify-start">
<Share className="h-4 w-4 mr-2" />
Share Video
</Button>
<Button variant="outline" size="sm" className="w-full justify-start">
<BookmarkPlus className="h-4 w-4 mr-2" />
Save to Playlist
</Button>
</div>
</div>
</div>
</div>
</aside>
{/* Main Content */}
<main className="flex-1 min-w-0">
<div className="p-6">
{/* Video Player */}
<div className="relative w-full aspect-video bg-black rounded-lg overflow-hidden mb-6">
<img
src={videoData.thumbnail}
alt={videoData.title}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 flex items-center justify-center">
<button
onClick={() => setIsPlaying(!isPlaying)}
className="w-16 h-16 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center hover:bg-white/30 transition-colors"
>
{isPlaying ? (
<Pause className="h-8 w-8 text-white" />
) : (
<Play className="h-8 w-8 text-white ml-1" />
)}
</button>
</div>
{/* Video Progress Bar */}
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/50 to-transparent p-4">
<div className="flex items-center gap-4 text-white text-[14px] mb-2">
<button className="hover:opacity-75">
<SkipBack className="h-4 w-4" />
</button>
<span>30:45 / {videoData.duration}</span>
<button className="hover:opacity-75">
<SkipForward className="h-4 w-4" />
</button>
<div className="flex-1" />
<button className="hover:opacity-75">
<Subtitles className="h-4 w-4" />
</button>
<button className="hover:opacity-75">
<Volume2 className="h-4 w-4" />
</button>
<button className="hover:opacity-75">
<Settings className="h-4 w-4" />
</button>
<button className="hover:opacity-75">
<Maximize className="h-4 w-4" />
</button>
</div>
<Progress
value={videoData.progress}
className="h-1 bg-white/20 [&>div]:bg-[var(--color-brand-accent)]"
/>
</div>
</div>
{/* Video Description */}
<div className="mb-8">
<h2 className="text-[20px] font-semibold text-foreground mb-4">About this video</h2>
<p className="text-[16px] text-muted-foreground leading-relaxed">
{videoData.description}
</p>
</div>
{/* Transcript (if shown) */}
{showTranscript && videoData.transcriptAvailable && (
<div className="mb-8">
<h3 className="text-[20px] font-semibold text-foreground mb-4">Video Transcript</h3>
<div className="bg-muted/30 rounded-lg p-4 max-h-64 overflow-y-auto">
<p className="text-[14px] text-muted-foreground leading-relaxed">
[00:00] Welcome to this comprehensive communication masterclass. Today we'll explore the fundamental principles that make communication truly effective...
</p>
<p className="text-[14px] text-muted-foreground leading-relaxed mt-3">
[01:30] The first key element of effective communication is clarity. When we speak with clarity, we ensure our message is understood exactly as intended...
</p>
<p className="text-[14px] text-muted-foreground leading-relaxed mt-3">
[03:15] Active listening is perhaps the most underrated communication skill. It's not just about hearing words, but understanding the complete message...
</p>
</div>
</div>
)}
{/* Instructor Information */}
<div>
<h3 className="text-[20px] font-semibold text-foreground mb-4">Instructor</h3>
<div className="flex items-start gap-4 p-4 bg-muted/50 rounded-lg">
<img
src={videoData.instructor.avatar}
alt={videoData.instructor.name}
className="w-16 h-16 rounded-full object-cover"
/>
<div>
<h4 className="text-[18px] font-semibold text-foreground">{videoData.instructor.name}</h4>
<p className="text-[16px] text-[var(--color-brand-primary)] mb-2">{videoData.instructor.title}</p>
<p className="text-[16px] text-muted-foreground">{videoData.instructor.bio}</p>
</div>
</div>
</div>
</div>
</main>
{/* Right Sidebar - Download & Progress */}
<aside className="w-[300px] flex-shrink-0 bg-white border-l border-border">
<div className="h-full overflow-y-auto">
<div className="p-6">
<h3 className="text-[18px] font-semibold text-foreground mb-4">Download details</h3>
{/* Download Status */}
<div className="mb-6">
{videoData.isDownloaded ? (
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<WifiOff className="h-4 w-4 text-green-600" />
<span className="text-[14px] font-medium text-green-800">Available Offline</span>
</div>
<p className="text-[12px] text-green-600">
This video is downloaded and ready to watch without internet connection.
</p>
</div>
) : (
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Wifi className="h-4 w-4 text-blue-600" />
<span className="text-[14px] font-medium text-blue-800">Online Only</span>
</div>
<p className="text-[12px] text-blue-600 mb-3">
Download for offline viewing
</p>
<Button size="sm" onClick={handleDownload}>
<Download className="h-3 w-3 mr-1" />
Download ({videoData.fileSize})
</Button>
</div>
)}
</div>
{/* Video Details */}
<div className="mb-6">
<h4 className="text-[16px] font-medium text-foreground mb-4">Video Information</h4>
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-[14px] text-muted-foreground">Duration</span>
<span className="text-[14px] font-medium">{videoData.duration}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[14px] text-muted-foreground">File Size</span>
<span className="text-[14px] font-medium">{videoData.fileSize}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[14px] text-muted-foreground">Resolution</span>
<span className="text-[14px] font-medium">{videoData.resolution}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[14px] text-muted-foreground">Subtitles</span>
<span className="text-[14px] font-medium">
{videoData.subtitlesAvailable ? 'Available' : 'Not available'}
</span>
</div>
</div>
</div>
{/* Watch Progress */}
<div className="mb-6">
<h4 className="text-[16px] font-medium text-foreground mb-4">Your Progress</h4>
<div className="mb-3">
<div className="flex items-center justify-between mb-2">
<span className="text-[14px] text-muted-foreground">Completed</span>
<span className="text-[14px] font-medium text-[var(--color-brand-primary)]">{videoData.progress}%</span>
</div>
<Progress value={videoData.progress} className="h-2" />
</div>
{videoData.lastWatched && (
<p className="text-[12px] text-muted-foreground">
Last watched: {formatLastWatched(videoData.lastWatched)}
</p>
)}
</div>
{/* Related Actions */}
<div>
<h4 className="text-[16px] font-medium text-foreground mb-4">Actions</h4>
<div className="space-y-2">
<Button variant="outline" size="sm" className="w-full justify-start">
<Download className="h-4 w-4 mr-2" />
Download Materials
</Button>
<Button variant="outline" size="sm" className="w-full justify-start">
<Share className="h-4 w-4 mr-2" />
Share Video
</Button>
<Button variant="outline" size="sm" className="w-full justify-start">
<BookmarkPlus className="h-4 w-4 mr-2" />
Add to Favorites
</Button>
</div>
</div>
</div>
</div>
</aside>
</div>
</div>
);
}

View File

@@ -0,0 +1,229 @@
import React, { useState } from 'react';
import {
ChevronDown,
BookOpen,
Users,
Building2,
UserCircle2,
Settings,
LogOut,
Check
} from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger
} from './ui/dropdown-menu';
import { Button } from './ui/button';
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
import { Separator } from './ui/separator';
import { Badge } from './ui/badge';
interface User {
id: string;
firstName: string;
lastName: string;
email: string;
persona: 'corporate' | 'individual';
orgName?: string;
canSwitchMode: boolean;
canSwitchAccount: boolean;
avatar?: string;
}
interface ProfileSwitchDropdownProps {
user: User;
currentMode: 'corporate' | 'hr';
currentAccount: 'corporate' | 'personal';
onModeSwitch: (mode: 'corporate' | 'hr') => void;
onAccountSwitch: (account: 'corporate' | 'personal') => void;
onNavigate: (page: string) => void;
onSignOut?: () => void;
}
const recentAccounts = [
{ id: 'design-org', label: 'Design Org', avatar: 'D', active: false },
{ id: 'marketing-org', label: 'Marketing Org', avatar: 'M', active: false },
{ id: 'product-org', label: 'Product Org', avatar: 'P', active: false }
];
export function ProfileSwitchDropdown({
user,
currentMode,
currentAccount,
onModeSwitch,
onAccountSwitch,
onNavigate,
onSignOut
}: ProfileSwitchDropdownProps) {
const [isOpen, setIsOpen] = useState(false);
const handleSignOut = () => {
setIsOpen(false);
if (onSignOut) {
onSignOut();
}
};
const handleNavigateToSettings = () => {
setIsOpen(false);
onNavigate('settings');
};
return (
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="flex items-center gap-2 px-2 py-1.5 h-auto hover:bg-gray-50 rounded-lg"
>
<Avatar className="h-6 w-6">
<AvatarImage src={user.avatar} alt={`${user.firstName} ${user.lastName}`} />
<AvatarFallback className="text-xs bg-[var(--color-brand-primary)] text-white">
{user.firstName[0]}{user.lastName[0]}
</AvatarFallback>
</Avatar>
<span className="text-sm font-medium text-gray-700">
{user.firstName}
</span>
<ChevronDown className="h-3 w-3 text-gray-500" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="w-80 p-0 bg-white border border-gray-200 shadow-lg rounded-lg"
sideOffset={8}
>
{/* User Info Section */}
<div className="p-4 border-b border-gray-100">
<div className="flex items-center gap-3">
<Avatar className="h-10 w-10">
<AvatarImage src={user.avatar} alt={`${user.firstName} ${user.lastName}`} />
<AvatarFallback className="bg-[var(--color-brand-primary)] text-white">
{user.firstName[0]}{user.lastName[0]}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<p className="font-medium text-gray-900 truncate">
{user.firstName} {user.lastName}
</p>
<p className="text-sm text-gray-500 truncate">
{user.email}
</p>
</div>
</div>
</div>
{/* Switch Mode Section */}
{user.canSwitchMode && (
<div className="p-4 border-b border-gray-100">
<p className="text-sm font-medium text-gray-900 mb-3">Switch Mode</p>
<div className="flex bg-gray-100 rounded-lg p-1 gap-1">
<button
onClick={() => onModeSwitch('corporate')}
className={`flex-1 flex items-center justify-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all ${
currentMode === 'corporate'
? 'bg-white text-[var(--color-brand-primary)] shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
<BookOpen className="h-4 w-4" />
Corporate Learning
{currentMode === 'corporate' && <Check className="h-3 w-3" />}
</button>
<button
onClick={() => onModeSwitch('hr')}
className={`flex-1 flex items-center justify-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all ${
currentMode === 'hr'
? 'bg-white text-[var(--color-brand-primary)] shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
<Users className="h-4 w-4" />
HR Mode
{currentMode === 'hr' && <Check className="h-3 w-3" />}
</button>
</div>
</div>
)}
{/* Switch Accounts Section */}
{user.canSwitchAccount && (
<div className="p-4 border-b border-gray-100">
<p className="text-sm font-medium text-gray-900 mb-3">Switch Accounts</p>
<div className="space-y-2">
<button
onClick={() => onAccountSwitch('corporate')}
className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg text-left transition-all ${
currentAccount === 'corporate'
? 'bg-blue-50 text-[var(--color-brand-primary)]'
: 'hover:bg-gray-50 text-gray-700'
}`}
>
<Building2 className="h-4 w-4" />
<span className="flex-1 text-sm font-medium">Corporate</span>
{currentAccount === 'corporate' && (
<Check className="h-4 w-4 text-[var(--color-brand-primary)]" />
)}
</button>
<button
onClick={() => onAccountSwitch('personal')}
className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg text-left transition-all ${
currentAccount === 'personal'
? 'bg-blue-50 text-[var(--color-brand-primary)]'
: 'hover:bg-gray-50 text-gray-700'
}`}
>
<UserCircle2 className="h-4 w-4" />
<span className="flex-1 text-sm font-medium">Personal</span>
{currentAccount === 'personal' && (
<Check className="h-4 w-4 text-[var(--color-brand-primary)]" />
)}
</button>
</div>
</div>
)}
{/* Recent Accounts Section */}
<div className="p-4 border-b border-gray-100">
<p className="text-sm font-medium text-gray-900 mb-3">Recent Accounts</p>
<div className="flex gap-2 overflow-x-auto pb-1">
{recentAccounts.map((account) => (
<button
key={account.id}
className="flex-shrink-0 flex flex-col items-center gap-2 p-2 rounded-lg hover:bg-gray-50 transition-colors min-w-[60px]"
>
<div className="w-8 h-8 bg-[var(--color-brand-primary)] text-white rounded-full flex items-center justify-center text-sm font-medium">
{account.avatar}
</div>
<span className="text-xs text-gray-600 text-center leading-tight">
{account.label}
</span>
</button>
))}
</div>
</div>
{/* Footer Section */}
<div className="p-2">
<button
onClick={handleNavigateToSettings}
className="w-full flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-gray-50 transition-colors text-gray-700"
>
<Settings className="h-4 w-4" />
<span className="text-sm font-medium">Profile & Settings</span>
</button>
<button
onClick={handleSignOut}
className="w-full flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-gray-50 transition-colors text-gray-700"
>
<LogOut className="h-4 w-4" />
<span className="text-sm font-medium">Sign Out</span>
</button>
</div>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -0,0 +1,425 @@
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
import { Button } from './ui/button';
import { Progress } from './ui/progress';
import { Badge } from './ui/badge';
import {
ChevronRight,
ChevronDown,
ChevronUp,
Check,
Play,
Calendar,
Clock,
BookOpen,
Video,
Target,
User,
Award,
Users
} from 'lucide-react';
// Mock program data
interface ProgramData {
id: string;
title: string;
subtitle: string;
description: string;
duration: string;
level: string;
progress: number;
totalCourses: number;
completedCourses: number;
category: string;
instructor: {
name: string;
title: string;
bio: string;
avatar: string;
};
courses: ProgramCourse[];
learningOutcomes: string[];
thumbnail: string;
}
interface ProgramCourse {
id: string;
title: string;
duration: string;
isCompleted: boolean;
isActive: boolean;
progress: number;
description: string;
}
const programDatabase: Record<string, ProgramData> = {
'program-1': {
id: 'program-1',
title: 'Executive Leadership Development Program',
subtitle: 'Comprehensive Leadership Transformation',
description: 'An intensive 12-week program designed to transform mid-level managers into strategic leaders. This comprehensive program covers essential leadership competencies, strategic thinking, and organizational transformation skills.',
duration: '12 weeks',
level: 'Advanced',
progress: 42,
totalCourses: 6,
completedCourses: 2,
category: 'Leadership Development',
instructor: {
name: 'Dr. Sarah Williams',
title: 'Executive Leadership Expert',
bio: 'Dr. Williams has over 20 years of experience in executive coaching and leadership development, having worked with Fortune 100 companies.',
avatar: 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=100&h=100&fit=crop'
},
thumbnail: 'https://images.unsplash.com/photo-1552664730-d307ca884978?w=800&h=450&fit=crop',
learningOutcomes: [
'Develop strategic leadership vision and execution capabilities',
'Master advanced communication and influence techniques',
'Build high-performance teams and organizational culture',
'Navigate complex business challenges and drive transformation'
],
courses: [
{
id: 'course-1',
title: 'Strategic Leadership Foundations',
duration: '2 weeks',
isCompleted: true,
isActive: false,
progress: 100,
description: 'Core principles of strategic leadership and vision development'
},
{
id: 'course-2',
title: 'Advanced Communication & Influence',
duration: '2 weeks',
isCompleted: true,
isActive: false,
progress: 100,
description: 'Mastering executive communication and stakeholder influence'
},
{
id: 'course-3',
title: 'Building High-Performance Teams',
duration: '2 weeks',
isCompleted: false,
isActive: true,
progress: 67,
description: 'Team dynamics, culture building, and performance optimization'
},
{
id: 'course-4',
title: 'Change Management & Innovation',
duration: '2 weeks',
isCompleted: false,
isActive: false,
progress: 0,
description: 'Leading organizational change and fostering innovation'
},
{
id: 'course-5',
title: 'Financial Acumen for Leaders',
duration: '2 weeks',
isCompleted: false,
isActive: false,
progress: 0,
description: 'Understanding financial metrics and business strategy'
},
{
id: 'course-6',
title: 'Executive Capstone Project',
duration: '2 weeks',
isCompleted: false,
isActive: false,
progress: 0,
description: 'Applied leadership project with real business impact'
}
]
}
};
interface ProgramDetailPageProps {
programId: string;
onNavigateBack: () => void;
}
export function ProgramDetailPage({ programId, onNavigateBack }: ProgramDetailPageProps) {
const [programData] = useState<ProgramData>(programDatabase[programId] || programDatabase['program-1']);
const [expandedCourse, setExpandedCourse] = useState<string | null>(null);
const toggleCourse = (courseId: string) => {
setExpandedCourse(expandedCourse === courseId ? null : courseId);
};
return (
<div className="h-full bg-background">
{/* Breadcrumb Navigation - Full Width */}
<div className="bg-background border-b border-border py-3">
<div className="px-6">
<nav className="flex items-center gap-2 text-[14px] text-muted-foreground">
<button
onClick={onNavigateBack}
className="hover:text-[var(--color-brand-primary)] transition-colors"
>
Library
</button>
<ChevronRight className="h-3 w-3" />
<span>{programData.category}</span>
<ChevronRight className="h-3 w-3" />
<span className="text-foreground font-medium">Programs</span>
<ChevronRight className="h-3 w-3" />
<span className="text-foreground font-medium">{programData.title}</span>
</nav>
</div>
</div>
{/* Program Header - Full Width */}
<div className="bg-background border-b border-border py-6">
<div className="px-6">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<Badge variant="secondary" className="bg-[var(--color-brand-primary)]/10 text-[var(--color-brand-primary)] border-[var(--color-brand-primary)]/20">
Program
</Badge>
<Badge variant="outline" className="text-[12px]">
{programData.level}
</Badge>
</div>
<h1 className="text-[28px] font-bold leading-[34px] tracking-[-1px] mb-2 text-gray-900">
{programData.title}
</h1>
<p className="text-[16px] text-muted-foreground mb-3">
{programData.subtitle} {programData.duration}
</p>
<div className="flex items-center gap-4 text-[14px] text-muted-foreground">
<div className="flex items-center gap-1">
<BookOpen className="h-4 w-4" />
<span>{programData.completedCourses}/{programData.totalCourses} courses completed</span>
</div>
<div className="flex items-center gap-1">
<Clock className="h-4 w-4" />
<span>{programData.duration}</span>
</div>
</div>
</div>
<div className="flex items-center gap-3">
<div className="text-right">
<p className="text-[14px] text-muted-foreground">Overall Progress</p>
<p className="text-[24px] font-bold text-[var(--color-brand-primary)]">{programData.progress}%</p>
</div>
<div className="w-12 h-12 rounded-full bg-[var(--color-brand-primary)] flex items-center justify-center relative">
<div
className="w-8 h-8 rounded-full bg-[var(--color-brand-accent)]"
style={{
clipPath: `inset(0 ${100 - programData.progress}% 0 0)`
}}
/>
</div>
</div>
</div>
</div>
</div>
{/* Content Layout */}
<div className="flex h-full">
{/* Left Sidebar - Program Structure */}
<aside className="w-[300px] flex-shrink-0 bg-white border-r border-border">
<div className="h-full overflow-y-auto">
<div className="p-6">
<h3 className="text-[16px] font-semibold text-foreground mb-6">Program Structure</h3>
<div className="space-y-4">
{programData.courses.map((course, index) => (
<div key={course.id} className="mb-4">
<div
className="flex items-center justify-between mb-3 cursor-pointer hover:bg-gray-50 p-3 rounded-lg border border-border"
onClick={() => toggleCourse(course.id)}
>
<div className="flex items-center gap-3">
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
course.isCompleted
? 'bg-[#21A36A]'
: course.isActive
? 'bg-[var(--color-brand-primary)]'
: 'bg-gray-200'
}`}>
{course.isCompleted ? (
<Check className="h-4 w-4 text-white" />
) : (
<span className="text-[12px] font-medium text-white">{index + 1}</span>
)}
</div>
<div className="flex-1">
<h4 className="text-[14px] font-medium text-foreground">
{course.title}
</h4>
<p className="text-[12px] text-muted-foreground">
{course.duration}
</p>
</div>
</div>
{expandedCourse === course.id ? (
<ChevronUp className="h-4 w-4 text-muted-foreground" />
) : (
<ChevronDown className="h-4 w-4 text-muted-foreground" />
)}
</div>
{expandedCourse === course.id && (
<div className="ml-11 mb-3">
<p className="text-[13px] text-muted-foreground mb-3">
{course.description}
</p>
{!course.isCompleted && (
<div className="mb-3">
<div className="flex items-center justify-between mb-1">
<span className="text-[12px] text-muted-foreground">Progress</span>
<span className="text-[12px] text-[var(--color-brand-primary)]">{course.progress}%</span>
</div>
<Progress value={course.progress} className="h-2" />
</div>
)}
<Button
size="sm"
className="w-full"
variant={course.isCompleted ? "outline" : "default"}
>
{course.isCompleted ? "Review" : course.isActive ? "Continue" : "Start Course"}
</Button>
</div>
)}
</div>
))}
</div>
</div>
</div>
</aside>
{/* Main Content */}
<main className="flex-1 min-w-0">
<div className="p-6">
{/* Program Overview */}
<div className="mb-8">
<h2 className="text-[20px] font-semibold text-foreground mb-4">Program Overview</h2>
<img
src={programData.thumbnail}
alt={programData.title}
className="w-full h-64 object-cover rounded-lg mb-4"
/>
<p className="text-[16px] text-muted-foreground leading-relaxed">
{programData.description}
</p>
</div>
{/* Learning Outcomes */}
<div className="mb-8">
<h3 className="text-[20px] font-semibold text-foreground mb-4">What you'll achieve</h3>
<div className="space-y-3">
{programData.learningOutcomes.map((outcome, index) => (
<div key={index} className="flex items-start gap-3">
<div className="w-5 h-5 rounded-full bg-[#21A36A] flex items-center justify-center flex-shrink-0 mt-0.5">
<Check className="h-3 w-3 text-white" />
</div>
<p className="text-[16px] text-foreground">
{outcome}
</p>
</div>
))}
</div>
</div>
{/* Program Leader */}
<div>
<h3 className="text-[20px] font-semibold text-foreground mb-4">Program Leader</h3>
<div className="flex items-start gap-4 p-4 bg-muted/50 rounded-lg">
<img
src={programData.instructor.avatar}
alt={programData.instructor.name}
className="w-16 h-16 rounded-full object-cover"
/>
<div>
<h4 className="text-[18px] font-semibold text-foreground">{programData.instructor.name}</h4>
<p className="text-[16px] text-[var(--color-brand-primary)] mb-2">{programData.instructor.title}</p>
<p className="text-[16px] text-muted-foreground">{programData.instructor.bio}</p>
</div>
</div>
</div>
</div>
</main>
{/* Right Sidebar - Program Plan */}
<aside className="w-[300px] flex-shrink-0 bg-white border-l border-border">
<div className="h-full overflow-y-auto">
<div className="p-6">
<h3 className="text-[18px] font-semibold text-foreground mb-4">Program plan</h3>
<p className="text-[14px] text-muted-foreground mb-6">
I'm committed to completing this program in 12 weeks.
</p>
{/* Current Progress */}
<div className="mb-6">
<div className="flex items-center justify-between mb-4">
<h4 className="text-[16px] font-medium text-foreground">Current Progress</h4>
<span className="text-[14px] font-medium text-muted-foreground">Week 5</span>
</div>
<div className="mb-4">
<p className="text-[14px] text-muted-foreground mb-2">Your next milestone</p>
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded-full border-2 border-[var(--color-brand-primary)] flex items-center justify-center">
<div className="w-2 h-2 bg-[var(--color-brand-primary)] rounded-full" />
</div>
<div>
<p className="text-[14px] font-medium text-foreground">Team Building Course</p>
<p className="text-[12px] text-muted-foreground">Due in 5 days</p>
</div>
</div>
</div>
</div>
{/* Program Timeline */}
<div>
<h4 className="text-[16px] font-medium text-foreground mb-4">Program timeline</h4>
<div className="space-y-4">
{/* Start date */}
<div className="flex items-start gap-3">
<div className="w-5 h-5 rounded-full bg-[#21A36A] flex items-center justify-center flex-shrink-0 mt-0.5">
<Check className="h-3 w-3 text-white" />
</div>
<div>
<p className="text-[14px] font-medium text-foreground">Start date: January 15, 2024</p>
<p className="text-[12px] text-muted-foreground">Program commenced</p>
</div>
</div>
{/* Current course */}
<div className="flex items-start gap-3">
<div className="w-5 h-5 rounded-full bg-[var(--color-brand-primary)] flex items-center justify-center flex-shrink-0 mt-0.5">
<span className="text-[12px] font-medium text-white">3</span>
</div>
<div>
<p className="text-[14px] font-medium text-foreground">Building High-Performance Teams</p>
<p className="text-[12px] text-muted-foreground">In progress - Due March 15</p>
</div>
</div>
{/* End date */}
<div className="flex items-start gap-3">
<div className="w-5 h-5 flex items-center justify-center flex-shrink-0 mt-0.5">
<Award className="h-4 w-4 text-muted-foreground" />
</div>
<div>
<p className="text-[14px] font-medium text-foreground">Expected completion: April 15, 2024</p>
<p className="text-[12px] text-muted-foreground">Program certificate</p>
</div>
</div>
</div>
</div>
</div>
</div>
</aside>
</div>
</div>
);
}

View File

@@ -0,0 +1,437 @@
import React, { useState } from 'react';
import {
Download,
Share2,
Eye,
FileText,
Award,
Calendar,
ExternalLink,
CheckCircle,
Clock,
AlertCircle
} from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
import { Button } from './ui/button';
import { Badge } from './ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
import { Separator } from './ui/separator';
import { useAppContext } from './AppShell';
// Mock data for reports and certificates
const mockReportsData = {
certificates: [
{
id: 'cert-1',
title: 'Strategic Leadership Development Certificate',
courseTitle: 'Strategic Leadership Development',
issuedOn: '2024-08-15T00:00:00Z',
status: 'ready',
downloadUrl: '#',
shareUrl: '#',
thumbnail: 'https://images.unsplash.com/photo-1586953208448-b95a79798f07?w=400',
credentialId: 'KLC-SLD-2024-001'
},
{
id: 'cert-2',
title: 'Team Building Essentials Certificate',
courseTitle: 'Team Building Essentials',
issuedOn: '2024-07-20T00:00:00Z',
status: 'ready',
downloadUrl: '#',
shareUrl: '#',
thumbnail: 'https://images.unsplash.com/photo-1586953208448-b95a79798f07?w=400',
credentialId: 'KLC-TBE-2024-002'
},
{
id: 'cert-3',
title: 'Financial Management for Leaders Certificate',
courseTitle: 'Financial Management for Leaders',
issuedOn: null,
status: 'in-progress',
downloadUrl: null,
shareUrl: null,
thumbnail: 'https://images.unsplash.com/photo-1586953208448-b95a79798f07?w=400',
credentialId: null,
expectedDate: '2024-10-15T00:00:00Z'
}
],
leadershipReports: [
{
id: 'report-1',
title: 'Leadership Assessment Report - Q3 2024',
type: 'assessment',
courseId: 'course-1',
courseTitle: 'Strategic Leadership Development',
createdAt: '2024-08-20T00:00:00Z',
status: 'ready',
pdfUrl: '#',
summary: 'Comprehensive analysis of leadership competencies including strategic thinking, decision making, and team management capabilities.',
keyInsights: [
'Strong strategic thinking capabilities',
'Opportunity for improvement in delegation',
'Excellent communication skills'
]
},
{
id: 'report-2',
title: '360-Degree Feedback Report',
type: '360-feedback',
courseId: 'course-1',
courseTitle: 'Strategic Leadership Development',
createdAt: '2024-08-25T00:00:00Z',
status: 'ready',
pdfUrl: '#',
summary: 'Multi-perspective evaluation including self-assessment, peer feedback, and supervisor input on leadership effectiveness.',
keyInsights: [
'High emotional intelligence scores',
'Consistent feedback on visionary leadership',
'Development area: conflict resolution'
]
},
{
id: 'report-3',
title: 'Leadership Style Analysis',
type: 'profiler',
courseId: null,
courseTitle: null,
createdAt: '2024-09-01T00:00:00Z',
status: 'in-progress',
pdfUrl: null,
summary: 'Analysis of predominant leadership style and recommendations for situational leadership adaptation.',
expectedDate: '2024-09-10T00:00:00Z'
}
]
};
export function ReportsAndCertificates() {
const { user } = useAppContext();
const [activeTab, setActiveTab] = useState('certificates');
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString('en-IN', {
day: '2-digit',
month: 'short',
year: 'numeric',
timeZone: 'Asia/Kolkata'
});
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'ready': return CheckCircle;
case 'in-progress': return Clock;
case 'eligible': return AlertCircle;
default: return Clock;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'ready': return 'bg-green-100 text-green-800';
case 'in-progress': return 'bg-blue-100 text-blue-800';
case 'eligible': return 'bg-yellow-100 text-yellow-800';
default: return 'bg-gray-100 text-gray-800';
}
};
const CertificateCard = ({ certificate }: { certificate: any }) => {
const StatusIcon = getStatusIcon(certificate.status);
return (
<Card className="group hover:shadow-lg transition-all duration-300 border border-gray-200 bg-white">
<CardContent className="p-6">
<div className="flex items-start justify-between gap-6">
{/* Main Content */}
<div className="flex-1 space-y-4">
{/* Header with status */}
<div className="flex items-start justify-between gap-4">
<div className="flex items-start gap-3">
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-[#04045B] to-[#030344] flex items-center justify-center flex-shrink-0">
<Award className="h-6 w-6 text-white" />
</div>
<div className="space-y-1">
<h3 className="text-lg font-semibold text-gray-900 leading-tight">{certificate.title}</h3>
<p className="text-sm text-gray-600">{certificate.courseTitle}</p>
</div>
</div>
<div className="flex items-center gap-2">
<StatusIcon className={`h-4 w-4 ${
certificate.status === 'ready' ? 'text-green-600' :
certificate.status === 'in-progress' ? 'text-blue-600' : 'text-yellow-600'
}`} />
<Badge className={`capitalize font-medium ${getStatusColor(certificate.status)} border-0`}>
{certificate.status.replace('-', ' ')}
</Badge>
</div>
</div>
{/* Date Information */}
<div className="space-y-2">
{certificate.issuedOn && (
<div className="flex items-center gap-2 text-sm text-gray-600">
<Calendar className="h-4 w-4 text-gray-400" />
<span>Issued on {formatDate(certificate.issuedOn)}</span>
</div>
)}
{certificate.expectedDate && (
<div className="flex items-center gap-2 text-sm text-gray-600">
<Clock className="h-4 w-4 text-gray-400" />
<span>Expected by {formatDate(certificate.expectedDate)}</span>
</div>
)}
</div>
{/* Credential ID */}
{certificate.credentialId && (
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100">
<div className="flex items-center gap-2">
<span className="text-xs font-medium text-gray-500 uppercase tracking-wide">Credential ID</span>
<span className="text-sm font-mono text-gray-900">{certificate.credentialId}</span>
</div>
</div>
)}
</div>
{/* Action Buttons */}
{certificate.status === 'ready' && (
<div className="flex gap-2">
<Button size="sm" variant="outline" className="gap-2">
<Eye className="h-4 w-4" />
View
</Button>
<Button size="sm" className="gap-2 bg-[#04045B] hover:bg-[#030344]">
<Download className="h-4 w-4" />
Download
</Button>
<Button size="sm" variant="outline" className="gap-2">
<Share2 className="h-4 w-4" />
Share
</Button>
</div>
)}
</div>
</CardContent>
</Card>
);
};
const ReportCard = ({ report }: { report: any }) => {
const StatusIcon = getStatusIcon(report.status);
return (
<Card className="group hover:shadow-lg transition-all duration-300 border border-gray-200 bg-white">
<CardContent className="p-6">
<div className="space-y-6">
{/* Header */}
<div className="flex items-start justify-between gap-4">
<div className="flex items-start gap-4 flex-1">
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-[#04045B] to-[#030344] flex items-center justify-center flex-shrink-0">
<FileText className="h-6 w-6 text-white" />
</div>
<div className="space-y-2 flex-1">
<h3 className="text-lg font-semibold text-gray-900 leading-tight">{report.title}</h3>
<div className="flex items-center gap-3 flex-wrap">
<div className="flex items-center gap-2">
<StatusIcon className={`h-4 w-4 ${
report.status === 'ready' ? 'text-green-600' :
report.status === 'in-progress' ? 'text-blue-600' : 'text-yellow-600'
}`} />
<Badge className={`capitalize font-medium ${getStatusColor(report.status)} border-0`}>
{report.status.replace('-', ' ')}
</Badge>
</div>
<Badge variant="outline" className="capitalize font-medium bg-[#F8C301]/10 text-[#04045B] border-[#F8C301]/30">
{report.type.replace('-', ' ')}
</Badge>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex gap-2">
{report.status === 'ready' && (
<>
<Button size="sm" variant="outline" className="gap-2">
<Eye className="h-4 w-4" />
View
</Button>
<Button size="sm" className="gap-2 bg-[#04045B] hover:bg-[#030344]">
<Download className="h-4 w-4" />
Download
</Button>
</>
)}
{report.courseId && (
<Button size="sm" variant="outline" className="gap-2">
<ExternalLink className="h-4 w-4" />
Course
</Button>
)}
</div>
</div>
{/* Metadata */}
<div className="space-y-3">
{report.courseTitle && (
<div className="bg-blue-50 rounded-lg p-3 border border-blue-100">
<div className="flex items-center gap-2 text-sm">
<span className="text-gray-600">Related to:</span>
<span className="font-medium text-[#04045B]">{report.courseTitle}</span>
</div>
</div>
)}
<div className="flex items-center gap-2 text-sm text-gray-600">
<Calendar className="h-4 w-4 text-gray-400" />
<span>
{report.status === 'ready' ? 'Generated' : 'Expected'} on {formatDate(report.status === 'ready' ? report.createdAt : report.expectedDate)}
</span>
</div>
</div>
{/* Summary */}
<div className="bg-gray-50 rounded-lg p-4 border border-gray-100">
<p className="text-sm text-gray-700 leading-relaxed">{report.summary}</p>
</div>
{/* Key Insights */}
{report.keyInsights && report.keyInsights.length > 0 && (
<div className="space-y-3">
<h4 className="text-sm font-semibold text-gray-900 flex items-center gap-2">
<div className="w-2 h-2 bg-[#F8C301] rounded-full"></div>
Key Insights
</h4>
<div className="space-y-2">
{report.keyInsights.map((insight: string, index: number) => (
<div key={index} className="flex items-start gap-3 text-sm text-gray-700">
<div className="w-1.5 h-1.5 bg-[#04045B] rounded-full mt-2 flex-shrink-0" />
<span className="leading-relaxed">{insight}</span>
</div>
))}
</div>
</div>
)}
</div>
</CardContent>
</Card>
);
};
return (
<div className="p-6 space-y-8 bg-white min-h-screen">
{/* Header */}
<div className="space-y-2">
<h1 className="text-3xl font-semibold text-gray-900">Reports & Certificates</h1>
<p className="text-gray-600">
Access your leadership reports and download certificates to showcase your achievements
</p>
</div>
{/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-8">
<TabsList className="grid w-full max-w-lg grid-cols-2 bg-gray-50/80 border border-gray-200 p-1 rounded-xl shadow-sm h-auto">
<TabsTrigger
value="certificates"
className="flex items-center gap-3 px-6 py-3 rounded-lg font-medium transition-all data-[state=active]:bg-white data-[state=active]:text-[#04045B] data-[state=active]:shadow-md data-[state=active]:border data-[state=active]:border-[#04045B]/10"
>
<Award className="h-4 w-4" />
Certificates
</TabsTrigger>
<TabsTrigger
value="reports"
className="flex items-center gap-3 px-6 py-3 rounded-lg font-medium transition-all data-[state=active]:bg-white data-[state=active]:text-[#04045B] data-[state=active]:shadow-md data-[state=active]:border data-[state=active]:border-[#04045B]/10"
>
<FileText className="h-4 w-4" />
Reports
</TabsTrigger>
</TabsList>
{/* Certificates Tab */}
<TabsContent value="certificates" className="space-y-6">
<div className="flex items-center justify-between bg-gradient-to-r from-[#04045B]/5 to-[#F8C301]/5 p-6 rounded-xl border border-[#04045B]/10">
<div>
<h2 className="text-xl font-semibold text-gray-900">Your Certificates</h2>
<p className="text-sm text-gray-600 mt-1">
<span className="inline-flex items-center gap-1">
<span className="w-2 h-2 bg-green-500 rounded-full"></span>
{mockReportsData.certificates.filter(c => c.status === 'ready').length} ready
</span>
<span className="mx-2"></span>
<span className="inline-flex items-center gap-1">
<span className="w-2 h-2 bg-blue-500 rounded-full"></span>
{mockReportsData.certificates.filter(c => c.status === 'in-progress').length} in progress
</span>
</p>
</div>
</div>
<div className="space-y-5">
{mockReportsData.certificates.map((certificate) => (
<CertificateCard key={certificate.id} certificate={certificate} />
))}
</div>
{mockReportsData.certificates.length === 0 && (
<Card className="border-0 shadow-sm">
<CardContent className="p-12 text-center">
<div className="w-16 h-16 mx-auto mb-6 bg-gradient-to-br from-[#04045B]/10 to-[#F8C301]/10 rounded-full flex items-center justify-center">
<Award className="h-8 w-8 text-[#04045B]" />
</div>
<h3 className="font-semibold text-gray-900 mb-2">No certificates yet</h3>
<p className="text-sm text-gray-600">
Complete courses to earn certificates
</p>
</CardContent>
</Card>
)}
</TabsContent>
{/* Reports Tab */}
<TabsContent value="reports" className="space-y-6">
<div className="flex items-center justify-between bg-gradient-to-r from-[#04045B]/5 to-blue-50 p-6 rounded-xl border border-[#04045B]/10">
<div>
<h2 className="text-xl font-semibold text-gray-900">Leadership Reports</h2>
<p className="text-sm text-gray-600 mt-1">
<span className="inline-flex items-center gap-1">
<span className="w-2 h-2 bg-green-500 rounded-full"></span>
{mockReportsData.leadershipReports.filter(r => r.status === 'ready').length} ready
</span>
<span className="mx-2"></span>
<span className="inline-flex items-center gap-1">
<span className="w-2 h-2 bg-blue-500 rounded-full"></span>
{mockReportsData.leadershipReports.filter(r => r.status === 'in-progress').length} in progress
</span>
</p>
</div>
</div>
<div className="space-y-5">
{mockReportsData.leadershipReports.map((report) => (
<ReportCard key={report.id} report={report} />
))}
</div>
{mockReportsData.leadershipReports.length === 0 && (
<Card className="border-0 shadow-sm">
<CardContent className="p-12 text-center">
<div className="w-16 h-16 mx-auto mb-6 bg-gradient-to-br from-[#04045B]/10 to-blue-50 rounded-full flex items-center justify-center">
<FileText className="h-8 w-8 text-[#04045B]" />
</div>
<h3 className="font-semibold text-gray-900 mb-2">No reports yet</h3>
<p className="text-sm text-gray-600">
Complete assessments and profilers to generate reports
</p>
</CardContent>
</Card>
)}
</TabsContent>
</Tabs>
</div>
);
}

576
src/components/Settings.tsx Normal file
View File

@@ -0,0 +1,576 @@
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Label } from './ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import { Switch } from './ui/switch';
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
import { Badge } from './ui/badge';
import { Separator } from './ui/separator';
import {
User,
Shield,
Settings as SettingsIcon,
Building2,
Bell,
Globe,
Eye,
Smartphone,
Mail,
Lock
} from 'lucide-react';
interface User {
id: string;
firstName: string;
lastName?: string;
email: string;
persona: 'corporate' | 'individual';
orgName?: string;
avatar?: string;
}
interface SettingsProps {
user: User;
}
export function Settings({ user }: SettingsProps) {
const [twoFactorEnabled, setTwoFactorEnabled] = useState(false);
const [emailNotifications, setEmailNotifications] = useState(true);
const [pushNotifications, setPushNotifications] = useState(true);
const [weeklyDigest, setWeeklyDigest] = useState(true);
return (
<div className="p-6 space-y-8 bg-white min-h-screen">
{/* Header */}
<div className="space-y-2">
<h1 className="text-3xl font-semibold text-gray-900">Settings</h1>
<p className="text-gray-600">Manage your account settings and preferences</p>
</div>
<Tabs defaultValue="profile" className="space-y-8">
<TabsList className={`grid w-full ${user.persona === 'corporate' ? 'grid-cols-4' : 'grid-cols-3'} bg-gray-50/80 border border-gray-200 p-1 rounded-xl shadow-sm h-auto`}>
<TabsTrigger
value="profile"
className="flex items-center gap-3 px-6 py-3 rounded-lg font-medium transition-all data-[state=active]:bg-white data-[state=active]:text-[#04045B] data-[state=active]:shadow-md data-[state=active]:border data-[state=active]:border-[#04045B]/10"
>
<User className="h-4 w-4" />
Profile
</TabsTrigger>
<TabsTrigger
value="security"
className="flex items-center gap-3 px-6 py-3 rounded-lg font-medium transition-all data-[state=active]:bg-white data-[state=active]:text-[#04045B] data-[state=active]:shadow-md data-[state=active]:border data-[state=active]:border-[#04045B]/10"
>
<Shield className="h-4 w-4" />
Security
</TabsTrigger>
<TabsTrigger
value="preferences"
className="flex items-center gap-3 px-6 py-3 rounded-lg font-medium transition-all data-[state=active]:bg-white data-[state=active]:text-[#04045B] data-[state=active]:shadow-md data-[state=active]:border data-[state=active]:border-[#04045B]/10"
>
<SettingsIcon className="h-4 w-4" />
Preferences
</TabsTrigger>
{user.persona === 'corporate' && (
<TabsTrigger
value="organization"
className="flex items-center gap-3 px-6 py-3 rounded-lg font-medium transition-all data-[state=active]:bg-white data-[state=active]:text-[#04045B] data-[state=active]:shadow-md data-[state=active]:border data-[state=active]:border-[#04045B]/10"
>
<Building2 className="h-4 w-4" />
Organization
</TabsTrigger>
)}
</TabsList>
{/* Profile Tab */}
<TabsContent value="profile">
<div className="space-y-6">
<Card className="border border-gray-200 bg-white">
<CardHeader className="pb-6">
<CardTitle className="text-xl font-semibold text-gray-900 flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-[#04045B] to-[#030344] flex items-center justify-center">
<User className="h-4 w-4 text-white" />
</div>
Profile Information
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-center gap-6">
<Avatar className="h-20 w-20 border-4 border-gray-100">
<AvatarImage src={user.avatar} />
<AvatarFallback className="text-lg bg-gradient-to-br from-[#04045B] to-[#030344] text-white">
{user.firstName.charAt(0)}{user.lastName?.charAt(0) || ''}
</AvatarFallback>
</Avatar>
<div className="space-y-3">
<Button variant="outline" className="gap-2">
Change Photo
</Button>
<p className="text-sm text-gray-500">
JPG, PNG or GIF. Max file size 2MB.
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="firstName" className="text-sm font-medium text-gray-700">First Name</Label>
<Input id="firstName" defaultValue={user.firstName} className="bg-white border-gray-200" />
</div>
<div className="space-y-2">
<Label htmlFor="lastName" className="text-sm font-medium text-gray-700">Last Name</Label>
<Input id="lastName" defaultValue={user.lastName || ''} className="bg-white border-gray-200" />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="email" className="text-sm font-medium text-gray-700">Email</Label>
<Input id="email" type="email" defaultValue={user.email} className="bg-white border-gray-200" />
</div>
<div className="space-y-2">
<Label htmlFor="bio" className="text-sm font-medium text-gray-700">Bio</Label>
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100">
<textarea
id="bio"
className="w-full min-h-[100px] bg-white border border-gray-200 rounded-md px-3 py-2 text-sm resize-none"
placeholder="Tell us about yourself..."
/>
</div>
</div>
<div className="bg-blue-50 rounded-lg p-4 border border-blue-100">
<div className="flex items-center gap-3">
<Badge className="bg-[#04045B] text-white border-0">
{user.persona === 'corporate' ? 'Corporate Learner' : 'Individual Learner'}
</Badge>
{user.orgName && (
<Badge variant="outline" className="bg-white text-[#04045B] border-[#04045B]/20">
{user.orgName}
</Badge>
)}
</div>
</div>
<div className="flex justify-end">
<Button className="gap-2 bg-[#04045B] hover:bg-[#030344] text-white px-6">
Save Changes
</Button>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
{/* Security Tab */}
<TabsContent value="security">
<div className="space-y-6">
<Card className="border border-gray-200 bg-white">
<CardHeader className="pb-6">
<CardTitle className="text-xl font-semibold text-gray-900 flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-[#04045B] to-[#030344] flex items-center justify-center">
<Lock className="h-4 w-4 text-white" />
</div>
Password
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="currentPassword" className="text-sm font-medium text-gray-700">Current Password</Label>
<Input id="currentPassword" type="password" className="bg-white border-gray-200" />
</div>
<div className="space-y-2">
<Label htmlFor="newPassword" className="text-sm font-medium text-gray-700">New Password</Label>
<Input id="newPassword" type="password" className="bg-white border-gray-200" />
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword" className="text-sm font-medium text-gray-700">Confirm New Password</Label>
<Input id="confirmPassword" type="password" className="bg-white border-gray-200" />
</div>
</div>
<div className="flex justify-end">
<Button variant="outline" className="gap-2">
Update Password
</Button>
</div>
</CardContent>
</Card>
<Card className="border border-gray-200 bg-white">
<CardHeader className="pb-6">
<CardTitle className="text-xl font-semibold text-gray-900 flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-[#04045B] to-[#030344] flex items-center justify-center">
<Shield className="h-4 w-4 text-white" />
</div>
Two-Factor Authentication
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="bg-blue-50 rounded-lg p-4 border border-blue-100">
<div className="flex items-center justify-between">
<div className="space-y-1">
<p className="text-sm font-medium text-gray-900">Enable 2FA</p>
<p className="text-sm text-gray-600">
Add an extra layer of security to your account
</p>
</div>
<Switch
checked={twoFactorEnabled}
onCheckedChange={setTwoFactorEnabled}
/>
</div>
</div>
{twoFactorEnabled && (
<div className="space-y-4">
<div className="bg-gray-50 rounded-lg p-4 border border-gray-100">
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-green-500 to-green-600 flex items-center justify-center flex-shrink-0">
<Smartphone className="h-5 w-5 text-white" />
</div>
<div className="flex-1">
<p className="text-sm font-medium text-gray-900">Authenticator App</p>
<p className="text-sm text-gray-600">
Use an app like Google Authenticator or Authy
</p>
</div>
<Button variant="outline" size="sm" className="gap-2">
Setup
</Button>
</div>
</div>
</div>
)}
</CardContent>
</Card>
<Card className="border border-gray-200 bg-white">
<CardHeader className="pb-6">
<CardTitle className="text-xl font-semibold text-gray-900 flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-[#04045B] to-[#030344] flex items-center justify-center">
<Globe className="h-4 w-4 text-white" />
</div>
Active Sessions
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="bg-gray-50 rounded-lg p-4 border border-gray-100">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-green-500 to-green-600 flex items-center justify-center">
<Globe className="h-5 w-5 text-white" />
</div>
<div>
<p className="text-sm font-medium text-gray-900">Current session</p>
<p className="text-sm text-gray-600">
Chrome on macOS Mumbai, India
</p>
</div>
</div>
<Badge className="bg-green-100 text-green-800 border-0">
Active
</Badge>
</div>
</div>
<div className="flex justify-end">
<Button variant="destructive" size="sm" className="gap-2">
Sign out all other sessions
</Button>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
{/* Preferences Tab */}
<TabsContent value="preferences">
<div className="space-y-6">
<Card className="border border-gray-200 bg-white">
<CardHeader className="pb-6">
<CardTitle className="text-xl font-semibold text-gray-900 flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-[#04045B] to-[#030344] flex items-center justify-center">
<Bell className="h-4 w-4 text-white" />
</div>
Notifications
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-6">
<div className="bg-blue-50 rounded-lg p-4 border border-blue-100">
<div className="flex items-center justify-between">
<div className="space-y-1">
<div className="flex items-center gap-2">
<Mail className="h-4 w-4 text-[#04045B]" />
<p className="text-sm font-medium text-gray-900">Email Notifications</p>
</div>
<p className="text-sm text-gray-600">
Receive notifications about course updates and deadlines
</p>
</div>
<Switch
checked={emailNotifications}
onCheckedChange={setEmailNotifications}
/>
</div>
</div>
<div className="bg-gray-50 rounded-lg p-4 border border-gray-100">
<div className="flex items-center justify-between">
<div className="space-y-1">
<div className="flex items-center gap-2">
<Bell className="h-4 w-4 text-[#04045B]" />
<p className="text-sm font-medium text-gray-900">Push Notifications</p>
</div>
<p className="text-sm text-gray-600">
Get notified about important updates and reminders
</p>
</div>
<Switch
checked={pushNotifications}
onCheckedChange={setPushNotifications}
/>
</div>
</div>
<div className="bg-gray-50 rounded-lg p-4 border border-gray-100">
<div className="flex items-center justify-between">
<div className="space-y-1">
<p className="text-sm font-medium text-gray-900">Weekly Learning Digest</p>
<p className="text-sm text-gray-600">
Summary of your learning progress and recommendations
</p>
</div>
<Switch
checked={weeklyDigest}
onCheckedChange={setWeeklyDigest}
/>
</div>
</div>
</div>
</CardContent>
</Card>
<Card className="border border-gray-200 bg-white">
<CardHeader className="pb-6">
<CardTitle className="text-xl font-semibold text-gray-900 flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-[#04045B] to-[#030344] flex items-center justify-center">
<SettingsIcon className="h-4 w-4 text-white" />
</div>
Learning Preferences
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="space-y-2">
<Label className="text-sm font-medium text-gray-700">Preferred Learning Time</Label>
<Select defaultValue="morning">
<SelectTrigger className="bg-white border-gray-200">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="morning">Morning (6 AM - 12 PM)</SelectItem>
<SelectItem value="afternoon">Afternoon (12 PM - 6 PM)</SelectItem>
<SelectItem value="evening">Evening (6 PM - 10 PM)</SelectItem>
<SelectItem value="late">Late Night (10 PM - 12 AM)</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label className="text-sm font-medium text-gray-700">Language</Label>
<Select defaultValue="en">
<SelectTrigger className="bg-white border-gray-200">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="en">English</SelectItem>
<SelectItem value="hi">Hindi</SelectItem>
<SelectItem value="es">Spanish</SelectItem>
<SelectItem value="fr">French</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label className="text-sm font-medium text-gray-700">Time Zone</Label>
<Select defaultValue="ist">
<SelectTrigger className="bg-white border-gray-200">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="ist">India Standard Time (GMT+5:30)</SelectItem>
<SelectItem value="utc">UTC (GMT+0)</SelectItem>
<SelectItem value="pst">Pacific Time (GMT-8)</SelectItem>
<SelectItem value="est">Eastern Time (GMT-5)</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>
<Card className="border border-gray-200 bg-white">
<CardHeader className="pb-6">
<CardTitle className="text-xl font-semibold text-gray-900 flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-[#04045B] to-[#030344] flex items-center justify-center">
<Eye className="h-4 w-4 text-white" />
</div>
Accessibility
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-6">
<div className="bg-gray-50 rounded-lg p-4 border border-gray-100">
<div className="flex items-center justify-between">
<div className="space-y-1">
<div className="flex items-center gap-2">
<Eye className="h-4 w-4 text-[#04045B]" />
<p className="text-sm font-medium text-gray-900">High Contrast Mode</p>
</div>
<p className="text-sm text-gray-600">
Improve visibility with higher contrast colors
</p>
</div>
<Switch />
</div>
</div>
<div className="space-y-2">
<Label className="text-sm font-medium text-gray-700">Font Size</Label>
<Select defaultValue="medium">
<SelectTrigger className="bg-white border-gray-200">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="small">Small</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="large">Large</SelectItem>
<SelectItem value="extra-large">Extra Large</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
{/* Organization Tab (Corporate only) */}
{user.persona === 'corporate' && (
<TabsContent value="organization">
<div className="space-y-6">
<Card className="border border-gray-200 bg-white">
<CardHeader className="pb-6">
<CardTitle className="text-xl font-semibold text-gray-900 flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-[#04045B] to-[#030344] flex items-center justify-center">
<Building2 className="h-4 w-4 text-white" />
</div>
Organization Details
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="bg-blue-50 rounded-lg p-4 border border-blue-100 mb-6">
<p className="text-sm text-gray-600">
Contact your HR administrator to update organization details
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label className="text-sm font-medium text-gray-700">Organization Name</Label>
<Input defaultValue={user.orgName} disabled className="bg-gray-50 border-gray-200" />
</div>
<div className="space-y-2">
<Label className="text-sm font-medium text-gray-700">Employee ID</Label>
<Input defaultValue="EMP001234" disabled className="bg-gray-50 border-gray-200" />
</div>
<div className="space-y-2">
<Label className="text-sm font-medium text-gray-700">Department</Label>
<Input defaultValue="Engineering" disabled className="bg-gray-50 border-gray-200" />
</div>
<div className="space-y-2">
<Label className="text-sm font-medium text-gray-700">Manager</Label>
<Input defaultValue="Sarah Johnson" disabled className="bg-gray-50 border-gray-200" />
</div>
</div>
</CardContent>
</Card>
<Card className="border border-gray-200 bg-white">
<CardHeader className="pb-6">
<CardTitle className="text-xl font-semibold text-gray-900 flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-[#04045B] to-[#030344] flex items-center justify-center">
<Shield className="h-4 w-4 text-white" />
</div>
Learning Permissions
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-4">
<div className="bg-gray-50 rounded-lg p-4 border border-gray-100">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-900">Access to All Programmes</p>
<p className="text-sm text-gray-600">
Can access all corporate learning programmes
</p>
</div>
<Badge className="bg-green-100 text-green-800 border-0">
Enabled
</Badge>
</div>
</div>
<div className="bg-gray-50 rounded-lg p-4 border border-gray-100">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-900">Manager Reporting</p>
<p className="text-sm text-gray-600">
Learning progress is shared with manager
</p>
</div>
<Badge className="bg-green-100 text-green-800 border-0">
Enabled
</Badge>
</div>
</div>
<div className="bg-gray-50 rounded-lg p-4 border border-gray-100">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-900">Certification Authority</p>
<p className="text-sm text-gray-600">
Can issue official company certificates
</p>
</div>
<Badge className="bg-yellow-100 text-yellow-800 border-0">
Limited
</Badge>
</div>
</div>
</div>
<Separator className="my-6" />
<div className="bg-blue-50 rounded-lg p-4 border border-blue-100">
<p className="text-sm text-gray-600 mb-3">
For changes to permissions, please contact your HR administrator or submit a request through the HR portal.
</p>
<Button variant="outline" className="gap-2">
Contact HR
</Button>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
)}
</Tabs>
</div>
);
}

View File

@@ -0,0 +1,459 @@
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
import { Button } from './ui/button';
import { Badge } from './ui/badge';
import {
ChevronRight,
Play,
Pause,
Volume2,
Maximize,
Calendar,
Clock,
Users,
Video,
Download,
Share,
BookmarkPlus,
MessageSquare,
ThumbsUp,
Eye
} from 'lucide-react';
// Mock webinar data
interface WebinarData {
id: string;
title: string;
subtitle: string;
description: string;
duration: string;
scheduledDate: string;
status: 'upcoming' | 'live' | 'completed';
category: string;
attendees: number;
maxAttendees: number;
speaker: {
name: string;
title: string;
bio: string;
avatar: string;
company: string;
};
topics: string[];
thumbnail: string;
videoUrl?: string;
registrationRequired: boolean;
isRegistered: boolean;
}
const webinarDatabase: Record<string, WebinarData> = {
'webinar-1': {
id: 'webinar-1',
title: 'Future of Leadership in Remote Work',
subtitle: 'Strategies for Leading Distributed Teams',
description: 'Join us for an interactive session exploring the evolving landscape of leadership in the era of remote and hybrid work. Learn practical strategies for building trust, maintaining culture, and driving performance across distributed teams.',
duration: '60 minutes',
scheduledDate: '2024-03-20T14:00:00Z',
status: 'upcoming',
category: 'Leadership Development',
attendees: 847,
maxAttendees: 1000,
speaker: {
name: 'Alexandra Chen',
title: 'Chief People Officer',
bio: 'Alexandra is the Chief People Officer at GlobalTech and a recognized expert in remote work leadership. She has led distributed teams of 500+ people across 25 countries.',
avatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=100&h=100&fit=crop',
company: 'GlobalTech Solutions'
},
topics: [
'Building trust in remote teams',
'Virtual communication best practices',
'Performance management strategies',
'Creating inclusive remote culture',
'Technology tools for team collaboration'
],
thumbnail: 'https://images.unsplash.com/photo-1600880292203-757bb62b4baf?w=800&h=450&fit=crop',
registrationRequired: true,
isRegistered: true
},
'webinar-2': {
id: 'webinar-2',
title: 'AI in Learning & Development',
subtitle: 'Transforming Corporate Training',
description: 'Discover how artificial intelligence is revolutionizing learning and development. From personalized learning paths to intelligent content curation, explore the tools and strategies shaping the future of corporate training.',
duration: '45 minutes',
scheduledDate: '2024-02-15T10:00:00Z',
status: 'completed',
category: 'Technology & Innovation',
attendees: 1200,
maxAttendees: 1200,
speaker: {
name: 'Dr. Michael Rodriguez',
title: 'AI Research Director',
bio: 'Dr. Rodriguez leads AI research initiatives in educational technology and has published over 50 papers on machine learning applications in corporate learning.',
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=100&h=100&fit=crop',
company: 'EduTech Innovations'
},
topics: [
'AI-powered personalization',
'Intelligent content recommendations',
'Automated skills gap analysis',
'Predictive learning analytics',
'Ethical considerations in AI learning'
],
thumbnail: 'https://images.unsplash.com/photo-1485827404703-89b55fcc595e?w=800&h=450&fit=crop',
videoUrl: 'https://example.com/webinar-recording',
registrationRequired: false,
isRegistered: false
}
};
interface WebinarDetailPageProps {
webinarId: string;
onNavigateBack: () => void;
}
export function WebinarDetailPage({ webinarId, onNavigateBack }: WebinarDetailPageProps) {
const [webinarData] = useState<WebinarData>(webinarDatabase[webinarId] || webinarDatabase['webinar-1']);
const [isPlaying, setIsPlaying] = useState(false);
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
const getStatusBadge = () => {
switch (webinarData.status) {
case 'upcoming':
return <Badge className="bg-blue-100 text-blue-800 border-blue-200">Upcoming</Badge>;
case 'live':
return <Badge className="bg-red-100 text-red-800 border-red-200">Live Now</Badge>;
case 'completed':
return <Badge className="bg-green-100 text-green-800 border-green-200">Completed</Badge>;
default:
return null;
}
};
const getActionButton = () => {
if (webinarData.status === 'live') {
return (
<Button size="lg" className="bg-red-600 hover:bg-red-700">
<Video className="h-4 w-4 mr-2" />
Join Live Webinar
</Button>
);
} else if (webinarData.status === 'upcoming') {
return webinarData.isRegistered ? (
<Button size="lg" disabled>
<Calendar className="h-4 w-4 mr-2" />
Registered
</Button>
) : (
<Button size="lg">
<Calendar className="h-4 w-4 mr-2" />
Register Now
</Button>
);
} else {
return (
<Button size="lg">
<Play className="h-4 w-4 mr-2" />
Watch Recording
</Button>
);
}
};
return (
<div className="h-full bg-background">
{/* Breadcrumb Navigation - Full Width */}
<div className="bg-background border-b border-border py-3">
<div className="px-6">
<nav className="flex items-center gap-2 text-[14px] text-muted-foreground">
<button
onClick={onNavigateBack}
className="hover:text-[var(--color-brand-primary)] transition-colors"
>
Library
</button>
<ChevronRight className="h-3 w-3" />
<span>{webinarData.category}</span>
<ChevronRight className="h-3 w-3" />
<span className="text-foreground font-medium">Webinars</span>
<ChevronRight className="h-3 w-3" />
<span className="text-foreground font-medium">{webinarData.title}</span>
</nav>
</div>
</div>
{/* Webinar Header - Full Width */}
<div className="bg-background border-b border-border py-6">
<div className="px-6">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<Badge variant="secondary" className="bg-[var(--color-brand-primary)]/10 text-[var(--color-brand-primary)] border-[var(--color-brand-primary)]/20">
Webinar
</Badge>
{getStatusBadge()}
</div>
<h1 className="text-[28px] font-bold leading-[34px] tracking-[-1px] mb-2 text-gray-900">
{webinarData.title}
</h1>
<p className="text-[16px] text-muted-foreground mb-3">
{webinarData.subtitle}
</p>
<div className="flex items-center gap-4 text-[14px] text-muted-foreground">
<div className="flex items-center gap-1">
<Calendar className="h-4 w-4" />
<span>{formatDate(webinarData.scheduledDate)}</span>
</div>
<div className="flex items-center gap-1">
<Clock className="h-4 w-4" />
<span>{webinarData.duration}</span>
</div>
<div className="flex items-center gap-1">
<Users className="h-4 w-4" />
<span>{webinarData.attendees.toLocaleString()} attendees</span>
</div>
</div>
</div>
<div className="flex items-center gap-3">
{getActionButton()}
</div>
</div>
</div>
</div>
{/* Content Layout */}
<div className="flex h-full">
{/* Left Sidebar - Webinar Topics */}
<aside className="w-[300px] flex-shrink-0 bg-white border-r border-border">
<div className="h-full overflow-y-auto">
<div className="p-6">
<h3 className="text-[16px] font-semibold text-foreground mb-6">What we'll cover</h3>
<div className="space-y-3">
{webinarData.topics.map((topic, index) => (
<div key={index} className="flex items-start gap-3 p-3 bg-gray-50 rounded-lg">
<div className="w-6 h-6 rounded-full bg-[var(--color-brand-primary)] flex items-center justify-center flex-shrink-0">
<span className="text-[12px] font-medium text-white">{index + 1}</span>
</div>
<div>
<p className="text-[14px] font-medium text-foreground">{topic}</p>
</div>
</div>
))}
</div>
{/* Quick Actions */}
<div className="mt-8 pt-6 border-t border-border">
<h4 className="text-[14px] font-medium text-foreground mb-4">Quick Actions</h4>
<div className="space-y-2">
<Button variant="outline" size="sm" className="w-full justify-start">
<BookmarkPlus className="h-4 w-4 mr-2" />
Save for Later
</Button>
<Button variant="outline" size="sm" className="w-full justify-start">
<Share className="h-4 w-4 mr-2" />
Share Webinar
</Button>
{webinarData.status === 'completed' && (
<Button variant="outline" size="sm" className="w-full justify-start">
<Download className="h-4 w-4 mr-2" />
Download Materials
</Button>
)}
</div>
</div>
</div>
</div>
</aside>
{/* Main Content */}
<main className="flex-1 min-w-0">
<div className="p-6">
{/* Video Player or Placeholder */}
<div className="relative w-full aspect-video bg-black rounded-lg overflow-hidden mb-6">
<img
src={webinarData.thumbnail}
alt={webinarData.title}
className="w-full h-full object-cover"
/>
{webinarData.status === 'completed' && webinarData.videoUrl && (
<div className="absolute inset-0 flex items-center justify-center">
<button
onClick={() => setIsPlaying(!isPlaying)}
className="w-16 h-16 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center hover:bg-white/30 transition-colors"
>
{isPlaying ? (
<Pause className="h-8 w-8 text-white" />
) : (
<Play className="h-8 w-8 text-white ml-1" />
)}
</button>
</div>
)}
{webinarData.status === 'upcoming' && (
<div className="absolute inset-0 flex items-center justify-center bg-black/50">
<div className="text-center text-white">
<Calendar className="h-12 w-12 mx-auto mb-3" />
<p className="text-[18px] font-medium">Webinar starts soon</p>
<p className="text-[14px] opacity-80">{formatDate(webinarData.scheduledDate)}</p>
</div>
</div>
)}
{webinarData.status === 'live' && (
<div className="absolute top-4 left-4">
<Badge className="bg-red-600 text-white">
<div className="w-2 h-2 bg-white rounded-full animate-pulse mr-2" />
LIVE
</Badge>
</div>
)}
{webinarData.status === 'completed' && (
<div className="absolute bottom-4 left-4 right-4 flex items-center justify-between text-white text-[14px]">
<span>Recorded session available</span>
<div className="flex items-center gap-2">
<Volume2 className="h-4 w-4" />
<Maximize className="h-4 w-4" />
</div>
</div>
)}
</div>
{/* Webinar Description */}
<div className="mb-8">
<h2 className="text-[20px] font-semibold text-foreground mb-4">About this webinar</h2>
<p className="text-[16px] text-muted-foreground leading-relaxed">
{webinarData.description}
</p>
</div>
{/* Speaker Information */}
<div>
<h3 className="text-[20px] font-semibold text-foreground mb-4">Featured Speaker</h3>
<div className="flex items-start gap-4 p-4 bg-muted/50 rounded-lg">
<img
src={webinarData.speaker.avatar}
alt={webinarData.speaker.name}
className="w-16 h-16 rounded-full object-cover"
/>
<div>
<h4 className="text-[18px] font-semibold text-foreground">{webinarData.speaker.name}</h4>
<p className="text-[16px] text-[var(--color-brand-primary)] mb-1">{webinarData.speaker.title}</p>
<p className="text-[14px] text-muted-foreground mb-2">{webinarData.speaker.company}</p>
<p className="text-[16px] text-muted-foreground">{webinarData.speaker.bio}</p>
</div>
</div>
</div>
</div>
</main>
{/* Right Sidebar - Webinar Details */}
<aside className="w-[300px] flex-shrink-0 bg-white border-l border-border">
<div className="h-full overflow-y-auto">
<div className="p-6">
<h3 className="text-[18px] font-semibold text-foreground mb-4">Webinar details</h3>
{/* Event Information */}
<div className="space-y-4">
<div className="p-4 bg-muted/50 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Calendar className="h-4 w-4 text-muted-foreground" />
<span className="text-[14px] font-medium text-foreground">Date & Time</span>
</div>
<p className="text-[14px] text-muted-foreground">
{formatDate(webinarData.scheduledDate)}
</p>
</div>
<div className="p-4 bg-muted/50 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Clock className="h-4 w-4 text-muted-foreground" />
<span className="text-[14px] font-medium text-foreground">Duration</span>
</div>
<p className="text-[14px] text-muted-foreground">
{webinarData.duration}
</p>
</div>
<div className="p-4 bg-muted/50 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Users className="h-4 w-4 text-muted-foreground" />
<span className="text-[14px] font-medium text-foreground">Attendance</span>
</div>
<p className="text-[14px] text-muted-foreground">
{webinarData.attendees.toLocaleString()} of {webinarData.maxAttendees.toLocaleString()} registered
</p>
</div>
</div>
{/* Engagement Stats (for completed webinars) */}
{webinarData.status === 'completed' && (
<div className="mt-6">
<h4 className="text-[16px] font-medium text-foreground mb-4">Engagement</h4>
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Eye className="h-4 w-4 text-muted-foreground" />
<span className="text-[14px] text-foreground">Views</span>
</div>
<span className="text-[14px] font-medium">2,450</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<ThumbsUp className="h-4 w-4 text-muted-foreground" />
<span className="text-[14px] text-foreground">Likes</span>
</div>
<span className="text-[14px] font-medium">342</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<MessageSquare className="h-4 w-4 text-muted-foreground" />
<span className="text-[14px] text-foreground">Q&A</span>
</div>
<span className="text-[14px] font-medium">78</span>
</div>
</div>
</div>
)}
{/* Related Content */}
<div className="mt-6">
<h4 className="text-[16px] font-medium text-foreground mb-4">Related Content</h4>
<div className="space-y-3">
<div className="p-3 border border-border rounded-lg hover:bg-gray-50 cursor-pointer">
<p className="text-[14px] font-medium text-foreground">Remote Leadership Toolkit</p>
<p className="text-[12px] text-muted-foreground">Download guide</p>
</div>
<div className="p-3 border border-border rounded-lg hover:bg-gray-50 cursor-pointer">
<p className="text-[14px] font-medium text-foreground">Team Collaboration Course</p>
<p className="text-[12px] text-muted-foreground">4-week program</p>
</div>
</div>
</div>
</div>
</div>
</aside>
</div>
</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,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,57 @@
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
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...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-xl 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,140 @@
"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) => {
return <DialogPrimitive.Trigger ref={ref} data-slot="dialog-trigger" {...props} />;
});
DialogTrigger.displayName = "DialogTrigger";
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
}
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => {
return (
<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 = "DialogOverlay";
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => {
return (
<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 = "DialogContent";
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}
/>
);
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
);
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
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,257 @@
"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} />
);
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
);
}
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} />
);
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<DropdownMenuPrimitive.Item
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}
/>
);
}
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,37 @@
"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,
indicatorClassName,
...props
}: React.ComponentProps<typeof ProgressPrimitive.Root> & {
indicatorClassName?: string;
}) {
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={cn(
"bg-primary h-full w-full flex-1 transition-all",
indicatorClassName
)}
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 };

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

@@ -0,0 +1,139 @@
"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} />;
}
function SheetTrigger({
...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
}
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 };

View File

@@ -0,0 +1,47 @@
"use client";
import * as React from "react";
import * as TogglePrimitive from "@radix-ui/react-toggle@1.1.2";
import { cva, type VariantProps } from "class-variance-authority@0.7.1";
import { cn } from "./utils";
const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-9 px-2 min-w-9",
sm: "h-8 px-1.5 min-w-8",
lg: "h-10 px-2.5 min-w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function Toggle({
className,
variant,
size,
...props
}: React.ComponentProps<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>) {
return (
<TogglePrimitive.Root
data-slot="toggle"
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Toggle, toggleVariants };

View File

@@ -0,0 +1,61 @@
"use client";
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip@1.1.8";
import { cn } from "./utils";
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
);
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
);
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className,
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
);
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

View File

@@ -0,0 +1,21 @@
import * as React from "react";
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
undefined,
);
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange);
}, []);
return !!isMobile;
}

View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

26
src/global.d.ts vendored Normal file
View File

@@ -0,0 +1,26 @@
// declarations.d.ts
declare module "*.png" {
const src: string;
export default src;
}
declare module "*.jpg" {
const src: string;
export default src;
}
declare module "*.jpeg" {
const src: string;
export default src;
}
declare module "*.svg" {
import * as React from "react";
const ReactComponent: React.FunctionComponent<
React.SVGProps<SVGSVGElement> & { title?: string }
>;
export { ReactComponent };
const src: string;
export default src;
}

View File

@@ -0,0 +1,61 @@
**Add your own guidelines here**
<!--
System Guidelines
Use this file to provide the AI with rules and guidelines you want it to follow.
This template outlines a few examples of things you can add. You can add your own sections and format it to suit your needs
TIP: More context isn't always better. It can confuse the LLM. Try and add the most important rules you need
# General guidelines
Any general rules you want the AI to follow.
For example:
* Only use absolute positioning when necessary. Opt for responsive and well structured layouts that use flexbox and grid by default
* Refactor code as you go to keep code clean
* Keep file sizes small and put helper functions and components in their own files.
--------------
# Design system guidelines
Rules for how the AI should make generations look like your company's design system
Additionally, if you select a design system to use in the prompt box, you can reference
your design system's components, tokens, variables and components.
For example:
* Use a base font-size of 14px
* Date formats should always be in the format “Jun 10”
* The bottom toolbar should only ever have a maximum of 4 items
* Never use the floating action button with the bottom toolbar
* Chips should always come in sets of 3 or more
* Don't use a dropdown if there are 2 or fewer options
You can also create sub sections and add more specific details
For example:
## Button
The Button component is a fundamental interactive element in our design system, designed to trigger actions or navigate
users through the application. It provides visual feedback and clear affordances to enhance user experience.
### Usage
Buttons should be used for important actions that users need to take, such as form submissions, confirming choices,
or initiating processes. They communicate interactivity and should have clear, action-oriented labels.
### Variants
* Primary Button
* Purpose : Used for the main action in a section or page
* Visual Style : Bold, filled with the primary brand color
* Usage : One primary button per section to guide users toward the most important action
* Secondary Button
* Purpose : Used for alternative or supporting actions
* Visual Style : Outlined with the primary color, transparent background
* Usage : Can appear alongside a primary button for less important actions
* Tertiary Button
* Purpose : Used for the least important actions
* Visual Style : Text-only with no border, using primary color
* Usage : For actions that should be available but not emphasized
-->

7
src/main.tsx Normal file
View File

@@ -0,0 +1,7 @@
import { createRoot } from "react-dom/client";
import App from "./App";
import "./styles/globals.css";
createRoot(document.getElementById("root")!).render(<App />);

412
src/styles/globals.css Normal file
View File

@@ -0,0 +1,412 @@
@import "tailwindcss";
@custom-variant dark (&:is(.dark *));
:root {
/* === BRAND COLORS === */
--color-primary: #04045B; /* Brand Blue */
--color-accent: #F8C301; /* Highlight Yellow */
--color-black: #26231A; /* Body Text */
--color-gray-muted: #6F6F6F; /* Subtext or muted content - Updated to darker gray for better readability */
--color-bg-white: #FFFFFF; /* White Background - DEFAULT FOR ALL SECTIONS */
--color-bg-light: #FFFFFF; /* Section Background - CHANGED FROM #F9F9F9 TO WHITE */
/* Modern Dashboard Colors */
--color-brand-primary: #04045B;
--color-brand-secondary: #6F6F6F;
--color-brand-accent: #F8C301;
--color-brand-highlight: #04045B;
--color-brand-neutral: #FFFFFF;
--color-divider: #E5E7EB;
--font-size: 16px;
--background: #ffffff;
--foreground: #26231A;
--card: #ffffff;
--card-foreground: #26231A;
--popover: #ffffff;
--popover-foreground: #26231A;
--primary: var(--color-brand-primary);
--primary-foreground: #ffffff;
--secondary: var(--color-brand-secondary);
--secondary-foreground: #ffffff;
--muted: #F3F4F6;
--muted-foreground: var(--color-brand-secondary);
--accent: var(--color-brand-neutral);
--accent-foreground: #26231A;
--destructive: #DC2626;
--destructive-foreground: #ffffff;
--border: var(--color-divider);
--input: transparent;
--input-background: #FFFFFF;
--switch-background: #D1D5DB;
--font-weight-semibold: 600;
--font-weight-medium: 500;
--font-weight-normal: 400;
--ring: var(--color-brand-highlight);
--chart-1: var(--color-brand-primary);
--chart-2: var(--color-brand-highlight);
--chart-3: var(--color-brand-accent);
--chart-4: #10B981;
--chart-5: #F59E0B;
--radius: 0.5rem;
--sidebar: #FFFFFF;
--sidebar-foreground: #26231A;
--sidebar-primary: var(--color-brand-primary);
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: #FFFFFF;
--sidebar-accent-foreground: #6F6F6F;
--sidebar-border: var(--color-divider);
--sidebar-ring: var(--color-brand-highlight);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.145 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.145 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.269 0 0);
--input: oklch(0.269 0 0);
--ring: oklch(0.439 0 0);
--font-weight-medium: 500;
--font-weight-normal: 400;
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.439 0 0);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-input-background: var(--input-background);
--color-switch-background: var(--switch-background);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Modern focus styles */
button:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible,
[role="button"]:focus-visible {
outline: 2px solid var(--color-brand-highlight);
outline-offset: 2px;
}
/* Enhanced interactive states */
button:not(:disabled),
[role="button"]:not([aria-disabled="true"]) {
cursor: pointer;
}
button:disabled,
[aria-disabled="true"] {
opacity: 0.6;
cursor: not-allowed;
}
}
/* Line clamp utilities for text truncation */
@layer utilities {
.line-clamp-1 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
}
.line-clamp-2 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.line-clamp-3 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
}
/* Modern scrollbar styling */
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: #D1D5DB #FFFFFF;
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #FFFFFF;
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: #D1D5DB;
border-radius: 3px;
transition: background-color 0.2s ease;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background-color: var(--color-brand-secondary);
}
/* Modern component utilities */
.modern-card {
background: white;
border: 1px solid var(--color-divider);
border-radius: 0.75rem;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
transition: all 0.2s ease;
}
.modern-card:hover {
border-color: var(--color-brand-highlight);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.modern-button-primary {
background: var(--color-brand-primary);
color: white;
border: 1px solid var(--color-brand-primary);
border-radius: 0.5rem;
padding: 0.5rem 1rem;
font-weight: var(--font-weight-medium);
transition: all 0.2s ease;
outline: none;
}
.modern-button-primary:hover {
background: #030344;
border-color: #030344;
box-shadow: 0 4px 6px -1px rgba(4, 4, 91, 0.3);
transform: translateY(-1px);
}
.modern-button-secondary {
background: transparent;
color: var(--color-brand-primary);
border: 1px solid var(--color-divider);
border-radius: 0.5rem;
padding: 0.5rem 1rem;
font-weight: var(--font-weight-medium);
transition: all 0.2s ease;
outline: none;
}
.modern-button-secondary:hover {
background: var(--color-brand-neutral);
border-color: var(--color-brand-primary);
color: var(--color-brand-primary);
}
.modern-badge {
display: inline-flex;
align-items: center;
border-radius: 9999px;
padding: 0.25rem 0.75rem;
font-size: 0.75rem;
font-weight: var(--font-weight-medium);
line-height: 1;
}
.modern-badge-primary {
background: rgba(4, 4, 91, 0.1);
color: var(--color-brand-primary);
}
.modern-badge-secondary {
background: #FFFFFF;
color: var(--color-brand-secondary);
border: 1px solid var(--color-divider);
}
.modern-input {
background: var(--input-background);
border: 1px solid var(--color-divider);
border-radius: 0.5rem;
padding: 0.75rem 1rem;
font-size: 0.875rem;
transition: all 0.2s ease;
outline: none;
}
.modern-input:focus {
border-color: var(--color-brand-highlight);
box-shadow: 0 0 0 3px rgba(4, 4, 91, 0.1);
}
/* Consistent spacing utilities (8px grid) */
.space-xs { margin: 0.5rem; }
.space-sm { margin: 1rem; }
.space-md { margin: 1.5rem; }
.space-lg { margin: 2rem; }
.space-xl { margin: 3rem; }
.gap-xs { gap: 0.5rem; }
.gap-sm { gap: 1rem; }
.gap-md { gap: 1.5rem; }
.gap-lg { gap: 2rem; }
.gap-xl { gap: 3rem; }
}
/**
* Modern Typography System - Clean hierarchy with semibold headings
*/
@layer base {
:where(:not(:has([class*=" text-"]), :not(:has([class^="text-"])))) {
h1 {
font-size: 2rem;
font-weight: var(--font-weight-semibold);
line-height: 1.25;
color: #26231A;
letter-spacing: -0.025em;
}
h2 {
font-size: 1.5rem;
font-weight: var(--font-weight-semibold);
line-height: 1.375;
color: #26231A;
letter-spacing: -0.025em;
}
h3 {
font-size: 1.25rem;
font-weight: var(--font-weight-semibold);
line-height: 1.375;
color: #26231A;
}
h4 {
font-size: 1.125rem;
font-weight: var(--font-weight-semibold);
line-height: 1.5;
color: #26231A;
}
h5 {
font-size: 1rem;
font-weight: var(--font-weight-semibold);
line-height: 1.5;
color: #26231A;
}
h6 {
font-size: 0.875rem;
font-weight: var(--font-weight-semibold);
line-height: 1.5;
color: #6F6F6F;
text-transform: uppercase;
letter-spacing: 0.05em;
}
p {
font-size: 1rem;
font-weight: var(--font-weight-normal);
line-height: 1.625;
color: #6F6F6F;
}
label {
font-size: 0.875rem;
font-weight: var(--font-weight-medium);
line-height: 1.5;
color: #26231A;
}
button {
font-size: 0.875rem;
font-weight: var(--font-weight-medium);
line-height: 1.5;
letter-spacing: 0.025em;
}
input, textarea, select {
font-size: 0.875rem;
font-weight: var(--font-weight-normal);
line-height: 1.5;
color: #26231A;
}
small {
font-size: 0.75rem;
font-weight: var(--font-weight-normal);
line-height: 1.5;
color: var(--color-brand-secondary);
}
}
}
html {
font-size: var(--font-size);
}

21
tsconfig.json Normal file
View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

8
tsconfig.node.json Normal file
View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node"
},
"include": ["vite.config.ts"]
}

62
vite.config.ts Normal file
View File

@@ -0,0 +1,62 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
alias: {
'vaul@1.1.2': 'vaul',
'sonner@2.0.3': 'sonner',
'recharts@2.15.2': 'recharts',
'react-resizable-panels@2.1.7': 'react-resizable-panels',
'react-hook-form@7.55.0': 'react-hook-form',
'react-day-picker@8.10.1': 'react-day-picker',
'next-themes@0.4.6': 'next-themes',
'lucide-react@0.487.0': 'lucide-react',
'input-otp@1.4.2': 'input-otp',
'figma:asset/6b17aafb4d0b31099f8eec7b69e7d0a8b29ad00f.png': path.resolve(__dirname, './src/assets/6b17aafb4d0b31099f8eec7b69e7d0a8b29ad00f.png'),
'figma:asset/1e150e43f238df3e08fcbf5d8f4899c233264e9f.png': path.resolve(__dirname, './src/assets/1e150e43f238df3e08fcbf5d8f4899c233264e9f.png'),
'embla-carousel-react@8.6.0': 'embla-carousel-react',
'cmdk@1.1.1': 'cmdk',
'class-variance-authority@0.7.1': 'class-variance-authority',
'@radix-ui/react-tooltip@1.1.8': '@radix-ui/react-tooltip',
'@radix-ui/react-toggle@1.1.2': '@radix-ui/react-toggle',
'@radix-ui/react-toggle-group@1.1.2': '@radix-ui/react-toggle-group',
'@radix-ui/react-tabs@1.1.3': '@radix-ui/react-tabs',
'@radix-ui/react-switch@1.1.3': '@radix-ui/react-switch',
'@radix-ui/react-slot@1.1.2': '@radix-ui/react-slot',
'@radix-ui/react-slider@1.2.3': '@radix-ui/react-slider',
'@radix-ui/react-separator@1.1.2': '@radix-ui/react-separator',
'@radix-ui/react-select@2.1.6': '@radix-ui/react-select',
'@radix-ui/react-scroll-area@1.2.3': '@radix-ui/react-scroll-area',
'@radix-ui/react-radio-group@1.2.3': '@radix-ui/react-radio-group',
'@radix-ui/react-progress@1.1.2': '@radix-ui/react-progress',
'@radix-ui/react-popover@1.1.6': '@radix-ui/react-popover',
'@radix-ui/react-navigation-menu@1.2.5': '@radix-ui/react-navigation-menu',
'@radix-ui/react-menubar@1.1.6': '@radix-ui/react-menubar',
'@radix-ui/react-label@2.1.2': '@radix-ui/react-label',
'@radix-ui/react-hover-card@1.1.6': '@radix-ui/react-hover-card',
'@radix-ui/react-dropdown-menu@2.1.6': '@radix-ui/react-dropdown-menu',
'@radix-ui/react-dialog@1.1.6': '@radix-ui/react-dialog',
'@radix-ui/react-context-menu@2.2.6': '@radix-ui/react-context-menu',
'@radix-ui/react-collapsible@1.1.3': '@radix-ui/react-collapsible',
'@radix-ui/react-checkbox@1.1.4': '@radix-ui/react-checkbox',
'@radix-ui/react-avatar@1.1.3': '@radix-ui/react-avatar',
'@radix-ui/react-aspect-ratio@1.1.2': '@radix-ui/react-aspect-ratio',
'@radix-ui/react-alert-dialog@1.1.6': '@radix-ui/react-alert-dialog',
'@radix-ui/react-accordion@1.2.3': '@radix-ui/react-accordion',
'@': path.resolve(__dirname, './src'),
},
},
build: {
target: 'esnext',
outDir: 'build',
},
server: {
port: 3000,
open: true,
},
});