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

@@ -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>