Files
SkinbaseNova/resources/js/Pages/Moderation/WorldWebStoryEditor.jsx

475 lines
28 KiB
JavaScript

import React from 'react'
import { Head, Link, router, useForm, usePage } from '@inertiajs/react'
function getCsrfToken() {
if (typeof document === 'undefined') return ''
return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
}
async function requestJson(url, { method = 'POST', body } = {}) {
const response = await fetch(url, {
method,
credentials: 'same-origin',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-CSRF-TOKEN': getCsrfToken(),
'X-Requested-With': 'XMLHttpRequest',
},
body: body ? JSON.stringify(body) : undefined,
})
const payload = await response.json().catch(() => ({}))
if (!response.ok) {
throw new Error(payload?.message || payload?.errors?.story?.[0] || Object.values(payload?.errors || {})?.[0]?.[0] || 'Request failed.')
}
return payload
}
function replacePagePattern(pattern, pageId) {
return String(pattern || '').replace('__PAGE__', String(pageId))
}
function Field({ label, children, hint }) {
return (
<label className="block rounded-2xl border border-white/10 bg-white/[0.04] p-4 text-sm text-slate-300">
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">{label}</div>
<div className="mt-2">{children}</div>
{hint ? <div className="mt-2 text-xs text-slate-500">{hint}</div> : null}
</label>
)
}
function StoryPageCard({ page, endpoints, onChanged }) {
const [localPage, setLocalPage] = React.useState(page)
const [busy, setBusy] = React.useState(false)
const [error, setError] = React.useState('')
React.useEffect(() => {
setLocalPage(page)
}, [page])
async function save() {
setBusy(true)
setError('')
try {
await requestJson(replacePagePattern(endpoints.pagesUpdatePattern, page.id), {
method: 'PATCH',
body: {
...localPage,
overlay_strength: Number(localPage.overlay_strength || 35),
active: Boolean(localPage.active),
},
})
onChanged()
} catch (requestError) {
setError(requestError.message || 'Unable to save page.')
} finally {
setBusy(false)
}
}
async function destroy() {
setBusy(true)
setError('')
try {
await requestJson(replacePagePattern(endpoints.pagesDestroyPattern, page.id), { method: 'DELETE' })
onChanged()
} catch (requestError) {
setError(requestError.message || 'Unable to delete page.')
} finally {
setBusy(false)
}
}
return (
<article className="rounded-[24px] border border-white/10 bg-black/20 p-5">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Page {page.position}</div>
<div className="mt-2 text-lg font-semibold text-white">{page.headline || 'Untitled page'}</div>
</div>
<div className="flex gap-2">
<button type="button" onClick={save} disabled={busy} className="rounded-full border border-sky-300/20 bg-sky-400/12 px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-sky-50 transition hover:bg-sky-400/20 disabled:opacity-60">Save</button>
<button type="button" onClick={destroy} disabled={busy} className="rounded-full border border-rose-300/20 bg-rose-400/12 px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-rose-100 transition hover:bg-rose-400/20 disabled:opacity-60">Delete</button>
</div>
</div>
{error ? <div className="mt-4 rounded-2xl border border-rose-300/20 bg-rose-400/10 px-4 py-3 text-sm text-rose-100">{error}</div> : null}
<div className="mt-4 grid gap-3 xl:grid-cols-2">
<Field label="Headline">
<input value={localPage.headline || ''} onChange={(event) => setLocalPage((current) => ({ ...current, headline: event.target.value }))} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" />
</Field>
<Field label="Caption">
<input value={localPage.caption || ''} onChange={(event) => setLocalPage((current) => ({ ...current, caption: event.target.value }))} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" />
</Field>
<Field label="Body" hint="Maximum 180 characters.">
<textarea value={localPage.body || ''} onChange={(event) => setLocalPage((current) => ({ ...current, body: event.target.value }))} rows={3} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" />
</Field>
<Field label="Alt text">
<input value={localPage.alt_text || ''} onChange={(event) => setLocalPage((current) => ({ ...current, alt_text: event.target.value }))} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" />
</Field>
<Field label="Layout">
<select value={localPage.layout} onChange={(event) => setLocalPage((current) => ({ ...current, layout: event.target.value }))} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none">
{['cover', 'artwork', 'creator', 'mood', 'collection', 'cta'].map((value) => <option key={value} value={value}>{value}</option>)}
</select>
</Field>
<Field label="Background type">
<select value={localPage.background_type} onChange={(event) => setLocalPage((current) => ({ ...current, background_type: event.target.value }))} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none">
{['image', 'video', 'gradient'].map((value) => <option key={value} value={value}>{value}</option>)}
</select>
</Field>
<Field label="Background path">
<input value={localPage.background_path || ''} onChange={(event) => setLocalPage((current) => ({ ...current, background_path: event.target.value }))} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" />
</Field>
<Field label="Mobile background path">
<input value={localPage.background_mobile_path || ''} onChange={(event) => setLocalPage((current) => ({ ...current, background_mobile_path: event.target.value }))} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" />
</Field>
<Field label="CTA label">
<input value={localPage.cta_label || ''} onChange={(event) => setLocalPage((current) => ({ ...current, cta_label: event.target.value }))} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" />
</Field>
<Field label="CTA URL">
<input value={localPage.cta_url || ''} onChange={(event) => setLocalPage((current) => ({ ...current, cta_url: event.target.value }))} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" />
</Field>
<Field label="Text position">
<select value={localPage.text_position} onChange={(event) => setLocalPage((current) => ({ ...current, text_position: event.target.value }))} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none">
{['top', 'center', 'bottom'].map((value) => <option key={value} value={value}>{value}</option>)}
</select>
</Field>
<Field label="Animation">
<select value={localPage.animation || ''} onChange={(event) => setLocalPage((current) => ({ ...current, animation: event.target.value }))} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none">
<option value="">None</option>
{['fade-in', 'fly-in-bottom', 'pulse', 'pan-left', 'pan-right'].map((value) => <option key={value} value={value}>{value}</option>)}
</select>
</Field>
<Field label="Overlay strength">
<input type="number" min="0" max="100" value={localPage.overlay_strength || 35} onChange={(event) => setLocalPage((current) => ({ ...current, overlay_strength: event.target.value }))} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" />
</Field>
<Field label="Artwork ID">
<input type="number" min="1" value={localPage.artwork_id || ''} onChange={(event) => setLocalPage((current) => ({ ...current, artwork_id: event.target.value }))} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" />
</Field>
<Field label="Position">
<input type="number" min="1" value={localPage.position || 1} onChange={(event) => setLocalPage((current) => ({ ...current, position: event.target.value }))} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" />
</Field>
<Field label="Credit text">
<input value={localPage.credit_text || ''} onChange={(event) => setLocalPage((current) => ({ ...current, credit_text: event.target.value }))} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" />
</Field>
<label className="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-slate-300">
<input type="checkbox" checked={Boolean(localPage.active)} onChange={(event) => setLocalPage((current) => ({ ...current, active: event.target.checked }))} />
Page active
</label>
</div>
</article>
)
}
export default function WorldWebStoryEditor() {
const { props } = usePage()
const story = props.story
const endpoints = props.endpoints || {}
const worldOptions = props.worldOptions || []
const isNew = Boolean(props.isNew)
const [notice, setNotice] = React.useState('')
const [error, setError] = React.useState('')
const [pages, setPages] = React.useState(story.pages || [])
const [newPage, setNewPage] = React.useState({
layout: 'cover',
background_type: 'image',
headline: '',
body: '',
cta_label: '',
cta_url: '',
alt_text: '',
caption: '',
credit_text: '',
background_path: '',
background_mobile_path: '',
artwork_id: '',
text_position: 'bottom',
overlay_strength: 35,
animation: '',
active: true,
})
React.useEffect(() => {
setPages(story.pages || [])
}, [story.pages])
const form = useForm({
world_id: story.world_id || '',
slug: story.slug || '',
title: story.title || '',
subtitle: story.subtitle || '',
excerpt: story.excerpt || '',
description: story.description || '',
seo_title: story.seo_title || '',
seo_description: story.seo_description || '',
poster_portrait_path: story.poster_portrait_path || '',
poster_square_path: story.poster_square_path || '',
publisher_logo_path: story.publisher_logo_path || '',
status: story.status || 'draft',
featured: Boolean(story.featured),
active: Boolean(story.active),
noindex: Boolean(story.noindex),
published_at: story.published_at || '',
starts_at: story.starts_at || '',
ends_at: story.ends_at || '',
})
function submit(event) {
event.preventDefault()
setError('')
setNotice('')
const options = {
preserveScroll: true,
onSuccess: () => setNotice('Web story saved.'),
onError: (errors) => setError(Object.values(errors)[0] || 'Save failed.'),
}
if (isNew) {
form.post(endpoints.store, options)
return
}
form.patch(endpoints.update, options)
}
async function reloadEditor() {
router.reload({ preserveScroll: true, only: ['story'] })
}
async function createPage(event) {
event.preventDefault()
setError('')
setNotice('')
try {
await requestJson(endpoints.pagesStore, {
body: {
...newPage,
overlay_strength: Number(newPage.overlay_strength || 35),
artwork_id: newPage.artwork_id ? Number(newPage.artwork_id) : null,
active: Boolean(newPage.active),
},
})
setNotice('Page created.')
setNewPage({
layout: 'cover',
background_type: 'image',
headline: '',
body: '',
cta_label: '',
cta_url: '',
alt_text: '',
caption: '',
credit_text: '',
background_path: '',
background_mobile_path: '',
artwork_id: '',
text_position: 'bottom',
overlay_strength: 35,
animation: '',
active: true,
})
reloadEditor()
} catch (requestError) {
setError(requestError.message || 'Unable to create page.')
}
}
async function performStoryAction(url) {
setError('')
setNotice('')
try {
const payload = await requestJson(url)
setNotice(payload.message || 'Action completed.')
reloadEditor()
} catch (requestError) {
setError(requestError.message || 'Action failed.')
}
}
async function reorder(pageId, direction) {
const sorted = [...pages].sort((left, right) => left.position - right.position)
const currentIndex = sorted.findIndex((page) => page.id === pageId)
const targetIndex = currentIndex + direction
if (currentIndex < 0 || targetIndex < 0 || targetIndex >= sorted.length) return
const next = [...sorted]
;[next[currentIndex], next[targetIndex]] = [next[targetIndex], next[currentIndex]]
try {
await requestJson(endpoints.pagesReorder, {
body: { page_ids: next.map((page) => page.id) },
})
reloadEditor()
} catch (requestError) {
setError(requestError.message || 'Unable to reorder pages.')
}
}
async function generateFromWorld() {
if (!form.data.world_id) return
try {
const payload = await requestJson(endpoints.generateFromWorldPattern.replace('__WORLD__', String(form.data.world_id)), {
body: { force: true, pages: Math.max(5, pages.length || 7) },
})
setNotice(payload.message || 'Draft regenerated from World.')
if (payload.story?.edit_url) {
router.visit(payload.story.edit_url)
return
}
reloadEditor()
} catch (requestError) {
setError(requestError.message || 'Generation failed.')
}
}
return (
<div className="w-full pb-16 pt-8">
<Head title={isNew ? 'New World Web Story' : `Edit ${story.title}`} />
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<p className="text-[11px] font-semibold uppercase tracking-[0.28em] text-sky-200/80">Moderation surface</p>
<h1 className="mt-2 text-3xl font-semibold tracking-[-0.04em] text-white">{isNew ? 'Create World Web Story' : story.title}</h1>
<p className="mt-2 text-sm leading-relaxed text-slate-300">Build a standalone AMP story companion for a Skinbase World without changing the canonical World route.</p>
</div>
<div className="flex flex-wrap gap-2">
<Link href={endpoints.index} className="rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]">Back</Link>
{!isNew && story.public_url ? <a href={story.public_url} className="rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]">Open story</a> : null}
{!isNew ? <button type="button" onClick={() => performStoryAction(endpoints.publish)} className="rounded-full border border-emerald-300/20 bg-emerald-400/12 px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-emerald-100 transition hover:bg-emerald-400/18">Publish</button> : null}
{!isNew ? <button type="button" onClick={() => performStoryAction(endpoints.unpublish)} className="rounded-full border border-amber-300/20 bg-amber-400/12 px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-amber-100 transition hover:bg-amber-400/18">Unpublish</button> : null}
</div>
</div>
{notice ? <div className="mt-6 rounded-2xl border border-emerald-300/20 bg-emerald-400/10 px-4 py-3 text-sm text-emerald-50">{notice}</div> : null}
{error ? <div className="mt-6 rounded-2xl border border-rose-300/20 bg-rose-400/10 px-4 py-3 text-sm text-rose-100">{error}</div> : null}
<form onSubmit={submit} className="mt-6 rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.92))] p-6 shadow-[0_24px_70px_rgba(2,6,23,0.32)]">
<div className="grid gap-4 xl:grid-cols-2">
<Field label="Related World">
<select value={form.data.world_id} onChange={(event) => form.setData('world_id', event.target.value)} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none">
<option value="">No related World</option>
{worldOptions.map((world) => <option key={world.value} value={world.value}>{world.label}</option>)}
</select>
</Field>
<Field label="Slug">
<input value={form.data.slug} onChange={(event) => form.setData('slug', event.target.value)} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" />
</Field>
<Field label="Title">
<input value={form.data.title} onChange={(event) => form.setData('title', event.target.value)} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" />
</Field>
<Field label="Subtitle">
<input value={form.data.subtitle} onChange={(event) => form.setData('subtitle', event.target.value)} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" />
</Field>
<Field label="Excerpt">
<textarea value={form.data.excerpt} onChange={(event) => form.setData('excerpt', event.target.value)} rows={3} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" />
</Field>
<Field label="Description">
<textarea value={form.data.description} onChange={(event) => form.setData('description', event.target.value)} rows={3} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" />
</Field>
<Field label="SEO title">
<input value={form.data.seo_title} onChange={(event) => form.setData('seo_title', event.target.value)} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" />
</Field>
<Field label="SEO description">
<textarea value={form.data.seo_description} onChange={(event) => form.setData('seo_description', event.target.value)} rows={3} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" />
</Field>
<Field label="Poster portrait path">
<input value={form.data.poster_portrait_path} onChange={(event) => form.setData('poster_portrait_path', event.target.value)} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" />
</Field>
<Field label="Poster square path">
<input value={form.data.poster_square_path} onChange={(event) => form.setData('poster_square_path', event.target.value)} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" />
</Field>
<Field label="Publisher logo path">
<input value={form.data.publisher_logo_path} onChange={(event) => form.setData('publisher_logo_path', event.target.value)} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" />
</Field>
<Field label="Status">
<select value={form.data.status} onChange={(event) => form.setData('status', event.target.value)} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none">
{['draft', 'published', 'archived'].map((value) => <option key={value} value={value}>{value}</option>)}
</select>
</Field>
<Field label="Starts at">
<input type="datetime-local" value={form.data.starts_at ? form.data.starts_at.slice(0, 16) : ''} onChange={(event) => form.setData('starts_at', event.target.value)} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" />
</Field>
<Field label="Ends at">
<input type="datetime-local" value={form.data.ends_at ? form.data.ends_at.slice(0, 16) : ''} onChange={(event) => form.setData('ends_at', event.target.value)} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" />
</Field>
</div>
<div className="mt-4 flex flex-wrap gap-4">
<label className="flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-sm text-slate-300"><input type="checkbox" checked={Boolean(form.data.featured)} onChange={(event) => form.setData('featured', event.target.checked)} /> Featured</label>
<label className="flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-sm text-slate-300"><input type="checkbox" checked={Boolean(form.data.active)} onChange={(event) => form.setData('active', event.target.checked)} /> Active</label>
<label className="flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-sm text-slate-300"><input type="checkbox" checked={Boolean(form.data.noindex)} onChange={(event) => form.setData('noindex', event.target.checked)} /> Noindex</label>
</div>
<div className="mt-6 flex flex-wrap gap-3">
<button type="submit" disabled={form.processing} className="rounded-full border border-sky-300/20 bg-sky-400/12 px-5 py-2.5 text-xs font-semibold uppercase tracking-[0.14em] text-sky-50 transition hover:bg-sky-400/20 disabled:opacity-60">Save story</button>
{!isNew ? <button type="button" onClick={generateFromWorld} className="rounded-full border border-white/10 bg-white/[0.05] px-5 py-2.5 text-xs font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]">Regenerate from World</button> : null}
</div>
</form>
{!isNew ? (
<>
<section className="mt-8 rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.92))] p-6 shadow-[0_24px_70px_rgba(2,6,23,0.32)]">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<h2 className="text-2xl font-semibold tracking-[-0.03em] text-white">Validation</h2>
<p className="mt-2 text-sm leading-6 text-slate-300">Publish only when poster, logo, page count, alt text, and CTA rules are satisfied.</p>
</div>
<div className={`rounded-full border px-4 py-2 text-xs font-semibold uppercase tracking-[0.14em] ${story.validation?.valid ? 'border-emerald-300/20 bg-emerald-400/12 text-emerald-100' : 'border-amber-300/20 bg-amber-400/12 text-amber-100'}`}>
{story.validation?.valid ? 'Ready to publish' : 'Needs fixes'}
</div>
</div>
{(story.validation?.errors || []).length > 0 ? (
<div className="mt-4 rounded-2xl border border-amber-300/20 bg-amber-400/10 px-4 py-4 text-sm text-amber-50">
<ul className="space-y-2">
{(story.validation.errors || []).map((item) => <li key={item}>{item}</li>)}
</ul>
</div>
) : null}
</section>
<section className="mt-8 rounded-[32px] border border-white/10 bg-[linear-gradient(180deg,rgba(15,23,42,0.96),rgba(2,6,23,0.92))] p-6 shadow-[0_24px_70px_rgba(2,6,23,0.32)]">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<h2 className="text-2xl font-semibold tracking-[-0.03em] text-white">Story pages</h2>
<p className="mt-2 text-sm leading-6 text-slate-300">Keep each page short, visual, and clearly tied back to the World narrative.</p>
</div>
</div>
<form onSubmit={createPage} className="mt-6 grid gap-3 xl:grid-cols-2">
<Field label="New page headline"><input value={newPage.headline} onChange={(event) => setNewPage((current) => ({ ...current, headline: event.target.value }))} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" /></Field>
<Field label="New page caption"><input value={newPage.caption} onChange={(event) => setNewPage((current) => ({ ...current, caption: event.target.value }))} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" /></Field>
<Field label="New page body"><textarea value={newPage.body} onChange={(event) => setNewPage((current) => ({ ...current, body: event.target.value }))} rows={3} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" /></Field>
<Field label="Alt text"><input value={newPage.alt_text} onChange={(event) => setNewPage((current) => ({ ...current, alt_text: event.target.value }))} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" /></Field>
<Field label="Layout"><select value={newPage.layout} onChange={(event) => setNewPage((current) => ({ ...current, layout: event.target.value }))} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none">{['cover', 'artwork', 'creator', 'mood', 'collection', 'cta'].map((value) => <option key={value} value={value}>{value}</option>)}</select></Field>
<Field label="Background type"><select value={newPage.background_type} onChange={(event) => setNewPage((current) => ({ ...current, background_type: event.target.value }))} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none">{['image', 'video', 'gradient'].map((value) => <option key={value} value={value}>{value}</option>)}</select></Field>
<Field label="Background path"><input value={newPage.background_path} onChange={(event) => setNewPage((current) => ({ ...current, background_path: event.target.value }))} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" /></Field>
<Field label="Mobile background path"><input value={newPage.background_mobile_path} onChange={(event) => setNewPage((current) => ({ ...current, background_mobile_path: event.target.value }))} className="w-full rounded-xl border border-white/10 bg-slate-950/70 px-3 py-2 text-white outline-none" /></Field>
<div className="xl:col-span-2 flex justify-end">
<button type="submit" className="rounded-full border border-sky-300/20 bg-sky-400/12 px-5 py-2.5 text-xs font-semibold uppercase tracking-[0.14em] text-sky-50 transition hover:bg-sky-400/20">Add page</button>
</div>
</form>
<div className="mt-6 space-y-4">
{pages.sort((left, right) => left.position - right.position).map((page) => (
<div key={page.id}>
<div className="mb-2 flex justify-end gap-2">
<button type="button" onClick={() => reorder(page.id, -1)} className="rounded-full border border-white/10 bg-white/[0.05] px-3 py-1.5 text-[11px] font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]">Move up</button>
<button type="button" onClick={() => reorder(page.id, 1)} className="rounded-full border border-white/10 bg-white/[0.05] px-3 py-1.5 text-[11px] font-semibold uppercase tracking-[0.14em] text-white transition hover:bg-white/[0.09]">Move down</button>
</div>
<StoryPageCard page={page} endpoints={endpoints} onChanged={reloadEditor} />
</div>
))}
</div>
</section>
</>
) : null}
</div>
)
}