158 lines
5.5 KiB
TypeScript
158 lines
5.5 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { Button } from '../ui/button';
|
|
import { SearchInput } from '../ui/input';
|
|
import { Course } from '../../types';
|
|
import { CourseCard } from '../education/CourseCard';
|
|
import { Filter, BookOpen, GraduationCap, Loader2 } from 'lucide-react';
|
|
import { educationService } from '../../services/educationService';
|
|
import { logger } from '@/utils/logger';
|
|
|
|
interface EducationViewProps {
|
|
onCourseClick?: (course: Course) => void;
|
|
onMyCoursesClick?: () => void;
|
|
}
|
|
|
|
export const EducationView: React.FC<EducationViewProps> = ({
|
|
onCourseClick,
|
|
onMyCoursesClick,
|
|
}) => {
|
|
const [search, setSearch] = useState('');
|
|
const [filterLevel, setFilterLevel] = useState<string>('All');
|
|
const [filterPrice, setFilterPrice] = useState<string>('All');
|
|
const [courses, setCourses] = useState<Course[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
const loadCourses = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const data = await educationService.getCatalog();
|
|
setCourses(data);
|
|
} catch (e) {
|
|
logger.error('Failed to load courses', {
|
|
error: e instanceof Error ? e.message : String(e),
|
|
stack: e instanceof Error ? e.stack : undefined,
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
loadCourses();
|
|
}, []);
|
|
|
|
const filtered = courses.filter((c) => {
|
|
const matchSearch =
|
|
c.title.toLowerCase().includes(search.toLowerCase()) ||
|
|
c.tags?.some((t) => t.toLowerCase().includes(search.toLowerCase()));
|
|
const matchLevel = filterLevel === 'All' || c.level === filterLevel;
|
|
const matchPrice =
|
|
filterPrice === 'All' ||
|
|
(filterPrice === 'Free' ? c.price === 0 : (c.price || 0) > 0);
|
|
return matchSearch && matchLevel && matchPrice;
|
|
});
|
|
|
|
return (
|
|
<div className="space-y-8 animate-fadeIn pb-20">
|
|
{/* Header */}
|
|
<div className="flex flex-col md:flex-row justify-between items-end border-b border-kodo-steel/50 pb-6 gap-4">
|
|
<div>
|
|
<h2 className="text-2xl font-display font-bold text-white mb-2">
|
|
ACADEMY
|
|
</h2>
|
|
<p className="text-kodo-content-dim font-mono text-sm">
|
|
Level up your skills. Earn certificates.
|
|
</p>
|
|
</div>
|
|
<Button
|
|
variant="gaming"
|
|
icon={<GraduationCap className="w-4 h-4" />}
|
|
onClick={onMyCoursesClick}
|
|
>
|
|
MY LEARNING
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Filters & Search */}
|
|
<div className="flex flex-col md:flex-row gap-4 items-center bg-kodo-ink/50 p-4 rounded-xl border border-kodo-steel/50">
|
|
<div className="w-full md:w-96">
|
|
<SearchInput
|
|
placeholder="Search for courses, skills, or teachers..."
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-2 w-full md:w-auto">
|
|
<div className="flex items-center gap-2 bg-kodo-void rounded-lg p-1 border border-kodo-steel">
|
|
<Filter className="w-4 h-4 text-kodo-content-dim ml-2" />
|
|
<select
|
|
className="bg-transparent text-sm text-kodo-text-main focus:outline-none p-1 cursor-pointer"
|
|
value={filterLevel}
|
|
onChange={(e) => setFilterLevel(e.target.value)}
|
|
>
|
|
<option value="All">All Levels</option>
|
|
<option value="Beginner">Beginner</option>
|
|
<option value="Intermediate">Intermediate</option>
|
|
<option value="Advanced">Advanced</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 bg-kodo-void rounded-lg p-1 border border-kodo-steel">
|
|
<DollarSignIcon className="w-4 h-4 text-kodo-content-dim ml-2" />
|
|
<select
|
|
className="bg-transparent text-sm text-kodo-text-main focus:outline-none p-1 cursor-pointer"
|
|
value={filterPrice}
|
|
onChange={(e) => setFilterPrice(e.target.value)}
|
|
>
|
|
<option value="All">All Prices</option>
|
|
<option value="Free">Free</option>
|
|
<option value="Paid">Paid</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Course Grid */}
|
|
{loading ? (
|
|
<div className="flex justify-center py-20">
|
|
<Loader2 className="w-10 h-10 text-kodo-cyan animate-spin" />
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
|
{filtered.map((course) => (
|
|
<CourseCard
|
|
key={course.id}
|
|
course={course}
|
|
onClick={(c) => onCourseClick && onCourseClick(c)}
|
|
/>
|
|
))}
|
|
{filtered.length === 0 && (
|
|
<div className="col-span-full text-center py-20 text-kodo-content-dim">
|
|
<BookOpen className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
|
<p>No courses found matching your criteria.</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Helper Icon for local usage if needed, otherwise rely on lucide import
|
|
const DollarSignIcon = ({ className }: { className?: string }) => (
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="24"
|
|
height="24"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
className={className}
|
|
>
|
|
<line x1="12" x2="12" y1="2" y2="22" />
|
|
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
|
|
</svg>
|
|
);
|