201 lines
12 KiB
JavaScript
201 lines
12 KiB
JavaScript
import React, { useState } from 'react'
|
|
import { router, usePage } from '@inertiajs/react'
|
|
import StudioLayout from '../../Layouts/StudioLayout'
|
|
import { studioSurface, trackStudioEvent } from '../../utils/studioEvents'
|
|
|
|
async function requestJson(url, method = 'POST') {
|
|
const response = await fetch(url, {
|
|
method,
|
|
credentials: 'same-origin',
|
|
headers: {
|
|
Accept: 'application/json',
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '',
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
},
|
|
})
|
|
|
|
const payload = await response.json().catch(() => ({}))
|
|
|
|
if (!response.ok) {
|
|
throw new Error(payload?.message || 'Request failed')
|
|
}
|
|
|
|
return payload
|
|
}
|
|
|
|
function formatDate(value) {
|
|
if (!value) return 'Not scheduled'
|
|
const date = new Date(value)
|
|
if (Number.isNaN(date.getTime())) return 'Not scheduled'
|
|
return date.toLocaleString(undefined, { month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit' })
|
|
}
|
|
|
|
export default function StudioScheduled() {
|
|
const { props } = usePage()
|
|
const listing = props.listing || {}
|
|
const filters = listing.filters || {}
|
|
const summary = listing.summary || {}
|
|
const agenda = listing.agenda || []
|
|
const items = listing.items || []
|
|
const meta = listing.meta || {}
|
|
const rangeOptions = listing.range_options || []
|
|
const endpoints = props.endpoints || {}
|
|
const [busyId, setBusyId] = useState(null)
|
|
|
|
const updateFilters = (patch) => {
|
|
const next = { ...filters, ...patch }
|
|
if (patch.page == null) next.page = 1
|
|
|
|
trackStudioEvent('studio_scheduled_opened', {
|
|
surface: studioSurface(),
|
|
module: next.module,
|
|
meta: patch,
|
|
})
|
|
|
|
router.get(window.location.pathname, next, {
|
|
preserveScroll: true,
|
|
preserveState: true,
|
|
replace: true,
|
|
})
|
|
}
|
|
|
|
const actionUrl = (pattern, item) => String(pattern || '').replace('__MODULE__', item.module).replace('__ID__', item.numeric_id)
|
|
|
|
const runAction = async (item, key) => {
|
|
const url = actionUrl(key === 'publish' ? endpoints.publishNowPattern : endpoints.unschedulePattern, item)
|
|
if (!url) return
|
|
|
|
setBusyId(`${key}:${item.id}`)
|
|
|
|
try {
|
|
await requestJson(url)
|
|
trackStudioEvent(key === 'publish' ? 'studio_schedule_updated' : 'studio_schedule_cleared', {
|
|
surface: studioSurface(),
|
|
module: item.module,
|
|
item_module: item.module,
|
|
item_id: item.numeric_id,
|
|
})
|
|
router.reload({ only: ['listing', 'overview'] })
|
|
} catch (error) {
|
|
window.alert(error?.message || 'Unable to update schedule.')
|
|
} finally {
|
|
setBusyId(null)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<StudioLayout title={props.title} subtitle={props.description}>
|
|
<div className="space-y-6">
|
|
<section className="grid gap-4 xl:grid-cols-[minmax(0,1fr)_340px]">
|
|
<div className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
|
<div className="grid gap-4 md:grid-cols-3">
|
|
<div className="rounded-[24px] border border-white/10 bg-black/20 p-4">
|
|
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Scheduled total</div>
|
|
<div className="mt-2 text-3xl font-semibold text-white">{Number(summary.total || 0).toLocaleString()}</div>
|
|
</div>
|
|
<div className="rounded-[24px] border border-white/10 bg-black/20 p-4 md:col-span-2">
|
|
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Next publish slot</div>
|
|
<div className="mt-2 text-xl font-semibold text-white">{formatDate(summary.next_publish_at)}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-5 grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
|
{(summary.by_module || []).map((entry) => (
|
|
<div key={entry.key} className="rounded-[22px] border border-white/10 bg-black/20 p-4">
|
|
<div className="flex items-center gap-3 text-slate-300">
|
|
<i className={entry.icon} />
|
|
<span className="text-sm font-medium text-white">{entry.label}</span>
|
|
</div>
|
|
<div className="mt-3 text-2xl font-semibold text-white">{Number(entry.count || 0).toLocaleString()}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
|
|
<h2 className="text-lg font-semibold text-white">Agenda</h2>
|
|
<div className="mt-4 space-y-3">
|
|
{agenda.length > 0 ? agenda.slice(0, 6).map((day) => (
|
|
<div key={day.date} className="rounded-[22px] border border-white/10 bg-black/20 p-4">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<span className="text-sm font-semibold text-white">{day.label}</span>
|
|
<span className="text-xs uppercase tracking-[0.18em] text-slate-500">{day.count} items</span>
|
|
</div>
|
|
<div className="mt-2 text-sm text-slate-400">{day.items.slice(0, 2).map((item) => item.title).join(' • ')}</div>
|
|
</div>
|
|
)) : <div className="rounded-[22px] border border-dashed border-white/15 px-4 py-8 text-sm text-slate-400">No scheduled items yet.</div>}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="rounded-[30px] border border-white/10 bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.14),_transparent_35%),linear-gradient(135deg,_rgba(15,23,42,0.86),_rgba(2,6,23,0.96))] p-5 lg:p-6">
|
|
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-5">
|
|
<label className="space-y-2 text-sm text-slate-300">
|
|
<span className="block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Search scheduled work</span>
|
|
<input value={filters.q || ''} onChange={(event) => updateFilters({ q: event.target.value })} className="w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white" placeholder="Title or module" />
|
|
</label>
|
|
<label className="space-y-2 text-sm text-slate-300">
|
|
<span className="block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Module</span>
|
|
<select value={filters.module || 'all'} onChange={(event) => updateFilters({ module: event.target.value })} className="w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white">
|
|
{(listing.module_options || []).map((option) => <option key={option.value} value={option.value} className="bg-slate-900">{option.label}</option>)}
|
|
</select>
|
|
</label>
|
|
<label className="space-y-2 text-sm text-slate-300">
|
|
<span className="block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Date range</span>
|
|
<select value={filters.range || 'upcoming'} onChange={(event) => updateFilters({ range: event.target.value })} className="w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white">
|
|
{rangeOptions.map((option) => <option key={option.value} value={option.value} className="bg-slate-900">{option.label}</option>)}
|
|
</select>
|
|
</label>
|
|
<label className="space-y-2 text-sm text-slate-300">
|
|
<span className="block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Start date</span>
|
|
<input type="date" value={filters.start_date || ''} onChange={(event) => updateFilters({ range: 'custom', start_date: event.target.value })} className="w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white" />
|
|
</label>
|
|
<label className="space-y-2 text-sm text-slate-300">
|
|
<span className="block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">End date</span>
|
|
<input type="date" value={filters.end_date || ''} onChange={(event) => updateFilters({ range: 'custom', end_date: event.target.value })} className="w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white" />
|
|
</label>
|
|
<div className="flex items-end">
|
|
<button type="button" onClick={() => updateFilters({ q: '', module: 'all', range: 'upcoming', start_date: '', end_date: '' })} className="w-full rounded-2xl border border-white/10 px-4 py-3 text-sm text-slate-200">Reset</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="space-y-4">
|
|
{items.length > 0 ? items.map((item) => (
|
|
<article key={item.id} className="rounded-[28px] border border-white/10 bg-white/[0.03] p-5">
|
|
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
|
<div className="min-w-0">
|
|
<div className="flex flex-wrap items-center gap-3 text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-200/70">
|
|
<span>{item.module_label}</span>
|
|
<span>{item.status}</span>
|
|
</div>
|
|
<h2 className="mt-2 text-xl font-semibold text-white">{item.title}</h2>
|
|
<div className="mt-2 flex flex-wrap items-center gap-4 text-sm text-slate-400">
|
|
<span>Scheduled for {formatDate(item.scheduled_at || item.published_at)}</span>
|
|
{item.visibility && <span>Visibility: {item.visibility}</span>}
|
|
{item.updated_at && <span>Last edited {formatDate(item.updated_at)}</span>}
|
|
{item.schedule_timezone && <span>{item.schedule_timezone}</span>}
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-wrap gap-2">
|
|
<a href={item.edit_url || item.manage_url} className="inline-flex items-center gap-2 rounded-full border border-white/10 px-4 py-2 text-sm text-slate-200">Edit</a>
|
|
<a href={item.edit_url || item.manage_url} className="inline-flex items-center gap-2 rounded-full border border-white/10 px-4 py-2 text-sm text-slate-200">Reschedule</a>
|
|
{item.preview_url && <a href={item.preview_url} className="inline-flex items-center gap-2 rounded-full border border-white/10 px-4 py-2 text-sm text-slate-200">Preview</a>}
|
|
<button type="button" disabled={busyId === `publish:${item.id}`} onClick={() => runAction(item, 'publish')} className="inline-flex items-center gap-2 rounded-full border border-sky-300/20 bg-sky-300/10 px-4 py-2 text-sm text-sky-100 disabled:opacity-50">Publish now</button>
|
|
<button type="button" disabled={busyId === `unschedule:${item.id}`} onClick={() => runAction(item, 'unschedule')} className="inline-flex items-center gap-2 rounded-full border border-white/10 px-4 py-2 text-sm text-slate-200 disabled:opacity-50">Unschedule</button>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
)) : <div className="rounded-[28px] border border-dashed border-white/15 px-6 py-16 text-center text-slate-400">No scheduled content matches this view.</div>}
|
|
</section>
|
|
|
|
<div className="flex items-center justify-between rounded-[24px] border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-300">
|
|
<button type="button" disabled={(meta.current_page || 1) <= 1} onClick={() => updateFilters({ page: Math.max(1, (meta.current_page || 1) - 1) })} className="rounded-full border border-white/10 px-4 py-2 disabled:opacity-40">Previous</button>
|
|
<span className="text-xs uppercase tracking-[0.18em] text-slate-500">Page {meta.current_page || 1} of {meta.last_page || 1}</span>
|
|
<button type="button" disabled={(meta.current_page || 1) >= (meta.last_page || 1)} onClick={() => updateFilters({ page: (meta.current_page || 1) + 1 })} className="rounded-full border border-white/10 px-4 py-2 disabled:opacity-40">Next</button>
|
|
</div>
|
|
</div>
|
|
</StudioLayout>
|
|
)
|
|
} |