import React, { useEffect, useState } from 'react'
import { router, usePage } from '@inertiajs/react'
import StudioLayout from '../../Layouts/StudioLayout'
import { studioSurface, trackStudioEvent } from '../../utils/studioEvents'
import { formatReleaseCountdown, formatScheduledDate } from '../../utils/scheduleCountdown'
import NovaSelect from '../../components/ui/NovaSelect'
function parseFocusDate(value) {
if (!value) return new Date()
const parsed = new Date(`${value}T12:00:00`)
return Number.isNaN(parsed.getTime()) ? new Date() : parsed
}
function toFocusDateValue(date) {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
function shiftFocusDate(value, view, direction) {
const next = new Date(parseFocusDate(value))
if (view === 'week') {
next.setDate(next.getDate() + (direction * 7))
return toFocusDateValue(next)
}
const originalDay = next.getDate()
next.setDate(1)
next.setMonth(next.getMonth() + direction)
const lastDayOfMonth = new Date(next.getFullYear(), next.getMonth() + 1, 0).getDate()
next.setDate(Math.min(originalDay, lastDayOfMonth))
return toFocusDateValue(next)
}
function itemHref(item) {
return item.edit_url || item.manage_url || item.preview_url || item.view_url || '#'
}
function CalendarThumb({ item, className = 'h-full w-full', showTime = false }) {
return (
{item.image_url ? (
) : (
{item.module_label || 'Item'}
)}
{item.title}
{item.module_label}
{showTime && item.scheduled_at ? {formatScheduledDate(item.scheduled_at)} : null}
)
}
function CalendarInlineItem({ item, showTime = true }) {
return (
{item.image_url ? (

) : (
{String(item.module_label || 'Item').slice(0, 3)}
)}
{item.title}
{item.module_label}
{showTime && item.scheduled_at ? {formatScheduledDate(item.scheduled_at)} : null}
)
}
function CalendarDayModal({ day, busyKey, endpoints, onAction, onClose, nowMs }) {
if (!day) return null
return (
Day queue
{day.label || day.date}
{Number(day.count || 0).toLocaleString()} scheduled item{Number(day.count || 0) === 1 ? '' : 's'}
{(day.detail_items || []).map((item) => (
))}
)
}
function CalendarMonthDay({ day, onOpenDetail }) {
const items = day.items || []
const overflowCount = Number(day.overflow_count || Math.max(0, Number(day.count || 0) - items.length))
const hasItems = items.length > 0
return (
{day.day}
{hasItems ? (
) : (
{day.count}
)}
{hasItems ? (
{items.length === 1 ? (
) : (
{items.map((item) => (
))}
)}
{overflowCount > 0 ? (
) : null}
) : (
Quiet day
)}
)
}
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
}
export default function StudioCalendar() {
const { props } = usePage()
const calendar = props.calendar || {}
const filters = calendar.filters || {}
const summary = calendar.summary || {}
const currentView = filters.view || 'month'
const [busyKey, setBusyKey] = useState(null)
const [nowMs, setNowMs] = useState(() => Date.now())
const [selectedDay, setSelectedDay] = useState(null)
const updateFilters = (patch) => {
setSelectedDay(null)
const next = { ...filters, ...patch }
trackStudioEvent('studio_scheduled_opened', {
surface: studioSurface(),
module: next.module,
meta: patch,
})
router.get(window.location.pathname, next, {
preserveScroll: true,
preserveState: true,
replace: true,
})
}
const shiftCalendar = (direction) => {
updateFilters({ focus_date: shiftFocusDate(filters.focus_date, currentView, direction) })
}
const resetCalendarFocus = () => {
updateFilters({ focus_date: toFocusDateValue(new Date()) })
}
const runAction = async (pattern, item, key) => {
const url = String(pattern || '').replace('__MODULE__', item.module).replace('__ID__', String(item.numeric_id))
setBusyKey(`${key}:${item.id}`)
try {
await requestJson(url)
router.reload({ preserveScroll: true, preserveState: true })
} catch (error) {
window.alert(error?.message || 'Unable to update schedule.')
} finally {
setBusyKey(null)
}
}
useEffect(() => {
const hasTimedEntries = Boolean(summary.next_publish_at) || (calendar.scheduled_items || []).some((item) => Boolean(item.scheduled_at))
if (!hasTimedEntries) return undefined
const timer = window.setInterval(() => {
setNowMs(Date.now())
}, 1000)
return () => window.clearInterval(timer)
}, [calendar.scheduled_items, summary.next_publish_at])
useEffect(() => {
if (!selectedDay?.date) return
const nextSelectedDay = (calendar.month?.days || []).find((day) => day.date === selectedDay.date) || null
setSelectedDay(nextSelectedDay)
}, [calendar.month?.days, selectedDay?.date])
useEffect(() => {
if (!selectedDay) return undefined
const handleKeyDown = (event) => {
if (event.key === 'Escape') {
setSelectedDay(null)
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [selectedDay])
return (
Scheduled
{Number(summary.scheduled_total || 0).toLocaleString()}
Unscheduled
{Number(summary.unscheduled_total || 0).toLocaleString()}
Overloaded days
{Number(summary.overloaded_days || 0).toLocaleString()}
Next publish
{formatReleaseCountdown(summary.next_publish_at, nowMs)}
{summary.next_publish_at &&
{formatScheduledDate(summary.next_publish_at)}
}
View updateFilters({ view: val })} options={calendar.view_options || []} searchable={false} />
Module updateFilters({ module: val })} options={calendar.module_options || []} searchable={false} />
Queue updateFilters({ status: val })} options={calendar.status_options || []} searchable={false} />
{filters.view === 'week' ? (
<>
{calendar.week?.label}
Week planning
{(calendar.week?.days || []).map((day) => (
{day.label}
{day.items.length > 0 ? day.items.map((item) =>
) :
No scheduled items
}
))}
>
) : filters.view === 'agenda' ? (
<>
Agenda
{(calendar.agenda || []).map((group) =>
{group.label}
{group.count} items
{group.items.map((item) => )}
)}
>
) : (
<>
{calendar.month?.label}
Month planning
{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((label) =>
{label}
)}
{(calendar.month?.days || []).map((day) => )}
>
)}
setSelectedDay(null)} nowMs={nowMs} />
)
}