import React, { useMemo, useRef, useState } from 'react'
import { Head, Link, router, useForm } from '@inertiajs/react'
import AdminLayout from '../../../Layouts/AdminLayout'
import DateTimePicker from '../../../components/ui/DateTimePicker'
import NovaSelect from '../../../components/ui/NovaSelect'
import ShareToast from '../../../components/ui/ShareToast'
import WorldMediaUploadField from '../../../components/worlds/editor/WorldMediaUploadField'
const CHALLENGE_EDITOR_TABS = [
{
id: 'overview',
label: 'Overview',
description: 'Title, slug, access, and the compact summary shown in academy challenge lists.',
icon: 'fa-compass-drafting',
sections: ['challenge-identity'],
},
{
id: 'brief',
label: 'Brief',
description: 'Write the creative objective, rules, prizes, required tags, and eligible categories.',
icon: 'fa-bullseye',
sections: ['challenge-brief', 'challenge-rules'],
},
{
id: 'media',
label: 'Media',
description: 'Upload the hero cover and tune the visual used on challenge cards.',
icon: 'fa-image',
sections: ['challenge-media'],
},
{
id: 'timeline',
label: 'Timeline',
description: 'Manage submission and voting windows without mixing them into editorial copy.',
icon: 'fa-calendar-days',
sections: ['challenge-timeline'],
},
{
id: 'publish',
label: 'Publish',
description: 'Control status, visibility, featured placement, and the final public preview.',
icon: 'fa-rocket-launch',
sections: ['challenge-publishing', 'challenge-preview'],
},
]
const CHALLENGE_FIELD_TAB_MAP = {
title: 'overview',
slug: 'overview',
excerpt: 'overview',
access_level: 'overview',
description: 'brief',
brief: 'brief',
rules: 'brief',
prize_text: 'brief',
required_tags: 'brief',
allowed_categories: 'brief',
cover_image: 'media',
starts_at: 'timeline',
ends_at: 'timeline',
voting_starts_at: 'timeline',
voting_ends_at: 'timeline',
status: 'publish',
featured: 'publish',
active: '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 = '' }) {
const toneClass = tone === 'feature'
? 'bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.15),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({ activeTab, onChange, errorCounts }) {
const activeMeta = CHALLENGE_EDITOR_TABS.find((tab) => tab.id === activeTab) || CHALLENGE_EDITOR_TABS[0]
return (
{CHALLENGE_EDITOR_TABS.map((tab) => {
const isActive = tab.id === activeTab
const errorCount = Number(errorCounts?.[tab.id] || 0)
return (
)
})}
{activeMeta.description}
{activeMeta.sections.map((section) => (
{section.replace('challenge-', '').replace(/-/g, ' ')}
))}
)
}
function TextField({ label, value, onChange, error, hint, ...rest }) {
return (
)
}
function TextAreaField({ label, value, onChange, error, rows = 4, hint, placeholder }) {
return (
)
}
function ToggleField({ label, checked, onChange, help, error }) {
return (
)
}
function PlainTextPreview({ title, value, fallback }) {
return (
{title}
{String(value || '').trim() || fallback}
)
}
function statusMeta(status) {
const normalized = String(status || 'draft')
const labels = {
draft: 'Draft',
scheduled: 'Scheduled',
active: 'Active',
voting: 'Voting',
completed: 'Completed',
archived: 'Archived',
}
const classes = {
draft: 'border-white/10 bg-white/[0.04] text-slate-300',
scheduled: 'border-fuchsia-300/20 bg-fuchsia-300/10 text-fuchsia-100',
active: 'border-emerald-300/20 bg-emerald-300/10 text-emerald-100',
voting: 'border-sky-300/20 bg-sky-300/10 text-sky-100',
completed: 'border-amber-300/20 bg-amber-300/10 text-amber-100',
archived: 'border-slate-300/15 bg-slate-300/8 text-slate-300',
}
return {
label: labels[normalized] || normalized,
className: classes[normalized] || classes.draft,
}
}
function slugifyChallengeTitle(value) {
return String(value || '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 180)
}
function normalizeList(value) {
return String(value || '')
.split(/[,\n]/)
.map((item) => item.trim())
.filter(Boolean)
}
function normalizeAssetPreview(value, cdnBaseUrl) {
const trimmed = String(value || '').trim()
if (!trimmed) return ''
if (trimmed.startsWith('http://') || trimmed.startsWith('https://') || trimmed.startsWith('/')) return trimmed
return `${String(cdnBaseUrl || '').replace(/\/$/, '')}/${trimmed.replace(/^\//, '')}`
}
function stripPlainText(value) {
return String(value || '').replace(/\s+/g, ' ').trim()
}
function countWords(value) {
const text = stripPlainText(value)
return text ? text.split(/\s+/).length : 0
}
function formatDateLabel(value, fallback = 'Not set') {
if (!value) return fallback
const date = new Date(value)
if (Number.isNaN(date.getTime())) return fallback
return new Intl.DateTimeFormat('en', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
}).format(date)
}
function daysBetween(start, end) {
const startDate = start ? new Date(start) : null
const endDate = end ? new Date(end) : null
if (!startDate || !endDate || Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) {
return null
}
const diff = endDate.getTime() - startDate.getTime()
if (diff < 0) return 'Dates need review'
const days = Math.max(1, Math.ceil(diff / (1000 * 60 * 60 * 24)))
return `${days} day${days === 1 ? '' : 's'}`
}
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 firstChallengeErrorTab(errors) {
const firstKey = Object.keys(errors || {})[0]
if (!firstKey) return null
const baseKey = firstKey.split('.')[0]
return CHALLENGE_FIELD_TAB_MAP[baseKey] || null
}
function challengeTabErrorCounts(errors) {
const counts = {}
Object.keys(errors || {}).forEach((key) => {
const tabId = CHALLENGE_FIELD_TAB_MAP[key.split('.')[0]]
if (!tabId) return
counts[tabId] = Number(counts[tabId] || 0) + 1
})
return counts
}
function buildChallengePayload(data) {
return {
...data,
required_tags: normalizeList(data.required_tags),
allowed_categories: normalizeList(data.allowed_categories),
}
}
export default function ChallengeEditor({ title, subtitle, fields, record, submitUrl, indexUrl, destroyUrl, method, editorContext = {} }) {
const form = useForm(record)
const slugTouchedRef = useRef(String(record?.slug || '').trim() !== '')
const [activeTab, setActiveTab] = useState('overview')
const [coverPreviewUrl, setCoverPreviewUrl] = useState(record?.cover_image_url || normalizeAssetPreview(record?.cover_image, editorContext.coverCdnBaseUrl))
const [stagedCoverPath, setStagedCoverPath] = useState('')
const [toast, setToast] = useState({ id: 0, visible: false, message: '', variant: 'success' })
const accessField = getField(fields, 'access_level')
const statusField = getField(fields, 'status')
const status = statusMeta(form.data.status)
const requiredTags = useMemo(() => normalizeList(form.data.required_tags), [form.data.required_tags])
const allowedCategories = useMemo(() => normalizeList(form.data.allowed_categories), [form.data.allowed_categories])
const briefWordCount = countWords(`${form.data.description || ''} ${form.data.brief || ''} ${form.data.rules || ''}`)
const submissionWindow = daysBetween(form.data.starts_at, form.data.ends_at)
const votingWindow = daysBetween(form.data.voting_starts_at, form.data.voting_ends_at)
const publicPathPreview = `/academy/challenges/${form.data.slug || 'challenge-slug'}`
const errorCounts = challengeTabErrorCounts(form.errors)
const sectionClassName = (sectionId) => {
const tab = CHALLENGE_EDITOR_TABS.find((item) => item.sections.includes(sectionId))
return tab && tab.id !== activeTab ? 'hidden' : ''
}
const showToast = (message, variant = 'error') => {
setToast({
id: Date.now() + Math.random(),
visible: true,
message,
variant,
})
}
const setTitle = (nextTitle) => {
form.setData('title', nextTitle)
if (!slugTouchedRef.current) {
form.setData('slug', slugifyChallengeTitle(nextTitle))
}
}
const handleManualCoverChange = (nextValue) => {
setStagedCoverPath('')
form.setData('cover_image', nextValue)
setCoverPreviewUrl(normalizeAssetPreview(nextValue, editorContext.coverCdnBaseUrl))
}
const submit = (event) => {
event.preventDefault()
const payload = buildChallengePayload(form.data)
form.transform(() => payload)
const submitOptions = {
preserveScroll: true,
onError: (errors) => {
const nextTab = firstChallengeErrorTab(errors)
if (nextTab) {
setActiveTab(nextTab)
}
showToast(firstErrorMessage(errors), 'error')
},
onFinish: () => form.transform((data) => data),
}
if (method === 'patch') {
form.patch(submitUrl, submitOptions)
return
}
form.post(submitUrl, submitOptions)
}
const deleteChallenge = () => {
if (!destroyUrl) return
if (!window.confirm('Delete this challenge?')) return
router.delete(destroyUrl)
}
return (
setToast((current) => ({ ...current, visible: false }))}
/>
)
}