import React, { startTransition, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { Head, Link, router, useForm } from '@inertiajs/react'
import { marked } from 'marked'
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'
let lessonMarkdownTurndown = null
let lessonMarkdownTurndownPromise = null
async function loadLessonMarkdownTurndown() {
if (lessonMarkdownTurndown) {
return lessonMarkdownTurndown
}
if (typeof window === 'undefined') {
return null
}
if (!lessonMarkdownTurndownPromise) {
lessonMarkdownTurndownPromise = import('turndown')
.then(({ default: TurndownService }) => new TurndownService({
headingStyle: 'atx',
codeBlockStyle: 'fenced',
bulletListMarker: '-',
emDelimiter: '*',
}))
.then((service) => {
lessonMarkdownTurndown = service
return service
})
.catch(() => null)
}
return lessonMarkdownTurndownPromise
}
function getField(fields, name) {
return fields.find((field) => field.name === name) || null
}
const LESSON_EDITOR_TABS = [
{
id: 'write',
label: 'Write',
description: 'Headline, summary, and the full lesson article.',
icon: 'fa-pen-nib',
sections: ['lesson-story-setup', 'lesson-body-editor'],
},
{
id: 'blocks',
label: 'Blocks',
description: 'Reusable AI comparison modules and structured lesson inserts.',
icon: 'fa-layer-group',
sections: ['lesson-ai-comparisons'],
},
{
id: 'courses',
label: 'Courses',
description: 'Attach this lesson to courses, manage its public numbering, and reorder it inside guided paths.',
icon: 'fa-diagram-project',
sections: ['lesson-course-numbering', 'lesson-course-manager'],
},
{
id: 'publish',
label: 'Publish',
description: 'Visibility, discovery settings, scheduling, and search surfaces.',
icon: 'fa-rocket-launch',
sections: ['lesson-publishing', 'lesson-seo'],
},
{
id: 'assets',
label: 'Assets',
description: 'Hero cover, article cover, and lesson categories.',
icon: 'fa-images',
sections: ['lesson-cover', 'lesson-article-cover', 'lesson-categories'],
},
{
id: 'revisions',
label: 'Revisions',
description: 'Review saved lesson snapshots and restore the full lesson or a single field.',
icon: 'fa-clock-rotate-left',
sections: ['lesson-revisions'],
},
{
id: 'preview',
label: 'Preview',
description: 'Preview the lesson card, article imagery, and rendered body.',
icon: 'fa-eye',
sections: ['lesson-preview'],
},
]
const LESSON_FIELD_TAB_MAP = {
title: 'write',
slug: 'write',
excerpt: 'write',
content: 'write',
content_markdown: 'write',
lesson_type: 'publish',
difficulty: 'publish',
access_level: 'publish',
reading_minutes: 'publish',
tags: 'publish',
series_name: 'publish',
lesson_number: 'courses',
course_order: 'courses',
course_ids: 'courses',
category_id: 'assets',
published_at: 'publish',
featured: 'publish',
active: 'publish',
seo_title: 'publish',
seo_description: 'publish',
video_url: 'publish',
cover_image: 'assets',
article_cover_image: 'assets',
}
const LESSON_REVISION_FIELD_OPTIONS = [
{ value: 'title', label: 'Title' },
{ value: 'slug', label: 'Slug' },
{ value: 'lesson_number', label: 'Lesson number' },
{ value: 'course_order', label: 'Course order' },
{ value: 'series_name', label: 'Series name' },
{ value: 'excerpt', label: 'Excerpt' },
{ value: 'content', label: 'Article body' },
{ value: 'difficulty', label: 'Difficulty' },
{ value: 'access_level', label: 'Access level' },
{ value: 'lesson_type', label: 'Lesson type' },
{ value: 'cover_image', label: 'Cover image' },
{ value: 'article_cover_image', label: 'Article cover image' },
{ value: 'tags', label: 'Microtags' },
{ value: 'video_url', label: 'Video URL' },
{ value: 'reading_minutes', label: 'Reading minutes' },
{ value: 'featured', label: 'Featured toggle' },
{ value: 'active', label: 'Active toggle' },
{ value: 'published_at', label: 'Publish date' },
{ value: 'seo_title', label: 'SEO title' },
{ value: 'seo_description', label: 'SEO description' },
{ value: 'course_ids', label: 'Course attachments' },
{ value: 'blocks', label: 'AI comparison blocks' },
]
let comparisonEditorSequence = 0
function nextComparisonEditorKey(prefix) {
comparisonEditorSequence += 1
return `${prefix}-${comparisonEditorSequence}`
}
function FieldError({ message }) {
if (!message) return null
return
{message}
}
function CopyablePromptCard({ eyebrow, title, description, prompt, onCopy }) {
return (
{eyebrow}
{title}
{description ?
{description}
: null}
)
}
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('lesson-', '').replace(/-/g, ' ')}
))}
)
}
function firstLessonErrorTab(errors) {
const firstKey = Object.keys(errors || {})[0]
if (!firstKey) return null
if (firstKey.startsWith('blocks.')) return 'blocks'
return LESSON_FIELD_TAB_MAP[firstKey] || null
}
function lessonTabErrorCounts(errors) {
const counts = {}
Object.keys(errors || {}).forEach((key) => {
const tabId = key.startsWith('blocks.') ? 'blocks' : LESSON_FIELD_TAB_MAP[key]
if (!tabId) return
counts[tabId] = Number(counts[tabId] || 0) + 1
})
return counts
}
function firstErrorMessage(errors, fallback = 'Please correct the highlighted fields and try again.') {
const queue = [errors]
while (queue.length > 0) {
const current = queue.shift()
if (typeof current === 'string') {
const message = current.trim()
if (message) {
return message
}
continue
}
if (Array.isArray(current)) {
queue.push(...current)
continue
}
if (current && typeof current === 'object') {
queue.push(...Object.values(current))
}
}
return fallback
}
function TextField({ label, value, onChange, error, hint, ...rest }) {
return (
)
}
function TextAreaField({ label, value, onChange, error, rows = 4, hint }) {
return (
)
}
function ToggleField({ label, checked, onChange, help, error }) {
return (
)
}
function formatMissingNumbers(values) {
const items = Array.isArray(values) ? values.map((value) => Number(value)).filter((value) => Number.isInteger(value) && value > 0) : []
if (items.length === 0) return 'No gaps right now.'
return items.join(', ')
}
function formatCourseStep(orderNum) {
const numeric = Number(orderNum)
if (!Number.isFinite(numeric) || numeric < 0) return null
return `Step ${String(numeric + 1).padStart(2, '0')}`
}
function normalizeCourseManagerLessons(lessons) {
return (Array.isArray(lessons) ? [...lessons] : [])
.sort((left, right) => {
const orderDiff = Number(left?.order_num || 0) - Number(right?.order_num || 0)
if (orderDiff !== 0) return orderDiff
return Number(left?.id || 0) - Number(right?.id || 0)
})
.map((lesson, index) => ({
...lesson,
order_num: index,
display_order: index + 1,
}))
}
function reorderCourseManagerLessons(lessons, draggedLessonId, targetLessonId) {
const current = normalizeCourseManagerLessons(lessons)
const draggedIndex = current.findIndex((lesson) => Number(lesson.id) === Number(draggedLessonId))
const targetIndex = current.findIndex((lesson) => Number(lesson.id) === Number(targetLessonId))
if (draggedIndex === -1 || targetIndex === -1 || draggedIndex === targetIndex) {
return current
}
const nextLessons = [...current]
const [draggedLesson] = nextLessons.splice(draggedIndex, 1)
nextLessons.splice(targetIndex, 0, draggedLesson)
return normalizeCourseManagerLessons(nextLessons)
}
function moveCourseManagerLesson(lessons, lessonId, direction) {
const current = normalizeCourseManagerLessons(lessons)
const lessonIndex = current.findIndex((lesson) => Number(lesson.id) === Number(lessonId))
const nextIndex = lessonIndex + direction
if (lessonIndex === -1 || nextIndex < 0 || nextIndex >= current.length) {
return current
}
const nextLessons = [...current]
const [movedLesson] = nextLessons.splice(lessonIndex, 1)
nextLessons.splice(nextIndex, 0, movedLesson)
return normalizeCourseManagerLessons(nextLessons)
}
function courseManagerSignature(lessons) {
return JSON.stringify(normalizeCourseManagerLessons(lessons).map((lesson) => ({
id: Number(lesson.id),
order_num: Number(lesson.order_num || 0),
section_id: lesson.section_id == null ? null : Number(lesson.section_id),
})))
}
function slugifyLessonTitle(value) {
return String(value || '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 180)
}
function stripHtml(value) {
return String(value || '')
.replace(/