Replace native selects with NovaSelect
This commit is contained in:
@@ -3,6 +3,187 @@ 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 (
|
||||
<a href={itemHref(item)} className={`group relative block overflow-hidden rounded-2xl border border-white/10 bg-slate-950/80 ${className}`}>
|
||||
{item.image_url ? (
|
||||
<img src={item.image_url} alt={item.title || ''} className="h-full w-full object-cover transition duration-300 group-hover:scale-[1.03]" loading="lazy" />
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center bg-[linear-gradient(135deg,rgba(14,165,233,0.2),rgba(15,23,42,0.95))] text-[10px] font-semibold uppercase tracking-[0.18em] text-sky-100/80">
|
||||
{item.module_label || 'Item'}
|
||||
</div>
|
||||
)}
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 bg-gradient-to-t from-slate-950 via-slate-950/70 to-transparent p-2">
|
||||
<div className="truncate text-[11px] font-semibold text-white">{item.title}</div>
|
||||
<div className="mt-0.5 flex items-center justify-between gap-2 text-[10px] text-slate-300">
|
||||
<span className="truncate">{item.module_label}</span>
|
||||
{showTime && item.scheduled_at ? <span className="shrink-0 text-sky-200">{formatScheduledDate(item.scheduled_at)}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
function CalendarInlineItem({ item, showTime = true }) {
|
||||
return (
|
||||
<a href={itemHref(item)} className="flex items-center gap-3 rounded-2xl border border-white/10 bg-black/20 p-2.5 text-sm text-slate-200 transition hover:border-sky-300/20 hover:bg-sky-300/5">
|
||||
<div className="h-11 w-11 shrink-0 overflow-hidden rounded-xl border border-white/10 bg-slate-950/80">
|
||||
{item.image_url ? (
|
||||
<img src={item.image_url} alt={item.title || ''} className="h-full w-full object-cover" loading="lazy" />
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center bg-[linear-gradient(135deg,rgba(14,165,233,0.2),rgba(15,23,42,0.95))] text-[9px] font-semibold uppercase tracking-[0.18em] text-sky-100/80">
|
||||
{String(item.module_label || 'Item').slice(0, 3)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-semibold text-white">{item.title}</div>
|
||||
<div className="mt-0.5 flex items-center gap-2 text-[11px] text-slate-400">
|
||||
<span className="truncate">{item.module_label}</span>
|
||||
{showTime && item.scheduled_at ? <span className="shrink-0 text-sky-200">{formatScheduledDate(item.scheduled_at)}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
function CalendarDayModal({ day, busyKey, endpoints, onAction, onClose, nowMs }) {
|
||||
if (!day) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-end justify-center bg-slate-950/70 p-3 backdrop-blur-sm md:items-center md:p-6" role="dialog" aria-modal="true" aria-label={`Scheduled items for ${day.label || day.date}`}>
|
||||
<button type="button" aria-label="Close day details" className="absolute inset-0 cursor-default" onClick={onClose} />
|
||||
<div className="relative z-10 flex max-h-[90vh] w-full max-w-3xl flex-col overflow-hidden rounded-[30px] border border-white/10 bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.16),_transparent_30%),linear-gradient(180deg,_rgba(15,23,42,0.98),_rgba(2,6,23,0.98))] shadow-2xl shadow-black/40">
|
||||
<div className="flex items-start justify-between gap-4 border-b border-white/10 px-5 py-4 md:px-6">
|
||||
<div>
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Day queue</div>
|
||||
<h3 className="mt-1 text-xl font-semibold text-white">{day.label || day.date}</h3>
|
||||
<div className="mt-1 text-sm text-slate-400">{Number(day.count || 0).toLocaleString()} scheduled item{Number(day.count || 0) === 1 ? '' : 's'}</div>
|
||||
</div>
|
||||
<button type="button" onClick={onClose} className="rounded-full border border-white/10 px-3 py-1.5 text-sm text-slate-200 transition hover:border-sky-300/20 hover:bg-sky-300/10 hover:text-sky-100">Close</button>
|
||||
</div>
|
||||
|
||||
<div className="overflow-y-auto px-5 py-4 md:px-6 md:py-5">
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
{(day.detail_items || []).map((item) => (
|
||||
<div key={item.id} className="overflow-hidden rounded-[24px] border border-white/10 bg-black/20">
|
||||
<a href={itemHref(item)} className="group block">
|
||||
<div className="relative h-36 overflow-hidden border-b border-white/10 bg-slate-950/80">
|
||||
{item.image_url ? (
|
||||
<img src={item.image_url} alt={item.title || ''} className="h-full w-full object-cover transition duration-300 group-hover:scale-[1.03]" loading="lazy" />
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center bg-[linear-gradient(135deg,rgba(14,165,233,0.2),rgba(15,23,42,0.95))] text-sm font-semibold uppercase tracking-[0.18em] text-sky-100/80">
|
||||
{item.module_label || 'Item'}
|
||||
</div>
|
||||
)}
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 bg-gradient-to-t from-slate-950 via-slate-950/75 to-transparent p-3">
|
||||
<div className="truncate text-sm font-semibold text-white">{item.title}</div>
|
||||
<div className="mt-1 flex items-center justify-between gap-2 text-[11px] text-slate-300">
|
||||
<span className="truncate">{item.module_label}</span>
|
||||
{item.scheduled_at ? <span className="shrink-0 text-sky-200">{formatScheduledDate(item.scheduled_at)}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div className="p-4">
|
||||
<div className="text-xs font-medium text-sky-200">{formatReleaseCountdown(item.scheduled_at, nowMs)}</div>
|
||||
<div className="mt-1 text-xs text-slate-500">{item.subtitle || item.workflow?.readiness?.label || 'Scheduled item'}</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<button type="button" disabled={busyKey === `publish:${item.id}`} onClick={() => onAction(endpoints.publishNowPattern, item, 'publish')} className="rounded-full border border-sky-300/20 bg-sky-300/10 px-3 py-1.5 text-xs text-sky-100 disabled:opacity-50">Publish now</button>
|
||||
<button type="button" disabled={busyKey === `unschedule:${item.id}`} onClick={() => onAction(endpoints.unschedulePattern, item, 'unschedule')} className="rounded-full border border-white/10 px-3 py-1.5 text-xs text-slate-200 disabled:opacity-50">Unschedule</button>
|
||||
<a href={itemHref(item)} className="rounded-full border border-white/10 px-3 py-1.5 text-xs text-slate-200 transition hover:border-sky-300/20 hover:bg-sky-300/5">Open</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div
|
||||
className={`min-h-[156px] rounded-[22px] border p-3 ${day.is_current_month ? 'border-white/10 bg-black/20' : 'border-white/5 bg-black/10'}`}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className={`text-sm font-semibold ${day.is_current_month ? 'text-white' : 'text-slate-500'}`}>{day.day}</span>
|
||||
{hasItems ? (
|
||||
<button type="button" onClick={() => onOpenDetail(day)} className="rounded-full border border-white/10 px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-300 transition hover:border-sky-300/20 hover:bg-sky-300/10 hover:text-sky-100">{day.count}</button>
|
||||
) : (
|
||||
<span className="rounded-full border border-white/10 px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-400">{day.count}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hasItems ? (
|
||||
<div className="mt-3 space-y-2">
|
||||
{items.length === 1 ? (
|
||||
<CalendarThumb item={items[0]} className="h-[92px]" />
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{items.map((item) => (
|
||||
<CalendarThumb key={item.id} item={item} className="h-[58px]" />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{overflowCount > 0 ? (
|
||||
<button type="button" onClick={() => onOpenDetail(day)} className="w-full rounded-xl border border-dashed border-white/10 px-2.5 py-1.5 text-left text-[11px] font-medium text-slate-300 transition hover:border-sky-300/20 hover:bg-sky-300/5 hover:text-sky-100">
|
||||
+{overflowCount} more scheduled
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-6 rounded-2xl border border-dashed border-white/10 px-3 py-6 text-center text-[11px] uppercase tracking-[0.18em] text-slate-600">
|
||||
Quiet day
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
async function requestJson(url, method = 'POST') {
|
||||
const response = await fetch(url, {
|
||||
@@ -26,10 +207,13 @@ export default function StudioCalendar() {
|
||||
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(),
|
||||
@@ -43,6 +227,14 @@ export default function StudioCalendar() {
|
||||
})
|
||||
}
|
||||
|
||||
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}`)
|
||||
@@ -67,6 +259,26 @@ export default function StudioCalendar() {
|
||||
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 (
|
||||
<StudioLayout title={props.title} subtitle={props.description}>
|
||||
<div className="space-y-6">
|
||||
@@ -83,9 +295,9 @@ export default function StudioCalendar() {
|
||||
<span className="block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Search planning queue</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">View</span><select value={filters.view || 'month'} onChange={(event) => updateFilters({ view: event.target.value })} className="w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white">{(calendar.view_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">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">{(calendar.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">Queue</span><select value={filters.status || 'scheduled'} onChange={(event) => updateFilters({ status: event.target.value })} className="w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white">{(calendar.status_options || []).map((option) => <option key={option.value} value={option.value} className="bg-slate-900">{option.label}</option>)}</select></label>
|
||||
<div className="space-y-2 text-sm text-slate-300"><span className="block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">View</span><NovaSelect value={filters.view || 'month'} onChange={(val) => updateFilters({ view: val })} options={calendar.view_options || []} searchable={false} /></div>
|
||||
<div 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><NovaSelect value={filters.module || 'all'} onChange={(val) => updateFilters({ module: val })} options={calendar.module_options || []} searchable={false} /></div>
|
||||
<div className="space-y-2 text-sm text-slate-300"><span className="block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Queue</span><NovaSelect value={filters.status || 'scheduled'} onChange={(val) => updateFilters({ status: val })} options={calendar.status_options || []} searchable={false} /></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -93,12 +305,22 @@ export default function StudioCalendar() {
|
||||
<section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-5">
|
||||
{filters.view === 'week' ? (
|
||||
<>
|
||||
<h2 className="text-lg font-semibold text-white">{calendar.week?.label}</h2>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white">{calendar.week?.label}</h2>
|
||||
<div className="mt-1 text-xs uppercase tracking-[0.18em] text-slate-500">Week planning</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button type="button" onClick={() => shiftCalendar(-1)} className="rounded-full border border-white/10 px-3 py-1.5 text-sm text-slate-200 transition hover:border-sky-300/20 hover:bg-sky-300/10 hover:text-sky-100">Prev week</button>
|
||||
<button type="button" onClick={resetCalendarFocus} className="rounded-full border border-white/10 px-3 py-1.5 text-sm text-slate-200 transition hover:border-sky-300/20 hover:bg-sky-300/10 hover:text-sky-100">Today</button>
|
||||
<button type="button" onClick={() => shiftCalendar(1)} className="rounded-full border border-white/10 px-3 py-1.5 text-sm text-slate-200 transition hover:border-sky-300/20 hover:bg-sky-300/10 hover:text-sky-100">Next week</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 grid gap-3 md:grid-cols-2 xl:grid-cols-7">
|
||||
{(calendar.week?.days || []).map((day) => (
|
||||
<div key={day.date} className="rounded-[22px] border border-white/10 bg-black/20 p-3">
|
||||
<div className="text-sm font-semibold text-white">{day.label}</div>
|
||||
<div className="mt-3 space-y-2">{day.items.length > 0 ? day.items.map((item) => <a key={item.id} href={item.edit_url || item.manage_url} className="block rounded-2xl border border-white/10 px-3 py-2 text-xs text-slate-200">{item.title}</a>) : <div className="text-xs text-slate-500">No scheduled items</div>}</div>
|
||||
<div className="mt-3 space-y-2">{day.items.length > 0 ? day.items.map((item) => <CalendarInlineItem key={item.id} item={item} />) : <div className="text-xs text-slate-500">No scheduled items</div>}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -106,13 +328,23 @@ export default function StudioCalendar() {
|
||||
) : filters.view === 'agenda' ? (
|
||||
<>
|
||||
<h2 className="text-lg font-semibold text-white">Agenda</h2>
|
||||
<div className="mt-4 space-y-4">{(calendar.agenda || []).map((group) => <div key={group.date} className="rounded-[22px] border border-white/10 bg-black/20 p-4"><div className="flex items-center justify-between gap-3"><div className="text-base font-semibold text-white">{group.label}</div><div className="text-xs uppercase tracking-[0.18em] text-slate-500">{group.count} items</div></div><div className="mt-3 space-y-2">{group.items.map((item) => <a key={item.id} href={item.edit_url || item.manage_url} className="flex items-center justify-between gap-3 rounded-2xl border border-white/10 px-3 py-2 text-sm text-slate-200"><span>{item.title}</span><span className="text-xs text-slate-500">{formatScheduledDate(item.scheduled_at)}</span></a>)}</div></div>)}</div>
|
||||
<div className="mt-4 space-y-4">{(calendar.agenda || []).map((group) => <div key={group.date} className="rounded-[22px] border border-white/10 bg-black/20 p-4"><div className="flex items-center justify-between gap-3"><div className="text-base font-semibold text-white">{group.label}</div><div className="text-xs uppercase tracking-[0.18em] text-slate-500">{group.count} items</div></div><div className="mt-3 space-y-2">{group.items.map((item) => <CalendarInlineItem key={item.id} item={item} />)}</div></div>)}</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<h2 className="text-lg font-semibold text-white">{calendar.month?.label}</h2>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white">{calendar.month?.label}</h2>
|
||||
<div className="mt-1 text-xs uppercase tracking-[0.18em] text-slate-500">Month planning</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button type="button" onClick={() => shiftCalendar(-1)} className="rounded-full border border-white/10 px-3 py-1.5 text-sm text-slate-200 transition hover:border-sky-300/20 hover:bg-sky-300/10 hover:text-sky-100">Prev month</button>
|
||||
<button type="button" onClick={resetCalendarFocus} className="rounded-full border border-white/10 px-3 py-1.5 text-sm text-slate-200 transition hover:border-sky-300/20 hover:bg-sky-300/10 hover:text-sky-100">Today</button>
|
||||
<button type="button" onClick={() => shiftCalendar(1)} className="rounded-full border border-white/10 px-3 py-1.5 text-sm text-slate-200 transition hover:border-sky-300/20 hover:bg-sky-300/10 hover:text-sky-100">Next month</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 grid grid-cols-7 gap-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((label) => <div key={label} className="px-2 py-1">{label}</div>)}</div>
|
||||
<div className="mt-2 grid grid-cols-7 gap-2">{(calendar.month?.days || []).map((day) => <div key={day.date} className={`min-h-[120px] rounded-[22px] border p-3 ${day.is_current_month ? 'border-white/10 bg-black/20' : 'border-white/5 bg-black/10'}`}><div className="flex items-center justify-between gap-2"><span className={`text-sm font-semibold ${day.is_current_month ? 'text-white' : 'text-slate-500'}`}>{day.day}</span><span className="text-[10px] uppercase tracking-[0.18em] text-slate-500">{day.count}</span></div><div className="mt-3 space-y-2">{day.items.map((item) => <a key={item.id} href={item.edit_url || item.manage_url} className="block rounded-xl border border-white/10 px-2 py-1.5 text-[11px] text-slate-200">{item.title}</a>)}</div></div>)}</div>
|
||||
<div className="mt-2 grid grid-cols-7 gap-2">{(calendar.month?.days || []).map((day) => <CalendarMonthDay key={day.date} day={day} onOpenDetail={setSelectedDay} />)}</div>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
@@ -134,6 +366,8 @@ export default function StudioCalendar() {
|
||||
</section>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<CalendarDayModal day={selectedDay} busyKey={busyKey} endpoints={props.endpoints} onAction={runAction} onClose={() => setSelectedDay(null)} nowMs={nowMs} />
|
||||
</div>
|
||||
</StudioLayout>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user