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

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

View File

@@ -4,8 +4,90 @@ import SeoHead from '../../components/seo/SeoHead'
import NovaSelect from '../../components/ui/NovaSelect'
import { trackAcademySearchResultClick, trackUpgradeClick, useAcademyPageAnalytics } from '../../lib/academyAnalytics'
function CourseCard({ course, variant = 'default', analytics = null, searchContext = null, position = null }) {
const isFeatured = variant === 'featured'
function Breadcrumbs({ items = [] }) {
if (!items.length) {
return null
}
return (
<nav aria-label="Breadcrumb" className="flex flex-wrap items-center gap-2 text-xs font-semibold uppercase tracking-[0.18em] text-slate-400">
{items.map((item, index) => {
const isLast = index === items.length - 1
return (
<React.Fragment key={`${item.label}-${index}`}>
{isLast ? (
<span className="text-white/80">{item.label}</span>
) : (
<Link href={item.href} className="transition hover:text-white">{item.label}</Link>
)}
{!isLast ? <span className="text-slate-600">/</span> : null}
</React.Fragment>
)
})}
</nav>
)
}
function formatAccessDate(value) {
if (!value) {
return null
}
const parsed = new Date(value)
if (Number.isNaN(parsed.getTime())) {
return null
}
return new Intl.DateTimeFormat(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
}).format(parsed)
}
function academyAccessHeading(access) {
switch (access?.status) {
case 'staff_access':
return 'You currently have full staff access to the Academy.'
case 'grace_period':
return `${access.tierLabel} access is still active.`
case 'trialing':
return `${access.tierLabel} trial is active right now.`
case 'active':
return access?.hasPaidAccess ? `${access.tierLabel} access is active.` : 'Your Academy access is active.'
case 'free':
return 'You currently have Free access to the Academy.'
default:
return null
}
}
function academyAccessMeta(access) {
if (!access?.signedIn) {
return []
}
const items = [
{ label: 'Current tier', value: access?.tierLabel || 'Free' },
{ label: 'Status', value: access?.statusLabel || 'Free access' },
]
const formattedDate = formatAccessDate(access?.expiresAt)
if (formattedDate && access?.dateLabel) {
items.push({ label: access.dateLabel, value: formattedDate })
} else if (access?.renewsAutomatically) {
items.push({ label: 'Billing', value: 'Renews automatically' })
} else if (!access?.hasPaidAccess) {
items.push({ label: 'Upgrade', value: 'Creator and Pro unlock premium lessons and courses' })
}
return items
}
function CourseCard({ course, analytics = null, searchContext = null, position = null }) {
const progress = course?.progress || null
const cover = course?.cover_image_url || course?.teaser_image_url || course?.cover_image || course?.teaser_image || ''
const trackSearchClick = () => {
@@ -29,48 +111,56 @@ function CourseCard({ course, variant = 'default', analytics = null, searchConte
data-academy-search-query={searchContext?.query || undefined}
data-academy-search-results-count={searchContext?.resultsCount || undefined}
data-academy-search-position={position || undefined}
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(' ')}
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-amber-200/24 hover:bg-[linear-gradient(180deg,rgba(15,23,42,0.96),rgba(10,15,26,0.98))]"
>
<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>
<div className="relative aspect-[16/10] overflow-hidden bg-[linear-gradient(135deg,rgba(251,191,36,0.18),rgba(17,24,39,0.94))]">
{cover ? <img src={cover} 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">
{course?.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-amber-100">{course.difficulty}</span> : null}
{course?.access_level ? <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">{course.access_level}</span> : null}
{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 className="absolute bottom-4 left-4 right-4 flex flex-wrap items-end justify-between gap-3">
<div className="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-white">{course?.lessons_count || 0} lessons</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-white">{course?.estimated_minutes ? `${course.estimated_minutes} min` : 'Flexible pace'}</span>
</div>
</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 className="p-5">
<div className="flex items-center justify-between gap-3">
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-100/80">Academy course</p>
{course?.subtitle ? <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">{course.subtitle}</span> : null}
</div>
<h2 className="mt-3 text-2xl font-semibold tracking-[-0.04em] text-white transition group-hover:text-amber-50">{course.title}</h2>
<p className="mt-3 text-sm leading-7 text-slate-300">{course.excerpt || course.description || 'Structured Academy course.'}</p>
<p className="mt-4 text-xs uppercase tracking-[0.18em] text-slate-500">{progress ? `${progress.percent}% complete` : 'Start fresh'}{course?.access_level ? ` · ${course.access_level}` : ''}</p>
</div>
</Link>
)
}
export default function AcademyCoursesIndex({ seo, title, description, items, featuredCourses = [], filters = {}, pricingUrl, analytics }) {
export default function AcademyCoursesIndex({ seo, title, description, items, featuredCourses = [], filters = {}, pricingUrl, lessonsUrl, promptLibraryUrl, academyAccess = null, analytics }) {
const flash = usePage().props.flash || {}
useAcademyPageAnalytics(analytics)
const breadcrumbs = [
{ label: 'Academy', href: '/academy' },
{ label: 'Courses', href: '/academy/courses' },
]
const visibleItems = Array.isArray(items?.data) ? items.data : []
const totalCourses = Number(items?.total || items?.data?.length || 0)
const featuredCount = featuredCourses.length
const featuredCourse = featuredCourses.find((course) => course?.cover_image_url || course?.teaser_image_url || course?.cover_image || course?.teaser_image) || featuredCourses[0] || visibleItems[0] || null
const featuredCover = featuredCourse?.cover_image_url || featuredCourse?.teaser_image_url || featuredCourse?.cover_image || featuredCourse?.teaser_image || ''
const showSignedInAccess = Boolean(academyAccess?.signedIn)
const accessHeading = academyAccessHeading(academyAccess)
const accessMeta = academyAccessMeta(academyAccess)
const useBillingAction = showSignedInAccess && academyAccess?.hasPaidAccess && academyAccess?.billingUrl
const primaryActionLabel = useBillingAction ? (academyAccess?.status === 'grace_period' ? 'Renew now' : 'Manage billing') : 'See plans'
const primaryActionIcon = useBillingAction ? (academyAccess?.status === 'grace_period' ? 'fa-solid fa-rotate-right' : 'fa-solid fa-sliders') : 'fa-solid fa-arrow-up-right-from-square'
const primaryActionHref = useBillingAction ? academyAccess.billingUrl : pricingUrl
const searchContext = analytics?.search ? {
query: analytics.search.query,
normalizedQuery: analytics.search.normalizedQuery,
@@ -90,34 +180,137 @@ export default function AcademyCoursesIndex({ seo, title, description, items, fe
{ value: 'mixed', label: 'Mixed' },
]
const handlePrimaryAction = () => {
if (!useBillingAction) {
trackUpgradeClick(analytics, { source: 'academy_courses_index_hero_primary' })
}
}
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>
<section className="relative overflow-hidden rounded-[40px] border border-amber-200/12 bg-[linear-gradient(155deg,rgba(251,191,36,0.14),rgba(15,23,42,0.96)_36%,rgba(14,165,233,0.14))] p-5 shadow-[0_32px_96px_rgba(2,6,23,0.32)] md:p-6 xl:p-7">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_18%_14%,rgba(253,230,138,0.14),transparent_24%),linear-gradient(90deg,rgba(255,255,255,0.04)_1px,transparent_1px),linear-gradient(180deg,rgba(255,255,255,0.04)_1px,transparent_1px)] bg-[length:auto,24px_24px,24px_24px] opacity-75" />
<div className="absolute -left-8 top-12 h-36 w-36 rounded-full bg-amber-300/18 blur-3xl" />
<div className="absolute -right-10 bottom-0 h-40 w-40 rounded-full bg-sky-300/14 blur-3xl" />
<div className="relative grid gap-5 xl:grid-cols-[minmax(0,1.1fr)_360px] xl:items-start">
<div className="min-w-0">
<Breadcrumbs items={breadcrumbs} />
<div className="mt-3 flex flex-wrap items-center gap-2">
<span className="rounded-full border border-amber-200/18 bg-amber-300/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-amber-50/90">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-200">Courses</span>
<span className="rounded-full border border-white/10 bg-black/20 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-300">{totalCourses} guided paths</span>
</div>
<div className="mt-4 flex items-start justify-between gap-4">
<div className="max-w-4xl">
<h1 className="max-w-[13ch] text-4xl font-semibold tracking-[-0.05em] text-white md:max-w-[14ch] md:text-[4.2rem] xl:max-w-[15ch] xl:text-[4.5rem]">{title}</h1>
<p className="mt-4 max-w-3xl text-base leading-8 text-slate-200/95">{description}</p>
</div>
<span className="hidden h-20 w-20 shrink-0 items-center justify-center rounded-[26px] border border-white/10 bg-white/[0.04] text-2xl text-amber-50 shadow-[0_18px_40px_rgba(2,6,23,0.22)] md:flex">
<i className="fa-solid fa-route" />
</span>
</div>
<div className="mt-6 grid gap-3 sm:grid-cols-2">
<div className="rounded-[24px] border border-white/10 bg-slate-950/35 px-5 py-4 backdrop-blur-sm">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-amber-100/75">Library</p>
<p className="mt-2 text-lg font-semibold text-white">{totalCourses} guided courses</p>
</div>
<div className="rounded-[24px] border border-white/10 bg-slate-950/35 px-5 py-4 backdrop-blur-sm">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-amber-100/75">Focus</p>
<p className="mt-2 text-lg font-semibold text-white">Sequenced learning + tracked completion</p>
</div>
</div>
<div className="mt-5 flex flex-wrap gap-2.5">
<span className="rounded-full border border-white/10 bg-white/[0.06] px-4 py-2 text-[11px] font-semibold tracking-[0.16em] text-amber-50/90">Structured progression</span>
<span className="rounded-full border border-white/10 bg-white/[0.06] px-4 py-2 text-[11px] font-semibold tracking-[0.16em] text-amber-50/90">Tracked completion</span>
<span className="rounded-full border border-white/10 bg-white/[0.06] px-4 py-2 text-[11px] font-semibold tracking-[0.16em] text-amber-50/90">Reusable lesson paths</span>
</div>
<div className="mt-6 flex flex-wrap gap-3">
<Link href={lessonsUrl || '/academy/lessons'} className="inline-flex items-center gap-2 rounded-full border border-amber-200/26 bg-amber-300/12 px-5 py-3 text-sm font-semibold text-amber-50 transition hover:border-amber-200/36 hover:bg-amber-300/18">
<i className="fa-solid fa-book-open-reader text-xs" />
Browse lessons
</Link>
<Link href={promptLibraryUrl || '/academy/prompts'} className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.06] px-5 py-3 text-sm font-semibold text-white/90 transition hover:bg-white/[0.1]">
<i className="fa-solid fa-wand-magic-sparkles text-xs" />
Prompt library
</Link>
<Link href={primaryActionHref} onClick={handlePrimaryAction} className="inline-flex items-center gap-2 rounded-full border border-sky-200/20 bg-sky-300/10 px-5 py-3 text-sm font-semibold text-sky-50 transition hover:border-sky-200/30 hover:bg-sky-300/16">
<i className={`${primaryActionIcon} text-xs`} />
{primaryActionLabel}
</Link>
</div>
</div>
<div>
<div className="rounded-[28px] border border-white/10 bg-black/20 p-4 shadow-[0_18px_42px_rgba(2,6,23,0.18)] backdrop-blur-sm md:p-5">
<div className="flex items-center justify-between gap-3">
<p className="text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-400">Featured course</p>
{featuredCourse?.difficulty ? <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-white/80">{featuredCourse.difficulty}</span> : null}
</div>
<Link href={featuredCourse?.public_url || '#academy-courses-grid'} className="group mt-4 block overflow-hidden rounded-[24px] border border-white/10 bg-white/[0.04] transition hover:border-amber-200/24 hover:bg-white/[0.06]">
<div className="relative aspect-[16/10] overflow-hidden bg-[linear-gradient(135deg,rgba(251,191,36,0.18),rgba(17,24,39,0.94))]">
{featuredCover ? <img src={featuredCover} 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.78))]" />
<div className="absolute left-4 top-4 flex flex-wrap gap-2">
{featuredCourse?.access_level ? <span className="rounded-full border border-white/10 bg-black/30 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-amber-100">{featuredCourse.access_level}</span> : null}
{featuredCourse?.is_featured ? <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">Spotlight</span> : null}
</div>
</div>
<div className="p-4">
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">{featuredCourse?.subtitle || 'Guided learning path'}</p>
<h3 className="mt-2 text-lg font-semibold tracking-[-0.03em] text-white transition group-hover:text-amber-50">{featuredCourse?.title || 'Explore courses'}</h3>
<p className="mt-2 text-sm leading-7 text-slate-300">{featuredCourse?.excerpt || featuredCourse?.description || 'Open a structured Academy course built from reusable lessons.'}</p>
</div>
</Link>
</div>
</div>
<div className="xl:col-span-2">
<div className="rounded-[28px] border border-sky-300/20 bg-[linear-gradient(135deg,rgba(56,189,248,0.16),rgba(15,23,42,0.82))] p-4 shadow-[0_18px_45px_rgba(2,6,23,0.2)] md:p-5 lg:p-6">
<div className="flex items-start gap-3">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl border border-sky-300/25 bg-sky-300/12 text-sky-100"><i className="fa-solid fa-crown text-sm" /></span>
<div className="min-w-0">
<p className="text-[11px] font-semibold uppercase tracking-[0.2em] text-sky-100/80">{showSignedInAccess ? 'Your Academy access' : 'Upgrade for full access'}</p>
<p className="mt-1 max-w-4xl text-lg font-semibold text-white md:text-[1.85rem] md:leading-[1.45]">{showSignedInAccess ? accessHeading : 'Unlock the full course library, premium lesson paths, and the broader Academy learning track.'}</p>
</div>
</div>
{showSignedInAccess ? (
<div className="mt-4 grid gap-3 md:grid-cols-2 xl:grid-cols-3">
{accessMeta.map((item) => (
<div key={item.label} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 md:px-5 md:py-4">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-sky-100/70">{item.label}</p>
<p className="mt-2 text-sm font-semibold text-white md:text-base">{item.value}</p>
</div>
))}
</div>
) : null}
<div className="mt-4 flex flex-wrap gap-3">
<Link href={primaryActionHref} onClick={handlePrimaryAction} className="inline-flex items-center gap-2 rounded-full border border-sky-200/30 bg-sky-200/15 px-5 py-2.5 text-sm font-semibold text-sky-50 transition hover:bg-sky-200/22">
<i className={`${primaryActionIcon} text-xs`} />
{primaryActionLabel}
</Link>
<Link href={lessonsUrl || '/academy/lessons'} className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.06] px-5 py-2.5 text-sm font-semibold text-white/90 transition hover:bg-white/[0.1]">
<i className="fa-solid fa-book-open-reader text-xs" />
Browse lessons
</Link>
</div>
{academyAccess?.status === 'grace_period' ? <p className="mt-2 text-xs text-sky-100/75">Opens billing account to restore renewal before access ends.</p> : null}
</div>
</div>
<Link href={pricingUrl} onClick={() => trackUpgradeClick(analytics, { source: 'academy_courses_index_hero' })} 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" analytics={analytics} searchContext={searchContext} position={1} />
<div className="grid gap-5">
{featuredCourses.slice(1, 3).map((course, index) => <CourseCard key={course.id} course={course} analytics={analytics} searchContext={searchContext} position={index + 2} />)}
</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"
@@ -137,11 +330,11 @@ export default function AcademyCoursesIndex({ seo, title, description, items, fe
/>
</section>
{(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">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, index) => <CourseCard key={course.id} course={course} analytics={analytics} searchContext={searchContext} position={index + 1} />)}
<section id="academy-courses-grid" className="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
{visibleItems.map((course, index) => <CourseCard key={course.id} course={course} analytics={analytics} searchContext={searchContext} position={index + 1} />)}
</section>
)}
</div>

View File

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

View File

@@ -7,22 +7,110 @@ function academyHref(section, slug) {
return `/academy/${section}/${encodeURIComponent(slug)}`
}
function FeatureCard({ title, description, href, cta }) {
function formatStatValue(value, singular, plural = `${singular}s`) {
const numericValue = Number(value || 0)
return `${numericValue.toLocaleString()} ${numericValue === 1 ? singular : plural}`
}
function FeatureCard({ title, description, href, cta, icon, eyebrow, highlights = [], tags = [], meta, theme }) {
return (
<Link href={href} className="rounded-[28px] border border-white/10 bg-white/[0.04] p-6 transition hover:border-white/20 hover:bg-white/[0.06]">
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-200/80">Academy</p>
<h2 className="mt-3 text-2xl font-semibold tracking-[-0.04em] text-white">{title}</h2>
<p className="mt-3 text-sm leading-7 text-slate-300">{description}</p>
<span className="mt-5 inline-flex rounded-full border border-sky-300/25 bg-sky-300/12 px-4 py-2 text-sm font-semibold text-sky-100">{cta}</span>
<Link href={href} className={`group relative overflow-hidden rounded-[32px] border p-6 shadow-[0_24px_80px_rgba(2,6,23,0.22)] transition hover:-translate-y-1 hover:shadow-[0_30px_95px_rgba(2,6,23,0.32)] ${theme.shell}`}>
<div className={`absolute inset-0 ${theme.backdrop}`} />
<div className={`absolute inset-0 opacity-60 ${theme.pattern}`} />
<div className={`absolute -right-14 top-6 h-32 w-32 rounded-full blur-3xl ${theme.glow}`} />
<div className="relative flex min-h-[290px] flex-col">
<div className="flex items-start justify-between gap-4">
<div>
<p className={`text-[11px] font-semibold uppercase tracking-[0.24em] ${theme.eyebrow}`}>{eyebrow}</p>
<h2 className="mt-4 text-[2rem] font-semibold tracking-[-0.05em] text-white">{title}</h2>
</div>
<span className={`flex h-14 w-14 shrink-0 items-center justify-center rounded-[18px] border text-lg shadow-[0_14px_34px_rgba(2,6,23,0.28)] transition group-hover:scale-105 ${theme.iconWrap}`}>
<i className={icon} />
</span>
</div>
<p className="mt-5 max-w-[34ch] text-sm leading-7 text-slate-200/95">{description}</p>
<div className="mt-6 grid gap-3 sm:grid-cols-2">
{highlights.map((item) => (
<div key={`${title}-${item.label}`} className={`rounded-[22px] border px-4 py-3 backdrop-blur-sm ${theme.highlightCard}`}>
<p className={`text-[10px] font-semibold uppercase tracking-[0.18em] ${theme.highlightLabel}`}>{item.label}</p>
<p className="mt-1 text-sm font-semibold text-white">{item.value}</p>
</div>
))}
</div>
<div className="mt-5 flex flex-wrap gap-2">
{tags.map((tag) => (
<span key={`${title}-${tag}`} className={`rounded-full border px-3 py-1 text-[11px] font-semibold tracking-[0.12em] ${theme.tag}`}>
{tag}
</span>
))}
</div>
<div className="mt-auto flex items-center justify-between gap-4 pt-6">
<span className={`inline-flex rounded-full border px-4 py-2 text-sm font-semibold transition group-hover:translate-x-1 ${theme.cta}`}>{cta}</span>
<span className={`text-right text-[11px] font-semibold uppercase tracking-[0.2em] ${theme.meta}`}>{meta}</span>
</div>
</div>
</Link>
)
}
function FeatureRailCard({ eyebrow, title, description, icon, items = [], emptyText, actionHref = null, actionLabel = null, theme, renderItem }) {
return (
<section className={`relative overflow-hidden rounded-[30px] border p-6 shadow-[0_22px_70px_rgba(2,6,23,0.26)] ${theme.shell}`}>
<div className={`absolute inset-0 ${theme.backdrop}`} />
<div className={`absolute inset-0 opacity-60 ${theme.pattern}`} />
<div className={`absolute right-0 top-0 h-28 w-28 translate-x-8 -translate-y-6 rounded-full blur-3xl ${theme.glow}`} />
<div className="relative">
<div className="flex items-start justify-between gap-4">
<div className="max-w-[30ch]">
<p className={`text-[11px] font-semibold uppercase tracking-[0.24em] ${theme.eyebrow}`}>{eyebrow}</p>
<div className="mt-3 flex items-center gap-3">
<span className={`flex h-11 w-11 items-center justify-center rounded-[16px] border text-sm ${theme.iconWrap}`}>
<i className={icon} />
</span>
<h3 className="text-2xl font-semibold tracking-[-0.04em] text-white">{title}</h3>
</div>
<p className="mt-4 text-sm leading-7 text-slate-200/92">{description}</p>
</div>
{actionHref && actionLabel ? (
<Link href={actionHref} className={`inline-flex shrink-0 rounded-full border px-4 py-2 text-[11px] font-semibold uppercase tracking-[0.18em] transition ${theme.action}`}>
{actionLabel}
</Link>
) : null}
</div>
<div className="mt-6 space-y-3">
{items.length > 0 ? items.map((item, index) => renderItem(item, index)) : (
<div className={`rounded-[22px] border px-4 py-4 text-sm ${theme.empty}`}>
{emptyText}
</div>
)}
</div>
</div>
</section>
)
}
function MetricCard({ label, value, accent }) {
return (
<div className={`rounded-[24px] border px-5 py-5 backdrop-blur-sm ${accent.shell}`}>
<p className={`text-[10px] font-semibold uppercase tracking-[0.2em] ${accent.label}`}>{label}</p>
<p className="mt-3 text-3xl font-semibold tracking-[-0.05em] text-white md:text-[2.4rem]">{value}</p>
</div>
)
}
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]">
<Link href={course.public_url} className="group relative overflow-hidden rounded-[28px] border border-sky-200/12 bg-[linear-gradient(180deg,rgba(15,23,42,0.9),rgba(15,23,42,0.72))] transition hover:-translate-y-1 hover:border-sky-300/24 hover:shadow-[0_24px_72px_rgba(2,6,23,0.3)]">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(125,211,252,0.14),transparent_24%),linear-gradient(135deg,transparent_0%,transparent_48%,rgba(125,211,252,0.05)_48%,rgba(125,211,252,0.05)_52%,transparent_52%,transparent_100%)] opacity-80" />
<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))]" />
@@ -31,17 +119,316 @@ function FeaturedCourseCard({ course }) {
<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">
<div className="relative p-5">
<p className="text-[10px] font-semibold uppercase tracking-[0.2em] text-sky-100/75">Guided course</p>
<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 className="mt-4 flex items-center justify-between gap-3">
<p className="text-xs uppercase tracking-[0.18em] text-slate-500">{course.lessons_count || 0} lessons · {course.estimated_minutes ? `${course.estimated_minutes} min` : 'Flexible duration'}</p>
<span className="inline-flex rounded-full border border-sky-300/20 bg-sky-300/10 px-3 py-1.5 text-[10px] font-semibold uppercase tracking-[0.18em] text-sky-100 transition group-hover:translate-x-1">Open path</span>
</div>
</div>
</Link>
)
}
export default function AcademyIndex({ seo, pricingUrl, links, featureFlags, stats, featuredCourses, featuredLessons, featuredPrompts, featuredChallenges, analytics }) {
function formatAccessDate(value) {
if (!value) {
return null
}
const parsed = new Date(value)
if (Number.isNaN(parsed.getTime())) {
return null
}
return new Intl.DateTimeFormat(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
}).format(parsed)
}
function academyAccessHeading(access) {
switch (access?.status) {
case 'staff_access':
return 'You currently have full staff access to the Academy.'
case 'grace_period':
return `${access.tierLabel} access is still active.`
case 'trialing':
return `${access.tierLabel} trial is active right now.`
case 'active':
return access?.hasPaidAccess ? `${access.tierLabel} access is active.` : 'Your Academy access is active.'
case 'free':
return 'You currently have Free access to the Academy.'
default:
return 'Preview the Academy before you upgrade.'
}
}
function academyAccessMeta(access) {
const items = [
{ label: 'Current tier', value: access?.tierLabel || 'Guest' },
{ label: 'Status', value: access?.statusLabel || 'Preview access only' },
]
const formattedDate = formatAccessDate(access?.expiresAt)
if (formattedDate && access?.dateLabel) {
items.push({ label: access.dateLabel, value: formattedDate })
} else if (access?.renewsAutomatically) {
items.push({ label: 'Billing', value: 'Renews automatically' })
} else if (access?.signedIn && !access?.hasPaidAccess) {
items.push({ label: 'Upgrade', value: 'Creator and Pro unlock premium workflows' })
} else if (!access?.signedIn) {
items.push({ label: 'Upgrade', value: 'Sign in to track access and unlock premium content' })
}
return items
}
export default function AcademyIndex({ seo, pricingUrl, academyAccess = null, links, featureFlags, stats, featuredCourses, featuredLessons, featuredPrompts, featuredChallenges, analytics }) {
useAcademyPageAnalytics(analytics)
const accessHeading = academyAccessHeading(academyAccess)
const accessMeta = academyAccessMeta(academyAccess)
const useBillingAction = academyAccess?.signedIn && academyAccess?.hasPaidAccess && academyAccess?.billingUrl
const accessActionLabel = useBillingAction ? (academyAccess?.status === 'grace_period' ? 'Renew now' : 'Manage billing') : 'See plans'
const accessActionHref = useBillingAction ? academyAccess.billingUrl : pricingUrl
const academySections = [
{
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',
icon: 'fa-solid fa-route',
eyebrow: 'Academy paths',
highlights: [
{ label: 'Library', value: formatStatValue(stats?.courseCount, 'course') },
{ label: 'Includes', value: formatStatValue(stats?.lessonCount, 'lesson') },
],
tags: ['Progress tracked', 'Learning paths', 'Skill ladders'],
meta: 'Structured progression',
theme: {
shell: 'border-sky-300/18 bg-slate-950/40 hover:border-sky-300/30',
backdrop: 'bg-[linear-gradient(145deg,rgba(14,165,233,0.16),rgba(15,23,42,0.95)_42%,rgba(16,185,129,0.18))]',
pattern: 'bg-[radial-gradient(circle_at_top_left,rgba(125,211,252,0.16),transparent_30%),linear-gradient(125deg,transparent_0%,transparent_45%,rgba(125,211,252,0.08)_45%,rgba(125,211,252,0.08)_52%,transparent_52%,transparent_100%)]',
glow: 'bg-sky-300/25',
eyebrow: 'text-sky-100/80',
iconWrap: 'border-sky-200/20 bg-sky-300/12 text-sky-100',
highlightCard: 'border-sky-200/12 bg-slate-950/40',
highlightLabel: 'text-sky-100/75',
tag: 'border-sky-200/12 bg-sky-300/10 text-sky-100',
cta: 'border-sky-300/25 bg-sky-300/12 text-sky-100',
meta: 'text-sky-100/75',
},
},
{
title: 'Lessons',
description: 'Structured tutorials for prompt writing, cleanup workflows, AI ethics, and Skinbase-native publishing habits.',
href: links.lessons,
cta: 'Open lessons',
icon: 'fa-solid fa-book-open-reader',
eyebrow: 'Focused tutorials',
highlights: [
{ label: 'Depth', value: formatStatValue(stats?.lessonCount, 'lesson') },
{ label: 'Coverage', value: 'Prompt craft + workflow cleanup' },
],
tags: ['Short wins', 'Creative habits', 'Practical steps'],
meta: 'Skill-by-skill learning',
theme: {
shell: 'border-amber-300/18 bg-slate-950/40 hover:border-amber-300/30',
backdrop: 'bg-[linear-gradient(160deg,rgba(251,191,36,0.18),rgba(15,23,42,0.95)_40%,rgba(249,115,22,0.14))]',
pattern: 'bg-[radial-gradient(circle_at_top_right,rgba(253,230,138,0.14),transparent_28%),linear-gradient(180deg,transparent_0%,transparent_54%,rgba(251,191,36,0.08)_54%,rgba(251,191,36,0.08)_58%,transparent_58%,transparent_100%)]',
glow: 'bg-amber-300/20',
eyebrow: 'text-amber-100/85',
iconWrap: 'border-amber-200/20 bg-amber-300/12 text-amber-100',
highlightCard: 'border-amber-200/12 bg-slate-950/42',
highlightLabel: 'text-amber-100/75',
tag: 'border-amber-200/12 bg-amber-300/10 text-amber-100',
cta: 'border-amber-300/25 bg-amber-300/12 text-amber-100',
meta: 'text-amber-100/75',
},
},
{
title: 'Prompt Library',
description: 'Discover reusable prompt templates, locked premium previews, and creator-focused visual workflows.',
href: links.prompts,
cta: 'Explore prompts',
icon: 'fa-solid fa-wand-magic-sparkles',
eyebrow: 'Reusable prompt kits',
highlights: [
{ label: 'Templates', value: formatStatValue(stats?.promptCount, 'prompt') },
{ label: 'Use case', value: 'Reusable systems + premium previews' },
],
tags: ['Fast starts', 'Visual workflows', 'Copy + adapt'],
meta: 'High-speed ideation',
theme: {
shell: 'border-rose-300/18 bg-slate-950/40 hover:border-rose-300/30',
backdrop: 'bg-[linear-gradient(150deg,rgba(244,63,94,0.16),rgba(15,23,42,0.95)_38%,rgba(45,212,191,0.16))]',
pattern: 'bg-[radial-gradient(circle_at_20%_15%,rgba(251,113,133,0.16),transparent_24%),linear-gradient(90deg,rgba(255,255,255,0.04)_1px,transparent_1px),linear-gradient(180deg,rgba(255,255,255,0.04)_1px,transparent_1px)] bg-[length:auto,22px_22px,22px_22px]',
glow: 'bg-rose-300/20',
eyebrow: 'text-rose-100/85',
iconWrap: 'border-rose-200/20 bg-rose-300/12 text-rose-100',
highlightCard: 'border-rose-200/12 bg-slate-950/42',
highlightLabel: 'text-rose-100/75',
tag: 'border-rose-200/12 bg-rose-300/10 text-rose-100',
cta: 'border-rose-300/25 bg-rose-300/12 text-rose-100',
meta: 'text-rose-100/75',
},
},
]
const academyFeatureRails = [
{
key: 'lessons',
eyebrow: 'Featured lessons',
title: 'Jump-in tutorials',
description: 'Shorter Academy pieces for specific prompt problems, cleanup workflows, and publishing habits.',
icon: 'fa-solid fa-book-open-reader',
actionHref: links.lessons,
actionLabel: 'All lessons',
items: (featuredLessons || []).slice(0, 3),
emptyText: 'Featured lessons will appear here when the Academy team highlights a new tutorial.',
theme: {
shell: 'border-amber-300/16 bg-slate-950/45',
backdrop: 'bg-[linear-gradient(160deg,rgba(251,191,36,0.15),rgba(15,23,42,0.96)_42%,rgba(249,115,22,0.14))]',
pattern: 'bg-[radial-gradient(circle_at_top_right,rgba(253,230,138,0.12),transparent_24%),linear-gradient(180deg,transparent_0%,transparent_52%,rgba(251,191,36,0.08)_52%,rgba(251,191,36,0.08)_56%,transparent_56%,transparent_100%)]',
glow: 'bg-amber-300/18',
eyebrow: 'text-amber-100/82',
iconWrap: 'border-amber-200/20 bg-amber-300/12 text-amber-100',
action: 'border-amber-300/22 bg-amber-300/10 text-amber-100 hover:border-amber-300/34 hover:bg-amber-300/16',
item: 'border-amber-200/10 bg-slate-950/38 hover:border-amber-200/18 hover:bg-slate-950/52',
itemEyebrow: 'text-amber-100/75',
itemMeta: 'text-amber-100/70',
empty: 'border-amber-200/10 bg-slate-950/30 text-amber-50/80',
},
renderItem: (item, index, theme) => (
<Link key={item.id} href={academyHref('lessons', item.slug)} className={`group block rounded-[22px] border px-4 py-4 transition ${theme.item}`}>
<div className="flex items-start gap-3">
<span className={`mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-full border text-[10px] font-semibold ${theme.iconWrap}`}>{index + 1}</span>
<div className="min-w-0">
<span className={`block text-[10px] font-semibold uppercase tracking-[0.18em] ${theme.itemEyebrow}`}>{item.lesson_label || 'Featured lesson'}</span>
<span className="mt-2 block text-sm font-semibold text-white transition group-hover:text-amber-50">{item.title}</span>
<span className={`mt-2 block text-[11px] font-semibold uppercase tracking-[0.16em] ${theme.itemMeta}`}>Practical tutorial</span>
</div>
</div>
</Link>
),
},
{
key: 'prompts',
eyebrow: 'Featured prompts',
title: 'Reusable prompt packs',
description: 'Template-driven prompt entries designed for fast reuse, remixing, and premium workflow previews.',
icon: 'fa-solid fa-wand-magic-sparkles',
actionHref: links.promptPopular,
actionLabel: 'Top prompts',
items: (featuredPrompts || []).slice(0, 3),
emptyText: 'Featured prompts will appear here when reusable prompt templates are promoted on the homepage.',
theme: {
shell: 'border-rose-300/16 bg-slate-950/45',
backdrop: 'bg-[linear-gradient(155deg,rgba(244,63,94,0.14),rgba(15,23,42,0.96)_40%,rgba(45,212,191,0.14))]',
pattern: 'bg-[radial-gradient(circle_at_20%_18%,rgba(251,113,133,0.14),transparent_24%),linear-gradient(90deg,rgba(255,255,255,0.04)_1px,transparent_1px),linear-gradient(180deg,rgba(255,255,255,0.04)_1px,transparent_1px)] bg-[length:auto,22px_22px,22px_22px]',
glow: 'bg-rose-300/18',
eyebrow: 'text-rose-100/82',
iconWrap: 'border-rose-200/20 bg-rose-300/12 text-rose-100',
action: 'border-rose-300/22 bg-rose-300/10 text-rose-100 hover:border-rose-300/34 hover:bg-rose-300/16',
item: 'border-rose-200/10 bg-slate-950/38 hover:border-rose-200/18 hover:bg-slate-950/52',
itemEyebrow: 'text-rose-100/75',
itemMeta: 'text-rose-100/70',
empty: 'border-rose-200/10 bg-slate-950/30 text-rose-50/80',
},
renderItem: (item, index, theme) => (
<Link key={item.id} href={academyHref('prompts', item.slug)} className={`group block rounded-[22px] border px-4 py-4 transition ${theme.item}`}>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<span className={`block text-[10px] font-semibold uppercase tracking-[0.18em] ${theme.itemEyebrow}`}>Prompt template #{index + 1}</span>
<span className="mt-2 block text-sm font-semibold text-white transition group-hover:text-rose-50">{item.title}</span>
</div>
<span className={`rounded-full border px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] ${theme.iconWrap}`}>Template</span>
</div>
<span className={`mt-3 block text-[11px] font-semibold uppercase tracking-[0.16em] ${theme.itemMeta}`}>Reusable workflow</span>
</Link>
),
},
{
key: 'challenges',
eyebrow: 'Current challenges',
title: 'Build around a brief',
description: 'Academy challenges turn lessons and prompt systems into practical output with a clear creative objective.',
icon: 'fa-solid fa-trophy',
items: (featuredChallenges || []).slice(0, 3),
emptyText: 'Current challenges will appear here when the Academy team launches a new guided brief.',
theme: {
shell: 'border-emerald-300/16 bg-slate-950/45',
backdrop: 'bg-[linear-gradient(155deg,rgba(16,185,129,0.14),rgba(15,23,42,0.96)_42%,rgba(56,189,248,0.12))]',
pattern: 'bg-[radial-gradient(circle_at_top_left,rgba(110,231,183,0.14),transparent_24%),linear-gradient(135deg,transparent_0%,transparent_48%,rgba(16,185,129,0.08)_48%,rgba(16,185,129,0.08)_56%,transparent_56%,transparent_100%)]',
glow: 'bg-emerald-300/18',
eyebrow: 'text-emerald-100/82',
iconWrap: 'border-emerald-200/20 bg-emerald-300/12 text-emerald-100',
action: 'border-emerald-300/22 bg-emerald-300/10 text-emerald-100 hover:border-emerald-300/34 hover:bg-emerald-300/16',
item: 'border-emerald-200/10 bg-slate-950/38 hover:border-emerald-200/18 hover:bg-slate-950/52',
itemEyebrow: 'text-emerald-100/75',
itemMeta: 'text-emerald-100/70',
empty: 'border-emerald-200/10 bg-slate-950/30 text-emerald-50/80',
},
renderItem: (item, index, theme) => (
<Link key={item.id} href={academyHref('challenges', item.slug)} className={`group block rounded-[22px] border px-4 py-4 transition ${theme.item}`}>
<div className="flex items-start gap-3">
<span className={`mt-0.5 flex h-8 min-w-8 items-center justify-center rounded-full border px-2 text-[10px] font-semibold ${theme.iconWrap}`}>#{index + 1}</span>
<div className="min-w-0">
<span className={`block text-[10px] font-semibold uppercase tracking-[0.18em] ${theme.itemEyebrow}`}>Active brief</span>
<span className="mt-2 block text-sm font-semibold text-white transition group-hover:text-emerald-50">{item.title}</span>
<span className={`mt-2 block text-[11px] font-semibold uppercase tracking-[0.16em] ${theme.itemMeta}`}>Apply what you learned</span>
</div>
</div>
</Link>
),
},
]
const handleAccessAction = () => {
if (!useBillingAction) {
trackUpgradeClick(analytics, { source: 'academy_home_hero' })
}
}
const academyMetrics = [
{
key: 'courses',
label: 'Courses',
value: stats?.courseCount || 0,
accent: {
shell: 'border-sky-300/14 bg-sky-300/[0.08]',
label: 'text-sky-100/78',
},
},
{
key: 'lessons',
label: 'Lessons',
value: stats?.lessonCount || 0,
accent: {
shell: 'border-amber-300/14 bg-amber-300/[0.08]',
label: 'text-amber-100/78',
},
},
{
key: 'prompts',
label: 'Prompts',
value: stats?.promptCount || 0,
accent: {
shell: 'border-rose-300/14 bg-rose-300/[0.08]',
label: 'text-rose-100/78',
},
},
{
key: 'challenges',
label: 'Challenges',
value: stats?.challengeCount || 0,
accent: {
shell: 'border-emerald-300/14 bg-emerald-300/[0.08]',
label: 'text-emerald-100/78',
},
},
]
const jsonLd = [{
'@context': 'https://schema.org',
@@ -52,68 +439,115 @@ export default function AcademyIndex({ seo, pricingUrl, links, featureFlags, sta
}]
return (
<main className="min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(251,191,36,0.18),_transparent_24%),radial-gradient(circle_at_bottom_right,_rgba(56,189,248,0.18),_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(251,191,36,0.18),_transparent_24%),radial-gradient(circle_at_bottom_right,_rgba(56,189,248,0.18),_transparent_24%),linear-gradient(180deg,_#0f172a_0%,_#111827_100%)] px-4 py-6 sm:px-6 sm:py-8 lg:px-8 lg:py-10">
<SeoHead seo={seo || {}} title="Skinbase AI Academy" description={seo?.description} jsonLd={jsonLd} />
<div className="mx-auto max-w-[1440px] space-y-8">
<section className="overflow-hidden rounded-[40px] border border-white/10 bg-[linear-gradient(135deg,rgba(15,23,42,0.94),rgba(28,25,23,0.88)),radial-gradient(circle_at_top_right,rgba(251,191,36,0.2),transparent_26%)] p-8 shadow-[0_32px_100px_rgba(2,6,23,0.38)] md:p-10 lg:p-12">
<div className="grid gap-8 xl:grid-cols-[minmax(0,1fr)_360px] xl:items-end">
<div className="mx-auto max-w-[1440px] space-y-6 md:space-y-8 xl:space-y-10">
<section className="overflow-hidden rounded-[40px] border border-white/10 bg-[linear-gradient(135deg,rgba(15,23,42,0.94),rgba(28,25,23,0.88)),radial-gradient(circle_at_top_right,rgba(251,191,36,0.2),transparent_26%)] p-6 shadow-[0_32px_100px_rgba(2,6,23,0.38)] md:p-7 lg:p-8">
<div className="grid gap-5 xl:grid-cols-[minmax(0,1fr)_300px] xl:items-center">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-amber-200/80">Skinbase AI Academy</p>
<h1 className="mt-4 max-w-4xl text-4xl font-semibold tracking-[-0.05em] text-white md:text-5xl xl:text-6xl">Learn how to turn prompts into wallpapers, digital art, skins, covers, and visual worlds.</h1>
<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>
<h1 className="mt-3 max-w-[15ch] text-4xl font-semibold tracking-[-0.05em] text-white md:max-w-[16ch] md:text-5xl xl:max-w-[19ch] xl:text-[3.2rem]">Learn how to turn prompts into wallpapers, digital art, skins, covers, and visual worlds.</h1>
<p className="mt-4 max-w-3xl text-base leading-7 text-slate-300 md:text-lg md:leading-8">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">
<div className="mt-5 flex flex-wrap gap-2.5">
<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={links.promptPopular} className="rounded-full border border-rose-300/25 bg-rose-300/12 px-5 py-3 text-sm font-semibold text-rose-100 transition hover:border-rose-300/40 hover:bg-rose-300/18">Top prompts</Link>
<Link href={pricingUrl} onClick={() => trackUpgradeClick(analytics, { source: 'academy_home_hero' })} 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>
</div>
</div>
<div className="rounded-[30px] border border-white/10 bg-black/20 p-6">
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-300">Launch status</p>
<div className="mt-4 space-y-3 text-sm text-slate-300">
<div className="flex items-center justify-between rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3"><span>Challenges</span><span>{featureFlags?.challengesEnabled ? 'Enabled' : 'Disabled'}</span></div>
<div className="flex items-center justify-between rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3"><span>Badges</span><span>{featureFlags?.badgesEnabled ? 'Enabled' : 'Disabled'}</span></div>
<div className="flex items-center justify-between rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3"><span>Payments</span><span>{featureFlags?.paymentsEnabled ? 'Preview only' : 'Disabled'}</span></div>
<div className="rounded-[30px] border border-sky-300/20 bg-[linear-gradient(135deg,rgba(56,189,248,0.16),rgba(15,23,42,0.82))] p-4 md:p-5">
<div className="flex items-start gap-3">
<span className="flex h-11 w-11 items-center justify-center rounded-2xl border border-sky-300/25 bg-sky-300/12 text-sky-100"><i className="fa-solid fa-crown text-sm" /></span>
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-100/80">Your Academy access</p>
<p className="mt-1 text-lg font-semibold text-white">{accessHeading}</p>
</div>
</div>
<div className="mt-4 grid gap-3">
{accessMeta.map((item) => (
<div key={item.label} className="flex items-center justify-between rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-slate-200">
<span className="text-slate-300">{item.label}</span>
<span className="font-semibold text-white">{item.value}</span>
</div>
))}
</div>
<Link href={accessActionHref} onClick={handleAccessAction} className="mt-4 inline-flex 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">{accessActionLabel}</Link>
{academyAccess?.status === 'grace_period' ? <p className="mt-2 text-xs text-sky-100/75">Opens billing account to restore renewal before access ends.</p> : null}
</div>
</div>
</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" />
<section className="space-y-4 md:space-y-5">
<div className="flex flex-col gap-3 rounded-[30px] border border-white/10 bg-[linear-gradient(135deg,rgba(15,23,42,0.6),rgba(15,23,42,0.22))] px-5 py-4 shadow-[0_16px_48px_rgba(2,6,23,0.18)] md:flex-row md:items-end md:justify-between md:px-6">
<div className="max-w-2xl">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-100/75">Choose your Academy lane</p>
<h2 className="mt-2 text-2xl font-semibold tracking-[-0.045em] text-white md:text-[2rem]">Start with the format that matches how you learn.</h2>
</div>
<div className="flex items-center gap-3 text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-400">
<span className="h-px flex-1 bg-gradient-to-r from-transparent via-sky-300/35 to-transparent md:min-w-24" />
<span>Courses, lessons, prompts</span>
</div>
</div>
<div className="grid gap-5 lg:grid-cols-3">
{academySections.map((section) => (
<FeatureCard key={section.title} {...section} />
))}
</div>
</section>
<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 className="overflow-hidden rounded-[34px] border border-white/10 bg-[linear-gradient(135deg,rgba(15,23,42,0.82),rgba(15,23,42,0.58)),radial-gradient(circle_at_top_left,rgba(56,189,248,0.12),transparent_30%),radial-gradient(circle_at_bottom_right,rgba(251,191,36,0.12),transparent_28%)] p-3 shadow-[0_18px_56px_rgba(2,6,23,0.24)] sm:p-4">
<div className="grid gap-3 lg:grid-cols-4">
{academyMetrics.map((metric) => (
<MetricCard key={metric.key} label={metric.label} value={metric.value} accent={metric.accent} />
))}
</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>
<section className="relative overflow-hidden rounded-[36px] border border-sky-200/10 bg-[linear-gradient(135deg,rgba(15,23,42,0.82),rgba(3,7,18,0.94)),radial-gradient(circle_at_top_left,rgba(14,165,233,0.14),transparent_28%),radial-gradient(circle_at_bottom_right,rgba(16,185,129,0.1),transparent_30%)] p-5 shadow-[0_24px_70px_rgba(2,6,23,0.26)] md:p-6 lg:p-7">
<div className="absolute inset-0 bg-[linear-gradient(90deg,rgba(255,255,255,0.03)_1px,transparent_1px),linear-gradient(180deg,rgba(255,255,255,0.03)_1px,transparent_1px)] bg-[length:28px_28px] opacity-25" />
<div className="relative space-y-4 md:space-y-5">
<div className="flex flex-wrap items-end justify-between gap-4 rounded-[28px] border border-white/10 bg-black/15 px-5 py-4 backdrop-blur-sm">
<div className="max-w-2xl">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-100/78">Featured courses</p>
<h2 className="mt-2 text-3xl font-semibold tracking-[-0.045em] text-white">Guided Academy paths</h2>
<p className="mt-3 max-w-[54ch] text-sm leading-7 text-slate-300">Longer learning paths for people who want a clearer start-to-finish route instead of individual tutorials or standalone prompt templates.</p>
</div>
<div className="flex items-center gap-3">
<div className="rounded-full border border-sky-300/18 bg-sky-300/10 px-4 py-2 text-[11px] font-semibold uppercase tracking-[0.2em] text-sky-100/85">
{formatStatValue(featuredCourses.length, 'featured path')}
</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 transition hover:border-white/20 hover:bg-white/[0.08]">All courses</Link>
</div>
</div>
<div className="grid gap-5 xl:grid-cols-3">
{featuredCourses.slice(0, 3).map((course) => <FeaturedCourseCard key={course.id} course={course} />)}
</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"><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 className="grid gap-4 xl:grid-cols-3 xl:gap-5">
{academyFeatureRails.map((rail) => (
<FeatureRailCard
key={rail.key}
eyebrow={rail.eyebrow}
title={rail.title}
description={rail.description}
icon={rail.icon}
items={rail.items}
emptyText={rail.emptyText}
actionHref={rail.actionHref}
actionLabel={rail.actionLabel}
theme={rail.theme}
renderItem={(item, index) => rail.renderItem(item, index, rail.theme)}
/>
))}
</section>
</div>
</main>

View File

@@ -8,6 +8,31 @@ function academyHref(section, slug) {
return `/academy/${section}/${encodeURIComponent(slug)}`
}
function Breadcrumbs({ items = [] }) {
if (!items.length) {
return null
}
return (
<nav aria-label="Breadcrumb" className="flex flex-wrap items-center gap-2 text-xs font-semibold uppercase tracking-[0.18em] text-slate-400">
{items.map((item, index) => {
const isLast = index === items.length - 1
return (
<React.Fragment key={`${item.label}-${index}`}>
{isLast ? (
<span className="text-white/80">{item.label}</span>
) : (
<Link href={item.href} className="transition hover:text-white">{item.label}</Link>
)}
{!isLast ? <span className="text-slate-600">/</span> : null}
</React.Fragment>
)
})}
</nav>
)
}
function QueryFilters({ pageType, filters, categories }) {
if (pageType !== 'lessons' && pageType !== 'prompts') {
return null
@@ -88,79 +113,455 @@ function promptPreviewAsset(item) {
}
}
function PromptLibraryHero({ title, description, items, pricingUrl, totalCount }) {
const featuredImages = (items || [])
.map((item) => promptPreviewAsset(item))
.filter(Boolean)
.slice(0, 3)
function lessonPreviewAsset(item) {
const src = item?.cover_image_url || item?.article_cover_image_url || item?.cover_image || item?.article_cover_image || ''
const primaryImage = featuredImages[0] || null
const supportingImages = featuredImages.slice(1, 3)
if (!src) {
return null
}
return { src }
}
function PromptSpotlightCard({ item }) {
const preview = promptPreviewAsset(item)
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">
<Link href={academyHref('prompts', item.slug)} className="group rounded-[28px] border border-white/10 bg-white/[0.04] p-4 transition hover:border-sky-300/25 hover:bg-white/[0.06]">
<div className="grid gap-4 sm:grid-cols-[104px_minmax(0,1fr)] sm:items-center">
<div className="overflow-hidden rounded-[22px] border border-white/10 bg-[linear-gradient(135deg,rgba(56,189,248,0.18),rgba(17,24,39,0.94))] aspect-square">
{preview ? <img src={preview.src} srcSet={preview.srcSet || undefined} sizes="104px" alt="" aria-hidden="true" className="h-full w-full object-cover transition duration-500 group-hover:scale-[1.04]" /> : null}
</div>
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<span className="rounded-full border border-sky-300/20 bg-sky-300/10 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-sky-100">{item?.spotlight?.eyebrow || 'Prompt pick'}</span>
{item?.difficulty ? <span className="rounded-full border border-white/10 bg-black/20 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-300">{item.difficulty}</span> : null}
</div>
<h3 className="mt-3 text-lg font-semibold tracking-[-0.03em] text-white transition group-hover:text-sky-100">{item.title}</h3>
<p className="mt-2 line-clamp-2 text-sm leading-6 text-slate-300">{item.excerpt || item.prompt_preview || item.description || 'Reusable prompt template.'}</p>
<div className="mt-3 flex flex-wrap items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">
<span>{item?.category?.name || 'Academy'}</span>
{item?.tags?.[0] ? <span>{item.tags[0]}</span> : null}
</div>
</div>
</div>
</Link>
)
}
function PromptDiscoverySection({ id, title, description, items = [], href, ctaLabel }) {
if (!items.length) {
return null
}
return (
<section id={id} className="rounded-[34px] border border-white/10 bg-black/20 p-6 md:p-7">
<div className="flex flex-wrap items-end justify-between gap-4">
<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>
<p className="text-[11px] font-semibold uppercase tracking-[0.2em] text-sky-200/80">Prompt discovery</p>
<h2 className="mt-2 text-2xl font-semibold tracking-[-0.04em] text-white md:text-3xl">{title}</h2>
<p className="mt-3 max-w-2xl text-sm leading-7 text-slate-300">{description}</p>
</div>
{href && ctaLabel ? <Link href={href} className="rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-xs font-semibold uppercase tracking-[0.16em] text-white transition hover:bg-white/[0.09]">{ctaLabel}</Link> : null}
</div>
<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 className="mt-6 grid gap-4 xl:grid-cols-2">
{items.map((item) => <PromptSpotlightCard key={`spotlight-${item.id}`} item={item} />)}
</div>
</section>
)
}
function PopularPromptPeriodTabs({ currentPeriod, periods = [] }) {
if (!periods.length) {
return null
}
return (
<div className="mt-5 flex flex-wrap gap-3">
{periods.map((period) => (
<Link
key={period.value}
href={period.href}
className={`rounded-2xl border px-4 py-3 text-left transition ${period.active ? 'border-sky-200/35 bg-sky-200/15 text-sky-50' : 'border-white/10 bg-white/[0.04] text-white hover:bg-white/[0.08]'}`}
>
<span className="block text-[11px] font-semibold uppercase tracking-[0.18em]">{period.label}</span>
<span className="mt-1 block text-xs leading-5 text-inherit/80">{period.description}</span>
</Link>
))}
{currentPeriod?.description ? <div className="flex items-center rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-xs text-slate-300">{currentPeriod.description}</div> : null}
</div>
)
}
function formatAccessDate(value) {
if (!value) {
return null
}
const parsed = new Date(value)
if (Number.isNaN(parsed.getTime())) {
return null
}
return new Intl.DateTimeFormat(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
}).format(parsed)
}
function academyAccessHeading(access) {
switch (access?.status) {
case 'staff_access':
return 'You currently have full staff access to the Academy.'
case 'grace_period':
return `${access.tierLabel} access is still active.`
case 'trialing':
return `${access.tierLabel} trial is active right now.`
case 'active':
return access?.hasPaidAccess ? `${access.tierLabel} access is active.` : 'Your Academy access is active.'
case 'free':
return 'You currently have Free access to the Academy.'
default:
return null
}
}
function academyAccessMeta(access) {
if (!access?.signedIn) {
return []
}
const items = [
{ label: 'Current tier', value: access?.tierLabel || 'Free' },
{ label: 'Status', value: access?.statusLabel || 'Free access' },
]
const formattedDate = formatAccessDate(access?.expiresAt)
if (formattedDate && access?.dateLabel) {
items.push({ label: access.dateLabel, value: formattedDate })
} else if (access?.renewsAutomatically) {
items.push({ label: 'Billing', value: 'Renews automatically' })
} else if (!access?.hasPaidAccess) {
items.push({ label: 'Upgrade', value: 'Creator and Pro unlock premium prompts' })
}
return items
}
function PromptLibraryHero({ promptView = 'library', title, description, items, pricingUrl, coursesUrl, packsUrl, promptPopularUrl, promptLibraryUrl, popularPeriod, popularPeriods = [], totalCount, analytics, hasPopularSection, academyAccess = null }) {
const isPopularView = promptView === 'popular'
const statLabel = isPopularView ? `ranked prompts ${currentPeriodStatSuffix(popularPeriod)}` : 'prompts available'
const showSignedInAccess = Boolean(academyAccess?.signedIn)
const accessHeading = academyAccessHeading(academyAccess)
const accessMeta = academyAccessMeta(academyAccess)
const useBillingAction = showSignedInAccess && academyAccess?.hasPaidAccess && academyAccess?.billingUrl
const primaryActionLabel = useBillingAction ? (academyAccess?.status === 'grace_period' ? 'Renew now' : 'Manage billing') : 'Upgrade now'
const primaryActionIcon = useBillingAction ? (academyAccess?.status === 'grace_period' ? 'fa-solid fa-rotate-right' : 'fa-solid fa-sliders') : 'fa-solid fa-arrow-up-right-from-square'
const primaryActionHref = useBillingAction ? academyAccess.billingUrl : pricingUrl
const secondaryAction = isPopularView
? { href: promptLibraryUrl, label: 'Browse full library', icon: 'fa-solid fa-grid-2' }
: (hasPopularSection
? { href: promptPopularUrl, label: 'Top prompts', icon: 'fa-solid fa-fire' }
: { href: coursesUrl, label: 'Explore courses', icon: 'fa-solid fa-graduation-cap' })
const heroHighlights = [
{
label: isPopularView ? 'Ranking window' : 'Templates',
value: isPopularView ? `${totalCount || 0} ranked prompts` : `${totalCount || 0} prompts`,
},
{
label: 'Use case',
value: isPopularView ? 'High-performing systems + trend tracking' : 'Reusable systems + premium previews',
},
]
const heroTags = isPopularView
? ['Momentum picks', 'Copy trends', 'Compare windows']
: ['Fast starts', 'Visual workflows', 'Copy + adapt']
const handlePrimaryAction = () => {
if (!useBillingAction) {
trackUpgradeClick(analytics, { source: 'prompts_library_hero_primary' })
}
}
return (
<section className="relative overflow-hidden rounded-[40px] border border-rose-200/12 bg-[linear-gradient(150deg,rgba(244,63,94,0.14),rgba(15,23,42,0.96)_36%,rgba(45,212,191,0.14))] p-5 shadow-[0_32px_96px_rgba(2,6,23,0.32)] md:p-6 xl:p-7">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_18%_14%,rgba(251,113,133,0.15),transparent_24%),linear-gradient(90deg,rgba(255,255,255,0.04)_1px,transparent_1px),linear-gradient(180deg,rgba(255,255,255,0.04)_1px,transparent_1px)] bg-[length:auto,24px_24px,24px_24px] opacity-75" />
<div className="absolute -left-8 top-12 h-36 w-36 rounded-full bg-rose-300/18 blur-3xl" />
<div className="absolute -right-10 bottom-0 h-40 w-40 rounded-full bg-cyan-300/14 blur-3xl" />
<div className="relative grid gap-5 xl:grid-cols-[minmax(0,1.1fr)_360px] xl:items-start">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<span className="rounded-full border border-rose-200/18 bg-rose-300/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-rose-50/90">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-200">{isPopularView ? 'Popular prompts' : 'Prompt Library'}</span>
<span className="rounded-full border border-white/10 bg-black/20 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-300">{totalCount || 0} {statLabel}</span>
</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">{totalCount || 0} prompts available</span>
<div className="mt-4 flex items-start justify-between gap-4">
<div className="max-w-4xl">
<h1 className="max-w-[13ch] text-4xl font-semibold tracking-[-0.05em] text-white md:max-w-[14ch] md:text-[4.2rem] xl:max-w-[15ch] xl:text-[4.5rem]">{title}</h1>
<p className="mt-4 max-w-3xl text-base leading-8 text-slate-200/95">{description}</p>
</div>
<span className="hidden h-20 w-20 shrink-0 items-center justify-center rounded-[26px] border border-white/10 bg-white/[0.04] text-2xl text-rose-50 shadow-[0_18px_40px_rgba(2,6,23,0.22)] md:flex">
<i className="fa-solid fa-wand-magic-sparkles" />
</span>
</div>
{isPopularView ? <div className="mt-5"><PopularPromptPeriodTabs currentPeriod={popularPeriod} periods={popularPeriods} /></div> : null}
<div className="mt-6 grid gap-3 sm:grid-cols-2">
{heroHighlights.map((item) => (
<div key={item.label} className="rounded-[24px] border border-white/10 bg-slate-950/35 px-5 py-4 backdrop-blur-sm">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-rose-100/75">{item.label}</p>
<p className="mt-2 text-lg font-semibold leading-8 text-white">{item.value}</p>
</div>
))}
</div>
<div className="mt-5 flex flex-wrap gap-2.5">
{heroTags.map((tag) => (
<span key={tag} className="rounded-full border border-white/10 bg-white/[0.06] px-4 py-2 text-[11px] font-semibold tracking-[0.16em] text-rose-50/90">
{tag}
</span>
))}
</div>
<div className="mt-6 flex flex-wrap gap-3">
<Link href={primaryActionHref} onClick={handlePrimaryAction} className="inline-flex items-center gap-2 rounded-full border border-rose-200/26 bg-rose-300/12 px-5 py-3 text-sm font-semibold text-rose-50 transition hover:border-rose-200/36 hover:bg-rose-300/18">
<i className={`${primaryActionIcon} text-xs`} />
{primaryActionLabel}
</Link>
<Link href={secondaryAction.href} className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.06] px-5 py-3 text-sm font-semibold text-white/90 transition hover:bg-white/[0.1]">
<i className={`${secondaryAction.icon} text-xs`} />
{secondaryAction.label}
</Link>
<Link href={packsUrl} className="inline-flex items-center gap-2 rounded-full border border-cyan-200/20 bg-cyan-300/10 px-5 py-3 text-sm font-semibold text-cyan-50 transition hover:border-cyan-200/30 hover:bg-cyan-300/16">
<i className="fa-solid fa-box-open text-xs" />
See prompt packs
</Link>
</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.src} srcSet={primaryImage.srcSet || undefined} sizes="(max-width: 1279px) calc(100vw - 4rem), 420px" 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.src}-${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.src} srcSet={image.srcSet || undefined} sizes="(max-width: 1279px) calc(50vw - 2rem), 200px" 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 className="rounded-[28px] border border-white/10 bg-black/20 p-4 shadow-[0_18px_42px_rgba(2,6,23,0.18)] backdrop-blur-sm md:p-5">
<div className="flex items-center justify-between gap-3">
<p className="text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-400">Quick routes</p>
<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-white/80">{totalCount || 0} total</span>
</div>
)}
<div className="mt-3 grid gap-3">
<Link href={isPopularView ? promptLibraryUrl : coursesUrl} className="flex items-center justify-between rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-[0.9rem] text-sm font-semibold text-white transition hover:bg-white/[0.08]">
<span>{isPopularView ? 'Browse full prompt library' : 'Browse Academy courses'}</span>
<i className="fa-solid fa-arrow-right text-xs text-slate-400" />
</Link>
<Link href={packsUrl} className="flex items-center justify-between rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-[0.9rem] text-sm font-semibold text-white transition hover:bg-white/[0.08]">
<span>See prompt packs</span>
<i className="fa-solid fa-box-open text-xs text-slate-400" />
</Link>
{isPopularView ? (
<Link href={coursesUrl} className="flex items-center justify-between rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-[0.9rem] text-sm font-semibold text-white transition hover:bg-white/[0.08]">
<span>Explore Academy courses</span>
<i className="fa-solid fa-graduation-cap text-xs text-slate-400" />
</Link>
) : hasPopularSection ? (
<Link href={promptPopularUrl} className="flex items-center justify-between rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-[0.9rem] text-sm font-semibold text-white transition hover:bg-white/[0.08]">
<span>Open top prompts page</span>
<i className="fa-solid fa-fire text-xs text-slate-400" />
</Link>
) : null}
<div className="rounded-2xl border border-dashed border-white/10 bg-white/[0.03] px-4 py-[0.9rem] text-sm font-semibold text-white/85">{isPopularView ? 'Use the period tabs to compare momentum windows.' : 'Jump straight into packs, courses, or ranked prompts.'}</div>
</div>
</div>
</div>
<div className="xl:col-span-2">
<div className="rounded-[28px] border border-sky-300/20 bg-[linear-gradient(135deg,rgba(56,189,248,0.16),rgba(15,23,42,0.82))] p-4 shadow-[0_18px_45px_rgba(2,6,23,0.2)] md:p-5 lg:p-6">
<div className="flex items-start gap-3">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl border border-sky-300/25 bg-sky-300/12 text-sky-100"><i className="fa-solid fa-crown text-sm" /></span>
<div className="min-w-0">
<p className="text-[11px] font-semibold uppercase tracking-[0.2em] text-sky-100/80">{showSignedInAccess ? 'Your Academy access' : (isPopularView ? 'Turn rankings into results' : 'Upgrade for full access')}</p>
<p className="mt-1 max-w-4xl text-lg font-semibold text-white md:text-[1.85rem] md:leading-[1.45]">{showSignedInAccess ? accessHeading : (isPopularView ? 'Open the highest-performing prompts, then unlock the full text, helper prompts, variants, and premium workflows.' : 'Unlock full prompt text, helper prompts, variants, and premium workflows.')}</p>
</div>
</div>
{showSignedInAccess ? (
<div className="mt-4 grid gap-3 md:grid-cols-2 xl:grid-cols-3">
{accessMeta.map((item) => (
<div key={item.label} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 md:px-5 md:py-4">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-sky-100/70">{item.label}</p>
<p className="mt-2 text-sm font-semibold text-white md:text-base">{item.value}</p>
</div>
))}
</div>
) : null}
<div className="mt-4 flex flex-wrap gap-3">
<Link href={primaryActionHref} onClick={handlePrimaryAction} className="inline-flex items-center gap-2 rounded-full border border-sky-200/30 bg-sky-200/15 px-5 py-2.5 text-sm font-semibold text-sky-50 transition hover:bg-sky-200/22">
<i className={`${primaryActionIcon} text-xs`} />
{primaryActionLabel}
</Link>
<Link href={secondaryAction.href} className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.06] px-5 py-2.5 text-sm font-semibold text-white/90 transition hover:bg-white/[0.1]">
<i className={`${secondaryAction.icon} text-xs`} />
{secondaryAction.label}
</Link>
</div>
{academyAccess?.status === 'grace_period' ? <p className="mt-2 text-xs text-sky-100/75">Opens billing account to restore renewal before access ends.</p> : null}
</div>
</div>
</div>
</section>
)
}
function LessonsLibraryHero({ title, description, items = [], totalCount, pricingUrl, coursesUrl, promptLibraryUrl, academyAccess = null, analytics }) {
const featuredLesson = items.find((item) => lessonPreviewAsset(item)) || items[0] || null
const featuredPreview = lessonPreviewAsset(featuredLesson)
const showSignedInAccess = Boolean(academyAccess?.signedIn)
const accessHeading = academyAccessHeading(academyAccess)
const accessMeta = academyAccessMeta(academyAccess)
const useBillingAction = showSignedInAccess && academyAccess?.hasPaidAccess && academyAccess?.billingUrl
const primaryActionLabel = useBillingAction ? (academyAccess?.status === 'grace_period' ? 'Renew now' : 'Manage billing') : 'See plans'
const primaryActionIcon = useBillingAction ? (academyAccess?.status === 'grace_period' ? 'fa-solid fa-rotate-right' : 'fa-solid fa-sliders') : 'fa-solid fa-arrow-up-right-from-square'
const primaryActionHref = useBillingAction ? academyAccess.billingUrl : pricingUrl
const handlePrimaryAction = () => {
if (!useBillingAction) {
trackUpgradeClick(analytics, { source: 'lessons_library_hero_primary' })
}
}
return (
<section className="relative overflow-hidden rounded-[40px] border border-amber-200/12 bg-[linear-gradient(155deg,rgba(251,191,36,0.14),rgba(15,23,42,0.96)_36%,rgba(14,165,233,0.14))] p-5 shadow-[0_32px_96px_rgba(2,6,23,0.32)] md:p-6 xl:p-7">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_18%_14%,rgba(253,230,138,0.14),transparent_24%),linear-gradient(90deg,rgba(255,255,255,0.04)_1px,transparent_1px),linear-gradient(180deg,rgba(255,255,255,0.04)_1px,transparent_1px)] bg-[length:auto,24px_24px,24px_24px] opacity-75" />
<div className="absolute -left-8 top-12 h-36 w-36 rounded-full bg-amber-300/18 blur-3xl" />
<div className="absolute -right-10 bottom-0 h-40 w-40 rounded-full bg-sky-300/14 blur-3xl" />
<div className="relative grid gap-5 xl:grid-cols-[minmax(0,1.1fr)_360px] xl:items-start">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<span className="rounded-full border border-amber-200/18 bg-amber-300/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-amber-50/90">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-200">Lessons</span>
<span className="rounded-full border border-white/10 bg-black/20 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-300">{totalCount || 0} tutorials</span>
</div>
<div className="mt-4 flex items-start justify-between gap-4">
<div className="max-w-4xl">
<h1 className="max-w-[13ch] text-4xl font-semibold tracking-[-0.05em] text-white md:max-w-[14ch] md:text-[4.2rem] xl:max-w-[15ch] xl:text-[4.5rem]">{title}</h1>
<p className="mt-4 max-w-3xl text-base leading-8 text-slate-200/95">{description}</p>
</div>
<span className="hidden h-20 w-20 shrink-0 items-center justify-center rounded-[26px] border border-white/10 bg-white/[0.04] text-2xl text-amber-50 shadow-[0_18px_40px_rgba(2,6,23,0.22)] md:flex">
<i className="fa-solid fa-book-open-reader" />
</span>
</div>
<div className="mt-6 grid gap-3 sm:grid-cols-2">
<div className="rounded-[24px] border border-white/10 bg-slate-950/35 px-5 py-4 backdrop-blur-sm">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-amber-100/75">Library</p>
<p className="mt-2 text-lg font-semibold leading-8 text-white">{totalCount || 0} structured lessons</p>
</div>
<div className="rounded-[24px] border border-white/10 bg-slate-950/35 px-5 py-4 backdrop-blur-sm">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-amber-100/75">Focus</p>
<p className="mt-2 text-lg font-semibold leading-8 text-white">Prompt craft + workflow cleanup</p>
</div>
</div>
<div className="mt-5 flex flex-wrap gap-2.5">
<span className="rounded-full border border-white/10 bg-white/[0.06] px-4 py-2 text-[11px] font-semibold tracking-[0.16em] text-amber-50/90">Short wins</span>
<span className="rounded-full border border-white/10 bg-white/[0.06] px-4 py-2 text-[11px] font-semibold tracking-[0.16em] text-amber-50/90">Creative habits</span>
<span className="rounded-full border border-white/10 bg-white/[0.06] px-4 py-2 text-[11px] font-semibold tracking-[0.16em] text-amber-50/90">Practical steps</span>
</div>
<div className="mt-6 flex flex-wrap gap-3">
<Link href={coursesUrl} className="inline-flex items-center gap-2 rounded-full border border-amber-200/26 bg-amber-300/12 px-5 py-3 text-sm font-semibold text-amber-50 transition hover:border-amber-200/36 hover:bg-amber-300/18">
<i className="fa-solid fa-route text-xs" />
Browse courses
</Link>
<Link href={promptLibraryUrl || '/academy/prompts'} className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.06] px-5 py-3 text-sm font-semibold text-white/90 transition hover:bg-white/[0.1]">
<i className="fa-solid fa-wand-magic-sparkles text-xs" />
Prompt library
</Link>
<Link href={primaryActionHref} onClick={handlePrimaryAction} className="inline-flex items-center gap-2 rounded-full border border-sky-200/20 bg-sky-300/10 px-5 py-3 text-sm font-semibold text-sky-50 transition hover:border-sky-200/30 hover:bg-sky-300/16">
<i className={`${primaryActionIcon} text-xs`} />
{primaryActionLabel}
</Link>
</div>
</div>
<div>
<div className="rounded-[28px] border border-white/10 bg-black/20 p-4 shadow-[0_18px_42px_rgba(2,6,23,0.18)] backdrop-blur-sm md:p-5">
<div className="flex items-center justify-between gap-3">
<p className="text-[11px] font-semibold uppercase tracking-[0.2em] text-slate-400">Latest lesson</p>
{featuredLesson?.difficulty ? <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-white/80">{featuredLesson.difficulty}</span> : null}
</div>
<Link href={featuredLesson ? academyHref('lessons', featuredLesson.slug) : coursesUrl} className="group mt-4 block overflow-hidden rounded-[24px] border border-white/10 bg-white/[0.04] transition hover:border-amber-200/24 hover:bg-white/[0.06]">
<div className="relative aspect-[16/10] overflow-hidden bg-[linear-gradient(135deg,rgba(251,191,36,0.18),rgba(17,24,39,0.94))]">
{featuredPreview ? <img src={featuredPreview.src} 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.78))]" />
<div className="absolute left-4 top-4 flex flex-wrap gap-2">
{featuredLesson?.formatted_lesson_number ? <span className="rounded-full border border-white/10 bg-black/30 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-amber-100">{featuredLesson.formatted_lesson_number}</span> : null}
{featuredLesson ? <LockBadge item={featuredLesson} /> : null}
</div>
</div>
<div className="p-4">
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">{String(featuredLesson?.series_name || featuredLesson?.category?.name || 'Academy lesson').trim()}</p>
<h3 className="mt-2 text-lg font-semibold tracking-[-0.03em] text-white transition group-hover:text-amber-50">{featuredLesson?.title || 'Explore lessons'}</h3>
<p className="mt-2 text-sm leading-7 text-slate-300">{featuredLesson?.excerpt || featuredLesson?.content_preview || featuredLesson?.description || 'Open a practical Academy lesson.'}</p>
</div>
</Link>
</div>
</div>
<div className="xl:col-span-2">
<div className="rounded-[28px] border border-sky-300/20 bg-[linear-gradient(135deg,rgba(56,189,248,0.16),rgba(15,23,42,0.82))] p-4 shadow-[0_18px_45px_rgba(2,6,23,0.2)] md:p-5 lg:p-6">
<div className="flex items-start gap-3">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl border border-sky-300/25 bg-sky-300/12 text-sky-100"><i className="fa-solid fa-crown text-sm" /></span>
<div className="min-w-0">
<p className="text-[11px] font-semibold uppercase tracking-[0.2em] text-sky-100/80">{showSignedInAccess ? 'Your Academy access' : 'Upgrade for full access'}</p>
<p className="mt-1 max-w-4xl text-lg font-semibold text-white md:text-[1.85rem] md:leading-[1.45]">{showSignedInAccess ? accessHeading : 'Unlock the full lesson library, premium workflows, and the broader Academy learning track.'}</p>
</div>
</div>
{showSignedInAccess ? (
<div className="mt-4 grid gap-3 md:grid-cols-2 xl:grid-cols-3">
{accessMeta.map((item) => (
<div key={item.label} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 md:px-5 md:py-4">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-sky-100/70">{item.label}</p>
<p className="mt-2 text-sm font-semibold text-white md:text-base">{item.value}</p>
</div>
))}
</div>
) : null}
<div className="mt-4 flex flex-wrap gap-3">
<Link href={primaryActionHref} onClick={handlePrimaryAction} className="inline-flex items-center gap-2 rounded-full border border-sky-200/30 bg-sky-200/15 px-5 py-2.5 text-sm font-semibold text-sky-50 transition hover:bg-sky-200/22">
<i className={`${primaryActionIcon} text-xs`} />
{primaryActionLabel}
</Link>
<Link href={coursesUrl} className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.06] px-5 py-2.5 text-sm font-semibold text-white/90 transition hover:bg-white/[0.1]">
<i className="fa-solid fa-route text-xs" />
Browse courses
</Link>
</div>
{academyAccess?.status === 'grace_period' ? <p className="mt-2 text-xs text-sky-100/75">Opens billing account to restore renewal before access ends.</p> : null}
</div>
</div>
</div>
</section>
)
}
function currentPeriodStatSuffix(popularPeriod) {
if (!popularPeriod?.label) {
return 'this month'
}
return popularPeriod.label === '30 days' ? 'this month' : `for ${popularPeriod.label.toLowerCase()}`
}
function AcademyCard({ pageType, item, analytics, searchContext, position }) {
const lessonSeries = String(item?.series_name || '').trim()
const promptPreviewImage = item?.preview_image_thumb || item?.preview_image || ''
const promptPreviewSrcSet = item?.preview_image_srcset || ''
const lessonPreview = lessonPreviewAsset(item)
const contentType = searchResultContentType(pageType)
const href = itemHref(pageType, item)
const trackSearchClick = () => {
@@ -191,7 +592,7 @@ function AcademyCard({ pageType, item, analytics, searchContext, position }) {
{promptPreviewImage ? <img src={promptPreviewImage} srcSet={promptPreviewSrcSet || undefined} sizes="(max-width: 767px) calc(100vw - 2rem), (max-width: 1279px) calc(50vw - 2rem), 420px" 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>
<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]">{item?.ranking?.rank ? `#${item.ranking.rank} this month` : '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">
@@ -209,6 +610,47 @@ function AcademyCard({ pageType, item, analytics, searchContext, position }) {
</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?.ranking ? <p className="mt-4 text-xs uppercase tracking-[0.18em] text-slate-400">{item.ranking.prompt_copies > 0 ? `${item.ranking.prompt_copies} copies` : `${item.ranking.views} views`} · popularity {item.ranking.popularity_score}</p> : null}
{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>
)
}
if (pageType === 'lessons') {
return (
<Link
href={href}
onClick={trackSearchClick}
data-academy-content-type={contentType || undefined}
data-academy-content-id={item?.id || undefined}
data-academy-search-query={searchContext?.query || undefined}
data-academy-search-results-count={searchContext?.resultsCount || undefined}
data-academy-search-position={position || undefined}
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-amber-200/24 hover:bg-[linear-gradient(180deg,rgba(15,23,42,0.96),rgba(10,15,26,0.98))]"
>
<div className="relative aspect-[16/10] overflow-hidden bg-[linear-gradient(135deg,rgba(251,191,36,0.18),rgba(17,24,39,0.94))]">
{lessonPreview ? <img src={lessonPreview.src} 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">
{item?.formatted_lesson_number ? <span className="rounded-full border border-white/10 bg-black/30 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-amber-100">{item.formatted_lesson_number}</span> : null}
<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?.reading_minutes ? <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.reading_minutes} min</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-amber-100/80">{item?.category?.name || 'Academy lesson'}</p>
{lessonSeries ? <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">{lessonSeries}</span> : null}
</div>
<h2 className="mt-3 text-2xl font-semibold tracking-[-0.04em] text-white transition group-hover:text-amber-50">{item.title}</h2>
<p className="mt-3 text-sm leading-7 text-slate-300">{item.excerpt || item.description || item.content_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>
@@ -261,7 +703,7 @@ async function fetchAcademyPage(url) {
return response.json()
}
export default function AcademyList({ pageType, title, description, seo, items, filters, categories, pricingUrl, analytics }) {
export default function AcademyList({ pageType, promptView = 'library', title, description, seo, breadcrumbs = [], items, filters, categories, pricingUrl, coursesUrl, packsUrl, promptPopularUrl, promptLibraryUrl, popularPeriod = null, popularPeriods = [], featuredPrompts = [], popularPrompts = [], academyAccess = null, analytics }) {
const flash = usePage().props.flash || {}
useAcademyPageAnalytics(analytics)
const searchContext = analytics?.search ? {
@@ -280,6 +722,11 @@ export default function AcademyList({ pageType, title, description, seo, items,
})
const [loadingMore, setLoadingMore] = React.useState(false)
const sentinelRef = React.useRef(null)
const hasActivePromptFilters = pageType === 'prompts' && promptView === 'library' && Boolean(filters?.q || filters?.category || filters?.difficulty || filters?.tag)
const showPromptDiscovery = pageType === 'prompts' && promptView === 'library' && !hasActivePromptFilters
const showPopularFeatured = pageType === 'prompts' && promptView === 'popular' && featuredPrompts.length > 0
const infiniteLoadLabel = pageType === 'lessons' ? 'lessons' : 'prompts'
const usesInfiniteLoad = (pageType === 'prompts' && promptView === 'library') || pageType === 'lessons'
React.useEffect(() => {
setVisibleItems(initialItems)
@@ -292,11 +739,11 @@ export default function AcademyList({ pageType, title, description, seo, items,
setLoadingMore(false)
}, [initialItems, items?.current_page, items?.last_page, items?.next_page_url, items?.prev_page_url, pageType])
const hasMorePages = pageType === 'prompts' && pagination.currentPage < pagination.lastPage && Boolean(pagination.nextPageUrl)
const hasFallbackPagination = pageType === 'prompts' && pagination.lastPage > 1
const hasMorePages = usesInfiniteLoad && pagination.currentPage < pagination.lastPage && Boolean(pagination.nextPageUrl)
const hasFallbackPagination = usesInfiniteLoad && pagination.lastPage > 1
const loadMore = React.useCallback(async () => {
if (pageType !== 'prompts' || loadingMore || !pagination.nextPageUrl) {
if (!usesInfiniteLoad || loadingMore || !pagination.nextPageUrl) {
return
}
@@ -318,7 +765,7 @@ export default function AcademyList({ pageType, title, description, seo, items,
} finally {
setLoadingMore(false)
}
}, [loadingMore, pageType, pagination.currentPage, pagination.lastPage, pagination.nextPageUrl, pagination.prevPageUrl])
}, [loadingMore, pagination.currentPage, pagination.lastPage, pagination.nextPageUrl, pagination.prevPageUrl, usesInfiniteLoad])
React.useEffect(() => {
const sentinel = sentinelRef.current
@@ -343,7 +790,9 @@ export default function AcademyList({ pageType, title, description, seo, items,
<SeoHead seo={seo || {}} title={title} description={description} />
<div className="mx-auto max-w-[1360px] space-y-6">
{pageType === 'prompts' ? <PromptLibraryHero title={title} description={description} items={visibleItems} pricingUrl={pricingUrl} totalCount={Number(items?.total || visibleItems.length || 0)} /> : (
{(pageType === 'prompts' || pageType === 'lessons') ? <Breadcrumbs items={breadcrumbs} /> : null}
{pageType === 'prompts' ? <PromptLibraryHero promptView={promptView} title={title} description={description} items={visibleItems} pricingUrl={pricingUrl} coursesUrl={coursesUrl} packsUrl={packsUrl} promptPopularUrl={promptPopularUrl} promptLibraryUrl={promptLibraryUrl} popularPeriod={popularPeriod} popularPeriods={popularPeriods} totalCount={Number(items?.total || visibleItems.length || 0)} analytics={analytics} hasPopularSection={popularPrompts.length > 0} academyAccess={academyAccess} /> : pageType === 'lessons' ? <LessonsLibraryHero title={title} description={description} items={visibleItems} totalCount={Number(items?.total || visibleItems.length || 0)} pricingUrl={pricingUrl} coursesUrl={coursesUrl} promptLibraryUrl={promptLibraryUrl} academyAccess={academyAccess} analytics={analytics} /> : (
<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>
@@ -359,7 +808,16 @@ export default function AcademyList({ pageType, title, description, seo, items,
{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} />
{promptView === 'library' ? <QueryFilters pageType={pageType} filters={filters} categories={categories} /> : null}
{showPromptDiscovery ? (
<>
<PromptDiscoverySection id="popular-prompts" title="Popular prompts right now" description="See which prompt templates are getting the most momentum from views and copies this month." items={popularPrompts} href={promptPopularUrl} ctaLabel="Open rankings" />
<PromptDiscoverySection id="featured-prompts" title="Featured prompt picks" description="Hand-picked templates worth starting from if you want quick wins for wallpapers, worlds, portraits, and creator-style visuals." items={featuredPrompts} href={coursesUrl} ctaLabel="Browse courses" />
</>
) : null}
{showPopularFeatured ? <PromptDiscoverySection id="featured-prompts" title="Featured picks to try next" description="Once you have reviewed the top-performing prompts, jump into a few curated templates that are worth adapting into your own workflow." items={featuredPrompts} href={promptLibraryUrl} ctaLabel="Browse full library" /> : null}
{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>
@@ -369,11 +827,11 @@ export default function AcademyList({ pageType, title, description, seo, items,
{visibleItems.map((item, index) => <AcademyCard key={`${pageType}-${item.id}`} pageType={pageType} item={item} analytics={analytics} searchContext={searchContext} position={index + 1} />)}
</section>
{pageType === 'prompts' ? (
{usesInfiniteLoad ? (
<div className="pt-2">
<div ref={sentinelRef} className="h-10 w-full" aria-hidden="true" />
{loadingMore ? <div className="rounded-[22px] border border-white/10 bg-black/20 px-5 py-4 text-center text-sm text-slate-300">Loading more prompts...</div> : null}
{!hasMorePages && visibleItems.length > initialItems.length ? <div className="rounded-[22px] border border-white/10 bg-black/20 px-5 py-4 text-center text-sm text-slate-400">You have reached the end of the prompt library.</div> : null}
{loadingMore ? <div className="rounded-[22px] border border-white/10 bg-black/20 px-5 py-4 text-center text-sm text-slate-300">Loading more {infiniteLoadLabel}...</div> : null}
{!hasMorePages && visibleItems.length > initialItems.length ? <div className="rounded-[22px] border border-white/10 bg-black/20 px-5 py-4 text-center text-sm text-slate-400">You have reached the end of the {pageType === 'lessons' ? 'lesson library' : 'prompt library'}.</div> : null}
{hasFallbackPagination ? (
<div className="mt-4 flex flex-wrap items-center justify-between gap-3 rounded-[22px] border border-white/10 bg-black/20 px-5 py-4">
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">Auto-load is primary. Pagination is available as a backup.</div>

View File

@@ -681,6 +681,54 @@ function PromptVariantCard({ variant, analytics, contentId }) {
function PromptVariantsSection({ variants, analytics, contentId }) {
const visibleVariants = Array.isArray(variants) ? variants.filter((variant) => variant && typeof variant === 'object') : []
const [activeVariantKey, setActiveVariantKey] = useState('')
const variantsScrollRef = useRef(null)
const [canScrollVariantsLeft, setCanScrollVariantsLeft] = useState(false)
const [canScrollVariantsRight, setCanScrollVariantsRight] = useState(false)
useEffect(() => {
if (typeof window === 'undefined') {
return undefined
}
const updateVariantScrollState = () => {
const element = variantsScrollRef.current
if (!element) {
setCanScrollVariantsLeft(false)
setCanScrollVariantsRight(false)
return
}
const maxScrollLeft = Math.max(0, element.scrollWidth - element.clientWidth)
setCanScrollVariantsLeft(element.scrollLeft > 6)
setCanScrollVariantsRight(element.scrollLeft < maxScrollLeft - 6)
}
updateVariantScrollState()
const element = variantsScrollRef.current
if (!element) {
return undefined
}
element.addEventListener('scroll', updateVariantScrollState, { passive: true })
window.addEventListener('resize', updateVariantScrollState, { passive: true })
return () => {
element.removeEventListener('scroll', updateVariantScrollState)
window.removeEventListener('resize', updateVariantScrollState)
}
}, [visibleVariants.length])
const scrollVariants = (direction) => {
const element = variantsScrollRef.current
if (!element) return
const amount = Math.max(260, Math.floor(element.clientWidth * 0.7))
element.scrollBy({
left: direction === 'left' ? -amount : amount,
behavior: 'smooth',
})
}
useEffect(() => {
if (!visibleVariants.length) {
@@ -712,8 +760,34 @@ function PromptVariantsSection({ variants, analytics, contentId }) {
<p className="mt-3 text-sm leading-7 text-slate-300">Switch between safer, shorter, or more specialized prompt variants without losing the core creative direction.</p>
</div>
<div className="mt-6 overflow-x-auto pb-2">
<div className="inline-flex min-w-full gap-3" role="tablist" aria-label="Prompt variants">
<div className="relative mt-6">
<div className={`pointer-events-none absolute inset-y-0 left-0 z-10 w-14 bg-gradient-to-r from-[#182233] via-[#182233]/85 to-transparent transition ${canScrollVariantsLeft ? 'opacity-100' : 'opacity-0'}`} aria-hidden="true" />
<div className={`pointer-events-none absolute inset-y-0 right-0 z-10 w-14 bg-gradient-to-l from-[#182233] via-[#182233]/85 to-transparent transition ${canScrollVariantsRight ? 'opacity-100' : 'opacity-0'}`} aria-hidden="true" />
<button
type="button"
aria-label="Scroll prompt variants left"
onClick={() => scrollVariants('left')}
className={`absolute left-2 top-1/2 z-20 flex h-10 w-10 -translate-y-1/2 items-center justify-center rounded-full border border-white/12 bg-slate-950/80 text-white/80 shadow-[0_16px_36px_rgba(2,6,23,0.28)] backdrop-blur transition ${canScrollVariantsLeft ? 'opacity-100 hover:scale-105 hover:bg-slate-900/95' : 'pointer-events-none opacity-0'}`}
>
<i className="fa-solid fa-chevron-left text-sm" />
</button>
<button
type="button"
aria-label="Scroll prompt variants right"
onClick={() => scrollVariants('right')}
className={`absolute right-2 top-1/2 z-20 flex h-10 w-10 -translate-y-1/2 items-center justify-center rounded-full border border-white/12 bg-slate-950/80 text-white/80 shadow-[0_16px_36px_rgba(2,6,23,0.28)] backdrop-blur transition ${canScrollVariantsRight ? 'opacity-100 hover:scale-105 hover:bg-slate-900/95' : 'pointer-events-none opacity-0'}`}
>
<i className="fa-solid fa-chevron-right text-sm" />
</button>
<div
ref={variantsScrollRef}
className="flex gap-3 overflow-x-auto px-1 pb-3 pt-1 snap-x snap-mandatory scroll-smooth scrollbar-hide [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden"
role="tablist"
aria-label="Prompt variants"
>
{visibleVariants.map((variant, index) => {
const variantKey = String(variant?.slug || variant?.title || `variant-${index}`)
const isActive = activeVariant === variant
@@ -726,7 +800,7 @@ function PromptVariantsSection({ variants, analytics, contentId }) {
aria-selected={isActive}
onClick={() => setActiveVariantKey(variantKey)}
className={[
'min-w-[220px] rounded-[24px] border px-4 py-3 text-left transition',
'w-[min(360px,calc(100vw-4.5rem))] shrink-0 snap-start rounded-[24px] border px-4 py-3 text-left transition sm:w-[320px]',
isActive
? 'border-sky-300/30 bg-sky-300/12 shadow-[0_16px_40px_rgba(2,6,23,0.18)]'
: 'border-white/10 bg-black/20 hover:border-white/20 hover:bg-white/[0.05]',
@@ -1024,8 +1098,6 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], relat
const promptUnlockDetails = item?.unlock_description || promptUnlockDescription(item?.access_level)
const promptFeaturedExamples = promptPreviewImage ? promptPublicExamples.slice(0, 2) : promptPublicExamples.slice(0, 4)
const promptOverflowExamples = promptPublicExamples.slice(promptFeaturedExamples.length)
const promptModelsCovered = (promptHasFullAccess && promptComparisons.length ? promptComparisons : promptPublicExamples)
.map((entry, index) => entry.model_name || entry.provider || entry.title || `Model ${index + 1}`)
const promptComparisonGalleryImages = promptComparisons
.map((note, index) => {
const src = note.image_url || note.thumb_url || ''
@@ -1471,33 +1543,43 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], relat
{pageType === 'lesson' ? (
<div className="space-y-8">
<section className="overflow-hidden rounded-[40px] border border-white/10 bg-black/20 shadow-[0_24px_90px_rgba(15,23,42,0.34)]">
<div className="grid gap-0 lg:grid-cols-[minmax(0,1fr)_360px]">
<div className="relative overflow-hidden p-8 md:p-10 lg:p-12">
{lessonCover ? <img src={lessonCover} alt="" aria-hidden="true" className="absolute inset-0 h-full w-full object-cover opacity-15" /> : null}
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.22),_transparent_34%),linear-gradient(135deg,_rgba(2,6,23,0.96),_rgba(15,23,42,0.78))]" />
<div className="relative z-10 max-w-3xl">
<section className="relative overflow-hidden rounded-[40px] border border-sky-200/12 bg-[linear-gradient(145deg,rgba(14,165,233,0.16),rgba(15,23,42,0.95)_38%,rgba(251,191,36,0.14))] shadow-[0_24px_90px_rgba(15,23,42,0.34)]">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_18%_14%,rgba(125,211,252,0.14),transparent_24%),linear-gradient(90deg,rgba(255,255,255,0.04)_1px,transparent_1px),linear-gradient(180deg,rgba(255,255,255,0.04)_1px,transparent_1px)] bg-[length:auto,24px_24px,24px_24px] opacity-70" />
<div className="absolute -left-8 top-10 h-36 w-36 rounded-full bg-sky-300/18 blur-3xl" />
<div className="absolute -right-10 bottom-0 h-40 w-40 rounded-full bg-amber-300/14 blur-3xl" />
<div className="relative grid gap-6 p-5 md:p-6 lg:grid-cols-[minmax(0,1fr)_360px] lg:p-7">
<div className="relative overflow-hidden rounded-[34px] border border-white/10 bg-[linear-gradient(135deg,rgba(2,6,23,0.86),rgba(15,23,42,0.62))] p-5 shadow-[0_20px_46px_rgba(2,6,23,0.18)] md:p-6 lg:p-7">
<div className="absolute inset-0 bg-[linear-gradient(135deg,rgba(2,6,23,0.86),rgba(15,23,42,0.62))]" />
<div className="relative z-10 max-w-4xl">
<div className="flex flex-wrap gap-2">
<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">Skinbase AI Academy</span>
<span className="rounded-full border border-sky-200/18 bg-sky-300/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-50/90">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-200">Lesson</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>
</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>
<div className="mt-5 flex items-start justify-between gap-4">
<div className="max-w-3xl">
{item.lesson_label ? <p className="text-sm font-semibold uppercase tracking-[0.24em] text-amber-100">{item.lesson_label}</p> : null}
<h1 className="mt-4 text-4xl font-semibold tracking-[-0.055em] text-white md:text-5xl lg:text-[3.8rem]">{item.title}</h1>
<p className="mt-4 max-w-3xl text-base leading-8 text-slate-200/95 md:text-lg">{lessonSummary}</p>
</div>
<span className="hidden h-20 w-20 shrink-0 items-center justify-center rounded-[26px] border border-white/10 bg-white/[0.04] text-2xl text-sky-50 shadow-[0_18px_40px_rgba(2,6,23,0.22)] md:flex">
<i className="fa-solid fa-book-open-reader" />
</span>
</div>
{lessonTags.length ? (
<div className="mt-5 flex flex-wrap gap-2">
<div className="mt-5 flex flex-wrap gap-2.5">
{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>
<span key={tag} className="rounded-full border border-white/10 bg-white/[0.06] px-4 py-2 text-[11px] font-semibold tracking-[0.16em] text-sky-50/90">{tag}</span>
))}
</div>
) : null}
{courseContext?.title ? (
<div className="mt-6 max-w-2xl rounded-[24px] border border-white/10 bg-black/25 p-5">
<div className="mt-6 max-w-2xl rounded-[24px] border border-white/10 bg-slate-950/35 p-5 backdrop-blur-sm">
<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>
@@ -1520,24 +1602,29 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], relat
</div>
</div>
<aside className="border-t border-white/10 bg-white/[0.03] p-6 lg:border-l lg:border-t-0 lg:p-8">
<div className="space-y-5 lg:sticky lg:top-6">
<div className="overflow-hidden rounded-[28px] border border-white/10 bg-black/20">
{lessonCover ? <img src={lessonCover} alt={item.title} className="h-52 w-full object-cover" /> : <div className="flex h-52 items-center justify-center bg-[linear-gradient(135deg,_rgba(14,165,233,0.18),_rgba(17,24,39,0.94))] text-sm font-semibold uppercase tracking-[0.24em] text-slate-300">Lesson cover</div>}
<aside className="grid gap-4 self-start">
<div className="relative overflow-hidden rounded-[32px] border border-white/10 bg-slate-950 shadow-[0_24px_56px_rgba(2,6,23,0.24)]">
{lessonCover ? <img src={lessonCover} alt={item.title} className="h-[260px] w-full object-cover sm:h-[300px] lg:h-[320px]" /> : <div className="flex h-[260px] items-center justify-center bg-[linear-gradient(135deg,_rgba(14,165,233,0.22),_rgba(17,24,39,0.96))] text-sm font-semibold uppercase tracking-[0.24em] text-slate-300 sm:h-[300px] lg:h-[320px]">Lesson cover</div>}
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(2,6,23,0.02),rgba(2,6,23,0.2)_48%,rgba(2,6,23,0.88))]" />
<div className="absolute inset-x-0 bottom-0 p-4">
<div className="rounded-[24px] border border-white/10 bg-black/30 px-4 py-3 backdrop-blur-md">
<p className="text-[10px] font-semibold uppercase tracking-[0.22em] text-sky-100/80">Lesson cover</p>
<p className="mt-1 text-sm font-semibold text-white">{item.lesson_label || item.title}</p>
</div>
</div>
</div>
<div className="space-y-3">
<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} />
</div>
<div className="space-y-3 rounded-[30px] border border-white/10 bg-black/20 p-5 shadow-[0_18px_42px_rgba(2,6,23,0.18)] backdrop-blur-sm">
<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} />
</div>
<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.' : courseContext?.title ? 'This lesson is being tracked inside a course. Completion updates your course progress.' : 'Full lesson content is available below.'}</p>
</div>
<div className="rounded-[30px] border border-white/10 bg-slate-950/35 p-5 shadow-[0_18px_42px_rgba(2,6,23,0.18)] backdrop-blur-sm">
<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.' : courseContext?.title ? 'This lesson is being tracked inside a course. Completion updates your course progress.' : 'Full lesson content is available below.'}</p>
</div>
</aside>
</div>
@@ -1731,28 +1818,99 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], relat
</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(340px,0.8fr)_minmax(0,1.2fr)]">
<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-5 md:p-6 lg:min-h-[660px] lg:border-b-0 lg:border-r lg:border-white/10 lg:p-8">
<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">
<section className="relative overflow-hidden rounded-[40px] border border-rose-200/12 bg-[linear-gradient(150deg,rgba(244,63,94,0.14),rgba(15,23,42,0.96)_36%,rgba(45,212,191,0.14))] shadow-[0_24px_90px_rgba(15,23,42,0.34)]">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_18%_14%,rgba(251,113,133,0.15),transparent_24%),linear-gradient(90deg,rgba(255,255,255,0.04)_1px,transparent_1px),linear-gradient(180deg,rgba(255,255,255,0.04)_1px,transparent_1px)] bg-[length:auto,24px_24px,24px_24px] opacity-75" />
<div className="absolute -left-8 top-10 h-36 w-36 rounded-full bg-rose-300/16 blur-3xl" />
<div className="absolute -right-10 bottom-0 h-40 w-40 rounded-full bg-cyan-300/14 blur-3xl" />
<div className="relative grid gap-6 p-5 md:p-6 lg:grid-cols-[minmax(0,1fr)_minmax(320px,0.72fr)] lg:p-7">
<div className="min-w-0">
{academyBreadcrumbs.length ? (
<div className="mb-5">
<AcademyBreadcrumbs items={academyBreadcrumbs} />
</div>
) : null}
<div className="flex flex-wrap gap-2">
<span className="rounded-full border border-rose-200/18 bg-rose-300/10 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.22em] text-rose-50/90">Skinbase AI Academy</span>
<span className="rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.22em] text-slate-200">Prompt Library</span>
<span className="rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.22em] text-slate-300">{lessonCategory}</span>
<span className="rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.22em] 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-[10px] font-semibold uppercase tracking-[0.22em] 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-[10px] font-semibold uppercase tracking-[0.22em] 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-[10px] font-semibold uppercase tracking-[0.22em] text-sky-100">Featured</span> : null}
</div>
<div className="mt-5 flex items-start justify-between gap-4">
<div className="max-w-4xl">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-rose-100/80">Prompt template</p>
<h1 className="mt-3 max-w-[13ch] text-[clamp(2.6rem,5vw,4.8rem)] font-semibold leading-[0.95] tracking-[-0.05em] text-white">{item.title}</h1>
<p className="mt-4 max-w-3xl text-base leading-8 text-slate-200/95">{lessonSummary}</p>
</div>
<span className="hidden h-20 w-20 shrink-0 items-center justify-center rounded-[26px] border border-white/10 bg-white/[0.04] text-2xl text-rose-50 shadow-[0_18px_40px_rgba(2,6,23,0.22)] md:flex">
<i className="fa-solid fa-wand-magic-sparkles" />
</span>
</div>
<div className="mt-6 flex flex-wrap gap-3">
<button type="button" onClick={toggleLike} className="rounded-full border border-white/10 bg-white/[0.06] px-4 py-2.5 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.08]">{liked ? `Liked · ${likesCount}` : `Like · ${likesCount}`}</button>
<button type="button" onClick={toggleSave} className="rounded-full border border-sky-300/25 bg-sky-300/12 px-4 py-2.5 text-sm font-semibold text-sky-100">{saved ? `Saved · ${savesCount}` : `Save · ${savesCount}`}</button>
{promptHasFullAccess ? <PromptCopyButton prompt={item.prompt} analytics={analytics} contentId={item.id} eventType="academy_prompt_copy" metadata={{ copy_type: 'main_prompt', source: 'prompt_detail' }} /> : null}
{promptHasFullAccess && item.negative_prompt ? <PromptCopyButton prompt={item.negative_prompt} label="Copy negative" analytics={analytics} contentId={item.id} eventType="academy_prompt_negative_copy" metadata={{ copy_type: 'negative_prompt', source: 'prompt_detail' }} /> : null}
</div>
<div className="mt-7 grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
<PromptHeaderStat
label="Category"
value={lessonCategory}
icon="fa-layer-group"
accentClassName="border-sky-300/15 bg-sky-300/10 text-sky-100"
valueClassName="text-white"
/>
<PromptHeaderStat
label="Access"
value={formatMetaDisplay(item.access_level || 'free')}
icon={normalizePromptAccessLevel(item.access_level) === 'pro' ? 'fa-crown' : normalizePromptAccessLevel(item.access_level) === 'creator' ? 'fa-key' : 'fa-lock-open'}
accentClassName={normalizePromptAccessLevel(item.access_level) === 'pro' ? 'border-amber-300/20 bg-amber-300/10 text-amber-100' : normalizePromptAccessLevel(item.access_level) === 'creator' ? 'border-violet-300/20 bg-violet-300/10 text-violet-100' : 'border-emerald-300/20 bg-emerald-300/10 text-emerald-100'}
valueClassName={normalizePromptAccessLevel(item.access_level) === 'pro' ? 'text-amber-50' : normalizePromptAccessLevel(item.access_level) === 'creator' ? 'text-violet-50' : 'text-emerald-50'}
/>
<PromptHeaderStat
label="Difficulty"
value={formatMetaDisplay(lessonDifficulty)}
icon={String(lessonDifficulty || '').toLowerCase() === 'advanced' ? 'fa-bolt' : String(lessonDifficulty || '').toLowerCase() === 'beginner' ? 'fa-seedling' : 'fa-compass-drafting'}
accentClassName={String(lessonDifficulty || '').toLowerCase() === 'advanced' ? 'border-rose-300/20 bg-rose-300/10 text-rose-100' : String(lessonDifficulty || '').toLowerCase() === 'beginner' ? 'border-emerald-300/20 bg-emerald-300/10 text-emerald-100' : 'border-sky-300/20 bg-sky-300/10 text-sky-100'}
valueClassName={String(lessonDifficulty || '').toLowerCase() === 'advanced' ? 'text-rose-50' : String(lessonDifficulty || '').toLowerCase() === 'beginner' ? 'text-emerald-50' : 'text-sky-50'}
/>
<PromptHeaderStat
label="Updated"
value={lessonUpdated}
icon="fa-calendar-days"
accentClassName="border-white/10 bg-white/[0.05] text-slate-200"
valueClassName="text-white"
/>
</div>
</div>
<div className="grid gap-4 lg:pt-2">
<div className="flex h-full flex-col rounded-[30px] border border-white/10 bg-black/20 p-4 shadow-[0_18px_42px_rgba(2,6,23,0.18)] backdrop-blur-sm md:p-5">
<div className="flex items-center justify-between gap-3">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-[#ffd8cd]">Preview artwork</p>
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-rose-100/80">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-3 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"
className="group mt-4 flex min-h-[420px] flex-1 flex-col overflow-hidden rounded-[28px] 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 lg:min-h-[640px]"
disabled={!promptPreviewImage}
aria-label={promptPreviewImage ? `Open preview image for ${item.title}` : 'Preview image unavailable'}
>
{promptPreviewImage ? (
<div className="relative h-full min-h-[320px] overflow-hidden lg:min-h-[540px]">
<img src={promptPreviewThumbImage} srcSet={promptPreviewSrcSet || undefined} sizes="(max-width: 1023px) calc(100vw - 3rem), 720px" 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 className="relative min-h-0 flex-1 overflow-hidden">
<img src={promptPreviewImage || promptPreviewThumbImage} srcSet={promptPreviewSrcSet || undefined} sizes="(max-width: 1023px) calc(100vw - 3rem), 34vw" 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.36))]" />
<div className="absolute bottom-4 left-4 right-4 flex items-end justify-between gap-4 rounded-[24px] border border-white/10 bg-black/30 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>
@@ -1763,7 +1921,7 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], relat
</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 className="flex min-h-0 flex-1 items-center justify-center bg-[linear-gradient(135deg,rgba(251,146,60,0.14),rgba(17,24,39,0.96))] px-8 text-center">
<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>
@@ -1774,107 +1932,6 @@ export default function AcademyShow({ pageType, item, relatedLessons = [], relat
</button>
</div>
</div>
<div className="relative overflow-hidden p-6 md:p-8 lg:p-9">
<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-3xl">
{academyBreadcrumbs.length ? (
<div className="mb-5">
<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-[10px] font-semibold uppercase tracking-[0.22em] text-[#fff0ea]">Skinbase AI Academy</span>
<span className="rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.22em] text-slate-300">{lessonCategory}</span>
<span className="rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.22em] 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-[10px] font-semibold uppercase tracking-[0.22em] 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-[10px] font-semibold uppercase tracking-[0.22em] 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-[10px] font-semibold uppercase tracking-[0.22em] text-sky-100">Featured</span> : null}
</div>
<p className="mt-6 text-xs font-semibold uppercase tracking-[0.22em] text-[#ffd8cd]">Prompt template</p>
<h1 className="mt-3 max-w-3xl text-[clamp(2.4rem,4.8vw,4.5rem)] font-semibold leading-[0.95] tracking-[-0.05em] text-white">{item.title}</h1>
<p className="mt-4 max-w-2xl text-[15px] leading-7 text-slate-300 md:text-base">{lessonSummary}</p>
<div className="mt-6 flex flex-wrap gap-3">
<button type="button" onClick={toggleLike} className="rounded-full border border-white/10 bg-white/[0.06] px-4 py-2.5 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.08]">{liked ? `Liked · ${likesCount}` : `Like · ${likesCount}`}</button>
<button type="button" onClick={toggleSave} className="rounded-full border border-sky-300/25 bg-sky-300/12 px-4 py-2.5 text-sm font-semibold text-sky-100">{saved ? `Saved · ${savesCount}` : `Save · ${savesCount}`}</button>
{promptHasFullAccess ? <PromptCopyButton prompt={item.prompt} analytics={analytics} contentId={item.id} eventType="academy_prompt_copy" metadata={{ copy_type: 'main_prompt', source: 'prompt_detail' }} /> : null}
{promptHasFullAccess && item.negative_prompt ? <PromptCopyButton prompt={item.negative_prompt} label="Copy negative" analytics={analytics} contentId={item.id} eventType="academy_prompt_negative_copy" metadata={{ copy_type: 'negative_prompt', source: 'prompt_detail' }} /> : null}
</div>
<div className="mt-7 grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
<PromptHeaderStat
label="Category"
value={lessonCategory}
icon="fa-layer-group"
accentClassName="border-sky-300/15 bg-sky-300/10 text-sky-100"
valueClassName="text-white"
/>
<PromptHeaderStat
label="Access"
value={formatMetaDisplay(item.access_level || 'free')}
icon={normalizePromptAccessLevel(item.access_level) === 'pro' ? 'fa-crown' : normalizePromptAccessLevel(item.access_level) === 'creator' ? 'fa-key' : 'fa-lock-open'}
accentClassName={normalizePromptAccessLevel(item.access_level) === 'pro' ? 'border-amber-300/20 bg-amber-300/10 text-amber-100' : normalizePromptAccessLevel(item.access_level) === 'creator' ? 'border-violet-300/20 bg-violet-300/10 text-violet-100' : 'border-emerald-300/20 bg-emerald-300/10 text-emerald-100'}
valueClassName={normalizePromptAccessLevel(item.access_level) === 'pro' ? 'text-amber-50' : normalizePromptAccessLevel(item.access_level) === 'creator' ? 'text-violet-50' : 'text-emerald-50'}
/>
<PromptHeaderStat
label="Difficulty"
value={formatMetaDisplay(lessonDifficulty)}
icon={String(lessonDifficulty || '').toLowerCase() === 'advanced' ? 'fa-bolt' : String(lessonDifficulty || '').toLowerCase() === 'beginner' ? 'fa-seedling' : 'fa-compass-drafting'}
accentClassName={String(lessonDifficulty || '').toLowerCase() === 'advanced' ? 'border-rose-300/20 bg-rose-300/10 text-rose-100' : String(lessonDifficulty || '').toLowerCase() === 'beginner' ? 'border-emerald-300/20 bg-emerald-300/10 text-emerald-100' : 'border-sky-300/20 bg-sky-300/10 text-sky-100'}
valueClassName={String(lessonDifficulty || '').toLowerCase() === 'advanced' ? 'text-rose-50' : String(lessonDifficulty || '').toLowerCase() === 'beginner' ? 'text-emerald-50' : 'text-sky-50'}
/>
<PromptHeaderStat
label="Updated"
value={lessonUpdated}
icon="fa-calendar-days"
accentClassName="border-white/10 bg-white/[0.05] text-slate-200"
valueClassName="text-white"
/>
</div>
{lessonTags.length ? (
<div className="mt-7 rounded-[28px] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.045),rgba(148,163,184,0.03))] p-4 md:p-5">
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-400">Microtags</p>
<div className="mt-3 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-7 grid gap-4 xl:grid-cols-[minmax(0,1.08fr)_minmax(260px,0.92fr)]">
<div className="rounded-[28px] border border-white/10 bg-black/20 p-4 md: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
? `${promptAccessRequirement ? `${promptAccessRequirement} ` : ''}This page shows the prompt summary and public example results, but the reusable prompt system stays 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-4 md: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>