import React, { useEffect, useRef, useState } from 'react' import { Link, router, usePage } from '@inertiajs/react' import SeoHead from '../../components/seo/SeoHead' import { postAcademyAction, trackAcademyEvent, trackUpgradeClick, useAcademyPageAnalytics } from '../../lib/academyAnalytics' function academyHref(section, slug) { return `/academy/${section}/${encodeURIComponent(slug)}` } function AcademyBreadcrumbs({ items = [] }) { if (!items.length) return null return ( ) } function slugifyHeading(value, fallback = 'section') { const normalized = String(value || '') .toLowerCase() .trim() .replace(/[^a-z0-9\s-]/g, '') .replace(/\s+/g, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, '') return normalized || fallback } function formatLessonDate(value) { if (!value) return 'Recently updated' const date = new Date(value) if (Number.isNaN(date.getTime())) return 'Recently updated' return new Intl.DateTimeFormat('en', { month: 'short', day: 'numeric', year: 'numeric' }).format(date) } function formatLessonMinutes(minutes) { const value = Number(minutes || 0) return value > 0 ? `${value} min read` : 'Quick read' } function normalizePromptAccessLevel(accessLevel) { const value = String(accessLevel || 'free').trim().toLowerCase() return value === 'creator' || value === 'pro' ? value : 'free' } function promptRequirementText(accessLevel) { const level = normalizePromptAccessLevel(accessLevel) if (level === 'pro') return 'Requires Pro access.' if (level === 'creator') return 'Requires Creator or Pro access.' return null } function promptUnlockHeading(accessLevel) { const level = normalizePromptAccessLevel(accessLevel) if (level === 'pro') return 'Unlock the full Pro prompt.' if (level === 'creator') return 'Unlock the full Creator prompt.' return 'Unlock the full prompt.' } function promptUnlockDescription(accessLevel) { const level = normalizePromptAccessLevel(accessLevel) if (level === 'pro') { return 'Get the complete reusable prompt, negative prompt, workflow notes, model settings, and variation strategy.' } if (level === 'creator') { return 'Get the complete reusable prompt, negative prompt, workflow notes, and creative workflow.' } return 'Get the complete reusable prompt and workflow notes.' } function promptInlineImage(url, thumbUrl) { return thumbUrl || url || '' } function formatMetaDisplay(value) { const normalized = String(value || '').trim() if (!normalized) return '' return normalized .replace(/[_-]+/g, ' ') .replace(/\b\w/g, (character) => character.toUpperCase()) } function StatPill({ label, value, icon, accentClassName = 'border-white/10 bg-white/[0.04] text-slate-300', valueClassName = 'text-white' }) { return (

{label}

{value}

{icon ? ( ) : null}
) } function PromptHeaderStat({ label, value, icon, accentClassName = 'border-white/10 bg-white/[0.04] text-slate-300', valueClassName = 'text-white' }) { return (

{label}

{value}

{icon ? ( ) : null}
) } function LessonInfoRow({ label, value }) { return (
{label} {value}
) } function LessonNavCard({ direction, lesson }) { if (!lesson) return null const eyebrow = direction === 'previous' ? 'Previous lesson' : 'Next lesson' const alignClass = direction === 'previous' ? 'items-start text-left' : 'items-end text-right' const href = lesson.course_url || `/academy/lessons/${lesson.slug}` return (

{eyebrow}

{lesson.lesson_label ?

{lesson.lesson_label}

: null}

{lesson.title}

{lesson.excerpt || lesson.content_preview || 'Open the next step in this Academy sequence.'}

) } function LockedPanel({ pricingUrl, label, accessLevel, onUpgrade }) { const isPrompt = label === 'prompt' const requirement = promptRequirementText(accessLevel) return (

Premium content

{isPrompt ? promptUnlockHeading(accessLevel) : `Unlock the full ${label}.`}

{isPrompt ? promptUnlockDescription(accessLevel) : 'This preview is visible, but the full Academy content stays server-side until your account has the required access.'}

{requirement ?

{requirement}

: null} See Academy plans
) } function copyTextToClipboard(text) { const source = String(text || '') if (!source) return Promise.reject(new Error('Nothing to copy')) if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') { return navigator.clipboard.writeText(source) } const textarea = document.createElement('textarea') textarea.value = source textarea.setAttribute('readonly', 'true') textarea.style.position = 'fixed' textarea.style.top = '-1000px' textarea.style.left = '-1000px' document.body.appendChild(textarea) textarea.select() try { if (document.execCommand('copy')) { return Promise.resolve() } } finally { document.body.removeChild(textarea) } return Promise.reject(new Error('Clipboard unavailable')) } function PromptCopyButton({ prompt, label = 'Copy prompt', analytics = null, contentId = null, eventType = 'academy_prompt_copy', metadata = {} }) { const [status, setStatus] = useState('idle') const resetTimerRef = useRef(0) return ( ) } function ImageLightbox({ gallery, onClose, onNavigate }) { useEffect(() => { if (!gallery?.images?.length) return undefined const handleEscape = (event) => { if (event.key === 'Escape') { onClose() return } if (event.key === 'ArrowLeft') { onNavigate(-1) return } if (event.key === 'ArrowRight') { onNavigate(1) } } document.body.style.overflow = 'hidden' window.addEventListener('keydown', handleEscape) return () => { document.body.style.overflow = '' window.removeEventListener('keydown', handleEscape) } }, [gallery, onClose, onNavigate]) const images = Array.isArray(gallery?.images) ? gallery.images : [] const currentIndex = Math.max(0, Math.min(images.length - 1, Number(gallery?.index || 0))) const currentImage = images[currentIndex] if (!currentImage?.src) return null return (
{images.length > 1 ? ( ) : null} {images.length > 1 ? ( ) : null}
event.stopPropagation()}> {currentImage.alt {images.length > 1 ? (

{currentImage.alt || `Image ${currentIndex + 1}`}

{`Image ${currentIndex + 1} of ${images.length}`}

{images.map((image, index) => (
) : null}
) } function PromptToolNoteCard({ note, index, galleryIndex, onOpenImage }) { if (!note || typeof note !== 'object') return null const displayType = String(note.display_type || '').trim() const eyebrowLabel = displayType || 'AI comparison' const title = note.model_name || note.provider || `${displayType || 'Comparison'} ${String(index + 1).padStart(2, '0')}` const subtitle = [note.provider, note.model_name].filter(Boolean).join(' · ') const previewUrl = promptInlineImage(note.image_url, note.thumb_url) const hasContent = Boolean(displayType || note.notes || note.strengths || note.weaknesses || note.best_for || note.settings || previewUrl || note.score || subtitle) if (!hasContent) return null return (
{previewUrl ? ( ) : null}

{eyebrowLabel}

{title}

{subtitle ?

{subtitle}

: null}
{String(index + 1).padStart(2, '0')} {note.score ? {`Score ${note.score}/10`} : null}
{note.settings ? (

Generated in

{note.settings}

) : null} {note.notes ? (

Overall notes

{note.notes}

) : null} {note.best_for ? (

Best for

{note.best_for}

) : null}
{note.strengths ? (

Strengths

{note.strengths}

) : null} {note.weaknesses ? (

Weaknesses

{note.weaknesses}

) : null}
) } function normalizePromptDocumentation(documentation) { const source = documentation && typeof documentation === 'object' && !Array.isArray(documentation) ? documentation : {} const list = (key) => (Array.isArray(source[key]) ? source[key] : []) .map((item) => String(item || '').trim()) .filter(Boolean) return { summary: String(source.summary || '').trim(), best_for: list('best_for'), how_to_use: list('how_to_use'), required_inputs: list('required_inputs'), workflow: list('workflow'), tips: list('tips'), common_mistakes: list('common_mistakes'), data_accuracy_notes: list('data_accuracy_notes'), display_notes: String(source.display_notes || '').trim(), } } function PromptDocumentationPanel({ documentation }) { const hasContent = Boolean( documentation.summary || documentation.display_notes || documentation.best_for.length || documentation.how_to_use.length || documentation.required_inputs.length || documentation.workflow.length || documentation.tips.length || documentation.common_mistakes.length || documentation.data_accuracy_notes.length, ) if (!hasContent) return null return (

How to use

Prompt documentation

{documentation.summary ?

{documentation.summary}

: null}
{documentation.best_for.length ? (

Best for

{documentation.best_for.map((item) => ( {item} ))}
) : null}
{documentation.how_to_use.length ? (

How to use

    {documentation.how_to_use.map((step, index) => (
  1. {index + 1} {step}
  2. ))}
) : null} {documentation.workflow.length ? (

Workflow

    {documentation.workflow.map((step, index) => (
  1. {index + 1} {step}
  2. ))}
) : null}
{documentation.required_inputs.length ? (

Required inputs

    {documentation.required_inputs.map((item) =>
  • {item}
  • )}
) : null} {documentation.tips.length ? (

Tips

    {documentation.tips.map((item) =>
  • {item}
  • )}
) : null} {documentation.common_mistakes.length ? (

Common mistakes

    {documentation.common_mistakes.map((item) =>
  • {item}
  • )}
) : null}
{documentation.data_accuracy_notes.length ? (

Data accuracy notes

    {documentation.data_accuracy_notes.map((item) =>
  • {item}
  • )}
) : null} {documentation.display_notes ? (

Display note

{documentation.display_notes}

) : null}
) } function PromptPlaceholderCard({ placeholder }) { if (!placeholder || typeof placeholder !== 'object') return null const example = placeholder.example const defaultValue = placeholder.default const renderValue = (value) => { if (value == null || value === '') return null if (typeof value === 'object') { return
{JSON.stringify(value, null, 2)}
} return

{String(value)}

} return (

Placeholder

[{placeholder.key || 'VALUE'}] {placeholder.label ?

{placeholder.label}

: null}
{placeholder.type ? {placeholder.type} : null} {placeholder.required ? Required : null}
{placeholder.description ?

{placeholder.description}

: null} {example != null && example !== '' ? (

Example

{renderValue(example)}
) : null} {defaultValue != null && defaultValue !== '' ? (

Default

{renderValue(defaultValue)}
) : null}
) } function PromptFilledExampleCard({ example, analytics, contentId, index }) { const placeholderEntries = Object.entries(example?.placeholder_values || {}).filter(([key, value]) => String(key || '').trim() && value != null && value !== '' && value !== false) return (

Filled example {index + 1}

{example?.title || `Example ${index + 1}`}

{example?.description ?

{example.description}

: null}
{example?.prompt ? ( ) : null}
{placeholderEntries.length ? (
{placeholderEntries.map(([key, value]) => ( {key}: {String(value)} ))}
) : null} {example?.prompt ?
{example.prompt}
: null} {example?.negative_prompt ? (

Negative prompt

{example.negative_prompt}
) : null}
) } function PromptFilledExamplesSection({ examples, analytics, contentId }) { const visibleExamples = Array.isArray(examples) ? examples.filter((example) => example && typeof example === 'object') : [] const [activeExampleIndex, setActiveExampleIndex] = useState(0) const examplesScrollRef = useRef(null) const [canScrollExamplesLeft, setCanScrollExamplesLeft] = useState(false) const [canScrollExamplesRight, setCanScrollExamplesRight] = useState(false) useEffect(() => { if (typeof window === 'undefined') { return undefined } const updateExampleScrollState = () => { const element = examplesScrollRef.current if (!element) { setCanScrollExamplesLeft(false) setCanScrollExamplesRight(false) return } const maxScrollLeft = Math.max(0, element.scrollWidth - element.clientWidth) setCanScrollExamplesLeft(element.scrollLeft > 6) setCanScrollExamplesRight(element.scrollLeft < maxScrollLeft - 6) } updateExampleScrollState() const element = examplesScrollRef.current if (!element) { return undefined } element.addEventListener('scroll', updateExampleScrollState, { passive: true }) window.addEventListener('resize', updateExampleScrollState, { passive: true }) return () => { element.removeEventListener('scroll', updateExampleScrollState) window.removeEventListener('resize', updateExampleScrollState) } }, [visibleExamples.length]) useEffect(() => { if (!visibleExamples.length) { setActiveExampleIndex(0) return } setActiveExampleIndex((current) => Math.max(0, Math.min(current, visibleExamples.length - 1))) }, [visibleExamples.length]) if (!visibleExamples.length) return null const activeExample = visibleExamples[activeExampleIndex] || visibleExamples[0] const activeExampleLabel = String(activeExample?.title || '').trim() || `Example ${activeExampleIndex + 1}` const activeExampleDescription = String(activeExample?.description || '').trim() const scrollExamples = (direction) => { const element = examplesScrollRef.current if (!element) return const amount = Math.max(220, Math.floor(element.clientWidth * 0.65)) element.scrollBy({ left: direction === 'left' ? -amount : amount, behavior: 'smooth', }) } return (

Selected example

{activeExampleLabel}

{activeExampleDescription ?

{activeExampleDescription}

: null}
) } function PromptHelperPromptCard({ helperPrompt, analytics, contentId }) { if (!helperPrompt || typeof helperPrompt !== 'object') return null return (

Helper prompt

{helperPrompt.title || 'Helper prompt'}

{helperPrompt.description ?

{helperPrompt.description}

: null}
{helperPrompt.type ? {formatMetaDisplay(helperPrompt.type)} : null} {helperPrompt.expected_output ? {helperPrompt.expected_output} : null}

Prompt text

{helperPrompt.prompt}
) } function PromptVariantCard({ variant, analytics, contentId }) { if (!variant || typeof variant !== 'object') return null return (

Prompt variant

{variant.title || 'Variant'}

{variant.description ?

{variant.description}

: null}
{variant.recommended ? Recommended : null} {variant.slug ? {variant.slug} : null}
{variant.recommended_for?.length ? (
{variant.recommended_for.map((item) => ( {item} ))}
) : null}

Variant prompt

{variant.prompt}
{variant.negative_prompt ? (

Negative prompt

{variant.negative_prompt}
) : null} {variant.risk_notes?.length ? (

Risk notes

    {variant.risk_notes.map((item) =>
  • {item}
  • )}
) : null}
) } 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) { setActiveVariantKey('') return } const recommendedVariant = visibleVariants.find((variant) => variant?.recommended) const nextDefaultKey = String(recommendedVariant?.slug || recommendedVariant?.title || visibleVariants[0]?.slug || visibleVariants[0]?.title || 'variant-0') setActiveVariantKey((current) => { if (visibleVariants.some((variant, index) => String(variant?.slug || variant?.title || `variant-${index}`) === current)) { return current } return nextDefaultKey }) }, [visibleVariants]) if (!visibleVariants.length) return null const activeVariant = visibleVariants.find((variant, index) => String(variant?.slug || variant?.title || `variant-${index}`) === activeVariantKey) || visibleVariants[0] return (

Variants

Alternative prompt versions

Switch between safer, shorter, or more specialized prompt variants without losing the core creative direction.

) } function PromptPublicExampleCard({ example, index, galleryIndex, onOpenImage, className = '', frameClassName }) { if (!example || typeof example !== 'object') return null const previewUrl = promptInlineImage(example.image_url, example.thumb_url) if (!previewUrl) return null const title = example.title || `Prompt Example ${index + 1}` const subtitle = [example.provider, example.model_name].filter(Boolean).join(' · ') const resolvedFrameClassName = frameClassName || (index === 0 ? 'aspect-[6/5]' : 'aspect-[4/5]') return (
) } function AiComparisonSection({ block }) { const payload = block?.payload || {} const criteria = Array.isArray(payload.criteria) ? payload.criteria.filter(Boolean) : [] const results = Array.isArray(block?.comparison_results) ? block.comparison_results.filter((result) => result?.active !== false) : [] const hasPrompt = Boolean(payload.prompt) const hasNegativePrompt = Boolean(payload.negative_prompt) const hasUsefulData = Boolean(block?.title || payload.title || payload.intro || hasPrompt || hasNegativePrompt || payload.aspect_ratio || criteria.length || results.length) if (!hasUsefulData) return null return (

AI Model Comparison

{payload.title || block.title || 'Same Prompt, Different AI Models'}

{payload.intro ?

{payload.intro}

: null}
{payload.aspect_ratio ?
Aspect ratio {payload.aspect_ratio}
: null}
{hasPrompt ? (

Prompt used

Shared source prompt across all compared models

{payload.prompt}
{hasNegativePrompt ? (

Negative prompt

{payload.negative_prompt}
) : null}
) : null} {criteria.length ? (

What we compare

{criteria.map((criterion) => ( {criterion} ))}
) : null} {results.length ? (
{results.map((result) => { const imageUrl = result.thumb_url || result.image_url || result.thumb_path || result.image_path || '' const score = Number(result.score || 0) const hasScore = Number.isFinite(score) && score > 0 const altText = `${result.model_name || 'AI model'} by ${result.provider || 'unknown provider'} result for ${payload.prompt || 'comparison prompt'}` return (
{imageUrl ? ( {altText} ) : (
No comparison image provided.
)}

{result.model_name || result.provider || 'AI model'}

{result.provider ?

{result.provider}

: null}
{hasScore ?
{`Skinbase score ${score}/10`}
: null}
{result.settings ? (

Settings

{result.settings}

) : null} {result.strengths ? (

Strengths

{result.strengths}

) : null} {result.weaknesses ? (

Weaknesses

{result.weaknesses}

) : null} {result.best_for ? (

Best for

{result.best_for}

) : null}
) })}
) : null}
) } export default function AcademyShow({ pageType, item, relatedLessons = [], relatedCourses = [], previousLesson = null, nextLesson = null, seo, pricingUrl, completeUrl, completed: initialCompleted, saveUrl, unsaveUrl, saved: initialSaved, submitUrl, courseContext = null, interaction = null, interactionRoutes = null, loginUrl = null, analytics = null, progressRoutes = null }) { const flash = usePage().props.flash || {} useAcademyPageAnalytics(analytics) const [completed, setCompleted] = useState(Boolean(initialCompleted)) const [saved, setSaved] = useState(Boolean(initialSaved)) const [liked, setLiked] = useState(Boolean(interaction?.liked)) const [likesCount, setLikesCount] = useState(Number(interaction?.likes_count || 0)) const [savesCount, setSavesCount] = useState(Number(interaction?.saves_count || 0)) const [tableOfContents, setTableOfContents] = useState([]) const [activeHeadingId, setActiveHeadingId] = useState('') const [lightboxGallery, setLightboxGallery] = useState(null) const articleContentRef = useRef(null) const handledInitialHashRef = useRef(false) const lessonCover = item?.cover_image_url || item?.cover_image || '' const articleCover = item?.article_cover_image_url || item?.article_cover_image || '' const lessonCategory = item?.category?.name || 'Academy' const lessonSeries = String(item?.series_name || '').trim() || lessonCategory const lessonDifficulty = item?.difficulty || 'Intermediate' const lessonMinutes = formatLessonMinutes(item?.reading_minutes) const lessonUpdated = formatLessonDate(item?.published_at) const lessonBlocks = Array.isArray(item?.blocks) ? item.blocks : [] const relatedLessonList = Array.isArray(relatedLessons) ? relatedLessons : [] const relatedCourseList = Array.isArray(relatedCourses) ? relatedCourses : [] const courseOutline = Array.isArray(courseContext?.outline) ? courseContext.outline : [] const lessonSummary = item.excerpt || item.description || item.prompt_preview || item.content_preview || 'A focused Academy lesson with practical guidance and examples.' const lessonTags = Array.isArray(item?.tags) ? item.tags.filter(Boolean) : [] const promptPreviewImage = item?.preview_image || '' const promptPreviewThumbImage = item?.preview_image_thumb || promptPreviewImage const promptPreviewSrcSet = item?.preview_image_srcset || '' const promptBody = item?.prompt || item?.prompt_preview || '' const promptDocumentation = normalizePromptDocumentation(item?.documentation) const promptPlaceholders = Array.isArray(item?.placeholders) ? item.placeholders.filter((placeholder) => placeholder && typeof placeholder === 'object' && [ placeholder.key, placeholder.label, placeholder.description, placeholder.example, placeholder.default, placeholder.type, ].some((value) => value != null && value !== '' && value !== false)) : [] const promptHelperPrompts = Array.isArray(item?.helper_prompts) ? item.helper_prompts.filter((helperPrompt) => helperPrompt && typeof helperPrompt === 'object' && [ helperPrompt.title, helperPrompt.description, helperPrompt.prompt, helperPrompt.expected_output, helperPrompt.type, ].some(Boolean)) : [] const promptVariants = Array.isArray(item?.prompt_variants) ? item.prompt_variants.filter((variant) => variant && typeof variant === 'object' && [ variant.title, variant.description, variant.prompt, variant.negative_prompt, variant.slug, variant.recommended, ...(Array.isArray(variant.recommended_for) ? variant.recommended_for : []), ...(Array.isArray(variant.risk_notes) ? variant.risk_notes : []), ].some((value) => value != null && value !== '' && value !== false)) : [] const promptPublicExamples = Array.isArray(item?.public_examples) ? item.public_examples.filter((example) => example && typeof example === 'object' && [ example.title, example.caption, example.image_path, example.image_url, example.thumb_path, example.thumb_url, example.provider, example.model_name, example.score, ].some(Boolean)) : [] const promptComparisons = Array.isArray(item?.tool_notes) ? item.tool_notes.filter((note) => note && typeof note === 'object' && note.active !== false && [ note.display_type, note.provider, note.model_name, note.notes, note.strengths, note.weaknesses, note.best_for, note.image_path, note.image_url, note.thumb_path, note.thumb_url, note.settings, note.score, ].some(Boolean)) : [] const promptUsageNotes = String(item?.usage_notes || '').trim() const promptWorkflowNotes = String(item?.workflow_notes || '').trim() const promptHasFullAccess = Boolean(item?.prompt) const hasPromptDocumentation = Boolean( promptDocumentation.summary || promptDocumentation.display_notes || promptDocumentation.best_for.length || promptDocumentation.how_to_use.length || promptDocumentation.required_inputs.length || promptDocumentation.workflow.length || promptDocumentation.tips.length || promptDocumentation.common_mistakes.length || promptDocumentation.data_accuracy_notes.length, ) const hasPromptPlaceholders = Boolean(item?.has_placeholder_inputs) && promptPlaceholders.length > 0 const promptFilledExamples = Array.isArray(item?.filled_examples) ? item.filled_examples.filter((example) => example && typeof example === 'object' && [ example.title, example.description, example.prompt, example.negative_prompt, ...(example.placeholder_values && typeof example.placeholder_values === 'object' ? Object.values(example.placeholder_values) : []), ].some((value) => value != null && value !== '' && value !== false)) : [] const hasPromptFilledExamples = promptFilledExamples.length > 0 const promptFilledExamplesTotal = Number(item?.filled_examples_total || promptFilledExamples.length || 0) const promptHasMoreFilledExamples = Boolean(item?.has_more_filled_examples) || promptFilledExamplesTotal > promptFilledExamples.length const promptHasFullFilledExamplesAccess = Boolean(item?.has_full_filled_examples_access) const promptHasLockedFilledExamples = Boolean(item?.has_filled_examples) && (!Boolean(item?.can_access_filled_examples) || promptHasMoreFilledExamples) const promptHasLockedHelperPrompts = Boolean(item?.has_helper_prompts) && !promptHasFullAccess const promptHasLockedVariants = Boolean(item?.has_prompt_variants) && !promptHasFullAccess const hasPromptHelperPrompts = promptHelperPrompts.length > 0 const hasPromptVariants = promptVariants.length > 0 const showPromptHelperPrompts = true const promptAccessRequirement = item?.access_requirement || promptRequirementText(item?.access_level) const promptUnlockTitle = item?.unlock_heading || promptUnlockHeading(item?.access_level) 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 promptComparisonGalleryImages = promptComparisons .map((note, index) => { const src = note.image_url || note.thumb_url || '' if (!src) return null return { src, alt: note.model_name || note.provider || `Comparison ${index + 1}`, } }) .filter(Boolean) const promptPublicExampleGalleryImages = [ ...(promptPreviewImage ? [{ src: promptPreviewImage, alt: item?.title || 'Prompt preview' }] : []), ...promptPublicExamples .map((example, index) => { const src = example.image_url || example.thumb_url || '' if (!src) return null return { src, alt: example.alt || example.title || `Prompt example ${index + 1}`, } }) .filter(Boolean), ] const promptBestUseCase = promptComparisons[0]?.best_for || promptDocumentation.best_for[0] || promptUsageNotes || lessonSummary const academyBreadcrumbs = pageType === 'prompt' ? [ { label: 'Academy', href: '/academy' }, { label: 'Prompt Library', href: '/academy/prompts' }, { label: item?.title || 'Prompt' }, ] : [] const fontScaleStorageKey = 'academy.lesson.font-scale' const fontScaleMin = 0.95 const fontScaleMax = 1.12 const fontScaleStep = 0.04 const [lessonFontScale, setLessonFontScale] = useState(1.04) const findArticleHeading = (headingId) => { if (!headingId || typeof document === 'undefined') { return null } const escapedHeadingId = typeof CSS !== 'undefined' && typeof CSS.escape === 'function' ? CSS.escape(headingId) : String(headingId).replace(/[^a-zA-Z0-9_-]/g, '') return articleContentRef.current?.querySelector(`#${escapedHeadingId}`) || document.getElementById(headingId) } const markComplete = () => { if (!completeUrl || completed) return router.post(completeUrl, courseContext?.completePayload || {}, { preserveScroll: true, onSuccess: () => setCompleted(true), }) } const requireLogin = () => { if (loginUrl && typeof window !== 'undefined') { window.location.href = loginUrl } } const toggleLike = async () => { if (!interactionRoutes?.like || !analytics?.contentType || !analytics?.contentId) { return } if (analytics?.isGuest) { requireLogin() return } const payload = await postAcademyAction(interactionRoutes.like, { content_type: analytics.contentType, content_id: analytics.contentId, }) if (payload?.liked !== undefined) { setLiked(Boolean(payload.liked)) setLikesCount(Number(payload.likes_count || 0)) } } const toggleSave = async () => { if (interactionRoutes?.save && analytics?.contentType && analytics?.contentId) { if (analytics?.isGuest) { requireLogin() return } const payload = await postAcademyAction(interactionRoutes.save, { content_type: analytics.contentType, content_id: analytics.contentId, }) if (payload?.saved !== undefined) { setSaved(Boolean(payload.saved)) setSavesCount(Number(payload.saves_count || 0)) } return } const url = saved ? unsaveUrl : saveUrl if (!url) return const method = saved ? router.delete : router.post method(url, {}, { preserveScroll: true, onSuccess: () => setSaved(!saved), }) } useEffect(() => { if (pageType !== 'lesson' || !progressRoutes?.startLesson || !item?.id || analytics?.isGuest || completed || typeof window === 'undefined') { return } const onceKey = `academy-start-lesson:${item.id}:${courseContext?.id || 'solo'}` if (window.sessionStorage.getItem(onceKey)) { return } window.sessionStorage.setItem(onceKey, '1') void postAcademyAction(progressRoutes.startLesson, { lesson_id: item.id, course_id: courseContext?.id || null, }) }, [analytics?.isGuest, completed, courseContext?.id, item?.id, pageType, progressRoutes?.startLesson]) const decreaseFontSize = () => { setLessonFontScale((current) => Math.max(fontScaleMin, Number((current - fontScaleStep).toFixed(2)))) } const increaseFontSize = () => { setLessonFontScale((current) => Math.min(fontScaleMax, Number((current + fontScaleStep).toFixed(2)))) } const openPromptPreviewImage = () => { if (!promptPreviewImage) return setLightboxGallery({ images: [{ src: promptPreviewImage, alt: item?.title || 'Prompt preview' }], index: 0, }) } const openPromptComparisonGallery = (index) => { if (!promptComparisonGalleryImages.length) return setLightboxGallery({ images: promptComparisonGalleryImages, index: Math.max(0, Math.min(promptComparisonGalleryImages.length - 1, Number(index || 0))), }) } const openPromptExampleGallery = (index) => { if (!promptPublicExampleGalleryImages.length) return setLightboxGallery({ images: promptPublicExampleGalleryImages, index: Math.max(0, Math.min(promptPublicExampleGalleryImages.length - 1, Number(index || 0))), }) } const navigateLightboxGallery = (direction) => { setLightboxGallery((current) => { if (!current?.images?.length) return current const total = current.images.length const nextIndex = typeof direction === 'number' && Math.abs(direction) > 1 ? Math.max(0, Math.min(total - 1, current.index + direction)) : (current.index + direction + total) % total return { ...current, index: nextIndex, } }) } const scrollToHeading = (headingId, behavior = 'smooth') => { if (typeof window === 'undefined') { return } const heading = findArticleHeading(headingId) if (!heading) { return } const top = Math.max(0, window.scrollY + heading.getBoundingClientRect().top - 112) window.scrollTo({ top, behavior }) setActiveHeadingId(headingId) if (window.history?.replaceState) { window.history.replaceState(null, '', `${window.location.pathname}${window.location.search}#${headingId}`) } } useEffect(() => { handledInitialHashRef.current = false }, [item?.slug]) useEffect(() => { if (pageType !== 'lesson' || !item?.content || !articleContentRef.current) { setTableOfContents([]) setActiveHeadingId('') return } const headings = Array.from(articleContentRef.current.querySelectorAll('h2, h3')) const seenIds = new Map() const nextTableOfContents = headings.map((heading, index) => { const baseId = slugifyHeading(heading.textContent, `section-${index + 1}`) const seenCount = seenIds.get(baseId) ?? 0 const nextId = seenCount > 0 ? `${baseId}-${seenCount + 1}` : baseId seenIds.set(baseId, seenCount + 1) heading.id = nextId heading.style.scrollMarginTop = '128px' return { id: nextId, title: heading.textContent?.trim() || `Section ${index + 1}`, level: heading.tagName.toLowerCase(), } }) setTableOfContents(nextTableOfContents) }, [item?.content, pageType]) useEffect(() => { if (pageType !== 'lesson' || tableOfContents.length === 0 || handledInitialHashRef.current || typeof window === 'undefined') { return } const hash = window.location.hash.replace(/^#/, '').trim() if (!hash) { handledInitialHashRef.current = true return } const matchingEntry = tableOfContents.find((entry) => entry.id === hash) if (!matchingEntry) { handledInitialHashRef.current = true return } handledInitialHashRef.current = true window.requestAnimationFrame(() => scrollToHeading(matchingEntry.id, 'auto')) }, [pageType, tableOfContents]) useEffect(() => { if (pageType !== 'lesson' || tableOfContents.length === 0 || typeof window === 'undefined') { return undefined } const handleHashChange = () => { const hash = window.location.hash.replace(/^#/, '').trim() if (!hash) { return } const matchingEntry = tableOfContents.find((entry) => entry.id === hash) if (!matchingEntry) { return } window.requestAnimationFrame(() => scrollToHeading(matchingEntry.id, 'auto')) } window.addEventListener('hashchange', handleHashChange) return () => window.removeEventListener('hashchange', handleHashChange) }, [pageType, tableOfContents]) useEffect(() => { if (pageType !== 'lesson' || tableOfContents.length === 0 || !articleContentRef.current) { setActiveHeadingId('') return } const getActiveId = () => { const headings = Array.from(articleContentRef.current.querySelectorAll('h2[id], h3[id]')) if (!headings.length) return '' // offset accounts for sticky header height + small buffer const offset = 140 let activeId = headings[0].id for (const heading of headings) { if (heading.getBoundingClientRect().top <= offset) { activeId = heading.id } } return activeId } setActiveHeadingId(getActiveId()) const onScroll = () => setActiveHeadingId(getActiveId()) window.addEventListener('scroll', onScroll, { passive: true }) return () => window.removeEventListener('scroll', onScroll) }, [pageType, tableOfContents, lessonFontScale]) useEffect(() => { if (typeof window === 'undefined') { return } const storedValue = Number(window.localStorage.getItem(fontScaleStorageKey)) if (!Number.isFinite(storedValue)) { return } setLessonFontScale(Math.min(fontScaleMax, Math.max(fontScaleMin, storedValue))) }, [fontScaleMax, fontScaleMin, fontScaleStorageKey]) useEffect(() => { if (typeof window === 'undefined') { return } window.localStorage.setItem(fontScaleStorageKey, String(lessonFontScale)) }, [lessonFontScale]) useEffect(() => { if (pageType !== 'lesson' || !item?.content || !articleContentRef.current) { return } const codeBlocks = Array.from(articleContentRef.current.querySelectorAll('pre code')) if (!codeBlocks.length) { return } const fallbackCopyText = (text) => { const textarea = document.createElement('textarea') textarea.value = text textarea.setAttribute('readonly', 'true') textarea.style.position = 'fixed' textarea.style.top = '-1000px' textarea.style.left = '-1000px' document.body.appendChild(textarea) textarea.select() try { return document.execCommand('copy') } catch (_error) { return false } finally { document.body.removeChild(textarea) } } const copyText = (text) => { if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') { return navigator.clipboard.writeText(text) } return fallbackCopyText(text) ? Promise.resolve() : Promise.reject(new Error('Clipboard unavailable')) } codeBlocks.forEach((block) => { const pre = block.parentElement if (!pre || pre.dataset.academyCopyButtonMounted === 'true') { return } const button = document.createElement('button') const icon = document.createElement('span') const label = document.createElement('span') button.type = 'button' button.className = 'story-code-copy-button academy-code-copy-button' icon.className = 'story-code-copy-icon' icon.setAttribute('aria-hidden', 'true') icon.textContent = '⧉' label.className = 'story-code-copy-label' label.textContent = 'Copy' button.appendChild(icon) button.appendChild(label) button.dataset.copied = 'idle' button.setAttribute('aria-label', 'Copy code block') let resetTimer = 0 button.addEventListener('click', () => { const source = block.innerText || block.textContent || '' copyText(source) .then(() => { icon.textContent = '✓' label.textContent = 'Copied' button.dataset.copied = 'true' }) .catch(() => { icon.textContent = '!' label.textContent = 'Failed' button.dataset.copied = 'false' }) .finally(() => { window.clearTimeout(resetTimer) resetTimer = window.setTimeout(() => { icon.textContent = '⧉' label.textContent = 'Copy' button.dataset.copied = 'idle' }, 1800) }) }) pre.appendChild(button) pre.dataset.academyCopyButtonMounted = 'true' }) }, [item?.content, lessonFontScale, pageType]) return (
{flash.success ?
{flash.success}
: null} {flash.error ?
{flash.error}
: null} {item.locked ? trackUpgradeClick(analytics, { source: `${pageType}_locked_panel` })} /> : null} {pageType === 'lesson' ? (
Skinbase AI Academy Lesson {lessonCategory} {lessonDifficulty}
{item.lesson_label ?

{item.lesson_label}

: null}

{item.title}

{lessonSummary}

{lessonTags.length ? (
{lessonTags.map((tag) => ( {tag} ))}
) : null} {courseContext?.title ? (

Part of course

{courseContext.title}

{courseContext.subtitle || 'This lesson is being viewed inside a structured Academy course path.'}

) : null}
{completeUrl ? : null} {submitUrl ? Submit artwork : null}

Article

Lesson content

{lessonMinutes}
{Math.round(lessonFontScale * 100)}%
{articleCover ? (
{`${item.title}
) : null} {item.content ? (
{lessonBlocks.map((block) => )}
) : (
{item.content_preview}
{lessonBlocks.map((block) => )}
)}
{(previousLesson || nextLesson) ? (

{courseContext?.title ? 'Course navigation' : 'Lesson navigation'}

{courseContext?.title ? 'Continue this course' : 'Continue in order'}

) : null}
) : pageType === 'prompt' ? (
{academyBreadcrumbs.length ? (
) : null}
Skinbase AI Academy Prompt Library {lessonCategory} {lessonDifficulty} {item.aspect_ratio ? {item.aspect_ratio} : null} {item.prompt_of_week ? Prompt of the week : null} {item.featured ? Featured : null}

Prompt template

{item.title}

{lessonSummary}

{promptHasFullAccess ? : null} {promptHasFullAccess && item.negative_prompt ? : null}

Preview artwork

{promptPreviewImage ? Click to zoom : null}
{!promptHasFullAccess && (promptPreviewImage || promptPublicExamples.length) ? (

Public examples

Example results from this prompt

Preview the visual direction before unlocking the full prompt.

{item.locked && promptAccessRequirement ? {promptAccessRequirement} : null}
{promptPreviewImage ? ( ) : null} {promptFeaturedExamples.length ? (
{promptFeaturedExamples.map((example, index) => ( ))}
) : null}
{promptOverflowExamples.length ? (
{promptOverflowExamples.map((example, index) => ( ))}
) : null}
) : null}

Prompt body

Prompt text and exclusions

{promptHasFullAccess ? 'Full prompt' : 'Preview prompt'}

{promptHasFullAccess ? 'Ready to paste into your generation workflow.' : 'Upgrade your Academy access to reveal the complete prompt text.'}

{promptBody ? ( ) : null}
{promptBody || 'Prompt text is not available yet.'}
{!promptHasFullAccess ? (

{promptUnlockTitle || 'Unlock the full prompt'}

{promptUnlockDetails}

{promptAccessRequirement ?

{promptAccessRequirement}

: null}
) : null}
{item.negative_prompt ? (

Negative prompt

{item.negative_prompt}
) : null}
{(promptUsageNotes || promptWorkflowNotes) ? (

Prompt guidance

How to use this prompt

{!promptHasFullAccess ? Full notes visible with access : null}
{promptUsageNotes ? (

Usage notes

{promptUsageNotes}

) : null} {promptWorkflowNotes ? (

Workflow notes

{promptWorkflowNotes}

) : null}
) : null} {hasPromptDocumentation ? : null} {hasPromptPlaceholders ? (

Data

Placeholders and required inputs

Prepare these variables before using the final prompt so the output stays consistent and reusable.

{promptPlaceholders.map((placeholder, index) => ( ))}
) : null} {hasPromptFilledExamples ? (

Filled examples

{promptFilledExamplesTotal > 0 ? `${promptFilledExamplesTotal} ready-made prompt runs for real user inputs` : 'Ready-made prompt runs for real user inputs'}

{promptHasMoreFilledExamples ? `You can view ${promptFilledExamples.length} example${promptFilledExamples.length === 1 ? '' : 's'} right now. Upgrade to Pro to unlock all ${promptFilledExamplesTotal} filled prompt runs and copy a closer starting point instead of filling everything from scratch.` : 'These examples show how the prompt looks after swapping real placeholder values, so you can copy a closer starting point instead of filling everything from scratch.'}

) : null} {promptHasLockedFilledExamples ? (

Filled examples

{promptHasMoreFilledExamples && hasPromptFilledExamples ? `${Math.max(promptFilledExamplesTotal - promptFilledExamples.length, 0)} more filled prompt example${promptFilledExamplesTotal - promptFilledExamples.length === 1 ? '' : 's'} are available` : `${promptFilledExamplesTotal || 5} filled prompt examples are included`}

{promptHasMoreFilledExamples && hasPromptFilledExamples ? 'Creator access includes a smaller set here. Upgrade to Academy Pro to unlock the remaining filled prompt runs.' : 'This prompt ships with ready-made filled examples for different user inputs, but they unlock only for Academy Pro members.'}

trackUpgradeClick(analytics, { source: 'prompt_filled_examples_locked_panel' })} />
) : null} {showPromptHelperPrompts && hasPromptHelperPrompts ? (

Data helpers

Helper prompts for preparation and validation

Use these supporting prompts before or after the main prompt when you need better source data, cleaner structure, or a validation pass.

{promptHelperPrompts.map((helperPrompt, index) => ( ))}
) : null} {showPromptHelperPrompts && promptHasLockedHelperPrompts ? (

Data helpers

Helper prompts are included with this template

Data collection, validation, or refinement prompts are available once your Academy access matches this template.

trackUpgradeClick(analytics, { source: 'prompt_helper_locked_panel' })} />
) : null} {hasPromptVariants ? : null} {promptHasLockedVariants ? (

Variants

Alternative prompt versions are included

This prompt includes recommended or model-specific variants, but they stay locked until your Academy access level matches the template.

trackUpgradeClick(analytics, { source: 'prompt_variant_locked_panel' })} />
) : null} {promptComparisons.length ? (

AI model comparisons

How different models respond to the same prompt

Use these notes to decide which provider fits the result you want before you start tuning or post-processing.

{promptComparisons.map((note, index) => )}
) : null}
) : (
{pageType === 'pack' ? (

{item.description}

{(item.prompts || []).map((prompt) => (

{prompt.title}

{prompt.excerpt || prompt.prompt_preview}

))}
) : null} {pageType === 'challenge' ? (

Brief

{item.brief || item.description}

Rules

{item.rules || 'No special rules posted yet.'}
{(item.submissions || []).length ? (

Approved submissions

{item.submissions.map((submission) => (

{submission.artwork?.title || 'Submission'}

{submission.user?.name || 'Unknown creator'}

))}
) : null}
) : null}
)}
setLightboxGallery(null)} onNavigate={navigateLightboxGallery} />
) }