331 lines
11 KiB
TypeScript
331 lines
11 KiB
TypeScript
import React from 'react';
|
|
import { Card, CardContent, CardHeader } from '../ui/card';
|
|
import { Button } from '../ui/button';
|
|
import { Badge } from '../ui/badge';
|
|
import { Progress } from '../ui/progress';
|
|
import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar';
|
|
import {
|
|
Star,
|
|
Clock,
|
|
Users,
|
|
Play,
|
|
CheckCircle,
|
|
Calendar,
|
|
Award,
|
|
Eye,
|
|
Heart,
|
|
Share2,
|
|
Video,
|
|
FileText,
|
|
Headphones,
|
|
Monitor,
|
|
AlertCircle
|
|
} from 'lucide-react';
|
|
import { Course } from '../../pages/learner/data/libraryData';
|
|
const navigate = useNavigate();
|
|
|
|
interface CourseCardProps {
|
|
course: Course;
|
|
userType: 'individual' | 'corporate';
|
|
onEnroll?: (courseId: string) => void;
|
|
onContinue?: (courseId: string) => void;
|
|
onBookmark?: (courseId: string) => void;
|
|
}
|
|
|
|
const getTypeIcon = (type: string) => {
|
|
switch (type) {
|
|
case 'video': return Video;
|
|
case 'article': return FileText;
|
|
case 'audio': return Headphones;
|
|
case 'interactive': return Monitor;
|
|
default: return Video;
|
|
}
|
|
};
|
|
|
|
const getPriorityColor = (priority?: string) => {
|
|
switch (priority) {
|
|
case 'high': return 'text-destructive bg-destructive/10';
|
|
case 'medium': return 'text-orange-600 bg-orange-100';
|
|
case 'low': return 'text-success bg-success/10';
|
|
default: return '';
|
|
}
|
|
};
|
|
|
|
const getLevelColor = (level: string) => {
|
|
switch (level) {
|
|
case 'Beginner': return 'text-success bg-success/10';
|
|
case 'Intermediate': return 'text-primary bg-primary/10';
|
|
case 'Advanced': return 'text-destructive bg-destructive/10';
|
|
default: return 'text-muted-foreground bg-muted';
|
|
}
|
|
};
|
|
|
|
export function CourseCard({ course, userType, onEnroll, onContinue, onBookmark }: CourseCardProps) {
|
|
const TypeIcon = getTypeIcon(course.type);
|
|
const isOverdue = course.deadline && new Date(course.deadline) < new Date();
|
|
|
|
// Navigate to course details page with proper query parameters
|
|
const handleCourseNavigation = () => {
|
|
navigate(`/course?view=${userType}&courseId=${course.id}`);
|
|
};
|
|
|
|
return (
|
|
<Card className={`group hover:shadow-lg transition-all duration-300 ${
|
|
course.isFeatured ? 'ring-2 ring-primary/20' : ''
|
|
} ${isOverdue ? 'border-destructive/20 bg-destructive/5' : ''}`}>
|
|
{/* Course Thumbnail */}
|
|
<div className="relative overflow-hidden rounded-t-lg">
|
|
<img
|
|
src={course.thumbnail}
|
|
alt={course.title}
|
|
className="w-full h-48 object-cover group-hover:scale-105 transition-transform duration-300"
|
|
/>
|
|
|
|
{/* Overlay Badges */}
|
|
<div className="absolute top-3 left-3 flex flex-wrap gap-2">
|
|
{course.isFeatured && (
|
|
<Badge className="bg-primary text-primary-foreground text-xs">
|
|
Featured
|
|
</Badge>
|
|
)}
|
|
{course.isPremium && (
|
|
<Badge variant="secondary" className="text-xs">
|
|
Premium
|
|
</Badge>
|
|
)}
|
|
{course.organizationAssigned && userType === 'corporate' && (
|
|
<Badge variant="outline" className="text-xs bg-background/90">
|
|
Assigned
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
|
|
{/* Priority Badge for Corporate */}
|
|
{userType === 'corporate' && course.priority && (
|
|
<div className="absolute top-3 right-3">
|
|
<Badge variant="outline" className={`text-xs ${getPriorityColor(course.priority)}`}>
|
|
{course.priority.toUpperCase()}
|
|
</Badge>
|
|
</div>
|
|
)}
|
|
|
|
{/* Type Icon */}
|
|
<div className="absolute bottom-3 left-3">
|
|
<div className="bg-background/90 backdrop-blur-sm p-2 rounded-full">
|
|
<TypeIcon className="h-4 w-4 text-primary" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Course Status */}
|
|
<div className="absolute bottom-3 right-3">
|
|
{course.status === 'completed' && (
|
|
<div className="bg-success/90 backdrop-blur-sm p-2 rounded-full">
|
|
<CheckCircle className="h-4 w-4 text-white" />
|
|
</div>
|
|
)}
|
|
{course.status === 'bookmarked' && (
|
|
<div className="bg-orange-500/90 backdrop-blur-sm p-2 rounded-full">
|
|
<Heart className="h-4 w-4 text-white fill-current" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="text-lg font-semibold line-clamp-2 group-hover:text-primary transition-colors">
|
|
{course.title}
|
|
</h3>
|
|
<p className="text-base text-muted-foreground mt-2 line-clamp-2">
|
|
{course.description}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Instructor Info */}
|
|
<div className="flex items-center gap-3 mt-3">
|
|
<Avatar className="h-8 w-8">
|
|
<AvatarImage src={course.instructor.avatar} />
|
|
<AvatarFallback className="text-xs">
|
|
{course.instructor.name.split(' ').map(n => n[0]).join('')}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-base font-medium">{course.instructor.name}</p>
|
|
<p className="text-base text-muted-foreground">{course.instructor.title}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Course Metadata */}
|
|
<div className="flex items-center gap-4 mt-3 text-base text-muted-foreground">
|
|
<div className="flex items-center gap-1">
|
|
<Clock className="h-4 w-4" />
|
|
<span>{course.duration}</span>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<Eye className="h-4 w-4" />
|
|
<span>{course.lessonsCount} lessons</span>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<Users className="h-4 w-4" />
|
|
<span>{course.enrolledCount.toLocaleString()}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Rating */}
|
|
<div className="flex items-center gap-2 mt-2">
|
|
<div className="flex items-center gap-1">
|
|
{[...Array(5)].map((_, i) => (
|
|
<Star
|
|
key={i}
|
|
className={`h-4 w-4 ${
|
|
i < Math.floor(course.rating)
|
|
? 'text-yellow-500 fill-current'
|
|
: 'text-muted-foreground'
|
|
}`}
|
|
/>
|
|
))}
|
|
</div>
|
|
<span className="text-base font-medium">{course.rating}</span>
|
|
<span className="text-base text-muted-foreground">
|
|
({course.completionRate}% completion rate)
|
|
</span>
|
|
</div>
|
|
|
|
{/* Level and Category */}
|
|
<div className="flex items-center gap-2 mt-2">
|
|
<Badge variant="outline" className={`text-xs ${getLevelColor(course.level)}`}>
|
|
{course.level}
|
|
</Badge>
|
|
<Badge variant="secondary" className="text-xs">
|
|
{course.category}
|
|
</Badge>
|
|
</div>
|
|
|
|
{/* Tags */}
|
|
<div className="flex flex-wrap gap-1 mt-2">
|
|
{course.tags.slice(0, 3).map((tag) => (
|
|
<Badge key={tag} variant="outline" className="text-xs">
|
|
{tag}
|
|
</Badge>
|
|
))}
|
|
{course.tags.length > 3 && (
|
|
<Badge variant="outline" className="text-xs">
|
|
+{course.tags.length - 3} more
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</CardHeader>
|
|
|
|
<CardContent className="pt-0">
|
|
{/* Progress Bar for In-Progress Courses */}
|
|
{course.status === 'in-progress' && course.progress !== undefined && (
|
|
<div className="space-y-2 mb-4">
|
|
<div className="flex justify-between text-base">
|
|
<span>Progress</span>
|
|
<span>{course.progress}%</span>
|
|
</div>
|
|
<Progress value={course.progress} className="h-2" />
|
|
{course.lastAccessed && (
|
|
<p className="text-base text-muted-foreground">
|
|
Last accessed: {course.lastAccessed}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Deadline Warning for Corporate */}
|
|
{userType === 'corporate' && course.deadline && (
|
|
<div className={`p-3 rounded-lg mb-4 ${
|
|
isOverdue
|
|
? 'bg-destructive/5 border border-destructive/20'
|
|
: 'bg-orange-50 border border-orange-200'
|
|
}`}>
|
|
<div className="flex items-center gap-2">
|
|
<AlertCircle className={`h-4 w-4 ${isOverdue ? 'text-destructive' : 'text-orange-600'}`} />
|
|
<span className={`text-base font-medium ${isOverdue ? 'text-destructive' : 'text-orange-600'}`}>
|
|
{isOverdue ? 'Overdue' : 'Due'}: {new Date(course.deadline).toLocaleDateString()}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Action Buttons */}
|
|
<div className="flex items-center gap-2">
|
|
{course.status === 'not-started' && (
|
|
<Button
|
|
onClick={handleCourseNavigation}
|
|
className="flex-1 text-base min-h-[44px]"
|
|
>
|
|
<Play className="h-4 w-4 mr-2" />
|
|
Start Course
|
|
</Button>
|
|
)}
|
|
|
|
{course.status === 'in-progress' && (
|
|
<Button
|
|
onClick={handleCourseNavigation}
|
|
className="flex-1 text-base min-h-[44px]"
|
|
>
|
|
<Play className="h-4 w-4 mr-2" />
|
|
Continue Learning
|
|
</Button>
|
|
)}
|
|
|
|
{course.status === 'completed' && (
|
|
<Button
|
|
variant="outline"
|
|
onClick={handleCourseNavigation}
|
|
className="flex-1 text-base min-h-[44px]"
|
|
>
|
|
<Eye className="h-4 w-4 mr-2" />
|
|
Review Course
|
|
</Button>
|
|
)}
|
|
|
|
{course.status === 'bookmarked' && (
|
|
<Button
|
|
onClick={handleCourseNavigation}
|
|
className="flex-1 text-base min-h-[44px]"
|
|
>
|
|
<Play className="h-4 w-4 mr-2" />
|
|
Start Course
|
|
</Button>
|
|
)}
|
|
|
|
{/* Secondary Actions */}
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
onClick={() => onBookmark?.(course.id)}
|
|
className="min-h-[44px] min-w-[44px]"
|
|
aria-label="Bookmark course"
|
|
>
|
|
<Heart className={`h-4 w-4 ${course.status === 'bookmarked' ? 'fill-current text-red-500' : ''}`} />
|
|
</Button>
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
className="min-h-[44px] min-w-[44px]"
|
|
aria-label="Share course"
|
|
>
|
|
<Share2 className="h-4 w-4" />
|
|
</Button>
|
|
|
|
{course.certificate && course.status === 'completed' && (
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
className="min-h-[44px] min-w-[44px]"
|
|
aria-label="Download certificate"
|
|
>
|
|
<Award className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
} |