{story.title}
/{story.slug}{story.world ? ` • ${story.world.title}` : ''}
{story.excerpt ?{story.excerpt}
: null}import React from 'react' import { Head, Link, router, 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] || 'Request failed.') } return payload } function replacePattern(pattern, value) { return String(pattern || '').replace('__STORY__', String(value)).replace('__WORLD__', String(value)) } function StatusBadge({ story }) { const tone = story.status === 'published' ? 'border-emerald-300/20 bg-emerald-400/12 text-emerald-100' : story.status === 'archived' ? 'border-amber-300/20 bg-amber-400/12 text-amber-100' : 'border-white/10 bg-white/[0.06] text-slate-200' return {story.status} } export default function WorldWebStoriesIndex() { const { props } = usePage() const stories = props.stories || { data: [] } const endpoints = props.endpoints || {} const worldOptions = props.worldOptions || [] const [filters, setFilters] = React.useState(props.filters || { q: '', status: 'all' }) const [notice, setNotice] = React.useState('') const [error, setError] = React.useState('') const [busyKey, setBusyKey] = React.useState('') const [generator, setGenerator] = React.useState({ world_id: worldOptions[0]?.value || '', pages: 7, force: false, publish: false }) React.useEffect(() => { setFilters(props.filters || { q: '', status: 'all' }) }, [props.filters]) function applyFilters(event) { event.preventDefault() router.get(endpoints.index, filters, { preserveState: true, replace: true, preserveScroll: true }) } async function performAction(key, url, method = 'POST', body = null) { setBusyKey(key) setNotice('') setError('') try { const payload = await requestJson(url, { method, body }) setNotice(payload.message || 'Action completed.') router.reload({ only: ['stories', 'stats', 'filters'], preserveScroll: true }) } catch (requestError) { setError(requestError.message || 'Action failed.') } finally { setBusyKey('') } } async function generateDraft(event) { event.preventDefault() if (!generator.world_id) return setBusyKey('generate') setNotice('') setError('') try { const payload = await requestJson(replacePattern(endpoints.generatePattern, generator.world_id), { body: { pages: Number(generator.pages || 7), force: Boolean(generator.force), publish: Boolean(generator.publish), }, }) setNotice(payload.message || 'Web story generated.') if (payload.story?.edit_url) { router.visit(payload.story.edit_url) return } router.reload({ only: ['stories', 'stats'], preserveScroll: true }) } catch (requestError) { setError(requestError.message || 'Generation failed.') } finally { setBusyKey('') } } return (
Moderation surface
Create standalone AMP Web Stories for Skinbase Worlds, keep them self-canonical, and publish only when the story is complete and visible.
/{story.slug}{story.world ? ` • ${story.world.title}` : ''}
{story.excerpt ?{story.excerpt}
: null}