862 lines
52 KiB
JavaScript
862 lines
52 KiB
JavaScript
import React from 'react'
|
|
import { Link, router, usePage } from '@inertiajs/react'
|
|
import SeoHead from '../../components/seo/SeoHead'
|
|
import NovaSelect from '../../components/ui/NovaSelect'
|
|
import { trackAcademySearchResultClick, trackUpgradeClick, useAcademyPageAnalytics } from '../../lib/academyAnalytics'
|
|
|
|
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
|
|
}
|
|
|
|
const categoryOptions = [{ value: '', label: 'All categories' }, ...(categories || []).map((category) => ({ value: category.slug, label: category.name }))]
|
|
const difficultyOptions = [
|
|
{ value: '', label: 'All levels' },
|
|
{ value: 'beginner', label: 'Beginner' },
|
|
{ value: 'intermediate', label: 'Intermediate' },
|
|
{ value: 'advanced', label: 'Advanced' },
|
|
{ value: 'pro', label: 'Pro' },
|
|
]
|
|
|
|
return (
|
|
<div className="grid gap-3 rounded-[28px] border border-white/10 bg-black/20 p-5 md:grid-cols-3">
|
|
<input
|
|
defaultValue={filters?.q || ''}
|
|
placeholder={`Search ${pageType}`}
|
|
className="rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white outline-none placeholder:text-slate-500"
|
|
onKeyDown={(event) => {
|
|
if (event.key !== 'Enter') return
|
|
router.get(window.location.pathname, { ...filters, q: event.currentTarget.value }, { preserveState: true, preserveScroll: true })
|
|
}}
|
|
/>
|
|
<NovaSelect
|
|
value={filters?.category || ''}
|
|
onChange={(nextValue) => router.get(window.location.pathname, { ...filters, category: nextValue || undefined }, { preserveState: true, preserveScroll: true })}
|
|
options={categoryOptions}
|
|
searchable={false}
|
|
className="rounded-2xl bg-white/[0.04]"
|
|
placeholder="All categories"
|
|
/>
|
|
<NovaSelect
|
|
value={filters?.difficulty || ''}
|
|
onChange={(nextValue) => router.get(window.location.pathname, { ...filters, difficulty: nextValue || undefined }, { preserveState: true, preserveScroll: true })}
|
|
options={difficultyOptions}
|
|
searchable={false}
|
|
className="rounded-2xl bg-white/[0.04]"
|
|
placeholder="All levels"
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function LockBadge({ item }) {
|
|
if (!item?.locked) return <span className="rounded-full border border-emerald-300/25 bg-emerald-300/12 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.2em] text-emerald-100">{item.access_level}</span>
|
|
|
|
return <span className="rounded-full border border-amber-300/25 bg-amber-300/12 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.2em] text-amber-100">Locked · {item.access_level}</span>
|
|
}
|
|
|
|
function itemHref(pageType, item) {
|
|
if (pageType === 'lessons') return academyHref('lessons', item.slug)
|
|
if (pageType === 'prompts') return academyHref('prompts', item.slug)
|
|
if (pageType === 'packs') return academyHref('packs', item.slug)
|
|
return academyHref('challenges', item.slug)
|
|
}
|
|
|
|
function searchResultContentType(pageType) {
|
|
if (pageType === 'prompts') return 'academy_prompt'
|
|
if (pageType === 'lessons') return 'academy_lesson'
|
|
if (pageType === 'packs') return 'academy_prompt_pack'
|
|
if (pageType === 'challenges') return 'academy_challenge'
|
|
return null
|
|
}
|
|
|
|
function promptPreviewAsset(item) {
|
|
const full = item?.preview_image || ''
|
|
const thumb = item?.preview_image_thumb || full
|
|
|
|
if (!thumb) {
|
|
return null
|
|
}
|
|
|
|
return {
|
|
src: thumb,
|
|
srcSet: item?.preview_image_srcset || '',
|
|
}
|
|
}
|
|
|
|
function lessonPreviewAsset(item) {
|
|
const src = item?.cover_image_url || item?.article_cover_image_url || item?.cover_image || item?.article_cover_image || ''
|
|
|
|
if (!src) {
|
|
return null
|
|
}
|
|
|
|
return { src }
|
|
}
|
|
|
|
function PromptSpotlightCard({ item }) {
|
|
const preview = promptPreviewAsset(item)
|
|
|
|
return (
|
|
<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>
|
|
<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-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-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>
|
|
<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 = () => {
|
|
if (!searchContext?.query || !contentType) {
|
|
return
|
|
}
|
|
|
|
trackAcademySearchResultClick(analytics, searchContext, {
|
|
contentType,
|
|
contentId: item?.id,
|
|
position,
|
|
})
|
|
}
|
|
|
|
if (pageType === 'prompts') {
|
|
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-sky-300/25 hover:bg-[linear-gradient(180deg,rgba(15,23,42,0.96),rgba(10,15,26,0.98))]"
|
|
>
|
|
<div className="relative aspect-[16/11] overflow-hidden bg-[linear-gradient(135deg,rgba(56,189,248,0.18),rgba(17,24,39,0.94))]">
|
|
{promptPreviewImage ? <img src={promptPreviewImage} 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]">{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">
|
|
<div className="flex flex-wrap gap-2">
|
|
{item?.difficulty ? <span className="rounded-full border border-white/10 bg-black/30 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-white">{item.difficulty}</span> : null}
|
|
{item?.aspect_ratio ? <span className="rounded-full border border-white/10 bg-black/30 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-white">{item.aspect_ratio}</span> : null}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-5">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80">{item?.category?.name || 'Academy'}</p>
|
|
{Array.isArray(item?.tool_notes) && item.tool_notes.length ? <span className="rounded-full border border-[#ffcfbf]/15 bg-[#ffcfbf]/10 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-[#fff0ea]">{item.tool_notes.length} comparisons</span> : null}
|
|
</div>
|
|
<h2 className="mt-3 text-2xl font-semibold tracking-[-0.04em] text-white transition group-hover:text-sky-100">{item.title}</h2>
|
|
<p className="mt-3 text-sm leading-7 text-slate-300">{item.excerpt || item.description || item.prompt_preview || 'No description yet.'}</p>
|
|
{item?.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>
|
|
)
|
|
}
|
|
|
|
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="rounded-[28px] border border-white/10 bg-white/[0.04] p-5 transition hover:border-white/20 hover:bg-white/[0.06]"
|
|
>
|
|
<div className="flex items-center justify-between gap-3">
|
|
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80">{pageType.slice(0, -1)}</p>
|
|
<LockBadge item={item} />
|
|
</div>
|
|
{pageType === 'lessons' && item?.formatted_lesson_number ? (
|
|
<div className="mt-4 flex flex-wrap items-center gap-2">
|
|
<span className="rounded-full border border-amber-300/20 bg-amber-300/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.2em] text-amber-100">{item.formatted_lesson_number}</span>
|
|
{lessonSeries ? <span className="text-xs font-medium uppercase tracking-[0.18em] text-slate-500">{lessonSeries}</span> : null}
|
|
</div>
|
|
) : null}
|
|
<h2 className="mt-4 text-2xl font-semibold tracking-[-0.04em] text-white">{item.title}</h2>
|
|
<p className="mt-3 text-sm leading-7 text-slate-300">{item.excerpt || item.description || item.prompt_preview || item.content_preview || 'No description yet.'}</p>
|
|
{pageType === 'lessons' && item.tags?.length ? <p className="mt-4 text-xs uppercase tracking-[0.18em] text-slate-500">{item.tags.slice(0, 4).join(' · ')}</p> : null}
|
|
{pageType === 'prompts' && item.tags?.length ? <p className="mt-4 text-xs uppercase tracking-[0.18em] text-slate-500">{item.tags.slice(0, 4).join(' · ')}</p> : null}
|
|
{pageType === 'challenges' ? <p className="mt-4 text-xs uppercase tracking-[0.18em] text-slate-500">{item.status} · {item.submission_count ?? 0} submissions</p> : null}
|
|
</Link>
|
|
)
|
|
}
|
|
|
|
async function fetchAcademyPage(url) {
|
|
const response = await fetch(url, {
|
|
headers: {
|
|
Accept: 'application/json',
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
},
|
|
credentials: 'same-origin',
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to load the next page.')
|
|
}
|
|
|
|
return response.json()
|
|
}
|
|
|
|
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 ? {
|
|
query: analytics.search.query,
|
|
normalizedQuery: analytics.search.normalizedQuery,
|
|
resultsCount: analytics.search.resultsCount,
|
|
filters,
|
|
} : null
|
|
const initialItems = React.useMemo(() => (Array.isArray(items?.data) ? items.data : []), [items])
|
|
const [visibleItems, setVisibleItems] = React.useState(initialItems)
|
|
const [pagination, setPagination] = React.useState({
|
|
currentPage: Number(items?.current_page || 1),
|
|
lastPage: Number(items?.last_page || 1),
|
|
prevPageUrl: items?.prev_page_url || null,
|
|
nextPageUrl: items?.next_page_url || null,
|
|
})
|
|
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)
|
|
setPagination({
|
|
currentPage: Number(items?.current_page || 1),
|
|
lastPage: Number(items?.last_page || 1),
|
|
prevPageUrl: items?.prev_page_url || null,
|
|
nextPageUrl: items?.next_page_url || null,
|
|
})
|
|
setLoadingMore(false)
|
|
}, [initialItems, items?.current_page, items?.last_page, items?.next_page_url, items?.prev_page_url, pageType])
|
|
|
|
const hasMorePages = usesInfiniteLoad && pagination.currentPage < pagination.lastPage && Boolean(pagination.nextPageUrl)
|
|
const hasFallbackPagination = usesInfiniteLoad && pagination.lastPage > 1
|
|
|
|
const loadMore = React.useCallback(async () => {
|
|
if (!usesInfiniteLoad || loadingMore || !pagination.nextPageUrl) {
|
|
return
|
|
}
|
|
|
|
setLoadingMore(true)
|
|
|
|
try {
|
|
const payload = await fetchAcademyPage(pagination.nextPageUrl)
|
|
const nextItems = Array.isArray(payload?.data) ? payload.data : []
|
|
|
|
setVisibleItems((current) => [...current, ...nextItems.filter((item) => !current.some((existing) => String(existing.id) === String(item.id)))])
|
|
setPagination({
|
|
currentPage: Number(payload?.current_page || pagination.currentPage),
|
|
lastPage: Number(payload?.last_page || pagination.lastPage),
|
|
prevPageUrl: payload?.prev_page_url || pagination.prevPageUrl,
|
|
nextPageUrl: payload?.next_page_url || null,
|
|
})
|
|
} catch {
|
|
setPagination((current) => ({ ...current, nextPageUrl: null }))
|
|
} finally {
|
|
setLoadingMore(false)
|
|
}
|
|
}, [loadingMore, pagination.currentPage, pagination.lastPage, pagination.nextPageUrl, pagination.prevPageUrl, usesInfiniteLoad])
|
|
|
|
React.useEffect(() => {
|
|
const sentinel = sentinelRef.current
|
|
|
|
if (!sentinel || !hasMorePages || loadingMore || typeof window === 'undefined' || typeof window.IntersectionObserver !== 'function') {
|
|
return undefined
|
|
}
|
|
|
|
const observer = new window.IntersectionObserver((entries) => {
|
|
if (entries[0]?.isIntersecting) {
|
|
void loadMore()
|
|
}
|
|
}, { rootMargin: '360px 0px' })
|
|
|
|
observer.observe(sentinel)
|
|
|
|
return () => observer.disconnect()
|
|
}, [hasMorePages, loadMore, loadingMore])
|
|
|
|
return (
|
|
<main className="min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.15),_transparent_24%),radial-gradient(circle_at_bottom_right,_rgba(251,191,36,0.16),_transparent_24%),linear-gradient(180deg,_#0f172a_0%,_#111827_100%)] px-4 py-8 sm:px-6 lg:px-8">
|
|
<SeoHead seo={seo || {}} title={title} description={description} />
|
|
|
|
<div className="mx-auto max-w-[1360px] space-y-6">
|
|
{(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>
|
|
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-200/80">Skinbase AI Academy</p>
|
|
<h1 className="mt-3 text-4xl font-semibold tracking-[-0.05em] text-white md:text-5xl">{title}</h1>
|
|
<p className="mt-4 max-w-3xl text-base leading-8 text-slate-300">{description}</p>
|
|
</div>
|
|
<Link href={pricingUrl} onClick={() => trackUpgradeClick(analytics, { source: `${pageType}_list_hero` })} className="rounded-full border border-sky-300/25 bg-sky-300/12 px-5 py-3 text-sm font-semibold text-sky-100">Upgrade preview</Link>
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{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}
|
|
|
|
{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>
|
|
) : (
|
|
<>
|
|
<section className="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
|
|
{visibleItems.map((item, index) => <AcademyCard key={`${pageType}-${item.id}`} pageType={pageType} item={item} analytics={analytics} searchContext={searchContext} position={index + 1} />)}
|
|
</section>
|
|
|
|
{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 {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>
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
{pagination.prevPageUrl ? (
|
|
<Link href={pagination.prevPageUrl} preserveScroll className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]">
|
|
<i className="fa-solid fa-arrow-left text-[10px]" />
|
|
Previous
|
|
</Link>
|
|
) : null}
|
|
<span className="rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-slate-300">Page {pagination.currentPage || 1} of {pagination.lastPage || 1}</span>
|
|
{pagination.nextPageUrl ? (
|
|
<Link href={pagination.nextPageUrl} preserveScroll className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]">
|
|
Next
|
|
<i className="fa-solid fa-arrow-right text-[10px]" />
|
|
</Link>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
) : null}
|
|
</>
|
|
)}
|
|
</div>
|
|
</main>
|
|
)
|
|
} |