chore: commit remaining workspace changes

This commit is contained in:
2026-05-08 21:51:29 +02:00
parent 8d108b8a76
commit ff96ef796e
97 changed files with 18020 additions and 2196 deletions

View File

@@ -0,0 +1,125 @@
import React from 'react'
import { Link, router, usePage } from '@inertiajs/react'
import SeoHead from '../../components/seo/SeoHead'
import NovaSelect from '../../components/ui/NovaSelect'
function CourseCard({ course, variant = 'default' }) {
const isFeatured = variant === 'featured'
const progress = course?.progress || null
const cover = course?.cover_image_url || course?.teaser_image_url || course?.cover_image || course?.teaser_image || ''
return (
<Link
href={course.public_url}
className={[
'group overflow-hidden rounded-[30px] border border-white/10 transition hover:border-sky-300/25 hover:bg-white/[0.06]',
isFeatured ? 'bg-[linear-gradient(135deg,rgba(14,165,233,0.14),rgba(15,23,42,0.92))]' : 'bg-white/[0.04]',
].join(' ')}
>
<div className="relative">
{cover ? <img src={cover} alt="" aria-hidden="true" className={`w-full object-cover ${isFeatured ? 'h-56' : 'h-44'}`} /> : <div className={`w-full bg-[linear-gradient(135deg,rgba(14,165,233,0.22),rgba(15,23,42,0.92))] ${isFeatured ? 'h-56' : 'h-44'}`} />}
<div className="absolute inset-0 bg-[linear-gradient(180deg,transparent,rgba(2,6,23,0.82))]" />
<div className="absolute left-5 top-5 flex flex-wrap gap-2">
<span className="rounded-full border border-white/10 bg-black/30 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-100">{course.difficulty}</span>
<span className="rounded-full border border-white/10 bg-black/30 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-200">{course.access_level}</span>
{course.is_featured ? <span className="rounded-full border border-amber-300/20 bg-amber-300/15 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-100">Featured</span> : null}
</div>
</div>
<div className="p-6">
<h2 className={`font-semibold tracking-[-0.05em] text-white transition group-hover:text-sky-100 ${isFeatured ? 'text-3xl' : 'text-2xl'}`}>{course.title}</h2>
{course.subtitle ? <p className="mt-2 text-sm font-medium uppercase tracking-[0.18em] text-slate-400">{course.subtitle}</p> : null}
<p className="mt-4 text-sm leading-7 text-slate-300">{course.excerpt || course.description || 'Structured Academy course.'}</p>
<div className="mt-5 grid gap-3 sm:grid-cols-3">
<div className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3">
<p className="text-[10px] font-semibold uppercase tracking-[0.22em] text-slate-500">Lessons</p>
<p className="mt-2 text-sm font-semibold text-white">{course.lessons_count || 0}</p>
</div>
<div className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3">
<p className="text-[10px] font-semibold uppercase tracking-[0.22em] text-slate-500">Duration</p>
<p className="mt-2 text-sm font-semibold text-white">{course.estimated_minutes ? `${course.estimated_minutes} min` : 'Flexible'}</p>
</div>
<div className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3">
<p className="text-[10px] font-semibold uppercase tracking-[0.22em] text-slate-500">Progress</p>
<p className="mt-2 text-sm font-semibold text-white">{progress ? `${progress.percent}%` : 'Start fresh'}</p>
</div>
</div>
</div>
</Link>
)
}
export default function AcademyCoursesIndex({ seo, title, description, items, featuredCourses = [], filters = {}, pricingUrl }) {
const flash = usePage().props.flash || {}
const difficultyOptions = [
{ value: '', label: 'All levels' },
{ value: 'beginner', label: 'Beginner' },
{ value: 'intermediate', label: 'Intermediate' },
{ value: 'advanced', label: 'Advanced' },
]
const accessOptions = [
{ value: '', label: 'All access' },
{ value: 'free', label: 'Free' },
{ value: 'premium', label: 'Premium' },
{ value: 'mixed', label: 'Mixed' },
]
return (
<main className="min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.16),_transparent_24%),radial-gradient(circle_at_bottom_right,_rgba(251,191,36,0.16),_transparent_24%),linear-gradient(180deg,_#0f172a_0%,_#111827_100%)] px-4 py-8 sm:px-6 lg:px-8">
<SeoHead seo={seo || {}} title={title} description={description} />
<div className="mx-auto max-w-[1400px] space-y-6">
<section className="rounded-[40px] border border-white/10 bg-[linear-gradient(135deg,rgba(15,23,42,0.96),rgba(14,165,233,0.12))] p-8 shadow-[0_24px_90px_rgba(2,6,23,0.36)] md:p-10 lg:p-12">
<div className="flex flex-wrap items-end justify-between gap-6">
<div className="max-w-4xl">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-200/80">Skinbase AI Academy</p>
<h1 className="mt-4 text-4xl font-semibold tracking-[-0.055em] text-white md:text-5xl lg:text-6xl">{title}</h1>
<p className="mt-5 text-base leading-8 text-slate-300 md:text-lg">{description}</p>
</div>
<Link href={pricingUrl} className="rounded-full border border-amber-300/25 bg-amber-300/12 px-5 py-3 text-sm font-semibold text-amber-100">See Academy plans</Link>
</div>
</section>
{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}
{featuredCourses.length ? (
<section className="grid gap-5 xl:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]">
<CourseCard course={featuredCourses[0]} variant="featured" />
<div className="grid gap-5">
{featuredCourses.slice(1, 3).map((course) => <CourseCard key={course.id} course={course} />)}
</div>
</section>
) : null}
<section className="grid gap-3 rounded-[30px] border border-white/10 bg-black/20 p-5 md:grid-cols-2">
<NovaSelect
label="Difficulty"
value={filters?.difficulty || ''}
onChange={(nextValue) => router.get(window.location.pathname, { ...filters, difficulty: nextValue || undefined }, { preserveScroll: true, preserveState: true })}
options={difficultyOptions}
searchable={false}
className="rounded-2xl bg-white/[0.04]"
/>
<NovaSelect
label="Access"
value={filters?.access || ''}
onChange={(nextValue) => router.get(window.location.pathname, { ...filters, access: nextValue || undefined }, { preserveScroll: true, preserveState: true })}
options={accessOptions}
searchable={false}
className="rounded-2xl bg-white/[0.04]"
/>
</section>
{(items?.data || []).length === 0 ? (
<section className="rounded-[30px] border border-white/10 bg-white/[0.04] px-6 py-12 text-center text-slate-400">No published Academy courses matched these filters.</section>
) : (
<section className="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
{items.data.map((course) => <CourseCard key={course.id} course={course} />)}
</section>
)}
</div>
</main>
)
}

View File

@@ -0,0 +1,339 @@
import React, { useEffect, useMemo, useState } from 'react'
import { Link, usePage } from '@inertiajs/react'
import SeoHead from '../../components/seo/SeoHead'
function CourseBreadcrumbs({ items = [] }) {
if (!items.length) return null
return (
<nav aria-label="Breadcrumb" className="flex flex-wrap items-center gap-2 text-sm text-slate-400">
{items.map((item, index) => {
const isLast = index === items.length - 1
return (
<React.Fragment key={`${item.label}-${index}`}>
{index > 0 ? <span className="text-slate-600">/</span> : null}
{isLast ? (
<span className="font-medium text-slate-200">{item.label}</span>
) : (
<Link href={item.href} className="transition hover:text-white">
{item.label}
</Link>
)}
</React.Fragment>
)
})}
</nav>
)
}
function ProgressMeter({ progress }) {
const percent = Math.max(0, Math.min(100, Number(progress?.percent || 0)))
return (
<div className="rounded-[28px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.92),rgba(15,23,42,0.72))] p-5">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400">Progress</p>
<p className="mt-2 text-3xl font-semibold tracking-[-0.04em] text-white">{percent}%</p>
</div>
<span className="rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-slate-300">
{progress ? 'In progress' : 'Not started'}
</span>
</div>
<div className="mt-4 h-2.5 overflow-hidden rounded-full bg-white/[0.06]">
<div
className="h-full rounded-full bg-[linear-gradient(90deg,rgba(125,211,252,0.95),rgba(251,191,36,0.9))] transition-[width] duration-500"
style={{ width: `${percent}%` }}
/>
</div>
<p className="mt-3 text-sm leading-7 text-slate-300">
{progress
? `${progress.completedRequired}/${progress.totalRequired} required lessons completed`
: 'Start the course to begin tracking progress through required lessons.'}
</p>
</div>
)
}
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'
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)]',
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="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">
{thumbnail ? (
<img src={thumbnail} alt="" aria-hidden="true" className="h-40 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="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>
)}
{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">
<circle cx="8" cy="8" r="7" stroke="currentColor" strokeWidth="1.2" />
<path d="M4.75 8.2 7 10.4l4.25-4.8" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
</svg>
Done
</span>
) : null}
</div>
<div className="absolute inset-x-3 bottom-3 flex items-end justify-between gap-3">
<div>
{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}
</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="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>
{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>
<div className="mt-4 rounded-[24px] border border-white/10 bg-black/20 px-4 py-4">
<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>
{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}
</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>
</div>
<div className="flex items-center justify-between gap-3 xl:justify-end">
<span className="text-xs uppercase tracking-[0.16em] text-slate-500">Continue path</span>
<span className="inline-flex items-center gap-2 rounded-full border border-sky-300/20 bg-sky-300/10 px-4 py-2 text-sm font-semibold text-sky-100 transition group-hover:border-sky-300/35 group-hover:bg-sky-300/14 group-hover:text-white">
{ctaLabel}
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true" className="h-4 w-4">
<path d="M3.5 8h9m0 0-3.5-3.5M12.5 8l-3.5 3.5" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</span>
</div>
</div>
</div>
</div>
</div>
</Link>
)
}
function SectionBlock({ section, isActive = false }) {
if (!section?.is_visible) return null
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">
<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>
</div>
<h2 className="mt-2 text-2xl font-semibold tracking-[-0.04em] text-white">{section.title}</h2>
{section.description ? <p className="mt-3 max-w-3xl text-sm leading-7 text-slate-300">{section.description}</p> : null}
</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>
{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">
{(section.lessons || []).map((lesson) => (
<LessonChip key={lesson.course_lesson_id || lesson.id} lesson={lesson} />
))}
</div>
</section>
)
}
export default function AcademyCoursesShow({ seo, course, sections = [], unsectionedLessons = [], pricingUrl }) {
const flash = usePage().props.flash || {}
const cover = course?.cover_image_url || course?.cover_image || course?.teaser_image_url || course?.teaser_image || ''
const progress = course?.progress || null
const sectionJumpItems = useMemo(
() => [
...(unsectionedLessons.length ? [{ id: 'course-outline-core', label: 'Core lessons', count: unsectionedLessons.length }] : []),
...sections
.filter((section) => section?.is_visible)
.map((section) => ({ id: `section-${section.id}`, label: section.title, count: (section.lessons || []).length })),
],
[sections, unsectionedLessons],
)
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])
return (
<main className="min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.16),_transparent_24%),radial-gradient(circle_at_bottom_right,_rgba(251,191,36,0.16),_transparent_24%),linear-gradient(180deg,_#0f172a_0%,_#111827_100%)] px-4 py-8 sm:px-6 lg:px-8">
<SeoHead seo={seo || {}} title={course?.title} description={course?.excerpt || course?.description} />
<div className="mx-auto max-w-[1400px] space-y-6">
{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))]" />
<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-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-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>
</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>
)}
</div>
</div>
</div>
</aside>
</div>
</section>
<div className="space-y-6">
{unsectionedLessons.length ? (
<SectionBlock
section={{
order_num: -1,
title: 'Core lessons',
description: 'Lessons shown before the course branches into sections.',
is_visible: true,
lessons: unsectionedLessons,
}}
isActive={activeJumpId === 'course-outline-core'}
/>
) : null}
{sections.filter((section) => section?.is_visible).map((section) => (
<SectionBlock key={section.id} section={section} isActive={activeJumpId === `section-${section.id}`} />
))}
</div>
</div>
</main>
)
}

View File

@@ -17,7 +17,29 @@ function FeatureCard({ title, description, href, cta }) {
)
}
export default function AcademyIndex({ seo, pricingUrl, links, featureFlags, stats, featuredLessons, featuredPrompts, featuredChallenges }) {
function FeaturedCourseCard({ course }) {
const cover = course?.cover_image_url || course?.teaser_image_url || course?.cover_image || course?.teaser_image || ''
return (
<Link href={course.public_url} className="group overflow-hidden rounded-[28px] border border-white/10 bg-white/[0.04] transition hover:border-sky-300/25 hover:bg-white/[0.06]">
<div className="relative h-44 overflow-hidden bg-[linear-gradient(135deg,rgba(14,165,233,0.24),rgba(15,23,42,0.92))]">
{cover ? <img src={cover} alt="" aria-hidden="true" className="h-full w-full object-cover" /> : null}
<div className="absolute inset-0 bg-[linear-gradient(180deg,transparent,rgba(2,6,23,0.82))]" />
<div className="absolute left-4 top-4 flex flex-wrap gap-2">
<span className="rounded-full border border-white/10 bg-black/30 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-sky-100">{course.difficulty}</span>
<span className="rounded-full border border-white/10 bg-black/30 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-200">{course.access_level}</span>
</div>
</div>
<div className="p-5">
<h3 className="text-2xl font-semibold tracking-[-0.04em] text-white transition group-hover:text-sky-100">{course.title}</h3>
<p className="mt-3 text-sm leading-7 text-slate-300">{course.excerpt || course.description || 'Guided Academy course.'}</p>
<p className="mt-4 text-xs uppercase tracking-[0.18em] text-slate-500">{course.lessons_count || 0} lessons · {course.estimated_minutes ? `${course.estimated_minutes} min` : 'Flexible duration'}</p>
</div>
</Link>
)
}
export default function AcademyIndex({ seo, pricingUrl, links, featureFlags, stats, featuredCourses, featuredLessons, featuredPrompts, featuredChallenges }) {
const jsonLd = [{
'@context': 'https://schema.org',
'@type': 'WebPage',
@@ -39,6 +61,7 @@ export default function AcademyIndex({ seo, pricingUrl, links, featureFlags, sta
<p className="mt-5 max-w-3xl text-base leading-8 text-slate-300 md:text-lg">Skinbase AI Academy is the creative learning hub for AI-assisted art on Skinbase. Start with free lessons, explore prompt templates, and unlock premium workflows later.</p>
<div className="mt-7 flex flex-wrap gap-3">
<Link href={links.courses} className="rounded-full border border-sky-300/25 bg-sky-300/12 px-5 py-3 text-sm font-semibold text-sky-100 transition hover:border-sky-300/40 hover:bg-sky-300/18">Browse courses</Link>
<Link href={links.lessons} className="rounded-full border border-amber-300/25 bg-amber-300/12 px-5 py-3 text-sm font-semibold text-amber-100 transition hover:border-amber-300/40 hover:bg-amber-300/18">Browse lessons</Link>
<Link href={links.prompts} className="rounded-full border border-white/10 bg-white/[0.04] px-5 py-3 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.07]">Open prompt library</Link>
<Link href={pricingUrl} className="rounded-full border border-sky-300/25 bg-sky-300/12 px-5 py-3 text-sm font-semibold text-sky-100 transition hover:border-sky-300/40 hover:bg-sky-300/18">See plans</Link>
@@ -57,19 +80,35 @@ export default function AcademyIndex({ seo, pricingUrl, links, featureFlags, sta
</section>
<section className="grid gap-5 lg:grid-cols-3">
<FeatureCard title="Courses" description="Follow guided learning paths that stitch reusable Academy lessons into a clean progression with completion tracking." href={links.courses} cta="Browse courses" />
<FeatureCard title="Lessons" description="Structured tutorials for prompt writing, cleanup workflows, AI ethics, and Skinbase-native publishing habits." href={links.lessons} cta="Open lessons" />
<FeatureCard title="Prompt Library" description="Discover reusable prompt templates, locked premium previews, and creator-focused visual workflows." href={links.prompts} cta="Explore prompts" />
<FeatureCard title="Challenges" description="Join Academy creative briefs and submit artworks once the challenge system is enabled for your account." href={links.challenges} cta="View challenges" />
</section>
<section className="grid gap-5 lg:grid-cols-3">
<section className="grid gap-5 lg:grid-cols-4">
<div className="rounded-[28px] border border-white/10 bg-white/[0.04] p-6"><p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400">Courses</p><p className="mt-3 text-4xl font-semibold tracking-[-0.05em] text-white">{stats?.courseCount || 0}</p></div>
<div className="rounded-[28px] border border-white/10 bg-white/[0.04] p-6"><p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400">Lessons</p><p className="mt-3 text-4xl font-semibold tracking-[-0.05em] text-white">{stats?.lessonCount || 0}</p></div>
<div className="rounded-[28px] border border-white/10 bg-white/[0.04] p-6"><p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400">Prompts</p><p className="mt-3 text-4xl font-semibold tracking-[-0.05em] text-white">{stats?.promptCount || 0}</p></div>
<div className="rounded-[28px] border border-white/10 bg-white/[0.04] p-6"><p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400">Challenges</p><p className="mt-3 text-4xl font-semibold tracking-[-0.05em] text-white">{stats?.challengeCount || 0}</p></div>
</section>
{featuredCourses?.length ? (
<section className="space-y-5">
<div className="flex flex-wrap items-end justify-between gap-4">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400">Featured courses</p>
<h2 className="mt-2 text-3xl font-semibold tracking-[-0.045em] text-white">Guided Academy paths</h2>
</div>
<Link href={links.courses} className="rounded-full border border-white/10 bg-white/[0.04] px-5 py-3 text-sm font-semibold text-white">All courses</Link>
</div>
<div className="grid gap-5 xl:grid-cols-3">
{featuredCourses.slice(0, 3).map((course) => <FeaturedCourseCard key={course.id} course={course} />)}
</div>
</section>
) : null}
<section className="grid gap-5 xl:grid-cols-3">
<div className="rounded-[28px] border border-white/10 bg-black/20 p-6"><p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400">Featured lessons</p><div className="mt-4 space-y-3">{(featuredLessons || []).slice(0, 3).map((item) => <Link key={item.id} href={academyHref('lessons', item.slug)} className="block rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white">{item.title}</Link>)}</div></div>
<div className="rounded-[28px] border border-white/10 bg-black/20 p-6"><p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400">Featured lessons</p><div className="mt-4 space-y-3">{(featuredLessons || []).slice(0, 3).map((item) => <Link key={item.id} href={academyHref('lessons', item.slug)} className="block rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white"><span className="block text-[10px] font-semibold uppercase tracking-[0.18em] text-amber-100">{item.lesson_label || 'Featured lesson'}</span><span className="mt-1 block">{item.title}</span></Link>)}</div></div>
<div className="rounded-[28px] border border-white/10 bg-black/20 p-6"><p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400">Featured prompts</p><div className="mt-4 space-y-3">{(featuredPrompts || []).slice(0, 3).map((item) => <Link key={item.id} href={academyHref('prompts', item.slug)} className="block rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white">{item.title}</Link>)}</div></div>
<div className="rounded-[28px] border border-white/10 bg-black/20 p-6"><p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400">Current challenges</p><div className="mt-4 space-y-3">{(featuredChallenges || []).slice(0, 3).map((item) => <Link key={item.id} href={academyHref('challenges', item.slug)} className="block rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white">{item.title}</Link>)}</div></div>
</section>

View File

@@ -65,15 +65,125 @@ function itemHref(pageType, item) {
return academyHref('challenges', item.slug)
}
function PromptLibraryHero({ title, description, items, pricingUrl }) {
const featuredImages = (items || [])
.map((item) => item?.preview_image)
.filter(Boolean)
.slice(0, 3)
const primaryImage = featuredImages[0] || ''
const supportingImages = featuredImages.slice(1, 3)
return (
<section className="overflow-hidden rounded-[38px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(125,211,252,0.14),transparent_26%),radial-gradient(circle_at_bottom_right,rgba(255,207,191,0.16),transparent_26%),linear-gradient(135deg,rgba(4,9,18,0.98),rgba(15,23,42,0.92))] p-8 shadow-[0_28px_90px_rgba(2,6,23,0.28)] md:p-10 lg:p-12">
<div className="grid gap-8 xl:grid-cols-[minmax(0,1.15fr)_420px] xl:items-end">
<div>
<div className="flex flex-wrap gap-2">
<span className="rounded-full border border-[#ffcfbf]/20 bg-[#ffcfbf]/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-[#fff0ea]">Skinbase AI Academy</span>
<span className="rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-300">Prompt Library</span>
</div>
<h1 className="mt-5 max-w-4xl text-4xl font-semibold tracking-[-0.055em] text-white md:text-5xl xl:text-6xl">{title}</h1>
<p className="mt-5 max-w-3xl text-base leading-8 text-slate-300 md:text-lg">{description}</p>
<div className="mt-7 grid gap-3 sm:grid-cols-3">
<div className="rounded-[24px] border border-white/10 bg-black/20 px-5 py-4">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500">Visual-first</p>
<p className="mt-2 text-sm font-semibold text-white">Preview prompt results before opening the detail page.</p>
</div>
<div className="rounded-[24px] border border-white/10 bg-black/20 px-5 py-4">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500">Reusable</p>
<p className="mt-2 text-sm font-semibold text-white">Templates for wallpapers, covers, worlds, portraits, and more.</p>
</div>
<div className="rounded-[24px] border border-white/10 bg-black/20 px-5 py-4">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500">Comparison-ready</p>
<p className="mt-2 text-sm font-semibold text-white">See which prompts include provider-specific notes and outputs.</p>
</div>
</div>
<div className="mt-7 flex flex-wrap gap-3">
<Link href={pricingUrl} className="rounded-full border border-sky-300/25 bg-sky-300/12 px-5 py-3 text-sm font-semibold text-sky-100">Upgrade preview</Link>
<span className="rounded-full border border-white/10 bg-white/[0.04] px-5 py-3 text-sm font-semibold text-white/85">{items?.length || 0} prompts in view</span>
</div>
</div>
<div className="grid gap-3">
{primaryImage ? (
<>
<div className="overflow-hidden rounded-[28px] border border-white/10 bg-black/20 shadow-[0_18px_45px_rgba(2,6,23,0.18)] aspect-[16/10]">
<img src={primaryImage} alt="" aria-hidden="true" className="h-full w-full object-cover" />
</div>
{supportingImages.length ? (
<div className={`grid gap-3 ${supportingImages.length === 1 ? 'grid-cols-1' : 'grid-cols-2'}`}>
{supportingImages.map((image, index) => (
<div key={`${image}-${index}`} className="overflow-hidden rounded-[28px] border border-white/10 bg-black/20 shadow-[0_18px_45px_rgba(2,6,23,0.18)] aspect-square">
<img src={image} alt="" aria-hidden="true" className="h-full w-full object-cover" />
</div>
))}
</div>
) : null}
</>
) : (
<div className="col-span-2 flex aspect-[16/10] items-center justify-center rounded-[28px] border border-white/10 bg-[linear-gradient(135deg,rgba(56,189,248,0.12),rgba(17,24,39,0.92))] px-8 text-center text-sm font-semibold uppercase tracking-[0.24em] text-slate-300">
Prompt preview images will appear here
</div>
)}
</div>
</div>
</section>
)
}
function AcademyCard({ pageType, item }) {
const lessonSeries = String(item?.series_name || '').trim()
const promptPreviewImage = item?.preview_image || ''
if (pageType === 'prompts') {
return (
<Link href={itemHref(pageType, item)} className="group overflow-hidden rounded-[30px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.92),rgba(7,11,18,0.96))] shadow-[0_20px_50px_rgba(2,6,23,0.18)] transition hover:border-sky-300/25 hover:bg-[linear-gradient(180deg,rgba(15,23,42,0.96),rgba(10,15,26,0.98))]">
<div className="relative aspect-[16/11] overflow-hidden bg-[linear-gradient(135deg,rgba(56,189,248,0.18),rgba(17,24,39,0.94))]">
{promptPreviewImage ? <img src={promptPreviewImage} alt="" aria-hidden="true" className="h-full w-full object-cover transition duration-500 group-hover:scale-[1.04]" /> : null}
<div className="absolute inset-0 bg-[linear-gradient(180deg,transparent,rgba(2,6,23,0.72))]" />
<div className="absolute left-4 top-4 flex flex-wrap gap-2">
<span className="rounded-full border border-white/10 bg-black/30 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-[#fff0ea]">Prompt template</span>
<LockBadge item={item} />
</div>
<div className="absolute bottom-4 left-4 right-4 flex flex-wrap items-end justify-between gap-3">
<div className="flex flex-wrap gap-2">
{item?.difficulty ? <span className="rounded-full border border-white/10 bg-black/30 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-white">{item.difficulty}</span> : null}
{item?.aspect_ratio ? <span className="rounded-full border border-white/10 bg-black/30 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-white">{item.aspect_ratio}</span> : null}
</div>
</div>
</div>
<div className="p-5">
<div className="flex items-center justify-between gap-3">
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80">{item?.category?.name || 'Academy'}</p>
{Array.isArray(item?.tool_notes) && item.tool_notes.length ? <span className="rounded-full border border-[#ffcfbf]/15 bg-[#ffcfbf]/10 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-[#fff0ea]">{item.tool_notes.length} comparisons</span> : null}
</div>
<h2 className="mt-3 text-2xl font-semibold tracking-[-0.04em] text-white transition group-hover:text-sky-100">{item.title}</h2>
<p className="mt-3 text-sm leading-7 text-slate-300">{item.excerpt || item.description || item.prompt_preview || 'No description yet.'}</p>
{item.tags?.length ? <p className="mt-4 text-xs uppercase tracking-[0.18em] text-slate-500">{item.tags.slice(0, 4).join(' · ')}</p> : null}
</div>
</Link>
)
}
return (
<Link href={itemHref(pageType, item)} className="rounded-[28px] border border-white/10 bg-white/[0.04] p-5 transition hover:border-white/20 hover:bg-white/[0.06]">
<div className="flex items-center justify-between gap-3">
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80">{pageType.slice(0, -1)}</p>
<LockBadge item={item} />
</div>
{pageType === 'lessons' && item?.formatted_lesson_number ? (
<div className="mt-4 flex flex-wrap items-center gap-2">
<span className="rounded-full border border-amber-300/20 bg-amber-300/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.2em] text-amber-100">{item.formatted_lesson_number}</span>
{lessonSeries ? <span className="text-xs font-medium uppercase tracking-[0.18em] text-slate-500">{lessonSeries}</span> : null}
</div>
) : null}
<h2 className="mt-4 text-2xl font-semibold tracking-[-0.04em] text-white">{item.title}</h2>
<p className="mt-3 text-sm leading-7 text-slate-300">{item.excerpt || item.description || item.prompt_preview || item.content_preview || 'No description yet.'}</p>
{pageType === 'lessons' && item.tags?.length ? <p className="mt-4 text-xs uppercase tracking-[0.18em] text-slate-500">{item.tags.slice(0, 4).join(' · ')}</p> : null}
{pageType === 'prompts' && item.tags?.length ? <p className="mt-4 text-xs uppercase tracking-[0.18em] text-slate-500">{item.tags.slice(0, 4).join(' · ')}</p> : null}
{pageType === 'challenges' ? <p className="mt-4 text-xs uppercase tracking-[0.18em] text-slate-500">{item.status} · {item.submission_count ?? 0} submissions</p> : null}
</Link>
@@ -82,33 +192,36 @@ function AcademyCard({ pageType, item }) {
export default function AcademyList({ pageType, title, description, seo, items, filters, categories, pricingUrl }) {
const flash = usePage().props.flash || {}
const visibleItems = Array.isArray(items?.data) ? items.data : []
return (
<main className="min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.15),_transparent_24%),radial-gradient(circle_at_bottom_right,_rgba(251,191,36,0.16),_transparent_24%),linear-gradient(180deg,_#0f172a_0%,_#111827_100%)] px-4 py-8 sm:px-6 lg:px-8">
<SeoHead seo={seo || {}} title={title} description={description} />
<div className="mx-auto max-w-[1360px] space-y-6">
<section className="rounded-[38px] border border-white/10 bg-black/20 p-8 md:p-10">
<div className="flex flex-wrap items-end justify-between gap-5">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-200/80">Skinbase AI Academy</p>
<h1 className="mt-3 text-4xl font-semibold tracking-[-0.05em] text-white md:text-5xl">{title}</h1>
<p className="mt-4 max-w-3xl text-base leading-8 text-slate-300">{description}</p>
{pageType === 'prompts' ? <PromptLibraryHero title={title} description={description} items={visibleItems} pricingUrl={pricingUrl} /> : (
<section className="rounded-[38px] border border-white/10 bg-black/20 p-8 md:p-10">
<div className="flex flex-wrap items-end justify-between gap-5">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-200/80">Skinbase AI Academy</p>
<h1 className="mt-3 text-4xl font-semibold tracking-[-0.05em] text-white md:text-5xl">{title}</h1>
<p className="mt-4 max-w-3xl text-base leading-8 text-slate-300">{description}</p>
</div>
<Link href={pricingUrl} className="rounded-full border border-sky-300/25 bg-sky-300/12 px-5 py-3 text-sm font-semibold text-sky-100">Upgrade preview</Link>
</div>
<Link href={pricingUrl} className="rounded-full border border-sky-300/25 bg-sky-300/12 px-5 py-3 text-sm font-semibold text-sky-100">Upgrade preview</Link>
</div>
</section>
</section>
)}
{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}
<QueryFilters pageType={pageType} filters={filters} categories={categories} />
{(items?.data || []).length === 0 ? (
{visibleItems.length === 0 ? (
<section className="rounded-[30px] border border-white/10 bg-white/[0.04] px-6 py-12 text-center text-slate-400">Nothing matched this Academy view yet.</section>
) : (
<section className="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
{items.data.map((item) => <AcademyCard key={`${pageType}-${item.id}`} pageType={pageType} item={item} />)}
{visibleItems.map((item) => <AcademyCard key={`${pageType}-${item.id}`} pageType={pageType} item={item} />)}
</section>
)}
</div>

View File

@@ -2,6 +2,35 @@ import React, { useEffect, useRef, useState } from 'react'
import { Link, router, usePage } from '@inertiajs/react'
import SeoHead from '../../components/seo/SeoHead'
function academyHref(section, slug) {
return `/academy/${section}/${encodeURIComponent(slug)}`
}
function AcademyBreadcrumbs({ items = [] }) {
if (!items.length) return null
return (
<nav aria-label="Breadcrumb" className="flex flex-wrap items-center gap-2 text-sm text-slate-400">
{items.map((item, index) => {
const isLast = index === items.length - 1
return (
<React.Fragment key={`${item.label}-${index}`}>
{index > 0 ? <span className="text-slate-600">/</span> : null}
{isLast ? (
<span className="font-medium text-slate-200">{item.label}</span>
) : (
<Link href={item.href} className="transition hover:text-white">
{item.label}
</Link>
)}
</React.Fragment>
)
})}
</nav>
)
}
function slugifyHeading(value, fallback = 'section') {
const normalized = String(value || '')
.toLowerCase()
@@ -48,6 +77,29 @@ function LessonInfoRow({ label, value }) {
)
}
function LessonNavCard({ direction, lesson }) {
if (!lesson) return null
const eyebrow = direction === 'previous' ? 'Previous lesson' : 'Next lesson'
const alignClass = direction === 'previous' ? 'items-start text-left' : 'items-end text-right'
const href = lesson.course_url || `/academy/lessons/${lesson.slug}`
return (
<Link
href={href}
aria-label={`${eyebrow}: ${lesson.title}`}
className={`group flex min-h-full flex-col justify-between rounded-[28px] border border-white/10 bg-black/20 p-5 transition hover:border-sky-300/25 hover:bg-white/[0.06] ${alignClass}`}
>
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400">{eyebrow}</p>
{lesson.lesson_label ? <p className="mt-3 text-xs font-semibold uppercase tracking-[0.2em] text-amber-100">{lesson.lesson_label}</p> : null}
<h3 className="mt-3 text-2xl font-semibold tracking-[-0.04em] text-white transition group-hover:text-sky-100">{lesson.title}</h3>
</div>
<p className="mt-4 text-sm leading-7 text-slate-300">{lesson.excerpt || lesson.content_preview || 'Open the next step in this Academy sequence.'}</p>
</Link>
)
}
function LockedPanel({ pricingUrl, label }) {
return (
<div className="rounded-[28px] border border-amber-300/20 bg-amber-300/10 p-6 text-amber-50">
@@ -87,7 +139,7 @@ function copyTextToClipboard(text) {
return Promise.reject(new Error('Clipboard unavailable'))
}
function PromptCopyButton({ prompt }) {
function PromptCopyButton({ prompt, label = 'Copy prompt' }) {
const [status, setStatus] = useState('idle')
const resetTimerRef = useRef(0)
@@ -107,11 +159,172 @@ function PromptCopyButton({ prompt }) {
aria-label="Copy prompt"
>
<i className={`fa-solid ${status === 'copied' ? 'fa-check' : status === 'failed' ? 'fa-triangle-exclamation' : 'fa-copy'}`} />
<span>{status === 'copied' ? 'Copied' : status === 'failed' ? 'Copy failed' : 'Copy prompt'}</span>
<span>{status === 'copied' ? 'Copied' : status === 'failed' ? 'Copy failed' : label}</span>
</button>
)
}
function ImageLightbox({ gallery, onClose, onNavigate }) {
useEffect(() => {
if (!gallery?.images?.length) return undefined
const handleEscape = (event) => {
if (event.key === 'Escape') {
onClose()
return
}
if (event.key === 'ArrowLeft') {
onNavigate(-1)
return
}
if (event.key === 'ArrowRight') {
onNavigate(1)
}
}
document.body.style.overflow = 'hidden'
window.addEventListener('keydown', handleEscape)
return () => {
document.body.style.overflow = ''
window.removeEventListener('keydown', handleEscape)
}
}, [gallery, onClose, onNavigate])
const images = Array.isArray(gallery?.images) ? gallery.images : []
const currentIndex = Math.max(0, Math.min(images.length - 1, Number(gallery?.index || 0)))
const currentImage = images[currentIndex]
if (!currentImage?.src) return null
return (
<div className="fixed inset-0 z-[120] flex items-center justify-center bg-[#020611e6] p-4 backdrop-blur-md" onClick={onClose} role="dialog" aria-modal="true" aria-label={currentImage.alt || 'Image preview'}>
<button type="button" onClick={onClose} className="absolute right-4 top-4 inline-flex h-11 w-11 items-center justify-center rounded-full border border-white/10 bg-black/35 text-white transition hover:border-white/25 hover:bg-white/10" aria-label="Close image preview">
<i className="fa-solid fa-xmark text-lg" />
</button>
{images.length > 1 ? (
<button type="button" onClick={(event) => { event.stopPropagation(); onNavigate(-1) }} className="absolute left-4 top-1/2 inline-flex h-12 w-12 -translate-y-1/2 items-center justify-center rounded-full border border-white/10 bg-black/35 text-white transition hover:border-white/25 hover:bg-white/10" aria-label="Previous image">
<i className="fa-solid fa-chevron-left" />
</button>
) : null}
{images.length > 1 ? (
<button type="button" onClick={(event) => { event.stopPropagation(); onNavigate(1) }} className="absolute right-4 top-1/2 inline-flex h-12 w-12 -translate-y-1/2 items-center justify-center rounded-full border border-white/10 bg-black/35 text-white transition hover:border-white/25 hover:bg-white/10" aria-label="Next image">
<i className="fa-solid fa-chevron-right" />
</button>
) : null}
<div className="max-h-[92vh] max-w-[min(1400px,96vw)] overflow-hidden rounded-[30px] border border-white/10 bg-black/30 shadow-[0_30px_120px_rgba(0,0,0,0.5)]" onClick={(event) => event.stopPropagation()}>
<img src={currentImage.src} alt={currentImage.alt || ''} className="max-h-[92vh] w-full object-contain" />
{images.length > 1 ? (
<div className="flex items-center justify-between gap-4 border-t border-white/10 bg-black/35 px-5 py-3 text-sm text-slate-200">
<div>
<p className="font-semibold text-white">{currentImage.alt || `Image ${currentIndex + 1}`}</p>
<p className="text-xs uppercase tracking-[0.18em] text-slate-400">{`Image ${currentIndex + 1} of ${images.length}`}</p>
</div>
<div className="flex gap-2">
{images.map((image, index) => (
<button
key={`${image.src}-${index}`}
type="button"
onClick={() => onNavigate(index - currentIndex)}
className={`h-2.5 w-2.5 rounded-full transition ${index === currentIndex ? 'bg-white' : 'bg-white/25 hover:bg-white/45'}`}
aria-label={`Go to image ${index + 1}`}
/>
))}
</div>
</div>
) : null}
</div>
</div>
)
}
function PromptToolNoteCard({ note, index, galleryIndex, onOpenImage }) {
if (!note || typeof note !== 'object') return null
const title = note.model_name || note.provider || `Comparison ${String(index + 1).padStart(2, '0')}`
const subtitle = [note.provider, note.model_name].filter(Boolean).join(' · ')
const previewUrl = note.image_url || note.thumb_url || ''
const hasContent = Boolean(note.notes || note.strengths || note.weaknesses || note.best_for || note.settings || previewUrl || note.score || subtitle)
if (!hasContent) return null
return (
<article className="rounded-[28px] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.05),rgba(15,23,42,0.22))] p-5 shadow-[0_16px_40px_rgba(2,6,23,0.16)]">
{previewUrl ? (
<button
type="button"
onClick={() => onOpenImage?.(galleryIndex)}
className="group mb-5 block w-full overflow-hidden rounded-[24px] border border-white/10 bg-slate-950 text-left transition hover:border-sky-300/25 focus:outline-none focus:ring-2 focus:ring-sky-300/35"
aria-label={`Open comparison image for ${title}`}
>
<div className="relative">
<img src={previewUrl} alt={title} loading="lazy" className="aspect-[4/3] w-full object-cover transition duration-500 group-hover:scale-[1.03]" />
<div className="absolute inset-x-0 bottom-0 flex items-center justify-between gap-3 bg-[linear-gradient(180deg,transparent,rgba(2,6,23,0.72))] px-4 py-3">
<span className="text-[10px] font-semibold uppercase tracking-[0.2em] text-slate-100/90">Click to zoom</span>
<span className="inline-flex h-9 w-9 items-center justify-center rounded-full border border-white/10 bg-black/25 text-white">
<i className="fa-solid fa-expand" />
</span>
</div>
</div>
</button>
) : null}
<div className="flex items-start justify-between gap-4">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.2em] text-[#ffcfbf]">AI comparison</p>
<h3 className="mt-2 text-xl font-semibold tracking-[-0.03em] text-white">{title}</h3>
{subtitle ? <p className="mt-1 text-sm text-slate-400">{subtitle}</p> : null}
</div>
<div className="flex flex-col items-end gap-2">
<span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-300">{String(index + 1).padStart(2, '0')}</span>
{note.score ? <span className="rounded-full border border-[#ffcfbf]/20 bg-[#ffcfbf]/10 px-3 py-1 text-xs font-semibold text-[#fff0ea]">{`Score ${note.score}/10`}</span> : null}
</div>
</div>
<div className="mt-5 space-y-4">
{note.settings ? (
<div className="rounded-[22px] border border-white/10 bg-black/25 p-4">
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">Generated in</p>
<p className="mt-2 whitespace-pre-wrap text-sm leading-7 text-slate-200">{note.settings}</p>
</div>
) : null}
{note.notes ? (
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">Overall notes</p>
<p className="mt-2 whitespace-pre-wrap text-sm leading-7 text-slate-200">{note.notes}</p>
</div>
) : null}
{note.best_for ? (
<div className="rounded-[22px] border border-sky-300/15 bg-sky-300/10 p-4">
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-100">Best for</p>
<p className="mt-2 whitespace-pre-wrap text-sm leading-7 text-slate-100">{note.best_for}</p>
</div>
) : null}
<div className="grid gap-4 md:grid-cols-2">
{note.strengths ? (
<div className="rounded-[22px] border border-emerald-300/15 bg-emerald-300/10 p-4">
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-emerald-100">Strengths</p>
<p className="mt-2 whitespace-pre-wrap text-sm leading-7 text-slate-100">{note.strengths}</p>
</div>
) : null}
{note.weaknesses ? (
<div className="rounded-[22px] border border-amber-300/15 bg-amber-300/10 p-4">
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-amber-100">Weaknesses</p>
<p className="mt-2 whitespace-pre-wrap text-sm leading-7 text-slate-100">{note.weaknesses}</p>
</div>
) : null}
</div>
</div>
</article>
)
}
function AiComparisonSection({ block }) {
const payload = block?.payload || {}
const criteria = Array.isArray(payload.criteria) ? payload.criteria.filter(Boolean) : []
@@ -227,42 +440,89 @@ function AiComparisonSection({ block }) {
)
}
export default function AcademyShow({ pageType, item, relatedLessons = [], seo, pricingUrl, completeUrl, completed: initialCompleted, saveUrl, unsaveUrl, saved: initialSaved, submitUrl }) {
export default function AcademyShow({ pageType, item, relatedLessons = [], relatedCourses = [], previousLesson = null, nextLesson = null, seo, pricingUrl, completeUrl, completed: initialCompleted, saveUrl, unsaveUrl, saved: initialSaved, submitUrl, courseContext = null }) {
const flash = usePage().props.flash || {}
const [completed, setCompleted] = useState(Boolean(initialCompleted))
const [saved, setSaved] = useState(Boolean(initialSaved))
const [tableOfContents, setTableOfContents] = useState([])
const [activeHeadingId, setActiveHeadingId] = useState('')
const [lightboxGallery, setLightboxGallery] = useState(null)
const articleContentRef = useRef(null)
const handledInitialHashRef = useRef(false)
const lessonCover = item?.cover_image_url || item?.cover_image || ''
const articleCover = item?.article_cover_image_url || item?.article_cover_image || ''
const lessonCategory = item?.category?.name || 'Academy'
const lessonSeries = String(item?.series_name || '').trim() || lessonCategory
const lessonDifficulty = item?.difficulty || 'Intermediate'
const lessonMinutes = formatLessonMinutes(item?.reading_minutes)
const lessonUpdated = formatLessonDate(item?.published_at)
const lessonBlocks = Array.isArray(item?.blocks) ? item.blocks : []
const relatedLessonList = Array.isArray(relatedLessons) ? relatedLessons : []
const relatedCourseList = Array.isArray(relatedCourses) ? relatedCourses : []
const courseOutline = Array.isArray(courseContext?.outline) ? courseContext.outline : []
const lessonSummary = item.excerpt || item.description || item.prompt_preview || item.content_preview || 'A focused Academy lesson with practical guidance and examples.'
const lessonTags = Array.isArray(item?.tags) ? item.tags.filter(Boolean) : []
const promptPreviewImage = item?.preview_image || ''
const promptBody = item?.prompt || item?.prompt_preview || ''
const promptComparisons = Array.isArray(item?.tool_notes)
? item.tool_notes.filter((note) => note && typeof note === 'object' && note.active !== false && [
note.provider,
note.model_name,
note.notes,
note.strengths,
note.weaknesses,
note.best_for,
note.image_path,
note.image_url,
note.thumb_path,
note.thumb_url,
note.settings,
note.score,
].some(Boolean))
: []
const promptUsageNotes = String(item?.usage_notes || '').trim()
const promptWorkflowNotes = String(item?.workflow_notes || '').trim()
const promptHasFullAccess = Boolean(item?.prompt)
const promptModelsCovered = promptComparisons.map((note, index) => note.model_name || note.provider || `Model ${index + 1}`)
const promptComparisonGalleryImages = promptComparisons
.map((note, index) => {
const src = note.image_url || note.thumb_url || ''
if (!src) return null
return {
src,
alt: note.model_name || note.provider || `Comparison ${index + 1}`,
}
})
.filter(Boolean)
const academyBreadcrumbs = pageType === 'prompt'
? [
{ label: 'Academy', href: '/academy' },
{ label: 'Prompt Library', href: '/academy/prompts' },
{ label: item?.title || 'Prompt' },
]
: []
const fontScaleStorageKey = 'academy.lesson.font-scale'
const fontScaleMin = 0.95
const fontScaleMax = 1.12
const fontScaleStep = 0.04
const [lessonFontScale, setLessonFontScale] = useState(() => {
if (typeof window === 'undefined') {
return 1.04
const [lessonFontScale, setLessonFontScale] = useState(1.04)
const findArticleHeading = (headingId) => {
if (!headingId || typeof document === 'undefined') {
return null
}
const storedValue = Number(window.localStorage.getItem(fontScaleStorageKey))
const escapedHeadingId = typeof CSS !== 'undefined' && typeof CSS.escape === 'function'
? CSS.escape(headingId)
: String(headingId).replace(/[^a-zA-Z0-9_-]/g, '')
if (Number.isFinite(storedValue)) {
return Math.min(fontScaleMax, Math.max(fontScaleMin, storedValue))
}
return 1.04
})
return articleContentRef.current?.querySelector(`#${escapedHeadingId}`) || document.getElementById(headingId)
}
const markComplete = () => {
if (!completeUrl || completed) return
router.post(completeUrl, {}, {
router.post(completeUrl, courseContext?.completePayload || {}, {
preserveScroll: true,
onSuccess: () => setCompleted(true),
})
@@ -285,6 +545,64 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], seo,
setLessonFontScale((current) => Math.min(fontScaleMax, Number((current + fontScaleStep).toFixed(2))))
}
const openPromptPreviewImage = () => {
if (!promptPreviewImage) return
setLightboxGallery({
images: [{ src: promptPreviewImage, alt: item?.title || 'Prompt preview' }],
index: 0,
})
}
const openPromptComparisonGallery = (index) => {
if (!promptComparisonGalleryImages.length) return
setLightboxGallery({
images: promptComparisonGalleryImages,
index: Math.max(0, Math.min(promptComparisonGalleryImages.length - 1, Number(index || 0))),
})
}
const navigateLightboxGallery = (direction) => {
setLightboxGallery((current) => {
if (!current?.images?.length) return current
const total = current.images.length
const nextIndex = typeof direction === 'number' && Math.abs(direction) > 1
? Math.max(0, Math.min(total - 1, current.index + direction))
: (current.index + direction + total) % total
return {
...current,
index: nextIndex,
}
})
}
const scrollToHeading = (headingId, behavior = 'smooth') => {
if (typeof window === 'undefined') {
return
}
const heading = findArticleHeading(headingId)
if (!heading) {
return
}
const top = Math.max(0, window.scrollY + heading.getBoundingClientRect().top - 112)
window.scrollTo({ top, behavior })
setActiveHeadingId(headingId)
if (window.history?.replaceState) {
window.history.replaceState(null, '', `${window.location.pathname}${window.location.search}#${headingId}`)
}
}
useEffect(() => {
handledInitialHashRef.current = false
}, [item?.slug])
useEffect(() => {
if (pageType !== 'lesson' || !item?.content || !articleContentRef.current) {
setTableOfContents([])
@@ -301,6 +619,7 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], seo,
seenIds.set(baseId, seenCount + 1)
heading.id = nextId
heading.style.scrollMarginTop = '128px'
return {
id: nextId,
@@ -312,42 +631,98 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], seo,
setTableOfContents(nextTableOfContents)
}, [item?.content, pageType])
useEffect(() => {
if (pageType !== 'lesson' || tableOfContents.length === 0 || handledInitialHashRef.current || typeof window === 'undefined') {
return
}
const hash = window.location.hash.replace(/^#/, '').trim()
if (!hash) {
handledInitialHashRef.current = true
return
}
const matchingEntry = tableOfContents.find((entry) => entry.id === hash)
if (!matchingEntry) {
handledInitialHashRef.current = true
return
}
handledInitialHashRef.current = true
window.requestAnimationFrame(() => scrollToHeading(matchingEntry.id, 'auto'))
}, [pageType, tableOfContents])
useEffect(() => {
if (pageType !== 'lesson' || tableOfContents.length === 0 || typeof window === 'undefined') {
return undefined
}
const handleHashChange = () => {
const hash = window.location.hash.replace(/^#/, '').trim()
if (!hash) {
return
}
const matchingEntry = tableOfContents.find((entry) => entry.id === hash)
if (!matchingEntry) {
return
}
window.requestAnimationFrame(() => scrollToHeading(matchingEntry.id, 'auto'))
}
window.addEventListener('hashchange', handleHashChange)
return () => window.removeEventListener('hashchange', handleHashChange)
}, [pageType, tableOfContents])
useEffect(() => {
if (pageType !== 'lesson' || tableOfContents.length === 0 || !articleContentRef.current) {
setActiveHeadingId('')
return
}
const headingElements = Array.from(articleContentRef.current.querySelectorAll('h2, h3'))
const getActiveId = () => {
const headings = Array.from(articleContentRef.current.querySelectorAll('h2[id], h3[id]'))
if (!headings.length) return ''
if (!headingElements.length) {
setActiveHeadingId('')
// offset accounts for sticky header height + small buffer
const offset = 140
let activeId = headings[0].id
for (const heading of headings) {
if (heading.getBoundingClientRect().top <= offset) {
activeId = heading.id
}
}
return activeId
}
setActiveHeadingId(getActiveId())
const onScroll = () => setActiveHeadingId(getActiveId())
window.addEventListener('scroll', onScroll, { passive: true })
return () => window.removeEventListener('scroll', onScroll)
}, [pageType, tableOfContents, lessonFontScale])
useEffect(() => {
if (typeof window === 'undefined') {
return
}
const observer = new IntersectionObserver((entries) => {
const visibleEntries = entries
.filter((entry) => entry.isIntersecting)
.sort((left, right) => left.boundingClientRect.top - right.boundingClientRect.top)
const storedValue = Number(window.localStorage.getItem(fontScaleStorageKey))
if (visibleEntries.length) {
setActiveHeadingId((current) => visibleEntries[0].target.id || current)
}
}, {
root: null,
rootMargin: '-18% 0px -68% 0px',
threshold: [0, 1],
})
headingElements.forEach((heading) => observer.observe(heading))
const firstVisibleHeading = headingElements.find((heading) => heading.getBoundingClientRect().top >= 0) || headingElements[0]
if (firstVisibleHeading?.id) {
setActiveHeadingId(firstVisibleHeading.id)
if (!Number.isFinite(storedValue)) {
return
}
return () => observer.disconnect()
}, [pageType, tableOfContents, lessonFontScale])
setLessonFontScale(Math.min(fontScaleMax, Math.max(fontScaleMin, storedValue)))
}, [fontScaleMax, fontScaleMin, fontScaleStorageKey])
useEffect(() => {
if (typeof window === 'undefined') {
@@ -452,7 +827,7 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], seo,
}, [item?.content, lessonFontScale, pageType])
return (
<main className="min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.15),_transparent_24%),radial-gradient(circle_at_bottom_right,_rgba(251,191,36,0.16),_transparent_24%),linear-gradient(180deg,_#0f172a_0%,_#111827_100%)] px-4 py-8 sm:px-6 lg:px-8">
<main className="min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.16),_transparent_24%),radial-gradient(circle_at_bottom_right,_rgba(59,130,246,0.14),_transparent_26%),linear-gradient(180deg,_#0b1220_0%,_#111827_46%,_#0f172a_100%)] px-4 py-8 sm:px-6 lg:px-8">
<SeoHead seo={seo || {}} title={item?.title} description={item?.excerpt || item?.description} />
<div className="mx-auto max-w-[1320px] space-y-6">
@@ -475,9 +850,27 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], seo,
<span className="rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-300">{lessonDifficulty}</span>
</div>
{item.lesson_label ? <p className="mt-5 text-sm font-semibold uppercase tracking-[0.24em] text-amber-100">{item.lesson_label}</p> : null}
<h1 className="mt-5 text-4xl font-semibold tracking-[-0.055em] text-white md:text-5xl lg:text-6xl">{item.title}</h1>
<p className="mt-5 max-w-2xl text-base leading-8 text-slate-300 md:text-lg">{lessonSummary}</p>
{lessonTags.length ? (
<div className="mt-5 flex flex-wrap gap-2">
{lessonTags.map((tag) => (
<span key={tag} className="rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-200">{tag}</span>
))}
</div>
) : null}
{courseContext?.title ? (
<div className="mt-6 max-w-2xl rounded-[24px] border border-white/10 bg-black/25 p-5">
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400">Part of course</p>
<Link href={courseContext.showUrl} className="mt-2 inline-flex text-lg font-semibold text-sky-100 transition hover:text-white">{courseContext.title}</Link>
<p className="mt-2 text-sm leading-7 text-slate-300">{courseContext.subtitle || 'This lesson is being viewed inside a structured Academy course path.'}</p>
</div>
) : null}
<div className="mt-7 flex flex-wrap gap-3">
{completeUrl ? <button type="button" onClick={markComplete} className="rounded-full border border-emerald-300/25 bg-emerald-300/12 px-5 py-3 text-sm font-semibold text-emerald-100">{completed ? 'Completed' : 'Mark complete'}</button> : null}
{saveUrl ? <button type="button" onClick={toggleSave} className="rounded-full border border-sky-300/25 bg-sky-300/12 px-5 py-3 text-sm font-semibold text-sky-100">{saved ? 'Saved' : 'Save prompt'}</button> : null}
@@ -488,7 +881,7 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], seo,
<StatPill label="Category" value={lessonCategory} />
<StatPill label="Reading" value={lessonMinutes} />
<StatPill label="Updated" value={lessonUpdated} />
<StatPill label="Access" value={item.access_level || 'free'} />
<StatPill label={courseContext?.title ? 'Course progress' : 'Access'} value={courseContext?.progress ? `${courseContext.progress.percent}%` : (item.access_level || 'free')} />
</div>
</div>
</div>
@@ -500,7 +893,8 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], seo,
</div>
<div className="space-y-3">
<LessonInfoRow label="Series" value={lessonCategory} />
<LessonInfoRow label="Series" value={lessonSeries} />
{item.formatted_lesson_number ? <LessonInfoRow label="Lesson" value={item.formatted_lesson_number} /> : null}
<LessonInfoRow label="Difficulty" value={lessonDifficulty} />
<LessonInfoRow label="Reading time" value={lessonMinutes} />
<LessonInfoRow label="Published" value={lessonUpdated} />
@@ -508,7 +902,7 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], seo,
<div className="rounded-[28px] border border-white/10 bg-black/20 p-5">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400">Lesson status</p>
<p className="mt-3 text-sm leading-7 text-slate-300">{item.locked ? 'This lesson is partially locked for your account level.' : 'Full lesson content is available below.'}</p>
<p className="mt-3 text-sm leading-7 text-slate-300">{item.locked ? 'This lesson is partially locked for your account level.' : courseContext?.title ? 'This lesson is being tracked inside a course. Completion updates your course progress.' : 'Full lesson content is available below.'}</p>
</div>
</div>
</aside>
@@ -516,7 +910,7 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], seo,
</section>
<div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_360px]">
<article className="rounded-[32px] border border-white/10 bg-white/[0.04] p-6 text-slate-200 md:p-8">
<article className="rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.045),rgba(148,163,184,0.03))] p-6 text-slate-200 shadow-[0_24px_70px_rgba(2,6,23,0.2)] md:p-8">
<div className="flex flex-wrap items-center justify-between gap-4 border-b border-white/10 pb-5">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-200/80">Article</p>
@@ -549,6 +943,12 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], seo,
</div>
<div className="mt-6">
{articleCover ? (
<div className="mb-8 overflow-hidden rounded-[28px] border border-white/10 bg-black/20">
<img src={articleCover} alt={`${item.title} article cover`} className="w-full object-cover" />
</div>
) : null}
{item.content ? (
<div className="space-y-8">
<div
@@ -566,11 +966,23 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], seo,
</div>
)}
</div>
{(previousLesson || nextLesson) ? (
<section className="mt-10 border-t border-white/10 pt-8">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400">{courseContext?.title ? 'Course navigation' : 'Lesson navigation'}</p>
<h3 className="mt-2 text-2xl font-semibold tracking-[-0.04em] text-white">{courseContext?.title ? 'Continue this course' : 'Continue in order'}</h3>
<div className="mt-5 grid gap-4 md:grid-cols-2">
<LessonNavCard direction="previous" lesson={previousLesson} />
<LessonNavCard direction="next" lesson={nextLesson} />
</div>
</section>
) : null}
</article>
<aside className="space-y-6 lg:sticky lg:top-6 lg:self-start">
{tableOfContents.length ? (
<section className="rounded-[32px] border border-white/10 bg-white/[0.04] p-6 text-slate-200">
<section className="rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.92),rgba(15,23,42,0.84))] p-6 text-slate-200 shadow-[0_18px_50px_rgba(2,6,23,0.18)]">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400">On this page</p>
<h3 className="mt-2 text-2xl font-semibold tracking-[-0.04em] text-white">Table of contents</h3>
@@ -579,6 +991,10 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], seo,
<a
key={entry.id}
href={`#${entry.id}`}
onClick={(event) => {
event.preventDefault()
scrollToHeading(entry.id)
}}
aria-current={activeHeadingId === entry.id ? 'location' : undefined}
className={`academy-lesson-toc-link ${entry.level === 'h3' ? 'academy-lesson-toc-link-subtle' : ''} ${activeHeadingId === entry.id ? 'academy-lesson-toc-link-active' : ''}`}
>
@@ -590,27 +1006,56 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], seo,
</section>
) : null}
<section className="rounded-[32px] border border-white/10 bg-white/[0.04] p-6 text-slate-200">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400">Series info</p>
<h3 className="mt-2 text-2xl font-semibold tracking-[-0.04em] text-white">{lessonCategory}</h3>
<section className="rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.92),rgba(15,23,42,0.84))] p-6 text-slate-200 shadow-[0_18px_50px_rgba(2,6,23,0.18)]">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400">{courseContext?.title ? 'Course progress' : 'Series info'}</p>
<h3 className="mt-2 text-2xl font-semibold tracking-[-0.04em] text-white">{courseContext?.title ? courseContext.title : lessonSeries}</h3>
<div className="mt-5 space-y-3">
<LessonInfoRow label="Category" value={lessonCategory} />
{item.formatted_lesson_number ? <LessonInfoRow label="Lesson" value={item.formatted_lesson_number} /> : null}
<LessonInfoRow label="Difficulty" value={lessonDifficulty} />
<LessonInfoRow label="Reading" value={lessonMinutes} />
<LessonInfoRow label="Updated" value={lessonUpdated} />
<LessonInfoRow label={courseContext?.title ? 'Progress' : 'Updated'} value={courseContext?.progress ? `${courseContext.progress.completedRequired}/${courseContext.progress.totalRequired} completed` : lessonUpdated} />
</div>
</section>
{courseOutline.length ? (
<section className="rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.92),rgba(15,23,42,0.84))] p-6 text-slate-200 shadow-[0_18px_50px_rgba(2,6,23,0.18)]">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400">Course outline</p>
<div className="mt-4 space-y-2">
{courseOutline.map((outlineLesson, index) => (
<Link key={outlineLesson.course_lesson_id || outlineLesson.id || index} href={outlineLesson.course_url || `/academy/lessons/${outlineLesson.slug}`} className={`flex items-start gap-3 rounded-[20px] border px-4 py-3 text-sm transition ${outlineLesson.slug === item.slug ? 'border-sky-300/25 bg-sky-300/10 text-sky-100' : 'border-white/10 bg-black/20 text-slate-300 hover:border-sky-300/25 hover:bg-white/[0.06]'}`}>
<span className="mt-0.5 inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-white/10 bg-white/[0.05] text-[10px] font-semibold">{String(index + 1).padStart(2, '0')}</span>
<span className="min-w-0 flex-1">
<span className="block font-semibold">{outlineLesson.title}</span>
<span className="mt-1 block text-xs uppercase tracking-[0.16em] text-slate-500">{outlineLesson.is_required ? 'Required' : 'Optional'}</span>
</span>
</Link>
))}
</div>
</section>
) : null}
{lessonTags.length ? (
<section className="rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.92),rgba(15,23,42,0.84))] p-6 text-slate-200 shadow-[0_18px_50px_rgba(2,6,23,0.18)]">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400">Microtags</p>
<div className="mt-4 flex flex-wrap gap-2">
{lessonTags.map((tag) => (
<span key={tag} className="rounded-full border border-sky-300/20 bg-sky-300/10 px-3 py-1.5 text-xs font-semibold uppercase tracking-[0.18em] text-sky-100">{tag}</span>
))}
</div>
</section>
) : null}
{relatedLessonList.length ? (
<section className="rounded-[32px] border border-white/10 bg-white/[0.04] p-6 text-slate-200">
<section className="rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.92),rgba(15,23,42,0.84))] p-6 text-slate-200 shadow-[0_18px_50px_rgba(2,6,23,0.18)]">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400">Continue learning</p>
<h3 className="mt-2 text-2xl font-semibold tracking-[-0.04em] text-white">More in {lessonCategory}</h3>
<h3 className="mt-2 text-2xl font-semibold tracking-[-0.04em] text-white">More in {lessonSeries}</h3>
<div className="mt-5 space-y-3">
{relatedLessonList.map((relatedLesson, index) => (
<Link
key={relatedLesson.id}
href={`/academy/lessons/${relatedLesson.slug}`}
href={relatedLesson.course_url || `/academy/lessons/${relatedLesson.slug}`}
className="group flex gap-4 rounded-[22px] border border-white/10 bg-black/20 p-4 transition hover:border-sky-300/25 hover:bg-white/[0.06]"
>
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl border border-sky-300/15 bg-sky-300/10 text-sm font-semibold text-sky-100">
@@ -619,7 +1064,10 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], seo,
<div className="min-w-0 flex-1">
<div className="flex items-start justify-between gap-3">
<h4 className="text-sm font-semibold text-white transition group-hover:text-sky-100">{relatedLesson.title}</h4>
<div>
{relatedLesson.formatted_lesson_number ? <p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-amber-100">{relatedLesson.formatted_lesson_number}</p> : null}
<h4 className="text-sm font-semibold text-white transition group-hover:text-sky-100">{relatedLesson.title}</h4>
</div>
<span className="shrink-0 rounded-full border border-white/10 bg-white/[0.04] px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-400">{formatLessonMinutes(relatedLesson.reading_minutes)}</span>
</div>
<p className="mt-2 text-xs leading-6 text-slate-400">{relatedLesson.excerpt || relatedLesson.content_preview || 'Continue the series with the next lesson.'}</p>
@@ -629,20 +1077,242 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], seo,
</div>
</section>
) : null}
{relatedCourseList.length ? (
<section className="rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.92),rgba(15,23,42,0.84))] p-6 text-slate-200 shadow-[0_18px_50px_rgba(2,6,23,0.18)]">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400">Related courses</p>
<div className="mt-4 space-y-3">
{relatedCourseList.map((course) => (
<Link key={course.id} href={course.public_url} className="block rounded-[22px] border border-white/10 bg-black/20 p-4 transition hover:border-sky-300/25 hover:bg-white/[0.06]">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500">{course.difficulty} · {course.access_level}</p>
<h4 className="mt-2 text-sm font-semibold text-white">{course.title}</h4>
<p className="mt-2 text-xs leading-6 text-slate-400">{course.excerpt || course.description || 'Open this course to continue with a guided path.'}</p>
</Link>
))}
</div>
</section>
) : null}
</aside>
</div>
</div>
) : pageType === 'prompt' ? (
<div className="space-y-8">
<section className="overflow-hidden rounded-[40px] border border-white/10 bg-[linear-gradient(135deg,rgba(4,10,20,0.98),rgba(15,23,42,0.9))] shadow-[0_24px_90px_rgba(15,23,42,0.34)]">
<div className="grid gap-0 lg:grid-cols-[minmax(420px,0.92fr)_minmax(0,1.08fr)]">
<div className="relative border-b border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.18),transparent_34%),radial-gradient(circle_at_bottom_right,rgba(255,183,139,0.18),transparent_32%),linear-gradient(180deg,rgba(5,10,20,0.98),rgba(10,17,30,0.94))] p-6 md:p-8 lg:min-h-[760px] lg:border-b-0 lg:border-r lg:border-white/10 lg:p-10">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_20%_20%,rgba(125,211,252,0.12),transparent_24%),radial-gradient(circle_at_80%_75%,rgba(255,207,191,0.12),transparent_28%)]" />
<div className="relative flex h-full flex-col">
<div className="flex items-center justify-between gap-3">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-[#ffd8cd]">Preview artwork</p>
{promptPreviewImage ? <span className="rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-300">Click to zoom</span> : null}
</div>
<button
type="button"
onClick={openPromptPreviewImage}
className="group mt-4 flex-1 overflow-hidden rounded-[32px] border border-white/10 bg-black/30 text-left shadow-[0_24px_80px_rgba(2,6,23,0.26)] transition hover:border-sky-300/25 focus:outline-none focus:ring-2 focus:ring-sky-300/35"
disabled={!promptPreviewImage}
aria-label={promptPreviewImage ? `Open preview image for ${item.title}` : 'Preview image unavailable'}
>
{promptPreviewImage ? (
<div className="relative h-full min-h-[360px] overflow-hidden lg:min-h-[620px]">
<img src={promptPreviewImage} alt={item.title} className="h-full w-full object-cover transition duration-500 group-hover:scale-[1.03]" />
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(2,6,23,0.02),rgba(2,6,23,0.28))]" />
<div className="absolute bottom-4 left-4 right-4 flex items-end justify-between gap-4 rounded-[24px] border border-white/10 bg-black/25 px-4 py-3 backdrop-blur-md">
<div>
<p className="text-[10px] font-semibold uppercase tracking-[0.22em] text-sky-100/80">Prompt visual</p>
<p className="mt-1 text-sm font-semibold text-white">Open full-size preview</p>
</div>
<span className="inline-flex h-10 w-10 items-center justify-center rounded-full border border-white/10 bg-white/10 text-white">
<i className="fa-solid fa-expand" />
</span>
</div>
</div>
) : (
<div className="flex h-full min-h-[360px] items-center justify-center bg-[linear-gradient(135deg,rgba(251,146,60,0.14),rgba(17,24,39,0.96))] px-8 text-center lg:min-h-[620px]">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400">Visual placeholder</p>
<p className="mt-4 text-lg font-semibold text-white">Preview image coming soon</p>
<p className="mt-3 text-sm leading-7 text-slate-300">This prompt page will feel much better once the generated cover image is attached.</p>
</div>
</div>
)}
</button>
</div>
</div>
<div className="relative overflow-hidden p-8 md:p-10 lg:p-12">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,_rgba(255,183,139,0.14),_transparent_28%),radial-gradient(circle_at_bottom_right,_rgba(56,189,248,0.12),_transparent_28%)]" />
<div className="relative z-10 max-w-4xl">
{academyBreadcrumbs.length ? (
<div className="mb-6">
<AcademyBreadcrumbs items={academyBreadcrumbs} />
</div>
) : null}
<div className="flex flex-wrap gap-2">
<span className="rounded-full border border-[#ffcfbf]/20 bg-[#ffcfbf]/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-[#fff0ea]">Skinbase AI Academy</span>
<span className="rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-300">{lessonCategory}</span>
<span className="rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-300">{lessonDifficulty}</span>
{item.aspect_ratio ? <span className="rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-300">{item.aspect_ratio}</span> : null}
{item.prompt_of_week ? <span className="rounded-full border border-amber-300/25 bg-amber-300/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-amber-100">Prompt of the week</span> : null}
{item.featured ? <span className="rounded-full border border-sky-300/20 bg-sky-300/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-100">Featured</span> : null}
</div>
<p className="mt-8 text-sm font-semibold uppercase tracking-[0.24em] text-[#ffd8cd]">Prompt template</p>
<h1 className="mt-4 max-w-4xl text-4xl font-semibold tracking-[-0.055em] text-white md:text-5xl xl:text-6xl">{item.title}</h1>
<p className="mt-5 max-w-3xl text-base leading-8 text-slate-300 md:text-lg">{lessonSummary}</p>
<div className="mt-7 flex flex-wrap gap-3">
{saveUrl ? <button type="button" onClick={toggleSave} className="rounded-full border border-sky-300/25 bg-sky-300/12 px-5 py-3 text-sm font-semibold text-sky-100">{saved ? 'Saved' : 'Save prompt'}</button> : null}
{promptBody ? <PromptCopyButton prompt={promptBody} /> : null}
{item.negative_prompt ? <PromptCopyButton prompt={item.negative_prompt} label="Copy negative" /> : null}
</div>
<div className="mt-8 grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
<StatPill label="Category" value={lessonCategory} />
<StatPill label="Access" value={item.access_level || 'free'} />
<StatPill label="Difficulty" value={lessonDifficulty} />
<StatPill label="Updated" value={lessonUpdated} />
</div>
{lessonTags.length ? (
<div className="mt-8 rounded-[28px] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.045),rgba(148,163,184,0.03))] p-5">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400">Microtags</p>
<div className="mt-4 flex flex-wrap gap-2">
{lessonTags.map((tag) => (
<span key={tag} className="rounded-full border border-sky-300/20 bg-sky-300/10 px-3 py-1.5 text-xs font-semibold uppercase tracking-[0.18em] text-sky-100">{tag}</span>
))}
</div>
</div>
) : null}
<div className="mt-8 grid gap-4 xl:grid-cols-[minmax(0,1.05fr)_minmax(280px,0.95fr)]">
<div className="rounded-[28px] border border-white/10 bg-black/20 p-5">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400">Prompt status</p>
<p className="mt-3 text-sm leading-7 text-slate-300">
{item.locked
? 'This page shows the prompt summary, but the full prompt text and editor notes stay locked until your Academy access level matches the template.'
: 'This template includes the main prompt, reuse guidance, and model-specific comparison notes in one place.'}
</p>
</div>
{promptModelsCovered.length ? (
<div className="rounded-[28px] border border-[#ffcfbf]/12 bg-[linear-gradient(180deg,rgba(255,207,191,0.08),rgba(255,255,255,0.03))] p-5">
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-[#ffd8cd]">Compared with</p>
<p className="mt-2 text-sm text-slate-300">{promptModelsCovered.length} model{promptModelsCovered.length > 1 ? 's' : ''} documented for this prompt.</p>
</div>
<span className="rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-xs font-semibold text-white">{promptModelsCovered.length}</span>
</div>
<div className="mt-4 flex flex-wrap gap-2">
{promptModelsCovered.map((model) => (
<span key={model} className="rounded-full border border-[#ffcfbf]/15 bg-[#ffcfbf]/10 px-3 py-1.5 text-xs font-semibold uppercase tracking-[0.18em] text-[#fff0ea]">{model}</span>
))}
</div>
</div>
) : null}
</div>
</div>
</div>
</div>
</section>
<div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_360px]">
<div className="space-y-8">
<section className="rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.045),rgba(148,163,184,0.03))] p-6 text-slate-200 shadow-[0_24px_70px_rgba(2,6,23,0.2)] md:p-8">
<div className="flex flex-wrap items-center justify-between gap-4 border-b border-white/10 pb-5">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-[#ffd8cd]">Prompt body</p>
<h2 className="mt-2 text-2xl font-semibold tracking-[-0.04em] text-white">Prompt text and exclusions</h2>
</div>
</div>
<div className="mt-6 space-y-6">
<div className="rounded-[28px] border border-[#ffcfbf]/15 bg-[linear-gradient(180deg,rgba(255,207,191,0.08),rgba(255,255,255,0.03))] p-5 md:p-6">
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-[#fff0ea]">{promptHasFullAccess ? 'Full prompt' : 'Preview prompt'}</p>
<p className="mt-2 text-xs uppercase tracking-[0.16em] text-slate-500">{promptHasFullAccess ? 'Ready to paste into your generation workflow.' : 'Upgrade your Academy access to reveal the complete prompt text.'}</p>
</div>
</div>
<pre className="mt-4 whitespace-pre-wrap rounded-[24px] border border-white/10 bg-slate-950/80 p-4 text-sm leading-7 text-slate-100 md:p-5">{promptBody || 'Prompt text is not available yet.'}</pre>
</div>
{item.negative_prompt ? (
<div className="rounded-[28px] border border-white/10 bg-black/20 p-5 md:p-6">
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400">Negative prompt</p>
<pre className="mt-4 whitespace-pre-wrap rounded-[24px] border border-white/10 bg-slate-950/70 p-4 text-sm leading-7 text-slate-200 md:p-5">{item.negative_prompt}</pre>
</div>
) : null}
</div>
</section>
{(promptUsageNotes || promptWorkflowNotes) ? (
<section className="rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.92),rgba(15,23,42,0.82))] p-6 text-slate-200 shadow-[0_18px_50px_rgba(2,6,23,0.18)] md:p-8">
<div className="flex flex-wrap items-end justify-between gap-4">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400">Prompt guidance</p>
<h2 className="mt-2 text-2xl font-semibold tracking-[-0.04em] text-white">How to use this prompt</h2>
</div>
{!promptHasFullAccess ? <span className="rounded-full border border-amber-300/20 bg-amber-300/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-amber-100">Full notes visible with access</span> : null}
</div>
<div className="mt-6 grid gap-5 md:grid-cols-2">
{promptUsageNotes ? (
<div className="rounded-[26px] border border-white/10 bg-black/20 p-5">
<p className="text-[11px] font-semibold uppercase tracking-[0.2em] text-sky-200/75">Usage notes</p>
<p className="mt-3 whitespace-pre-wrap text-sm leading-7 text-slate-200">{promptUsageNotes}</p>
</div>
) : null}
{promptWorkflowNotes ? (
<div className="rounded-[26px] border border-white/10 bg-black/20 p-5">
<p className="text-[11px] font-semibold uppercase tracking-[0.2em] text-emerald-200/75">Workflow notes</p>
<p className="mt-3 whitespace-pre-wrap text-sm leading-7 text-slate-200">{promptWorkflowNotes}</p>
</div>
) : null}
</div>
</section>
) : null}
{promptComparisons.length ? (
<section className="rounded-[32px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(255,183,139,0.12),transparent_30%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.92))] p-6 text-slate-200 shadow-[0_24px_80px_rgba(2,6,23,0.28)] md:p-8">
<div className="max-w-3xl">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-[#ffcfbf]">AI model comparisons</p>
<h2 className="mt-2 text-2xl font-semibold tracking-[-0.04em] text-white md:text-3xl">How different models respond to the same prompt</h2>
<p className="mt-3 text-sm leading-7 text-slate-300 md:text-base">Use these notes to decide which provider fits the result you want before you start tuning or post-processing.</p>
</div>
<div className="mt-6 grid gap-5 xl:grid-cols-2">
{promptComparisons.map((note, index) => <PromptToolNoteCard key={`${note.provider || 'provider'}-${note.model_name || 'model'}-${index}`} note={note} index={index} galleryIndex={index} onOpenImage={openPromptComparisonGallery} />)}
</div>
</section>
) : null}
</div>
<aside className="space-y-6 lg:sticky lg:top-6 lg:self-start">
{lessonTags.length ? (
<section className="rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.92),rgba(15,23,42,0.84))] p-6 text-slate-200 shadow-[0_18px_50px_rgba(2,6,23,0.18)]">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400">Microtags</p>
<div className="mt-4 flex flex-wrap gap-2">
{lessonTags.map((tag) => (
<span key={tag} className="rounded-full border border-sky-300/20 bg-sky-300/10 px-3 py-1.5 text-xs font-semibold uppercase tracking-[0.18em] text-sky-100">{tag}</span>
))}
</div>
</section>
) : null}
<section className="rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.92),rgba(15,23,42,0.84))] p-6 text-slate-200 shadow-[0_18px_50px_rgba(2,6,23,0.18)]">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400">Best use case</p>
<p className="mt-3 text-sm leading-7 text-slate-300">{promptComparisons[0]?.best_for || promptUsageNotes || lessonSummary}</p>
</section>
</aside>
</div>
</div>
) : (
<section className="rounded-[30px] border border-white/10 bg-white/[0.04] p-6 text-slate-200">
{pageType === 'prompt' ? (
<div className="space-y-6">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400">Prompt</p>
<pre className="mt-3 whitespace-pre-wrap rounded-2xl border border-white/10 bg-black/20 p-4 text-sm leading-7 text-slate-200">{item.prompt || item.prompt_preview}</pre>
</div>
{item.negative_prompt ? <div><p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-400">Negative prompt</p><pre className="mt-3 whitespace-pre-wrap rounded-2xl border border-white/10 bg-black/20 p-4 text-sm leading-7 text-slate-200">{item.negative_prompt}</pre></div> : null}
</div>
) : null}
{pageType === 'pack' ? (
<div className="space-y-5">
<p className="text-sm leading-8 text-slate-200">{item.description}</p>
@@ -686,6 +1356,8 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], seo,
</section>
)}
</div>
<ImageLightbox gallery={lightboxGallery} onClose={() => setLightboxGallery(null)} onNavigate={navigateLightboxGallery} />
</main>
)
}