import React, { useEffect, useRef, useState } from 'react' import { Link, router, usePage } from '@inertiajs/react' import SeoHead from '../../components/seo/SeoHead' function academyHref(section, slug) { return `/academy/${section}/${encodeURIComponent(slug)}` } function AcademyBreadcrumbs({ items = [] }) { if (!items.length) return null return ( ) } 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 StatPill({ label, value }) { return (

{label}

{value}

) } 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 }) { return (

Premium content

Unlock the full {label}.

This preview is visible, but the full Academy content stays server-side until your account has the required Creator or Pro access.

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' }) { 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 title = note.model_name || note.provider || `Comparison ${String(index + 1).padStart(2, '0')}` const subtitle = [note.provider, note.model_name].filter(Boolean).join(' · ') const previewUrl = note.image_url || note.thumb_url || '' const hasContent = Boolean(note.notes || note.strengths || note.weaknesses || note.best_for || note.settings || previewUrl || note.score || subtitle) if (!hasContent) return null return (
{previewUrl ? ( ) : null}

AI comparison

{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 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 }) { const flash = usePage().props.flash || {} const [completed, setCompleted] = useState(Boolean(initialCompleted)) const [saved, setSaved] = useState(Boolean(initialSaved)) const [tableOfContents, setTableOfContents] = useState([]) const [activeHeadingId, setActiveHeadingId] = useState('') const [lightboxGallery, setLightboxGallery] = useState(null) const articleContentRef = useRef(null) const handledInitialHashRef = useRef(false) const lessonCover = item?.cover_image_url || item?.cover_image || '' const articleCover = item?.article_cover_image_url || item?.article_cover_image || '' const lessonCategory = item?.category?.name || 'Academy' const lessonSeries = String(item?.series_name || '').trim() || lessonCategory const lessonDifficulty = item?.difficulty || 'Intermediate' const lessonMinutes = formatLessonMinutes(item?.reading_minutes) const lessonUpdated = formatLessonDate(item?.published_at) const lessonBlocks = Array.isArray(item?.blocks) ? item.blocks : [] const relatedLessonList = Array.isArray(relatedLessons) ? relatedLessons : [] const relatedCourseList = Array.isArray(relatedCourses) ? relatedCourses : [] const courseOutline = Array.isArray(courseContext?.outline) ? courseContext.outline : [] const lessonSummary = item.excerpt || item.description || item.prompt_preview || item.content_preview || 'A focused Academy lesson with practical guidance and examples.' const lessonTags = Array.isArray(item?.tags) ? item.tags.filter(Boolean) : [] const promptPreviewImage = item?.preview_image || '' const promptBody = item?.prompt || item?.prompt_preview || '' const promptComparisons = Array.isArray(item?.tool_notes) ? item.tool_notes.filter((note) => note && typeof note === 'object' && note.active !== false && [ note.provider, note.model_name, note.notes, note.strengths, note.weaknesses, note.best_for, note.image_path, note.image_url, note.thumb_path, note.thumb_url, note.settings, note.score, ].some(Boolean)) : [] const promptUsageNotes = String(item?.usage_notes || '').trim() const promptWorkflowNotes = String(item?.workflow_notes || '').trim() const promptHasFullAccess = Boolean(item?.prompt) const promptModelsCovered = promptComparisons.map((note, index) => note.model_name || note.provider || `Model ${index + 1}`) const promptComparisonGalleryImages = promptComparisons .map((note, index) => { const src = note.image_url || note.thumb_url || '' if (!src) return null return { src, alt: note.model_name || note.provider || `Comparison ${index + 1}`, } }) .filter(Boolean) const academyBreadcrumbs = pageType === 'prompt' ? [ { label: 'Academy', href: '/academy' }, { label: 'Prompt Library', href: '/academy/prompts' }, { label: item?.title || 'Prompt' }, ] : [] const fontScaleStorageKey = 'academy.lesson.font-scale' const fontScaleMin = 0.95 const fontScaleMax = 1.12 const fontScaleStep = 0.04 const [lessonFontScale, setLessonFontScale] = useState(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 toggleSave = () => { const url = saved ? unsaveUrl : saveUrl const method = saved ? router.delete : router.post method(url, {}, { preserveScroll: true, onSuccess: () => setSaved(!saved), }) } 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 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 ? : null} {pageType === 'lesson' ? (
{lessonCover ? : null}
Skinbase AI Academy {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} {saveUrl ? : 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' ? (

Preview artwork

{promptPreviewImage ? Click to zoom : null}
{academyBreadcrumbs.length ? (
) : null}
Skinbase AI Academy {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}

{saveUrl ? : null} {promptBody ? : null} {item.negative_prompt ? : null}
{lessonTags.length ? (

Microtags

{lessonTags.map((tag) => ( {tag} ))}
) : null}

Prompt status

{item.locked ? 'This page shows the prompt summary, but the full prompt text and editor notes stay locked until your Academy access level matches the template.' : 'This template includes the main prompt, reuse guidance, and model-specific comparison notes in one place.'}

{promptModelsCovered.length ? (

Compared with

{promptModelsCovered.length} model{promptModelsCovered.length > 1 ? 's' : ''} documented for this prompt.

{promptModelsCovered.length}
{promptModelsCovered.map((model) => ( {model} ))}
) : 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 || 'Prompt text is not available yet.'}
{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} {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} />
) }