Files
compor-academy/pages/TrailPage.tsx
Cauê Faleiros f5aa826b32
All checks were successful
CI / Validate frontend and API (push) Successful in 36s
CI / Build and publish Docker images (push) Successful in 20s
feat: add editable ordered trail pages
2026-09-08 15:53:20 -03:00

35 lines
3.9 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { ArrowLeft, BookOpen, Loader2 } from 'lucide-react';
import { useNavigate, useParams } from 'react-router-dom';
import { Course } from '../types';
import { getCourses, getLearningPaths, PublicLearningPath } from '../services/db';
import { CourseCoverImage } from '../components/CourseCoverImage';
import { CourseCard } from '../components/CourseCard';
export const TrailPage: React.FC<{ onPlay: (course: Course) => void }> = ({ onPlay }) => {
const { trailId } = useParams<{ trailId: string }>();
const navigate = useNavigate();
const [trail, setTrail] = useState<PublicLearningPath | null>(null);
const [courses, setCourses] = useState<Course[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
if (!trailId) return;
Promise.all([getLearningPaths(), getCourses()])
.then(([paths, availableCourses]) => {
const found = paths.find((item) => item.id === trailId) || null;
setTrail(found);
if (found) {
const byId = new Map(availableCourses.map((course) => [course.id, course]));
setCourses(found.courses.map((course) => byId.get(course.id)).filter((course): course is Course => Boolean(course)));
}
})
.finally(() => setIsLoading(false));
}, [trailId]);
if (isLoading) return <main className="flex min-h-[65vh] items-center justify-center pt-24"><Loader2 className="h-8 w-8 animate-spin text-orange-400" /></main>;
if (!trail) return <main className="mx-auto flex min-h-[65vh] max-w-6xl items-center px-6 pt-24"><div><p className="text-xs font-bold uppercase tracking-[0.18em] text-orange-400">Trilha indisponível</p><h1 className="mt-3 text-3xl font-bold text-white">Esta trilha não está publicada.</h1></div></main>;
return <main className="min-h-screen pb-20 pt-20"><section className="relative overflow-hidden border-b border-white/10 bg-zinc-950"><div className="absolute inset-0 opacity-30">{trail.coverImageUrl && <CourseCoverImage source={trail.coverImageUrl} alt="" className="h-full w-full object-cover" style={{ transform: `scale(${trail.coverImageZoom})`, objectPosition: `${trail.coverImagePositionX}% ${trail.coverImagePositionY}%` }} />}</div><div className="absolute inset-0 bg-gradient-to-r from-black via-black/90 to-black/45" /><div className="relative mx-auto max-w-6xl px-6 py-14 sm:px-8 sm:py-20"><button onClick={() => navigate('/')} className="mb-8 inline-flex items-center gap-2 text-sm font-semibold text-gray-300 transition hover:text-orange-300"><ArrowLeft className="h-4 w-4" /> Voltar para cursos</button><p className="text-xs font-bold uppercase tracking-[0.18em] text-orange-400">Trilha de cursos</p><h1 className="mt-3 max-w-3xl text-4xl font-bold tracking-tight text-white sm:text-5xl">{trail.title}</h1>{trail.description && <p className="mt-5 max-w-2xl text-base leading-7 text-gray-300">{trail.description}</p>}<div className="mt-7 inline-flex items-center gap-2 rounded-full border border-white/15 bg-black/30 px-4 py-2 text-sm font-semibold text-white"><BookOpen className="h-4 w-4 text-orange-400" />{courses.length} {courses.length === 1 ? 'curso na trilha' : 'cursos na trilha'}</div></div></section><section className="mx-auto max-w-6xl px-6 py-12 sm:px-8"><div className="mb-7"><p className="text-xs font-bold uppercase tracking-[0.18em] text-orange-400">Sua sequência</p><h2 className="mt-2 text-2xl font-bold text-white">Cursos da trilha</h2></div>{courses.length ? <div className="grid gap-7 sm:grid-cols-2 lg:grid-cols-3">{courses.map((course, index) => <div key={course.id}><p className="mb-3 text-xs font-bold text-orange-300">{String(index + 1).padStart(2, '0')} · Curso</p><CourseCard course={course} variant="landscape" onClick={onPlay} /></div>)}</div> : <div className="rounded-2xl border border-dashed border-white/15 p-10 text-center text-sm text-gray-500">Nenhum curso publicado nesta trilha.</div>}</section></main>;
};