Skinbase AI Academy
{title}
{description}
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 (
{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.
{item?.category?.name || 'Academy'}
{Array.isArray(item?.tool_notes) && item.tool_notes.length ? {item.tool_notes.length} comparisons : null}{item.excerpt || item.description || item.prompt_preview || 'No description yet.'}
{item.tags?.length ?{item.tags.slice(0, 4).join(' · ')}
: null}{pageType.slice(0, -1)}
{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 (Skinbase AI Academy
{description}