initial proyect
This commit is contained in:
commit
509272a643
31 files changed
+7170
No files matched your search
@@ -0,0 +1,259 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import {
|
||||
FiFilter,
|
||||
FiFlag,
|
||||
FiStar,
|
||||
FiPercent,
|
||||
FiSpeaker,
|
||||
FiLayers,
|
||||
FiRefreshCw
|
||||
} from 'react-icons/fi';
|
||||
import type { Course, ActiveTabFilter } from '../../types/course';
|
||||
import { CourseCard } from './CourseCard';
|
||||
import { CategoryPills } from './CategoryPills';
|
||||
|
||||
interface CourseGridProps {
|
||||
courses: Course[];
|
||||
cartCourseIds: string[];
|
||||
searchQuery: string;
|
||||
onAddToCart: (course: Course) => void;
|
||||
onOpenDetails: (course: Course) => void;
|
||||
onClearSearch: () => void;
|
||||
}
|
||||
|
||||
export const CourseGrid: React.FC<CourseGridProps> = ({
|
||||
courses,
|
||||
cartCourseIds,
|
||||
searchQuery,
|
||||
onAddToCart,
|
||||
onOpenDetails,
|
||||
onClearSearch,
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState<ActiveTabFilter>('all');
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>('Todos');
|
||||
const [selectedLevel, setSelectedLevel] = useState<string>('Todos');
|
||||
const [sortBy, setSortBy] = useState<'rating' | 'price-asc' | 'price-desc' | 'students'>('students');
|
||||
|
||||
// Filtered & Sorted courses computation
|
||||
const filteredCourses = useMemo(() => {
|
||||
return courses
|
||||
.filter((course) => {
|
||||
// Search query filter
|
||||
if (searchQuery.trim()) {
|
||||
const q = searchQuery.toLowerCase();
|
||||
const matchTitle = course.title.toLowerCase().includes(q);
|
||||
const matchSub = course.subtitle.toLowerCase().includes(q);
|
||||
const matchCat = course.category.toLowerCase().includes(q);
|
||||
const matchInst = course.instructor.name.toLowerCase().includes(q);
|
||||
if (!matchTitle && !matchSub && !matchCat && !matchInst) return false;
|
||||
}
|
||||
|
||||
// Category filter
|
||||
if (selectedCategory !== 'Todos') {
|
||||
if (course.category !== selectedCategory) return false;
|
||||
}
|
||||
|
||||
// Level filter
|
||||
if (selectedLevel !== 'Todos') {
|
||||
if (course.level !== selectedLevel && course.level !== 'Todos los niveles') return false;
|
||||
}
|
||||
|
||||
// Tab filter
|
||||
if (activeTab === 'popular' && course.studentsCount < 6000) return false;
|
||||
if (activeTab === 'top-rated' && course.rating < 4.9) return false;
|
||||
if (activeTab === 'offers' && !course.badge?.includes('Oferta') && course.originalPrice <= course.price) return false;
|
||||
if (activeTab === 'new' && course.badge !== 'Nuevo') return false;
|
||||
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (sortBy === 'rating') return b.rating - a.rating;
|
||||
if (sortBy === 'price-asc') return a.price - b.price;
|
||||
if (sortBy === 'price-desc') return b.price - a.price;
|
||||
return b.studentsCount - a.studentsCount; // default: popular
|
||||
});
|
||||
}, [courses, searchQuery, selectedCategory, selectedLevel, activeTab, sortBy]);
|
||||
|
||||
const resetAllFilters = () => {
|
||||
setActiveTab('all');
|
||||
setSelectedCategory('Todos');
|
||||
setSelectedLevel('Todos');
|
||||
setSortBy('students');
|
||||
onClearSearch();
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="catalog-section" className="py-12 bg-slate-950/60">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
|
||||
{/* Section Header */}
|
||||
<div className="flex flex-col md:flex-row md:items-end justify-between mb-8 gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-cyan-400 text-xs font-bold uppercase tracking-wider mb-2">
|
||||
<FiLayers />
|
||||
<span>Catálogo Completo</span>
|
||||
</div>
|
||||
<h2 className="text-2xl sm:text-3xl font-extrabold text-white tracking-tight">
|
||||
Explora Cursos Destacados
|
||||
</h2>
|
||||
<p className="text-sm text-slate-400 mt-1">
|
||||
Filtra por tu área de interés, nivel o aprovecha promociones de temporada.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Quick Tabs */}
|
||||
<div className="flex flex-wrap items-center gap-1.5 p-1 bg-slate-900 border border-slate-800 rounded-xl">
|
||||
<button
|
||||
onClick={() => setActiveTab('all')}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
||||
activeTab === 'all'
|
||||
? 'bg-cyan-500/20 text-cyan-300 border border-cyan-500/40'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Todos ({courses.length})
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('popular')}
|
||||
className={`flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
||||
activeTab === 'popular'
|
||||
? 'bg-amber-500/20 text-amber-300 border border-amber-500/40'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<FiFlag className="text-amber-400" /> Populares
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('top-rated')}
|
||||
className={`flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
||||
activeTab === 'top-rated'
|
||||
? 'bg-blue-500/20 text-blue-300 border border-blue-500/40'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<FiStar className="text-blue-400" /> Top Calificados
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('offers')}
|
||||
className={`flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
||||
activeTab === 'offers'
|
||||
? 'bg-rose-500/20 text-rose-300 border border-rose-500/40'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<FiPercent className="text-rose-400" /> Ofertas
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('new')}
|
||||
className={`flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
||||
activeTab === 'new'
|
||||
? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/40'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<FiSpeaker className="text-emerald-400" /> Nuevos
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Categories Bar */}
|
||||
<div className="mb-6">
|
||||
<CategoryPills
|
||||
selectedCategory={selectedCategory}
|
||||
onSelectCategory={setSelectedCategory}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Secondary Filter & Sorting Controls */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-4 p-4 rounded-xl bg-slate-900/50 border border-slate-800/80 mb-8">
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<span className="flex items-center gap-1 text-xs font-semibold text-slate-400">
|
||||
<FiFilter className="text-cyan-400" /> Filtros:
|
||||
</span>
|
||||
|
||||
{/* Level Filter */}
|
||||
<select
|
||||
value={selectedLevel}
|
||||
onChange={(e) => setSelectedLevel(e.target.value)}
|
||||
className="bg-slate-800 text-slate-200 border border-slate-700 rounded-lg px-3 py-1.5 text-xs font-medium focus:outline-none focus:border-cyan-500"
|
||||
>
|
||||
<option value="Todos">Nivel: Todos</option>
|
||||
<option value="Principiante">Principiante</option>
|
||||
<option value="Intermedio">Intermedio</option>
|
||||
<option value="Avanzado">Avanzado</option>
|
||||
</select>
|
||||
|
||||
{/* Clear Filters if active */}
|
||||
{(selectedCategory !== 'Todos' || selectedLevel !== 'Todos' || activeTab !== 'all' || searchQuery.trim()) && (
|
||||
<button
|
||||
onClick={resetAllFilters}
|
||||
className="flex items-center gap-1 text-xs text-rose-400 hover:text-rose-300 font-medium px-2 py-1 rounded bg-rose-500/10 border border-rose-500/20 transition"
|
||||
>
|
||||
<FiRefreshCw className="text-[10px]" /> Limpiar filtros
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sort By Dropdown & Results Counter */}
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-slate-400">
|
||||
Mostrando <strong className="text-white font-mono">{filteredCourses.length}</strong> cursos
|
||||
</span>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-slate-400">Ordenar por:</span>
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value as any)}
|
||||
className="bg-slate-800 text-slate-200 border border-slate-700 rounded-lg px-3 py-1.5 text-xs font-medium focus:outline-none focus:border-cyan-500"
|
||||
>
|
||||
<option value="students">Más populares (Alumnos)</option>
|
||||
<option value="rating">Mejor valorados</option>
|
||||
<option value="price-asc">Precio: Menor a Mayor</option>
|
||||
<option value="price-desc">Precio: Mayor a Menor</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Courses Grid */}
|
||||
{filteredCourses.length > 0 ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 lg:gap-8">
|
||||
{filteredCourses.map((course) => (
|
||||
<CourseCard
|
||||
key={course.id}
|
||||
course={course}
|
||||
isInCart={cartCourseIds.includes(course.id)}
|
||||
onAddToCart={onAddToCart}
|
||||
onOpenDetails={onOpenDetails}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-20 bg-slate-900/40 rounded-2xl border border-slate-800 p-8 space-y-4">
|
||||
<div className="w-16 h-16 rounded-full bg-slate-800 text-slate-400 flex items-center justify-center mx-auto text-2xl">
|
||||
<FiFilter />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-white">No se encontraron cursos con estos filtros</h3>
|
||||
<p className="text-xs text-slate-400 max-w-md mx-auto">
|
||||
Intenta cambiar los términos de búsqueda, la categoría o restablecer los filtros para ver todos los cursos disponibles.
|
||||
</p>
|
||||
<button
|
||||
onClick={resetAllFilters}
|
||||
className="px-4 py-2 bg-gradient-to-r from-blue-600 to-cyan-500 text-white text-xs font-bold rounded-xl shadow-lg shadow-cyan-500/20"
|
||||
>
|
||||
Restablecer todos los filtros
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user