{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'
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
}
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
{item.access_level || 'free'}
Status
{item.active ? 'Visible' : 'Hidden'}
{item.excerpt || 'No excerpt added yet.'}
| 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
|
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 }) { 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 ({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.
Active
{summary.active}
Featured
{summary.featured}
Prompt of week
{summary.promptOfWeek}
Comparisons
{summary.comparisons}
Manage Academy content below. Changes clear Academy cache automatically.
Manage Academy content below. Changes clear Academy cache automatically.
Create record{column.replaceAll('_', ' ')}