{item.title}
{item.subtitle ?{item.subtitle}
: null}{item.excerpt || 'No excerpt added yet.'}
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
}
return (
Course cover
No cover image attached yet
Course cover wall
Course artwork will appear here once covers are added.
{label}
{value}
{item.subtitle}
: null}{item.excerpt || 'No excerpt added yet.'}
| 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
|
Prompt preview
No image attached yet
{item.excerpt || 'Add an excerpt to make this prompt easier to scan in moderation.'}
{item.tags?.length ? (Updated
{formatDateLabel(item.updated_at)}
Access
Status
{item.active ? 'Visible' : 'Hidden'}
{item.excerpt || 'No excerpt added yet.'}
| 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
|
{label}
{value}
Prompt preview wall
Preview images will appear here as prompts get covers.
{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.
{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.' )}
No courses matched your search.
No courses exist yet.
Create the first course{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 ({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.
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.' )}
No prompt templates matched these filters.
Manage Academy content below. Changes clear Academy cache automatically.
Create record{column.replaceAll('_', ' ')}