import React from 'react' import { Link, router, usePage } from '@inertiajs/react' import SeoHead from '../../components/seo/SeoHead' import NovaSelect from '../../components/ui/NovaSelect' import { trackAcademySearchResultClick, trackUpgradeClick, useAcademyPageAnalytics } from '../../lib/academyAnalytics' function academyHref(section, slug) { return `/academy/${section}/${encodeURIComponent(slug)}` } function QueryFilters({ pageType, filters, categories }) { if (pageType !== 'lessons' && pageType !== 'prompts') { return null } const categoryOptions = [{ value: '', label: 'All categories' }, ...(categories || []).map((category) => ({ value: category.slug, label: category.name }))] const difficultyOptions = [ { value: '', label: 'All levels' }, { value: 'beginner', label: 'Beginner' }, { value: 'intermediate', label: 'Intermediate' }, { value: 'advanced', label: 'Advanced' }, { value: 'pro', label: 'Pro' }, ] return (
{ if (event.key !== 'Enter') return router.get(window.location.pathname, { ...filters, q: event.currentTarget.value }, { preserveState: true, preserveScroll: true }) }} /> router.get(window.location.pathname, { ...filters, category: nextValue || undefined }, { preserveState: true, preserveScroll: true })} options={categoryOptions} searchable={false} className="rounded-2xl bg-white/[0.04]" placeholder="All categories" /> router.get(window.location.pathname, { ...filters, difficulty: nextValue || undefined }, { preserveState: true, preserveScroll: true })} options={difficultyOptions} searchable={false} className="rounded-2xl bg-white/[0.04]" placeholder="All levels" />
) } function LockBadge({ item }) { if (!item?.locked) return {item.access_level} return Locked · {item.access_level} } function itemHref(pageType, item) { if (pageType === 'lessons') return academyHref('lessons', item.slug) if (pageType === 'prompts') return academyHref('prompts', item.slug) if (pageType === 'packs') return academyHref('packs', item.slug) return academyHref('challenges', item.slug) } function searchResultContentType(pageType) { if (pageType === 'prompts') return 'academy_prompt' if (pageType === 'lessons') return 'academy_lesson' if (pageType === 'packs') return 'academy_prompt_pack' if (pageType === 'challenges') return 'academy_challenge' return null } function PromptLibraryHero({ title, description, items, pricingUrl, totalCount }) { const featuredImages = (items || []) .map((item) => item?.preview_image) .filter(Boolean) .slice(0, 3) const primaryImage = featuredImages[0] || '' const supportingImages = featuredImages.slice(1, 3) return (
Skinbase AI Academy Prompt Library

{title}

{description}

Visual-first

Preview prompt results before opening the detail page.

Reusable

Templates for wallpapers, covers, worlds, portraits, and more.

Comparison-ready

See which prompts include provider-specific notes and outputs.

Upgrade preview {totalCount || 0} prompts available
{primaryImage ? ( <>
{supportingImages.length ? (
{supportingImages.map((image, index) => (
))}
) : null} ) : (
Prompt preview images will appear here
)}
) } function AcademyCard({ pageType, item, analytics, searchContext, position }) { const lessonSeries = String(item?.series_name || '').trim() const promptPreviewImage = item?.preview_image || '' const contentType = searchResultContentType(pageType) const href = itemHref(pageType, item) const trackSearchClick = () => { if (!searchContext?.query || !contentType) { return } trackAcademySearchResultClick(analytics, searchContext, { contentType, contentId: item?.id, position, }) } if (pageType === 'prompts') { return (
{promptPreviewImage ? : null}
Prompt template
{item?.difficulty ? {item.difficulty} : null} {item?.aspect_ratio ? {item.aspect_ratio} : null}

{item?.category?.name || 'Academy'}

{Array.isArray(item?.tool_notes) && item.tool_notes.length ? {item.tool_notes.length} comparisons : null}

{item.title}

{item.excerpt || item.description || item.prompt_preview || 'No description yet.'}

{item.tags?.length ?

{item.tags.slice(0, 4).join(' · ')}

: null}
) } return (

{pageType.slice(0, -1)}

{pageType === 'lessons' && item?.formatted_lesson_number ? (
{item.formatted_lesson_number} {lessonSeries ? {lessonSeries} : null}
) : null}

{item.title}

{item.excerpt || item.description || item.prompt_preview || item.content_preview || 'No description yet.'}

{pageType === 'lessons' && item.tags?.length ?

{item.tags.slice(0, 4).join(' · ')}

: null} {pageType === 'prompts' && item.tags?.length ?

{item.tags.slice(0, 4).join(' · ')}

: null} {pageType === 'challenges' ?

{item.status} · {item.submission_count ?? 0} submissions

: null} ) } async function fetchAcademyPage(url) { const response = await fetch(url, { headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest', }, credentials: 'same-origin', }) if (!response.ok) { throw new Error('Failed to load the next page.') } return response.json() } export default function AcademyList({ pageType, title, description, seo, items, filters, categories, pricingUrl, analytics }) { const flash = usePage().props.flash || {} useAcademyPageAnalytics(analytics) const searchContext = analytics?.search ? { query: analytics.search.query, normalizedQuery: analytics.search.normalizedQuery, resultsCount: analytics.search.resultsCount, filters, } : null const initialItems = React.useMemo(() => (Array.isArray(items?.data) ? items.data : []), [items]) const [visibleItems, setVisibleItems] = React.useState(initialItems) const [pagination, setPagination] = React.useState({ currentPage: Number(items?.current_page || 1), lastPage: Number(items?.last_page || 1), nextPageUrl: items?.next_page_url || null, }) const [loadingMore, setLoadingMore] = React.useState(false) const sentinelRef = React.useRef(null) React.useEffect(() => { setVisibleItems(initialItems) setPagination({ currentPage: Number(items?.current_page || 1), lastPage: Number(items?.last_page || 1), nextPageUrl: items?.next_page_url || null, }) setLoadingMore(false) }, [initialItems, items?.current_page, items?.last_page, items?.next_page_url, pageType]) const hasMorePages = pageType === 'prompts' && pagination.currentPage < pagination.lastPage && Boolean(pagination.nextPageUrl) const loadMore = React.useCallback(async () => { if (pageType !== 'prompts' || loadingMore || !pagination.nextPageUrl) { return } setLoadingMore(true) try { const payload = await fetchAcademyPage(pagination.nextPageUrl) const nextItems = Array.isArray(payload?.data) ? payload.data : [] setVisibleItems((current) => [...current, ...nextItems.filter((item) => !current.some((existing) => String(existing.id) === String(item.id)))]) setPagination({ currentPage: Number(payload?.current_page || pagination.currentPage), lastPage: Number(payload?.last_page || pagination.lastPage), nextPageUrl: payload?.next_page_url || null, }) } catch { setPagination((current) => ({ ...current, nextPageUrl: null })) } finally { setLoadingMore(false) } }, [loadingMore, pageType, pagination.currentPage, pagination.lastPage, pagination.nextPageUrl]) React.useEffect(() => { const sentinel = sentinelRef.current if (!sentinel || !hasMorePages || loadingMore || typeof window === 'undefined' || typeof window.IntersectionObserver !== 'function') { return undefined } const observer = new window.IntersectionObserver((entries) => { if (entries[0]?.isIntersecting) { void loadMore() } }, { rootMargin: '360px 0px' }) observer.observe(sentinel) return () => observer.disconnect() }, [hasMorePages, loadMore, loadingMore]) return (
{pageType === 'prompts' ? : (

Skinbase AI Academy

{title}

{description}

trackUpgradeClick(analytics, { source: `${pageType}_list_hero` })} className="rounded-full border border-sky-300/25 bg-sky-300/12 px-5 py-3 text-sm font-semibold text-sky-100">Upgrade preview
)} {flash.success ?
{flash.success}
: null} {flash.error ?
{flash.error}
: null} {visibleItems.length === 0 ? (
Nothing matched this Academy view yet.
) : ( <>
{visibleItems.map((item, index) => )}
{pageType === 'prompts' ? (
) : null} )}
) }