Monitor websites uptime using Cloudflare Workers
Something went wrong. Try again.
TSX
at optimization
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869import { useCallback, useEffect, useMemo, useState, type DragEvent, type FormEvent } from 'react';import { useNavigate } from '@tanstack/react-router';import { normalizePublicMonitorSlug, publicStatusPageSlugSchema, type MonitorSummary, type StatusPageSave,} from '@uptime/contracts';
import { api, type PublicStatusPage as PublicStatusPageData } from './api.js';import { monitorDisplayName } from './monitor-format.js';import { formatPercentage } from './status-page-format.js';import { UptimeStrip, uptimeSeverity } from './uptime-strip.js';
interface EditorGroup { key: string; title: string; monitorIds: string[]; collapsed: boolean; width: 'full' | 'half'; showBadges: boolean;}
type SortableMonitor = Pick<MonitorSummary['monitor'], 'name' | 'url'>;const monitorNameCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
export function sortMonitorIdsAlphabetically( monitorIds: readonly string[], monitorById: ReadonlyMap<string, SortableMonitor>,): string[] { return [...monitorIds].sort((leftId, rightId) => { const left = monitorById.get(leftId); const right = monitorById.get(rightId); const leftLabel = left ? monitorDisplayName(left) : leftId; const rightLabel = right ? monitorDisplayName(right) : rightId; return ( monitorNameCollator.compare(leftLabel, rightLabel) || monitorNameCollator.compare(left?.url ?? leftId, right?.url ?? rightId) || leftId.localeCompare(rightId) ); });}
export function StatusPagesIndex() { const [pages, setPages] = useState<Awaited<ReturnType<typeof api.statusPages>>['statusPages']>(); const [error, setError] = useState(''); useEffect(() => { void api .statusPages() .then((result) => setPages(result.statusPages)) .catch((reason) => setError(reason instanceof Error ? reason.message : 'Could not load pages'), ); }, []); return ( <section className="overview status-pages-index"> <div className="page-head"> <div> <p className="mono-label">PUBLIC COMMUNICATION</p> <h1>Status pages</h1> <p>Group monitors into a public, ninety-day service history.</p> </div> <a className="button button--primary" href="/status-pages/new"> Add status page </a> </div> {error ? ( <p className="state state--error">{error}</p> ) : !pages ? ( <p className="state state--loading">Loading status pages…</p> ) : pages.length === 0 ? ( <p className="empty-inline">No status pages yet.</p> ) : ( <div className="status-page-list"> {pages.map((page) => ( <div className="status-page-list__row" key={page.id}> <a className="status-page-list__details" href={`/status-pages/${page.id}`}> <strong>{page.title}</strong> <span>{page.monitorCount} monitors</span> <small>Updated {new Date(page.updatedAt).toLocaleString()}</small> </a> <a className="status-page-list__external" href={`/status/${page.publicSlug ?? page.id}`} target="_blank" rel="noreferrer" aria-label={`Open ${page.title} public status page`} title="Open public status page" > <svg viewBox="0 0 24 24" aria-hidden="true"> <path d="M14 4h6v6M20 4l-9 9" /> <path d="M18 13v5a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h5" /> </svg> </a> </div> ))} </div> )} </section> );}
export function StatusPageEditor({ statusPageId }: { statusPageId: string }) { const creating = statusPageId === 'new'; const navigate = useNavigate(); const [title, setTitle] = useState(''); const [publicSlug, setPublicSlug] = useState<string | null>(null); const [groups, setGroups] = useState<EditorGroup[]>([]); const [monitors, setMonitors] = useState<MonitorSummary[]>([]); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(false); const [error, setError] = useState(''); useEffect(() => { Promise.all([api.monitors(), creating ? Promise.resolve(null) : api.statusPage(statusPageId)]) .then(([monitorResult, page]) => { setMonitors(monitorResult.monitors); if (page && 'groups' in page) { setTitle(page.title); setPublicSlug(page.publicSlug); setGroups( page.groups.map((group) => ({ key: group.id, title: group.title, monitorIds: group.monitors.map((monitor) => monitor.id), collapsed: false, width: group.width ?? 'full', showBadges: group.showBadges ?? true, })), ); } else { setGroups([ { key: crypto.randomUUID(), title: 'Services', monitorIds: [], collapsed: false, width: 'full', showBadges: true, }, ]); } }) .catch((reason) => setError(reason instanceof Error ? reason.message : 'Could not load page')) .finally(() => setLoading(false)); }, [creating, statusPageId]); const monitorById = useMemo( () => new Map(monitors.map((item) => [item.monitor.id, item.monitor])), [monitors], ); const selected = new Set(groups.flatMap((group) => group.monitorIds)); const available = monitors.filter((item) => !selected.has(item.monitor.id)); const slugError = publicSlug ? (publicStatusPageSlugSchema.safeParse(publicSlug).error?.issues[0]?.message ?? '') : '';
function moveGroup(source: number, target: number) { setGroups((current) => reorder(current, source, target)); } function moveGroupBeside(source: number, target: number, side: 'left' | 'right') { if (source === target) return; setGroups((current) => { const moving = current[source]; const targetGroup = current[target]; if (!moving || !targetGroup) return current; const next = current.filter((_, index) => index !== source); const targetIndex = next.findIndex((group) => group.key === targetGroup.key); next.splice(targetIndex + (side === 'right' ? 1 : 0), 0, moving); return next.map((group) => group.key === moving.key || group.key === targetGroup.key ? { ...group, width: 'half' as const } : group, ); }); } function moveMonitor(monitorId: string, targetGroup: number, targetPosition: number) { setGroups((current) => { const next = current.map((group) => ({ ...group, monitorIds: [...group.monitorIds] })); let sourceGroup = -1; let sourcePosition = -1; for (const [groupIndex, group] of next.entries()) { const index = group.monitorIds.indexOf(monitorId); if (index >= 0) { sourceGroup = groupIndex; sourcePosition = index; group.monitorIds.splice(index, 1); } } const adjustedPosition = sourceGroup === targetGroup && sourcePosition < targetPosition ? targetPosition - 1 : targetPosition; next[targetGroup]?.monitorIds.splice(adjustedPosition, 0, monitorId); return next; }); } function removeMonitor(monitorId: string) { setGroups((current) => current.map((group) => ({ ...group, monitorIds: group.monitorIds.filter((id) => id !== monitorId), })), ); } function sortGroupAlphabetically(groupIndex: number) { setGroups((current) => current.map((group, index) => index === groupIndex ? { ...group, monitorIds: sortMonitorIdsAlphabetically(group.monitorIds, monitorById), } : group, ), ); } function dragPayload(event: DragEvent, payload: object) { event.dataTransfer.effectAllowed = 'move'; event.dataTransfer.setData('application/json', JSON.stringify(payload)); } function dropped(event: DragEvent, groupIndex: number, position?: number) { event.preventDefault(); try { const payload = JSON.parse(event.dataTransfer.getData('application/json')) as { type: 'group' | 'monitor'; index?: number; monitorId?: string; }; if (payload.type === 'group' && payload.index !== undefined) { const bounds = event.currentTarget.getBoundingClientRect(); const horizontalPosition = bounds.width ? (event.clientX - bounds.left) / bounds.width : 0.5; if (horizontalPosition < 0.33) moveGroupBeside(payload.index, groupIndex, 'left'); else if (horizontalPosition > 0.67) moveGroupBeside(payload.index, groupIndex, 'right'); else moveGroup(payload.index, groupIndex); } if (payload.type === 'monitor' && payload.monitorId) moveMonitor( payload.monitorId, groupIndex, position ?? groups[groupIndex]?.monitorIds.length ?? 0, ); } catch { // Ignore unrelated drag payloads. } } async function save(event: FormEvent) { event.preventDefault(); setBusy(true); setError(''); const input: StatusPageSave = { title, publicSlug, groups: groups.map((group) => ({ title: group.title, monitorIds: group.monitorIds, width: group.width, showBadges: group.showBadges, })), }; try { const saved = creating ? await api.createStatusPage(input) : await api.updateStatusPage(statusPageId, input); void navigate({ to: '/status-pages/$statusPageId', params: { statusPageId: saved.id } }); } catch (reason) { setError(reason instanceof Error ? reason.message : 'Could not save status page'); } finally { setBusy(false); } } if (loading) return <p className="state state--loading">Loading status page…</p>; return ( <form className="status-page-editor" onSubmit={save}> <div className="page-head"> <div> <p className="mono-label">STATUS PAGE EDITOR</p> <h1>{creating ? 'New status page' : title || 'Status page'}</h1> <p> Drag groups and monitors to set the public display order. Drop a group on another group’s left or right edge to place them in two columns. </p> </div> <div className="form-actions"> {!creating && ( <a className="button button--quiet" href={`/status/${publicSlug ?? statusPageId}`} target="_blank" > Open public page </a> )} <button className="button button--primary" disabled={busy || !title.trim() || Boolean(slugError)} > {busy ? 'Saving…' : 'Save page'} </button> </div> </div> {error && <p className="state state--error">{error}</p>} <label className="field status-page-editor__title"> <span>Page title</span> <input value={title} onChange={(event) => setTitle(event.target.value)} maxLength={120} required /> </label> <label className="field public-slug-field status-page-editor__slug"> <span> Public URL slug <em>optional</em> </span> <span className="public-slug-field__input"> <span aria-hidden="true">/status/</span> <input value={publicSlug ?? ''} placeholder={creating ? 'service-status' : statusPageId} disabled={busy} onBlur={() => { if (publicSlug) setPublicSlug(normalizePublicMonitorSlug(publicSlug)); }} onChange={(event) => setPublicSlug(event.target.value || null)} aria-invalid={Boolean(slugError)} aria-describedby="status-page-slug-help" /> </span> <small id="status-page-slug-help" className={slugError ? 'field-error' : ''}> {slugError || 'Uses lowercase letters, numbers, dots, and hyphens. Leave empty to use the page ID.'} </small> </label> <div className="status-page-editor__layout"> <section className="status-page-editor__groups" aria-label="Status page groups"> {groups.map((group, groupIndex) => ( <article className={`status-group-editor status-group-editor--${group.width}${group.collapsed ? ' status-group-editor--collapsed' : ''}`} key={group.key} onDragOver={(event) => event.preventDefault()} onDrop={(event) => dropped(event, groupIndex)} > <div className="status-group-editor__head"> <span className="drag-handle" draggable title="Drag to reorder group" onDragStart={(event) => dragPayload(event, { type: 'group', index: groupIndex })} > ⠿ </span> <input aria-label="Group title" value={group.title} maxLength={120} required onChange={(event) => setGroups((current) => current.map((item, index) => index === groupIndex ? { ...item, title: event.target.value } : item, ), ) } /> <div className="status-group-editor__actions"> <button type="button" className="button button--quiet" aria-pressed={group.showBadges} aria-label={`${group.showBadges ? 'Hide' : 'Show'} badges in ${group.title || 'group'}`} onClick={() => setGroups((current) => current.map((item, index) => index === groupIndex ? { ...item, showBadges: !item.showBadges } : item, ), ) } > Badges {group.showBadges ? 'on' : 'off'} </button> <button type="button" className="button button--quiet" aria-label={`Make ${group.title || 'group'} ${group.width === 'full' ? 'half width' : 'full width'}`} onClick={() => setGroups((current) => current.map((item, index) => index === groupIndex ? { ...item, width: item.width === 'full' ? 'half' : 'full' } : item, ), ) } > {group.width === 'full' ? '½ width' : 'Full width'} </button> <button type="button" className="button button--quiet" aria-expanded={!group.collapsed} aria-controls={`status-group-${group.key}-monitors`} aria-label={`${group.collapsed ? 'Expand' : 'Collapse'} ${group.title || 'group'}`} onClick={() => setGroups((current) => current.map((item, index) => index === groupIndex ? { ...item, collapsed: !item.collapsed } : item, ), ) } > {group.collapsed ? 'Expand' : 'Collapse'} </button> <button type="button" className="button button--quiet" disabled={group.monitorIds.length < 2} aria-label={`Sort monitors in ${group.title || 'group'} alphabetically`} onClick={() => sortGroupAlphabetically(groupIndex)} > Sort A–Z </button> <button type="button" className="button button--quiet" onClick={() => setGroups((current) => current.filter((_, index) => index !== groupIndex)) } > Remove </button> </div> </div> <div className="status-group-editor__monitors" id={`status-group-${group.key}-monitors`} hidden={group.collapsed} > {group.monitorIds.map((monitorId, position) => { const monitor = monitorById.get(monitorId); if (!monitor) return null; return ( <div className="status-monitor-editor" key={monitorId} draggable onDragStart={(event) => dragPayload(event, { type: 'monitor', monitorId })} onDragOver={(event) => event.preventDefault()} onDrop={(event) => { event.stopPropagation(); dropped(event, groupIndex, position); }} > <span className="drag-handle" title="Drag to move monitor"> ⠿ </span> <span> <span className="monitor-row__name"> <strong>{monitorDisplayName(monitor)}</strong> {group.showBadges && monitor.badge && ( <span className="monitor-badge" style={ { '--badge-color': monitor.badge.color } as React.CSSProperties } > {monitor.badge.name} </span> )} </span> <small>{monitor.url}</small> </span> <button type="button" onClick={() => removeMonitor(monitorId)} aria-label={`Remove ${monitor.name ?? monitor.url}`} > × </button> </div> ); })} {group.monitorIds.length === 0 && <p>Drop monitors here</p>} </div> </article> ))} <button type="button" className="button button--quiet" onClick={() => setGroups((current) => [ ...current, { key: crypto.randomUUID(), title: `Group ${current.length + 1}`, monitorIds: [], collapsed: false, width: 'full', showBadges: true, }, ]) } > Add group </button> </section> <aside className="status-page-editor__available"> <h2>Available monitors</h2> <p>Drag a monitor into a group. Included monitors become publicly viewable.</p> {available.map((item) => ( <div className="status-monitor-editor" key={item.monitor.id} draggable onDragStart={(event) => dragPayload(event, { type: 'monitor', monitorId: item.monitor.id }) } > <span className="drag-handle" title="Drag to add monitor"> ⠿ </span> <span> <span className="monitor-row__name"> <strong>{monitorDisplayName(item.monitor)}</strong> {item.monitor.badge && ( <span className="monitor-badge" style={{ '--badge-color': item.monitor.badge.color } as React.CSSProperties} > {item.monitor.badge.name} </span> )} </span> <small>{item.monitor.url}</small> </span> </div> ))} {available.length === 0 && <p className="empty-inline">All monitors are assigned.</p>} </aside> </div> </form> );}
export const statusPageRefreshIntervalMs = 60_000;
export function PublicStatusPage({ statusPageId }: { statusPageId: string }) { const [page, setPage] = useState<PublicStatusPageData>(); const [error, setError] = useState(''); const [autoRefresh, setAutoRefresh] = useState(false); const refreshPage = useCallback(() => { return api .publicStatusPage(statusPageId) .then((result) => { setPage(result); setError(''); }) .catch((reason) => setError(reason instanceof Error ? reason.message : 'Could not load page'), ); }, [statusPageId]); useEffect(() => { void refreshPage(); }, [refreshPage]); useEffect(() => { if (!autoRefresh) return; const timer = window.setInterval(() => void refreshPage(), statusPageRefreshIntervalMs); return () => window.clearInterval(timer); }, [autoRefresh, refreshPage]); if (error && !page) return <p className="state state--error">{error}</p>; if (!page) return <p className="state state--loading">Loading status…</p>; const problemMonitors = [ ...new Map( page.groups .flatMap((group) => group.monitors) .filter((monitor) => { const displayStatus = monitor.recoveryStatus ?? monitor.status; return ( displayStatus === 'recovering' || (displayStatus === 'down' && (monitor.affectedRegionIds === undefined || monitor.affectedRegionIds.length > 0)) ); }) .map((monitor) => [monitor.id, monitor] as const), ).values(), ]; return ( <article className="public-status-page"> <header className="public-status-page__head"> <div className="public-status-page__topline"> <p className="product-mark">UPTIME STATUS</p> <button type="button" className="button button--quiet public-status-page__refresh-toggle" aria-pressed={autoRefresh} onClick={() => setAutoRefresh((enabled) => !enabled)} > Auto-refresh {autoRefresh ? 'on' : 'off'} </button> </div> <h1>{page.title}</h1> <p>Service availability over the last 90 days.</p> {error && ( <p className="field-error" role="status"> Latest refresh failed: {error} </p> )} </header> {problemMonitors.length > 0 && ( <section className="status-problem-summary" aria-labelledby="status-problem-title"> <div className="status-problem-summary__heading"> <span className="status-problem-summary__indicator" aria-hidden="true"> ! </span> <h2 id="status-problem-title">Some systems currently have problems</h2> </div> <ul className="status-problem-summary__list"> {problemMonitors.map((monitor) => ( <li className={ monitor.recoveryStatus === 'recovering' ? 'status-problem-summary__list-item--recovering' : undefined } key={monitor.id} > <a href={`/monitors/public/${monitor.publicSlug ?? monitor.id}?statusPage=${encodeURIComponent(statusPageId)}`} > {monitorDisplayName(monitor)} </a> <span className={ monitor.recoveryStatus === 'recovering' ? 'status-problem-summary__state--recovering' : undefined } > {monitor.recoveryStatus === 'recovering' ? 'Recovering' : monitor.configuredRegionCount > 1 && (monitor.affectedRegionIds?.length ?? 0) > 0 && (monitor.affectedRegionIds?.length ?? 0) < monitor.configuredRegionCount ? `Issues (${monitor.affectedRegionIds?.join(', ')})` : 'Currently down'} </span> </li> ))} </ul> </section> )} <div className="public-status-page__groups"> {statusPageGroupRows(page.groups).map((row) => { const paired = row.length === 2; const pairedRowCount = paired ? Math.max( ...row.map((group) => Math.min(monitorsPerStatusGroupPage, group.monitors.length)), ) : undefined; const pairedHasPagination = paired ? row.some((group) => group.monitors.length > monitorsPerStatusGroupPage) : false; const pairedTrackCount = pairedRowCount !== undefined ? 1 + pairedRowCount + (pairedHasPagination ? 1 : 0) : undefined; return ( <div className={`public-status-page__group-row${paired ? ' public-status-page__group-row--paired' : ''}`} key={row.map((group) => group.id).join(':')} style={ pairedTrackCount !== undefined ? ({ '--paired-track-count': pairedTrackCount } as React.CSSProperties) : undefined } > {row.map((group) => ( <PublicStatusGroup group={group} statusPageId={statusPageId} pairedHasPagination={pairedHasPagination} {...(pairedRowCount === undefined ? {} : { pairedRowCount })} key={group.id} /> ))} </div> ); })} </div> </article> );}
const monitorsPerStatusGroupPage = 10;
function StatusGroupPagination({ groupTitle, currentPage, pageCount, position, setPage,}: { groupTitle: string; currentPage: number; pageCount: number; position: 'top' | 'bottom'; setPage: React.Dispatch<React.SetStateAction<number>>;}) { return ( <nav className={`group-pagination group-pagination--${position}`} aria-label={`${groupTitle} pagination (${position})`} > <button type="button" className="button button--quiet" disabled={currentPage === 0} onClick={() => setPage((value) => Math.max(0, value - 1))} > Previous </button> <span className="tnum"> {currentPage + 1} / {pageCount} </span> <button type="button" className="button button--quiet" disabled={currentPage === pageCount - 1} onClick={() => setPage((value) => Math.min(pageCount - 1, value + 1))} > Next </button> </nav> );}
function PublicStatusGroup({ group, statusPageId, pairedRowCount, pairedHasPagination = false,}: { group: PublicStatusPageData['groups'][number]; statusPageId: string; pairedRowCount?: number; pairedHasPagination?: boolean;}) { const [page, setPage] = useState(0); const pageCount = Math.max(1, Math.ceil(group.monitors.length / monitorsPerStatusGroupPage)); const currentPage = Math.min(page, pageCount - 1); const visibleMonitors = group.monitors.slice( currentPage * monitorsPerStatusGroupPage, (currentPage + 1) * monitorsPerStatusGroupPage, ); return ( <section className={`public-status-group public-status-group--${group.width ?? 'full'}${pairedRowCount !== undefined ? ' public-status-group--paired' : ''}`} > <div className="public-status-group__head"> <div className="public-status-group__title"> <h2>{group.title}</h2> <span className="public-status-group__count">({group.monitors.length} total)</span> </div> {pageCount > 1 && ( <StatusGroupPagination groupTitle={group.title} currentPage={currentPage} pageCount={pageCount} position="top" setPage={setPage} /> )} </div> {visibleMonitors.map((monitor) => { const displayStatus = monitor.recoveryStatus ?? monitor.status; const percentageSeverity = uptimeSeverity( monitor.uptimePercentage, monitor.uptimeThresholds, ); return ( <a className="public-status-monitor" href={`/monitors/public/${monitor.publicSlug ?? monitor.id}?statusPage=${encodeURIComponent(statusPageId)}`} key={monitor.id} > <span className="public-status-monitor__summary"> <span className="public-status-monitor__name"> <strong>{monitorDisplayName(monitor)}</strong> {group.showBadges !== false && monitor.badge && ( <span className="monitor-badge" style={{ '--badge-color': monitor.badge.color } as React.CSSProperties} > {monitor.badge.name} </span> )} <span className={`status-dot status-dot--${displayStatus}`} /> </span> <span className={`public-status-monitor__uptime public-status-monitor__uptime--${percentageSeverity} tnum`} > {formatPercentage(monitor.uptimePercentage)} </span> <span className={`public-status-monitor__state public-status-monitor__state--${displayStatus}`} > {displayStatus === 'up' ? 'Up' : displayStatus === 'recovering' ? 'Recovering' : displayStatus === 'down' ? 'Issues' : 'No data'} </span> </span> <UptimeStrip days={monitor.days} label={`${monitor.name ?? monitor.url} daily uptime history`} thresholds={monitor.uptimeThresholds} /> </a> ); })} {pairedRowCount !== undefined && visibleMonitors.length < pairedRowCount && ( <span className="public-status-group__fill" style={{ gridRow: `span ${pairedRowCount - visibleMonitors.length}` }} aria-hidden="true" /> )} {pageCount > 1 && ( <StatusGroupPagination groupTitle={group.title} currentPage={currentPage} pageCount={pageCount} position="bottom" setPage={setPage} /> )} {pairedHasPagination && pageCount === 1 && ( <span className="public-status-group__pagination-spacer" aria-hidden="true" /> )} </section> );}
function statusPageGroupRows(groups: PublicStatusPageData['groups']) { const rows: Array<PublicStatusPageData['groups']> = []; for (let index = 0; index < groups.length; index += 1) { const group = groups[index]; if (!group) continue; const next = groups[index + 1]; if ((group.width ?? 'full') === 'half' && next && (next.width ?? 'full') === 'half') { rows.push([group, next]); index += 1; } else { rows.push([group]); } } return rows;}
function reorder<T>(items: T[], source: number, target: number) { const next = [...items]; const [item] = next.splice(source, 1); if (item !== undefined) next.splice(target, 0, item); return next;}