- Created automated script (scripts/align-8px-grid.py) to align all spacing to 8px grid - Replaced non-8px-aligned spacing: gap-3/p-3/m-3 (12px) → gap-4/p-4/m-4 (16px), gap-5/p-5/m-5 (20px) → gap-6/p-6/m-6 (24px), gap-10/p-10/m-10 (40px) → gap-12/p-12/m-12 (48px), gap-20/p-20/m-20 (80px) → gap-24/p-24/m-24 (96px) - Preserved: 4px values (gap-1, p-1, m-1) as they may be intentional fine-tuning, responsive breakpoints (sm:, md:, lg:), test files, documentation - Modified files across all components to ensure consistent 8px grid alignment - Action 11.2.1.3: Align all elements to 8px grid - COMPLETE
348 lines
13 KiB
TypeScript
348 lines
13 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { Button } from '../ui/button';
|
|
import { Card } from '../ui/card';
|
|
import { Course } from '../../types';
|
|
import {
|
|
PlayCircle,
|
|
Star,
|
|
Users,
|
|
CheckCircle,
|
|
Clock,
|
|
Globe,
|
|
ShieldCheck,
|
|
Lock,
|
|
ChevronDown,
|
|
ChevronUp,
|
|
} from 'lucide-react';
|
|
import { useToast } from '../../context/ToastContext';
|
|
|
|
interface CourseDetailViewProps {
|
|
course: Course;
|
|
onBack: () => void;
|
|
onEnroll: () => void;
|
|
isEnrolled?: boolean;
|
|
}
|
|
|
|
export const CourseDetailView: React.FC<CourseDetailViewProps> = ({
|
|
course,
|
|
onBack,
|
|
onEnroll,
|
|
isEnrolled,
|
|
}) => {
|
|
const { addToast: _addToast } = useToast();
|
|
const [activeTab, setActiveTab] = useState<
|
|
'overview' | 'curriculum' | 'reviews'
|
|
>('overview');
|
|
const [expandedModule, setExpandedModule] = useState<string | null>(
|
|
course.modules?.[0].id || null,
|
|
);
|
|
|
|
const toggleModule = (id: string) => {
|
|
setExpandedModule(expandedModule === id ? null : id);
|
|
};
|
|
|
|
return (
|
|
<div className="animate-fadeIn pb-20 max-w-7xl mx-auto">
|
|
{/* Breadcrumb */}
|
|
<div className="mb-6">
|
|
<Button
|
|
variant="ghost"
|
|
onClick={onBack}
|
|
className="pl-0 text-kodo-content-dim hover:text-white"
|
|
>
|
|
← Back to Courses
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
{/* Left Content */}
|
|
<div className="lg:col-span-2 space-y-8">
|
|
{/* Header */}
|
|
<div>
|
|
<h1 className="text-3xl md:text-4xl font-display font-bold text-white mb-4">
|
|
{course.title}
|
|
</h1>
|
|
<p className="text-xl text-kodo-text-main mb-6 font-light">
|
|
{course.description}
|
|
</p>
|
|
|
|
<div className="flex flex-wrap items-center gap-6 text-sm text-kodo-content-dim mb-6">
|
|
{course.rating && (
|
|
<span className="flex items-center gap-1 text-kodo-gold font-bold">
|
|
<Star className="w-4 h-4 fill-current" /> {course.rating}
|
|
</span>
|
|
)}
|
|
<span className="flex items-center gap-1">
|
|
<Users className="w-4 h-4" />{' '}
|
|
{(course.studentCount || 0).toLocaleString()} students
|
|
</span>
|
|
<span className="flex items-center gap-1">
|
|
<Clock className="w-4 h-4" /> {course.duration} total
|
|
</span>
|
|
<span className="flex items-center gap-1">
|
|
<Globe className="w-4 h-4" /> English
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-4">
|
|
<img
|
|
src={`https://ui-avatars.com/api/?name=${course.instructor}&background=random`}
|
|
className="w-10 h-10 rounded-full"
|
|
/>
|
|
<div>
|
|
<div className="text-xs text-kodo-content-dim uppercase">
|
|
Created by
|
|
</div>
|
|
<div className="text-sm font-bold text-white text-kodo-cyan cursor-pointer hover:underline">
|
|
{course.instructor}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tabs */}
|
|
<div className="border-b border-kodo-steel flex gap-6">
|
|
{['overview', 'curriculum', 'reviews'].map((tab) => (
|
|
<button
|
|
key={tab}
|
|
onClick={() => setActiveTab(tab as any)}
|
|
className={`pb-3 text-sm font-bold uppercase tracking-wider border-b-2 transition-colors ${activeTab === tab ? 'border-kodo-cyan text-white' : 'border-transparent text-kodo-content-dim hover:text-kodo-text-main'}`}
|
|
>
|
|
{tab}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Tab Content */}
|
|
{activeTab === 'overview' && (
|
|
<div className="space-y-8 animate-fadeIn">
|
|
<Card variant="default">
|
|
<h3 className="font-bold text-white text-lg mb-4">
|
|
What you'll learn
|
|
</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{course.whatYouWillLearn?.map((item, i) => (
|
|
<div key={i} className="flex gap-4 text-sm text-kodo-text-main">
|
|
<CheckCircle className="w-4 h-4 text-kodo-lime flex-shrink-0 mt-0.5" />
|
|
<span>{item}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
|
|
<div>
|
|
<h3 className="font-bold text-white text-lg mb-4">
|
|
Requirements
|
|
</h3>
|
|
<ul className="list-disc pl-5 space-y-1 text-sm text-kodo-content-dim">
|
|
{course.requirements?.map((req, i) => (
|
|
<li key={i}>{req}</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'curriculum' && (
|
|
<div className="space-y-4 animate-fadeIn">
|
|
<div className="flex justify-between items-center text-sm text-kodo-content-dim mb-2">
|
|
<span>
|
|
{course.modules?.length} Modules •{' '}
|
|
{course.modules?.reduce(
|
|
(acc, m) => acc + m.lessons.length,
|
|
0,
|
|
)}{' '}
|
|
Lessons
|
|
</span>
|
|
<button
|
|
className="text-kodo-cyan hover:underline"
|
|
onClick={() =>
|
|
setExpandedModule(expandedModule ? null : 'all')
|
|
}
|
|
>
|
|
{expandedModule === 'all' ? 'Collapse All' : 'Expand All'}
|
|
</button>
|
|
</div>
|
|
|
|
{course.modules?.map((module) => (
|
|
<div
|
|
key={module.id}
|
|
className="border border-kodo-steel rounded-lg overflow-hidden bg-kodo-ink/30"
|
|
>
|
|
<div
|
|
className="p-4 flex justify-between items-center cursor-pointer hover:bg-white/5 transition-colors"
|
|
onClick={() => toggleModule(module.id)}
|
|
>
|
|
<h4 className="font-bold text-white flex items-center gap-4">
|
|
{expandedModule === module.id ||
|
|
expandedModule === 'all' ? (
|
|
<ChevronUp className="w-4 h-4" />
|
|
) : (
|
|
<ChevronDown className="w-4 h-4" />
|
|
)}
|
|
{module.title}
|
|
</h4>
|
|
<span className="text-xs text-kodo-content-dim">
|
|
{module.lessons.length} lectures
|
|
</span>
|
|
</div>
|
|
|
|
{(expandedModule === module.id ||
|
|
expandedModule === 'all') && (
|
|
<div className="border-t border-kodo-steel">
|
|
{module.lessons.map((lesson) => (
|
|
<div
|
|
key={lesson.id}
|
|
className="p-4 pl-8 flex justify-between items-center hover:bg-white/5 border-b border-kodo-steel/30 last:border-0"
|
|
>
|
|
<div className="flex items-center gap-4 text-sm text-kodo-text-main">
|
|
{lesson.type === 'video' ? (
|
|
<PlayCircle className="w-4 h-4" />
|
|
) : (
|
|
<ShieldCheck className="w-4 h-4" />
|
|
)}
|
|
{lesson.title}
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
{lesson.isLocked && !isEnrolled && (
|
|
<Lock className="w-3 h-3 text-kodo-content-dim" />
|
|
)}
|
|
<span className="text-xs text-kodo-content-dim">
|
|
{lesson.duration}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'reviews' && (
|
|
<div className="space-y-6 animate-fadeIn">
|
|
{course.reviews?.map((review) => (
|
|
<div
|
|
key={review.id}
|
|
className="border-b border-kodo-steel/50 pb-6"
|
|
>
|
|
<div className="flex items-center gap-4 mb-2">
|
|
<img
|
|
src={review.avatar}
|
|
className="w-10 h-10 rounded-full"
|
|
/>
|
|
<div>
|
|
<div className="font-bold text-white text-sm">
|
|
{review.username}
|
|
</div>
|
|
<div className="flex text-kodo-gold text-xs">
|
|
{[...Array(5)].map((_, i) => (
|
|
<Star
|
|
key={i}
|
|
className={`w-3 h-3 ${i < review.rating ? 'fill-current' : 'text-kodo-text-main'}`}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<span className="ml-auto text-xs text-kodo-content-dim">
|
|
{review.date}
|
|
</span>
|
|
</div>
|
|
<p className="text-sm text-kodo-text-main">{review.comment}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Right Sidebar */}
|
|
<div className="relative">
|
|
<div className="sticky top-24 space-y-6">
|
|
<Card
|
|
variant="default"
|
|
className="p-0 overflow-hidden border-kodo-steel/30"
|
|
>
|
|
{/* Preview Video Placeholder */}
|
|
<div className="relative aspect-video bg-black group cursor-pointer">
|
|
<img
|
|
src={course.thumbnailUrl}
|
|
className="w-full h-full object-cover opacity-80"
|
|
/>
|
|
<div className="absolute inset-0 flex items-center justify-center">
|
|
<div className="w-16 h-16 bg-white/90 rounded-full flex items-center justify-center shadow-lg group-hover:scale-110 transition-transform">
|
|
<PlayCircle className="w-8 h-8 text-black fill-current" />
|
|
</div>
|
|
</div>
|
|
<div className="absolute bottom-4 text-center w-full text-white font-bold text-sm drop-shadow-md">
|
|
Preview Course
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-6">
|
|
<div className="text-3xl font-display font-bold text-white mb-2">
|
|
{isEnrolled
|
|
? 'Enrolled'
|
|
: course.price && course.price > 0
|
|
? `$${course.price}`
|
|
: 'Free'}
|
|
</div>
|
|
{course.price && course.price > 0 && !isEnrolled && (
|
|
<p className="text-kodo-content-dim text-xs mb-6 line-through">
|
|
$199.99 (85% off)
|
|
</p>
|
|
)}
|
|
|
|
{isEnrolled ? (
|
|
<Button
|
|
variant="primary"
|
|
className="w-full h-12 text-lg"
|
|
onClick={onEnroll}
|
|
>
|
|
CONTINUE LEARNING
|
|
</Button>
|
|
) : (
|
|
<div className="space-y-4">
|
|
<Button
|
|
variant="primary"
|
|
className="w-full h-12 text-lg"
|
|
onClick={onEnroll}
|
|
>
|
|
ENROLL NOW
|
|
</Button>
|
|
<p className="text-center text-xs text-kodo-content-dim">
|
|
30-Day Money-Back Guarantee
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="mt-6 space-y-4">
|
|
<h4 className="font-bold text-white text-sm">
|
|
This course includes:
|
|
</h4>
|
|
<ul className="text-sm text-kodo-content-dim space-y-2">
|
|
<li className="flex items-center gap-4">
|
|
<PlayCircle className="w-4 h-4" /> {course.duration}{' '}
|
|
on-demand video
|
|
</li>
|
|
<li className="flex items-center gap-4">
|
|
<ShieldCheck className="w-4 h-4" /> Full lifetime access
|
|
</li>
|
|
<li className="flex items-center gap-4">
|
|
<Globe className="w-4 h-4" /> Access on mobile and TV
|
|
</li>
|
|
{course.certificateAvailable && (
|
|
<li className="flex items-center gap-4">
|
|
<Star className="w-4 h-4" /> Certificate of completion
|
|
</li>
|
|
)}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|