import React, { useEffect, useRef, useState } from 'react' import { Link, router, usePage } from '@inertiajs/react' import SeoHead from '../../components/seo/SeoHead' 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 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 }) { const [status, setStatus] = useState('idle') const resetTimerRef = useRef(0) 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 = [], seo, pricingUrl, completeUrl, completed: initialCompleted, saveUrl, unsaveUrl, saved: initialSaved, submitUrl }) { 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 articleContentRef = useRef(null) const lessonCover = item?.cover_image_url || item?.cover_image || '' const lessonCategory = item?.category?.name || 'Academy' 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 lessonSummary = item.excerpt || item.description || item.prompt_preview || item.content_preview || 'A focused Academy lesson with practical guidance and examples.' const fontScaleStorageKey = 'academy.lesson.font-scale' const fontScaleMin = 0.95 const fontScaleMax = 1.12 const fontScaleStep = 0.04 const [lessonFontScale, setLessonFontScale] = useState(() => { if (typeof window === 'undefined') { return 1.04 } const storedValue = Number(window.localStorage.getItem(fontScaleStorageKey)) if (Number.isFinite(storedValue)) { return Math.min(fontScaleMax, Math.max(fontScaleMin, storedValue)) } return 1.04 }) const markComplete = () => { if (!completeUrl || completed) return router.post(completeUrl, {}, { 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)))) } 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 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 || !articleContentRef.current) { setActiveHeadingId('') return } const headingElements = Array.from(articleContentRef.current.querySelectorAll('h2, h3')) if (!headingElements.length) { setActiveHeadingId('') return } const observer = new IntersectionObserver((entries) => { const visibleEntries = entries .filter((entry) => entry.isIntersecting) .sort((left, right) => left.boundingClientRect.top - right.boundingClientRect.top) if (visibleEntries.length) { setActiveHeadingId((current) => visibleEntries[0].target.id || current) } }, { root: null, rootMargin: '-18% 0px -68% 0px', threshold: [0, 1], }) headingElements.forEach((heading) => observer.observe(heading)) const firstVisibleHeading = headingElements.find((heading) => heading.getBoundingClientRect().top >= 0) || headingElements[0] if (firstVisibleHeading?.id) { setActiveHeadingId(firstVisibleHeading.id) } return () => observer.disconnect() }, [pageType, tableOfContents, lessonFontScale]) 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.title}

{lessonSummary}

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

Article

Lesson content

{lessonMinutes}
{Math.round(lessonFontScale * 100)}%
{item.content ? (
{lessonBlocks.map((block) => )}
) : (
{item.content_preview}
{lessonBlocks.map((block) => )}
)}
) : (
{pageType === 'prompt' ? (

Prompt

{item.prompt || item.prompt_preview}
{item.negative_prompt ?

Negative prompt

{item.negative_prompt}
: null}
) : 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}
)}
) }