feat: reorganización de categorías y refactorización de CourseGrid

This commit is contained in:
NolbertoChagala committed 2026-08-26 11:26:30 -05:00
1 parent 7a5786eaa8
commit ae415fef4e
8 files changed
+626 -253

No files matched your search

+541 -186
View File
@@ -1,16 +1,23 @@
import { useState, useMemo } from 'react';
import {
FiFilter,
FiFlag,
FiStar,
FiPercent,
FiSpeaker,
FiLayers,
FiRefreshCw
import { useState, useMemo, useEffect } from 'react';
import {
FiFilter,
FiLayers,
FiSearch,
FiCode,
FiShield,
FiTrendingUp,
FiChevronRight,
FiGlobe,
FiFeather,
FiActivity,
FiUsers,
FiZap,
FiBriefcase,
FiMonitor,
FiGrid
} from 'react-icons/fi';
import type { Course, ActiveTabFilter } from '../../types/course';
import type { Course } from '../../types/course';
import { CourseCard } from './CourseCard';
import { CategoryPills } from './CategoryPills';
interface CourseGridProps {
courses: Course[];
@@ -19,6 +26,7 @@ interface CourseGridProps {
onAddToCart: (course: Course) => void;
onOpenDetails: (course: Course) => void;
onClearSearch: () => void;
showBanners?: boolean;
}
export const CourseGrid: React.FC<CourseGridProps> = ({
@@ -29,18 +37,139 @@ export const CourseGrid: React.FC<CourseGridProps> = ({
onOpenDetails,
onClearSearch,
}) => {
const [activeTab, setActiveTab] = useState<ActiveTabFilter>('all');
const [selectedCategory, setSelectedCategory] = useState<string>('Todos');
const [selectedSubcategory, setSelectedSubcategory] = useState<string>('Todos');
const [selectedPopularArea, setSelectedPopularArea] = useState<string>('Todos');
const [selectedLevel, setSelectedLevel] = useState<string>('Todos');
const [categorySearch, setCategorySearch] = useState<string>('');
const [sortBy, setSortBy] = useState<'rating' | 'price-asc' | 'price-desc' | 'students'>('students');
const [localSearch, setLocalSearch] = useState('');
const [showAll, setShowAll] = useState(false);
// Sync local search when global searchQuery prop changes (e.g. from navbar or clear button)
useEffect(() => {
setLocalSearch(searchQuery);
}, [searchQuery]);
// Reset subcategory and level when selected category changes
useEffect(() => {
setSelectedSubcategory('Todos');
setSelectedLevel('Todos');
}, [selectedCategory]);
// Reset popular area when category changes (avoid conflicts)
useEffect(() => {
if (selectedCategory !== 'Todos') {
setSelectedPopularArea('Todos');
setShowAll(false);
}
}, [selectedCategory]);
// Reset showAll when search is entered
useEffect(() => {
if (localSearch.trim() !== '') {
setShowAll(false);
}
}, [localSearch]);
// Helper to map and sanitize category names for flexible database mapping
const getCourseCategory = (course: Course) => {
const cat = course.category;
if (cat === 'Desarrollo Web') return 'Informática';
if (cat === 'Diseño UI/UX') return 'Informática'; // UI/UX maps to Informática
if (cat === 'Inteligencia Artificial') return 'Ciencia de datos';
if (cat === 'Ciberseguridad') return 'Tecnologías de la información';
if (cat === 'Marketing Digital') return 'Empresas';
if (cat === 'Fotografía') return 'Artes y Humanidades';
return cat;
};
// Helper to map subcategory topic names dynamically
const getCourseSubcategory = (course: Course) => {
if (course.subcategory) return course.subcategory;
// Fallback topic mappings based on keywords in title
const title = course.title.toLowerCase();
const cat = course.category;
if (cat === 'Desarrollo Web') {
if (title.includes('react') || title.includes('next.js') || title.includes('typescript')) return 'Desarrollo Web';
if (title.includes('aws') || title.includes('cloud') || title.includes('devops') || title.includes('docker')) return 'DevOps & Cloud';
return 'Desarrollo Web';
}
if (cat === 'Diseño UI/UX') {
if (title.includes('ux') || title.includes('figma')) return 'Diseño UX/UI';
return 'Sistemas de diseño';
}
if (cat === 'Fotografía') return 'Fotografía Digital';
if (cat === 'Inteligencia Artificial') return 'IA Generativa & LLMs';
if (cat === 'Ciberseguridad') return 'Ethical Hacking';
if (cat === 'Marketing Digital') return 'Embudos de venta';
return 'General';
};
// Compute subcategory list dynamically based on selected category
const subcategories = useMemo(() => {
if (selectedCategory === 'Todos') return [];
const subcats = courses
.filter((c) => getCourseCategory(c) === selectedCategory)
.map((c) => getCourseSubcategory(c));
return ['Todos', ...Array.from(new Set(subcats))];
}, [courses, selectedCategory]);
// Category information for dedicated page headers (description and metrics)
const categoryDetails = useMemo(() => {
if (selectedCategory === 'Todos') return null;
switch (selectedCategory) {
case 'Informática':
return {
description: 'Domina la programación web, algoritmos avanzados, TypeScript, frameworks modernos como React y Next.js, y el desarrollo estructurado de software.',
credentials: 53,
totalCourses: 310
};
case 'Ciencia de datos':
return {
description: 'Conviértete en experto en analizar grandes conjuntos de datos, modelado de Machine Learning, visualización científica e integración de Inteligencia Artificial.',
credentials: 35,
totalCourses: 182
};
case 'Tecnologías de la información':
return {
description: 'Capacítate en administración de redes, infraestructura cloud en AWS, automatización DevOps y auditorías avanzadas de ciberseguridad ofensiva.',
credentials: 29,
totalCourses: 150
};
case 'Empresas':
return {
description: 'Desarrolla habilidades comerciales estratégicas, administración ágil de proyectos (Scrum/Kanban) y embudos de marketing digital de alta conversión.',
credentials: 42,
totalCourses: 220
};
case 'Artes y Humanidades':
return {
description: 'Explora tu potencial creativo y artístico mediante cursos de fotografía digital profesional, composición visual y edición avanzada de imágenes.',
credentials: 18,
totalCourses: 94
};
default:
return {
description: 'Explora cursos líderes en esta disciplina, obtén credenciales de nivel profesional y acelera tu inserción en el mercado laboral.',
credentials: 12,
totalCourses: 60
};
}
}, [selectedCategory]);
// Filtered & Sorted courses computation
const filteredCourses = useMemo(() => {
return courses
.filter((course) => {
// Search query filter
if (searchQuery.trim()) {
const q = searchQuery.toLowerCase();
const activeSearch = localSearch.trim() || searchQuery.trim();
if (activeSearch) {
const q = activeSearch.toLowerCase();
const matchTitle = course.title.toLowerCase().includes(q);
const matchSub = course.subtitle.toLowerCase().includes(q);
const matchCat = course.category.toLowerCase().includes(q);
@@ -48,21 +177,28 @@ export const CourseGrid: React.FC<CourseGridProps> = ({
if (!matchTitle && !matchSub && !matchCat && !matchInst) return false;
}
// Category filter
// Category Page Filter (active only when selectedCategory !== 'Todos')
if (selectedCategory !== 'Todos') {
if (course.category !== selectedCategory) return false;
}
const cat = getCourseCategory(course);
if (cat !== selectedCategory) return false;
// Level filter
if (selectedLevel !== 'Todos') {
if (course.level !== selectedLevel && course.level !== 'Todos los niveles') return false;
}
// Level Filter (inside category page level pills)
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;
// Subcategory filter
if (selectedSubcategory !== 'Todos') {
const sub = getCourseSubcategory(course);
if (sub !== selectedSubcategory) return false;
}
} else {
// Main Page Popular Area Filter (active only when selectedCategory === 'Todos')
if (selectedPopularArea !== 'Todos') {
const cat = getCourseCategory(course);
if (cat !== selectedPopularArea) return false;
}
}
return true;
})
@@ -72,188 +208,407 @@ export const CourseGrid: React.FC<CourseGridProps> = ({
if (sortBy === 'price-desc') return b.price - a.price;
return b.studentsCount - a.studentsCount; // default: popular
});
}, [courses, searchQuery, selectedCategory, selectedLevel, activeTab, sortBy]);
}, [courses, searchQuery, localSearch, selectedCategory, selectedSubcategory, selectedPopularArea, selectedLevel, sortBy]);
// Filtered courses for general list when selectedCategory === 'Todos'
const generalCourses = useMemo<Course[]>(() => {
return courses
.filter((course) => {
// Search query filter
const activeSearch = localSearch.trim() || searchQuery.trim();
if (activeSearch) {
const q = activeSearch.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;
}
// Popular area filter
if (selectedPopularArea !== 'Todos') {
const cat = getCourseCategory(course);
if (cat !== selectedPopularArea) return false;
}
return true;
})
.sort((a, b) => b.studentsCount - a.studentsCount);
}, [courses, searchQuery, localSearch, selectedPopularArea]);
const resetAllFilters = () => {
setActiveTab('all');
setSelectedCategory('Todos');
setSelectedSubcategory('Todos');
setSelectedPopularArea('Todos');
setSelectedLevel('Todos');
setSortBy('students');
setLocalSearch('');
setShowAll(false);
onClearSearch();
};
// Dynamic Icon Mapper Helper for category pills
const getCategoryIcon = (catName: string) => {
const name = catName.toLowerCase();
if (name.includes('informática') || name.includes('computación') || name.includes('programación') || name.includes('código')) {
return <FiCode className="text-xs shrink-0 text-cyan-400" />;
}
if (name.includes('tecnolog') || name.includes('redes') || name.includes('sistemas') || name.includes('monitor')) {
return <FiMonitor className="text-xs shrink-0 text-cyan-400" />;
}
if (name.includes('datos') || name.includes('inteligencia') || name.includes('ia') || name.includes('analytics') || name.includes('trending')) {
return <FiTrendingUp className="text-xs shrink-0" />;
}
if (name.includes('ingenier') || name.includes('física') || name.includes('química') || name.includes('actividad')) {
return <FiActivity className="text-xs shrink-0 text-cyan-400" />;
}
if (name.includes('matemát') || name.includes('lógica') || name.includes('cálculo') || name.includes('grid')) {
return <FiGrid className="text-xs shrink-0" />;
}
if (name.includes('empresa') || name.includes('negocio') || name.includes('finanzas') || name.includes('briefcase')) {
return <FiBriefcase className="text-xs shrink-0" />;
}
if (name.includes('arte') || name.includes('humanidad') || name.includes('diseño') || name.includes('feather') || name.includes('fotografía')) {
return <FiFeather className="text-xs shrink-0" />;
}
if (name.includes('idioma') || name.includes('lengua') || name.includes('globe') || name.includes('inglés')) {
return <FiGlobe className="text-xs shrink-0" />;
}
if (name.includes('social') || name.includes('comunidad') || name.includes('users') || name.includes('psicología')) {
return <FiUsers className="text-xs shrink-0" />;
}
if (name.includes('personal') || name.includes('liderazgo') || name.includes('zap') || name.includes('crecimiento')) {
return <FiZap className="text-xs shrink-0 text-cyan-400" />;
}
if (name.includes('salud') || name.includes('shield') || name.includes('medicina') || name.includes('bienestar')) {
return <FiShield className="text-xs shrink-0 text-cyan-400" />;
}
return <FiLayers className="text-xs shrink-0" />; // generic fallback
};
// Get all unique category names
const allCategories = useMemo(() => {
return Array.from(new Set(courses.map(getCourseCategory))).sort();
}, [courses]);
// Filter categories by categorySearch query
const filteredCategories = useMemo(() => {
const query = categorySearch.trim().toLowerCase();
return allCategories.filter(cat => cat.toLowerCase().includes(query));
}, [allCategories, categorySearch]);
// Popular Areas matching "Más populares" pills from Coursera screenshot
const popularAreasList = [
{ name: 'Empresas' },
{ name: 'Ciencia de datos' },
{ name: 'Tecnologías de la información' },
{ name: 'Informática' },
];
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>
<section id="catalog-section" className="py-6 bg-slate-950/60">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-8">
{/* VIEW TYPE A: GENERAL EXPLORE PAGE */}
{selectedCategory === 'Todos' ? (
<div className="space-y-8">
{/* Section Header & Search Bar (Side-by-Side) */}
<div className="border-b border-slate-800/80 pb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div className="space-y-1">
<div className="flex items-center gap-2 text-cyan-400 text-xs font-bold">
<FiLayers />
<span>Catálogo de recursos</span>
</div>
<h2 className="text-2xl sm:text-3xl font-extrabold text-white tracking-tight">
Explorar por categoría
</h2>
<p className="text-sm text-slate-400">
Selecciona una categoría para ver los cursos y certificaciones disponibles.
</p>
</div>
{/* Search Bar compact on the right */}
<div className="relative w-full md:w-80 lg:w-96 shrink-0">
<FiSearch className="absolute left-4 top-3.5 text-slate-400 text-base" />
<input
type="text"
value={localSearch}
onChange={(e) => setLocalSearch(e.target.value)}
placeholder="Buscar cursos..."
className="w-full pl-11 pr-4 py-3 bg-slate-900 border border-slate-800 rounded-2xl text-sm text-slate-200 placeholder-slate-500 focus:outline-none focus:border-cyan-500 transition-colors"
/>
</div>
</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>
{/* Explore Categories Directory */}
<div className="space-y-4 pt-2">
{/* Category Search Filter Input */}
{allCategories.length > 5 && (
<div className="flex justify-end">
<div className="relative w-full sm:w-64 shrink-0">
<FiFilter className="absolute left-3.5 top-2.5 text-slate-500 text-xs" />
<input
type="text"
value={categorySearch}
onChange={(e) => setCategorySearch(e.target.value)}
placeholder="Filtrar categorías por nombre..."
className="w-full pl-9 pr-4 py-2 bg-slate-900 border border-slate-800 rounded-full text-xs text-slate-200 placeholder-slate-505 focus:outline-none focus:border-cyan-500 transition-colors"
/>
</div>
</div>
)}
<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>
{filteredCategories.length > 0 ? (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
{filteredCategories.map((catName) => (
<button
key={catName}
onClick={() => setSelectedCategory(catName)}
className="flex items-center gap-3 pl-3 pr-4 py-2.5 bg-slate-900/30 hover:bg-slate-900 border border-slate-850 hover:border-cyan-500/30 text-slate-300 hover:text-white rounded-full text-xs font-semibold hover:-translate-y-0.5 hover:shadow-lg hover:shadow-cyan-500/5 transition-all duration-200 cursor-pointer group"
>
{/* Circle icon wrapper */}
<div className="w-6 h-6 rounded-full bg-slate-950 border border-slate-850 flex items-center justify-center text-slate-400 group-hover:text-cyan-400 transition-colors shrink-0">
{getCategoryIcon(catName)}
</div>
<span className="truncate">{catName}</span>
</button>
))}
</div>
) : (
<div className="py-8 text-center bg-slate-900/10 rounded-2xl border border-slate-900/80 p-6">
<p className="text-xs text-slate-400">
No se encontraron categorías que coincidan con "{categorySearch}".
</p>
</div>
)}
</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}
/>
))}
{/* 2. Áreas más populares (Sentence case, descriptive subtitle) */}
<div className="space-y-3 pt-4 border-t border-slate-900/60">
<h3 className="text-xl font-bold text-white tracking-tight">
Áreas más populares
</h3>
<p className="text-xs text-slate-400">
Descubre los temas de mayor demanda laboral en la actualidad.
</p>
<div className="flex flex-wrap gap-2.5">
{/* Todos Pill */}
<button
onClick={() => {
setSelectedPopularArea('Todos');
setShowAll(false);
}}
className={`px-4 py-2 rounded-full text-xs font-semibold border transition-all cursor-pointer ${selectedPopularArea === 'Todos' && !showAll
? 'bg-slate-200 border-slate-200 text-slate-950 font-bold shadow-md'
: 'bg-slate-900/30 hover:bg-slate-900 border-slate-800/80 hover:border-slate-700 text-slate-300 hover:text-white'
}`}
>
Todos
</button>
{/* Popular Area Pills */}
{popularAreasList.map((area) => {
const isSelected = selectedPopularArea === area.name;
return (
<button
key={area.name}
onClick={() => setSelectedPopularArea((prev) => (prev === area.name ? 'Todos' : area.name))}
className={`px-4 py-2 rounded-full text-xs font-semibold border transition-all cursor-pointer ${isSelected
? 'bg-slate-200 border-slate-200 text-slate-950 font-bold shadow-md'
: 'bg-slate-900/30 hover:bg-slate-900 border-slate-800/80 hover:border-slate-700 text-slate-300 hover:text-white'
}`}
>
{area.name}
</button>
);
})}
</div>
</div>
{/* General Courses Grid */}
<div className="pt-6 border-t border-slate-900/80 space-y-4">
<div className="space-y-1">
<h3 className="text-xl font-bold text-white tracking-tight">
Listado de cursos
</h3>
<p className="text-xs text-slate-400">
Todos los cursos incluyen material de aprendizaje de alta calidad y acceso permanente.
</p>
</div>
{generalCourses.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 lg:gap-8">
{generalCourses.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</h3>
<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 filtros
</button>
</div>
)}
</div>
</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 />
/* VIEW TYPE B: DEDICATED CATEGORY PAGE */
<div className="space-y-8">
{/* Breadcrumbs Navigation */}
<div className="flex items-center gap-2 text-xs text-slate-400 select-none">
<span
className="hover:text-cyan-400 cursor-pointer transition-colors"
onClick={resetAllFilters}
>
Categorías
</span>
<FiChevronRight className="text-[10px] text-slate-650" />
<span className="text-slate-200 font-medium select-text">
{selectedCategory}
</span>
</div>
{/* Category Page Header Block (Title, Description, and Metrics side-by-side) */}
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-6 pb-6 border-b border-slate-800">
<div className="space-y-3 max-w-3xl">
<h1 className="text-3xl sm:text-4xl font-extrabold text-white tracking-tight">
{selectedCategory}
</h1>
{categoryDetails && (
<p className="text-sm text-slate-400 leading-relaxed">
{categoryDetails.description}
</p>
)}
</div>
{/* Metrics block on the right */}
{categoryDetails && (
<div className="flex items-center gap-8 lg:pl-8 lg:border-l border-slate-800 shrink-0 select-none">
<div className="text-center">
<div className="text-3xl font-extrabold text-white">{categoryDetails.credentials}</div>
<div className="text-xs text-slate-400 font-medium">credenciales</div>
</div>
<div className="text-center">
<div className="text-xs text-slate-400 font-medium">cursos</div>
<div className="text-3xl font-extrabold text-white">de {categoryDetails.totalCourses}</div>
</div>
</div>
)}
</div>
{/* Subcategories - visible only when category has multiple topics */}
{subcategories.length > 2 && (
<div className="space-y-3 pt-1">
<div className="text-xs text-slate-400 font-semibold">Temas de {selectedCategory}</div>
<div className="flex flex-wrap gap-2.5">
{subcategories.map((sub) => {
const isSubSelected = selectedSubcategory === sub;
return (
<button
key={sub}
onClick={() => setSelectedSubcategory(sub)}
className={`flex items-center gap-1.5 px-4 py-2 rounded-full text-xs font-semibold transition-all border cursor-pointer ${isSubSelected
? 'bg-cyan-500/10 border-cyan-500/40 text-cyan-300 shadow-md shadow-cyan-500/5'
: 'bg-slate-900/60 hover:bg-slate-900 border-slate-800 hover:border-slate-700 text-slate-300 hover:text-white'
}`}
>
<span>{sub === 'Todos' ? `Todos en ${selectedCategory}` : sub}</span>
</button>
);
})}
</div>
</div>
)}
{/* "Filtrar por nivel" (Sentence case, descriptive subtitle, capsules format) */}
<div className="space-y-3">
<h3 className="text-xl font-bold text-white tracking-tight">
Filtrar por nivel
</h3>
<p className="text-xs text-slate-400">
Selecciona la dificultad adecuada para tu ritmo de aprendizaje.
</p>
<div className="flex flex-wrap gap-2.5">
{['Todos', 'Principiante', 'Intermedio', 'Avanzado'].map((lvl) => {
const isLvlSelected = selectedLevel === lvl;
return (
<button
key={lvl}
onClick={() => setSelectedLevel(lvl)}
className={`px-4 py-2 rounded-full text-xs font-semibold border transition-all cursor-pointer ${isLvlSelected
? 'bg-slate-200 border-slate-200 text-slate-950 font-bold shadow-md'
: 'bg-slate-900/30 hover:bg-slate-900 border-slate-800/80 hover:border-slate-700 text-slate-300 hover:text-white'
}`}
>
{lvl === 'Todos' ? 'Todos los niveles' : lvl}
</button>
);
})}
</div>
</div>
{/* Section: Listado de cursos (Descriptive title & subtitle) */}
<div className="pt-6 border-t border-slate-900/80 space-y-4">
<div className="space-y-1">
<h3 className="text-xl font-bold text-white tracking-tight">
Listado de cursos de {selectedCategory}
</h3>
<p className="text-xs text-slate-400">
Encuentra el material didáctico necesario para avanzar en tu formación profesional.
</p>
</div>
{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 en este nivel</h3>
<button
onClick={() => setSelectedLevel('Todos')}
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"
>
Ver todos los niveles
</button>
</div>
)}
</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>
);
};
};