import React, { useEffect, useMemo, useState } from 'react' import { Head, Link, router, usePage } from '@inertiajs/react' import AdminLayout from '../../../Layouts/AdminLayout' import AccessBadge from '../../../components/academy/billing/AccessBadge' const PROMPT_VIEW_STORAGE_KEY = 'skinbase.admin.academy.prompts.view' const COURSE_VIEW_STORAGE_KEY = 'skinbase.admin.academy.courses.view' const PROMPT_VIEW_OPTIONS = [ { value: 'gallery', label: 'Gallery', icon: 'fa-images' }, { value: 'grid', label: 'Grid', icon: 'fa-grid-2' }, { value: 'table', label: 'Table', icon: 'fa-table-list' }, ] const COURSE_VIEW_OPTIONS = [ { value: 'grid', label: 'Grid', icon: 'fa-grid-2' }, { value: 'table', label: 'Table', icon: 'fa-table-list' }, ] function formatDateLabel(value) { if (!value) return 'Recently updated' const date = new Date(value) if (Number.isNaN(date.getTime())) return 'Recently updated' return new Intl.DateTimeFormat('en-GB', { day: 'numeric', month: 'short', year: 'numeric' }).format(date) } function paginationLabel(label) { return String(label || '') .replace(/«/g, 'Previous') .replace(/»/g, 'Next') .replace(/<[^>]+>/g, '') .trim() } function courseStatusMeta(status) { const normalized = String(status || 'draft') if (normalized === 'published') { return { label: 'Published', className: 'border-emerald-300/20 bg-emerald-300/10 text-emerald-100' } } if (normalized === 'review') { return { label: 'Review', className: 'border-amber-300/20 bg-amber-300/10 text-amber-100' } } if (normalized === 'archived') { return { label: 'Archived', className: 'border-white/10 bg-white/[0.04] text-slate-300' } } return { label: 'Draft', className: 'border-slate-500/20 bg-slate-500/10 text-slate-300' } } function courseAccessMeta(accessLevel) { const normalized = String(accessLevel || 'free') if (normalized === 'premium') { return { label: 'Premium', className: 'border-[#ffcfbf]/20 bg-[#ffcfbf]/10 text-[#fff0ea]' } } if (normalized === 'mixed') { return { label: 'Mixed', className: 'border-sky-300/20 bg-sky-300/10 text-sky-100' } } return { label: 'Free', className: 'border-white/10 bg-white/[0.05] text-slate-200' } } function courseSummary(items = [], summary = null) { if (summary && typeof summary === 'object') { return { total: Number(summary.total || 0), published: Number(summary.published || 0), featured: Number(summary.featured || 0), drafts: Number(summary.drafts || 0), visibleOnPage: Array.isArray(items) ? items.length : 0, } } return items.reduce((accumulator, item) => ({ total: accumulator.total + 1, published: accumulator.published + (item.status === 'published' ? 1 : 0), featured: accumulator.featured + (item.is_featured ? 1 : 0), drafts: accumulator.drafts + (item.status === 'draft' ? 1 : 0), visibleOnPage: accumulator.visibleOnPage + 1, }), { total: 0, published: 0, featured: 0, drafts: 0, visibleOnPage: 0 }) } function promptSummary(items = [], summary = null) { if (summary && typeof summary === 'object') { return { total: Number(summary.total || 0), active: Number(summary.active || 0), featured: Number(summary.featured || 0), promptOfWeek: Number(summary.promptOfWeek || 0), comparisons: Array.isArray(items) ? items.reduce((count, item) => count + Number(item.comparisons_count || 0), 0) : 0, access: { free: Number(summary.access?.free || 0), creator: Number(summary.access?.creator || 0), pro: Number(summary.access?.pro || 0), }, } } return items.reduce((summary, item) => ({ total: summary.total + 1, active: summary.active + (item.active ? 1 : 0), featured: summary.featured + (item.featured ? 1 : 0), promptOfWeek: summary.promptOfWeek + (item.prompt_of_week ? 1 : 0), comparisons: summary.comparisons + Number(item.comparisons_count || 0), access: { free: summary.access.free + (item.access_level === 'free' ? 1 : 0), creator: summary.access.creator + (item.access_level === 'creator' ? 1 : 0), pro: summary.access.pro + (item.access_level === 'pro' ? 1 : 0), }, }), { total: 0, active: 0, featured: 0, promptOfWeek: 0, comparisons: 0, access: { free: 0, creator: 0, pro: 0 } }) } function PromptFlag({ children, tone = 'default' }) { const toneClass = tone === 'warm' ? 'border-[#ffcfbf]/20 bg-[#ffcfbf]/10 text-[#fff0ea]' : tone === 'sky' ? 'border-sky-300/20 bg-sky-300/10 text-sky-100' : tone === 'emerald' ? 'border-emerald-300/20 bg-emerald-300/10 text-emerald-100' : 'border-white/10 bg-white/[0.05] text-slate-200' return {children} } function CoursePill({ children, tone = 'default' }) { const toneClass = tone === 'warm' ? 'border-[#ffcfbf]/20 bg-[#ffcfbf]/10 text-[#fff0ea]' : tone === 'sky' ? 'border-sky-300/20 bg-sky-300/10 text-sky-100' : tone === 'emerald' ? 'border-emerald-300/20 bg-emerald-300/10 text-emerald-100' : 'border-white/10 bg-white/[0.05] text-slate-200' return {children} } function CourseCover({ item, compact = false }) { if (item.cover_image_url) { return {item.title} } return (

Course cover

No cover image attached yet

) } function CourseCoverWall({ items = [] }) { const images = items .map((item) => item?.cover_image_url) .filter(Boolean) .slice(0, 4) if (!images.length) { return (

Course cover wall

Course artwork will appear here once covers are added.

) } return (
{images.length > 1 ? (
{images.slice(1, 4).map((image, index) => (
))}
) : null}
) } function CourseStatCard({ label, value, tone = 'default' }) { const toneClass = tone === 'sky' ? 'border-sky-300/20 bg-sky-300/10 text-sky-100' : tone === 'emerald' ? 'border-emerald-300/20 bg-emerald-300/10 text-emerald-100' : tone === 'warm' ? 'border-[#ffcfbf]/20 bg-[#ffcfbf]/10 text-[#fff0ea]' : 'border-white/10 bg-black/20 text-slate-300' return (

{label}

{value}

) } function PromptActions({ item }) { return (
{item.preview_url ? Preview : null} Edit
) } function CourseActions({ item }) { return (
Builder Edit
) } function CourseGridCard({ item }) { const status = courseStatusMeta(item.status) const access = courseAccessMeta(item.access_level) return (
{item.lessons_count || 0} lessons {item.is_featured ? 'Featured' : 'Course'} {status.label}

{item.title}

{item.subtitle ?

{item.subtitle}

: null}

{item.excerpt || 'No excerpt added yet.'}

{access.label} {formatDateLabel(item.updated_at)}
) } function CourseTable({ items }) { return (
{items.map((item) => { const status = courseStatusMeta(item.status) const access = courseAccessMeta(item.access_level) return ( ) })}
Cover Course Access Status Lessons Updated Actions

{item.title}

{item.subtitle ?

{item.subtitle}

: null}

{item.excerpt || 'No excerpt added yet.'}

{access.label} {status.label}

{item.lessons_count || 0} lessons

{item.is_featured ? 'Featured' : 'Standard'}

{formatDateLabel(item.updated_at)}
Builder Edit
) } function CourseSearchBar({ value, onChange, onSubmit, onClear, viewMode, onViewModeChange }) { return (
onChange(event.target.value)} placeholder="Search title, slug, subtitle, excerpt, or description…" className="w-full rounded-2xl border border-white/10 bg-black/20 py-3 pl-9 pr-4 text-sm text-white placeholder:text-slate-600 focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10" />
{value ? ( ) : null}
{COURSE_VIEW_OPTIONS.map((option) => { const active = option.value === viewMode return ( ) })}
) } function PromptPreview({ item, compact = false }) { if (item.preview_image_url) { return {item.title} } return (

Prompt preview

No image attached yet

) } function PromptMeta({ item }) { return (
{item.access_level ? : null} {item.category_name ? {item.category_name} : null} {item.difficulty ? {item.difficulty} : null} {item.aspect_ratio ? {item.aspect_ratio} : null} {item.featured ? Featured : null} {item.prompt_of_week ? Prompt of week : null} {item.active ? 'Active' : 'Draft'}
) } function PromptGalleryCard({ item }) { return (
{Number(item.views_count || 0).toLocaleString()} views {item.comparisons_count || 0} comparisons {item.slug ? {item.slug} : null}

{item.title}

{item.excerpt || 'Add an excerpt to make this prompt easier to scan in moderation.'}

{item.tags?.length ? (
{item.tags.slice(0, 5).map((tag) => ( {tag} ))}
) : null}

Updated

{formatDateLabel(item.updated_at)}

Access

Status

{item.active ? 'Visible' : 'Hidden'}

) } function PromptGridCard({ item }) { return (

{item.title}

{item.excerpt || 'No excerpt added yet.'}

{formatDateLabel(item.updated_at)} {Number(item.views_count || 0).toLocaleString()} views
) } function PromptTable({ items }) { return (
{items.map((item) => ( ))}
Prompt Category Access Signals Views Updated Actions

{item.title}

{item.excerpt || 'No excerpt added yet.'}

{item.category_name || 'Uncategorized'}

{item.comparisons_count || 0} comparisons

{item.difficulty || 'No difficulty'}

{item.active ? 'Active' : 'Draft'}

{Number(item.views_count || 0).toLocaleString()} {formatDateLabel(item.updated_at)}
{item.preview_url ? Preview : null} Edit
) } function PromptStatCard({ label, value, tone = 'default' }) { const toneClass = tone === 'sky' ? 'border-sky-300/20 bg-sky-300/10 text-sky-100' : tone === 'emerald' ? 'border-emerald-300/20 bg-emerald-300/10 text-emerald-100' : tone === 'warm' ? 'border-amber-300/20 bg-amber-300/10 text-amber-100' : 'border-white/10 bg-black/20 text-slate-300' return (

{label}

{value}

) } function PromptSelect({ value, options = [], onChange }) { return ( ) } function PromptSearchBar({ filters, onChange, onSubmit, onReset, viewMode, onViewModeChange, filterOptions = {} }) { return (
onChange('search', event.target.value)} placeholder="Search title, slug, excerpt, prompt text, or category…" className="w-full rounded-2xl border border-white/10 bg-black/20 py-3 pl-9 pr-4 text-sm text-white placeholder:text-slate-600 focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10" />
onChange('category', value)} options={filterOptions.categories} /> onChange('access_level', value)} options={filterOptions.access} /> onChange('order', value)} options={filterOptions.order} />
onChange('difficulty', value)} options={filterOptions.difficulty} /> onChange('featured', value)} options={filterOptions.featured} /> onChange('prompt_of_week', value)} options={filterOptions.promptOfWeek} /> onChange('active', value)} options={filterOptions.active} />
{PROMPT_VIEW_OPTIONS.map((option) => { const active = option.value === viewMode return ( ) })}
) } function PromptHeroCollage({ items = [] }) { const images = items .map((item) => item?.preview_image_url) .filter(Boolean) .slice(0, 4) if (!images.length) { return (

Prompt preview wall

Preview images will appear here as prompts get covers.

) } return (
{images.length > 1 ? (
{images.slice(1, 4).map((image, index) => (
))}
) : null}
) } function CourseIndexContent({ title, subtitle, items, createUrl, filters = {}, summary = {} }) { const { url } = usePage() const courses = items?.data || [] const [viewMode, setViewMode] = useState('grid') const [searchValue, setSearchValue] = useState(filters.search || '') useEffect(() => { setSearchValue(filters.search || '') }, [filters.search]) useEffect(() => { if (typeof window === 'undefined') return const storedView = window.localStorage.getItem(COURSE_VIEW_STORAGE_KEY) if (COURSE_VIEW_OPTIONS.some((option) => option.value === storedView)) { setViewMode(storedView) } }, []) useEffect(() => { if (typeof window === 'undefined') return window.localStorage.setItem(COURSE_VIEW_STORAGE_KEY, viewMode) }, [viewMode]) const stats = useMemo(() => courseSummary(courses, summary), [courses, summary]) const currentPath = url.split('?')[0] const hasSearch = Boolean(searchValue.trim()) const meta = items?.meta || {} const handleSearch = (event) => { event.preventDefault() router.get(currentPath, { search: searchValue.trim() || undefined }, { preserveScroll: true, preserveState: true, replace: true }) } const handleClearSearch = () => { setSearchValue('') router.get(currentPath, {}, { preserveScroll: true, preserveState: true, replace: true }) } return (
Academy moderation Course library

{title}

{subtitle} Search courses quickly, switch between grid and table views, and jump into editing with a cleaner visual overview of covers, status, and lesson counts.

Create course Open public courses {meta.total || courses.length} courses in view

{meta.total ? ( <> Showing {meta.from || 0}-{meta.to || 0} of {meta.total} courses {hasSearch ? filtered by “{searchValue.trim()}” : null} ) : ( 'Manage Academy courses below. Changes clear Academy cache automatically.' )}

Create course
{courses.length === 0 ? (
{hasSearch ? (

No courses matched your search.

) : (

No courses exist yet.

Create the first course
)}
) : viewMode === 'table' ? ( ) : (
{courses.map((item) => )}
)}
) } function PaginationLinks({ links = [] }) { if (!Array.isArray(links) || links.length <= 3) return null return (
{links.map((link, index) => { const label = paginationLabel(link.label) const className = link.active ? 'border-sky-300/25 bg-sky-300/12 text-sky-100' : 'border-white/10 bg-white/[0.04] text-slate-200 hover:border-white/20 hover:bg-white/[0.07]' return link.url ? ( {label} ) : ( {label} ) })}
) } function renderCrudCell(column, item) { if (column === 'active') { const active = Boolean(item.active) return ( {active ? 'Active' : 'Inactive'} ) } if (column === 'course_names') { const courseNames = Array.isArray(item.course_names) ? item.course_names.filter(Boolean) : [] if (courseNames.length === 0) { return Not attached } return (
{courseNames.map((courseName) => ( {courseName} ))}
) } if (column === 'course_order') { return {item.course_order ?? 'Not set'} } return

{String(item[column] ?? '')}

} function PromptIndexContent({ title, subtitle, items, createUrl, filters = {}, summary = {}, filterOptions = {} }) { const { url } = usePage() const promptItems = items?.data || [] const stats = useMemo(() => promptSummary(promptItems, summary), [promptItems, summary]) const [viewMode, setViewMode] = useState('gallery') const [query, setQuery] = useState({ search: filters.search || '', category: filters.category || 'all', featured: filters.featured || 'all', prompt_of_week: filters.prompt_of_week || 'all', active: filters.active || 'all', access_level: filters.access_level || 'all', difficulty: filters.difficulty || 'all', order: filters.order || 'updated_desc', }) useEffect(() => { if (typeof window === 'undefined') return const storedView = window.localStorage.getItem(PROMPT_VIEW_STORAGE_KEY) if (PROMPT_VIEW_OPTIONS.some((option) => option.value === storedView)) { setViewMode(storedView) } }, []) useEffect(() => { if (typeof window === 'undefined') return window.localStorage.setItem(PROMPT_VIEW_STORAGE_KEY, viewMode) }, [viewMode]) useEffect(() => { setQuery({ search: filters.search || '', category: filters.category || 'all', featured: filters.featured || 'all', prompt_of_week: filters.prompt_of_week || 'all', active: filters.active || 'all', access_level: filters.access_level || 'all', difficulty: filters.difficulty || 'all', order: filters.order || 'updated_desc', }) }, [filters]) const currentPath = url.split('?')[0] const meta = items?.meta || {} const hasFilters = Boolean( (query.search || '').trim() || query.category !== 'all' || query.featured !== 'all' || query.prompt_of_week !== 'all' || query.active !== 'all' || query.access_level !== 'all' || query.difficulty !== 'all' || query.order !== 'updated_desc' ) const applyQuery = (nextQuery) => { const payload = {} if ((nextQuery.search || '').trim()) payload.search = nextQuery.search.trim() if (nextQuery.category && nextQuery.category !== 'all') payload.category = nextQuery.category if (nextQuery.featured && nextQuery.featured !== 'all') payload.featured = nextQuery.featured if (nextQuery.prompt_of_week && nextQuery.prompt_of_week !== 'all') payload.prompt_of_week = nextQuery.prompt_of_week if (nextQuery.active && nextQuery.active !== 'all') payload.active = nextQuery.active if (nextQuery.access_level && nextQuery.access_level !== 'all') payload.access_level = nextQuery.access_level if (nextQuery.difficulty && nextQuery.difficulty !== 'all') payload.difficulty = nextQuery.difficulty if (nextQuery.order && nextQuery.order !== 'updated_desc') payload.order = nextQuery.order router.get(currentPath, payload, { preserveScroll: true, preserveState: true, replace: true }) } const handleFilterChange = (key, value) => { setQuery((current) => ({ ...current, [key]: value })) } const handleSubmit = (event) => { event.preventDefault() applyQuery(query) } const handleReset = () => { const nextQuery = { search: '', category: 'all', featured: 'all', prompt_of_week: 'all', active: 'all', access_level: 'all', difficulty: 'all', order: 'updated_desc', } setQuery(nextQuery) router.get(currentPath, {}, { preserveScroll: true, preserveState: true, replace: true }) } return (
Academy moderation Prompt library

{title}

{subtitle} Review prompts in a visual-first moderation surface, jump into edits quickly, and switch between gallery, grid, or table depending on the task in front of you.

Visual-first

Curate covers and prompt outputs before opening the form.

Workflow-ready

Switch between gallery, compact cards, and scan-heavy tables.

Comparison-aware

Spot prompts with provider notes and attached result references.

Create prompt Open public library {stats.total} prompts in view
count + Number(item.views_count || 0), 0).toLocaleString()} />

Free access

{stats.access.free}

Creator access

{stats.access.creator}

Pro access

{stats.access.pro}

{meta.total ? ( <> Showing {meta.from || 0}-{meta.to || 0} of {meta.total} prompts {hasFilters ? with active search or filters : null} ) : ( 'Manage Academy content below. Changes clear Academy cache automatically.' )}

View public library Create prompt
{promptItems.length === 0 ? (
{hasFilters ? (

No prompt templates matched these filters.

) : ( 'No prompt templates exist yet.' )}
) : viewMode === 'table' ? ( ) : viewMode === 'grid' ? (
{promptItems.map((item) => )}
) : (
{promptItems.map((item) => )}
)}
) } export default function AcademyCrudIndex({ title, subtitle, items, columns, createUrl }) { const flash = usePage().props.flash || {} const resource = usePage().props.resource const filters = usePage().props.filters || {} const summary = usePage().props.summary || {} const filterOptions = usePage().props.filterOptions || {} return ( {flash.success ?
{flash.success}
: null} {resource === 'courses' ? ( ) : resource === 'prompts' ? ( ) : ( <>

Manage Academy content below. Changes clear Academy cache automatically.

Create record
{(items?.data || []).length === 0 ? (
No records exist yet.
) : (
{items.data.map((item) => (
{columns.map((column) => (

{column.replaceAll('_', ' ')}

{renderCrudCell(column, item)}
))}
{item.builder_url ? Builder : null} Edit
))}
)} )}
) }