"use client";
import { StatusBlankEvents } from "@openstatus/ui/components/blocks/status-blank";
import {
StatusEvent,
StatusEventAffected,
StatusEventAffectedBadge,
StatusEventAside,
StatusEventContent,
StatusEventDate,
StatusEventGroup,
StatusEventTimelineMaintenance,
StatusEventTimelineReport,
StatusEventTitle,
} from "@openstatus/ui/components/blocks/status-events";
import { useStatusBlocksLabels } from "@openstatus/ui/components/blocks/status-i18n";
import type {
StatusReport,
Maintenance,
} from "@openstatus/ui/components/blocks/status.types";
import { useMemo } from "react";
type UnifiedEvent = {
id: number;
title: string;
startDate: Date;
} & (
| { type: "report"; data: StatusReport }
| { type: "maintenance"; data: Maintenance }
);
/**
* isStatusReport - Type guard for discriminating status reports
*
* Type guard function that narrows a UnifiedEvent to a status report event.
* Used internally by StatusFeed to distinguish between reports and maintenance.
*
* @param event - The unified event to check
* @returns True if the event is a status report
*
* @example
* ```tsx
* if (isStatusReport(event)) {
* // TypeScript knows event.data is StatusReport
* console.log(event.data.updates);
* }
* ```
*/
function isStatusReport(
event: UnifiedEvent,
): event is UnifiedEvent & { type: "report"; data: StatusReport } {
return event.type === "report";
}
/**
* isMaintenance - Type guard for discriminating maintenance events
*
* Type guard function that narrows a UnifiedEvent to a maintenance event.
* Used internally by StatusFeed to distinguish between reports and maintenance.
*
* @param event - The unified event to check
* @returns True if the event is a maintenance
*
* @example
* ```tsx
* if (isMaintenance(event)) {
* // TypeScript knows event.data is Maintenance
* console.log(event.data.from, event.data.to);
* }
* ```
*/
function isMaintenance(
event: UnifiedEvent,
): event is UnifiedEvent & { type: "maintenance"; data: Maintenance } {
return event.type === "maintenance";
}
/**
* StatusFeed - Unified feed of incident reports and maintenance events
*
* Displays a chronological feed combining both status reports (incidents) and
* scheduled maintenance, sorted by date from newest to oldest. The component
* intelligently merges the two event types and renders them using a discriminated
* union pattern for type safety.
*
* **Key Features**:
* - **Unified Timeline**: Combines reports and maintenance in chronological order
* - **Discriminated Union**: Type-safe rendering using TypeScript discriminated unions
* - **Memoization**: Uses `useMemo` to prevent flicker on re-renders
* - **Empty State**: Shows a styled empty state when no events are present
* - **Automatic Rendering**: Each event type is rendered with appropriate components
*
* **Data Structure**:
* - **StatusReport**: Incident reports with updates timeline
* - id, title, affected services
* - updates array with status, message, date
* - **Maintenance**: Scheduled maintenance windows
* - id, title, message, affected services
* - from/to date range
*
* The component creates a unified event array internally, using stable keys
* (`${type}-${id}`) to prevent React flicker when data updates.
*
* @param statusReports - Array of incident reports to display
* @param maintenances - Array of maintenance events to display
*
* @example
* // Feed with both reports and maintenance
* ```tsx
*
* ```
*
* @example
* // Empty state (no events)
* ```tsx
*
* // Displays: "No recent notifications" empty state
* ```
*
* @example
* // Only incident reports
* ```tsx
*
* ```
*
* @see StatusEventGroup - For the feed container
* @see StatusEventTimelineReport - For incident rendering
* @see StatusEventTimelineMaintenance - For maintenance rendering
* @see isStatusReport - For type guard discrimination
* @see isMaintenance - For type guard discrimination
*/
export function StatusFeed({
statusReports = [],
maintenances = [],
footer,
emptyAction,
renderReportMessage,
renderMaintenanceMessage,
renderEvent,
...props
}: React.ComponentProps<"div"> & {
statusReports?: StatusReport[];
maintenances?: Maintenance[];
footer?: React.ReactNode;
emptyAction?: React.ReactNode;
renderReportMessage?: (message: string) => React.ReactNode;
renderMaintenanceMessage?: (message: string) => React.ReactNode;
renderEvent?: (
event:
| { type: "report"; data: StatusReport }
| { type: "maintenance"; data: Maintenance },
children: React.ReactNode,
) => React.ReactNode;
}) {
const labels = useStatusBlocksLabels();
// Memoize the unified events array to prevent flicker on re-renders
const unifiedEvents = useMemo(() => {
return [
...statusReports.map((report): UnifiedEvent => ({
id: report.id,
title: report.title,
type: "report",
startDate:
report.updates[report.updates.length - 1]?.date || new Date(),
data: report,
})),
...maintenances.map((maintenance): UnifiedEvent => ({
id: maintenance.id,
title: maintenance.title,
type: "maintenance",
startDate: maintenance.from,
data: maintenance,
})),
].sort((a, b) => b.startDate.getTime() - a.startDate.getTime());
}, [statusReports, maintenances]);
if (unifiedEvents.length === 0) {
return (
);
}
return (
<>
{unifiedEvents.map((event) => {
// Use stable key to prevent flicker
const key = `${event.type}-${event.id}`;
if (isStatusReport(event)) {
const report = event.data;
const node = (
{report.title}
{report.affected.length > 0 && (
{report.affected.map((affected, index) => (
{affected}
))}
)}
);
return (
{renderEvent
? renderEvent({ type: "report", data: report }, node)
: node}
);
}
if (isMaintenance(event)) {
const maintenance = event.data;
const node = (
{maintenance.title}
{maintenance.affected.length > 0 && (
{maintenance.affected.map((affected, index) => (
{affected}
))}
)}
);
return (
{renderEvent
? renderEvent(
{ type: "maintenance", data: maintenance },
node,
)
: node}
);
}
return null;
})}
{footer}
>
);
}
StatusFeed.displayName = "StatusFeed";