# Status Feed

> Unified feed component combining status reports and maintenance events

A shadcn/ui block from the [@openstatus](https://openstatus.dev/registry) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/openstatus/status-feed).

- Page, with a live preview: https://shadcn.exchange/r/openstatus/status-feed
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/openstatus/status-feed-light-7ea1df5f136e7b70.png
- Registry item: https://openstatus.dev/r/status-feed.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://openstatus.dev/r/status-feed.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `https://openstatus.dev/r/status-blank.json`, `https://openstatus.dev/r/status-events.json`, `https://openstatus.dev/r/status-types.json`, `https://openstatus.dev/r/status-i18n.json`

## Files

### `src/components/blocks/status-feed.tsx`

````tsx
"use client";
import { StatusBlankEvents } from "@/components/blocks/status-blank";
import {
  StatusEvent,
  StatusEventAffected,
  StatusEventAffectedBadge,
  StatusEventAside,
  StatusEventContent,
  StatusEventDate,
  StatusEventGroup,
  StatusEventTimelineMaintenance,
  StatusEventTimelineReport,
  StatusEventTitle,
} from "@/components/blocks/status-events";
import { useStatusBlocksLabels } from "@/components/blocks/status-i18n";
import type {
  StatusReport,
  Maintenance,
} from "@/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
 * <StatusFeed
 *   statusReports={[
 *     {
 *       id: 1,
 *       title: "API Outage",
 *       affected: ["API", "Database"],
 *       updates: [
 *         {
 *           status: "resolved",
 *           message: "All systems operational",
 *           date: new Date("2024-01-15T12:00:00Z")
 *         },
 *         {
 *           status: "investigating",
 *           message: "Investigating API timeouts",
 *           date: new Date("2024-01-15T11:00:00Z")
 *         }
 *       ]
 *     }
 *   ]}
 *   maintenances={[
 *     {
 *       id: 2,
 *       title: "Database Upgrade",
 *       message: "Upgrading to PostgreSQL 15",
 *       affected: ["Database"],
 *       from: new Date("2024-01-20T02:00:00Z"),
 *       to: new Date("2024-01-20T04:00:00Z")
 *     }
 *   ]}
 * />
 * ```
 *
 * @example
 * // Empty state (no events)
 * ```tsx
 * <StatusFeed statusReports={[]} maintenances={[]} />
 * // Displays: "No recent notifications" empty state
 * ```
 *
 * @example
 * // Only incident reports
 * ```tsx
 * <StatusFeed
 *   statusReports={incidentReports}
 *   maintenances={[]}
 * />
 * ```
 *
 * @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<UnifiedEvent[]>(() => {
    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 (
      <StatusBlankEvents
        title={labels.noRecentNotifications}
        description={labels.noRecentNotificationsDescription}
        action={emptyAction}
      />
    );
  }

  return (
    <>
      <StatusEventGroup data-slot="status-feed" {...props}>
        {unifiedEvents.map((event) => {
          // Use stable key to prevent flicker
          const key = `${event.type}-${event.id}`;

          if (isStatusReport(event)) {
            const report = event.data;
            const node = (
              <StatusEventContent>
                <StatusEventTitle>{report.title}</StatusEventTitle>
                {report.affected.length > 0 && (
                  <StatusEventAffected>
                    {report.affected.map((affected, index) => (
                      <StatusEventAffectedBadge key={index}>
                        {affected}
                      </StatusEventAffectedBadge>
                    ))}
                  </StatusEventAffected>
                )}
                <StatusEventTimelineReport
                  updates={report.updates}
                  renderMessage={renderReportMessage}
                />
              </StatusEventContent>
            );
            return (
              <StatusEvent key={key}>
                <StatusEventAside>
                  <StatusEventDate date={event.startDate} />
                </StatusEventAside>
                {renderEvent
                  ? renderEvent({ type: "report", data: report }, node)
                  : node}
              </StatusEvent>
            );
          }

          if (isMaintenance(event)) {
            const maintenance = event.data;
            const node = (
              <StatusEventContent>
                <StatusEventTitle>{maintenance.title}</StatusEventTitle>
                {maintenance.affected.length > 0 && (
                  <StatusEventAffected>
                    {maintenance.affected.map((affected, index) => (
                      <StatusEventAffectedBadge key={index}>
                        {affected}
                      </StatusEventAffectedBadge>
                    ))}
                  </StatusEventAffected>
                )}
                <StatusEventTimelineMaintenance
                  maintenance={{
                    title: maintenance.title,
                    message: maintenance.message,
                    maintenanceUpdates: maintenance.maintenanceUpdates,
                    from: maintenance.from,
                    to: maintenance.to,
                  }}
                  renderMessage={renderMaintenanceMessage}
                />
              </StatusEventContent>
            );
            return (
              <StatusEvent key={key}>
                <StatusEventAside>
                  <StatusEventDate date={event.startDate} />
                </StatusEventAside>
                {renderEvent
                  ? renderEvent(
                      { type: "maintenance", data: maintenance },
                      node,
                    )
                  : node}
              </StatusEvent>
            );
          }
          return null;
        })}
      </StatusEventGroup>
      {footer}
    </>
  );
}
StatusFeed.displayName = "StatusFeed";
````
