import React, { useEffect, useMemo, useState } from 'react'
import { Link, router, usePage } from '@inertiajs/react'
import SeoHead from '../../components/seo/SeoHead'
import { postAcademyAction, trackUpgradeClick, useAcademyPageAnalytics } from '../../lib/academyAnalytics'
function CourseBreadcrumbs({ items = [] }) {
if (!items.length) return null
return (
{items.map((item, index) => {
const isLast = index === items.length - 1
return (
{index > 0 ? / : null}
{isLast ? (
{item.label}
) : (
{item.label}
)}
)
})}
)
}
function ProgressMeter({ progress }) {
const percent = Math.max(0, Math.min(100, Number(progress?.percent || 0)))
return (
{progress ? 'In progress' : 'Not started'}
{progress
? `${progress.completedRequired}/${progress.totalRequired} required lessons completed`
: 'Start the course to begin tracking progress through required lessons.'}
)
}
function LessonChip({ lesson }) {
const thumbnail = lesson?.cover_image_url || lesson?.article_cover_image_url || lesson?.cover_image || lesson?.article_cover_image || ''
const stepLabel = lesson?.course_step_label || null
const stepNumber = Number(lesson?.course_step_number || 0)
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 (
{thumbnail ? (
) : (
)}
{lesson.is_required ? 'Required' : 'Optional'}
{isCompleted ? (
Done
) : null}
{stepLabel ?
{stepLabel}
: null}
{stepNumber > 0 ?
{String(stepNumber).padStart(2, '0')}
: null}
{!stepNumber && lesson.formatted_lesson_number ?
{lesson.formatted_lesson_number}
: null}
{stepLabel ?
{stepLabel}
: null}
{lesson.formatted_lesson_number ?
{lesson.formatted_lesson_number} : null}
{difficultyLabel}
{accessLabel}
{readingMinutes > 0 ?
{readingMinutes} min : null}
{lesson.title}
{supportCopy}
{lesson.excerpt || lesson.content_preview || 'Open this lesson inside the course.'}
{lessonTypeLabel}
{lesson.category_name ? {lesson.category_name} : null}
Course flow
Lesson path
Status
{statusLabel}
Access
{accessLabel}
Read time
{readingMinutes > 0 ? `${readingMinutes} min` : 'Quick read'}
)
}
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 (
Course section
{section.order_num + 1}
{requiredCount > 0 ?
{requiredCount} required : null}
{section.title}
{section.description ?
{section.description}
: null}
{lessonCount} lessons mapped in this section
{lessonCount} lessons
{isActive ? Reading now : null}
{(section.lessons || []).map((lesson) => (
))}
)
}
export default function AcademyCoursesShow({ seo, course, sections = [], unsectionedLessons = [], pricingUrl, startUrl = null, interaction = null, interactionRoutes = null, loginUrl = null, analytics = null }) {
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 }] : []),
...visibleSections
.map((section) => ({ id: `section-${section.id}`, label: section.title, count: (section.lessons || []).length })),
],
[unsectionedLessons, visibleSections],
)
const [activeJumpId, setActiveJumpId] = useState(sectionJumpItems[0]?.id || null)
const breadcrumbs = [
{ label: 'Academy', href: '/academy' },
{ label: 'Courses', href: '/academy/courses' },
{ label: course?.title || 'Course', href: course?.public_url || '#' },
]
useEffect(() => {
if (!sectionJumpItems.length || typeof window === 'undefined' || typeof IntersectionObserver === 'undefined') {
return undefined
}
const observer = new IntersectionObserver(
(entries) => {
const visibleEntries = entries.filter((entry) => entry.isIntersecting).sort((left, right) => right.intersectionRatio - left.intersectionRatio)
if (!visibleEntries.length) return
setActiveJumpId(visibleEntries[0].target.id)
},
{
rootMargin: '-20% 0px -55% 0px',
threshold: [0.2, 0.45, 0.7],
},
)
const elements = sectionJumpItems.map((item) => document.getElementById(item.id)).filter(Boolean)
elements.forEach((element) => observer.observe(element))
return () => observer.disconnect()
}, [sectionJumpItems])
const requireLogin = () => {
if (loginUrl && typeof window !== 'undefined') {
window.location.href = loginUrl
}
}
const startCourse = () => {
if (!startUrl) {
requireLogin()
return
}
router.post(startUrl)
}
const toggleLike = async () => {
if (!interactionRoutes?.like || !analytics?.contentType || !analytics?.contentId) {
return
}
if (analytics?.isGuest) {
requireLogin()
return
}
const payload = await postAcademyAction(interactionRoutes.like, {
content_type: analytics.contentType,
content_id: analytics.contentId,
})
if (payload?.liked !== undefined) {
setLiked(Boolean(payload.liked))
setLikesCount(Number(payload.likes_count || 0))
}
}
const toggleSave = async () => {
if (!interactionRoutes?.save || !analytics?.contentType || !analytics?.contentId) {
return
}
if (analytics?.isGuest) {
requireLogin()
return
}
const payload = await postAcademyAction(interactionRoutes.save, {
content_type: analytics.contentType,
content_id: analytics.contentId,
})
if (payload?.saved !== undefined) {
setSaved(Boolean(payload.saved))
setSavesCount(Number(payload.saves_count || 0))
}
}
return (
{flash.success ?
{flash.success}
: null}
{flash.error ?
{flash.error}
: null}
{heroBackground ?
: null}
Skinbase AI Academy
Course path
{course?.difficulty}
{course?.access_level}
{progress?.percent ? {progress.percent}% complete : null}
{course?.subtitle ?
{course.subtitle}
: null}
{course?.title}
{course?.excerpt || course?.description}
{progress?.percent ? 'Continue course' : 'Start course'}
{liked ? `Liked · ${likesCount}` : `Like · ${likesCount}`}
{saved ? `Saved · ${savesCount}` : `Save · ${savesCount}`}
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
Library
{totalLessons} lessons
Structure
{totalSections} sections
Status
{progress?.percent ? `${progress.percent}% complete` : 'Ready to start'}
{unsectionedLessons.length ? (
) : null}
{sections.filter((section) => section?.is_visible).map((section) => (
))}
)
}