Allow heading tags (h1-h6) in ContentSanitizer so news editor headings render

This commit is contained in:
2026-06-04 07:52:57 +02:00
parent 0b33a1b074
commit 15870ddb1f
191 changed files with 15453 additions and 1786 deletions

View File

@@ -64,35 +64,35 @@ function LessonChip({ lesson }) {
const isCompleted = Boolean(lesson?.completed)
const readingMinutes = Number(lesson?.reading_minutes || 0)
const ctaLabel = isCompleted ? 'Review lesson' : 'Open lesson'
const difficultyLabel = lesson?.difficulty || 'lesson'
const accessLabel = lesson?.access_level || 'free'
const lessonTypeLabel = lesson?.lesson_type || 'article'
const statusLabel = isCompleted ? 'Completed' : lesson?.is_required ? 'Required next' : 'Optional read'
const supportCopy = isCompleted ? 'You already finished this lesson.' : lesson?.is_required ? 'Recommended as the next required step in this course.' : 'Optional depth you can take at your own pace.'
return (
<Link
href={lesson.course_url || `/academy/lessons/${lesson.slug}`}
className={[
'group relative overflow-hidden rounded-[32px] border bg-[linear-gradient(180deg,rgba(15,23,42,0.92),rgba(15,23,42,0.64))] shadow-[0_24px_50px_rgba(2,6,23,0.2)] transition duration-200 hover:-translate-y-1 hover:border-sky-300/30 hover:shadow-[0_28px_70px_rgba(14,165,233,0.12)]',
'group relative overflow-hidden rounded-[34px] border bg-[linear-gradient(180deg,rgba(15,23,42,0.96),rgba(15,23,42,0.78))] shadow-[0_24px_54px_rgba(2,6,23,0.22)] transition duration-200 hover:-translate-y-1 hover:border-sky-300/30 hover:shadow-[0_28px_74px_rgba(14,165,233,0.16)]',
isCompleted ? 'border-emerald-300/25' : 'border-white/10',
].join(' ')}
>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(125,211,252,0.09),transparent_24%),linear-gradient(135deg,transparent,rgba(255,255,255,0.02))] opacity-70 transition duration-200 group-hover:opacity-100" />
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(125,211,252,0.12),transparent_24%),radial-gradient(circle_at_bottom_left,rgba(251,191,36,0.08),transparent_28%),linear-gradient(135deg,transparent,rgba(255,255,255,0.02))] opacity-80 transition duration-200 group-hover:opacity-100" />
<div className="absolute inset-x-0 top-0 h-px bg-[linear-gradient(90deg,transparent,rgba(125,211,252,0.42),transparent)]" />
<div className="relative grid gap-0 lg:grid-cols-[172px_minmax(0,1fr)]">
<div className="relative border-b border-white/10 bg-slate-950 lg:border-b-0 lg:border-r">
<div className="relative grid gap-0 lg:grid-cols-[188px_minmax(0,1fr)]">
<div className="relative border-b border-white/10 bg-slate-950/90 lg:border-b-0 lg:border-r lg:border-white/10">
{thumbnail ? (
<img src={thumbnail} alt="" aria-hidden="true" className="h-40 w-full object-cover lg:h-full" />
<img src={thumbnail} alt="" aria-hidden="true" className="h-44 w-full object-cover lg:h-full" />
) : (
<div className="h-40 w-full bg-[linear-gradient(135deg,rgba(56,189,248,0.18),rgba(15,23,42,0.92))] lg:h-full" />
<div className="h-44 w-full bg-[linear-gradient(135deg,rgba(56,189,248,0.2),rgba(15,23,42,0.96))] lg:h-full" />
)}
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(2,6,23,0.04),rgba(2,6,23,0.84))]" />
<div className="absolute inset-x-3 top-3 flex items-start justify-between gap-3">
{lesson.is_required ? (
<span className="rounded-full border border-white/10 bg-black/35 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-white/80 backdrop-blur">
Required
</span>
) : (
<span className="rounded-full border border-white/10 bg-black/35 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-white/65 backdrop-blur">
Optional
</span>
)}
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(2,6,23,0.08),rgba(2,6,23,0.42)_42%,rgba(2,6,23,0.9))]" />
<div className="absolute inset-x-4 top-4 flex items-start justify-between gap-3">
<span className={`rounded-full border px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] backdrop-blur ${lesson.is_required ? 'border-white/10 bg-black/40 text-white/82' : 'border-white/10 bg-black/30 text-white/62'}`}>
{lesson.is_required ? 'Required' : 'Optional'}
</span>
{isCompleted ? (
<span className="inline-flex items-center gap-1.5 rounded-full border border-emerald-300/25 bg-emerald-300/12 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-emerald-100 backdrop-blur">
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true" className="h-3.5 w-3.5">
@@ -103,50 +103,55 @@ function LessonChip({ lesson }) {
</span>
) : null}
</div>
<div className="absolute inset-x-3 bottom-3 flex items-end justify-between gap-3">
<div>
<div className="absolute inset-x-4 bottom-4 flex items-end justify-between gap-3">
<div className="rounded-[24px] border border-white/10 bg-black/30 px-3 py-2 backdrop-blur-sm">
{stepLabel ? <p className="text-[10px] font-semibold uppercase tracking-[0.24em] text-sky-100/80">{stepLabel}</p> : null}
{stepNumber > 0 ? <p className="mt-1 text-5xl font-semibold tracking-[-0.1em] text-white">{String(stepNumber).padStart(2, '0')}</p> : null}
{!stepNumber && lesson.formatted_lesson_number ? <p className="mt-1 text-sm font-semibold uppercase tracking-[0.16em] text-white/80">{lesson.formatted_lesson_number}</p> : null}
</div>
</div>
</div>
<div className="p-5 md:p-6">
<div className="grid gap-5 xl:grid-cols-[minmax(0,1fr)_200px] xl:items-start">
<div className="grid gap-5 xl:grid-cols-[minmax(0,1fr)_240px] xl:items-start">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2.5">
{stepLabel ? <p className="text-[10px] font-semibold uppercase tracking-[0.22em] text-sky-100">{stepLabel}</p> : null}
{lesson.formatted_lesson_number ? <span className="rounded-full border border-white/10 bg-white/[0.04] px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-400">{lesson.formatted_lesson_number}</span> : null}
<span className="rounded-full border border-white/10 bg-white/[0.04] px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-400">{lesson.difficulty || 'lesson'}</span>
<span className="rounded-full border border-white/10 bg-white/[0.04] px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-400">{lesson.access_level || 'free'}</span>
<span className="rounded-full border border-white/10 bg-white/[0.04] px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-400">{difficultyLabel}</span>
<span className="rounded-full border border-white/10 bg-white/[0.04] px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-400">{accessLabel}</span>
{readingMinutes > 0 ? <span className="rounded-full border border-white/10 bg-white/[0.04] px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-400">{readingMinutes} min</span> : null}
</div>
<h3 className="mt-3 max-w-3xl text-[1.65rem] font-semibold tracking-[-0.05em] text-white transition group-hover:text-sky-100">{lesson.title}</h3>
<p className="mt-2 text-sm text-slate-400">{isCompleted ? 'You already finished this lesson.' : 'Follow this step next in the course path.'}</p>
<p className="mt-2 text-sm text-slate-400">{supportCopy}</p>
<div className="mt-4 rounded-[24px] border border-white/10 bg-black/20 px-4 py-4">
<div className="mt-4 rounded-[26px] border border-white/10 bg-[linear-gradient(180deg,rgba(2,6,23,0.36),rgba(2,6,23,0.18))] px-4 py-4 shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]">
<p className="text-sm leading-7 text-slate-300">{lesson.excerpt || lesson.content_preview || 'Open this lesson inside the course.'}</p>
</div>
<div className="mt-4 flex flex-wrap gap-2 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400">
<span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1.5">{lesson.lesson_type || 'article'}</span>
<div className="mt-5 flex flex-wrap items-center gap-2.5 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400">
<span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1.5">{lessonTypeLabel}</span>
{lesson.category_name ? <span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1.5">{lesson.category_name}</span> : null}
<span className="text-slate-500">Course flow</span>
</div>
</div>
<div className="flex h-full flex-col justify-between gap-4 xl:border-l xl:border-white/10 xl:pl-5">
<div className="grid gap-3 sm:grid-cols-3 xl:grid-cols-1">
<div className="rounded-[22px] border border-white/10 bg-black/20 px-4 py-3">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500">Status</p>
<p className={`mt-2 text-sm font-semibold ${isCompleted ? 'text-emerald-100' : 'text-white'}`}>{isCompleted ? 'Completed' : 'Up next'}</p>
</div>
<div className="rounded-[22px] border border-white/10 bg-black/20 px-4 py-3">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500">Access</p>
<p className="mt-2 text-sm font-semibold text-white">{lesson.access_level || 'Free'}</p>
</div>
<div className="rounded-[22px] border border-white/10 bg-black/20 px-4 py-3">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500">Read time</p>
<p className="mt-2 text-sm font-semibold text-white">{readingMinutes > 0 ? `${readingMinutes} min` : 'Quick read'}</p>
<div className="flex h-full flex-col justify-between gap-4 xl:border-l xl:border-white/10 xl:pl-6">
<div className="rounded-[28px] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02))] p-4 shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]">
<p className="text-[10px] font-semibold uppercase tracking-[0.22em] text-slate-500">Lesson path</p>
<div className="mt-4 space-y-3">
<div className="flex items-center justify-between gap-3 rounded-[18px] border border-white/10 bg-black/20 px-3 py-3">
<span className="text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500">Status</span>
<span className={`text-sm font-semibold ${isCompleted ? 'text-emerald-100' : 'text-white'}`}>{statusLabel}</span>
</div>
<div className="flex items-center justify-between gap-3 rounded-[18px] border border-white/10 bg-black/20 px-3 py-3">
<span className="text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500">Access</span>
<span className="text-sm font-semibold text-white">{accessLabel}</span>
</div>
<div className="flex items-center justify-between gap-3 rounded-[18px] border border-white/10 bg-black/20 px-3 py-3">
<span className="text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500">Read time</span>
<span className="text-sm font-semibold text-white">{readingMinutes > 0 ? `${readingMinutes} min` : 'Quick read'}</span>
</div>
</div>
</div>
@@ -170,26 +175,32 @@ function LessonChip({ lesson }) {
function SectionBlock({ section, isActive = false }) {
if (!section?.is_visible) return null
const lessonCount = section.lessons?.length || 0
const requiredCount = (section.lessons || []).filter((lesson) => lesson?.is_required).length
return (
<section className={`rounded-[32px] border p-6 transition md:p-7 ${isActive ? 'border-sky-300/25 bg-[linear-gradient(180deg,rgba(14,165,233,0.08),rgba(255,255,255,0.04))] shadow-[0_22px_50px_rgba(14,165,233,0.08)]' : 'border-white/10 bg-white/[0.04]'}`}>
<div className="flex flex-wrap items-start justify-between gap-4">
<div>
<div className="flex flex-wrap items-center gap-2">
<section className={`relative overflow-hidden rounded-[34px] border p-6 transition md:p-7 ${isActive ? 'border-sky-300/25 bg-[linear-gradient(180deg,rgba(14,165,233,0.08),rgba(255,255,255,0.04))] shadow-[0_24px_56px_rgba(14,165,233,0.08)]' : 'border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.04),rgba(255,255,255,0.02))]'}`}>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(125,211,252,0.08),transparent_28%),linear-gradient(135deg,transparent,rgba(255,255,255,0.015))] opacity-80" />
<div className="relative flex flex-wrap items-start justify-between gap-5">
<div className="max-w-4xl">
<div className="flex flex-wrap items-center gap-2.5">
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400">Course section</p>
<span className={`rounded-full border px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] ${isActive ? 'border-sky-300/20 bg-sky-300/12 text-sky-100' : 'border-white/10 bg-black/20 text-slate-300'}`}>
{section.order_num + 1}
</span>
{requiredCount > 0 ? <span className="rounded-full border border-white/10 bg-black/20 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-300">{requiredCount} required</span> : null}
</div>
<h2 className="mt-2 text-2xl font-semibold tracking-[-0.04em] text-white">{section.title}</h2>
<h2 className="mt-3 text-2xl font-semibold tracking-[-0.05em] text-white md:text-[2rem]">{section.title}</h2>
{section.description ? <p className="mt-3 max-w-3xl text-sm leading-7 text-slate-300">{section.description}</p> : null}
<p className="mt-4 text-xs uppercase tracking-[0.18em] text-slate-500">{lessonCount} lessons mapped in this section</p>
</div>
<div className="flex flex-wrap gap-2">
<span className="rounded-full border border-white/10 bg-black/20 px-4 py-2 text-xs font-semibold uppercase tracking-[0.18em] text-slate-300">{section.lessons?.length || 0} lessons</span>
<span className="rounded-full border border-white/10 bg-black/20 px-4 py-2 text-xs font-semibold uppercase tracking-[0.18em] text-slate-300">{lessonCount} lessons</span>
{isActive ? <span className="rounded-full border border-sky-300/20 bg-sky-300/12 px-4 py-2 text-xs font-semibold uppercase tracking-[0.18em] text-sky-100">Reading now</span> : null}
</div>
</div>
<div className="mt-5 space-y-6">
<div className="relative mt-6 space-y-6">
{(section.lessons || []).map((lesson) => (
<LessonChip key={lesson.course_lesson_id || lesson.id} lesson={lesson} />
))}
@@ -202,20 +213,24 @@ export default function AcademyCoursesShow({ seo, course, sections = [], unsecti
const flash = usePage().props.flash || {}
useAcademyPageAnalytics(analytics)
const cover = course?.cover_image_url || course?.cover_image || course?.teaser_image_url || course?.teaser_image || ''
const heroBackground = course?.teaser_image_url || course?.teaser_image || course?.cover_image_url || course?.cover_image || ''
const progress = course?.progress || null
const [liked, setLiked] = useState(Boolean(interaction?.liked))
const [saved, setSaved] = useState(Boolean(interaction?.saved))
const [likesCount, setLikesCount] = useState(Number(interaction?.likes_count || 0))
const [savesCount, setSavesCount] = useState(Number(interaction?.saves_count || 0))
const visibleSections = sections.filter((section) => section?.is_visible)
const totalLessons = Number(course?.lessons_count || (unsectionedLessons.length + visibleSections.reduce((sum, section) => sum + (section.lessons || []).length, 0)))
const totalSections = visibleSections.length + (unsectionedLessons.length ? 1 : 0)
const estimatedMinutes = course?.estimated_minutes ? `${course.estimated_minutes} min` : 'Flexible pace'
const sectionJumpItems = useMemo(
() => [
...(unsectionedLessons.length ? [{ id: 'course-outline-core', label: 'Core lessons', count: unsectionedLessons.length }] : []),
...sections
.filter((section) => section?.is_visible)
...visibleSections
.map((section) => ({ id: `section-${section.id}`, label: section.title, count: (section.lessons || []).length })),
],
[sections, unsectionedLessons],
[unsectionedLessons, visibleSections],
)
const [activeJumpId, setActiveJumpId] = useState(sectionJumpItems[0]?.id || null)
@@ -316,69 +331,95 @@ export default function AcademyCoursesShow({ seo, course, sections = [], unsecti
{flash.success ? <div className="rounded-2xl border border-emerald-300/20 bg-emerald-300/10 px-4 py-3 text-sm text-emerald-100">{flash.success}</div> : null}
{flash.error ? <div className="rounded-2xl border border-rose-300/20 bg-rose-300/10 px-4 py-3 text-sm text-rose-100">{flash.error}</div> : null}
<section className="overflow-hidden rounded-[40px] border border-white/10 bg-black/20 shadow-[0_24px_90px_rgba(2,6,23,0.34)]">
<div className="grid gap-0 xl:grid-cols-[minmax(0,1.2fr)_360px]">
<div className="relative overflow-hidden p-6 md:p-8 lg:p-10 xl:p-12">
{cover ? <img src={cover} alt="" aria-hidden="true" className="absolute inset-0 h-full w-full object-cover opacity-[0.18]" /> : null}
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,_rgba(125,211,252,0.18),_transparent_28%),radial-gradient(circle_at_78%_26%,_rgba(251,191,36,0.12),_transparent_20%),linear-gradient(135deg,_rgba(2,6,23,0.98),_rgba(15,23,42,0.85))]" />
<section className="relative overflow-hidden rounded-[40px] border border-sky-200/12 bg-[linear-gradient(145deg,rgba(14,165,233,0.16),rgba(15,23,42,0.95)_38%,rgba(251,191,36,0.14))] shadow-[0_24px_90px_rgba(2,6,23,0.34)]">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_18%_14%,rgba(125,211,252,0.14),transparent_24%),linear-gradient(90deg,rgba(255,255,255,0.04)_1px,transparent_1px),linear-gradient(180deg,rgba(255,255,255,0.04)_1px,transparent_1px)] bg-[length:auto,24px_24px,24px_24px] opacity-70" />
<div className="absolute -left-8 top-10 h-36 w-36 rounded-full bg-sky-300/18 blur-3xl" />
<div className="absolute -right-10 bottom-0 h-40 w-40 rounded-full bg-amber-300/14 blur-3xl" />
<div className="relative grid gap-6 p-5 md:p-6 xl:grid-cols-[minmax(0,1fr)_360px] xl:p-7">
<div className="min-w-0">
{heroBackground ? <img src={heroBackground} alt="" aria-hidden="true" className="absolute inset-0 h-full w-full object-cover opacity-[0.08]" /> : null}
<div className="relative z-10 max-w-5xl">
<CourseBreadcrumbs items={breadcrumbs} />
<div className="mt-5 flex flex-wrap items-center gap-2.5">
<span className="rounded-full border border-sky-300/20 bg-sky-300/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-100">Academy course</span>
<span className="rounded-full border border-sky-200/18 bg-sky-300/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-50/90">Skinbase AI Academy</span>
<span className="rounded-full border border-white/10 bg-white/[0.06] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-200">Course path</span>
<span className="rounded-full border border-white/10 bg-white/[0.06] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-300">{course?.difficulty}</span>
<span className="rounded-full border border-white/10 bg-white/[0.06] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-300">{course?.access_level}</span>
{progress?.percent ? <span className="rounded-full border border-emerald-300/20 bg-emerald-300/12 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.22em] text-emerald-100">{progress.percent}% complete</span> : null}
</div>
<div className="mt-6">
<h1 className="text-4xl font-semibold tracking-[-0.06em] text-white md:text-5xl lg:text-[3.75rem]">{course?.title}</h1>
{course?.subtitle ? <p className="mt-4 text-sm font-semibold uppercase tracking-[0.24em] text-amber-100/90">{course.subtitle}</p> : null}
<p className="mt-5 max-w-3xl text-base leading-8 text-slate-300 md:text-lg">{course?.excerpt || course?.description}</p>
<div className="mt-6 flex flex-wrap gap-3">
<button type="button" onClick={startCourse} className="rounded-full border border-sky-300/25 bg-sky-300/12 px-5 py-3 text-sm font-semibold text-sky-100">{progress?.percent ? 'Continue course' : 'Start course'}</button>
<button type="button" onClick={toggleLike} className="rounded-full border border-white/10 bg-white/[0.06] px-5 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.08]">{liked ? `Liked · ${likesCount}` : `Like · ${likesCount}`}</button>
<button type="button" onClick={toggleSave} className="rounded-full border border-white/10 bg-white/[0.06] px-5 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.08]">{saved ? `Saved · ${savesCount}` : `Save · ${savesCount}`}</button>
<Link href={pricingUrl} onClick={() => trackUpgradeClick(analytics, { source: 'academy_course_header' })} className="rounded-full border border-amber-300/25 bg-amber-300/12 px-5 py-3 text-sm font-semibold text-amber-100">See plans</Link>
<div className="mt-5 flex items-start justify-between gap-4">
<div className="max-w-4xl">
{course?.subtitle ? <p className="text-sm font-semibold uppercase tracking-[0.24em] text-amber-100/90">{course.subtitle}</p> : null}
<h1 className="mt-4 text-4xl font-semibold tracking-[-0.06em] text-white md:text-5xl lg:text-[3.9rem]">{course?.title}</h1>
<p className="mt-4 max-w-3xl text-base leading-8 text-slate-200/95 md:text-lg">{course?.excerpt || course?.description}</p>
</div>
<span className="hidden h-20 w-20 shrink-0 items-center justify-center rounded-[26px] border border-white/10 bg-white/[0.04] text-2xl text-sky-50 shadow-[0_18px_40px_rgba(2,6,23,0.22)] md:flex">
<i className="fa-solid fa-route" />
</span>
</div>
<div className="mt-7 overflow-hidden rounded-[32px] border border-white/10 bg-slate-950/80 shadow-[0_24px_60px_rgba(2,6,23,0.32)]">
{cover ? (
<img src={cover} alt="" aria-hidden="true" className="w-full object-contain" />
) : (
<div className="flex h-[360px] items-center justify-center bg-[linear-gradient(135deg,rgba(56,189,248,0.18),rgba(15,23,42,0.92))] px-6 text-center text-sm text-slate-400">
No course cover image yet
</div>
)}
<div className="mt-6 flex flex-wrap gap-3">
<button type="button" onClick={startCourse} className="rounded-full border border-sky-300/25 bg-sky-300/12 px-5 py-3 text-sm font-semibold text-sky-100">{progress?.percent ? 'Continue course' : 'Start course'}</button>
<button type="button" onClick={toggleLike} className="rounded-full border border-white/10 bg-white/[0.06] px-5 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.08]">{liked ? `Liked · ${likesCount}` : `Like · ${likesCount}`}</button>
<button type="button" onClick={toggleSave} className="rounded-full border border-white/10 bg-white/[0.06] px-5 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.08]">{saved ? `Saved · ${savesCount}` : `Save · ${savesCount}`}</button>
<Link href={pricingUrl} onClick={() => trackUpgradeClick(analytics, { source: 'academy_course_header' })} className="rounded-full border border-amber-300/25 bg-amber-300/12 px-5 py-3 text-sm font-semibold text-amber-100">See plans</Link>
</div>
<div className="mt-8 grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
<div className="rounded-[24px] border border-white/10 bg-slate-950/35 px-4 py-4 backdrop-blur-sm">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-sky-100/75">Library</p>
<p className="mt-2 text-lg font-semibold text-white">{totalLessons} lessons</p>
</div>
<div className="rounded-[24px] border border-white/10 bg-slate-950/35 px-4 py-4 backdrop-blur-sm">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-sky-100/75">Structure</p>
<p className="mt-2 text-lg font-semibold text-white">{totalSections} sections</p>
</div>
<div className="rounded-[24px] border border-white/10 bg-slate-950/35 px-4 py-4 backdrop-blur-sm">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-sky-100/75">Pace</p>
<p className="mt-2 text-lg font-semibold text-white">{estimatedMinutes}</p>
</div>
<div className="rounded-[24px] border border-white/10 bg-slate-950/35 px-4 py-4 backdrop-blur-sm">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-sky-100/75">Status</p>
<p className="mt-2 text-lg font-semibold text-white">{progress?.percent ? `${progress.percent}% complete` : 'Ready to start'}</p>
</div>
</div>
</div>
</div>
<aside className="border-t border-white/10 bg-white/[0.03] p-6 xl:border-l xl:border-t-0 xl:p-8">
<div className="space-y-4 xl:sticky xl:top-6">
<ProgressMeter progress={progress} />
<div className="rounded-[28px] border border-white/10 bg-black/20 p-5">
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400">Jump through the course</p>
<div className="mt-4 space-y-2">
{sectionJumpItems.length ? (
sectionJumpItems.map((item) => (
<a
key={item.id}
href={`#${item.id}`}
onClick={() => setActiveJumpId(item.id)}
className={`flex items-center justify-between rounded-2xl border px-4 py-3 text-sm transition ${activeJumpId === item.id ? 'border-sky-300/25 bg-sky-300/12 text-white' : 'border-white/10 bg-white/[0.03] text-slate-300 hover:border-white/20 hover:bg-white/[0.06]'}`}
>
<span className="font-medium">{item.label}</span>
<span className="rounded-full border border-white/10 bg-black/20 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.14em] text-slate-400">{item.count}</span>
</a>
))
) : (
<p className="rounded-2xl border border-dashed border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-400">No course outline items are available yet.</p>
)}
<aside className="grid gap-4 self-start xl:pt-2">
<div className="overflow-hidden rounded-[30px] border border-white/10 bg-black/20 shadow-[0_18px_42px_rgba(2,6,23,0.18)] backdrop-blur-sm">
{cover ? (
<img src={cover} alt={course?.title} className="h-56 w-full object-cover" />
) : (
<div className="flex h-56 items-center justify-center bg-[linear-gradient(135deg,rgba(56,189,248,0.18),rgba(15,23,42,0.92))] px-6 text-center text-sm text-slate-400">
No course cover image yet
</div>
)}
</div>
<ProgressMeter progress={progress} />
<div className="rounded-[30px] border border-white/10 bg-black/20 p-5 shadow-[0_18px_42px_rgba(2,6,23,0.18)] backdrop-blur-sm">
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400">Jump through the course</p>
<div className="mt-4 space-y-2">
{sectionJumpItems.length ? (
sectionJumpItems.map((item) => (
<a
key={item.id}
href={`#${item.id}`}
onClick={() => setActiveJumpId(item.id)}
className={`flex items-center justify-between rounded-2xl border px-4 py-3 text-sm transition ${activeJumpId === item.id ? 'border-sky-300/25 bg-sky-300/12 text-white' : 'border-white/10 bg-white/[0.03] text-slate-300 hover:border-white/20 hover:bg-white/[0.06]'}`}
>
<span className="font-medium">{item.label}</span>
<span className="rounded-full border border-white/10 bg-black/20 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.14em] text-slate-400">{item.count}</span>
</a>
))
) : (
<p className="rounded-2xl border border-dashed border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-400">No course outline items are available yet.</p>
)}
</div>
</div>
</aside>