import React, { useDeferredValue, useEffect, useMemo, useRef, useState } from 'react'
import { Head, Link, router, useForm } from '@inertiajs/react'
import { createPortal } from 'react-dom'
import AdminLayout from '../../../Layouts/AdminLayout'
import RichTextEditor from '../../../components/forum/RichTextEditor'
import WorldMediaUploadField from '../../../components/worlds/editor/WorldMediaUploadField'
import DateTimePicker from '../../../components/ui/DateTimePicker'
import NovaSelect from '../../../components/ui/NovaSelect'
import ShareToast from '../../../components/ui/ShareToast'
const COURSE_EDITOR_TABS = [
{
id: 'overview',
label: 'Overview',
description: 'Title, slug, positioning, and the short summary shown on course cards.',
icon: 'fa-compass-drafting',
sections: ['course-identity'],
},
{
id: 'content',
label: 'Content',
description: 'Use the richer WYSIWYG surface for the main course description and learning pitch.',
icon: 'fa-pen-nib',
sections: ['course-description'],
},
{
id: 'media',
label: 'Media',
description: 'Upload and tune the cover and teaser visuals used across the public course surfaces.',
icon: 'fa-images',
sections: ['course-media'],
},
{
id: 'lessons',
label: 'Lessons',
description: 'Build the lesson sequence, drag to reorder, and add or remove lessons without opening the full builder.',
icon: 'fa-list-ol',
sections: ['course-lessons-manager'],
},
{
id: 'publish',
label: 'Publish',
description: 'Control access, status, ordering, scheduling, and featured placement.',
icon: 'fa-rocket-launch',
sections: ['course-publishing', 'course-seo'],
},
{
id: 'preview',
label: 'Preview',
description: 'Scan the public-facing course card, media, and rendered long description before publishing.',
icon: 'fa-eye',
sections: ['course-preview'],
},
]
const COURSE_FIELD_TAB_MAP = {
title: 'overview',
slug: 'overview',
subtitle: 'overview',
excerpt: 'overview',
description: 'content',
cover_image: 'media',
teaser_image: 'media',
access_level: 'publish',
difficulty: 'publish',
status: 'publish',
order_num: 'publish',
estimated_minutes: 'publish',
published_at: 'publish',
is_featured: 'publish',
seo_title: 'publish',
seo_description: 'publish',
meta_keywords: 'publish',
og_title: 'publish',
og_description: 'publish',
og_image: 'publish',
}
function getField(fields, name) {
return fields.find((field) => field.name === name) || null
}
function FieldError({ message }) {
if (!message) return null
return
{message}
}
function SectionCard({ id, eyebrow, title, description, actions, children, tone = 'default', className = '', contentClassName = '' }) {
const toneClass = tone === 'feature'
? 'bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.16),transparent_38%),linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.92))] shadow-[0_24px_70px_rgba(2,6,23,0.28)]'
: 'bg-white/[0.03]'
return (
{eyebrow ?
{eyebrow}
: null}
{title}
{description ?
{description}
: null}
{actions ?
{actions}
: null}
{children}
)
}
function EditorWorkspaceTabs({ tabs, activeTab, onChange, errorCounts }) {
const activeMeta = tabs.find((tab) => tab.id === activeTab) || tabs[0]
return (
{tabs.map((tab) => {
const isActive = tab.id === activeTab
const errorCount = Number(errorCounts?.[tab.id] || 0)
return (
)
})}
{activeMeta.description}
{activeMeta.sections.map((section) => (
{section.replace('course-', '').replace(/-/g, ' ')}
))}
)
}
function TextField({ label, value, onChange, error, hint, ...rest }) {
return (
)
}
function TextAreaField({ label, value, onChange, error, rows = 4, hint }) {
return (
)
}
function CheckboxCardField({ label, checked, onChange, description, error }) {
return (
)
}
function OutlineSectionPill({ section }) {
return (
{section.title}
{section.is_visible ? 'Visible section' : 'Hidden section'}
{section.lesson_count}
)
}
function slugifyCourseTitle(value) {
return String(value || '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 180)
}
function formatLessonStep(orderNum) {
const numeric = Number(orderNum)
if (!Number.isFinite(numeric) || numeric < 0) return null
return `Step ${String(numeric + 1).padStart(2, '0')}`
}
function lessonActivityBadgeMeta(lesson) {
const isActive = Boolean(lesson?.active)
return {
label: isActive ? 'Active' : 'Inactive',
className: isActive
? 'border-emerald-300/20 bg-emerald-300/10 text-emerald-100'
: 'border-amber-300/20 bg-amber-300/10 text-amber-200',
}
}
function lessonPublicationBadgeMeta(lesson) {
const state = String(lesson?.publication_state || 'draft')
const label = String(lesson?.publication_label || (state === 'published' ? 'Published' : state === 'scheduled' ? 'Scheduled' : 'Unscheduled'))
if (state === 'published') {
return {
label,
className: 'border-sky-300/20 bg-sky-300/10 text-sky-100',
}
}
if (state === 'scheduled') {
return {
label,
className: 'border-fuchsia-300/20 bg-fuchsia-300/10 text-fuchsia-100',
}
}
return {
label,
className: 'border-white/10 bg-white/[0.04] text-slate-400',
}
}
function CourseSectionCreateCard({ storeUrl, nextOrderNum }) {
const form = useForm({
title: '',
slug: '',
description: '',
order_num: nextOrderNum,
is_visible: true,
})
useEffect(() => {
form.setData('order_num', nextOrderNum)
}, [nextOrderNum])
const createSection = () => {
if (!storeUrl) return
form.post(storeUrl, {
preserveScroll: true,
onSuccess: () => {
form.setData({
title: '',
slug: '',
description: '',
order_num: nextOrderNum,
is_visible: true,
})
},
})
}
return (
)
}
function CourseSectionCard({ section }) {
const form = useForm({
title: section.title || '',
slug: section.slug || '',
description: section.description || '',
order_num: section.order_num || 0,
is_visible: Boolean(section.is_visible),
})
useEffect(() => {
form.setData({
title: section.title || '',
slug: section.slug || '',
description: section.description || '',
order_num: section.order_num || 0,
is_visible: Boolean(section.is_visible),
})
}, [section.description, section.is_visible, section.order_num, section.slug, section.title])
const saveSection = () => {
if (!section.update_url) return
form.patch(section.update_url, { preserveScroll: true })
}
const deleteSection = () => {
if (!section.destroy_url) return
if (!window.confirm(`Delete section "${section.title}"? Lessons assigned to it will become unsectioned.`)) return
router.delete(section.destroy_url, { preserveScroll: true })
}
return (
)
}
function normalizeLessonManagerLessons(lessons) {
return (Array.isArray(lessons) ? [...lessons] : [])
.sort((a, b) => {
const diff = Number(a?.order_num || 0) - Number(b?.order_num || 0)
return diff !== 0 ? diff : Number(a?.id || 0) - Number(b?.id || 0)
})
.map((lesson, index) => ({ ...lesson, order_num: index, display_order: index + 1 }))
}
function reorderLessonManagerLessons(lessons, draggedId, targetId) {
const current = normalizeLessonManagerLessons(lessons)
const di = current.findIndex((l) => Number(l.id) === Number(draggedId))
const ti = current.findIndex((l) => Number(l.id) === Number(targetId))
if (di === -1 || ti === -1 || di === ti) return current
const next = [...current]
const [moved] = next.splice(di, 1)
next.splice(ti, 0, moved)
return normalizeLessonManagerLessons(next)
}
function moveLessonManagerLesson(lessons, lessonId, direction) {
const current = normalizeLessonManagerLessons(lessons)
const idx = current.findIndex((l) => Number(l.id) === Number(lessonId))
const nextIdx = idx + direction
if (idx === -1 || nextIdx < 0 || nextIdx >= current.length) return current
const next = [...current]
const [moved] = next.splice(idx, 1)
next.splice(nextIdx, 0, moved)
return normalizeLessonManagerLessons(next)
}
function lessonManagerSignature(lessons) {
return JSON.stringify(normalizeLessonManagerLessons(lessons).map((l) => ({
id: Number(l.id),
order_num: Number(l.order_num || 0),
section_id: l.section_id == null ? null : Number(l.section_id),
})))
}
function stripHtml(value) {
return String(value || '')
.replace(/