import React, { useEffect, useMemo, useState } from 'react' import { Head, Link, router, usePage } from '@inertiajs/react' import AdminLayout from '../../../Layouts/AdminLayout' 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 = []) { 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), }), { total: 0, active: 0, featured: 0, promptOfWeek: 0, comparisons: 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.category_name ? {item.category_name} : null} {item.difficulty ? {item.difficulty} : null} {item.access_level ? {item.access_level} : 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 (
{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

{item.access_level || 'free'}

Status

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

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

{item.title}

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

{formatDateLabel(item.updated_at)} {item.comparisons_count || 0} comparisons
) } function PromptTable({ items }) { return (
{items.map((item) => ( ))}
Prompt Category Access Signals Updated Actions

{item.title}

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

{item.category_name || 'Uncategorized'} {item.access_level || 'free'}

{item.comparisons_count || 0} comparisons

{item.difficulty || 'No difficulty'}

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

{formatDateLabel(item.updated_at)}
{item.preview_url ? Preview : null} Edit
) } 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 }) { const promptItems = items?.data || [] const summary = promptSummary(promptItems) const [viewMode, setViewMode] = useState('gallery') 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]) 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.

{PROMPT_VIEW_OPTIONS.map((option) => { const active = option.value === viewMode return ( ) })}
Create prompt Open public library {summary.total} prompts in view

Active

{summary.active}

Featured

{summary.featured}

Prompt of week

{summary.promptOfWeek}

Comparisons

{summary.comparisons}

Manage Academy content below. Changes clear Academy cache automatically.

View public library Create prompt
{promptItems.length === 0 ? (
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 || {} 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
))}
)} )}
) }