Openstatus www.openstatus.dev

chore: init status page app (#1333)

* chore: init status page

* chore: format fix script

authored by

Maximilian Kaske and committed by
GitHub
ef54f12d e1ab84ab

+12861 -190
+1 -1
apps/dashboard/src/app/(dashboard)/status-pages/[id]/sidebar.tsx
··· 67 67 <Tooltip> 68 68 <TooltipTrigger className="align-middle"> 69 69 <img 70 - className="h-5 border rounded-sm" 70 + className="h-5 rounded-sm border" 71 71 src={BADGE_URL} 72 72 alt="badge" 73 73 />
+1 -1
apps/dashboard/src/app/(public)/login/layout.tsx
··· 13 13 14 14 return ( 15 15 <div className="grid min-h-screen grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-5"> 16 - <aside className="col-span-1 flex w-full flex-col gap-4 border border-border p-4 backdrop-blur-[2px] md:p-8 xl:col-span-2 bg-sidebar"> 16 + <aside className="col-span-1 flex w-full flex-col gap-4 border border-border bg-sidebar p-4 backdrop-blur-[2px] md:p-8 xl:col-span-2"> 17 17 <a href="https://openstatus.dev" className="relative h-8 w-8"> 18 18 <Image 19 19 src="https://openstatus.dev/icon.png"
+1
apps/status-page/.gitignore
··· 1 + .vercel
+21
apps/status-page/components.json
··· 1 + { 2 + "$schema": "https://ui.shadcn.com/schema.json", 3 + "style": "new-york", 4 + "rsc": true, 5 + "tsx": true, 6 + "tailwind": { 7 + "config": "", 8 + "css": "src/app/globals.css", 9 + "baseColor": "neutral", 10 + "cssVariables": true, 11 + "prefix": "" 12 + }, 13 + "aliases": { 14 + "components": "@/components", 15 + "utils": "@/lib/utils", 16 + "ui": "@/components/ui", 17 + "lib": "@/lib", 18 + "hooks": "@/hooks" 19 + }, 20 + "iconLibrary": "lucide" 21 + }
+5
apps/status-page/next-env.d.ts
··· 1 + /// <reference types="next" /> 2 + /// <reference types="next/image-types/global" /> 3 + 4 + // NOTE: This file should not be edited 5 + // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+17
apps/status-page/next.config.ts
··· 1 + import type { NextConfig } from "next"; 2 + 3 + const nextConfig: NextConfig = { 4 + images: { 5 + remotePatterns: [ 6 + new URL("https://openstatus.dev/**"), 7 + new URL("https://**.public.blob.vercel-storage.com/**"), 8 + ], 9 + }, 10 + logging: { 11 + fetches: { 12 + fullUrl: true, 13 + }, 14 + }, 15 + }; 16 + 17 + export default nextConfig;
+102
apps/status-page/package.json
··· 1 + { 2 + "name": "@openstatus/status-page", 3 + "version": "1.0.0", 4 + "private": true, 5 + "scripts": { 6 + "dev": "next dev --turbopack", 7 + "build": "next build", 8 + "start": "next start", 9 + "lint": "next lint", 10 + "tsc": "tsc --noEmit" 11 + }, 12 + "dependencies": { 13 + "@date-fns/tz": "1.2.0", 14 + "@date-fns/utc": "2.1.0", 15 + "@dnd-kit/core": "6.3.1", 16 + "@dnd-kit/modifiers": "9.0.0", 17 + "@dnd-kit/sortable": "10.0.0", 18 + "@dnd-kit/utilities": "3.2.2", 19 + "@hookform/devtools": "4.4.0", 20 + "@hookform/resolvers": "3.9.1", 21 + "@libsql/client": "0.15.9", 22 + "@openpanel/nextjs": "1.0.8", 23 + "@openstatus/analytics": "workspace:*", 24 + "@openstatus/api": "workspace:*", 25 + "@openstatus/assertions": "workspace:*", 26 + "@openstatus/db": "workspace:*", 27 + "@openstatus/emails": "workspace:*", 28 + "@openstatus/error": "workspace:*", 29 + "@openstatus/header-analysis": "workspace:*", 30 + "@openstatus/notification-discord": "workspace:*", 31 + "@openstatus/notification-emails": "workspace:*", 32 + "@openstatus/notification-ntfy": "workspace:*", 33 + "@openstatus/notification-opsgenie": "workspace:*", 34 + "@openstatus/notification-pagerduty": "workspace:*", 35 + "@openstatus/notification-slack": "workspace:*", 36 + "@openstatus/notification-webhook": "workspace:*", 37 + "@openstatus/react": "workspace:*", 38 + "@openstatus/tinybird": "workspace:*", 39 + "@openstatus/tracker": "workspace:*", 40 + "@openstatus/upstash": "workspace:*", 41 + "@openstatus/utils": "workspace:*", 42 + "@radix-ui/react-alert-dialog": "1.1.14", 43 + "@radix-ui/react-avatar": "1.1.10", 44 + "@radix-ui/react-checkbox": "1.3.2", 45 + "@radix-ui/react-collapsible": "1.1.11", 46 + "@radix-ui/react-dialog": "1.1.14", 47 + "@radix-ui/react-dropdown-menu": "2.1.15", 48 + "@radix-ui/react-hover-card": "1.1.14", 49 + "@radix-ui/react-label": "2.1.7", 50 + "@radix-ui/react-popover": "1.1.14", 51 + "@radix-ui/react-portal": "1.1.9", 52 + "@radix-ui/react-progress": "1.1.7", 53 + "@radix-ui/react-radio-group": "1.3.7", 54 + "@radix-ui/react-select": "2.2.5", 55 + "@radix-ui/react-separator": "1.1.7", 56 + "@radix-ui/react-slider": "1.3.5", 57 + "@radix-ui/react-slot": "1.2.3", 58 + "@radix-ui/react-switch": "1.2.5", 59 + "@radix-ui/react-tabs": "1.1.12", 60 + "@radix-ui/react-tooltip": "1.2.7", 61 + "@sentry/nextjs": "8.46.0", 62 + "@tanstack/react-query": "5.81.5", 63 + "@tanstack/react-table": "8.21.3", 64 + "@trpc/client": "11.4.4", 65 + "@trpc/next": "11.4.4", 66 + "@trpc/react-query": "11.4.4", 67 + "@trpc/server": "11.4.4", 68 + "@trpc/tanstack-react-query": "11.4.4", 69 + "class-variance-authority": "0.7.1", 70 + "clsx": "2.1.1", 71 + "cmdk": "1.1.1", 72 + "date-fns": "4.1.0", 73 + "lucide-react": "0.525.0", 74 + "next": "15.4.7", 75 + "next-themes": "0.4.6", 76 + "nuqs": "2.4.3", 77 + "react": "19.1.1", 78 + "react-day-picker": "8.10.1", 79 + "react-dom": "19.1.1", 80 + "react-hook-form": "7.54.1", 81 + "recharts": "2.15.0", 82 + "rehype-react": "8.0.0", 83 + "remark-gfm": "4.0.1", 84 + "remark-parse": "11.0.0", 85 + "remark-rehype": "11.1.2", 86 + "sonner": "2.0.5", 87 + "superjson": "2.2.2", 88 + "tailwind-merge": "3.3.1", 89 + "unified": "11.0.5", 90 + "zod": "3.24.1" 91 + }, 92 + "devDependencies": { 93 + "@tailwindcss/postcss": "4.1.11", 94 + "@types/node": "24.0.8", 95 + "@types/react": "19.1.10", 96 + "@types/react-dom": "19.1.7", 97 + "shadcn": "2.7.0", 98 + "tailwindcss": "4.1.11", 99 + "tw-animate-css": "1.3.4", 100 + "typescript": "5.7.2" 101 + } 102 + }
+5
apps/status-page/postcss.config.mjs
··· 1 + const config = { 2 + plugins: ["@tailwindcss/postcss"], 3 + }; 4 + 5 + export default config;
apps/status-page/public/fonts/CalSans-SemiBold.ttf

This is a binary file and will not be displayed.

+27
apps/status-page/sentry.client.config.ts
··· 1 + // This file configures the initialization of Sentry on the client. 2 + // The config you add here will be used whenever a users loads a page in their browser. 3 + // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 + 5 + import * as Sentry from "@sentry/nextjs"; 6 + 7 + Sentry.init({ 8 + dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, 9 + 10 + // Adjust this value in production, or use tracesSampler for greater control 11 + tracesSampleRate: 0.5, 12 + 13 + // Setting this option to true will print useful information to the console while you're setting up Sentry. 14 + debug: false, 15 + 16 + replaysOnErrorSampleRate: 1.0, 17 + 18 + // This sets the sample rate to be 10%. You may want this to be 100% while 19 + // in development and sample at a lower rate in production 20 + replaysSessionSampleRate: 0.1, 21 + 22 + // You can remove this option if you're not planning to use the Sentry Session Replay feature: 23 + integrations: [ 24 + Sentry.replayIntegration({ maskAllText: true, blockAllMedia: true }), 25 + Sentry.captureConsoleIntegration({ levels: ["error"] }), 26 + ], 27 + });
+16
apps/status-page/sentry.edge.config.ts
··· 1 + // This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on). 2 + // The config you add here will be used whenever one of the edge features is loaded. 3 + // Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally. 4 + // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 5 + import * as Sentry from "@sentry/nextjs"; 6 + 7 + Sentry.init({ 8 + dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, 9 + 10 + // Adjust this value in production, or use tracesSampler for greater control 11 + tracesSampleRate: 0, 12 + 13 + // Setting this option to true will print useful information to the console while you're setting up Sentry. 14 + debug: false, 15 + integrations: [Sentry.captureConsoleIntegration({ levels: ["error"] })], 16 + });
+16
apps/status-page/sentry.server.config.ts
··· 1 + // This file configures the initialization of Sentry on the server. 2 + // The config you add here will be used whenever the server handles a request. 3 + // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 + 5 + import * as Sentry from "@sentry/nextjs"; 6 + 7 + Sentry.init({ 8 + dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, 9 + 10 + // Adjust this value in production, or use tracesSampler for greater control 11 + tracesSampleRate: 0.2, 12 + 13 + // Setting this option to true will print useful information to the console while you're setting up Sentry. 14 + debug: false, 15 + integrations: [Sentry.captureConsoleIntegration({ levels: ["error"] })], 16 + });
+3
apps/status-page/src/app/(public)/page.tsx
··· 1 + export default function Page() { 2 + return <div>Status Page</div>; 3 + }
+7
apps/status-page/src/app/(status-page)/[domain]/events/(list)/page.tsx
··· 1 + "use client"; 2 + 3 + import { StatusEventsTabs } from "@/components/status-page/status-events"; 4 + 5 + export default function Page() { 6 + return <StatusEventsTabs />; 7 + }
+98
apps/status-page/src/app/(status-page)/[domain]/events/(view)/maintenance/page.tsx
··· 1 + "use client"; 2 + 3 + import { formatDate } from "@/lib/formatter"; 4 + 5 + import { 6 + StatusEvent, 7 + StatusEventAffected, 8 + StatusEventAside, 9 + StatusEventContent, 10 + StatusEventTimelineMaintenance, 11 + StatusEventTitle, 12 + } from "@/components/status-page/status-events"; 13 + import { Badge } from "@/components/ui/badge"; 14 + import { Button } from "@/components/ui/button"; 15 + import { maintenances } from "@/data/maintenances"; 16 + import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"; 17 + import { cn } from "@/lib/utils"; 18 + import { ArrowLeft, Check, Copy } from "lucide-react"; 19 + import Link from "next/link"; 20 + 21 + const maintenance = maintenances[0]; 22 + 23 + export default function EventPage() { 24 + const isFuture = maintenance.startDate > new Date(); 25 + return ( 26 + <div className="flex flex-col gap-4"> 27 + <div className="flex w-full flex-row items-center justify-between gap-2 py-0.5"> 28 + <BackButton /> 29 + <CopyButton /> 30 + </div> 31 + <StatusEvent> 32 + <StatusEventAside> 33 + <span className="font-medium text-foreground/80"> 34 + {formatDate(maintenance.startDate, { month: "short" })} 35 + </span> 36 + {isFuture ? ( 37 + <span className="text-info text-sm">Upcoming</span> 38 + ) : null} 39 + </StatusEventAside> 40 + <StatusEventContent hoverable={false}> 41 + <StatusEventTitle>{maintenance.title}</StatusEventTitle> 42 + <StatusEventAffected className="flex flex-wrap gap-1"> 43 + {maintenance.affected.map((affected) => ( 44 + <Badge key={affected} variant="outline" className="text-[10px]"> 45 + {affected} 46 + </Badge> 47 + ))} 48 + </StatusEventAffected> 49 + <StatusEventTimelineMaintenance maintenance={maintenance} /> 50 + </StatusEventContent> 51 + </StatusEvent> 52 + </div> 53 + ); 54 + } 55 + 56 + function BackButton({ 57 + className, 58 + ...props 59 + }: React.ComponentProps<typeof Button>) { 60 + return ( 61 + <Button 62 + variant="ghost" 63 + size="sm" 64 + className={cn("text-muted-foreground", className)} 65 + asChild 66 + {...props} 67 + > 68 + <Link href="/status-page/events"> 69 + <ArrowLeft /> 70 + Back 71 + </Link> 72 + </Button> 73 + ); 74 + } 75 + 76 + function CopyButton({ 77 + className, 78 + ...props 79 + }: React.ComponentProps<typeof Button>) { 80 + const { copy, isCopied } = useCopyToClipboard(); 81 + 82 + return ( 83 + <Button 84 + variant="outline" 85 + size="icon" 86 + onClick={() => 87 + copy(window.location.href, { 88 + successMessage: "Link copied to clipboard", 89 + }) 90 + } 91 + className={cn("size-8", className)} 92 + {...props} 93 + > 94 + {isCopied ? <Check /> : <Copy />} 95 + <span className="sr-only">Copy Link</span> 96 + </Button> 97 + ); 98 + }
+95
apps/status-page/src/app/(status-page)/[domain]/events/(view)/report/page.tsx
··· 1 + "use client"; 2 + 3 + import { formatDate } from "@/lib/formatter"; 4 + 5 + import { 6 + StatusEvent, 7 + StatusEventAffected, 8 + StatusEventAside, 9 + StatusEventContent, 10 + StatusEventTimelineReport, 11 + StatusEventTitle, 12 + } from "@/components/status-page/status-events"; 13 + import { Badge } from "@/components/ui/badge"; 14 + import { Button } from "@/components/ui/button"; 15 + import { statusReports } from "@/data/status-reports"; 16 + import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"; 17 + import { cn } from "@/lib/utils"; 18 + import { ArrowLeft, Check, Copy } from "lucide-react"; 19 + import Link from "next/link"; 20 + 21 + const report = statusReports[1]; 22 + 23 + export default function EventPage() { 24 + return ( 25 + <div className="flex flex-col gap-4"> 26 + <div className="flex w-full flex-row items-center justify-between gap-2 py-0.5"> 27 + <BackButton /> 28 + <CopyButton /> 29 + </div> 30 + <StatusEvent> 31 + <StatusEventAside> 32 + <span className="font-medium text-foreground/80"> 33 + {formatDate(report.startedAt, { month: "short" })} 34 + </span> 35 + </StatusEventAside> 36 + <StatusEventContent hoverable={false}> 37 + <StatusEventTitle>{report.name}</StatusEventTitle> 38 + <StatusEventAffected className="flex flex-wrap gap-1"> 39 + {report.affected.map((affected) => ( 40 + // TODO: use StatusEventAffectedBadge component 41 + <Badge key={affected} variant="outline" className="text-[10px]"> 42 + {affected} 43 + </Badge> 44 + ))} 45 + </StatusEventAffected> 46 + <StatusEventTimelineReport updates={report.updates} /> 47 + </StatusEventContent> 48 + </StatusEvent> 49 + </div> 50 + ); 51 + } 52 + 53 + function BackButton({ 54 + className, 55 + ...props 56 + }: React.ComponentProps<typeof Button>) { 57 + return ( 58 + <Button 59 + variant="ghost" 60 + size="sm" 61 + className={cn("text-muted-foreground", className)} 62 + asChild 63 + {...props} 64 + > 65 + <Link href="/status-page/events"> 66 + <ArrowLeft /> 67 + Back 68 + </Link> 69 + </Button> 70 + ); 71 + } 72 + 73 + function CopyButton({ 74 + className, 75 + ...props 76 + }: React.ComponentProps<typeof Button>) { 77 + const { copy, isCopied } = useCopyToClipboard(); 78 + 79 + return ( 80 + <Button 81 + variant="outline" 82 + size="icon" 83 + onClick={() => 84 + copy(window.location.href, { 85 + successMessage: "Link copied to clipboard", 86 + }) 87 + } 88 + className={cn("size-8", className)} 89 + {...props} 90 + > 91 + {isCopied ? <Check /> : <Copy />} 92 + <span className="sr-only">Copy Link</span> 93 + </Button> 94 + ); 95 + }
+27
apps/status-page/src/app/(status-page)/[domain]/events/layout.tsx
··· 1 + "use client"; 2 + 3 + import { useStatusPage } from "@/components/status-page/floating-button"; 4 + import { 5 + Status, 6 + StatusContent, 7 + StatusDescription, 8 + StatusHeader, 9 + StatusTitle, 10 + } from "@/components/status-page/status"; 11 + 12 + export default function EventLayout({ 13 + children, 14 + }: { 15 + children: React.ReactNode; 16 + }) { 17 + const { variant } = useStatusPage(); 18 + return ( 19 + <Status variant={variant}> 20 + <StatusHeader> 21 + <StatusTitle>Craft</StatusTitle> 22 + <StatusDescription>Stay informed about the stability</StatusDescription> 23 + </StatusHeader> 24 + <StatusContent>{children}</StatusContent> 25 + </Status> 26 + ); 27 + }
+149
apps/status-page/src/app/(status-page)/[domain]/layout.tsx
··· 1 + "use client"; 2 + 3 + /** 4 + * TODO: 5 + * - add different header 6 + * - add different chart/tracker 7 + * - add subscription popover (choose which one you'd like to allow) 8 + * - use the '@/components/status-page` for the components 9 + */ 10 + 11 + import { Link } from "@/components/common/link"; 12 + import { 13 + FloatingButton, 14 + StatusPageProvider, 15 + } from "@/components/status-page/floating-button"; 16 + import { StatusUpdates } from "@/components/status-page/status-updates"; 17 + import { Button } from "@/components/ui/button"; 18 + import { 19 + Sheet, 20 + SheetContent, 21 + SheetHeader, 22 + SheetTitle, 23 + SheetTrigger, 24 + } from "@/components/ui/sheet"; 25 + import { cn } from "@/lib/utils"; 26 + import { Menu } from "lucide-react"; 27 + import NextLink from "next/link"; 28 + import { usePathname } from "next/navigation"; 29 + import { useState } from "react"; 30 + const nav = [ 31 + { label: "Status", href: "/status-page" }, 32 + { label: "Events", href: "/status-page/events" }, 33 + { label: "Monitors", href: "/status-page/monitors" }, 34 + ]; 35 + 36 + export default function Layout({ children }: { children: React.ReactNode }) { 37 + return ( 38 + <StatusPageProvider> 39 + <div className="flex min-h-screen flex-col gap-4"> 40 + <header className="w-full border-b"> 41 + <nav className="mx-auto flex max-w-2xl items-center justify-between gap-3 px-3 py-2"> 42 + {/* NOTE: same width as the `StatusUpdates` button */} 43 + <div className="w-[105px] shrink-0"> 44 + <Link href="/"> 45 + <img 46 + src="https://www.openstatus.dev/icon.png" 47 + alt="Craft" 48 + className="size-8 rounded-full border" 49 + /> 50 + </Link> 51 + </div> 52 + <NavDesktop className="hidden md:flex" /> 53 + <StatusUpdates className="hidden md:block" /> 54 + <div className="flex gap-3 md:hidden"> 55 + <NavMobile /> 56 + <StatusUpdates /> 57 + </div> 58 + </nav> 59 + </header> 60 + <main className="mx-auto w-full max-w-2xl flex-1 px-3 py-2"> 61 + {children} 62 + </main> 63 + <footer className="w-full border-t"> 64 + <div className="mx-auto max-w-2xl px-3 py-2"> 65 + <p className="text-center text-muted-foreground"> 66 + Powered by <Link href="#">OpenStatus</Link> 67 + </p> 68 + </div> 69 + </footer> 70 + </div> 71 + <FloatingButton /> 72 + </StatusPageProvider> 73 + ); 74 + } 75 + 76 + function NavDesktop({ className, ...props }: React.ComponentProps<"ul">) { 77 + const pathname = usePathname(); 78 + return ( 79 + <ul className={cn("flex flex-row gap-2", className)} {...props}> 80 + {nav.map((item) => { 81 + const isActive = 82 + item.href === "/status-page" 83 + ? pathname === item.href 84 + : pathname.startsWith(item.href); 85 + return ( 86 + <li key={item.label}> 87 + <Button 88 + variant={isActive ? "secondary" : "ghost"} 89 + size="sm" 90 + asChild 91 + > 92 + <NextLink href={item.href}>{item.label}</NextLink> 93 + </Button> 94 + </li> 95 + ); 96 + })} 97 + </ul> 98 + ); 99 + } 100 + 101 + function NavMobile({ 102 + className, 103 + ...props 104 + }: React.ComponentProps<typeof Button>) { 105 + const pathname = usePathname(); 106 + const [open, setOpen] = useState(false); 107 + return ( 108 + <Sheet open={open} onOpenChange={setOpen}> 109 + <SheetTrigger asChild> 110 + <Button 111 + variant="secondary" 112 + size="sm" 113 + className={cn("size-8", className)} 114 + {...props} 115 + > 116 + <Menu /> 117 + </Button> 118 + </SheetTrigger> 119 + <SheetContent side="top"> 120 + <SheetHeader className="border-b"> 121 + <SheetTitle>Menu</SheetTitle> 122 + </SheetHeader> 123 + <div className="px-1 pb-4"> 124 + <ul className="flex flex-col gap-1"> 125 + {nav.map((item) => { 126 + const isActive = 127 + item.href === "/status-page" 128 + ? pathname === item.href 129 + : pathname.startsWith(item.href); 130 + return ( 131 + <li key={item.label} className="w-full"> 132 + <Button 133 + variant={isActive ? "secondary" : "ghost"} 134 + onClick={() => setOpen(false)} 135 + className="w-full justify-start" 136 + size="sm" 137 + asChild 138 + > 139 + <NextLink href={item.href}>{item.label}</NextLink> 140 + </Button> 141 + </li> 142 + ); 143 + })} 144 + </ul> 145 + </div> 146 + </SheetContent> 147 + </Sheet> 148 + ); 149 + }
+53
apps/status-page/src/app/(status-page)/[domain]/monitors/page.tsx
··· 1 + "use client"; 2 + 3 + import { ChartAreaPercentiles } from "@/components/chart/chart-area-percentiles"; 4 + import { useStatusPage } from "@/components/status-page/floating-button"; 5 + import { 6 + Status, 7 + StatusContent, 8 + StatusDescription, 9 + StatusHeader, 10 + StatusTitle, 11 + } from "@/components/status-page/status"; 12 + import { StatusMonitorTitle } from "@/components/status-page/status-monitor"; 13 + import { StatusMonitorDescription } from "@/components/status-page/status-monitor"; 14 + import { monitors } from "@/data/monitors"; 15 + import Link from "next/link"; 16 + 17 + export default function Page() { 18 + const { variant } = useStatusPage(); 19 + return ( 20 + <Status variant={variant}> 21 + <StatusHeader> 22 + <StatusTitle>Craft</StatusTitle> 23 + <StatusDescription>Stay informed about the stability</StatusDescription> 24 + </StatusHeader> 25 + {/* TODO: create components */} 26 + <StatusContent className="flex flex-col gap-6"> 27 + {monitors 28 + .filter((monitor) => monitor.public) 29 + .map((monitor) => ( 30 + <Link 31 + key={monitor.id} 32 + href="/status-page/monitors/view" 33 + className="rounded-lg" 34 + > 35 + <div className="group -mx-3 -my-2 flex flex-col gap-2 rounded-lg border border-transparent px-3 py-2 hover:border-border/50 hover:bg-muted/50"> 36 + <div className="flex flex-row items-center gap-2"> 37 + <StatusMonitorTitle>{monitor.name}</StatusMonitorTitle> 38 + <StatusMonitorDescription> 39 + {monitor.description} 40 + </StatusMonitorDescription> 41 + </div> 42 + <ChartAreaPercentiles 43 + className="h-[80px]" 44 + legendClassName="pb-1" 45 + singleSeries 46 + /> 47 + </div> 48 + </Link> 49 + ))} 50 + </StatusContent> 51 + </Status> 52 + ); 53 + }
+370
apps/status-page/src/app/(status-page)/[domain]/monitors/view/page.tsx
··· 1 + "use client"; 2 + 3 + import { ChartAreaPercentiles } from "@/components/chart/chart-area-percentiles"; 4 + import { ChartLineRegions } from "@/components/chart/chart-line-regions"; 5 + import { 6 + MetricCard, 7 + MetricCardGroup, 8 + MetricCardHeader, 9 + MetricCardTitle, 10 + MetricCardValue, 11 + } from "@/components/content/metric-card"; 12 + import { 13 + Status, 14 + StatusContent, 15 + StatusDescription, 16 + StatusHeader, 17 + StatusTitle, 18 + } from "@/components/status-page/status"; 19 + import { 20 + StatusChartContent, 21 + StatusChartDescription, 22 + StatusChartHeader, 23 + StatusChartTitle, 24 + } from "@/components/status-page/status-charts"; 25 + import { StatusMonitor } from "@/components/status-page/status-monitor"; 26 + import { chartData } from "@/components/status-page/utils"; 27 + import { Badge } from "@/components/ui/badge"; 28 + import { Button } from "@/components/ui/button"; 29 + import { 30 + DropdownMenu, 31 + DropdownMenuContent, 32 + DropdownMenuGroup, 33 + DropdownMenuItem, 34 + DropdownMenuLabel, 35 + DropdownMenuTrigger, 36 + } from "@/components/ui/dropdown-menu"; 37 + import { 38 + Popover, 39 + PopoverContent, 40 + PopoverTrigger, 41 + } from "@/components/ui/popover"; 42 + import { Separator } from "@/components/ui/separator"; 43 + import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 44 + import { monitors } from "@/data/monitors"; 45 + import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"; 46 + import { formatNumber } from "@/lib/formatter"; 47 + import { cn } from "@/lib/utils"; 48 + import { Check, Copy, TrendingUp } from "lucide-react"; 49 + import { useState } from "react"; 50 + 51 + // TODO: add error range on ChartAreaLatency 52 + // TODO: add timerange (1d, 7d, 14d) or leave as is and have 7d default? 53 + // TODO: how to deal with the latency by region percentiles + interval/resolution 54 + 55 + const metrics = [ 56 + { 57 + label: "UPTIME", 58 + value: "99.99%", 59 + variant: "success" as const, 60 + }, 61 + { 62 + label: "FAILS", 63 + value: "3", 64 + variant: "destructive" as const, 65 + }, 66 + { 67 + label: "DEGRADED", 68 + value: "0", 69 + variant: "warning" as const, 70 + }, 71 + { 72 + label: "CHECKS", 73 + value: "5.102", 74 + variant: "ghost" as const, 75 + }, 76 + ]; 77 + 78 + export default function Page() { 79 + return ( 80 + <Status> 81 + <StatusHeader> 82 + <StatusTitle>OpenStatus 418</StatusTitle> 83 + <StatusDescription> 84 + I&apos;m a teapot - Just random values 85 + </StatusDescription> 86 + </StatusHeader> 87 + <StatusContent className="flex flex-col gap-6"> 88 + <div className="flex w-full flex-row items-center justify-between gap-2 py-0.5"> 89 + <DropdownPeriod /> 90 + <CopyButton /> 91 + </div> 92 + <StatusMonitorTabs defaultValue="global"> 93 + <StatusMonitorTabsList className="grid grid-cols-3"> 94 + <StatusMonitorTabsTrigger value="global"> 95 + <StatusMonitorTabsTriggerLabel> 96 + Global Latency 97 + </StatusMonitorTabsTriggerLabel> 98 + <StatusMonitorTabsTriggerValue> 99 + 287 - 568ms{" "} 100 + <Badge variant="outline" className="py-px text-[10px]"> 101 + p75 102 + </Badge> 103 + </StatusMonitorTabsTriggerValue> 104 + </StatusMonitorTabsTrigger> 105 + <StatusMonitorTabsTrigger value="region"> 106 + <StatusMonitorTabsTriggerLabel> 107 + Region Latency 108 + </StatusMonitorTabsTriggerLabel> 109 + <StatusMonitorTabsTriggerValue> 110 + 7 regions{" "} 111 + <Badge 112 + variant="outline" 113 + className="py-px font-mono text-[10px]" 114 + > 115 + arn <TrendingUp className="size-3" /> 116 + </Badge> 117 + </StatusMonitorTabsTriggerValue> 118 + </StatusMonitorTabsTrigger> 119 + <StatusMonitorTabsTrigger value="uptime"> 120 + <StatusMonitorTabsTriggerLabel> 121 + Uptime 122 + </StatusMonitorTabsTriggerLabel> 123 + <StatusMonitorTabsTriggerValue> 124 + 99.99%{" "} 125 + <Badge variant="outline" className="py-px text-[10px]"> 126 + {formatNumber(5102, { 127 + notation: "compact", 128 + compactDisplay: "short", 129 + }).replace("K", "k")}{" "} 130 + checks 131 + </Badge> 132 + </StatusMonitorTabsTriggerValue> 133 + </StatusMonitorTabsTrigger> 134 + </StatusMonitorTabsList> 135 + <StatusMonitorTabsContent value="global"> 136 + <StatusChartContent> 137 + <StatusChartHeader> 138 + <StatusChartTitle>Global Latency</StatusChartTitle> 139 + <StatusChartDescription> 140 + The aggregated latency from all active regions based on 141 + different <PopoverQuantile>quantiles</PopoverQuantile>. 142 + </StatusChartDescription> 143 + </StatusChartHeader> 144 + <ChartAreaPercentiles 145 + className="h-[250px]" 146 + legendClassName="justify-start pt-1 ps-1" 147 + legendVerticalAlign="top" 148 + xAxisHide={false} 149 + yAxisDomain={[0, "dataMax"]} 150 + /> 151 + </StatusChartContent> 152 + </StatusMonitorTabsContent> 153 + <StatusMonitorTabsContent value="region"> 154 + <StatusChartContent> 155 + <StatusChartHeader> 156 + <StatusChartTitle>Latency by Region</StatusChartTitle> 157 + <StatusChartDescription> 158 + {/* TODO: we could add an information to p95 that it takes the highest selected global latency percentile */} 159 + Region latency per{" "} 160 + <code className="font-medium text-foreground">p75</code>{" "} 161 + <PopoverQuantile>quantile</PopoverQuantile>, sorted by slowest 162 + region. Compare up to{" "} 163 + <code className="font-medium text-foreground">3</code>{" "} 164 + regions. 165 + </StatusChartDescription> 166 + </StatusChartHeader> 167 + <ChartLineRegions className="h-[250px]" /> 168 + </StatusChartContent> 169 + </StatusMonitorTabsContent> 170 + <StatusMonitorTabsContent value="uptime"> 171 + <StatusChartContent> 172 + <StatusChartHeader> 173 + <StatusChartTitle>Total Uptime</StatusChartTitle> 174 + <StatusChartDescription> 175 + Main values of uptime and availability, transparent. 176 + </StatusChartDescription> 177 + </StatusChartHeader> 178 + <MetricCardGroup className="sm:grid-cols-4 lg:grid-cols-4"> 179 + {metrics.map((metric) => { 180 + if (metric === null) 181 + return <div key={metric} className="hidden lg:block" />; 182 + return ( 183 + <MetricCard key={metric.label} variant={metric.variant}> 184 + <MetricCardHeader> 185 + <MetricCardTitle className="truncate"> 186 + {metric.label} 187 + </MetricCardTitle> 188 + </MetricCardHeader> 189 + <MetricCardValue>{metric.value}</MetricCardValue> 190 + </MetricCard> 191 + ); 192 + })} 193 + </MetricCardGroup> 194 + <StatusMonitor 195 + barType="absolute" 196 + cardType="requests" 197 + data={chartData} 198 + monitor={monitors[1]} 199 + /> 200 + </StatusChartContent> 201 + </StatusMonitorTabsContent> 202 + </StatusMonitorTabs> 203 + </StatusContent> 204 + </Status> 205 + ); 206 + } 207 + 208 + // Use Link instead of copy (same for reports and maintenance) 209 + function CopyButton({ 210 + className, 211 + ...props 212 + }: React.ComponentProps<typeof Button>) { 213 + const { copy, isCopied } = useCopyToClipboard(); 214 + 215 + return ( 216 + <Button 217 + variant="outline" 218 + size="icon" 219 + onClick={() => 220 + copy(window.location.href, { 221 + successMessage: "Link copied to clipboard", 222 + }) 223 + } 224 + className={cn("size-8", className)} 225 + {...props} 226 + > 227 + {isCopied ? <Check /> : <Copy />} 228 + <span className="sr-only">Copy Link</span> 229 + </Button> 230 + ); 231 + } 232 + 233 + const PERIOD_VALUES = [ 234 + { 235 + value: "1d", 236 + label: "Last day", 237 + }, 238 + { 239 + value: "7d", 240 + label: "Last 7 days", 241 + }, 242 + { 243 + value: "14d", 244 + label: "Last 14 days", 245 + }, 246 + ]; 247 + 248 + function DropdownPeriod() { 249 + const [period, setPeriod] = useState("1d"); 250 + return ( 251 + <DropdownMenu> 252 + <DropdownMenuTrigger asChild> 253 + <Button variant="outline" size="sm"> 254 + {PERIOD_VALUES.find(({ value }) => value === period)?.label} 255 + </Button> 256 + </DropdownMenuTrigger> 257 + <DropdownMenuContent align="start"> 258 + <DropdownMenuGroup> 259 + <DropdownMenuLabel className="font-medium text-muted-foreground text-xs"> 260 + Period 261 + </DropdownMenuLabel> 262 + {PERIOD_VALUES.map(({ value, label }) => ( 263 + <DropdownMenuItem key={value} onSelect={() => setPeriod(value)}> 264 + {label} 265 + {period === value ? <Check className="ml-auto shrink-0" /> : null} 266 + </DropdownMenuItem> 267 + ))} 268 + </DropdownMenuGroup> 269 + </DropdownMenuContent> 270 + </DropdownMenu> 271 + ); 272 + } 273 + 274 + function PopoverQuantile({ 275 + children, 276 + className, 277 + ...props 278 + }: React.ComponentProps<typeof PopoverTrigger>) { 279 + return ( 280 + <Popover> 281 + <PopoverTrigger 282 + className={cn( 283 + "shrink-0 rounded-md p-0 underline decoration-muted-foreground/70 decoration-dotted underline-offset-2 outline-none transition-all hover:decoration-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=open]:decoration-foreground dark:aria-invalid:ring-destructive/40", 284 + className, 285 + )} 286 + {...props} 287 + > 288 + {children} 289 + </PopoverTrigger> 290 + <PopoverContent side="top" className="p-0 text-sm"> 291 + <p className="px-3 py-2 font-medium"> 292 + A quantile represents a specific percentile in your dataset. 293 + </p> 294 + <Separator /> 295 + <p className="px-3 py-2 text-muted-foreground"> 296 + For example, p50 is the 50th percentile - the point below which 50% of 297 + data falls. Higher percentiles include more data and highlight the 298 + upper range. 299 + </p> 300 + </PopoverContent> 301 + </Popover> 302 + ); 303 + } 304 + 305 + function StatusMonitorTabs({ 306 + className, 307 + ...props 308 + }: React.ComponentProps<typeof Tabs>) { 309 + return <Tabs className={cn("gap-6", className)} {...props} />; 310 + } 311 + 312 + function StatusMonitorTabsList({ 313 + className, 314 + ...props 315 + }: React.ComponentProps<typeof TabsList>) { 316 + return ( 317 + <TabsList 318 + className={cn("flex h-auto min-h-fit w-full", className)} 319 + {...props} 320 + /> 321 + ); 322 + } 323 + 324 + function StatusMonitorTabsTrigger({ 325 + className, 326 + ...props 327 + }: React.ComponentProps<typeof TabsTrigger>) { 328 + return ( 329 + <TabsTrigger 330 + className={cn( 331 + "min-w-0 flex-1 flex-col items-start gap-0.5 text-foreground dark:text-foreground", 332 + className, 333 + )} 334 + {...props} 335 + /> 336 + ); 337 + } 338 + 339 + function StatusMonitorTabsTriggerLabel({ 340 + className, 341 + ...props 342 + }: React.ComponentProps<"div">) { 343 + return ( 344 + <div className={cn("w-full truncate text-left", className)} {...props} /> 345 + ); 346 + } 347 + 348 + function StatusMonitorTabsTriggerValue({ 349 + className, 350 + ...props 351 + }: React.ComponentProps<"div">) { 352 + return ( 353 + <div 354 + className={cn( 355 + "text-wrap text-left text-muted-foreground text-xs", 356 + className, 357 + )} 358 + {...props} 359 + /> 360 + ); 361 + } 362 + 363 + function StatusMonitorTabsContent({ 364 + className, 365 + ...props 366 + }: React.ComponentProps<typeof TabsContent>) { 367 + return ( 368 + <TabsContent className={cn("flex flex-col gap-2", className)} {...props} /> 369 + ); 370 + }
+92
apps/status-page/src/app/(status-page)/[domain]/page.tsx
··· 1 + "use client"; 2 + 3 + import { useStatusPage } from "@/components/status-page/floating-button"; 4 + import { 5 + Status, 6 + StatusBanner, 7 + StatusContent, 8 + StatusDescription, 9 + StatusEmptyState, 10 + StatusEmptyStateDescription, 11 + StatusEmptyStateTitle, 12 + StatusHeader, 13 + StatusTitle, 14 + } from "@/components/status-page/status"; 15 + import { StatusMonitor } from "@/components/status-page/status-monitor"; 16 + import { StatusTrackerGroup } from "@/components/status-page/status-tracker-group"; 17 + import { chartData } from "@/components/status-page/utils"; 18 + import { monitors } from "@/data/monitors"; 19 + import { Newspaper } from "lucide-react"; 20 + 21 + export default function Page() { 22 + const { variant, cardType, barType, showUptime } = useStatusPage(); 23 + 24 + return ( 25 + <div className="flex flex-col gap-6"> 26 + <Status variant={variant}> 27 + <StatusHeader> 28 + <StatusTitle>Craft</StatusTitle> 29 + <StatusDescription> 30 + Stay informed about the stability 31 + </StatusDescription> 32 + </StatusHeader> 33 + <StatusBanner /> 34 + <StatusContent> 35 + <StatusMonitor 36 + variant={variant} 37 + cardType={cardType} 38 + barType={barType} 39 + data={chartData} 40 + monitor={monitors[1]} 41 + showUptime={showUptime} 42 + /> 43 + <StatusTrackerGroup title="US Endpoints" variant={variant}> 44 + <StatusMonitor 45 + variant={variant} 46 + cardType={cardType} 47 + barType={barType} 48 + data={chartData} 49 + monitor={monitors[0]} 50 + showUptime={showUptime} 51 + /> 52 + <StatusMonitor 53 + variant={variant} 54 + cardType={cardType} 55 + barType={barType} 56 + data={chartData} 57 + monitor={monitors[1]} 58 + showUptime={showUptime} 59 + /> 60 + </StatusTrackerGroup> 61 + <StatusTrackerGroup title="EU Endpoints" variant={variant}> 62 + <StatusMonitor 63 + variant={variant} 64 + cardType={cardType} 65 + barType={barType} 66 + data={chartData} 67 + monitor={monitors[0]} 68 + showUptime={showUptime} 69 + /> 70 + <StatusMonitor 71 + variant={variant} 72 + cardType={cardType} 73 + barType={barType} 74 + data={chartData} 75 + monitor={monitors[1]} 76 + showUptime={showUptime} 77 + /> 78 + </StatusTrackerGroup> 79 + </StatusContent> 80 + <StatusContent> 81 + <StatusEmptyState> 82 + <Newspaper className="size-4 text-muted-foreground" /> 83 + <StatusEmptyStateTitle>No recent reports</StatusEmptyStateTitle> 84 + <StatusEmptyStateDescription> 85 + There have been no reports within the last 7 days. 86 + </StatusEmptyStateDescription> 87 + </StatusEmptyState> 88 + </StatusContent> 89 + </Status> 90 + </div> 91 + ); 92 + }
+21
apps/status-page/src/app/api/trpc/edge/[trpc]/route.ts
··· 1 + import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; 2 + import type { NextRequest } from "next/server"; 3 + 4 + import { createTRPCContext } from "@openstatus/api"; 5 + import { edgeRouter } from "@openstatus/api/src/edge"; 6 + 7 + export const runtime = "edge"; 8 + 9 + const handler = (req: NextRequest) => 10 + fetchRequestHandler({ 11 + endpoint: "/api/trpc/edge", 12 + router: edgeRouter, 13 + req: req, 14 + createContext: () => createTRPCContext({ req }), 15 + onError: ({ error }) => { 16 + console.log("Error in tRPC handler (edge)"); 17 + console.error(error); 18 + }, 19 + }); 20 + 21 + export { handler as GET, handler as POST };
+22
apps/status-page/src/app/api/trpc/lambda/[trpc]/route.ts
··· 1 + import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; 2 + import type { NextRequest } from "next/server"; 3 + 4 + import { createTRPCContext } from "@openstatus/api"; 5 + import { lambdaRouter } from "@openstatus/api/src/lambda"; 6 + 7 + // Stripe is incompatible with Edge runtimes due to using Node.js events 8 + // export const runtime = "edge"; 9 + 10 + const handler = (req: NextRequest) => 11 + fetchRequestHandler({ 12 + endpoint: "/api/trpc/lambda", 13 + router: lambdaRouter, 14 + req: req, 15 + createContext: () => createTRPCContext({ req }), 16 + onError: ({ error }) => { 17 + console.log("Error in tRPC handler (lambda)"); 18 + console.error(error); 19 + }, 20 + }); 21 + 22 + export { handler as GET, handler as POST };
apps/status-page/src/app/apple.ico

This is a binary file and will not be displayed.

apps/status-page/src/app/favicon.ico

This is a binary file and will not be displayed.

+25
apps/status-page/src/app/global-error.tsx
··· 1 + "use client"; 2 + 3 + import * as Sentry from "@sentry/nextjs"; 4 + import NextError from "next/error"; 5 + import { useEffect } from "react"; 6 + 7 + export default function GlobalError({ 8 + error, 9 + }: { 10 + error: Error & { digest?: string }; 11 + }) { 12 + useEffect(() => { 13 + Sentry.captureException(error); 14 + }, [error]); 15 + 16 + return ( 17 + <html lang="en"> 18 + <body> 19 + {/* This is the default Next.js error component but it doesn't allow omitting the statusCode property yet. */} 20 + {/* biome-ignore lint/suspicious/noExplicitAny: <explanation> */} 21 + <NextError statusCode={undefined as any} /> 22 + </body> 23 + </html> 24 + ); 25 + }
+177
apps/status-page/src/app/globals.css
··· 1 + @import "tailwindcss"; 2 + @import "tw-animate-css"; 3 + 4 + /* safelist */ 5 + @source inline("has-data-[slot=slider-range]:bg-red-500"); 6 + 7 + @custom-variant dark (&:is(.dark *)); 8 + 9 + @theme { 10 + --breakpoint-xs: 30rem; 11 + } 12 + 13 + @theme inline { 14 + --color-background: var(--background); 15 + --color-foreground: var(--foreground); 16 + --font-sans: var(--font-geist-sans); 17 + --font-mono: var(--font-geist-mono); 18 + --color-sidebar-ring: var(--sidebar-ring); 19 + --color-sidebar-border: var(--sidebar-border); 20 + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 21 + --color-sidebar-accent: var(--sidebar-accent); 22 + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 23 + --color-sidebar-primary: var(--sidebar-primary); 24 + --color-sidebar-foreground: var(--sidebar-foreground); 25 + --color-sidebar: var(--sidebar); 26 + --color-chart-5: var(--chart-5); 27 + --color-chart-4: var(--chart-4); 28 + --color-chart-3: var(--chart-3); 29 + --color-chart-2: var(--chart-2); 30 + --color-chart-1: var(--chart-1); 31 + --color-ring: var(--ring); 32 + --color-input: var(--input); 33 + --color-border: var(--border); 34 + --color-destructive: var(--destructive); 35 + --color-accent-foreground: var(--accent-foreground); 36 + --color-accent: var(--accent); 37 + --color-muted-foreground: var(--muted-foreground); 38 + --color-muted: var(--muted); 39 + --color-secondary-foreground: var(--secondary-foreground); 40 + --color-secondary: var(--secondary); 41 + --color-primary-foreground: var(--primary-foreground); 42 + --color-primary: var(--primary); 43 + --color-popover-foreground: var(--popover-foreground); 44 + --color-popover: var(--popover); 45 + --color-card-foreground: var(--card-foreground); 46 + --color-card: var(--card); 47 + --radius-sm: calc(var(--radius) - 4px); 48 + --radius-md: calc(var(--radius) - 2px); 49 + --radius-lg: var(--radius); 50 + --radius-xl: calc(var(--radius) + 4px); 51 + 52 + --color-success: var(--success); 53 + --color-warning: var(--warning); 54 + --color-info: var(--info); 55 + } 56 + 57 + :root { 58 + --radius: 0.625rem; 59 + --background: oklch(1 0 0); 60 + --foreground: oklch(0.145 0 0); 61 + --card: oklch(1 0 0); 62 + --card-foreground: oklch(0.145 0 0); 63 + --popover: oklch(1 0 0); 64 + --popover-foreground: oklch(0.145 0 0); 65 + --primary: oklch(0.205 0 0); 66 + --primary-foreground: oklch(0.985 0 0); 67 + --secondary: oklch(0.97 0 0); 68 + --secondary-foreground: oklch(0.205 0 0); 69 + --muted: oklch(0.97 0 0); 70 + --muted-foreground: oklch(0.556 0 0); 71 + --accent: oklch(0.97 0 0); 72 + --accent-foreground: oklch(0.205 0 0); 73 + --destructive: oklch(0.577 0.245 27.325); 74 + --border: oklch(0.922 0 0); 75 + --input: oklch(0.922 0 0); 76 + --ring: oklch(0.708 0 0); 77 + --chart-1: oklch(0.646 0.222 41.116); 78 + --chart-2: oklch(0.6 0.118 184.704); 79 + --chart-3: oklch(0.398 0.07 227.392); 80 + --chart-4: oklch(0.828 0.189 84.429); 81 + --chart-5: oklch(0.769 0.188 70.08); 82 + --sidebar: oklch(0.985 0 0); 83 + --sidebar-foreground: oklch(0.145 0 0); 84 + --sidebar-primary: oklch(0.205 0 0); 85 + --sidebar-primary-foreground: oklch(0.985 0 0); 86 + --sidebar-accent: oklch(0.97 0 0); 87 + --sidebar-accent-foreground: oklch(0.205 0 0); 88 + --sidebar-border: oklch(0.922 0 0); 89 + --sidebar-ring: oklch(0.708 0 0); 90 + 91 + --success: oklch(0.72 0.19 150); 92 + --warning: oklch(0.77 0.16 70); 93 + --info: oklch(0.62 0.19 260); 94 + 95 + --rainbow-1: oklch(0.64 0.21 25); 96 + --rainbow-2: oklch(0.70 0.19 48); 97 + --rainbow-3: oklch(0.77 0.16 70); 98 + --rainbow-4: oklch(0.80 0.16 86); 99 + --rainbow-5: oklch(0.77 0.20 131); 100 + --rainbow-6: oklch(0.72 0.19 150); 101 + --rainbow-7: oklch(0.70 0.15 162); 102 + --rainbow-8: oklch(0.70 0.12 183); 103 + --rainbow-9: oklch(0.71 0.13 215); 104 + --rainbow-10: oklch(0.68 0.15 237); 105 + --rainbow-11: oklch(0.62 0.19 260); 106 + --rainbow-12: oklch(0.59 0.20 277); 107 + --rainbow-13: oklch(0.61 0.22 293); 108 + --rainbow-14: oklch(0.63 0.23 304); 109 + --rainbow-15: oklch(0.67 0.26 322); 110 + --rainbow-16: oklch(0.66 0.21 354); 111 + --rainbow-17: oklch(0.65 0.22 16); 112 + } 113 + 114 + .dark { 115 + --background: oklch(0.145 0 0); 116 + --foreground: oklch(0.985 0 0); 117 + --card: oklch(0.205 0 0); 118 + --card-foreground: oklch(0.985 0 0); 119 + --popover: oklch(0.205 0 0); 120 + --popover-foreground: oklch(0.985 0 0); 121 + --primary: oklch(0.922 0 0); 122 + --primary-foreground: oklch(0.205 0 0); 123 + --secondary: oklch(0.269 0 0); 124 + --secondary-foreground: oklch(0.985 0 0); 125 + --muted: oklch(0.269 0 0); 126 + --muted-foreground: oklch(0.708 0 0); 127 + --accent: oklch(0.269 0 0); 128 + --accent-foreground: oklch(0.985 0 0); 129 + --destructive: oklch(0.704 0.191 22.216); 130 + --border: oklch(1 0 0 / 10%); 131 + --input: oklch(1 0 0 / 15%); 132 + --ring: oklch(0.556 0 0); 133 + --chart-1: oklch(0.488 0.243 264.376); 134 + --chart-2: oklch(0.696 0.17 162.48); 135 + --chart-3: oklch(0.769 0.188 70.08); 136 + --chart-4: oklch(0.627 0.265 303.9); 137 + --chart-5: oklch(0.645 0.246 16.439); 138 + --sidebar: oklch(0.205 0 0); 139 + --sidebar-foreground: oklch(0.985 0 0); 140 + --sidebar-primary: oklch(0.488 0.243 264.376); 141 + --sidebar-primary-foreground: oklch(0.985 0 0); 142 + --sidebar-accent: oklch(0.269 0 0); 143 + --sidebar-accent-foreground: oklch(0.985 0 0); 144 + --sidebar-border: oklch(1 0 0 / 10%); 145 + --sidebar-ring: oklch(0.556 0 0); 146 + 147 + --success: oklch(0.72 0.19 150); 148 + --warning: oklch(0.77 0.16 70); 149 + --info: oklch(0.62 0.19 260); 150 + 151 + --rainbow-1: oklch(0.64 0.21 25); 152 + --rainbow-2: oklch(0.70 0.19 48); 153 + --rainbow-3: oklch(0.77 0.16 70); 154 + --rainbow-4: oklch(0.80 0.16 86); 155 + --rainbow-5: oklch(0.77 0.20 131); 156 + --rainbow-6: oklch(0.72 0.19 150); 157 + --rainbow-7: oklch(0.70 0.15 162); 158 + --rainbow-8: oklch(0.70 0.12 183); 159 + --rainbow-9: oklch(0.71 0.13 215); 160 + --rainbow-10: oklch(0.68 0.15 237); 161 + --rainbow-11: oklch(0.62 0.19 260); 162 + --rainbow-12: oklch(0.59 0.20 277); 163 + --rainbow-13: oklch(0.61 0.22 293); 164 + --rainbow-14: oklch(0.63 0.23 304); 165 + --rainbow-15: oklch(0.67 0.26 322); 166 + --rainbow-16: oklch(0.66 0.21 354); 167 + --rainbow-17: oklch(0.65 0.22 16); 168 + } 169 + 170 + @layer base { 171 + * { 172 + @apply border-border outline-ring/50; 173 + } 174 + body { 175 + @apply bg-background text-foreground; 176 + } 177 + }
+73
apps/status-page/src/app/layout.tsx
··· 1 + import type { Metadata } from "next"; 2 + import { Geist, Geist_Mono } from "next/font/google"; 3 + import "./globals.css"; 4 + import { TailwindIndicator } from "@/components/tailwind-indicator"; 5 + import { ThemeProvider } from "@/components/theme-provider"; 6 + import { Toaster } from "@/components/ui/sonner"; 7 + import { TRPCReactProvider } from "@/lib/trpc/client"; 8 + import { cn } from "@/lib/utils"; 9 + import LocalFont from "next/font/local"; 10 + import { NuqsAdapter } from "nuqs/adapters/next/app"; 11 + import { ogMetadata, twitterMetadata } from "./metadata"; 12 + import { defaultMetadata } from "./metadata"; 13 + 14 + const cal = LocalFont({ 15 + src: "../../public/fonts/CalSans-SemiBold.ttf", 16 + variable: "--font-cal-sans", 17 + }); 18 + 19 + const geistSans = Geist({ 20 + variable: "--font-geist-sans", 21 + subsets: ["latin"], 22 + }); 23 + 24 + const geistMono = Geist_Mono({ 25 + variable: "--font-geist-mono", 26 + subsets: ["latin"], 27 + }); 28 + 29 + export const metadata: Metadata = { 30 + ...defaultMetadata, 31 + twitter: { 32 + ...twitterMetadata, 33 + }, 34 + openGraph: { 35 + ...ogMetadata, 36 + }, 37 + }; 38 + 39 + // export const dynamic = "error"; 40 + 41 + export default function RootLayout({ 42 + children, 43 + }: Readonly<{ 44 + children: React.ReactNode; 45 + }>) { 46 + return ( 47 + <html lang="en" suppressHydrationWarning> 48 + <body 49 + className={cn( 50 + geistSans.variable, 51 + geistMono.variable, 52 + cal.variable, 53 + "antialiased", 54 + )} 55 + > 56 + <NuqsAdapter> 57 + <ThemeProvider 58 + attribute="class" 59 + defaultTheme="system" 60 + enableSystem 61 + disableTransitionOnChange 62 + > 63 + <TRPCReactProvider> 64 + {children} 65 + <TailwindIndicator /> 66 + <Toaster richColors expand /> 67 + </TRPCReactProvider> 68 + </ThemeProvider> 69 + </NuqsAdapter> 70 + </body> 71 + </html> 72 + ); 73 + }
+37
apps/status-page/src/app/metadata.ts
··· 1 + import type { Metadata } from "next"; 2 + 3 + export const TITLE = "OpenStatus"; 4 + export const DESCRIPTION = 5 + "OpenStatus is an open-source platform to monitor your services and keep your users informed."; 6 + 7 + const OG_TITLE = "OpenStatus"; 8 + const OG_DESCRIPTION = "Monitor your services and keep your users informed."; 9 + const FOOTER = "app.openstatus.dev"; 10 + const IMAGE = "assets/og/dashboard-v2.png"; 11 + 12 + export const defaultMetadata: Metadata = { 13 + title: { 14 + template: `%s | ${TITLE}`, 15 + default: TITLE, 16 + }, 17 + description: DESCRIPTION, 18 + metadataBase: new URL("https://www.openstatus.dev"), 19 + }; 20 + 21 + export const twitterMetadata: Metadata["twitter"] = { 22 + title: TITLE, 23 + description: DESCRIPTION, 24 + card: "summary_large_image", 25 + images: [ 26 + `/api/og?title=${OG_TITLE}&description=${OG_DESCRIPTION}&footer=${FOOTER}&image=${IMAGE}`, 27 + ], 28 + }; 29 + 30 + export const ogMetadata: Metadata["openGraph"] = { 31 + title: TITLE, 32 + description: DESCRIPTION, 33 + type: "website", 34 + images: [ 35 + `/api/og?title=${OG_TITLE}&description=${OG_DESCRIPTION}&footer=${FOOTER}&image=${IMAGE}`, 36 + ], 37 + };
+8
apps/status-page/src/app/react-table.d.ts
··· 1 + import "@tanstack/react-table"; 2 + 3 + declare module "@tanstack/react-table" { 4 + interface ColumnMeta { 5 + headerClassName?: string; 6 + cellClassName?: string; 7 + } 8 + }
+239
apps/status-page/src/components/chart/chart-area-percentiles.tsx
··· 1 + "use client"; 2 + 3 + import { 4 + type ChartConfig, 5 + ChartContainer, 6 + ChartLegend, 7 + ChartTooltip, 8 + ChartTooltipContent, 9 + } from "@/components/ui/chart"; 10 + import { regionPercentile } from "@/data/region-percentile"; 11 + import { formatMilliseconds } from "@/lib/formatter"; 12 + import { cn } from "@/lib/utils"; 13 + import { useState } from "react"; 14 + import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; 15 + import type { AxisDomain } from "recharts/types/util/types"; 16 + import { ChartLegendBadge } from "./chart-legend-badge"; 17 + import { ChartTooltipNumber } from "./chart-tooltip-number"; 18 + 19 + const chartConfig = { 20 + p50: { 21 + label: "p50", 22 + color: "var(--chart-1)", 23 + }, 24 + p75: { 25 + label: "p75", 26 + color: "var(--chart-2)", 27 + }, 28 + p90: { 29 + label: "p90", 30 + color: "var(--chart-4)", 31 + }, 32 + p95: { 33 + label: "p95", 34 + color: "var(--chart-3)", 35 + }, 36 + p99: { 37 + label: "p99", 38 + color: "var(--chart-5)", 39 + }, 40 + error: { 41 + label: "error", 42 + color: "var(--destructive)", 43 + }, 44 + } satisfies ChartConfig; 45 + 46 + function avg(values: number[]) { 47 + return Math.round( 48 + values.reduce((acc, curr) => acc + curr, 0) / values.length, 49 + ); 50 + } 51 + 52 + const chartData = regionPercentile; 53 + 54 + export function ChartAreaPercentiles({ 55 + className, 56 + singleSeries, 57 + xAxisHide = true, 58 + legendVerticalAlign = "bottom", 59 + legendClassName, 60 + withError = false, 61 + yAxisDomain = ["dataMin", "dataMax"], 62 + }: { 63 + className?: string; 64 + singleSeries?: boolean; 65 + xAxisHide?: boolean; 66 + legendVerticalAlign?: "top" | "bottom"; 67 + legendClassName?: string; 68 + withError?: boolean; 69 + yAxisDomain?: AxisDomain; 70 + }) { 71 + const [activeSeries, setActiveSeries] = useState< 72 + Array<keyof typeof chartConfig> 73 + >(["p75"]); 74 + 75 + return ( 76 + <ChartContainer 77 + config={chartConfig} 78 + className={cn("h-[100px] w-full", className)} 79 + > 80 + <AreaChart 81 + accessibilityLayer 82 + data={chartData} 83 + margin={{ 84 + left: 0, 85 + right: 0, 86 + // NOTE: otherwise the line is cut off 87 + top: 2, 88 + bottom: 2, 89 + }} 90 + > 91 + <ChartLegend 92 + verticalAlign={legendVerticalAlign} 93 + content={ 94 + <ChartLegendBadge 95 + handleActive={(item) => { 96 + setActiveSeries((prev) => { 97 + if (item.dataKey) { 98 + const key = item.dataKey as keyof typeof chartConfig; 99 + if (singleSeries) { 100 + return [key]; 101 + } 102 + if (prev.includes(key)) { 103 + return prev.filter((item) => item !== key); 104 + } 105 + return [...prev, key]; 106 + } 107 + return prev; 108 + }); 109 + }} 110 + active={activeSeries} 111 + annotation={{ 112 + p50: formatMilliseconds(avg(chartData.map((item) => item.p50))), 113 + p75: formatMilliseconds(avg(chartData.map((item) => item.p75))), 114 + p90: formatMilliseconds(avg(chartData.map((item) => item.p90))), 115 + p95: formatMilliseconds(avg(chartData.map((item) => item.p95))), 116 + p99: formatMilliseconds(avg(chartData.map((item) => item.p99))), 117 + }} 118 + className={cn("overflow-x-scroll", legendClassName)} 119 + /> 120 + } 121 + /> 122 + <CartesianGrid vertical={false} /> 123 + <XAxis dataKey="timestamp" hide={xAxisHide} /> 124 + <ChartTooltip 125 + cursor={false} 126 + content={ 127 + <ChartTooltipContent 128 + className="w-[180px]" 129 + formatter={(value, name) => ( 130 + <ChartTooltipNumber 131 + chartConfig={chartConfig} 132 + value={value} 133 + name={name} 134 + /> 135 + )} 136 + /> 137 + } 138 + /> 139 + <defs> 140 + <linearGradient id="fillP50" x1="0" y1="0" x2="0" y2="1"> 141 + <stop offset="5%" stopColor="var(--color-p50)" stopOpacity={0.8} /> 142 + <stop offset="95%" stopColor="var(--color-p50)" stopOpacity={0.1} /> 143 + </linearGradient> 144 + <linearGradient id="fillP75" x1="0" y1="0" x2="0" y2="1"> 145 + <stop offset="5%" stopColor="var(--color-p75)" stopOpacity={0.8} /> 146 + <stop offset="95%" stopColor="var(--color-p75)" stopOpacity={0.1} /> 147 + </linearGradient> 148 + <linearGradient id="fillP90" x1="0" y1="0" x2="0" y2="1"> 149 + <stop offset="5%" stopColor="var(--color-p90)" stopOpacity={0.8} /> 150 + <stop offset="95%" stopColor="var(--color-p90)" stopOpacity={0.1} /> 151 + </linearGradient> 152 + <linearGradient id="fillP95" x1="0" y1="0" x2="0" y2="1"> 153 + <stop offset="5%" stopColor="var(--color-p95)" stopOpacity={0.8} /> 154 + <stop offset="95%" stopColor="var(--color-p95)" stopOpacity={0.1} /> 155 + </linearGradient> 156 + <linearGradient id="fillP99" x1="0" y1="0" x2="0" y2="1"> 157 + <stop offset="5%" stopColor="var(--color-p99)" stopOpacity={0.8} /> 158 + <stop offset="95%" stopColor="var(--color-p99)" stopOpacity={0.1} /> 159 + </linearGradient> 160 + </defs> 161 + {withError ? ( 162 + <Area 163 + dataKey="error" 164 + type="monotone" 165 + stroke="var(--color-error)" 166 + strokeWidth={1} 167 + fill="var(--color-error)" 168 + fillOpacity={0.5} 169 + legendType="none" 170 + tooltipType="none" 171 + yAxisId="error" 172 + dot={false} 173 + activeDot={false} 174 + /> 175 + ) : null} 176 + <Area 177 + hide={!activeSeries.includes("p50")} 178 + dataKey="p50" 179 + type="monotone" 180 + stroke="var(--color-p50)" 181 + fill="url(#fillP50)" 182 + fillOpacity={0.4} 183 + dot={false} 184 + yAxisId="percentile" 185 + /> 186 + <Area 187 + hide={!activeSeries.includes("p75")} 188 + dataKey="p75" 189 + type="monotone" 190 + stroke="var(--color-p75)" 191 + fill="url(#fillP75)" 192 + fillOpacity={0.4} 193 + dot={false} 194 + yAxisId="percentile" 195 + /> 196 + {/* <Area 197 + hide={!activeSeries.includes("p90")} 198 + dataKey="p90" 199 + type="monotone" 200 + stroke="var(--color-p90)" 201 + fill="url(#fillP90)" 202 + fillOpacity={0.4} 203 + dot={false} 204 + yAxisId="percentile" 205 + /> */} 206 + <Area 207 + hide={!activeSeries.includes("p95")} 208 + dataKey="p95" 209 + type="monotone" 210 + stroke="var(--color-p95)" 211 + fill="url(#fillP95)" 212 + fillOpacity={0.4} 213 + dot={false} 214 + yAxisId="percentile" 215 + /> 216 + <Area 217 + hide={!activeSeries.includes("p99")} 218 + dataKey="p99" 219 + type="monotone" 220 + stroke="var(--color-p99)" 221 + fill="url(#fillP99)" 222 + fillOpacity={0.4} 223 + dot={false} 224 + yAxisId="percentile" 225 + /> 226 + <YAxis 227 + domain={yAxisDomain} 228 + tickLine={false} 229 + axisLine={false} 230 + tickMargin={8} 231 + orientation="right" 232 + yAxisId="percentile" 233 + tickFormatter={(value) => `${value}ms`} 234 + /> 235 + <YAxis orientation="left" yAxisId="error" hide /> 236 + </AreaChart> 237 + </ChartContainer> 238 + ); 239 + }
+88
apps/status-page/src/components/chart/chart-bar-uptime-light.tsx
··· 1 + "use client"; 2 + 3 + import { Skeleton } from "@/components/ui/skeleton"; 4 + import { Bar, BarChart, XAxis } from "recharts"; 5 + 6 + import { 7 + type ChartConfig, 8 + ChartContainer, 9 + ChartTooltip, 10 + ChartTooltipContent, 11 + } from "@/components/ui/chart"; 12 + import { mapUptime } from "@/data/metrics.client"; 13 + import { useTRPC } from "@/lib/trpc/client"; 14 + import type { Region } from "@openstatus/db/src/schema/constants"; 15 + import { useQuery } from "@tanstack/react-query"; 16 + // import { startOfDay, subDays } from "date-fns"; 17 + 18 + const chartConfig = { 19 + ok: { 20 + label: "Success", 21 + color: "var(--color-success)", 22 + }, 23 + degraded: { 24 + label: "Degraded", 25 + color: "var(--color-warning)", 26 + }, 27 + error: { 28 + label: "Error", 29 + color: "var(--color-destructive)", 30 + }, 31 + } satisfies ChartConfig; 32 + 33 + export function ChartBarUptimeLight({ 34 + monitorId, 35 + type, 36 + regions, 37 + }: { 38 + monitorId: string; 39 + type: "http" | "tcp"; 40 + regions?: Region[]; 41 + }) { 42 + const trpc = useTRPC(); 43 + 44 + const { data: uptime, isLoading } = useQuery( 45 + trpc.tinybird.uptime.queryOptions({ 46 + interval: 60 * 24, 47 + // fromDate: startOfDay(subDays(new Date(), 7)).toISOString(), // FIXME: 48 + period: "7d", 49 + monitorId, 50 + regions, 51 + type, 52 + }), 53 + ); 54 + 55 + if (isLoading) { 56 + return <Skeleton className=" my-auto h-5 w-full" />; 57 + } 58 + 59 + const refinedUptime = uptime ? mapUptime(uptime) : []; 60 + 61 + if (refinedUptime.length === 0) { 62 + return <span className="text-muted-foreground">-</span>; 63 + } 64 + 65 + return ( 66 + <ChartContainer config={chartConfig} className="h-[28px] w-full"> 67 + <BarChart accessibilityLayer data={refinedUptime} barCategoryGap={1}> 68 + <ChartTooltip 69 + cursor={false} 70 + allowEscapeViewBox={{ x: false, y: true }} 71 + wrapperStyle={{ zIndex: 1 }} 72 + content={<ChartTooltipContent indicator="dot" />} 73 + /> 74 + <Bar dataKey="ok" stackId="a" fill="var(--color-ok)" /> 75 + <Bar dataKey="error" stackId="a" fill="var(--color-error)" /> 76 + <Bar dataKey="degraded" stackId="a" fill="var(--color-degraded)" /> 77 + <XAxis 78 + dataKey="interval" 79 + tickLine={false} 80 + tickMargin={8} 81 + minTickGap={10} 82 + axisLine={false} 83 + hide 84 + /> 85 + </BarChart> 86 + </ChartContainer> 87 + ); 88 + }
+110
apps/status-page/src/components/chart/chart-bar-uptime.tsx
··· 1 + "use client"; 2 + 3 + import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts"; 4 + 5 + import { 6 + type ChartConfig, 7 + ChartContainer, 8 + ChartLegend, 9 + ChartLegendContent, 10 + ChartTooltip, 11 + ChartTooltipContent, 12 + } from "@/components/ui/chart"; 13 + import { type PERIODS, mapUptime } from "@/data/metrics.client"; 14 + import { useIsMobile } from "@/hooks/use-mobile"; 15 + import { useTRPC } from "@/lib/trpc/client"; 16 + import type { Region } from "@openstatus/db/src/schema/constants"; 17 + import { useQuery } from "@tanstack/react-query"; 18 + import { endOfDay, startOfDay, subDays } from "date-fns"; 19 + 20 + const chartConfig = { 21 + ok: { 22 + label: "Success", 23 + color: "var(--color-success)", 24 + }, 25 + degraded: { 26 + label: "Degraded", 27 + color: "var(--color-warning)", 28 + }, 29 + error: { 30 + label: "Error", 31 + color: "var(--color-destructive)", 32 + }, 33 + } satisfies ChartConfig; 34 + 35 + const periodToInterval = { 36 + "1d": 60, 37 + "7d": 240, 38 + "14d": 480, 39 + } satisfies Record<(typeof PERIODS)[number], number>; 40 + 41 + const periodToFromDate = { 42 + "1d": startOfDay(subDays(new Date(), 1)), 43 + "7d": startOfDay(subDays(new Date(), 7)), 44 + "14d": startOfDay(subDays(new Date(), 14)), 45 + } satisfies Record<(typeof PERIODS)[number], Date>; 46 + 47 + export function ChartBarUptime({ 48 + monitorId, 49 + period, 50 + type, 51 + regions, 52 + }: { 53 + monitorId: string; 54 + period: (typeof PERIODS)[number]; 55 + type: "http" | "tcp"; 56 + regions: Region[]; 57 + }) { 58 + const isMobile = useIsMobile(); 59 + const trpc = useTRPC(); 60 + const fromDate = periodToFromDate[period]; 61 + const toDate = endOfDay(new Date()); 62 + const interval = periodToInterval[period]; 63 + 64 + const { data: uptime } = useQuery( 65 + trpc.tinybird.uptime.queryOptions({ 66 + monitorId, 67 + fromDate: fromDate.toISOString(), 68 + toDate: toDate.toISOString(), 69 + regions, 70 + interval, 71 + type, 72 + }), 73 + ); 74 + 75 + const refinedUptime = uptime ? mapUptime(uptime) : []; 76 + 77 + return ( 78 + <ChartContainer config={chartConfig} className="h-[130px] w-full"> 79 + <BarChart 80 + accessibilityLayer 81 + data={refinedUptime} 82 + barCategoryGap={isMobile ? 0 : 2} 83 + > 84 + <CartesianGrid vertical={false} /> 85 + <ChartTooltip 86 + cursor={false} 87 + content={<ChartTooltipContent indicator="dot" />} 88 + /> 89 + <Bar dataKey="ok" stackId="a" fill="var(--color-ok)" /> 90 + <Bar dataKey="error" stackId="a" fill="var(--color-error)" /> 91 + <Bar dataKey="degraded" stackId="a" fill="var(--color-degraded)" /> 92 + <YAxis 93 + domain={["dataMin", "dataMax"]} 94 + tickLine={false} 95 + axisLine={false} 96 + tickMargin={8} 97 + orientation="right" 98 + /> 99 + <XAxis 100 + dataKey="interval" 101 + tickLine={false} 102 + tickMargin={8} 103 + minTickGap={10} 104 + axisLine={false} 105 + /> 106 + <ChartLegend content={<ChartLegendContent />} /> 107 + </BarChart> 108 + </ChartContainer> 109 + ); 110 + }
+127
apps/status-page/src/components/chart/chart-legend-badge.tsx
··· 1 + import { badgeVariants } from "@/components/ui/badge"; 2 + import { getPayloadConfigFromPayload, useChart } from "@/components/ui/chart"; 3 + import { 4 + Tooltip, 5 + TooltipContent, 6 + TooltipProvider, 7 + TooltipTrigger, 8 + } from "@/components/ui/tooltip"; 9 + import { cn } from "@/lib/utils"; 10 + import type * as RechartsPrimitive from "recharts"; 11 + import type { Payload } from "recharts/types/component/DefaultLegendContent"; 12 + 13 + export function ChartLegendBadge({ 14 + className, 15 + hideIcon = false, 16 + payload, 17 + verticalAlign = "bottom", 18 + nameKey, 19 + handleActive, 20 + active, 21 + maxActive, 22 + annotation, 23 + tooltip, 24 + }: React.ComponentProps<"div"> & 25 + Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & { 26 + hideIcon?: boolean; 27 + nameKey?: string; 28 + // NOTE: additional props compared to default shadcn/ui Legend component 29 + handleActive?: (item: Payload) => void; 30 + active?: Payload["dataKey"][]; 31 + maxActive?: number; 32 + annotation?: Record<string, string | number | undefined>; 33 + tooltip?: Record<string, string | undefined>; 34 + }) { 35 + const { config } = useChart(); 36 + 37 + if (!payload?.length) { 38 + return null; 39 + } 40 + 41 + const hasMaxActive = active && maxActive ? active.length >= maxActive : false; 42 + 43 + return ( 44 + <div 45 + className={cn( 46 + "flex items-center justify-center gap-2", 47 + verticalAlign === "top" ? "pb-3" : "pt-3", 48 + className, 49 + )} 50 + > 51 + {payload 52 + // NOTE: recharts supports "none" type for legend items 53 + .filter((item) => item.type !== "none") 54 + .map((item) => { 55 + const key = `${nameKey || item.dataKey || "value"}`; 56 + const itemConfig = getPayloadConfigFromPayload(config, item, key); 57 + const suffix = annotation?.[item.dataKey as string]; 58 + const tooltipLabel = tooltip?.[item.dataKey as string]; 59 + const isActive = active?.includes(item.dataKey); 60 + 61 + const badge = ( 62 + <button 63 + key={item.value} 64 + type="button" 65 + className={cn( 66 + badgeVariants({ variant: "outline" }), 67 + "outline-none", 68 + "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground", 69 + !isActive && "opacity-60", 70 + !isActive && hasMaxActive && "cursor-not-allowed opacity-40", 71 + )} 72 + onClick={(e) => { 73 + e.stopPropagation(); 74 + e.preventDefault(); 75 + handleActive?.(item); 76 + }} 77 + disabled={!isActive && hasMaxActive} 78 + > 79 + {itemConfig?.icon && !hideIcon ? ( 80 + <itemConfig.icon /> 81 + ) : ( 82 + <div 83 + className="h-2 w-2 shrink-0 rounded-[2px]" 84 + style={{ 85 + backgroundColor: item.color, 86 + }} 87 + /> 88 + )} 89 + {itemConfig?.label} 90 + {suffix ? ( 91 + <span className="font-mono text-[10px] text-muted-foreground"> 92 + {suffix} 93 + </span> 94 + ) : null} 95 + </button> 96 + ); 97 + 98 + if (tooltipLabel) { 99 + return ( 100 + <ChartLegendTooltip key={item.value} tooltip={tooltipLabel}> 101 + {badge} 102 + </ChartLegendTooltip> 103 + ); 104 + } 105 + 106 + return badge; 107 + })} 108 + </div> 109 + ); 110 + } 111 + 112 + function ChartLegendTooltip({ 113 + children, 114 + tooltip, 115 + ...props 116 + }: React.ComponentProps<typeof TooltipTrigger> & { tooltip: string }) { 117 + return ( 118 + <TooltipProvider> 119 + <Tooltip delayDuration={0}> 120 + <TooltipTrigger className="rounded-md" asChild {...props}> 121 + {children} 122 + </TooltipTrigger> 123 + <TooltipContent>{tooltip}</TooltipContent> 124 + </Tooltip> 125 + </TooltipProvider> 126 + ); 127 + }
+93
apps/status-page/src/components/chart/chart-line-region.tsx
··· 1 + "use client"; 2 + 3 + import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"; 4 + 5 + import { 6 + type ChartConfig, 7 + ChartContainer, 8 + ChartTooltip, 9 + ChartTooltipContent, 10 + } from "@/components/ui/chart"; 11 + import { cn } from "@/lib/utils"; 12 + import { ChartTooltipNumber } from "./chart-tooltip-number"; 13 + 14 + const chartConfig = { 15 + latency: { 16 + label: "Latency", 17 + color: "var(--success)", 18 + }, 19 + } satisfies ChartConfig; 20 + 21 + export type TrendPoint = { 22 + timestamp: number; // unix millis 23 + latency: number; // milliseconds 24 + }; 25 + 26 + export function ChartLineRegion({ 27 + className, 28 + data, 29 + }: { 30 + className?: string; 31 + data: TrendPoint[]; 32 + }) { 33 + const trendData = data ?? []; 34 + 35 + const chartData = trendData.map((d) => ({ 36 + timestamp: new Date(d.timestamp).toLocaleString("default", { 37 + hour: "numeric", 38 + minute: "numeric", 39 + day: "numeric", 40 + month: "short", 41 + }), 42 + latency: d.latency, 43 + })); 44 + 45 + return ( 46 + <ChartContainer 47 + config={chartConfig} 48 + className={cn("h-[100px] w-full", className)} 49 + > 50 + <LineChart 51 + accessibilityLayer 52 + data={chartData} 53 + margin={{ 54 + left: 12, 55 + right: 12, 56 + }} 57 + > 58 + <CartesianGrid vertical={false} /> 59 + <XAxis dataKey="timestamp" hide /> 60 + <ChartTooltip 61 + cursor={false} 62 + content={ 63 + <ChartTooltipContent 64 + className="w-[180px]" 65 + formatter={(value, name) => ( 66 + <ChartTooltipNumber 67 + chartConfig={chartConfig} 68 + value={value} 69 + name={name} 70 + /> 71 + )} 72 + /> 73 + } 74 + /> 75 + <Line 76 + dataKey="latency" 77 + type="monotone" 78 + stroke="var(--color-latency)" 79 + strokeWidth={2} 80 + dot={false} 81 + /> 82 + <YAxis 83 + domain={["dataMin", "dataMax"]} 84 + tickLine={false} 85 + axisLine={false} 86 + tickMargin={8} 87 + orientation="right" 88 + tickFormatter={(value) => `${value}ms`} 89 + /> 90 + </LineChart> 91 + </ChartContainer> 92 + ); 93 + }
+208
apps/status-page/src/components/chart/chart-line-regions.tsx
··· 1 + "use client"; 2 + 3 + import { 4 + CartesianGrid, 5 + Line, 6 + LineChart, 7 + XAxis, 8 + // XAxis, 9 + YAxis, 10 + } from "recharts"; 11 + 12 + import { 13 + type ChartConfig, 14 + ChartContainer, 15 + ChartLegend, 16 + ChartTooltip, 17 + ChartTooltipContent, 18 + } from "@/components/ui/chart"; 19 + import { regions } from "@/data/regions"; 20 + import { formatMilliseconds } from "@/lib/formatter"; 21 + import { cn } from "@/lib/utils"; 22 + import { useState } from "react"; 23 + import { ChartLegendBadge } from "./chart-legend-badge"; 24 + import { ChartTooltipNumber } from "./chart-tooltip-number"; 25 + 26 + const r = regions.filter((r) => 27 + ["ams", "bog", "arn", "atl", "bom", "syd", "fra"].includes(r.code), 28 + ); 29 + 30 + const randomizer = Math.random() * 50; 31 + 32 + const chartData = Array.from({ length: 30 }, (_, i) => ({ 33 + timestamp: new Date( 34 + new Date().setMinutes(new Date().getMinutes() - i), 35 + ).toLocaleString("default", { 36 + hour: "numeric", 37 + minute: "numeric", 38 + }), 39 + ams: Math.floor(Math.random() * randomizer) * 100 * 0.75, 40 + bog: Math.floor(Math.random() * randomizer) * 100 * 0.75, 41 + arn: Math.floor(Math.random() * randomizer) * 100 * 0.75, 42 + atl: Math.floor(Math.random() * randomizer) * 100 * 0.75, 43 + bom: Math.floor(Math.random() * randomizer) * 100 * 0.75, 44 + syd: Math.floor(Math.random() * randomizer) * 100 * 0.75, 45 + fra: Math.floor(Math.random() * randomizer) * 100 * 0.75, 46 + })); 47 + 48 + const s = r.sort((a, b) => { 49 + const aAvg = avg( 50 + chartData.map((d) => { 51 + const value = d[a.code as keyof typeof d]; 52 + if (typeof value === "number") { 53 + return value; 54 + } 55 + return 0; 56 + }), 57 + ); 58 + const bAvg = avg( 59 + chartData.map((d) => { 60 + const value = d[b.code as keyof typeof d]; 61 + if (typeof value === "number") { 62 + return value; 63 + } 64 + return 0; 65 + }), 66 + ); 67 + return bAvg - aAvg; 68 + }); 69 + 70 + const chartConfig = s 71 + .map((item, index) => ({ 72 + code: item.code, 73 + label: item.code, 74 + color: `var(--rainbow-${index + 1})`, 75 + })) 76 + .reduce( 77 + (acc, item) => { 78 + acc[item.code] = item; 79 + return acc; 80 + }, 81 + {} as Record<string, { label: string; color: string }>, 82 + ) satisfies ChartConfig; 83 + 84 + function avg(values: number[]) { 85 + return Math.round( 86 + values.reduce((acc, curr) => acc + curr, 0) / values.length, 87 + ); 88 + } 89 + 90 + const annotation = r.reduce( 91 + (acc, item) => { 92 + acc[item.code] = formatMilliseconds( 93 + avg( 94 + chartData.map((d) => { 95 + const value = d[item.code as keyof typeof d]; 96 + if (typeof value === "number") { 97 + return value; 98 + } 99 + return 0; 100 + }), 101 + ), 102 + ); 103 + return acc; 104 + }, 105 + {} as Record<string, string>, 106 + ); 107 + 108 + const tooltip = r.reduce( 109 + (acc, item) => { 110 + acc[item.code] = item.location; 111 + return acc; 112 + }, 113 + {} as Record<string, string>, 114 + ); 115 + 116 + export function ChartLineRegions({ className }: { className?: string }) { 117 + const [activeSeries, setActiveSeries] = useState< 118 + Array<keyof typeof chartConfig> 119 + >([s[0].code, s[1].code]); 120 + return ( 121 + <ChartContainer 122 + config={chartConfig} 123 + className={cn("h-[100px] w-full", className)} 124 + > 125 + <LineChart 126 + accessibilityLayer 127 + data={chartData} 128 + margin={{ 129 + left: 0, 130 + right: 0, 131 + top: 2, 132 + bottom: 2, 133 + }} 134 + > 135 + <CartesianGrid vertical={false} /> 136 + <XAxis dataKey="timestamp" /> 137 + <ChartTooltip 138 + cursor={false} 139 + content={ 140 + <ChartTooltipContent 141 + formatter={(value, name) => ( 142 + <ChartTooltipNumber 143 + chartConfig={chartConfig} 144 + value={value} 145 + name={name} 146 + labelFormatter={(_, name) => { 147 + const region = regions.find((r) => r.code === name); 148 + return ( 149 + <> 150 + <span className="font-mono">{name}</span>{" "} 151 + <span className="text-muted-foreground text-xs"> 152 + {region?.location} 153 + </span> 154 + </> 155 + ); 156 + }} 157 + /> 158 + )} 159 + /> 160 + } 161 + /> 162 + {r.map((item) => ( 163 + <Line 164 + key={item.code} 165 + dataKey={item.code} 166 + type="monotone" 167 + stroke={`var(--color-${item.code})`} 168 + dot={false} 169 + hide={!activeSeries.includes(item.code)} 170 + /> 171 + ))} 172 + 173 + <YAxis 174 + domain={["dataMin", "dataMax"]} 175 + tickLine={false} 176 + axisLine={false} 177 + tickMargin={8} 178 + orientation="right" 179 + tickFormatter={(value) => `${value}ms`} 180 + /> 181 + <ChartLegend 182 + verticalAlign="top" 183 + content={ 184 + <ChartLegendBadge 185 + handleActive={(item) => { 186 + setActiveSeries((prev) => { 187 + if (item.dataKey) { 188 + const key = item.dataKey as keyof typeof chartConfig; 189 + if (prev.includes(key)) { 190 + return prev.filter((item) => item !== key); 191 + } 192 + return [...prev, key]; 193 + } 194 + return prev; 195 + }); 196 + }} 197 + active={activeSeries} 198 + maxActive={3} 199 + annotation={annotation} 200 + tooltip={tooltip} 201 + className="justify-start overflow-x-scroll ps-1 pt-1 font-mono" 202 + /> 203 + } 204 + /> 205 + </LineChart> 206 + </ChartContainer> 207 + ); 208 + }
+59
apps/status-page/src/components/chart/chart-tooltip-number.tsx
··· 1 + import type { ChartConfig } from "@/components/ui/chart"; 2 + import { cn } from "@/lib/utils"; 3 + import type { 4 + NameType, 5 + ValueType, 6 + } from "recharts/types/component/DefaultTooltipContent"; 7 + 8 + interface ChartTooltipNumberProps { 9 + chartConfig: ChartConfig; 10 + value: ValueType; 11 + name: NameType; 12 + } 13 + 14 + export function ChartTooltipNumber({ 15 + value, 16 + name, 17 + chartConfig, 18 + }: ChartTooltipNumberProps) { 19 + return ( 20 + <ChartTooltipNumberRaw 21 + value={value} 22 + label={chartConfig[name as keyof typeof chartConfig]?.label || name} 23 + style={ 24 + { 25 + "--color-bg": `var(--color-${name})`, 26 + } as React.CSSProperties 27 + } 28 + /> 29 + ); 30 + } 31 + 32 + export function ChartTooltipNumberRaw({ 33 + value, 34 + label, 35 + style, 36 + className, 37 + }: { 38 + value: ValueType; 39 + label: React.ReactNode; 40 + style?: React.CSSProperties; 41 + className?: string; 42 + }) { 43 + return ( 44 + <> 45 + <div 46 + className={cn( 47 + "h-2.5 w-2.5 shrink-0 rounded-[2px] bg-(--color-bg)", 48 + className, 49 + )} 50 + style={style} 51 + /> 52 + <span>{label}</span> 53 + <div className="ml-auto flex items-baseline gap-0.5 font-medium font-mono text-foreground tabular-nums"> 54 + {value} 55 + <span className="font-normal text-muted-foreground">ms</span> 56 + </div> 57 + </> 58 + ); 59 + }
+19
apps/status-page/src/components/common/kbd.tsx
··· 1 + import { cn } from "@/lib/utils"; 2 + 3 + export function Kbd({ 4 + children, 5 + className, 6 + ...props 7 + }: React.ComponentProps<"kbd">) { 8 + return ( 9 + <kbd 10 + className={cn( 11 + "-me-1 ms-2 inline-flex h-5 max-h-full items-center rounded border bg-background px-1 font-[inherit] font-medium text-[0.625rem] text-muted-foreground/70", 12 + className, 13 + )} 14 + {...props} 15 + > 16 + {children} 17 + </kbd> 18 + ); 19 + }
+19
apps/status-page/src/components/common/link.tsx
··· 1 + import { cn } from "@/lib/utils"; 2 + import NextLink from "next/link"; 3 + 4 + // TODO: we could add cva variants for the link 5 + 6 + export function Link({ 7 + children, 8 + className, 9 + ...props 10 + }: React.ComponentProps<typeof NextLink>) { 11 + return ( 12 + <NextLink 13 + className={cn("font-medium text-foreground", className)} 14 + {...props} 15 + > 16 + {children} 17 + </NextLink> 18 + ); 19 + }
+46
apps/status-page/src/components/content/empty-state.tsx
··· 1 + import { cn } from "@/lib/utils"; 2 + 3 + export function EmptyStateContainer({ 4 + children, 5 + className, 6 + ...props 7 + }: React.ComponentProps<"div">) { 8 + return ( 9 + <div 10 + className={cn( 11 + "flex h-full flex-col items-center justify-center gap-2 rounded-lg border border-border border-dashed p-4", 12 + className, 13 + )} 14 + {...props} 15 + > 16 + {children} 17 + </div> 18 + ); 19 + } 20 + 21 + export function EmptyStateTitle({ 22 + children, 23 + className, 24 + ...props 25 + }: React.ComponentProps<"p">) { 26 + return ( 27 + <p className={cn("text-foreground", className)} {...props}> 28 + {children} 29 + </p> 30 + ); 31 + } 32 + 33 + export function EmptyStateDescription({ 34 + children, 35 + className, 36 + ...props 37 + }: React.ComponentProps<"p">) { 38 + return ( 39 + <p 40 + className={cn("text-center text-muted-foreground text-sm", className)} 41 + {...props} 42 + > 43 + {children} 44 + </p> 45 + ); 46 + }
+197
apps/status-page/src/components/content/metric-card.tsx
··· 1 + import type { VariantProps } from "class-variance-authority"; 2 + import { cva } from "class-variance-authority"; 3 + import { ChevronDown, ChevronUp } from "lucide-react"; 4 + 5 + import { Badge } from "@/components/ui/badge"; 6 + import { Skeleton } from "@/components/ui/skeleton"; 7 + 8 + import { cn } from "@/lib/utils"; 9 + import type React from "react"; 10 + 11 + const metricCardVariants = cva( 12 + "flex flex-col gap-1 border rounded-lg px-3 py-2 text-card-foreground", 13 + { 14 + variants: { 15 + variant: { 16 + default: "border-input bg-card", 17 + ghost: "border-transparent", 18 + destructive: "border-destructive/80 bg-destructive/10", 19 + success: "border-success/80 bg-success/10", 20 + warning: "border-warning/80 bg-warning/10", 21 + }, 22 + }, 23 + defaultVariants: { 24 + variant: "default", 25 + }, 26 + }, 27 + ); 28 + 29 + export function MetricCard({ 30 + children, 31 + className, 32 + variant, 33 + ...props 34 + }: React.ComponentProps<"div"> & VariantProps<typeof metricCardVariants>) { 35 + return ( 36 + <div 37 + data-variant={variant} 38 + className={cn(metricCardVariants({ variant, className }), "group")} 39 + {...props} 40 + > 41 + {children} 42 + </div> 43 + ); 44 + } 45 + 46 + export function MetricCardTitle({ 47 + children, 48 + className, 49 + ...props 50 + }: React.ComponentProps<"p">) { 51 + return ( 52 + <p className={cn("font-medium text-sm", className)} {...props}> 53 + {children} 54 + </p> 55 + ); 56 + } 57 + 58 + export function MetricCardHeader({ 59 + children, 60 + className, 61 + ...props 62 + }: React.ComponentProps<"div">) { 63 + return ( 64 + <div 65 + className={cn( 66 + "text-muted-foreground", 67 + "group-data-[variant=destructive]:text-destructive", 68 + "group-data-[variant=success]:text-success", 69 + "group-data-[variant=warning]:text-warning", 70 + className, 71 + )} 72 + {...props} 73 + > 74 + {children} 75 + </div> 76 + ); 77 + } 78 + 79 + export function MetricCardValue({ 80 + children, 81 + className, 82 + ...props 83 + }: React.ComponentProps<"p">) { 84 + return ( 85 + <p className={cn("font-semibold text-foreground", className)} {...props}> 86 + {children} 87 + </p> 88 + ); 89 + } 90 + 91 + export function MetricCardGroup({ 92 + children, 93 + className, 94 + ...props 95 + }: React.ComponentProps<"div">) { 96 + return ( 97 + <div 98 + className={cn( 99 + "grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5", 100 + className, 101 + )} 102 + {...props} 103 + > 104 + {children} 105 + </div> 106 + ); 107 + } 108 + 109 + const badgeVariants = cva("px-1.5 font-mono text-[10px]", { 110 + variants: { 111 + variant: { 112 + default: "border-border", 113 + increase: 114 + "border-destructive/20 bg-destructive/10 hover:bg-destructive/10 text-destructive", 115 + decrease: 116 + "border-success/20 bg-success/10 hover:bg-success/10 text-success", 117 + }, 118 + }, 119 + defaultVariants: { 120 + variant: "default", 121 + }, 122 + }); 123 + 124 + export function MetricCardBadge({ 125 + value, 126 + decimal = 1, 127 + className, 128 + ...props 129 + }: React.ComponentProps<typeof Badge> & { 130 + value: number; 131 + decimal?: number; 132 + }) { 133 + const round = 10 ** decimal; // 10^1 = 10 (1 decimal), 10^2 = 100 (2 decimals), etc. 134 + const percentage = Math.round((value - 1) * 100 * round) / round; 135 + 136 + const variant: VariantProps<typeof badgeVariants>["variant"] = 137 + percentage > 0 ? "increase" : percentage < 0 ? "decrease" : "default"; 138 + 139 + return ( 140 + <Badge 141 + variant="secondary" 142 + className={badgeVariants({ variant, className })} 143 + {...props} 144 + > 145 + {percentage !== 0 ? ( 146 + <span> 147 + {percentage > 0 ? <ChevronUp className="mr-px size-2.5" /> : null} 148 + {percentage < 0 ? <ChevronDown className="mr-px size-2.5" /> : null} 149 + </span> 150 + ) : null} 151 + {Math.abs(percentage)}% 152 + </Badge> 153 + ); 154 + } 155 + 156 + const metricCardButtonVariants = cva( 157 + "group w-full text-left transition-all rounded-md outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 cursor-pointer", 158 + // TODO: discuss if we want rings 159 + ); 160 + 161 + export function MetricCardButton({ 162 + children, 163 + className, 164 + variant, 165 + ...props 166 + }: React.ComponentProps<"button"> & VariantProps<typeof metricCardVariants>) { 167 + return ( 168 + <button 169 + type="button" 170 + data-variant={variant} 171 + className={cn( 172 + metricCardVariants({ variant, className }), 173 + metricCardButtonVariants(), 174 + )} 175 + {...props} 176 + > 177 + {children} 178 + </button> 179 + ); 180 + } 181 + 182 + export function MetricCardSkeleton({ 183 + className, 184 + ...props 185 + }: React.ComponentProps<typeof Skeleton>) { 186 + return ( 187 + <Skeleton 188 + className={cn( 189 + "group-data-[variant=destructive]:bg-destructive/50", 190 + "group-data-[variant=success]:bg-success/50", 191 + "group-data-[variant=warning]:bg-warning/50", 192 + className, 193 + )} 194 + {...props} 195 + /> 196 + ); 197 + }
+34
apps/status-page/src/components/content/process-message.tsx
··· 1 + import type { AnchorHTMLAttributes } from "react"; 2 + import { Fragment, createElement } from "react"; 3 + import { jsx, jsxs } from "react/jsx-runtime"; 4 + import rehypeReact from "rehype-react"; 5 + import remarkParse from "remark-parse"; 6 + import remarkRehype from "remark-rehype"; 7 + import { unified } from "unified"; 8 + 9 + export function ProcessMessage({ value }: { value: string }) { 10 + const result = unified() 11 + .use(remarkParse) 12 + .use(remarkRehype) 13 + .use(rehypeReact, { 14 + createElement, 15 + Fragment, 16 + jsx, 17 + jsxs, 18 + components: { 19 + a: (props: AnchorHTMLAttributes<HTMLAnchorElement>) => { 20 + return ( 21 + <a 22 + target="_blank" 23 + rel="noreferrer" 24 + className="underline" 25 + {...props} 26 + /> 27 + ); 28 + }, 29 + } as { [key: string]: React.ComponentType<unknown> }, 30 + }) 31 + .processSync(value).result; 32 + 33 + return result; 34 + }
+106
apps/status-page/src/components/content/section.tsx
··· 1 + import { cn } from "@/lib/utils"; 2 + 3 + export function Section({ 4 + children, 5 + className, 6 + ...props 7 + }: React.ComponentProps<"section">) { 8 + return ( 9 + <section className={cn("space-y-4", className)} {...props}> 10 + {children} 11 + </section> 12 + ); 13 + } 14 + 15 + export function SectionHeader({ 16 + children, 17 + className, 18 + ...props 19 + }: React.ComponentProps<"div">) { 20 + return ( 21 + <div className={cn("flex flex-col gap-1.5", className)} {...props}> 22 + {children} 23 + </div> 24 + ); 25 + } 26 + 27 + export function SectionHeaderRow({ 28 + children, 29 + className, 30 + ...props 31 + }: React.ComponentProps<"div">) { 32 + return ( 33 + <div 34 + className={cn( 35 + "flex flex-col gap-1.5 sm:flex-row sm:items-end sm:justify-between", 36 + className, 37 + )} 38 + {...props} 39 + > 40 + {children} 41 + </div> 42 + ); 43 + } 44 + 45 + export function SectionDescription({ 46 + children, 47 + className, 48 + ...props 49 + }: React.ComponentProps<"p">) { 50 + return ( 51 + <p className={cn("text-muted-foreground text-sm", className)} {...props}> 52 + {children} 53 + </p> 54 + ); 55 + } 56 + 57 + export function SectionTitle({ 58 + children, 59 + className, 60 + ...props 61 + }: React.ComponentProps<"p">) { 62 + return ( 63 + <p className={cn("font-medium text-lg", className)} {...props}> 64 + {children} 65 + </p> 66 + ); 67 + } 68 + 69 + export function SectionGroup({ 70 + children, 71 + className, 72 + ...props 73 + }: React.ComponentProps<"div">) { 74 + return ( 75 + <div 76 + className={cn("mx-auto w-full max-w-4xl space-y-8 px-4 py-8", className)} 77 + {...props} 78 + > 79 + {children} 80 + </div> 81 + ); 82 + } 83 + 84 + export function SectionGroupHeader({ 85 + children, 86 + className, 87 + ...props 88 + }: React.ComponentProps<"div">) { 89 + return ( 90 + <div className={cn("space-y-1.5", className)} {...props}> 91 + {children} 92 + </div> 93 + ); 94 + } 95 + 96 + export function SectionGroupTitle({ 97 + children, 98 + className, 99 + ...props 100 + }: React.ComponentProps<"p">) { 101 + return ( 102 + <p className={cn("font-bold text-4xl", className)} {...props}> 103 + {children} 104 + </p> 105 + ); 106 + }
apps/status-page/src/components/controls-filter/.gitkeep

This is a binary file and will not be displayed.

+45
apps/status-page/src/components/controls-search/button-reset.tsx
··· 1 + "use client"; 2 + 3 + import { Button } from "@/components/ui/button"; 4 + import { X } from "lucide-react"; 5 + import { useRouter, useSearchParams } from "next/navigation"; 6 + 7 + export function ButtonReset({ only }: { only?: string[] }) { 8 + const searchParams = useSearchParams(); 9 + const router = useRouter(); 10 + 11 + // Determine if at least one parameter that should be reset is present (or any parameter if `only` is undefined) 12 + const hasParamsToReset = only 13 + ? only.some((key) => searchParams.has(key)) 14 + : !!searchParams.toString(); 15 + 16 + if (!hasParamsToReset) return null; 17 + 18 + const handleClick = () => { 19 + // Clone the current search params so we can mutate them 20 + const params = new URLSearchParams(searchParams.toString()); 21 + 22 + if (only && only.length > 0) { 23 + // Remove only the specified keys 24 + only.forEach((key) => params.delete(key)); 25 + const query = params.toString(); 26 + router.push( 27 + query 28 + ? `${window.location.pathname}?${query}` 29 + : window.location.pathname, 30 + ); 31 + } else { 32 + // No `only` prop provided – remove all query parameters 33 + router.push(window.location.pathname); 34 + } 35 + }; 36 + 37 + if (!hasParamsToReset) return null; 38 + 39 + return ( 40 + <Button variant="ghost" size="sm" onClick={handleClick}> 41 + <X /> 42 + Reset 43 + </Button> 44 + ); 45 + }
+163
apps/status-page/src/components/controls-search/command-region.tsx
··· 1 + "use client"; 2 + 3 + import { Link } from "@/components/common/link"; 4 + import { 5 + BillingOverlay, 6 + BillingOverlayButton, 7 + BillingOverlayDescription, 8 + } from "@/components/content/billing-overlay"; 9 + import { Button } from "@/components/ui/button"; 10 + import { 11 + Command, 12 + CommandEmpty, 13 + CommandGroup, 14 + CommandInput, 15 + CommandItem, 16 + CommandList, 17 + CommandSeparator, 18 + } from "@/components/ui/command"; 19 + import { 20 + Popover, 21 + PopoverContent, 22 + PopoverTrigger, 23 + } from "@/components/ui/popover"; 24 + import { REGIONS } from "@/data/metrics.client"; 25 + import { useTRPC } from "@/lib/trpc/client"; 26 + import { cn } from "@/lib/utils"; 27 + import { groupByContinent } from "@openstatus/utils"; 28 + import { useQuery } from "@tanstack/react-query"; 29 + import { Check, Lock } from "lucide-react"; 30 + import { parseAsArrayOf, parseAsStringLiteral, useQueryState } from "nuqs"; 31 + 32 + export const parseRegions = (regions: (typeof REGIONS)[number][]) => 33 + parseAsArrayOf( 34 + parseAsStringLiteral(REGIONS.filter((region) => regions.includes(region))), 35 + ).withDefault(regions as unknown as (typeof REGIONS)[number][]); 36 + 37 + export function CommandRegion({ 38 + regions, 39 + }: { 40 + regions: (typeof REGIONS)[number][]; 41 + }) { 42 + const trpc = useTRPC(); 43 + const { data: workspace } = useQuery(trpc.workspace.get.queryOptions()); 44 + const [selectedRegions, setSelectedRegions] = useQueryState( 45 + "regions", 46 + parseRegions(regions), 47 + ); 48 + 49 + const limited = workspace?.plan === "free"; 50 + 51 + return ( 52 + <Popover> 53 + <PopoverTrigger asChild> 54 + <Button variant="outline" size="sm"> 55 + {selectedRegions.length === regions.length 56 + ? "All Regions" 57 + : `${selectedRegions.length} Regions`} 58 + </Button> 59 + </PopoverTrigger> 60 + <PopoverContent 61 + align="start" 62 + className="relative w-[200px] overflow-hidden p-0" 63 + > 64 + <Command> 65 + <CommandInput placeholder="Search region..." disabled={limited} /> 66 + <CommandList> 67 + <CommandGroup forceMount> 68 + <CommandItem 69 + onSelect={() => { 70 + const items = document.querySelectorAll( 71 + '[data-slot="command-item"][data-disabled="false"]', 72 + ); 73 + const codes: (typeof REGIONS)[number][] = []; 74 + 75 + items.forEach((item) => { 76 + const code = item.getAttribute("data-value"); 77 + if (code && code !== "select-all") { 78 + codes.push(code as (typeof REGIONS)[number]); 79 + } 80 + }); 81 + 82 + if (codes.length === selectedRegions.length) { 83 + setSelectedRegions([]); 84 + } else { 85 + setSelectedRegions(codes); 86 + } 87 + }} 88 + value="select-all" 89 + disabled={limited} 90 + > 91 + Toggle selection 92 + </CommandItem> 93 + </CommandGroup> 94 + <CommandSeparator alwaysRender /> 95 + {Object.entries(groupByContinent).map( 96 + ([continent, continentRegions]) => { 97 + const allowedRegions = continentRegions.filter((region) => 98 + regions.includes(region.code), 99 + ); 100 + 101 + if (allowedRegions.length === 0) { 102 + return null; 103 + } 104 + return ( 105 + <CommandGroup key={continent} heading={continent}> 106 + {allowedRegions.map((region) => ( 107 + <CommandItem 108 + disabled={limited} 109 + key={region.code} 110 + value={region.code} 111 + keywords={[ 112 + region.code, 113 + region.location, 114 + region.continent, 115 + region.flag, 116 + ]} 117 + onSelect={() => { 118 + setSelectedRegions((prev) => 119 + prev.includes(region.code) 120 + ? prev.filter((r) => r !== region.code) 121 + : [...prev, region.code], 122 + ); 123 + }} 124 + > 125 + <span className="mr-1">{region.flag}</span> 126 + {region.code} 127 + <span className="ml-1 truncate text-muted-foreground text-xs"> 128 + {region.location} 129 + </span> 130 + <Check 131 + className={cn( 132 + "ml-auto", 133 + selectedRegions.includes(region.code) 134 + ? "opacity-100" 135 + : "opacity-0", 136 + )} 137 + /> 138 + </CommandItem> 139 + ))} 140 + </CommandGroup> 141 + ); 142 + }, 143 + )} 144 + <CommandEmpty>No region found.</CommandEmpty> 145 + </CommandList> 146 + </Command> 147 + {limited ? ( 148 + <BillingOverlay className="to-70%"> 149 + <BillingOverlayButton asChild> 150 + <Link href="/settings/billing"> 151 + <Lock /> 152 + Upgrade 153 + </Link> 154 + </BillingOverlayButton> 155 + <BillingOverlayDescription> 156 + Filter by region is only available on paid plans. 157 + </BillingOverlayDescription> 158 + </BillingOverlay> 159 + ) : null} 160 + </PopoverContent> 161 + </Popover> 162 + ); 163 + }
+87
apps/status-page/src/components/controls-search/command-tags.tsx
··· 1 + "use client"; 2 + 3 + import { Button } from "@/components/ui/button"; 4 + import { 5 + Command, 6 + CommandEmpty, 7 + CommandGroup, 8 + CommandInput, 9 + CommandItem, 10 + CommandList, 11 + } from "@/components/ui/command"; 12 + import { 13 + Popover, 14 + PopoverContent, 15 + PopoverTrigger, 16 + } from "@/components/ui/popover"; 17 + import { useTRPC } from "@/lib/trpc/client"; 18 + import { cn } from "@/lib/utils"; 19 + import { useQuery } from "@tanstack/react-query"; 20 + import { Check } from "lucide-react"; 21 + import { parseAsArrayOf, parseAsString, useQueryState } from "nuqs"; 22 + 23 + export function CommandTags() { 24 + const trpc = useTRPC(); 25 + const { data: tags } = useQuery(trpc.monitorTag.list.queryOptions()); 26 + const [selectedTags, setSelectedTags] = useQueryState( 27 + "tags", 28 + parseAsArrayOf(parseAsString).withDefault([]).withOptions({ 29 + shallow: false, 30 + }), 31 + ); 32 + 33 + return ( 34 + <Popover> 35 + <PopoverTrigger asChild> 36 + <Button variant="outline" size="sm"> 37 + {selectedTags.length === (tags?.length ?? 0) 38 + ? "All Tags" 39 + : `${selectedTags.length} Tags`} 40 + </Button> 41 + </PopoverTrigger> 42 + <PopoverContent 43 + align="start" 44 + className="relative w-[200px] overflow-hidden p-0" 45 + > 46 + <Command> 47 + <CommandInput placeholder="Search tag..." /> 48 + <CommandList> 49 + <CommandGroup> 50 + {tags?.map((tag) => ( 51 + <CommandItem 52 + key={tag.id} 53 + value={tag.name} 54 + keywords={[tag.name]} 55 + onSelect={() => { 56 + setSelectedTags((prev) => 57 + prev.includes(tag.name) 58 + ? prev.filter((r) => r !== tag.name) 59 + : [...prev, tag.name], 60 + ); 61 + }} 62 + > 63 + <div className="flex items-center gap-2"> 64 + <span 65 + className="size-2.5 rounded-full" 66 + style={{ backgroundColor: tag.color }} 67 + /> 68 + {tag.name} 69 + </div> 70 + <Check 71 + className={cn( 72 + "ml-auto", 73 + selectedTags.includes(tag.name) 74 + ? "opacity-100" 75 + : "opacity-0", 76 + )} 77 + /> 78 + </CommandItem> 79 + ))} 80 + </CommandGroup> 81 + <CommandEmpty>No tag found.</CommandEmpty> 82 + </CommandList> 83 + </Command> 84 + </PopoverContent> 85 + </Popover> 86 + ); 87 + }
+56
apps/status-page/src/components/controls-search/dropdown-interval.tsx
··· 1 + "use client"; 2 + 3 + import { Button } from "@/components/ui/button"; 4 + import { 5 + DropdownMenu, 6 + DropdownMenuContent, 7 + DropdownMenuGroup, 8 + DropdownMenuItem, 9 + DropdownMenuLabel, 10 + DropdownMenuTrigger, 11 + } from "@/components/ui/dropdown-menu"; 12 + import { INTERVALS } from "@/data/metrics.client"; 13 + import { Check } from "lucide-react"; 14 + import { parseAsNumberLiteral, useQueryState } from "nuqs"; 15 + 16 + const MAPPING = { 17 + 5: "5 minutes", 18 + 15: "15 minutes", 19 + 30: "30 minutes", 20 + 60: "1 hour", 21 + 120: "2 hours", 22 + 240: "4 hours", 23 + 480: "8 hours", 24 + 1440: "1 day", 25 + } as const; 26 + 27 + const parseInterval = parseAsNumberLiteral(INTERVALS).withDefault(30); 28 + 29 + export function DropdownInterval() { 30 + const [interval, setInterval] = useQueryState("interval", parseInterval); 31 + 32 + return ( 33 + <DropdownMenu> 34 + <DropdownMenuTrigger asChild> 35 + <Button variant="outline" size="sm"> 36 + {MAPPING[interval]} 37 + </Button> 38 + </DropdownMenuTrigger> 39 + <DropdownMenuContent align="start"> 40 + <DropdownMenuGroup> 41 + <DropdownMenuLabel className="font-medium text-muted-foreground text-xs"> 42 + Resolution 43 + </DropdownMenuLabel> 44 + {INTERVALS.map((item) => ( 45 + <DropdownMenuItem key={item} onSelect={() => setInterval(item)}> 46 + {MAPPING[item]} 47 + {interval === item ? ( 48 + <Check className="ml-auto shrink-0" /> 49 + ) : null} 50 + </DropdownMenuItem> 51 + ))} 52 + </DropdownMenuGroup> 53 + </DropdownMenuContent> 54 + </DropdownMenu> 55 + ); 56 + }
+53
apps/status-page/src/components/controls-search/dropdown-percentile.tsx
··· 1 + "use client"; 2 + 3 + import { Button } from "@/components/ui/button"; 4 + import { 5 + DropdownMenu, 6 + DropdownMenuContent, 7 + DropdownMenuGroup, 8 + DropdownMenuItem, 9 + DropdownMenuLabel, 10 + DropdownMenuTrigger, 11 + } from "@/components/ui/dropdown-menu"; 12 + import { PERCENTILES } from "@/data/metrics.client"; 13 + import { cn } from "@/lib/utils"; 14 + import { Check } from "lucide-react"; 15 + import { parseAsStringLiteral, useQueryState } from "nuqs"; 16 + 17 + const parsePercentile = parseAsStringLiteral(PERCENTILES).withDefault("p50"); 18 + 19 + export function DropdownPercentile() { 20 + const [percentile, setPercentile] = useQueryState( 21 + "percentile", 22 + parsePercentile, 23 + ); 24 + 25 + return ( 26 + <DropdownMenu> 27 + <DropdownMenuTrigger asChild> 28 + <Button variant="outline" size="sm" className="capitalize"> 29 + {percentile} 30 + </Button> 31 + </DropdownMenuTrigger> 32 + <DropdownMenuContent align="start"> 33 + <DropdownMenuGroup> 34 + <DropdownMenuLabel className="font-medium text-muted-foreground text-xs"> 35 + Percentile 36 + </DropdownMenuLabel> 37 + {PERCENTILES.map((item) => ( 38 + <DropdownMenuItem 39 + key={item} 40 + onSelect={() => setPercentile(item)} 41 + className={cn("capitalize")} 42 + > 43 + {item} 44 + {percentile === item ? ( 45 + <Check className="ml-auto shrink-0" /> 46 + ) : null} 47 + </DropdownMenuItem> 48 + ))} 49 + </DropdownMenuGroup> 50 + </DropdownMenuContent> 51 + </DropdownMenu> 52 + ); 53 + }
+59
apps/status-page/src/components/controls-search/dropdown-period.tsx
··· 1 + "use client"; 2 + 3 + import { Button } from "@/components/ui/button"; 4 + import { 5 + DropdownMenu, 6 + DropdownMenuContent, 7 + DropdownMenuGroup, 8 + DropdownMenuItem, 9 + DropdownMenuLabel, 10 + DropdownMenuTrigger, 11 + } from "@/components/ui/dropdown-menu"; 12 + import { PERIODS } from "@/data/metrics.client"; 13 + import { Check } from "lucide-react"; 14 + import { parseAsStringLiteral, useQueryState } from "nuqs"; 15 + 16 + // TODO: where to move it? 17 + export const PERIOD_VALUES = [ 18 + { 19 + value: "1d", 20 + label: "Last day", 21 + }, 22 + { 23 + value: "7d", 24 + label: "Last 7 days", 25 + }, 26 + { 27 + value: "14d", 28 + label: "Last 14 days", 29 + }, 30 + ] satisfies { value: (typeof PERIODS)[number]; label: string }[]; 31 + 32 + const parsePeriod = parseAsStringLiteral(PERIODS).withDefault("1d"); 33 + 34 + export function DropdownPeriod() { 35 + const [period, setPeriod] = useQueryState("period", parsePeriod); 36 + 37 + return ( 38 + <DropdownMenu> 39 + <DropdownMenuTrigger asChild> 40 + <Button variant="outline" size="sm"> 41 + {PERIOD_VALUES.find(({ value }) => value === period)?.label} 42 + </Button> 43 + </DropdownMenuTrigger> 44 + <DropdownMenuContent align="start"> 45 + <DropdownMenuGroup> 46 + <DropdownMenuLabel className="font-medium text-muted-foreground text-xs"> 47 + Period 48 + </DropdownMenuLabel> 49 + {PERIOD_VALUES.map(({ value, label }) => ( 50 + <DropdownMenuItem key={value} onSelect={() => setPeriod(value)}> 51 + {label} 52 + {period === value ? <Check className="ml-auto shrink-0" /> : null} 53 + </DropdownMenuItem> 54 + ))} 55 + </DropdownMenuGroup> 56 + </DropdownMenuContent> 57 + </DropdownMenu> 58 + ); 59 + }
+48
apps/status-page/src/components/controls-search/dropdown-status.tsx
··· 1 + "use client"; 2 + 3 + import { Button } from "@/components/ui/button"; 4 + import { 5 + DropdownMenu, 6 + DropdownMenuContent, 7 + DropdownMenuGroup, 8 + DropdownMenuItem, 9 + DropdownMenuLabel, 10 + DropdownMenuTrigger, 11 + } from "@/components/ui/dropdown-menu"; 12 + import { STATUS } from "@/data/metrics.client"; 13 + import { cn } from "@/lib/utils"; 14 + import { Check } from "lucide-react"; 15 + import { parseAsStringLiteral, useQueryState } from "nuqs"; 16 + 17 + const parseStatus = parseAsStringLiteral(STATUS); 18 + 19 + export function DropdownStatus() { 20 + const [status, setStatus] = useQueryState("status", parseStatus); 21 + 22 + return ( 23 + <DropdownMenu> 24 + <DropdownMenuTrigger asChild> 25 + <Button variant="outline" size="sm" className="capitalize"> 26 + {status ?? "All Status"} 27 + </Button> 28 + </DropdownMenuTrigger> 29 + <DropdownMenuContent align="start"> 30 + <DropdownMenuGroup> 31 + <DropdownMenuLabel className="font-medium text-muted-foreground text-xs"> 32 + Request Status 33 + </DropdownMenuLabel> 34 + {STATUS.map((item) => ( 35 + <DropdownMenuItem 36 + key={item} 37 + onSelect={() => setStatus(item)} 38 + className={cn("capitalize")} 39 + > 40 + {item} 41 + {status === item ? <Check className="ml-auto shrink-0" /> : null} 42 + </DropdownMenuItem> 43 + ))} 44 + </DropdownMenuGroup> 45 + </DropdownMenuContent> 46 + </DropdownMenu> 47 + ); 48 + }
+48
apps/status-page/src/components/controls-search/dropdown-trigger.tsx
··· 1 + "use client"; 2 + 3 + import { Button } from "@/components/ui/button"; 4 + import { 5 + DropdownMenu, 6 + DropdownMenuContent, 7 + DropdownMenuGroup, 8 + DropdownMenuItem, 9 + DropdownMenuLabel, 10 + DropdownMenuTrigger, 11 + } from "@/components/ui/dropdown-menu"; 12 + import { TRIGGER } from "@/data/metrics.client"; 13 + import { cn } from "@/lib/utils"; 14 + import { Check } from "lucide-react"; 15 + import { parseAsStringLiteral, useQueryState } from "nuqs"; 16 + 17 + const parseTrigger = parseAsStringLiteral(TRIGGER); 18 + 19 + export function DropdownTrigger() { 20 + const [trigger, setTrigger] = useQueryState("trigger", parseTrigger); 21 + 22 + return ( 23 + <DropdownMenu> 24 + <DropdownMenuTrigger asChild> 25 + <Button variant="outline" size="sm" className="capitalize"> 26 + {trigger ?? "All Trigger"} 27 + </Button> 28 + </DropdownMenuTrigger> 29 + <DropdownMenuContent align="start"> 30 + <DropdownMenuGroup> 31 + <DropdownMenuLabel className="font-medium text-muted-foreground text-xs"> 32 + Trigger 33 + </DropdownMenuLabel> 34 + {TRIGGER.map((item) => ( 35 + <DropdownMenuItem 36 + key={item} 37 + onSelect={() => setTrigger(item)} 38 + className={cn("capitalize")} 39 + > 40 + {item === "cron" ? "Scheduled" : "API"} 41 + {trigger === item ? <Check className="ml-auto shrink-0" /> : null} 42 + </DropdownMenuItem> 43 + ))} 44 + </DropdownMenuGroup> 45 + </DropdownMenuContent> 46 + </DropdownMenu> 47 + ); 48 + }
+141
apps/status-page/src/components/controls-search/popover-date.tsx
··· 1 + import { DatePicker } from "@/components/date-picker"; 2 + import { Button } from "@/components/ui/button"; 3 + import { 4 + Popover, 5 + PopoverContent, 6 + PopoverTrigger, 7 + } from "@/components/ui/popover"; 8 + import { formatDateRange } from "@/lib/formatter"; 9 + import { endOfDay, startOfDay, subDays, subHours } from "date-fns"; 10 + import { parseAsIsoDateTime, useQueryState } from "nuqs"; 11 + import { useEffect, useMemo, useRef, useState } from "react"; 12 + import type { DateRange } from "react-day-picker"; 13 + 14 + export function PopoverDate() { 15 + const [open, setOpen] = useState(false); 16 + const today = useRef(new Date()); 17 + const [from, setFrom] = useQueryState( 18 + "from", 19 + parseAsIsoDateTime.withDefault(startOfDay(today.current)), 20 + ); 21 + const [to, setTo] = useQueryState( 22 + "to", 23 + parseAsIsoDateTime.withDefault(endOfDay(today.current)), 24 + ); 25 + const [range, setRange] = useState<DateRange>({ from, to }); 26 + 27 + // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation> 28 + const presets = useMemo( 29 + () => [ 30 + { 31 + id: "today", 32 + label: "Today", 33 + values: { 34 + from: startOfDay(today.current), 35 + to: endOfDay(today.current), 36 + }, 37 + shortcut: "t", 38 + }, 39 + { 40 + id: "yesterday", 41 + label: "Yesterday", 42 + values: { 43 + from: startOfDay(subDays(today.current, 1)), 44 + to: endOfDay(subDays(today.current, 1)), 45 + }, 46 + shortcut: "y", 47 + }, 48 + { 49 + id: "lastHour", 50 + label: "Last hour", 51 + values: { 52 + from: subHours(today.current, 1), 53 + to: today.current, 54 + }, 55 + shortcut: "h", 56 + }, 57 + { 58 + id: "last6Hours", 59 + label: "Last 6 hours", 60 + values: { 61 + from: subHours(today.current, 5), 62 + to: today.current, 63 + }, 64 + shortcut: "s", 65 + }, 66 + { 67 + id: "last24Hours", 68 + label: "Last 24 hours", 69 + values: { 70 + from: subHours(today.current, 23), 71 + to: today.current, 72 + }, 73 + shortcut: "d", 74 + }, 75 + { 76 + id: "last7Days", 77 + label: "Last 7 days", 78 + values: { 79 + from: subDays(today.current, 6), 80 + to: today.current, 81 + }, 82 + shortcut: "w", 83 + }, 84 + { 85 + id: "last14Days", 86 + label: "Last 14 days", 87 + values: { 88 + from: subDays(today.current, 13), 89 + to: today.current, 90 + }, 91 + shortcut: "b", 92 + }, 93 + ], 94 + [today], 95 + ); 96 + 97 + // instead use `range` state 98 + const selected = presets.find((period) => { 99 + return ( 100 + from.getTime() === period.values.from.getTime() && 101 + to.getTime() === period.values.to.getTime() 102 + ); 103 + }); 104 + 105 + // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation> 106 + useEffect(() => { 107 + if (!open) { 108 + setFrom(range.from ?? null); 109 + setTo(range.to ?? null); 110 + } 111 + }, [open]); 112 + 113 + useEffect(() => { 114 + const down = (e: KeyboardEvent) => { 115 + if (!open) return; 116 + 117 + presets.map((preset) => { 118 + if (preset.shortcut === e.key) { 119 + setFrom(preset.values.from); 120 + setTo(preset.values.to); 121 + setRange({ from: preset.values.from, to: preset.values.to }); 122 + } 123 + }); 124 + }; 125 + document.addEventListener("keydown", down); 126 + return () => document.removeEventListener("keydown", down); 127 + }, [presets, open, setFrom, setTo]); 128 + 129 + return ( 130 + <Popover open={open} onOpenChange={setOpen}> 131 + <PopoverTrigger asChild> 132 + <Button variant="outline" size="sm"> 133 + {selected?.label ?? formatDateRange(from, to)} 134 + </Button> 135 + </PopoverTrigger> 136 + <PopoverContent className="w-auto p-0" side="bottom" align="start"> 137 + <DatePicker presets={presets} range={range} onSelect={setRange} /> 138 + </PopoverContent> 139 + </Popover> 140 + ); 141 + }
+128
apps/status-page/src/components/date-picker.tsx
··· 1 + "use client"; 2 + 3 + import { useState } from "react"; 4 + import type { DateRange } from "react-day-picker"; 5 + 6 + import { Kbd } from "@/components/common/kbd"; 7 + import { Button } from "@/components/ui/button"; 8 + import { Calendar } from "@/components/ui/calendar"; 9 + import { Input } from "@/components/ui/input"; 10 + import { Label } from "@/components/ui/label"; 11 + import { Separator } from "@/components/ui/separator"; 12 + import { formatDateForInput } from "@/lib/formatter"; 13 + import { endOfDay } from "date-fns"; 14 + 15 + type DatePickerProps = { 16 + range: DateRange; 17 + onSelect: (range: DateRange) => void; 18 + presets: { id: string; label: string; values: DateRange; shortcut: string }[]; 19 + }; 20 + 21 + export function DatePicker({ range, onSelect, presets }: DatePickerProps) { 22 + const [today] = useState(new Date()); 23 + const disableBefore = presets[presets.length - 1]?.values?.from; 24 + 25 + return ( 26 + <div> 27 + <div className="flex flex-row"> 28 + <div className="relative py-4"> 29 + <div className="h-full"> 30 + <div className="flex flex-col px-1"> 31 + <div className="px-3 py-1 font-medium text-muted-foreground text-xs"> 32 + Presets 33 + </div> 34 + {presets.map((preset) => { 35 + const isSelected = 36 + range.from?.getTime() === preset.values.from?.getTime() && 37 + range.to?.getTime() === preset.values.to?.getTime(); 38 + 39 + return ( 40 + <Button 41 + key={preset.id} 42 + variant={isSelected ? "outline" : "ghost"} 43 + size="sm" 44 + className="w-full justify-between border border-transparent" 45 + onClick={() => { 46 + onSelect(preset.values); 47 + }} 48 + > 49 + <span>{preset.label}</span> 50 + <Kbd className="font-mono uppercase">{preset.shortcut}</Kbd> 51 + </Button> 52 + ); 53 + })} 54 + </div> 55 + </div> 56 + </div> 57 + <Separator orientation="vertical" className="h-auto! w-px" /> 58 + <div className="flex flex-1 items-center justify-center"> 59 + <Calendar 60 + mode="range" 61 + selected={range} 62 + onSelect={(newDate) => { 63 + if (newDate) { 64 + onSelect({ 65 + ...newDate, 66 + to: newDate.to ? endOfDay(newDate.to) : undefined, 67 + }); 68 + } 69 + }} 70 + className="p-2" 71 + disabled={[ 72 + { after: today }, // Dates before today 73 + { before: disableBefore ?? today }, // Dates before last action 74 + ]} 75 + /> 76 + </div> 77 + </div> 78 + <Separator /> 79 + <div className="flex flex-col gap-2 px-3 py-4"> 80 + <p className="px-1 font-medium text-muted-foreground text-xs"> 81 + Custom Range 82 + </p> 83 + <div className="grid gap-2 sm:grid-cols-2"> 84 + <div className="grid w-full gap-1.5"> 85 + <Label htmlFor="from" className="px-1"> 86 + Start 87 + </Label> 88 + <Input 89 + type="datetime-local" 90 + id="from" 91 + name="from" 92 + min={formatDateForInput(disableBefore ?? today)} 93 + max={formatDateForInput(today)} 94 + value={range.from ? formatDateForInput(range.from) : ""} 95 + onChange={(e) => { 96 + const newDate = new Date(e.target.value); 97 + if (!Number.isNaN(newDate.getTime())) { 98 + onSelect({ ...range, from: newDate }); 99 + } 100 + }} 101 + disabled={!range.from} 102 + /> 103 + </div> 104 + <div className="grid w-full gap-1.5"> 105 + <Label htmlFor="to" className="px-1"> 106 + End 107 + </Label> 108 + <Input 109 + type="datetime-local" 110 + id="to" 111 + name="to" 112 + min={formatDateForInput(range.from ?? today)} 113 + max={formatDateForInput(today)} 114 + value={range.to ? formatDateForInput(range.to) : ""} 115 + onChange={(e) => { 116 + const newDate = new Date(e.target.value); 117 + if (!Number.isNaN(newDate.getTime())) { 118 + onSelect({ ...range, to: newDate }); 119 + } 120 + }} 121 + disabled={!range.to} 122 + /> 123 + </div> 124 + </div> 125 + </div> 126 + </div> 127 + ); 128 + }
+49
apps/status-page/src/components/development-indicator.tsx
··· 1 + "use client"; 2 + 3 + import { useIsMobile } from "@/hooks/use-mobile"; 4 + import * as Portal from "@radix-ui/react-portal"; 5 + import { Kbd } from "./common/kbd"; 6 + import { 7 + Tooltip, 8 + TooltipContent, 9 + TooltipProvider, 10 + TooltipTrigger, 11 + } from "./ui/tooltip"; 12 + 13 + export function DevelopmentIndicator() { 14 + const isMobile = useIsMobile(); 15 + 16 + if (process.env.NODE_ENV !== "production") return null; 17 + 18 + return ( 19 + <Portal.Root> 20 + <div className="pointer-events-none fixed inset-0 z-[9999] border-2 border-destructive" /> 21 + <div className="fixed inset-x-0 bottom-0 z-[9999] select-none"> 22 + <div className="flex items-center justify-center"> 23 + <TooltipProvider delayDuration={0}> 24 + <Tooltip> 25 + <TooltipTrigger> 26 + <div className="w-fit rounded-t bg-destructive px-2 py-1 font-mono text-background text-xs"> 27 + In Beta 28 + </div> 29 + </TooltipTrigger> 30 + <TooltipContent side="top"> 31 + {!isMobile ? ( 32 + <p> 33 + Press{" "} 34 + <Kbd variant="secondary" className="-me-0 ms-0"> 35 + F 36 + </Kbd>{" "} 37 + key to provide feedback. 38 + </p> 39 + ) : ( 40 + <p>Use a larger screen to provide feedback.</p> 41 + )} 42 + </TooltipContent> 43 + </Tooltip> 44 + </TooltipProvider> 45 + </div> 46 + </div> 47 + </Portal.Root> 48 + ); 49 + }
+15
apps/status-page/src/components/icons/discord.tsx
··· 1 + export function DiscordIcon(props: React.ComponentProps<"svg">) { 2 + return ( 3 + <svg 4 + role="img" 5 + viewBox="0 0 24 24" 6 + xmlns="http://www.w3.org/2000/svg" 7 + stroke="currentColor" 8 + fill="currentColor" 9 + {...props} 10 + > 11 + <title>Discord</title> 12 + <path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" /> 13 + </svg> 14 + ); 15 + }
+15
apps/status-page/src/components/icons/github.tsx
··· 1 + export function GitHubIcon(props: React.ComponentProps<"svg">) { 2 + return ( 3 + <svg 4 + role="img" 5 + viewBox="0 0 24 24" 6 + xmlns="http://www.w3.org/2000/svg" 7 + stroke="currentColor" 8 + fill="currentColor" 9 + {...props} 10 + > 11 + <title>GitHub</title> 12 + <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" /> 13 + </svg> 14 + ); 15 + }
+15
apps/status-page/src/components/icons/google.tsx
··· 1 + export function GoogleIcon(props: React.ComponentProps<"svg">) { 2 + return ( 3 + <svg 4 + role="img" 5 + viewBox="0 0 24 24" 6 + xmlns="http://www.w3.org/2000/svg" 7 + stroke="currentColor" 8 + fill="currentColor" 9 + {...props} 10 + > 11 + <title>Google</title> 12 + <path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" /> 13 + </svg> 14 + ); 15 + }
+15
apps/status-page/src/components/icons/opsgenie.tsx
··· 1 + export function OpsGenieIcon(props: React.ComponentProps<"svg">) { 2 + return ( 3 + <svg 4 + role="img" 5 + viewBox="0 0 24 24" 6 + xmlns="http://www.w3.org/2000/svg" 7 + stroke="currentColor" 8 + fill="currentColor" 9 + {...props} 10 + > 11 + <title>Opsgenie</title> 12 + <path d="M12.002 0a5.988 5.988 0 1 1 0 11.975 5.988 5.988 0 0 1 0-11.975zm9.723 13.026h-.03l-4.527-2.242a.671.671 0 0 0-.876.268 22.408 22.408 0 0 1-4.306 5.217 22.407 22.407 0 0 1-4.286-5.2.671.671 0 0 0-.876-.269l-4.535 2.226h-.03a.671.671 0 0 0-.248.902 28.85 28.85 0 0 0 4.55 5.933l-.002.001c.024.025.05.048.075.072.335.335.676.664 1.027.981.081.074.165.144.247.217.315.278.632.555.96.82.144.117.295.227.441.341.277.216.552.434.837.639.44.318.888.625 1.346.917a.963.963 0 0 0 1.007.017c.487-.312.962-.64 1.428-.98.068-.05.132-.103.2-.153.358-.266.713-.537 1.06-.82.234-.19.46-.39.688-.588.17-.147.34-.291.506-.442.295-.268.58-.545.864-.825.061-.06.127-.118.188-.179l-.004-.002a28.852 28.852 0 0 0 4.565-5.949.671.671 0 0 0-.269-.902z" /> 13 + </svg> 14 + ); 15 + }
+15
apps/status-page/src/components/icons/pagerduty.tsx
··· 1 + export function PagerDutyIcon(props: React.ComponentProps<"svg">) { 2 + return ( 3 + <svg 4 + role="img" 5 + viewBox="0 0 24 24" 6 + xmlns="http://www.w3.org/2000/svg" 7 + stroke="currentColor" 8 + fill="currentColor" 9 + {...props} 10 + > 11 + <title>PagerDuty</title> 12 + <path d="M16.965 1.18C15.085.164 13.769 0 10.683 0H3.73v14.55h6.926c2.743 0 4.8-.164 6.61-1.37 1.975-1.303 3.004-3.484 3.004-6.007 0-2.716-1.262-4.896-3.305-5.994zm-5.5 10.326h-4.21V3.113l3.977-.027c3.62-.028 5.43 1.234 5.43 4.128 0 3.113-2.248 4.292-5.197 4.292zM3.73 17.61h3.525V24H3.73Z" /> 13 + </svg> 14 + ); 15 + }
+15
apps/status-page/src/components/icons/slack.tsx
··· 1 + export function SlackIcon(props: React.ComponentProps<"svg">) { 2 + return ( 3 + <svg 4 + role="img" 5 + viewBox="0 0 24 24" 6 + xmlns="http://www.w3.org/2000/svg" 7 + stroke="currentColor" 8 + fill="currentColor" 9 + {...props} 10 + > 11 + <title>Slack</title> 12 + <path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z" /> 13 + </svg> 14 + ); 15 + }
+117
apps/status-page/src/components/status-page/community-themes.ts
··· 1 + export const defaultTheme = { 2 + light: {} as React.CSSProperties, 3 + dark: {} as React.CSSProperties, 4 + } as const; 5 + 6 + export const supabaseTheme = { 7 + light: { 8 + "--background": "oklch(99.11% 0 0)", 9 + "--foreground": "oklch(20.46% 0 0)", 10 + "--border": "oklch(90.37% 0 0)", 11 + "--input": "oklch(90.37% 0 0)", 12 + 13 + "--primary": "oklch(76.26% 0.154 159.27)", 14 + "--primary-foreground": "oklch(20.46% 0 0)", 15 + "--muted": "oklch(97.61% 0 0)", 16 + "--muted-foreground": "oklch(54.52% 0 0)", 17 + "--secondary": "oklch(97.61% 0 0)", 18 + "--secondary-foreground": "oklch(20.46% 0 0)", 19 + "--accent": "oklch(97.61% 0 0)", 20 + "--accent-foreground": "oklch(20.46% 0 0)", 21 + 22 + "--success": "oklch(76.26% 0.154 159.27)", 23 + "--destructive": "oklch(62.71% 0.1936 33.34)", 24 + "--warning": "oklch(81.69% 0.1639 75.84)", 25 + "--info": "oklch(61.26% 0.218 283.85)", 26 + } as React.CSSProperties, 27 + dark: { 28 + "--background": "oklch(18.22% 0 0)", 29 + "--foreground": "oklch(98.51% 0 0)", 30 + "--border": "oklch(30.12% 0 0)", 31 + "--input": "oklch(30.12% 0 0)", 32 + 33 + "--primary": "oklch(68.56% 0.1558 158.13)", 34 + "--primary-foreground": "oklch(18.22% 0 0)", 35 + "--muted": "oklch(26.03% 0 0)", 36 + "--muted-foreground": "oklch(63.01% 0 0)", 37 + "--secondary": "oklch(26.03% 0 0)", 38 + "--secondary-foreground": "oklch(98.51% 0 0)", 39 + "--accent": "oklch(26.03% 0 0)", 40 + "--accent-foreground": "oklch(98.51% 0 0)", 41 + 42 + "--success": "oklch(68.56% 0.1558 158.13)", 43 + "--destructive": "oklch(62.71% 0.1936 33.34)", 44 + "--warning": "oklch(70.84% 0.1523 71.24)", 45 + "--info": "oklch(61.26% 0.218 283.85)", 46 + } as React.CSSProperties, 47 + }; 48 + 49 + export const githubTheme = { 50 + light: { 51 + "--background": "oklch(100% 0 0)", 52 + "--foreground": "oklch(24.29% 0.0045 247.86)", 53 + "--border": "oklch(85.86% 0.0054 251.18)", 54 + "--input": "oklch(85.86% 0.0054 251.18)", 55 + 56 + "--primary": "oklch(60.81% 0.1567 142.5)", 57 + "--primary-foreground": "oklch(24.29% 0.0045 247.86)", 58 + "--muted": "oklch(97.86% 0.0019 247.86)", 59 + "--muted-foreground": "oklch(40.78% 0.0056 247.86)", 60 + "--secondary": "oklch(97.86% 0.0019 247.86)", 61 + "--secondary-foreground": "oklch(24.29% 0.0045 247.86)", 62 + "--accent": "oklch(97.86% 0.0019 247.86)", 63 + "--accent-foreground": "oklch(24.29% 0.0045 247.86)", 64 + 65 + "--success": "oklch(60.81% 0.1567 142.5)", 66 + "--destructive": "oklch(58.79% 0.1577 22.18)", 67 + "--warning": "oklch(81.84% 0.1328 85.87)", 68 + "--info": "oklch(45.2% 0.1445 252.03)", 69 + } as React.CSSProperties, 70 + dark: { 71 + "--background": "oklch(10.39% 0.0194 248.34)", 72 + "--foreground": "oklch(100% 0 0)", 73 + "--border": "oklch(58.41% 0.011 252.87)", 74 + "--input": "oklch(58.41% 0.011 252.87)", 75 + 76 + "--primary": "oklch(54.34% 0.1634 145.98)", 77 + "--primary-foreground": "oklch(100% 0 0)", 78 + "--muted": "oklch(33.39% 0.0223 256.4)", 79 + "--muted-foreground": "oklch(79.7% 0.0169 262.74)", 80 + "--secondary": "oklch(33.39% 0.0223 256.4)", 81 + "--secondary-foreground": "oklch(100% 0 0)", 82 + "--accent": "oklch(33.39% 0.0223 256.4)", 83 + "--accent-foreground": "oklch(100% 0 0)", 84 + 85 + "--success": "oklch(54.34% 0.1634 145.98)", 86 + "--destructive": "oklch(47.1% 0.1909 25.95)", 87 + "--warning": "oklch(40.97% 0.2064 289.57)", 88 + "--info": "oklch(46.96% 0.2957 264.51)", 89 + } as React.CSSProperties, 90 + }; 91 + 92 + export const THEMES = { 93 + // supabase: supabaseTheme, 94 + default: { 95 + name: "Default", 96 + author: { name: "@openstatus", url: "https://openstatus.dev" }, 97 + ...defaultTheme, 98 + }, 99 + github: { 100 + name: "Github", 101 + author: { name: "@openstatus", url: "https://openstatus.dev" }, 102 + ...githubTheme, 103 + }, 104 + supabase: { 105 + name: "Supabase", 106 + author: { name: "@supabase", url: "https://supabase.com" }, 107 + ...supabaseTheme, 108 + }, 109 + } as const satisfies Record< 110 + string, 111 + { 112 + name: string; 113 + author: { name: string; url: string }; 114 + light: React.CSSProperties; 115 + dark: React.CSSProperties; 116 + } 117 + >;
+286
apps/status-page/src/components/status-page/floating-button.tsx
··· 1 + "use client"; 2 + 3 + import { ThemeToggle } from "@/components/theme-toggle"; 4 + import { Button } from "@/components/ui/button"; 5 + import { Label } from "@/components/ui/label"; 6 + import { 7 + Popover, 8 + PopoverContent, 9 + PopoverTrigger, 10 + } from "@/components/ui/popover"; 11 + import { 12 + Select, 13 + SelectContent, 14 + SelectItem, 15 + SelectTrigger, 16 + SelectValue, 17 + } from "@/components/ui/select"; 18 + import { Separator } from "@/components/ui/separator"; 19 + import { cn } from "@/lib/utils"; 20 + import { Settings } from "lucide-react"; 21 + import { useTheme } from "next-themes"; 22 + import type React from "react"; 23 + import { createContext, useContext, useEffect, useState } from "react"; 24 + import { THEMES } from "./community-themes"; 25 + 26 + export const VARIANT = ["success", "degraded", "error", "info"] as const; 27 + export type VariantType = (typeof VARIANT)[number]; 28 + 29 + export const CARD_TYPE = ["duration", "requests", "dominant"] as const; 30 + export type CardType = (typeof CARD_TYPE)[number]; 31 + 32 + export const BAR_TYPE = ["absolute", "dominant"] as const; 33 + export type BarType = (typeof BAR_TYPE)[number]; 34 + 35 + export const COMMUNITY_THEME = ["default", "github", "supabase"] as const; 36 + export type CommunityTheme = (typeof COMMUNITY_THEME)[number]; 37 + 38 + interface StatusPageContextType { 39 + variant: VariantType; 40 + setVariant: (variant: VariantType) => void; 41 + cardType: CardType; 42 + setCardType: (cardType: CardType) => void; 43 + barType: BarType; 44 + setBarType: (barType: BarType) => void; 45 + showUptime: boolean; 46 + setShowUptime: (showUptime: boolean) => void; 47 + communityTheme: CommunityTheme; 48 + setCommunityTheme: (communityTheme: CommunityTheme) => void; 49 + } 50 + 51 + const StatusPageContext = createContext<StatusPageContextType | null>(null); 52 + 53 + export function useStatusPage() { 54 + const context = useContext(StatusPageContext); 55 + if (!context) { 56 + throw new Error("useStatusPage must be used within a StatusPageProvider"); 57 + } 58 + return context; 59 + } 60 + 61 + export function StatusPageProvider({ 62 + children, 63 + defaultVariant = "success", 64 + defaultCardType = "duration", 65 + defaultBarType = "absolute", 66 + defaultShowUptime = true, 67 + defaultCommunityTheme = "default", 68 + }: { 69 + children: React.ReactNode; 70 + defaultVariant?: VariantType; 71 + defaultCardType?: CardType; 72 + defaultBarType?: BarType; 73 + defaultShowUptime?: boolean; 74 + defaultCommunityTheme?: CommunityTheme; 75 + }) { 76 + const [variant, setVariant] = useState<VariantType>(defaultVariant); 77 + const [cardType, setCardType] = useState<CardType>(defaultCardType); 78 + const [barType, setBarType] = useState<BarType>(defaultBarType); 79 + const [showUptime, setShowUptime] = useState<boolean>(defaultShowUptime); 80 + const { resolvedTheme } = useTheme(); 81 + const [communityTheme, setCommunityTheme] = useState<CommunityTheme>( 82 + defaultCommunityTheme, 83 + ); 84 + 85 + useEffect(() => { 86 + const theme = resolvedTheme as "dark" | "light"; 87 + if (["dark", "light"].includes(theme)) { 88 + Object.keys(THEMES[communityTheme][theme]).forEach((key) => { 89 + const element = document.documentElement; 90 + const value = 91 + THEMES[communityTheme][theme][ 92 + key as keyof (typeof THEMES)[typeof communityTheme][typeof theme] 93 + ]; 94 + if (value) { 95 + element.style.setProperty(key, value as string); 96 + } 97 + }); 98 + } 99 + if (communityTheme === "default") { 100 + document.documentElement.removeAttribute("style"); 101 + } 102 + }, [resolvedTheme, communityTheme]); 103 + 104 + return ( 105 + <StatusPageContext.Provider 106 + value={{ 107 + variant, 108 + setVariant, 109 + cardType, 110 + setCardType, 111 + barType, 112 + setBarType, 113 + showUptime, 114 + setShowUptime, 115 + communityTheme, 116 + setCommunityTheme, 117 + }} 118 + > 119 + <div 120 + style={ 121 + communityTheme 122 + ? THEMES[communityTheme][resolvedTheme as "dark" | "light"] 123 + : undefined 124 + } 125 + > 126 + {children} 127 + </div> 128 + </StatusPageContext.Provider> 129 + ); 130 + } 131 + 132 + export function FloatingButton({ className }: { className?: string }) { 133 + const { 134 + variant, 135 + setVariant, 136 + cardType, 137 + setCardType, 138 + barType, 139 + setBarType, 140 + showUptime, 141 + setShowUptime, 142 + communityTheme, 143 + setCommunityTheme, 144 + } = useStatusPage(); 145 + 146 + return ( 147 + <div className={cn("fixed right-4 bottom-4 z-50 bg-background", className)}> 148 + <Popover> 149 + <PopoverTrigger asChild> 150 + <Button 151 + size="icon" 152 + variant="outline" 153 + className="size-12 rounded-full" 154 + > 155 + <Settings className="size-5" /> 156 + <span className="sr-only">Open status page settings</span> 157 + </Button> 158 + </PopoverTrigger> 159 + <PopoverContent className="w-80 p-0" align="end"> 160 + <div className="space-y-4 p-4"> 161 + <div className="space-y-2"> 162 + <h4 className="font-medium leading-none">Status Page Settings</h4> 163 + <p className="text-muted-foreground text-sm"> 164 + Configure the status page appearance 165 + </p> 166 + </div> 167 + <div className="grid grid-cols-2 gap-4"> 168 + <div className="space-y-2"> 169 + <Label htmlFor="status-variant">Status Variant</Label> 170 + <Select 171 + value={variant} 172 + onValueChange={(v) => setVariant(v as VariantType)} 173 + > 174 + <SelectTrigger 175 + id="status-variant" 176 + className="w-full capitalize" 177 + > 178 + <SelectValue /> 179 + </SelectTrigger> 180 + <SelectContent> 181 + {VARIANT.map((v) => ( 182 + <SelectItem key={v} value={v} className="capitalize"> 183 + {v} 184 + </SelectItem> 185 + ))} 186 + </SelectContent> 187 + </Select> 188 + </div> 189 + <div className="space-y-2"> 190 + <Label htmlFor="show-uptime">Show Uptime</Label> 191 + <Select 192 + value={showUptime ? "true" : "false"} 193 + onValueChange={(v) => setShowUptime(v === "true")} 194 + > 195 + <SelectTrigger id="show-uptime" className="w-full capitalize"> 196 + <SelectValue /> 197 + </SelectTrigger> 198 + <SelectContent> 199 + {["true", "false"].map((v) => ( 200 + <SelectItem key={v} value={v} className="capitalize"> 201 + {v} 202 + </SelectItem> 203 + ))} 204 + </SelectContent> 205 + </Select> 206 + </div> 207 + <div className="space-y-2"> 208 + <Label htmlFor="card-type">Card Type</Label> 209 + <Select 210 + value={cardType} 211 + onValueChange={(v) => setCardType(v as CardType)} 212 + > 213 + <SelectTrigger id="card-type" className="w-full capitalize"> 214 + <SelectValue /> 215 + </SelectTrigger> 216 + <SelectContent> 217 + {CARD_TYPE.map((v) => ( 218 + <SelectItem key={v} value={v} className="capitalize"> 219 + {v} 220 + </SelectItem> 221 + ))} 222 + </SelectContent> 223 + </Select> 224 + </div> 225 + <div className="space-y-2"> 226 + <Label htmlFor="bar-type">Bar Type</Label> 227 + <Select 228 + value={barType} 229 + onValueChange={(v) => setBarType(v as BarType)} 230 + > 231 + <SelectTrigger id="bar-type" className="w-full capitalize"> 232 + <SelectValue /> 233 + </SelectTrigger> 234 + <SelectContent> 235 + {BAR_TYPE.map((v) => ( 236 + <SelectItem key={v} value={v} className="capitalize"> 237 + {v} 238 + </SelectItem> 239 + ))} 240 + </SelectContent> 241 + </Select> 242 + </div> 243 + <div className="space-y-2"> 244 + <Label htmlFor="theme">Theme</Label> 245 + <ThemeToggle id="theme" className="w-full" /> 246 + </div> 247 + <div className="space-y-2"> 248 + <Label htmlFor="community-theme">Community Theme</Label> 249 + <Select 250 + value={communityTheme} 251 + onValueChange={(v) => setCommunityTheme(v as CommunityTheme)} 252 + > 253 + <SelectTrigger 254 + id="community-theme" 255 + className="w-full capitalize" 256 + > 257 + <SelectValue /> 258 + </SelectTrigger> 259 + <SelectContent> 260 + {COMMUNITY_THEME.map((v) => ( 261 + <SelectItem key={v} value={v} className="capitalize"> 262 + {v} 263 + </SelectItem> 264 + ))} 265 + </SelectContent> 266 + </Select> 267 + </div> 268 + </div> 269 + </div> 270 + <Separator /> 271 + <div className="p-4"> 272 + <Button className="w-full" size="sm" asChild> 273 + <a 274 + href="https://github.com/openstatusHQ/openstatus-template" 275 + target="_blank" 276 + rel="noreferrer" 277 + > 278 + GitHub Repo 279 + </a> 280 + </Button> 281 + </div> 282 + </PopoverContent> 283 + </Popover> 284 + </div> 285 + ); 286 + }
+21
apps/status-page/src/components/status-page/messages.ts
··· 1 + export const messages = { 2 + long: { 3 + success: "All Systems Operational", 4 + degraded: "Degraded Performance", 5 + error: "Downtime Performance", 6 + info: "Maintenance", 7 + }, 8 + short: { 9 + success: "Operational", 10 + degraded: "Degraded", 11 + error: "Downtime", 12 + info: "Maintenance", 13 + }, 14 + }; 15 + 16 + export const requests = { 17 + success: "Normal", 18 + degraded: "Degraded", 19 + error: "Error", 20 + info: "Maintenance", 21 + };
+65
apps/status-page/src/components/status-page/status-charts.tsx
··· 1 + import { cn } from "@/lib/utils"; 2 + 3 + export function StatusChartContent({ 4 + children, 5 + className, 6 + ...props 7 + }: React.ComponentProps<"div">) { 8 + return ( 9 + <div 10 + data-slot="status-chart-content" 11 + className={cn("flex flex-col gap-3", className)} 12 + {...props} 13 + > 14 + {children} 15 + </div> 16 + ); 17 + } 18 + 19 + export function StatusChartHeader({ 20 + children, 21 + className, 22 + ...props 23 + }: React.ComponentProps<"div">) { 24 + return ( 25 + <div 26 + data-slot="status-chart-header" 27 + className={cn("flex flex-col", className)} 28 + {...props} 29 + > 30 + {children} 31 + </div> 32 + ); 33 + } 34 + 35 + export function StatusChartTitle({ 36 + children, 37 + className, 38 + ...props 39 + }: React.ComponentProps<"div">) { 40 + return ( 41 + <div 42 + data-slot="status-chart-title" 43 + className={cn("font-medium text-base text-foreground", className)} 44 + {...props} 45 + > 46 + {children} 47 + </div> 48 + ); 49 + } 50 + 51 + export function StatusChartDescription({ 52 + children, 53 + className, 54 + ...props 55 + }: React.ComponentProps<"div">) { 56 + return ( 57 + <div 58 + data-slot="status-chart-description" 59 + className={cn("text-muted-foreground text-sm", className)} 60 + {...props} 61 + > 62 + {children} 63 + </div> 64 + ); 65 + }
+446
apps/status-page/src/components/status-page/status-events.tsx
··· 1 + import { Badge } from "@/components/ui/badge"; 2 + import { Separator } from "@/components/ui/separator"; 3 + import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 4 + import { type Maintenance, maintenances } from "@/data/maintenances"; 5 + import { type StatusReport, statusReports } from "@/data/status-reports"; 6 + import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"; 7 + import { formatDate, formatTime } from "@/lib/formatter"; 8 + import { cn } from "@/lib/utils"; 9 + import { UTCDate } from "@date-fns/utc"; 10 + import { HoverCardPortal } from "@radix-ui/react-hover-card"; 11 + import { 12 + format, 13 + formatDistanceStrict, 14 + formatDistanceToNowStrict, 15 + } from "date-fns"; 16 + import { Check, Copy } from "lucide-react"; 17 + import Link from "next/link"; 18 + import { 19 + HoverCard, 20 + HoverCardContent, 21 + HoverCardTrigger, 22 + } from "../ui/hover-card"; 23 + 24 + const STATUS_LABELS = { 25 + operational: "Resolved", 26 + monitoring: "Monitoring", 27 + identified: "Identified", 28 + investigating: "Investigating", 29 + }; 30 + 31 + // TODO: move to page level 32 + export function StatusEventsTabs() { 33 + return ( 34 + <Tabs defaultValue="reports" className="gap-4"> 35 + <TabsList> 36 + <TabsTrigger value="reports">Reports</TabsTrigger> 37 + <TabsTrigger value="maintenances">Maintenances</TabsTrigger> 38 + </TabsList> 39 + <TabsContent value="reports" className="flex flex-col gap-4"> 40 + {statusReports.map((report) => ( 41 + <StatusEvent key={report.id}> 42 + <StatusEventAside> 43 + <span className="font-medium text-foreground/80"> 44 + {formatDate(report.startedAt, { month: "short" })} 45 + </span> 46 + </StatusEventAside> 47 + <Link href="/status-page/events/report" className="rounded-lg"> 48 + <StatusEventContent> 49 + <StatusEventTitle>{report.name}</StatusEventTitle> 50 + <StatusEventAffected className="flex flex-wrap gap-1"> 51 + {report.affected.map((affected) => ( 52 + <Badge 53 + key={affected} 54 + variant="outline" 55 + className="text-[10px]" 56 + > 57 + {affected} 58 + </Badge> 59 + ))} 60 + </StatusEventAffected> 61 + <StatusEventTimelineReport updates={report.updates} /> 62 + </StatusEventContent> 63 + </Link> 64 + </StatusEvent> 65 + ))} 66 + </TabsContent> 67 + <TabsContent value="maintenances" className="flex flex-col gap-4"> 68 + {maintenances.map((maintenance) => { 69 + const isFuture = maintenance.startDate > new Date(); 70 + return ( 71 + <StatusEvent key={maintenance.id}> 72 + <StatusEventAside> 73 + <span className="font-medium text-foreground/80"> 74 + {formatDate(maintenance.startDate, { month: "short" })} 75 + </span> 76 + {isFuture ? ( 77 + <span className="text-info text-sm">Upcoming</span> 78 + ) : null} 79 + </StatusEventAside> 80 + <Link 81 + href="/status-page/events/maintenance" 82 + className="rounded-lg" 83 + > 84 + <StatusEventContent> 85 + <StatusEventTitle>{maintenance.title}</StatusEventTitle> 86 + <StatusEventAffected className="flex flex-wrap gap-1"> 87 + {maintenance.affected.map((affected) => ( 88 + <Badge 89 + key={affected} 90 + variant="outline" 91 + className="text-[10px]" 92 + > 93 + {affected} 94 + </Badge> 95 + ))} 96 + </StatusEventAffected> 97 + <StatusEventTimelineMaintenance maintenance={maintenance} /> 98 + </StatusEventContent> 99 + </Link> 100 + </StatusEvent> 101 + ); 102 + })} 103 + </TabsContent> 104 + </Tabs> 105 + ); 106 + } 107 + 108 + // TODO: rename file to status-event and move the `StatusEvents` component to the page level. 109 + 110 + export function StatusEvent({ 111 + className, 112 + children, 113 + ...props 114 + }: React.ComponentProps<"div">) { 115 + return ( 116 + <div className={cn("relative flex flex-col gap-2", className)} {...props}> 117 + {children} 118 + </div> 119 + ); 120 + } 121 + 122 + export function StatusEventContent({ 123 + className, 124 + hoverable = true, 125 + children, 126 + ...props 127 + }: React.ComponentProps<"div"> & { 128 + hoverable?: boolean; 129 + }) { 130 + // TODO: add Link 131 + return ( 132 + <div 133 + data-hoverable={hoverable} 134 + className={cn( 135 + "group -mx-3 -my-2 flex flex-col gap-2 rounded-lg border border-transparent px-3 py-2", 136 + "data-[hoverable=true]:hover:cursor-pointer data-[hoverable=true]:hover:border-border/50 data-[hoverable=true]:hover:bg-muted/50", 137 + className, 138 + )} 139 + {...props} 140 + > 141 + {children} 142 + </div> 143 + ); 144 + } 145 + 146 + export function StatusEventTitle({ 147 + className, 148 + children, 149 + ...props 150 + }: React.ComponentProps<"div">) { 151 + return ( 152 + <div className={cn("font-medium", className)} {...props}> 153 + {children} 154 + </div> 155 + ); 156 + } 157 + 158 + // TODO: affected monitors 159 + export function StatusEventAffected({ 160 + className, 161 + children, 162 + ...props 163 + }: React.ComponentProps<"div">) { 164 + return ( 165 + <div className={cn("text-muted-foreground text-sm", className)} {...props}> 166 + {children} 167 + </div> 168 + ); 169 + } 170 + 171 + export function StatusEventAside({ 172 + className, 173 + children, 174 + ...props 175 + }: React.ComponentProps<"div">) { 176 + return ( 177 + <div className="lg:-left-32 lg:absolute lg:top-0 lg:h-full"> 178 + <div 179 + className={cn( 180 + "flex flex-col gap-1 lg:sticky lg:top-0 lg:left-0", 181 + className, 182 + )} 183 + {...props} 184 + > 185 + {children} 186 + </div> 187 + </div> 188 + ); 189 + } 190 + 191 + export function StatusEventTimelineReport({ 192 + className, 193 + updates, 194 + ...props 195 + }: React.ComponentProps<"div"> & { 196 + updates: StatusReport["updates"]; 197 + }) { 198 + const startedAt = new Date(updates[0].date); 199 + const endedAt = new Date(updates[updates.length - 1].date); 200 + const duration = formatDistanceStrict(startedAt, endedAt); 201 + return ( 202 + <div className={cn("text-muted-foreground text-sm", className)} {...props}> 203 + {/* TODO: make sure they are sorted by date */} 204 + {updates 205 + .sort((a, b) => b.date.getTime() - a.date.getTime()) 206 + .map((update, index) => ( 207 + <StatusEventTimelineReportUpdate 208 + key={update.id} 209 + report={update} 210 + duration={ 211 + index === 0 && update.status === "operational" 212 + ? duration 213 + : undefined 214 + } 215 + withSeparator={index !== updates.length - 1} 216 + /> 217 + ))} 218 + </div> 219 + ); 220 + } 221 + 222 + function StatusEventTimelineReportUpdate({ 223 + report, 224 + duration, 225 + withSeparator = true, 226 + }: { 227 + report: StatusReport["updates"][number]; 228 + withSeparator?: boolean; 229 + duration?: string; 230 + }) { 231 + return ( 232 + <div data-variant={report.status} className="group"> 233 + <div className="flex flex-row items-center justify-between gap-2"> 234 + <div className="flex flex-row gap-2"> 235 + <div className="flex flex-col"> 236 + <div className="flex h-5 flex-col items-center justify-center"> 237 + <StatusEventTimelineDot /> 238 + </div> 239 + {withSeparator ? <StatusEventTimelineSeparator /> : null} 240 + </div> 241 + <div className="mb-2"> 242 + <StatusEventTimelineTitle> 243 + <span>{STATUS_LABELS[report.status]}</span>{" "} 244 + <span className="font-mono text-muted-foreground/70 text-xs underline decoration-dashed underline-offset-2"> 245 + <StatusEventDateHoverCard date={new Date(report.date)}> 246 + {formatTime(report.date)} 247 + </StatusEventDateHoverCard> 248 + </span>{" "} 249 + {duration ? ( 250 + <span className="font-mono text-muted-foreground/70 text-xs"> 251 + (in {duration}) 252 + </span> 253 + ) : null} 254 + </StatusEventTimelineTitle> 255 + <StatusEventTimelineMessage> 256 + {report.message} 257 + </StatusEventTimelineMessage> 258 + </div> 259 + </div> 260 + </div> 261 + </div> 262 + ); 263 + } 264 + 265 + export function StatusEventTimelineMaintenance({ 266 + maintenance, 267 + }: { 268 + maintenance: Maintenance; 269 + }) { 270 + const start = new Date(maintenance.startDate); 271 + const end = new Date(maintenance.endDate); 272 + const duration = formatDistanceStrict(start, end); 273 + return ( 274 + <div data-variant="maintenance" className="group"> 275 + <div className="flex flex-row items-center justify-between gap-2"> 276 + <div className="flex flex-row gap-2"> 277 + <div className="flex flex-col"> 278 + <div className="flex h-5 flex-col items-center justify-center"> 279 + <StatusEventTimelineDot /> 280 + </div> 281 + </div> 282 + <div className="mb-2"> 283 + <StatusEventTimelineTitle> 284 + <span>Maintenance</span>{" "} 285 + <span className="font-mono text-muted-foreground/70 text-xs"> 286 + <span className="underline decoration-dashed underline-offset-2"> 287 + <StatusEventDateHoverCard date={new Date(start)}> 288 + {formatTime(start)} 289 + </StatusEventDateHoverCard> 290 + </span> 291 + {" - "} 292 + <span className="underline decoration-dashed underline-offset-2"> 293 + <StatusEventDateHoverCard date={new Date(end)}> 294 + {formatTime(end)} 295 + </StatusEventDateHoverCard> 296 + </span> 297 + </span>{" "} 298 + {duration ? ( 299 + <span className="font-mono text-muted-foreground/70 text-xs"> 300 + (for {duration}) 301 + </span> 302 + ) : null} 303 + </StatusEventTimelineTitle> 304 + <StatusEventTimelineMessage> 305 + {maintenance.message} 306 + </StatusEventTimelineMessage> 307 + </div> 308 + </div> 309 + </div> 310 + </div> 311 + ); 312 + } 313 + 314 + export function StatusEventTimelineTitle({ 315 + className, 316 + children, 317 + ...props 318 + }: React.ComponentProps<"div">) { 319 + return ( 320 + <div 321 + className={cn("font-medium text-foreground text-sm", className)} 322 + {...props} 323 + > 324 + {children} 325 + </div> 326 + ); 327 + } 328 + 329 + // TODO: should support markdown 330 + export function StatusEventTimelineMessage({ 331 + className, 332 + children, 333 + ...props 334 + }: React.ComponentProps<"div">) { 335 + return ( 336 + <div className={cn("text-muted-foreground text-sm", className)} {...props}> 337 + {children} 338 + </div> 339 + ); 340 + } 341 + 342 + export function StatusEventTimelineDot({ 343 + className, 344 + ...props 345 + }: React.ComponentProps<"div">) { 346 + return ( 347 + <div 348 + className={cn( 349 + "size-2.5 shrink-0 rounded-full bg-muted", 350 + "group-data-[variant=operational]:bg-success", 351 + "group-data-[variant=monitoring]:bg-info", 352 + "group-data-[variant=identified]:bg-warning", 353 + "group-data-[variant=investigating]:bg-destructive", 354 + "group-data-[variant=maintenance]:bg-info", 355 + className, 356 + )} 357 + {...props} 358 + /> 359 + ); 360 + } 361 + 362 + export function StatusEventTimelineSeparator({ 363 + className, 364 + ...props 365 + }: React.ComponentProps<typeof Separator>) { 366 + return ( 367 + <Separator 368 + orientation="vertical" 369 + className={cn( 370 + "mx-auto flex-1", 371 + "group-data-[variant=operational]:bg-success", 372 + "group-data-[variant=monitoring]:bg-info", 373 + "group-data-[variant=identified]:bg-warning", 374 + "group-data-[variant=investigating]:bg-destructive", 375 + "group-data-[variant=maintenance]:bg-info", 376 + className, 377 + )} 378 + {...props} 379 + /> 380 + ); 381 + } 382 + 383 + export function StatusEventDateHoverCard({ 384 + date, 385 + side = "right", 386 + align = "start", 387 + alignOffset = -4, 388 + sideOffset, 389 + children, 390 + }: React.ComponentProps<typeof HoverCardContent> & { date: Date }) { 391 + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; 392 + return ( 393 + <HoverCard openDelay={0} closeDelay={0}> 394 + {/* NOTE: the trigger is an `a` tag per default */} 395 + <HoverCardTrigger asChild> 396 + <span>{children}</span> 397 + </HoverCardTrigger> 398 + <HoverCardPortal> 399 + <HoverCardContent 400 + className="z-10 w-auto p-2" 401 + {...{ side, align, alignOffset, sideOffset }} 402 + > 403 + <dl className="flex flex-col gap-1"> 404 + <Row value={format(date, "LLL dd, y HH:mm:ss")} label={timezone} /> 405 + <Row 406 + value={format(new UTCDate(date), "LLL dd, y HH:mm:ss")} 407 + label="UTC" 408 + /> 409 + {/* <Row value={date.toISOString()} label="ISO" /> */} 410 + {/* <Row value={String(date.getTime())} label="Timestamp" /> */} 411 + <Row 412 + value={formatDistanceToNowStrict(date, { addSuffix: true })} 413 + label="Relative" 414 + /> 415 + </dl> 416 + </HoverCardContent> 417 + </HoverCardPortal> 418 + </HoverCard> 419 + ); 420 + } 421 + 422 + function Row({ value, label }: { value: string; label: string }) { 423 + const { copy, isCopied } = useCopyToClipboard(); 424 + 425 + return ( 426 + <div 427 + className="group flex items-center justify-between gap-4 text-sm" 428 + onClick={(e) => { 429 + e.stopPropagation(); 430 + copy(value, {}); 431 + }} 432 + > 433 + <dt className="text-muted-foreground">{label}</dt> 434 + <dd className="flex items-center gap-1 truncate font-mono"> 435 + <span className="invisible group-hover:visible"> 436 + {!isCopied ? ( 437 + <Copy className="h-3 w-3" /> 438 + ) : ( 439 + <Check className="h-3 w-3" /> 440 + )} 441 + </span> 442 + {value} 443 + </dd> 444 + </div> 445 + ); 446 + }
+176
apps/status-page/src/components/status-page/status-monitor.tsx
··· 1 + "use client"; 2 + 3 + import { 4 + Tooltip, 5 + TooltipContent, 6 + TooltipProvider, 7 + TooltipTrigger, 8 + } from "@/components/ui/tooltip"; 9 + import type { Monitor } from "@/data/monitors"; 10 + import { useMediaQuery } from "@/hooks/use-media-query"; 11 + import { cn } from "@/lib/utils"; 12 + import { formatDistanceToNowStrict } from "date-fns"; 13 + import { 14 + AlertCircleIcon, 15 + CheckIcon, 16 + InfoIcon, 17 + TriangleAlertIcon, 18 + WrenchIcon, 19 + } from "lucide-react"; 20 + import { useState } from "react"; 21 + import type { BarType, CardType, VariantType } from "./floating-button"; 22 + import { StatusTracker } from "./status-tracker"; 23 + import type { ChartData } from "./utils"; 24 + 25 + export function StatusMonitor({ 26 + className, 27 + variant = "success", 28 + cardType = "duration", 29 + barType = "absolute", 30 + showUptime = true, 31 + data, 32 + monitor, 33 + ...props 34 + }: React.ComponentProps<"div"> & { 35 + variant?: VariantType; 36 + cardType?: CardType; 37 + barType?: BarType; 38 + showUptime?: boolean; 39 + monitor: Monitor; 40 + data: ChartData[]; 41 + }) { 42 + return ( 43 + <div 44 + data-slot="status-monitor" 45 + data-variant={variant} 46 + className={cn("group/monitor flex flex-col gap-1", className)} 47 + {...props} 48 + > 49 + <div className="flex flex-row items-center justify-between gap-4"> 50 + <div className="flex flex-row items-center gap-2"> 51 + <StatusMonitorTitle>{monitor.name}</StatusMonitorTitle> 52 + <StatusMonitorDescription> 53 + {monitor.description} 54 + </StatusMonitorDescription> 55 + </div> 56 + <div className="flex flex-row items-center gap-2"> 57 + {showUptime ? <StatusMonitorUptime /> : null} 58 + <StatusMonitorIcon /> 59 + </div> 60 + </div> 61 + <StatusTracker cardType={cardType} barType={barType} data={data} /> 62 + <div 63 + className={cn( 64 + "flex flex-row items-center justify-between text-muted-foreground text-xs", 65 + className, 66 + )} 67 + {...props} 68 + > 69 + <div> 70 + {formatDistanceToNowStrict(new Date(data[0].timestamp), { 71 + unit: "day", 72 + })} 73 + </div> 74 + <div>today</div> 75 + </div> 76 + </div> 77 + ); 78 + } 79 + 80 + export function StatusMonitorTitle({ 81 + children, 82 + className, 83 + ...props 84 + }: React.ComponentProps<"div">) { 85 + return ( 86 + <div className={cn("font-medium", className)} {...props}> 87 + {children} 88 + </div> 89 + ); 90 + } 91 + 92 + export function StatusMonitorDescription({ 93 + onClick, 94 + children, 95 + ...props 96 + }: React.ComponentProps<typeof TooltipTrigger>) { 97 + const isTouch = useMediaQuery("(hover: none)"); 98 + const [open, setOpen] = useState(false); 99 + 100 + return ( 101 + <TooltipProvider delayDuration={0}> 102 + <Tooltip open={open} onOpenChange={setOpen}> 103 + <TooltipTrigger 104 + onClick={(e) => { 105 + if (isTouch) setOpen((prev) => !prev); 106 + onClick?.(e); 107 + }} 108 + {...props} 109 + > 110 + <InfoIcon className="size-4 text-muted-foreground" /> 111 + </TooltipTrigger> 112 + <TooltipContent> 113 + <p>{children}</p> 114 + </TooltipContent> 115 + </Tooltip> 116 + </TooltipProvider> 117 + ); 118 + } 119 + export function StatusMonitorIcon({ 120 + className, 121 + ...props 122 + }: React.ComponentProps<"div">) { 123 + return ( 124 + <div 125 + className={cn( 126 + "flex size-4 items-center justify-center rounded-full bg-muted text-background [&>svg]:size-2.5", 127 + "group-data-[variant=success]/monitor:bg-success", 128 + "group-data-[variant=degraded]/monitor:bg-warning", 129 + "group-data-[variant=error]/monitor:bg-destructive", 130 + "group-data-[variant=info]/monitor:bg-info", 131 + className, 132 + )} 133 + {...props} 134 + > 135 + <CheckIcon className="hidden group-data-[variant=success]/monitor:block" /> 136 + <TriangleAlertIcon className="hidden group-data-[variant=degraded]/monitor:block" /> 137 + <AlertCircleIcon className="hidden group-data-[variant=error]/monitor:block" /> 138 + <WrenchIcon className="hidden group-data-[variant=info]/monitor:block" /> 139 + </div> 140 + ); 141 + } 142 + export function StatusMonitorUptime({ 143 + className, 144 + ...props 145 + }: React.ComponentProps<"div">) { 146 + return ( 147 + <div 148 + {...props} 149 + className={cn("font-mono text-muted-foreground text-sm", className)} 150 + > 151 + 99.90% 152 + </div> 153 + ); 154 + } 155 + 156 + export function StatusMonitorStatus({ 157 + className, 158 + ...props 159 + }: React.ComponentProps<"div">) { 160 + return ( 161 + <div className={cn(className)} {...props}> 162 + <span className="hidden group-data-[variant=success]/monitor:block"> 163 + Operational 164 + </span> 165 + <span className="hidden group-data-[variant=degraded]/monitor:block"> 166 + Degraded 167 + </span> 168 + <span className="hidden group-data-[variant=error]/monitor:block"> 169 + Downtime 170 + </span> 171 + <span className="hidden group-data-[variant=info]/monitor:block"> 172 + Maintenance 173 + </span> 174 + </div> 175 + ); 176 + }
+53
apps/status-page/src/components/status-page/status-tracker-group.tsx
··· 1 + import { 2 + Collapsible, 3 + CollapsibleContent, 4 + CollapsibleTrigger, 5 + } from "@/components/ui/collapsible"; 6 + import { cn } from "@/lib/utils"; 7 + import type { VariantType } from "./floating-button"; 8 + import { StatusMonitorIcon, StatusMonitorStatus } from "./status-monitor"; 9 + 10 + export function StatusTrackerGroup({ 11 + children, 12 + title, 13 + variant, 14 + className, 15 + ...props 16 + }: React.ComponentProps<typeof CollapsibleTrigger> & { 17 + title: string; 18 + variant?: VariantType; 19 + }) { 20 + return ( 21 + <Collapsible 22 + className={cn( 23 + "-mx-3", 24 + "rounded-lg border border-transparent hover:border-border/50 hover:bg-muted/50 data-[state=open]:border-border/50 data-[state=open]:bg-muted/50", 25 + className, 26 + )} 27 + > 28 + <CollapsibleTrigger 29 + className={cn( 30 + "group/monitor flex w-full items-center justify-between gap-2 rounded-lg px-3 py-2 font-medium", 31 + "cursor-pointer", 32 + className, 33 + )} 34 + data-variant={variant} 35 + {...props} 36 + > 37 + {title} 38 + <div className="flex items-center gap-2"> 39 + <StatusMonitorStatus className="text-sm" /> 40 + <StatusMonitorIcon /> 41 + </div> 42 + </CollapsibleTrigger> 43 + <CollapsibleContent 44 + className={cn( 45 + "flex flex-col gap-3 border-border/50 border-t px-3 py-2", 46 + "overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down", 47 + )} 48 + > 49 + {children} 50 + </CollapsibleContent> 51 + </Collapsible> 52 + ); 53 + }
+320
apps/status-page/src/components/status-page/status-tracker.tsx
··· 1 + "use client"; 2 + 3 + import { Kbd } from "@/components/common/kbd"; 4 + import { 5 + HoverCard, 6 + HoverCardContent, 7 + HoverCardTrigger, 8 + } from "@/components/ui/hover-card"; 9 + import { Separator } from "@/components/ui/separator"; 10 + // TODO: make it a property of the component 11 + import { statusReports } from "@/data/status-reports"; 12 + import { useMediaQuery } from "@/hooks/use-media-query"; 13 + import { formatDateRange } from "@/lib/formatter"; 14 + import { formatDistanceStrict, isSameDay } from "date-fns"; 15 + import Link from "next/link"; 16 + import { useEffect, useRef, useState } from "react"; 17 + import { type BarType, type CardType, VARIANT } from "./floating-button"; 18 + import { messages, requests } from "./messages"; 19 + import { type ChartData, chartConfig, getHighestPriorityStatus } from "./utils"; 20 + 21 + // TODO: keyboard arrow navigation 22 + // FIXME: on small screens, avoid pinned state 23 + // TODO: only on real mobile devices, use click events 24 + // TODO: improve status reports -> add duration and time 25 + // TODO: support headless mode -> both card and bar type share only maintenance or degraded mode 26 + // TODO: support status page logo + onClick to homepage 27 + // TODO: widget type -> current status only | with status history 28 + 29 + const STATUS = VARIANT; 30 + 31 + export function StatusTracker({ 32 + cardType = "duration", 33 + barType = "absolute", 34 + data, 35 + }: { 36 + cardType?: CardType; 37 + barType?: BarType; 38 + data: ChartData[]; 39 + }) { 40 + const [pinnedIndex, setPinnedIndex] = useState<number | null>(null); 41 + const containerRef = useRef<HTMLDivElement>(null); 42 + const isTouch = useMediaQuery("(hover: none)"); 43 + 44 + // Window-level Escape key listener 45 + useEffect(() => { 46 + const handleEscape = (e: KeyboardEvent) => { 47 + if (e.key === "Escape" && pinnedIndex !== null) { 48 + setPinnedIndex(null); 49 + } 50 + }; 51 + 52 + window.addEventListener("keydown", handleEscape); 53 + return () => window.removeEventListener("keydown", handleEscape); 54 + }, [pinnedIndex]); 55 + 56 + // Document-level outside click listener 57 + useEffect(() => { 58 + const handleOutsideClick = (e: MouseEvent) => { 59 + if ( 60 + pinnedIndex !== null && 61 + containerRef.current && 62 + !containerRef.current.contains(e.target as Node) 63 + ) { 64 + setPinnedIndex(null); 65 + } 66 + }; 67 + 68 + if (pinnedIndex !== null) { 69 + document.addEventListener("mousedown", handleOutsideClick); 70 + return () => 71 + document.removeEventListener("mousedown", handleOutsideClick); 72 + } 73 + }, [pinnedIndex]); 74 + 75 + // Handle keyboard events for accessibility (kept for fallback) 76 + const handleKeyDown = (e: React.KeyboardEvent) => { 77 + if (e.key === "Escape") { 78 + setPinnedIndex(null); 79 + } 80 + }; 81 + 82 + const handleBarClick = (index: number) => { 83 + // Toggle pinned state: if clicking the same bar, unpin it; otherwise, pin the new bar 84 + if (pinnedIndex === index) { 85 + setPinnedIndex(null); 86 + } else { 87 + setPinnedIndex(index); 88 + } 89 + }; 90 + 91 + return ( 92 + <div 93 + ref={containerRef} 94 + className="flex h-[50px] w-full items-end" 95 + onKeyDown={handleKeyDown} 96 + // tabIndex={0} 97 + > 98 + {data.map((item, index) => { 99 + const isPinned = pinnedIndex === index; 100 + 101 + const reports = statusReports.filter((report) => { 102 + const reportDate = new Date(report.startedAt); 103 + const itemDate = new Date(item.timestamp); 104 + return isSameDay(reportDate, itemDate); 105 + }); 106 + 107 + return ( 108 + <HoverCard 109 + key={item.timestamp} 110 + openDelay={0} 111 + closeDelay={0} 112 + open={isPinned ? true : undefined} 113 + > 114 + <HoverCardTrigger asChild> 115 + <div 116 + className="group relative flex h-full w-full cursor-pointer flex-col px-px transition-opacity hover:opacity-80" // sm:px-0.5 117 + onClick={() => handleBarClick(index)} 118 + > 119 + {(() => { 120 + switch (barType) { 121 + case "absolute": 122 + return <StatusTrackerTriggerAbsolute item={item} />; 123 + case "dominant": 124 + return <StatusTrackerTriggerDominant item={item} />; 125 + default: 126 + return null; 127 + } 128 + })()} 129 + </div> 130 + </HoverCardTrigger> 131 + <HoverCardContent side="top" align="center" className="w-auto p-0"> 132 + <div> 133 + <div className="p-2 text-xs"> 134 + {new Date(item.timestamp).toLocaleDateString("default", { 135 + day: "numeric", 136 + month: "short", 137 + })} 138 + </div> 139 + <Separator /> 140 + <div className="space-y-1 p-2 text-sm"> 141 + {(() => { 142 + switch (cardType) { 143 + case "duration": 144 + return <StatusTrackerContentDuration item={item} />; 145 + case "dominant": 146 + return <StatusTrackerContentDominant item={item} />; 147 + case "requests": 148 + return <StatusTrackerContentRequests item={item} />; 149 + default: 150 + return null; 151 + } 152 + })()} 153 + </div> 154 + {reports.length > 0 ? ( 155 + <> 156 + <Separator /> 157 + <div className="p-2"> 158 + {reports.map((report) => { 159 + const updates = report.updates.sort( 160 + (a, b) => a.date.getTime() - b.date.getTime(), 161 + ); 162 + const startedAt = new Date(updates[0].date); 163 + const endedAt = new Date( 164 + updates[updates.length - 1].date, 165 + ); 166 + const duration = formatDistanceStrict( 167 + startedAt, 168 + endedAt, 169 + ); 170 + return ( 171 + <Link 172 + key={report.id} 173 + href="/status-page/events/report" 174 + > 175 + <div className="group relative text-sm"> 176 + {/* NOTE: this is to make the text truncate based on the with of the sibling element */} 177 + {/* REMINDER: height needs to be equal the text height */} 178 + <div className="h-4 w-full" /> 179 + <div className="absolute inset-0 text-muted-foreground hover:text-foreground"> 180 + <div className="truncate">{report.name}</div> 181 + </div> 182 + <div className="mt-1 text-muted-foreground text-xs"> 183 + {formatDateRange(startedAt, endedAt)}{" "} 184 + <span className="ml-1.5 font-mono text-muted-foreground/70"> 185 + {duration} 186 + </span> 187 + </div> 188 + </div> 189 + </Link> 190 + ); 191 + })} 192 + </div> 193 + </> 194 + ) : null} 195 + {isPinned && !isTouch && ( 196 + <> 197 + <Separator /> 198 + <div className="flex cursor-pointer items-center p-2 text-muted-foreground text-xs"> 199 + <span>Click again to unpin</span> 200 + <Kbd>Esc</Kbd> 201 + </div> 202 + </> 203 + )} 204 + </div> 205 + </HoverCardContent> 206 + </HoverCard> 207 + ); 208 + })} 209 + </div> 210 + ); 211 + } 212 + 213 + function StatusTrackerTriggerAbsolute({ item }: { item: ChartData }) { 214 + const total = item.success + item.degraded + item.info + item.error; 215 + 216 + return STATUS.map((status) => { 217 + const value = item[status as keyof typeof item] as number; 218 + if (value === 0) return null; 219 + const heightPercentage = (value / total) * 100; 220 + return ( 221 + <div 222 + key={`${item.timestamp}-${status}`} 223 + className="w-full transition-all" 224 + style={{ 225 + height: `${heightPercentage}%`, 226 + backgroundColor: chartConfig[status].color, 227 + // IDEA: only for status === "success", make the color less pop to emphasize the other statuses 228 + }} 229 + /> 230 + ); 231 + }); 232 + } 233 + 234 + function StatusTrackerTriggerDominant({ item }: { item: ChartData }) { 235 + const highestPriorityStatus = getHighestPriorityStatus(item); 236 + 237 + return ( 238 + <div 239 + key={`${item.timestamp}-${highestPriorityStatus}`} 240 + className="w-full transition-all" 241 + style={{ 242 + height: "100%", 243 + backgroundColor: chartConfig[highestPriorityStatus].color, 244 + }} 245 + /> 246 + ); 247 + } 248 + 249 + function StatusTrackerContentDuration({ item }: { item: ChartData }) { 250 + return STATUS.map((status) => { 251 + const value = item[status]; 252 + if (value === 0) return null; 253 + 254 + // const percentage = ((value / total) * 100).toFixed(1); 255 + 256 + const now = new Date(); 257 + const duration = formatDistanceStrict( 258 + now, 259 + new Date(now.getTime() + value * 60 * 1000), 260 + ); 261 + 262 + return ( 263 + <div key={status} className="flex items-baseline gap-4"> 264 + <div className="flex items-center gap-2"> 265 + <div 266 + className="h-2.5 w-2.5 rounded-sm" 267 + style={{ 268 + backgroundColor: chartConfig[status].color, 269 + }} 270 + /> 271 + <div className="text-sm">{messages.short[status]}</div> 272 + </div> 273 + <div className="ml-auto font-mono text-muted-foreground text-xs tracking-tight"> 274 + {duration} 275 + </div> 276 + </div> 277 + ); 278 + }); 279 + } 280 + 281 + function StatusTrackerContentDominant({ item }: { item: ChartData }) { 282 + const highestPriorityStatus = getHighestPriorityStatus(item); 283 + return ( 284 + <div className="flex min-w-32 items-baseline gap-4"> 285 + <div className="flex items-center gap-2"> 286 + <div 287 + className="h-2.5 w-2.5 rounded-sm" 288 + style={{ 289 + backgroundColor: chartConfig[highestPriorityStatus].color, 290 + }} 291 + /> 292 + <div className="text-sm">{messages.short[highestPriorityStatus]}</div> 293 + </div> 294 + </div> 295 + ); 296 + } 297 + 298 + function StatusTrackerContentRequests({ item }: { item: ChartData }) { 299 + return STATUS.map((status) => { 300 + const value = item[status]; 301 + if (value === 0) return null; 302 + 303 + return ( 304 + <div key={status} className="flex items-baseline gap-4"> 305 + <div className="flex items-center gap-2"> 306 + <div 307 + className="h-2.5 w-2.5 rounded-sm" 308 + style={{ 309 + backgroundColor: chartConfig[status].color, 310 + }} 311 + /> 312 + <div className="text-sm">{requests[status]}</div> 313 + </div> 314 + <div className="ml-auto font-mono text-muted-foreground text-xs tracking-tight"> 315 + {value} req 316 + </div> 317 + </div> 318 + ); 319 + }); 320 + }
+96
apps/status-page/src/components/status-page/status-updates.tsx
··· 1 + import { Button } from "@/components/ui/button"; 2 + import { 3 + Popover, 4 + PopoverContent, 5 + PopoverTrigger, 6 + } from "@/components/ui/popover"; 7 + import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 8 + import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"; 9 + import { cn } from "@/lib/utils"; 10 + import { Input } from "../ui/input"; 11 + 12 + export function StatusUpdates({ 13 + className, 14 + ...props 15 + }: React.ComponentProps<typeof Button>) { 16 + return ( 17 + <Popover> 18 + <PopoverTrigger asChild> 19 + <Button 20 + size="sm" 21 + variant="outline" 22 + className={cn(className)} 23 + {...props} 24 + > 25 + Get updates 26 + </Button> 27 + </PopoverTrigger> 28 + <PopoverContent align="end" className="overflow-hidden p-0"> 29 + <Tabs defaultValue="email"> 30 + <TabsList className="w-full rounded-none border-b"> 31 + <TabsTrigger value="email">Email</TabsTrigger> 32 + <TabsTrigger value="rss">RSS</TabsTrigger> 33 + <TabsTrigger value="atom">Atom</TabsTrigger> 34 + </TabsList> 35 + <TabsContent value="email" className="flex flex-col gap-2"> 36 + <div className="flex flex-col gap-2 border-b px-2 pb-2"> 37 + <p className="text-foreground text-sm"> 38 + Get email notifications whenever a report has been created or 39 + resolved 40 + </p> 41 + <Input placeholder="notify@me.com" /> 42 + </div> 43 + <div className="px-2 pb-2"> 44 + <Button className="w-full">Subscribe</Button> 45 + </div> 46 + </TabsContent> 47 + <TabsContent value="rss" className="flex flex-col gap-2"> 48 + <div className="border-b px-2 pb-2"> 49 + <Input 50 + placeholder="https://status.openstatus.dev/feed/rss" 51 + className="disabled:opacity-90" 52 + disabled 53 + /> 54 + </div> 55 + <div className="px-2 pb-2"> 56 + <CopyButton 57 + className="w-full" 58 + value="https://status.openstatus.dev/feed/rss" 59 + /> 60 + </div> 61 + </TabsContent> 62 + <TabsContent value="atom" className="flex flex-col gap-2"> 63 + <div className="border-b px-2 pb-2"> 64 + <Input 65 + placeholder="https://status.openstatus.dev/feed/atom" 66 + className="disabled:opacity-90" 67 + disabled 68 + /> 69 + </div> 70 + <div className="px-2 pb-2"> 71 + <CopyButton 72 + className="w-full" 73 + value="https://status.openstatus.dev/feed/atom" 74 + /> 75 + </div> 76 + </TabsContent> 77 + </Tabs> 78 + </PopoverContent> 79 + </Popover> 80 + ); 81 + } 82 + 83 + function CopyButton({ 84 + value, 85 + className, 86 + }: { 87 + value: string; 88 + className?: string; 89 + }) { 90 + const { copy, isCopied } = useCopyToClipboard(); 91 + return ( 92 + <Button size="sm" className={className} onClick={() => copy(value, {})}> 93 + {isCopied ? "Copied" : "Copy link"} 94 + </Button> 95 + ); 96 + }
+233
apps/status-page/src/components/status-page/status.tsx
··· 1 + import { 2 + Tooltip, 3 + TooltipContent, 4 + TooltipProvider, 5 + TooltipTrigger, 6 + } from "@/components/ui/tooltip"; 7 + import { cn } from "@/lib/utils"; 8 + import { UTCDate } from "@date-fns/utc"; 9 + import { format } from "date-fns"; 10 + import { 11 + AlertCircleIcon, 12 + CheckIcon, 13 + TriangleAlertIcon, 14 + WrenchIcon, 15 + } from "lucide-react"; 16 + import { messages } from "./messages"; 17 + 18 + export function Status({ 19 + children, 20 + className, 21 + variant = "success", 22 + ...props 23 + }: React.ComponentProps<"div"> & { 24 + variant?: "success" | "degraded" | "error" | "info"; 25 + }) { 26 + return ( 27 + <div 28 + data-variant={variant} 29 + data-slot="status" 30 + className={cn("group peer flex flex-col gap-8", className)} 31 + {...props} 32 + > 33 + {children} 34 + </div> 35 + ); 36 + } 37 + 38 + export function StatusBrand({ 39 + src, 40 + alt, 41 + className, 42 + ...props 43 + }: React.ComponentProps<"img">) { 44 + return ( 45 + // biome-ignore lint/a11y/useAltText: <explanation> 46 + <img src={src} alt={alt} className={cn("size-8", className)} {...props} /> 47 + ); 48 + } 49 + 50 + export function StatusHeader({ 51 + children, 52 + className, 53 + ...props 54 + }: React.ComponentProps<"div">) { 55 + return ( 56 + <div 57 + data-slot="status-header" 58 + className={cn("@container/status-header", className)} 59 + {...props} 60 + > 61 + {children} 62 + </div> 63 + ); 64 + } 65 + 66 + export function StatusTitle({ 67 + children, 68 + className, 69 + ...props 70 + }: React.ComponentProps<"div">) { 71 + return ( 72 + <div 73 + className={cn( 74 + "font-semibold text-foreground text-lg leading-none", 75 + className, 76 + )} 77 + {...props} 78 + > 79 + {children} 80 + </div> 81 + ); 82 + } 83 + 84 + export function StatusDescription({ 85 + children, 86 + className, 87 + }: React.ComponentProps<"div">) { 88 + return ( 89 + <div className={cn("text-muted-foreground", className)}>{children}</div> 90 + ); 91 + } 92 + 93 + export function StatusContent({ 94 + children, 95 + className, 96 + }: React.ComponentProps<"div">) { 97 + return <div className={cn("flex flex-col gap-3", className)}>{children}</div>; 98 + } 99 + 100 + export function StatusBanner({ className }: React.ComponentProps<"div">) { 101 + return ( 102 + <div 103 + className={cn( 104 + "flex items-center gap-3 rounded-lg border px-3 py-2", 105 + "group-data-[variant=success]:border-success/20 group-data-[variant=success]:bg-success/10", 106 + "group-data-[variant=degraded]:border-warning/20 group-data-[variant=degraded]:bg-warning/10", 107 + "group-data-[variant=error]:border-destructive/20 group-data-[variant=error]:bg-destructive/10", 108 + "group-data-[variant=info]:border-info/20 group-data-[variant=info]:bg-info/10", 109 + className, 110 + )} 111 + > 112 + <StatusIcon className="flex-shrink-0" /> 113 + <div className="flex flex-1 flex-wrap items-center justify-between gap-2"> 114 + <StatusBannerMessage className="font-semibold text-xl" /> 115 + <StatusTimestamp date={new Date()} className="text-xs" /> 116 + </div> 117 + </div> 118 + ); 119 + } 120 + 121 + export function StatusBannerMessage({ 122 + className, 123 + ...props 124 + }: React.ComponentProps<"div">) { 125 + return ( 126 + <div className={cn(className)} {...props}> 127 + <span className="hidden group-data-[variant=success]:block"> 128 + {messages.long.success} 129 + </span> 130 + <span className="hidden group-data-[variant=degraded]:block"> 131 + {messages.long.degraded} 132 + </span> 133 + <span className="hidden group-data-[variant=error]:block"> 134 + {messages.long.error} 135 + </span> 136 + <span className="hidden group-data-[variant=info]:block"> 137 + {messages.long.info} 138 + </span> 139 + </div> 140 + ); 141 + } 142 + 143 + export function StatusIcon({ 144 + className, 145 + ...props 146 + }: React.ComponentProps<"div">) { 147 + return ( 148 + <div 149 + className={cn( 150 + "flex size-7 items-center justify-center rounded-full bg-muted text-background [&>svg]:size-4", 151 + "group-data-[variant=success]:bg-success", 152 + "group-data-[variant=degraded]:bg-warning", 153 + "group-data-[variant=error]:bg-destructive", 154 + "group-data-[variant=info]:bg-info", 155 + className, 156 + )} 157 + {...props} 158 + > 159 + <CheckIcon className="hidden group-data-[variant=success]:block" /> 160 + <TriangleAlertIcon className="hidden group-data-[variant=degraded]:block" /> 161 + <AlertCircleIcon className="hidden group-data-[variant=error]:block" /> 162 + <WrenchIcon className="hidden group-data-[variant=info]:block" /> 163 + </div> 164 + ); 165 + } 166 + 167 + export function StatusTimestamp({ 168 + date, 169 + className, 170 + ...props 171 + }: React.ComponentProps<typeof TooltipTrigger> & { date: Date }) { 172 + return ( 173 + <TooltipProvider> 174 + <Tooltip> 175 + {/* TODO: add outline focus */} 176 + <TooltipTrigger 177 + className={cn( 178 + "font-mono text-muted-foreground underline decoration-muted-foreground/30 decoration-dashed underline-offset-4", 179 + className, 180 + )} 181 + {...props} 182 + > 183 + {format(new UTCDate(date), "LLL dd, y HH:mm (z)")} 184 + </TooltipTrigger> 185 + <TooltipContent> 186 + <p className="font-mono">{format(date, "LLL dd, y HH:mm (z)")}</p> 187 + </TooltipContent> 188 + </Tooltip> 189 + </TooltipProvider> 190 + ); 191 + } 192 + 193 + export function StatusEmptyState({ 194 + children, 195 + className, 196 + ...props 197 + }: React.ComponentProps<"div">) { 198 + return ( 199 + <div 200 + className={cn( 201 + "flex flex-col items-center justify-center gap-0.5 rounded-lg border border-dashed px-3 py-2 text-center", 202 + className, 203 + )} 204 + {...props} 205 + > 206 + {children} 207 + </div> 208 + ); 209 + } 210 + 211 + export function StatusEmptyStateTitle({ 212 + children, 213 + className, 214 + ...props 215 + }: React.ComponentProps<"div">) { 216 + return ( 217 + <div className={cn("font-medium", className)} {...props}> 218 + {children} 219 + </div> 220 + ); 221 + } 222 + 223 + export function StatusEmptyStateDescription({ 224 + children, 225 + className, 226 + ...props 227 + }: React.ComponentProps<"div">) { 228 + return ( 229 + <div className={cn("text-muted-foreground text-sm", className)} {...props}> 230 + {children} 231 + </div> 232 + ); 233 + }
+84
apps/status-page/src/components/status-page/utils.ts
··· 1 + import type { ChartConfig } from "@/components/ui/chart"; 2 + import { VARIANT } from "./floating-button"; 3 + 4 + export const chartData = Array.from({ length: 45 }, (_, i) => { 5 + const date = new Date(); 6 + date.setDate(date.getDate() - i); 7 + 8 + // Simulate realistic daily status distribution that sums to 1440 minutes 9 + let error = 0; 10 + let degraded = 0; 11 + let success = 1440; // Start with all minutes as ok 12 + let info = 0; 13 + 14 + // Simulate some incidents on certain days 15 + if (i === 3) { 16 + // Day 3: Major incident for 2 hours (120 minutes) 17 + error = 120; 18 + success -= error; 19 + } else if (i === 16) { 20 + // Day 16: Degraded performance for 4 hours (240 minutes) 21 + degraded = 240; 22 + success -= degraded; 23 + } else if (i === 8) { 24 + // Day 8: Brief outage (30 minutes) + some degraded performance (60 minutes) 25 + error = 30; 26 + degraded = 60; 27 + success -= error + degraded; 28 + } else if (i === 13) { 29 + info = 120; 30 + success -= info; 31 + } else if (i === 22) { 32 + // Day 22: Extended degraded performance (6 hours = 360 minutes) 33 + degraded = 360; 34 + success -= degraded; 35 + } else if (Math.random() > 0.85) { 36 + // Random minor issues on some days (5-15 minutes of degraded performance) 37 + degraded = Math.floor(Math.random() * 10) + 5; 38 + success -= degraded; 39 + } 40 + 41 + return { 42 + timestamp: date.getTime(), 43 + info, 44 + degraded, 45 + error, 46 + success, 47 + }; 48 + }).reverse(); 49 + 50 + export type ChartData = (typeof chartData)[number]; 51 + 52 + export const chartConfig = { 53 + success: { 54 + label: "success", 55 + color: "var(--success)", 56 + }, 57 + degraded: { 58 + label: "degraded", 59 + color: "var(--warning)", 60 + }, 61 + error: { 62 + label: "error", 63 + color: "var(--destructive)", 64 + }, 65 + info: { 66 + label: "info", 67 + color: "var(--info)", 68 + }, 69 + } satisfies ChartConfig; 70 + 71 + export const PRIORITY = { 72 + error: 3, 73 + degraded: 2, 74 + info: 1, 75 + success: 0, 76 + } as const; // satisfies Record<XXX, number>; 77 + 78 + export function getHighestPriorityStatus(item: ChartData) { 79 + return ( 80 + VARIANT.filter((status) => item[status] > 0).sort( 81 + (a, b) => PRIORITY[b] - PRIORITY[a], 82 + )[0] || "success" 83 + ); 84 + }
+14
apps/status-page/src/components/tailwind-indicator.tsx
··· 1 + export function TailwindIndicator() { 2 + if (process.env.NODE_ENV === "production") return null; 3 + 4 + return ( 5 + <div className="fixed bottom-1 left-1 z-50 flex h-6 w-6 items-center justify-center rounded-full bg-foreground p-3 font-mono text-background text-xs"> 6 + <div className="block sm:hidden">xs</div> 7 + <div className="hidden sm:block md:hidden">sm</div> 8 + <div className="hidden md:block lg:hidden">md</div> 9 + <div className="hidden lg:block xl:hidden">lg</div> 10 + <div className="hidden xl:block 2xl:hidden">xl</div> 11 + <div className="hidden 2xl:block">2xl</div> 12 + </div> 13 + ); 14 + }
+11
apps/status-page/src/components/theme-provider.tsx
··· 1 + "use client"; 2 + 3 + import { ThemeProvider as NextThemesProvider } from "next-themes"; 4 + import type * as React from "react"; 5 + 6 + export function ThemeProvider({ 7 + children, 8 + ...props 9 + }: React.ComponentProps<typeof NextThemesProvider>) { 10 + return <NextThemesProvider {...props}>{children}</NextThemesProvider>; 11 + }
+67
apps/status-page/src/components/theme-toggle.tsx
··· 1 + "use client"; 2 + 3 + import { useTheme } from "next-themes"; 4 + import type * as React from "react"; 5 + 6 + import { 7 + Select, 8 + SelectContent, 9 + SelectItem, 10 + SelectTrigger, 11 + SelectValue, 12 + } from "@/components/ui/select"; 13 + import { cn } from "@/lib/utils"; 14 + import { Laptop, Moon, Sun } from "lucide-react"; 15 + import { useState } from "react"; 16 + import { useEffect } from "react"; 17 + 18 + export function ThemeToggle({ 19 + className, 20 + ...props 21 + }: React.ComponentProps<typeof SelectTrigger>) { 22 + const { setTheme, theme } = useTheme(); 23 + const [mounted, setMounted] = useState(false); 24 + 25 + useEffect(() => { 26 + setMounted(true); 27 + }, []); 28 + 29 + // NOTE: hydration error if we don't do this 30 + if (!mounted) { 31 + return ( 32 + <Select> 33 + <SelectTrigger className={cn("w-[180px]", className)} {...props}> 34 + <SelectValue placeholder="Select theme" /> 35 + </SelectTrigger> 36 + </Select> 37 + ); 38 + } 39 + 40 + return ( 41 + <Select value={theme} onValueChange={setTheme}> 42 + <SelectTrigger className={cn("w-[180px]", className)} {...props}> 43 + <SelectValue defaultValue={theme} placeholder="Select theme" /> 44 + </SelectTrigger> 45 + <SelectContent> 46 + <SelectItem value="light"> 47 + <div className="flex items-center gap-2"> 48 + <Sun className="h-4 w-4" /> 49 + <span>Light</span> 50 + </div> 51 + </SelectItem> 52 + <SelectItem value="dark"> 53 + <div className="flex items-center gap-2"> 54 + <Moon className="h-4 w-4" /> 55 + <span>Dark</span> 56 + </div> 57 + </SelectItem> 58 + <SelectItem value="system"> 59 + <div className="flex items-center gap-2"> 60 + <Laptop className="h-4 w-4" /> 61 + <span>System</span> 62 + </div> 63 + </SelectItem> 64 + </SelectContent> 65 + </Select> 66 + ); 67 + }
+157
apps/status-page/src/components/ui/alert-dialog.tsx
··· 1 + "use client"; 2 + 3 + import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; 4 + import type * as React from "react"; 5 + 6 + import { buttonVariants } from "@/components/ui/button"; 7 + import { cn } from "@/lib/utils"; 8 + 9 + function AlertDialog({ 10 + ...props 11 + }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) { 12 + return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />; 13 + } 14 + 15 + function AlertDialogTrigger({ 16 + ...props 17 + }: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) { 18 + return ( 19 + <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} /> 20 + ); 21 + } 22 + 23 + function AlertDialogPortal({ 24 + ...props 25 + }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) { 26 + return ( 27 + <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} /> 28 + ); 29 + } 30 + 31 + function AlertDialogOverlay({ 32 + className, 33 + ...props 34 + }: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) { 35 + return ( 36 + <AlertDialogPrimitive.Overlay 37 + data-slot="alert-dialog-overlay" 38 + className={cn( 39 + "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=open]:animate-in", 40 + className, 41 + )} 42 + {...props} 43 + /> 44 + ); 45 + } 46 + 47 + function AlertDialogContent({ 48 + className, 49 + ...props 50 + }: React.ComponentProps<typeof AlertDialogPrimitive.Content>) { 51 + return ( 52 + <AlertDialogPortal> 53 + <AlertDialogOverlay /> 54 + <AlertDialogPrimitive.Content 55 + data-slot="alert-dialog-content" 56 + className={cn( 57 + "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 data-[state=closed]:animate-out data-[state=open]:animate-in sm:max-w-lg", 58 + className, 59 + )} 60 + {...props} 61 + /> 62 + </AlertDialogPortal> 63 + ); 64 + } 65 + 66 + function AlertDialogHeader({ 67 + className, 68 + ...props 69 + }: React.ComponentProps<"div">) { 70 + return ( 71 + <div 72 + data-slot="alert-dialog-header" 73 + className={cn("flex flex-col gap-2 text-center sm:text-left", className)} 74 + {...props} 75 + /> 76 + ); 77 + } 78 + 79 + function AlertDialogFooter({ 80 + className, 81 + ...props 82 + }: React.ComponentProps<"div">) { 83 + return ( 84 + <div 85 + data-slot="alert-dialog-footer" 86 + className={cn( 87 + "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", 88 + className, 89 + )} 90 + {...props} 91 + /> 92 + ); 93 + } 94 + 95 + function AlertDialogTitle({ 96 + className, 97 + ...props 98 + }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) { 99 + return ( 100 + <AlertDialogPrimitive.Title 101 + data-slot="alert-dialog-title" 102 + className={cn("font-semibold text-lg", className)} 103 + {...props} 104 + /> 105 + ); 106 + } 107 + 108 + function AlertDialogDescription({ 109 + className, 110 + ...props 111 + }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) { 112 + return ( 113 + <AlertDialogPrimitive.Description 114 + data-slot="alert-dialog-description" 115 + className={cn("text-muted-foreground text-sm", className)} 116 + {...props} 117 + /> 118 + ); 119 + } 120 + 121 + function AlertDialogAction({ 122 + className, 123 + ...props 124 + }: React.ComponentProps<typeof AlertDialogPrimitive.Action>) { 125 + return ( 126 + <AlertDialogPrimitive.Action 127 + className={cn(buttonVariants(), className)} 128 + {...props} 129 + /> 130 + ); 131 + } 132 + 133 + function AlertDialogCancel({ 134 + className, 135 + ...props 136 + }: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) { 137 + return ( 138 + <AlertDialogPrimitive.Cancel 139 + className={cn(buttonVariants({ variant: "outline" }), className)} 140 + {...props} 141 + /> 142 + ); 143 + } 144 + 145 + export { 146 + AlertDialog, 147 + AlertDialogPortal, 148 + AlertDialogOverlay, 149 + AlertDialogTrigger, 150 + AlertDialogContent, 151 + AlertDialogHeader, 152 + AlertDialogFooter, 153 + AlertDialogTitle, 154 + AlertDialogDescription, 155 + AlertDialogAction, 156 + AlertDialogCancel, 157 + };
+66
apps/status-page/src/components/ui/alert.tsx
··· 1 + import { type VariantProps, cva } from "class-variance-authority"; 2 + import type * as React from "react"; 3 + 4 + import { cn } from "@/lib/utils"; 5 + 6 + const alertVariants = cva( 7 + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", 8 + { 9 + variants: { 10 + variant: { 11 + default: "bg-card text-card-foreground", 12 + destructive: 13 + "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", 14 + }, 15 + }, 16 + defaultVariants: { 17 + variant: "default", 18 + }, 19 + }, 20 + ); 21 + 22 + function Alert({ 23 + className, 24 + variant, 25 + ...props 26 + }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) { 27 + return ( 28 + <div 29 + data-slot="alert" 30 + role="alert" 31 + className={cn(alertVariants({ variant }), className)} 32 + {...props} 33 + /> 34 + ); 35 + } 36 + 37 + function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { 38 + return ( 39 + <div 40 + data-slot="alert-title" 41 + className={cn( 42 + "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", 43 + className, 44 + )} 45 + {...props} 46 + /> 47 + ); 48 + } 49 + 50 + function AlertDescription({ 51 + className, 52 + ...props 53 + }: React.ComponentProps<"div">) { 54 + return ( 55 + <div 56 + data-slot="alert-description" 57 + className={cn( 58 + "col-start-2 grid justify-items-start gap-1 text-muted-foreground text-sm [&_p]:leading-relaxed", 59 + className, 60 + )} 61 + {...props} 62 + /> 63 + ); 64 + } 65 + 66 + export { Alert, AlertTitle, AlertDescription };
+53
apps/status-page/src/components/ui/avatar.tsx
··· 1 + "use client"; 2 + 3 + import * as AvatarPrimitive from "@radix-ui/react-avatar"; 4 + import type * as React from "react"; 5 + 6 + import { cn } from "@/lib/utils"; 7 + 8 + function Avatar({ 9 + className, 10 + ...props 11 + }: React.ComponentProps<typeof AvatarPrimitive.Root>) { 12 + return ( 13 + <AvatarPrimitive.Root 14 + data-slot="avatar" 15 + className={cn( 16 + "relative flex size-8 shrink-0 overflow-hidden rounded-full", 17 + className, 18 + )} 19 + {...props} 20 + /> 21 + ); 22 + } 23 + 24 + function AvatarImage({ 25 + className, 26 + ...props 27 + }: React.ComponentProps<typeof AvatarPrimitive.Image>) { 28 + return ( 29 + <AvatarPrimitive.Image 30 + data-slot="avatar-image" 31 + className={cn("aspect-square size-full", className)} 32 + {...props} 33 + /> 34 + ); 35 + } 36 + 37 + function AvatarFallback({ 38 + className, 39 + ...props 40 + }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) { 41 + return ( 42 + <AvatarPrimitive.Fallback 43 + data-slot="avatar-fallback" 44 + className={cn( 45 + "flex size-full items-center justify-center rounded-full bg-muted", 46 + className, 47 + )} 48 + {...props} 49 + /> 50 + ); 51 + } 52 + 53 + export { Avatar, AvatarImage, AvatarFallback };
+43
apps/status-page/src/components/ui/badge.tsx
··· 1 + import { Slot } from "@radix-ui/react-slot"; 2 + import { type VariantProps, cva } from "class-variance-authority"; 3 + import type * as React from "react"; 4 + 5 + import { cn } from "@/lib/utils"; 6 + 7 + const badgeVariants = cva( 8 + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", 9 + { 10 + variants: { 11 + variant: { 12 + default: 13 + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", 14 + secondary: 15 + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", 16 + destructive: 17 + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 18 + outline: 19 + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", 20 + }, 21 + }, 22 + defaultVariants: { 23 + variant: "default", 24 + }, 25 + }, 26 + ); 27 + 28 + export type BadgeProps = React.ComponentProps<"span"> & 29 + VariantProps<typeof badgeVariants> & { asChild?: boolean }; 30 + 31 + function Badge({ className, variant, asChild = false, ...props }: BadgeProps) { 32 + const Comp = asChild ? Slot : "span"; 33 + 34 + return ( 35 + <Comp 36 + data-slot="badge" 37 + className={cn(badgeVariants({ variant }), className)} 38 + {...props} 39 + /> 40 + ); 41 + } 42 + 43 + export { Badge, badgeVariants };
+109
apps/status-page/src/components/ui/breadcrumb.tsx
··· 1 + import { Slot } from "@radix-ui/react-slot"; 2 + import { ChevronRight, MoreHorizontal } from "lucide-react"; 3 + import type * as React from "react"; 4 + 5 + import { cn } from "@/lib/utils"; 6 + 7 + function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { 8 + return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />; 9 + } 10 + 11 + function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) { 12 + return ( 13 + <ol 14 + data-slot="breadcrumb-list" 15 + className={cn( 16 + "flex flex-wrap items-center gap-1.5 break-words text-muted-foreground text-sm sm:gap-2.5", 17 + className, 18 + )} 19 + {...props} 20 + /> 21 + ); 22 + } 23 + 24 + function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) { 25 + return ( 26 + <li 27 + data-slot="breadcrumb-item" 28 + className={cn("inline-flex items-center gap-1.5", className)} 29 + {...props} 30 + /> 31 + ); 32 + } 33 + 34 + function BreadcrumbLink({ 35 + asChild, 36 + className, 37 + ...props 38 + }: React.ComponentProps<"a"> & { 39 + asChild?: boolean; 40 + }) { 41 + const Comp = asChild ? Slot : "a"; 42 + 43 + return ( 44 + <Comp 45 + data-slot="breadcrumb-link" 46 + className={cn("transition-colors hover:text-foreground", className)} 47 + {...props} 48 + /> 49 + ); 50 + } 51 + 52 + function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) { 53 + return ( 54 + <span 55 + data-slot="breadcrumb-page" 56 + role="link" 57 + aria-disabled="true" 58 + aria-current="page" 59 + className={cn("font-normal text-foreground", className)} 60 + {...props} 61 + /> 62 + ); 63 + } 64 + 65 + function BreadcrumbSeparator({ 66 + children, 67 + className, 68 + ...props 69 + }: React.ComponentProps<"li">) { 70 + return ( 71 + <li 72 + data-slot="breadcrumb-separator" 73 + role="presentation" 74 + aria-hidden="true" 75 + className={cn("[&>svg]:size-3.5", className)} 76 + {...props} 77 + > 78 + {children ?? <ChevronRight />} 79 + </li> 80 + ); 81 + } 82 + 83 + function BreadcrumbEllipsis({ 84 + className, 85 + ...props 86 + }: React.ComponentProps<"span">) { 87 + return ( 88 + <span 89 + data-slot="breadcrumb-ellipsis" 90 + role="presentation" 91 + aria-hidden="true" 92 + className={cn("flex size-9 items-center justify-center", className)} 93 + {...props} 94 + > 95 + <MoreHorizontal className="size-4" /> 96 + <span className="sr-only">More</span> 97 + </span> 98 + ); 99 + } 100 + 101 + export { 102 + Breadcrumb, 103 + BreadcrumbList, 104 + BreadcrumbItem, 105 + BreadcrumbLink, 106 + BreadcrumbPage, 107 + BreadcrumbSeparator, 108 + BreadcrumbEllipsis, 109 + };
+59
apps/status-page/src/components/ui/button.tsx
··· 1 + import { Slot } from "@radix-ui/react-slot"; 2 + import { type VariantProps, cva } from "class-variance-authority"; 3 + import type * as React from "react"; 4 + 5 + import { cn } from "@/lib/utils"; 6 + 7 + const buttonVariants = cva( 8 + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 + { 10 + variants: { 11 + variant: { 12 + default: 13 + "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", 14 + destructive: 15 + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 16 + outline: 17 + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 18 + secondary: 19 + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 20 + ghost: 21 + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 22 + link: "text-primary underline-offset-4 hover:underline", 23 + }, 24 + size: { 25 + default: "h-9 px-4 py-2 has-[>svg]:px-3", 26 + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 27 + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 28 + icon: "size-9", 29 + }, 30 + }, 31 + defaultVariants: { 32 + variant: "default", 33 + size: "default", 34 + }, 35 + }, 36 + ); 37 + 38 + function Button({ 39 + className, 40 + variant, 41 + size, 42 + asChild = false, 43 + ...props 44 + }: React.ComponentProps<"button"> & 45 + VariantProps<typeof buttonVariants> & { 46 + asChild?: boolean; 47 + }) { 48 + const Comp = asChild ? Slot : "button"; 49 + 50 + return ( 51 + <Comp 52 + data-slot="button" 53 + className={cn(buttonVariants({ variant, size, className }))} 54 + {...props} 55 + /> 56 + ); 57 + } 58 + 59 + export { Button, buttonVariants };
+75
apps/status-page/src/components/ui/calendar.tsx
··· 1 + "use client"; 2 + 3 + import { ChevronLeft, ChevronRight } from "lucide-react"; 4 + import type * as React from "react"; 5 + import { DayPicker } from "react-day-picker"; 6 + 7 + import { buttonVariants } from "@/components/ui/button"; 8 + import { cn } from "@/lib/utils"; 9 + 10 + function Calendar({ 11 + className, 12 + classNames, 13 + showOutsideDays = true, 14 + ...props 15 + }: React.ComponentProps<typeof DayPicker>) { 16 + return ( 17 + <DayPicker 18 + showOutsideDays={showOutsideDays} 19 + className={cn("p-3", className)} 20 + classNames={{ 21 + months: "flex flex-col sm:flex-row gap-2", 22 + month: "flex flex-col gap-4", 23 + caption: "flex justify-center pt-1 relative items-center w-full", 24 + caption_label: "text-sm font-medium", 25 + nav: "flex items-center gap-1", 26 + nav_button: cn( 27 + buttonVariants({ variant: "outline" }), 28 + "size-7 bg-transparent p-0 opacity-50 hover:opacity-100", 29 + ), 30 + nav_button_previous: "absolute left-1", 31 + nav_button_next: "absolute right-1", 32 + table: "w-full border-collapse space-x-1", 33 + head_row: "flex", 34 + head_cell: 35 + "text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]", 36 + row: "flex w-full mt-2", 37 + cell: cn( 38 + "relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md", 39 + props.mode === "range" 40 + ? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md" 41 + : "[&:has([aria-selected])]:rounded-md", 42 + ), 43 + day: cn( 44 + buttonVariants({ variant: "ghost" }), 45 + "size-8 p-0 font-normal aria-selected:opacity-100", 46 + ), 47 + day_range_start: 48 + "day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground", 49 + day_range_end: 50 + "day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground", 51 + day_selected: 52 + "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", 53 + day_today: "bg-accent text-accent-foreground", 54 + day_outside: 55 + "day-outside text-muted-foreground aria-selected:text-muted-foreground", 56 + day_disabled: "text-muted-foreground opacity-50", 57 + day_range_middle: 58 + "aria-selected:bg-accent aria-selected:text-accent-foreground", 59 + day_hidden: "invisible", 60 + ...classNames, 61 + }} 62 + components={{ 63 + IconLeft: ({ className, ...props }) => ( 64 + <ChevronLeft className={cn("size-4", className)} {...props} /> 65 + ), 66 + IconRight: ({ className, ...props }) => ( 67 + <ChevronRight className={cn("size-4", className)} {...props} /> 68 + ), 69 + }} 70 + {...props} 71 + /> 72 + ); 73 + } 74 + 75 + export { Calendar };
+92
apps/status-page/src/components/ui/card.tsx
··· 1 + import type * as React from "react"; 2 + 3 + import { cn } from "@/lib/utils"; 4 + 5 + function Card({ className, ...props }: React.ComponentProps<"div">) { 6 + return ( 7 + <div 8 + data-slot="card" 9 + className={cn( 10 + "flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm", 11 + className, 12 + )} 13 + {...props} 14 + /> 15 + ); 16 + } 17 + 18 + function CardHeader({ className, ...props }: React.ComponentProps<"div">) { 19 + return ( 20 + <div 21 + data-slot="card-header" 22 + className={cn( 23 + "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6", 24 + className, 25 + )} 26 + {...props} 27 + /> 28 + ); 29 + } 30 + 31 + function CardTitle({ className, ...props }: React.ComponentProps<"div">) { 32 + return ( 33 + <div 34 + data-slot="card-title" 35 + className={cn("font-semibold leading-none", className)} 36 + {...props} 37 + /> 38 + ); 39 + } 40 + 41 + function CardDescription({ className, ...props }: React.ComponentProps<"div">) { 42 + return ( 43 + <div 44 + data-slot="card-description" 45 + className={cn("text-muted-foreground text-sm", className)} 46 + {...props} 47 + /> 48 + ); 49 + } 50 + 51 + function CardAction({ className, ...props }: React.ComponentProps<"div">) { 52 + return ( 53 + <div 54 + data-slot="card-action" 55 + className={cn( 56 + "col-start-2 row-span-2 row-start-1 self-start justify-self-end", 57 + className, 58 + )} 59 + {...props} 60 + /> 61 + ); 62 + } 63 + 64 + function CardContent({ className, ...props }: React.ComponentProps<"div">) { 65 + return ( 66 + <div 67 + data-slot="card-content" 68 + className={cn("px-6", className)} 69 + {...props} 70 + /> 71 + ); 72 + } 73 + 74 + function CardFooter({ className, ...props }: React.ComponentProps<"div">) { 75 + return ( 76 + <div 77 + data-slot="card-footer" 78 + className={cn("flex items-center px-6 [.border-t]:pt-6", className)} 79 + {...props} 80 + /> 81 + ); 82 + } 83 + 84 + export { 85 + Card, 86 + CardHeader, 87 + CardFooter, 88 + CardTitle, 89 + CardAction, 90 + CardDescription, 91 + CardContent, 92 + };
+358
apps/status-page/src/components/ui/chart.tsx
··· 1 + "use client"; 2 + 3 + import * as React from "react"; 4 + import * as RechartsPrimitive from "recharts"; 5 + 6 + import { cn } from "@/lib/utils"; 7 + 8 + // Format: { THEME_NAME: CSS_SELECTOR } 9 + const THEMES = { light: "", dark: ".dark" } as const; 10 + 11 + export type ChartConfig = { 12 + [k in string]: { 13 + label?: React.ReactNode; 14 + icon?: React.ComponentType; 15 + } & ( 16 + | { color?: string; theme?: never } 17 + | { color?: never; theme: Record<keyof typeof THEMES, string> } 18 + ); 19 + }; 20 + 21 + type ChartContextProps = { 22 + config: ChartConfig; 23 + }; 24 + 25 + const ChartContext = React.createContext<ChartContextProps | null>(null); 26 + 27 + export function useChart() { 28 + const context = React.useContext(ChartContext); 29 + 30 + if (!context) { 31 + throw new Error("useChart must be used within a <ChartContainer />"); 32 + } 33 + 34 + return context; 35 + } 36 + 37 + function ChartContainer({ 38 + id, 39 + className, 40 + children, 41 + config, 42 + ...props 43 + }: React.ComponentProps<"div"> & { 44 + config: ChartConfig; 45 + children: React.ComponentProps< 46 + typeof RechartsPrimitive.ResponsiveContainer 47 + >["children"]; 48 + }) { 49 + const uniqueId = React.useId(); 50 + const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`; 51 + 52 + return ( 53 + <ChartContext.Provider value={{ config }}> 54 + <div 55 + data-slot="chart" 56 + data-chart={chartId} 57 + className={cn( 58 + "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-hidden [&_.recharts-surface]:outline-hidden", 59 + className, 60 + )} 61 + {...props} 62 + > 63 + <ChartStyle id={chartId} config={config} /> 64 + <RechartsPrimitive.ResponsiveContainer> 65 + {children} 66 + </RechartsPrimitive.ResponsiveContainer> 67 + </div> 68 + </ChartContext.Provider> 69 + ); 70 + } 71 + 72 + const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { 73 + const colorConfig = Object.entries(config).filter( 74 + ([, config]) => config.theme || config.color, 75 + ); 76 + 77 + if (!colorConfig.length) { 78 + return null; 79 + } 80 + 81 + return ( 82 + <style 83 + // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation> 84 + dangerouslySetInnerHTML={{ 85 + __html: Object.entries(THEMES) 86 + .map( 87 + ([theme, prefix]) => ` 88 + ${prefix} [data-chart=${id}] { 89 + ${colorConfig 90 + .map(([key, itemConfig]) => { 91 + const color = 92 + itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || 93 + itemConfig.color; 94 + return color ? ` --color-${key}: ${color};` : null; 95 + }) 96 + .join("\n")} 97 + } 98 + `, 99 + ) 100 + .join("\n"), 101 + }} 102 + /> 103 + ); 104 + }; 105 + 106 + const ChartTooltip = RechartsPrimitive.Tooltip; 107 + 108 + function ChartTooltipContent({ 109 + active, 110 + payload, 111 + className, 112 + indicator = "dot", 113 + hideLabel = false, 114 + hideIndicator = false, 115 + label, 116 + labelFormatter, 117 + labelClassName, 118 + formatter, 119 + color, 120 + nameKey, 121 + labelKey, 122 + }: React.ComponentProps<typeof RechartsPrimitive.Tooltip> & 123 + React.ComponentProps<"div"> & { 124 + hideLabel?: boolean; 125 + hideIndicator?: boolean; 126 + indicator?: "line" | "dot" | "dashed"; 127 + nameKey?: string; 128 + labelKey?: string; 129 + }) { 130 + const { config } = useChart(); 131 + 132 + const tooltipLabel = React.useMemo(() => { 133 + if (hideLabel || !payload?.length) { 134 + return null; 135 + } 136 + 137 + const [item] = payload; 138 + const key = `${labelKey || item?.dataKey || item?.name || "value"}`; 139 + const itemConfig = getPayloadConfigFromPayload(config, item, key); 140 + const value = 141 + !labelKey && typeof label === "string" 142 + ? config[label as keyof typeof config]?.label || label 143 + : itemConfig?.label; 144 + 145 + if (labelFormatter) { 146 + return ( 147 + <div className={cn("font-medium", labelClassName)}> 148 + {labelFormatter(value, payload)} 149 + </div> 150 + ); 151 + } 152 + 153 + if (!value) { 154 + return null; 155 + } 156 + 157 + return <div className={cn("font-medium", labelClassName)}>{value}</div>; 158 + }, [ 159 + label, 160 + labelFormatter, 161 + payload, 162 + hideLabel, 163 + labelClassName, 164 + config, 165 + labelKey, 166 + ]); 167 + 168 + if (!active || !payload?.length) { 169 + return null; 170 + } 171 + 172 + const nestLabel = payload.length === 1 && indicator !== "dot"; 173 + 174 + return ( 175 + <div 176 + className={cn( 177 + "grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl", 178 + className, 179 + )} 180 + > 181 + {!nestLabel ? tooltipLabel : null} 182 + <div className="grid gap-1.5"> 183 + {payload 184 + .filter((item) => item.type !== "none") 185 + .map((item, index) => { 186 + const key = `${nameKey || item.name || item.dataKey || "value"}`; 187 + const itemConfig = getPayloadConfigFromPayload(config, item, key); 188 + const indicatorColor = color || item.payload.fill || item.color; 189 + 190 + return ( 191 + <div 192 + key={item.dataKey} 193 + className={cn( 194 + "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground", 195 + indicator === "dot" && "items-center", 196 + )} 197 + > 198 + {formatter && item?.value !== undefined && item.name ? ( 199 + formatter(item.value, item.name, item, index, item.payload) 200 + ) : ( 201 + <> 202 + {itemConfig?.icon ? ( 203 + <itemConfig.icon /> 204 + ) : ( 205 + !hideIndicator && ( 206 + <div 207 + className={cn( 208 + "shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)", 209 + { 210 + "h-2.5 w-2.5": indicator === "dot", 211 + "w-1": indicator === "line", 212 + "w-0 border-[1.5px] border-dashed bg-transparent": 213 + indicator === "dashed", 214 + "my-0.5": nestLabel && indicator === "dashed", 215 + }, 216 + )} 217 + style={ 218 + { 219 + "--color-bg": indicatorColor, 220 + "--color-border": indicatorColor, 221 + } as React.CSSProperties 222 + } 223 + /> 224 + ) 225 + )} 226 + <div 227 + className={cn( 228 + "flex flex-1 justify-between leading-none", 229 + nestLabel ? "items-end" : "items-center", 230 + )} 231 + > 232 + <div className="grid gap-1.5"> 233 + {nestLabel ? tooltipLabel : null} 234 + <span className="text-muted-foreground"> 235 + {itemConfig?.label || item.name} 236 + </span> 237 + </div> 238 + {item.value && ( 239 + <span className="font-medium font-mono text-foreground tabular-nums"> 240 + {item.value.toLocaleString()} 241 + </span> 242 + )} 243 + </div> 244 + </> 245 + )} 246 + </div> 247 + ); 248 + })} 249 + </div> 250 + </div> 251 + ); 252 + } 253 + 254 + const ChartLegend = RechartsPrimitive.Legend; 255 + 256 + function ChartLegendContent({ 257 + className, 258 + hideIcon = false, 259 + payload, 260 + verticalAlign = "bottom", 261 + nameKey, 262 + }: React.ComponentProps<"div"> & 263 + Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & { 264 + hideIcon?: boolean; 265 + nameKey?: string; 266 + }) { 267 + const { config } = useChart(); 268 + 269 + if (!payload?.length) { 270 + return null; 271 + } 272 + 273 + return ( 274 + <div 275 + className={cn( 276 + "flex items-center justify-center gap-4", 277 + verticalAlign === "top" ? "pb-3" : "pt-3", 278 + className, 279 + )} 280 + > 281 + {payload 282 + .filter((item) => item.type !== "none") 283 + .map((item) => { 284 + const key = `${nameKey || item.dataKey || "value"}`; 285 + const itemConfig = getPayloadConfigFromPayload(config, item, key); 286 + 287 + return ( 288 + <div 289 + key={item.value} 290 + className={cn( 291 + "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground", 292 + )} 293 + > 294 + {itemConfig?.icon && !hideIcon ? ( 295 + <itemConfig.icon /> 296 + ) : ( 297 + <div 298 + className="h-2 w-2 shrink-0 rounded-[2px]" 299 + style={{ 300 + backgroundColor: item.color, 301 + }} 302 + /> 303 + )} 304 + {itemConfig?.label} 305 + </div> 306 + ); 307 + })} 308 + </div> 309 + ); 310 + } 311 + 312 + // Helper to extract item config from a payload. 313 + export function getPayloadConfigFromPayload( 314 + config: ChartConfig, 315 + payload: unknown, 316 + key: string, 317 + ) { 318 + if (typeof payload !== "object" || payload === null) { 319 + return undefined; 320 + } 321 + 322 + const payloadPayload = 323 + "payload" in payload && 324 + typeof payload.payload === "object" && 325 + payload.payload !== null 326 + ? payload.payload 327 + : undefined; 328 + 329 + let configLabelKey: string = key; 330 + 331 + if ( 332 + key in payload && 333 + typeof payload[key as keyof typeof payload] === "string" 334 + ) { 335 + configLabelKey = payload[key as keyof typeof payload] as string; 336 + } else if ( 337 + payloadPayload && 338 + key in payloadPayload && 339 + typeof payloadPayload[key as keyof typeof payloadPayload] === "string" 340 + ) { 341 + configLabelKey = payloadPayload[ 342 + key as keyof typeof payloadPayload 343 + ] as string; 344 + } 345 + 346 + return configLabelKey in config 347 + ? config[configLabelKey] 348 + : config[key as keyof typeof config]; 349 + } 350 + 351 + export { 352 + ChartContainer, 353 + ChartTooltip, 354 + ChartTooltipContent, 355 + ChartLegend, 356 + ChartLegendContent, 357 + ChartStyle, 358 + };
+32
apps/status-page/src/components/ui/checkbox.tsx
··· 1 + "use client"; 2 + 3 + import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; 4 + import { CheckIcon } from "lucide-react"; 5 + import type * as React from "react"; 6 + 7 + import { cn } from "@/lib/utils"; 8 + 9 + function Checkbox({ 10 + className, 11 + ...props 12 + }: React.ComponentProps<typeof CheckboxPrimitive.Root>) { 13 + return ( 14 + <CheckboxPrimitive.Root 15 + data-slot="checkbox" 16 + className={cn( 17 + "peer size-4 shrink-0 rounded-[4px] border border-input shadow-xs outline-none transition-shadow focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:bg-input/30 dark:data-[state=checked]:bg-primary dark:aria-invalid:ring-destructive/40", 18 + className, 19 + )} 20 + {...props} 21 + > 22 + <CheckboxPrimitive.Indicator 23 + data-slot="checkbox-indicator" 24 + className="flex items-center justify-center text-current transition-none" 25 + > 26 + <CheckIcon className="size-3.5" /> 27 + </CheckboxPrimitive.Indicator> 28 + </CheckboxPrimitive.Root> 29 + ); 30 + } 31 + 32 + export { Checkbox };
+33
apps/status-page/src/components/ui/collapsible.tsx
··· 1 + "use client"; 2 + 3 + import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; 4 + 5 + function Collapsible({ 6 + ...props 7 + }: React.ComponentProps<typeof CollapsiblePrimitive.Root>) { 8 + return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />; 9 + } 10 + 11 + function CollapsibleTrigger({ 12 + ...props 13 + }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) { 14 + return ( 15 + <CollapsiblePrimitive.CollapsibleTrigger 16 + data-slot="collapsible-trigger" 17 + {...props} 18 + /> 19 + ); 20 + } 21 + 22 + function CollapsibleContent({ 23 + ...props 24 + }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) { 25 + return ( 26 + <CollapsiblePrimitive.CollapsibleContent 27 + data-slot="collapsible-content" 28 + {...props} 29 + /> 30 + ); 31 + } 32 + 33 + export { Collapsible, CollapsibleTrigger, CollapsibleContent };
+177
apps/status-page/src/components/ui/command.tsx
··· 1 + "use client"; 2 + 3 + import { Command as CommandPrimitive } from "cmdk"; 4 + import { SearchIcon } from "lucide-react"; 5 + import type * as React from "react"; 6 + 7 + import { 8 + Dialog, 9 + DialogContent, 10 + DialogDescription, 11 + DialogHeader, 12 + DialogTitle, 13 + } from "@/components/ui/dialog"; 14 + import { cn } from "@/lib/utils"; 15 + 16 + function Command({ 17 + className, 18 + ...props 19 + }: React.ComponentProps<typeof CommandPrimitive>) { 20 + return ( 21 + <CommandPrimitive 22 + data-slot="command" 23 + className={cn( 24 + "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground", 25 + className, 26 + )} 27 + {...props} 28 + /> 29 + ); 30 + } 31 + 32 + function CommandDialog({ 33 + title = "Command Palette", 34 + description = "Search for a command to run...", 35 + children, 36 + ...props 37 + }: React.ComponentProps<typeof Dialog> & { 38 + title?: string; 39 + description?: string; 40 + }) { 41 + return ( 42 + <Dialog {...props}> 43 + <DialogHeader className="sr-only"> 44 + <DialogTitle>{title}</DialogTitle> 45 + <DialogDescription>{description}</DialogDescription> 46 + </DialogHeader> 47 + <DialogContent className="overflow-hidden p-0"> 48 + <Command className="**:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"> 49 + {children} 50 + </Command> 51 + </DialogContent> 52 + </Dialog> 53 + ); 54 + } 55 + 56 + function CommandInput({ 57 + className, 58 + ...props 59 + }: React.ComponentProps<typeof CommandPrimitive.Input>) { 60 + return ( 61 + <div 62 + data-slot="command-input-wrapper" 63 + className="flex h-9 items-center gap-2 border-b px-3" 64 + > 65 + <SearchIcon className="size-4 shrink-0 opacity-50" /> 66 + <CommandPrimitive.Input 67 + data-slot="command-input" 68 + className={cn( 69 + "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50", 70 + className, 71 + )} 72 + {...props} 73 + /> 74 + </div> 75 + ); 76 + } 77 + 78 + function CommandList({ 79 + className, 80 + ...props 81 + }: React.ComponentProps<typeof CommandPrimitive.List>) { 82 + return ( 83 + <CommandPrimitive.List 84 + data-slot="command-list" 85 + className={cn( 86 + "max-h-[300px] scroll-py-1 overflow-y-auto overflow-x-hidden", 87 + className, 88 + )} 89 + {...props} 90 + /> 91 + ); 92 + } 93 + 94 + function CommandEmpty({ 95 + ...props 96 + }: React.ComponentProps<typeof CommandPrimitive.Empty>) { 97 + return ( 98 + <CommandPrimitive.Empty 99 + data-slot="command-empty" 100 + className="py-6 text-center text-sm" 101 + {...props} 102 + /> 103 + ); 104 + } 105 + 106 + function CommandGroup({ 107 + className, 108 + ...props 109 + }: React.ComponentProps<typeof CommandPrimitive.Group>) { 110 + return ( 111 + <CommandPrimitive.Group 112 + data-slot="command-group" 113 + className={cn( 114 + "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:text-xs", 115 + className, 116 + )} 117 + {...props} 118 + /> 119 + ); 120 + } 121 + 122 + function CommandSeparator({ 123 + className, 124 + ...props 125 + }: React.ComponentProps<typeof CommandPrimitive.Separator>) { 126 + return ( 127 + <CommandPrimitive.Separator 128 + data-slot="command-separator" 129 + className={cn("-mx-1 h-px bg-border", className)} 130 + {...props} 131 + /> 132 + ); 133 + } 134 + 135 + function CommandItem({ 136 + className, 137 + ...props 138 + }: React.ComponentProps<typeof CommandPrimitive.Item>) { 139 + return ( 140 + <CommandPrimitive.Item 141 + data-slot="command-item" 142 + className={cn( 143 + "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0", 144 + className, 145 + )} 146 + {...props} 147 + /> 148 + ); 149 + } 150 + 151 + function CommandShortcut({ 152 + className, 153 + ...props 154 + }: React.ComponentProps<"span">) { 155 + return ( 156 + <span 157 + data-slot="command-shortcut" 158 + className={cn( 159 + "ml-auto text-muted-foreground text-xs tracking-widest", 160 + className, 161 + )} 162 + {...props} 163 + /> 164 + ); 165 + } 166 + 167 + export { 168 + Command, 169 + CommandDialog, 170 + CommandInput, 171 + CommandList, 172 + CommandEmpty, 173 + CommandGroup, 174 + CommandItem, 175 + CommandShortcut, 176 + CommandSeparator, 177 + };
+173
apps/status-page/src/components/ui/data-table/data-table-action-bar.tsx
··· 1 + "use client"; 2 + 3 + import { Button } from "@/components/ui/button"; 4 + import { Separator } from "@/components/ui/separator"; 5 + import { 6 + Tooltip, 7 + TooltipContent, 8 + TooltipTrigger, 9 + } from "@/components/ui/tooltip"; 10 + import { cn } from "@/lib/utils"; 11 + import type { Table } from "@tanstack/react-table"; 12 + import { Loader, X } from "lucide-react"; 13 + import * as React from "react"; 14 + import * as ReactDOM from "react-dom"; 15 + 16 + export interface DataTableActionBarProps<TData> 17 + extends React.ComponentProps<"div"> { 18 + table: Table<TData>; 19 + visible?: boolean; 20 + container?: Element | DocumentFragment | null; 21 + } 22 + 23 + function DataTableActionBar<TData>({ 24 + table, 25 + visible: visibleProp, 26 + container: containerProp, 27 + children, 28 + className, 29 + ...props 30 + }: DataTableActionBarProps<TData>) { 31 + const [mounted, setMounted] = React.useState(false); 32 + 33 + React.useLayoutEffect(() => { 34 + setMounted(true); 35 + }, []); 36 + 37 + React.useEffect(() => { 38 + function onKeyDown(event: KeyboardEvent) { 39 + if (event.key === "Escape") { 40 + table.toggleAllRowsSelected(false); 41 + } 42 + } 43 + 44 + window.addEventListener("keydown", onKeyDown); 45 + return () => window.removeEventListener("keydown", onKeyDown); 46 + }, [table]); 47 + 48 + const container = 49 + containerProp ?? (mounted ? globalThis.document?.body : null); 50 + 51 + if (!container) return null; 52 + 53 + const visible = 54 + visibleProp ?? table.getFilteredSelectedRowModel().rows.length > 0; 55 + 56 + return ReactDOM.createPortal( 57 + <div> 58 + {visible && ( 59 + <div 60 + role="toolbar" 61 + aria-orientation="horizontal" 62 + className={cn( 63 + "fixed inset-x-0 bottom-6 z-50 mx-auto flex w-fit flex-wrap items-center justify-center gap-2 rounded-md border bg-background p-2 text-foreground shadow-sm", 64 + className, 65 + )} 66 + {...props} 67 + > 68 + {children} 69 + </div> 70 + )} 71 + </div>, 72 + container, 73 + ); 74 + } 75 + 76 + interface DataTableActionBarActionProps 77 + extends React.ComponentProps<typeof Button> { 78 + tooltip?: string; 79 + isPending?: boolean; 80 + } 81 + 82 + function DataTableActionBarAction({ 83 + size = "sm", 84 + tooltip, 85 + isPending, 86 + disabled, 87 + className, 88 + children, 89 + ...props 90 + }: DataTableActionBarActionProps) { 91 + const trigger = ( 92 + <Button 93 + variant="secondary" 94 + size={size} 95 + className={cn( 96 + "gap-1.5 border border-secondary bg-secondary/50 hover:bg-secondary/70 [&>svg]:size-3.5", 97 + size === "icon" ? "size-7" : "h-7", 98 + className, 99 + )} 100 + disabled={disabled || isPending} 101 + {...props} 102 + > 103 + {isPending ? <Loader className="animate-spin" /> : children} 104 + </Button> 105 + ); 106 + 107 + if (!tooltip) return trigger; 108 + 109 + return ( 110 + <Tooltip> 111 + <TooltipTrigger asChild>{trigger}</TooltipTrigger> 112 + <TooltipContent 113 + sideOffset={6} 114 + className="border bg-accent font-semibold text-foreground dark:bg-zinc-900 [&>span]:hidden" 115 + > 116 + <p>{tooltip}</p> 117 + </TooltipContent> 118 + </Tooltip> 119 + ); 120 + } 121 + 122 + interface DataTableActionBarSelectionProps<TData> { 123 + table: Table<TData>; 124 + } 125 + 126 + function DataTableActionBarSelection<TData>({ 127 + table, 128 + }: DataTableActionBarSelectionProps<TData>) { 129 + const onClearSelection = React.useCallback(() => { 130 + table.toggleAllRowsSelected(false); 131 + }, [table]); 132 + 133 + return ( 134 + <div className="flex h-7 items-center rounded-md border pr-1 pl-2.5"> 135 + <span className="whitespace-nowrap text-xs"> 136 + {table.getFilteredSelectedRowModel().rows.length} selected 137 + </span> 138 + <Separator 139 + orientation="vertical" 140 + className="mr-1 ml-2 data-[orientation=vertical]:h-4" 141 + /> 142 + <Tooltip> 143 + <TooltipTrigger asChild> 144 + <Button 145 + variant="ghost" 146 + size="icon" 147 + className="size-5" 148 + onClick={onClearSelection} 149 + > 150 + <X className="size-3.5" /> 151 + </Button> 152 + </TooltipTrigger> 153 + <TooltipContent 154 + sideOffset={10} 155 + className="flex items-center gap-2 border bg-accent px-2 py-1 font-semibold text-foreground dark:bg-zinc-900 [&>span]:hidden" 156 + > 157 + <p>Clear selection</p> 158 + <kbd className="select-none rounded border bg-background px-1.5 py-px font-mono font-normal text-[0.7rem] text-foreground shadow-xs"> 159 + <abbr title="Escape" className="no-underline"> 160 + Esc 161 + </abbr> 162 + </kbd> 163 + </TooltipContent> 164 + </Tooltip> 165 + </div> 166 + ); 167 + } 168 + 169 + export { 170 + DataTableActionBar, 171 + DataTableActionBarAction, 172 + DataTableActionBarSelection, 173 + };
+58
apps/status-page/src/components/ui/data-table/data-table-column-header.tsx
··· 1 + import type { Column } from "@tanstack/react-table"; 2 + import { ChevronDown, ChevronUp } from "lucide-react"; 3 + 4 + import { Button } from "@/components/ui/button"; 5 + 6 + import { cn } from "@/lib/utils"; 7 + 8 + interface DataTableColumnHeaderProps<TData, TValue> 9 + extends React.ComponentProps<"button"> { 10 + column: Column<TData, TValue>; 11 + title: string; 12 + } 13 + 14 + export function DataTableColumnHeader<TData, TValue>({ 15 + column, 16 + title, 17 + className, 18 + ...props 19 + }: DataTableColumnHeaderProps<TData, TValue>) { 20 + if (!column.getCanSort()) { 21 + return <div className={cn(className)}>{title}</div>; 22 + } 23 + 24 + return ( 25 + <Button 26 + variant="ghost" 27 + size="sm" 28 + onClick={() => { 29 + column.toggleSorting(undefined); 30 + }} 31 + className={cn( 32 + "flex h-7 w-full items-center justify-between gap-2 px-0 py-0 hover:bg-transparent dark:hover:bg-transparent", 33 + className, 34 + )} 35 + {...props} 36 + > 37 + <span>{title}</span> 38 + <span className="flex flex-col"> 39 + <ChevronUp 40 + className={cn( 41 + "-mb-0.5 size-3", 42 + column.getIsSorted() === "asc" 43 + ? "text-accent-foreground" 44 + : "text-muted-foreground", 45 + )} 46 + /> 47 + <ChevronDown 48 + className={cn( 49 + "-mt-0.5 size-3", 50 + column.getIsSorted() === "desc" 51 + ? "text-accent-foreground" 52 + : "text-muted-foreground", 53 + )} 54 + /> 55 + </span> 56 + </Button> 57 + ); 58 + }
+149
apps/status-page/src/components/ui/data-table/data-table-faceted-filter.tsx
··· 1 + import type { Column } from "@tanstack/react-table"; 2 + import { Check, PlusCircle } from "lucide-react"; 3 + import type * as React from "react"; 4 + 5 + import { Badge } from "@/components/ui/badge"; 6 + import { Button } from "@/components/ui/button"; 7 + import { 8 + Command, 9 + CommandEmpty, 10 + CommandGroup, 11 + CommandInput, 12 + CommandItem, 13 + CommandList, 14 + CommandSeparator, 15 + } from "@/components/ui/command"; 16 + import { 17 + Popover, 18 + PopoverContent, 19 + PopoverTrigger, 20 + } from "@/components/ui/popover"; 21 + import { Separator } from "@/components/ui/separator"; 22 + import { cn } from "@/lib/utils"; 23 + 24 + interface DataTableFacetedFilterProps<TData, TValue> { 25 + column?: Column<TData, TValue>; 26 + title?: string; 27 + options: { 28 + label: string; 29 + value: string | number; 30 + icon?: React.ComponentType<{ className?: string }>; 31 + }[]; 32 + } 33 + 34 + export function DataTableFacetedFilter<TData, TValue>({ 35 + column, 36 + title, 37 + options, 38 + }: DataTableFacetedFilterProps<TData, TValue>) { 39 + const facets = column?.getFacetedUniqueValues(); 40 + const selectedValues = new Set( 41 + column?.getFilterValue() as (string | number)[], 42 + ); 43 + 44 + return ( 45 + <Popover> 46 + <PopoverTrigger asChild> 47 + <Button variant="outline" size="sm" className="h-8 border-dashed"> 48 + <PlusCircle /> 49 + {title} 50 + {selectedValues?.size > 0 && ( 51 + <> 52 + <Separator orientation="vertical" className="mx-2 h-4" /> 53 + <Badge 54 + variant="secondary" 55 + className="rounded-sm px-1 font-normal lg:hidden" 56 + > 57 + {selectedValues.size} 58 + </Badge> 59 + <div className="hidden space-x-1 lg:flex"> 60 + {selectedValues.size > 2 ? ( 61 + <Badge 62 + variant="secondary" 63 + className="rounded-sm px-1 font-normal" 64 + > 65 + {selectedValues.size} selected 66 + </Badge> 67 + ) : ( 68 + options 69 + .filter((option) => selectedValues.has(option.value)) 70 + .map((option) => ( 71 + <Badge 72 + variant="secondary" 73 + key={option.value} 74 + className="rounded-sm px-1 font-normal" 75 + > 76 + {option.label} 77 + </Badge> 78 + )) 79 + )} 80 + </div> 81 + </> 82 + )} 83 + </Button> 84 + </PopoverTrigger> 85 + <PopoverContent className="w-[200px] p-0" align="start"> 86 + <Command> 87 + <CommandInput placeholder={title} /> 88 + <CommandList> 89 + <CommandEmpty>No results found.</CommandEmpty> 90 + <CommandGroup> 91 + {options.map((option) => { 92 + const isSelected = selectedValues.has(option.value); 93 + return ( 94 + <CommandItem 95 + key={option.value} 96 + onSelect={() => { 97 + if (isSelected) { 98 + selectedValues.delete(option.value); 99 + } else { 100 + selectedValues.add(option.value); 101 + } 102 + const filterValues = Array.from(selectedValues); 103 + column?.setFilterValue( 104 + filterValues.length ? filterValues : undefined, 105 + ); 106 + }} 107 + > 108 + <div 109 + className={cn( 110 + "mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary", 111 + isSelected 112 + ? "bg-primary text-primary-foreground" 113 + : "opacity-50 [&_svg]:invisible", 114 + )} 115 + > 116 + <Check /> 117 + </div> 118 + {option.icon && ( 119 + <option.icon className="mr-2 h-4 w-4 text-muted-foreground" /> 120 + )} 121 + <span>{option.label}</span> 122 + {facets?.get(option.value) && ( 123 + <span className="ml-auto flex h-4 w-4 items-center justify-center font-mono text-xs"> 124 + {facets.get(option.value)} 125 + </span> 126 + )} 127 + </CommandItem> 128 + ); 129 + })} 130 + </CommandGroup> 131 + {selectedValues.size > 0 && ( 132 + <> 133 + <CommandSeparator /> 134 + <CommandGroup> 135 + <CommandItem 136 + onSelect={() => column?.setFilterValue(undefined)} 137 + className="justify-center text-center" 138 + > 139 + Clear filters 140 + </CommandItem> 141 + </CommandGroup> 142 + </> 143 + )} 144 + </CommandList> 145 + </Command> 146 + </PopoverContent> 147 + </Popover> 148 + ); 149 + }
+148
apps/status-page/src/components/ui/data-table/data-table-pagination.tsx
··· 1 + import type { Table } from "@tanstack/react-table"; 2 + import { 3 + ChevronLeft, 4 + ChevronRight, 5 + ChevronsLeft, 6 + ChevronsRight, 7 + } from "lucide-react"; 8 + 9 + import { Button } from "@/components/ui/button"; 10 + import { 11 + Select, 12 + SelectContent, 13 + SelectItem, 14 + SelectTrigger, 15 + SelectValue, 16 + } from "@/components/ui/select"; 17 + 18 + export interface DataTablePaginationProps<TData> { 19 + table: Table<TData>; 20 + } 21 + 22 + export function DataTablePagination<TData>({ 23 + table, 24 + }: DataTablePaginationProps<TData>) { 25 + return ( 26 + <div className="flex flex-wrap items-center justify-between gap-2"> 27 + <div className="flex-1 text-muted-foreground text-sm"> 28 + {table.getFilteredSelectedRowModel().rows.length} of{" "} 29 + {table.getFilteredRowModel().rows.length} row(s) selected. 30 + </div> 31 + <div className="flex items-center space-x-6 lg:space-x-8"> 32 + <div className="flex items-center space-x-2"> 33 + <p className="font-medium text-sm">Rows per page</p> 34 + <Select 35 + value={`${table.getState().pagination.pageSize}`} 36 + onValueChange={(value) => { 37 + table.setPageSize(Number(value)); 38 + }} 39 + > 40 + <SelectTrigger className="h-8 w-[70px]"> 41 + <SelectValue placeholder={table.getState().pagination.pageSize} /> 42 + </SelectTrigger> 43 + <SelectContent side="top"> 44 + {[10, 20, 30, 40, 50].map((pageSize) => ( 45 + <SelectItem key={pageSize} value={`${pageSize}`}> 46 + {pageSize} 47 + </SelectItem> 48 + ))} 49 + </SelectContent> 50 + </Select> 51 + </div> 52 + <div className="flex items-center justify-center font-medium text-sm"> 53 + Page {table.getState().pagination.pageIndex + 1} of{" "} 54 + {table.getPageCount()} 55 + </div> 56 + <div className="flex items-center space-x-2"> 57 + <Button 58 + variant="outline" 59 + className="hidden h-8 w-8 p-0 lg:flex" 60 + onClick={() => table.setPageIndex(0)} 61 + disabled={!table.getCanPreviousPage()} 62 + > 63 + <span className="sr-only">Go to first page</span> 64 + <ChevronsLeft /> 65 + </Button> 66 + <Button 67 + variant="outline" 68 + className="h-8 w-8 p-0" 69 + onClick={() => table.previousPage()} 70 + disabled={!table.getCanPreviousPage()} 71 + > 72 + <span className="sr-only">Go to previous page</span> 73 + <ChevronLeft /> 74 + </Button> 75 + <Button 76 + variant="outline" 77 + className="h-8 w-8 p-0" 78 + onClick={() => table.nextPage()} 79 + disabled={!table.getCanNextPage()} 80 + > 81 + <span className="sr-only">Go to next page</span> 82 + <ChevronRight /> 83 + </Button> 84 + <Button 85 + variant="outline" 86 + className="hidden h-8 w-8 p-0 lg:flex" 87 + onClick={() => table.setPageIndex(table.getPageCount() - 1)} 88 + disabled={!table.getCanNextPage()} 89 + > 90 + <span className="sr-only">Go to last page</span> 91 + <ChevronsRight /> 92 + </Button> 93 + </div> 94 + </div> 95 + </div> 96 + ); 97 + } 98 + 99 + export function DataTablePaginationSimple<TData>({ 100 + table, 101 + }: DataTablePaginationProps<TData>) { 102 + return ( 103 + <div className="flex items-center justify-between"> 104 + <div className="flex-1 text-muted-foreground text-sm"> 105 + {table.getFilteredRowModel().rows.length} of{" "} 106 + {table.getPreFilteredRowModel().rows.length} row(s) filtered. 107 + </div> 108 + <div className="flex items-center space-x-2"> 109 + <Button 110 + variant="outline" 111 + className="hidden h-8 w-8 p-0 lg:flex" 112 + onClick={() => table.setPageIndex(0)} 113 + disabled={!table.getCanPreviousPage()} 114 + > 115 + <span className="sr-only">Go to first page</span> 116 + <ChevronsLeft /> 117 + </Button> 118 + <Button 119 + variant="outline" 120 + className="h-8 w-8 p-0" 121 + onClick={() => table.previousPage()} 122 + disabled={!table.getCanPreviousPage()} 123 + > 124 + <span className="sr-only">Go to previous page</span> 125 + <ChevronLeft /> 126 + </Button> 127 + <Button 128 + variant="outline" 129 + className="h-8 w-8 p-0" 130 + onClick={() => table.nextPage()} 131 + disabled={!table.getCanNextPage()} 132 + > 133 + <span className="sr-only">Go to next page</span> 134 + <ChevronRight /> 135 + </Button> 136 + <Button 137 + variant="outline" 138 + className="hidden h-8 w-8 p-0 lg:flex" 139 + onClick={() => table.setPageIndex(table.getPageCount() - 1)} 140 + disabled={!table.getCanNextPage()} 141 + > 142 + <span className="sr-only">Go to last page</span> 143 + <ChevronsRight /> 144 + </Button> 145 + </div> 146 + </div> 147 + ); 148 + }
+64
apps/status-page/src/components/ui/data-table/data-table-skeleton.tsx
··· 1 + import { Skeleton } from "@/components/ui/skeleton"; 2 + import { 3 + Table, 4 + TableBody, 5 + TableCell, 6 + TableHead, 7 + TableHeader, 8 + TableRow, 9 + } from "@/components/ui/table"; 10 + 11 + interface DataTableSkeletonProps { 12 + /** 13 + * Number of rows to render 14 + * @default 10 15 + */ 16 + rows?: number; 17 + } 18 + 19 + // TODO: add checkbox skeleton (for MonitorTable e.g.) 20 + 21 + export function DataTableSkeleton({ rows = 3 }: DataTableSkeletonProps) { 22 + return ( 23 + <Table> 24 + <TableHeader className="bg-muted/50"> 25 + <TableRow className="hover:bg-transparent"> 26 + <TableHead> 27 + <Skeleton className="my-1.5 h-4 w-24" /> 28 + </TableHead> 29 + <TableHead className="hidden sm:table-cell"> 30 + <Skeleton className="my-1.5 h-4 w-32" /> 31 + </TableHead> 32 + <TableHead className="hidden md:table-cell"> 33 + <Skeleton className="my-1.5 h-4 w-16" /> 34 + </TableHead> 35 + <TableHead> 36 + <Skeleton className="my-1.5 h-4 w-20" /> 37 + </TableHead> 38 + <TableHead className="flex items-center justify-end" /> 39 + </TableRow> 40 + </TableHeader> 41 + <TableBody> 42 + {new Array(rows).fill(0).map((_, i) => ( 43 + <TableRow key={i} className="hover:bg-transparent"> 44 + <TableCell> 45 + <Skeleton className="my-1.5 h-4 w-full max-w-40" /> 46 + </TableCell> 47 + <TableCell className="hidden sm:table-cell"> 48 + <Skeleton className="my-1.5 h-4 w-full max-w-52" /> 49 + </TableCell> 50 + <TableCell className="hidden md:table-cell"> 51 + <Skeleton className="my-1.5 h-4 w-24" /> 52 + </TableCell> 53 + <TableCell> 54 + <Skeleton className="my-1.5 h-4 w-full max-w-40" /> 55 + </TableCell> 56 + <TableCell className="flex justify-end"> 57 + <Skeleton className="my-1.5 h-5 w-5" /> 58 + </TableCell> 59 + </TableRow> 60 + ))} 61 + </TableBody> 62 + </Table> 63 + ); 64 + }
+60
apps/status-page/src/components/ui/data-table/data-table-toobar.tsx
··· 1 + "use client"; 2 + 3 + import type { Table } from "@tanstack/react-table"; 4 + import { X } from "lucide-react"; 5 + 6 + import { Button } from "@/components/ui/button"; 7 + import { DataTableViewOptions } from "@/components/ui/data-table/data-table-view-options"; 8 + import { Input } from "@/components/ui/input"; 9 + 10 + import { DataTableFacetedFilter } from "@/components/ui/data-table/data-table-faceted-filter"; 11 + 12 + export interface DataTableToolbarProps<TData> { 13 + table: Table<TData>; 14 + } 15 + 16 + export function DataTableToolbar<TData>({ 17 + table, 18 + }: DataTableToolbarProps<TData>) { 19 + const isFiltered = table.getState().columnFilters.length > 0; 20 + 21 + return ( 22 + <div className="flex items-center justify-between"> 23 + <div className="flex flex-1 items-center space-x-2"> 24 + <Input 25 + placeholder="Filter entries..." 26 + value={(table.getColumn("title")?.getFilterValue() as string) ?? ""} 27 + onChange={(event) => 28 + table.getColumn("title")?.setFilterValue(event.target.value) 29 + } 30 + className="h-8 w-[150px] lg:w-[250px]" 31 + /> 32 + {table.getColumn("status") && ( 33 + <DataTableFacetedFilter 34 + column={table.getColumn("status")} 35 + title="Status" 36 + options={[]} 37 + /> 38 + )} 39 + {table.getColumn("tags") && ( 40 + <DataTableFacetedFilter 41 + column={table.getColumn("tags")} 42 + title="Tags" 43 + options={[]} 44 + /> 45 + )} 46 + {isFiltered && ( 47 + <Button 48 + variant="ghost" 49 + onClick={() => table.resetColumnFilters()} 50 + className="h-8 px-2 lg:px-3" 51 + > 52 + Reset 53 + <X /> 54 + </Button> 55 + )} 56 + </div> 57 + <DataTableViewOptions table={table} /> 58 + </div> 59 + ); 60 + }
+59
apps/status-page/src/components/ui/data-table/data-table-view-options.tsx
··· 1 + "use client"; 2 + 3 + import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; 4 + import type { Table } from "@tanstack/react-table"; 5 + import { Settings2 } from "lucide-react"; 6 + 7 + import { Button } from "@/components/ui/button"; 8 + import { 9 + DropdownMenu, 10 + DropdownMenuCheckboxItem, 11 + DropdownMenuContent, 12 + DropdownMenuLabel, 13 + DropdownMenuSeparator, 14 + } from "@/components/ui/dropdown-menu"; 15 + 16 + interface DataTableViewOptionsProps<TData> { 17 + table: Table<TData>; 18 + } 19 + 20 + export function DataTableViewOptions<TData>({ 21 + table, 22 + }: DataTableViewOptionsProps<TData>) { 23 + return ( 24 + <DropdownMenu> 25 + <DropdownMenuTrigger asChild> 26 + <Button 27 + variant="outline" 28 + size="sm" 29 + className="ml-auto hidden h-8 lg:flex" 30 + > 31 + <Settings2 /> 32 + View 33 + </Button> 34 + </DropdownMenuTrigger> 35 + <DropdownMenuContent align="end" className="w-[150px]"> 36 + <DropdownMenuLabel>Toggle columns</DropdownMenuLabel> 37 + <DropdownMenuSeparator /> 38 + {table 39 + .getAllColumns() 40 + .filter( 41 + (column) => 42 + typeof column.accessorFn !== "undefined" && column.getCanHide(), 43 + ) 44 + .map((column) => { 45 + return ( 46 + <DropdownMenuCheckboxItem 47 + key={column.id} 48 + className="capitalize" 49 + checked={column.getIsVisible()} 50 + onCheckedChange={(value) => column.toggleVisibility(!!value)} 51 + > 52 + {column.id} 53 + </DropdownMenuCheckboxItem> 54 + ); 55 + })} 56 + </DropdownMenuContent> 57 + </DropdownMenu> 58 + ); 59 + }
+208
apps/status-page/src/components/ui/data-table/data-table.tsx
··· 1 + "use client"; 2 + 3 + import { 4 + type ColumnDef, 5 + type ColumnFiltersState, 6 + type PaginationState, 7 + type Row, 8 + type SortingState, 9 + type VisibilityState, 10 + flexRender, 11 + getCoreRowModel, 12 + getExpandedRowModel, 13 + getFacetedRowModel, 14 + getFacetedUniqueValues, 15 + getFilteredRowModel, 16 + getPaginationRowModel, 17 + getSortedRowModel, 18 + useReactTable, 19 + } from "@tanstack/react-table"; 20 + import * as React from "react"; 21 + 22 + import { 23 + Table, 24 + TableBody, 25 + TableCell, 26 + TableHead, 27 + TableHeader, 28 + TableRow, 29 + } from "@/components/ui/table"; 30 + import { Fragment } from "react"; 31 + import type { DataTableActionBarProps } from "./data-table-action-bar"; 32 + import type { DataTablePaginationProps } from "./data-table-pagination"; 33 + import type { DataTableToolbarProps } from "./data-table-toobar"; 34 + 35 + export interface DataTableProps<TData, TValue> { 36 + columns: ColumnDef<TData, TValue>[]; 37 + data: TData[]; 38 + rowComponent?: React.ComponentType<{ row: Row<TData> }>; 39 + toolbarComponent?: React.ComponentType<DataTableToolbarProps<TData>>; 40 + actionBar?: React.ComponentType<DataTableActionBarProps<TData>>; 41 + paginationComponent?: React.ComponentType<DataTablePaginationProps<TData>>; 42 + onRowClick?: (row: Row<TData>) => void; 43 + defaultSorting?: SortingState; 44 + defaultColumnVisibility?: VisibilityState; 45 + defaultColumnFilters?: ColumnFiltersState; 46 + defaultPagination?: PaginationState; 47 + autoResetPageIndex?: boolean; 48 + 49 + /** access the state from the parent component */ 50 + columnFilters?: ColumnFiltersState; 51 + setColumnFilters?: React.Dispatch<React.SetStateAction<ColumnFiltersState>>; 52 + sorting?: SortingState; 53 + setSorting?: React.Dispatch<React.SetStateAction<SortingState>>; 54 + pagination?: PaginationState; 55 + setPagination?: React.Dispatch<React.SetStateAction<PaginationState>>; 56 + } 57 + 58 + export function DataTable<TData, TValue>({ 59 + columns, 60 + data, 61 + rowComponent, 62 + toolbarComponent, 63 + actionBar, 64 + paginationComponent, 65 + onRowClick, 66 + defaultSorting = [], 67 + defaultColumnVisibility = {}, 68 + defaultColumnFilters = [], 69 + defaultPagination = { pageIndex: 0, pageSize: 10 }, 70 + autoResetPageIndex = true, 71 + columnFilters, 72 + setColumnFilters, 73 + sorting, 74 + setSorting, 75 + pagination, 76 + setPagination, 77 + }: DataTableProps<TData, TValue>) { 78 + // biome-ignore lint/suspicious/noExplicitAny: <explanation> 79 + const [globalFilter, setGlobalFilter] = React.useState<any>(); 80 + const [rowSelection, setRowSelection] = React.useState({}); 81 + const [columnVisibility, setColumnVisibility] = 82 + React.useState<VisibilityState>(defaultColumnVisibility); 83 + const [internalPagination, setInternalPagination] = 84 + React.useState<PaginationState>(defaultPagination); 85 + const [internalColumnFilters, setInternalColumnFilters] = 86 + React.useState<ColumnFiltersState>(defaultColumnFilters); 87 + const [internalSorting, setInternalSorting] = 88 + React.useState<SortingState>(defaultSorting); 89 + 90 + // Use controlled or uncontrolled column filters 91 + const columnFiltersState = columnFilters ?? internalColumnFilters; 92 + const setColumnFiltersState = setColumnFilters ?? setInternalColumnFilters; 93 + const sortingState = sorting ?? internalSorting; 94 + const setSortingState = setSorting ?? setInternalSorting; 95 + const paginationState = pagination ?? internalPagination; 96 + const setPaginationState = setPagination ?? setInternalPagination; 97 + 98 + const table = useReactTable({ 99 + data, 100 + columns, 101 + state: { 102 + sorting: sortingState, 103 + columnVisibility, 104 + rowSelection, 105 + pagination: paginationState, 106 + columnFilters: columnFiltersState, 107 + globalFilter, 108 + }, 109 + enableRowSelection: true, 110 + onRowSelectionChange: setRowSelection, 111 + onSortingChange: setSortingState, 112 + onColumnFiltersChange: setColumnFiltersState, 113 + onColumnVisibilityChange: setColumnVisibility, 114 + onPaginationChange: setPaginationState, 115 + onGlobalFilterChange: setGlobalFilter, 116 + getCoreRowModel: getCoreRowModel(), 117 + getFilteredRowModel: getFilteredRowModel(), 118 + getPaginationRowModel: getPaginationRowModel(), 119 + getSortedRowModel: getSortedRowModel(), 120 + getFacetedRowModel: getFacetedRowModel(), 121 + getFacetedUniqueValues: getFacetedUniqueValues(), 122 + getExpandedRowModel: getExpandedRowModel(), 123 + autoResetPageIndex, 124 + // @ts-expect-error as we have an id in the data 125 + getRowCanExpand: (row) => Boolean(row.original.id), 126 + }); 127 + 128 + return ( 129 + <div className="grid gap-2"> 130 + {toolbarComponent 131 + ? React.createElement(toolbarComponent, { table }) 132 + : null} 133 + <Table> 134 + <TableHeader> 135 + {table.getHeaderGroups().map((headerGroup) => ( 136 + <TableRow key={headerGroup.id}> 137 + {headerGroup.headers.map((header) => { 138 + return ( 139 + <TableHead 140 + key={header.id} 141 + colSpan={header.colSpan} 142 + className={header.column.columnDef.meta?.headerClassName} 143 + > 144 + {header.isPlaceholder 145 + ? null 146 + : flexRender( 147 + header.column.columnDef.header, 148 + header.getContext(), 149 + )} 150 + </TableHead> 151 + ); 152 + })} 153 + </TableRow> 154 + ))} 155 + </TableHeader> 156 + <TableBody> 157 + {table.getRowModel().rows?.length ? ( 158 + table.getRowModel().rows.map((row) => ( 159 + <Fragment key={row.id}> 160 + <TableRow 161 + data-state={ 162 + (row.getIsSelected() || row.getIsExpanded()) && "selected" 163 + } 164 + onClick={() => onRowClick?.(row)} 165 + className="data-[state=selected]:bg-muted/50" 166 + > 167 + {row.getVisibleCells().map((cell) => ( 168 + <TableCell 169 + key={cell.id} 170 + className={cell.column.columnDef.meta?.cellClassName} 171 + > 172 + {flexRender( 173 + cell.column.columnDef.cell, 174 + cell.getContext(), 175 + )} 176 + </TableCell> 177 + ))} 178 + </TableRow> 179 + {row.getIsExpanded() && ( 180 + <TableRow className="hover:bg-background"> 181 + <TableCell 182 + className="p-0" 183 + colSpan={row.getVisibleCells().length} 184 + > 185 + {rowComponent 186 + ? React.createElement(rowComponent, { row }) 187 + : null} 188 + </TableCell> 189 + </TableRow> 190 + )} 191 + </Fragment> 192 + )) 193 + ) : ( 194 + <TableRow> 195 + <TableCell colSpan={columns.length} className="h-24 text-center"> 196 + No results. 197 + </TableCell> 198 + </TableRow> 199 + )} 200 + </TableBody> 201 + {actionBar ? React.createElement(actionBar, { table }) : null} 202 + </Table> 203 + {paginationComponent 204 + ? React.createElement(paginationComponent, { table }) 205 + : null} 206 + </div> 207 + ); 208 + }
+135
apps/status-page/src/components/ui/dialog.tsx
··· 1 + "use client"; 2 + 3 + import * as DialogPrimitive from "@radix-ui/react-dialog"; 4 + import { XIcon } from "lucide-react"; 5 + import type * as React from "react"; 6 + 7 + import { cn } from "@/lib/utils"; 8 + 9 + function Dialog({ 10 + ...props 11 + }: React.ComponentProps<typeof DialogPrimitive.Root>) { 12 + return <DialogPrimitive.Root data-slot="dialog" {...props} />; 13 + } 14 + 15 + function DialogTrigger({ 16 + ...props 17 + }: React.ComponentProps<typeof DialogPrimitive.Trigger>) { 18 + return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />; 19 + } 20 + 21 + function DialogPortal({ 22 + ...props 23 + }: React.ComponentProps<typeof DialogPrimitive.Portal>) { 24 + return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />; 25 + } 26 + 27 + function DialogClose({ 28 + ...props 29 + }: React.ComponentProps<typeof DialogPrimitive.Close>) { 30 + return <DialogPrimitive.Close data-slot="dialog-close" {...props} />; 31 + } 32 + 33 + function DialogOverlay({ 34 + className, 35 + ...props 36 + }: React.ComponentProps<typeof DialogPrimitive.Overlay>) { 37 + return ( 38 + <DialogPrimitive.Overlay 39 + data-slot="dialog-overlay" 40 + className={cn( 41 + "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=open]:animate-in", 42 + className, 43 + )} 44 + {...props} 45 + /> 46 + ); 47 + } 48 + 49 + function DialogContent({ 50 + className, 51 + children, 52 + ...props 53 + }: React.ComponentProps<typeof DialogPrimitive.Content>) { 54 + return ( 55 + <DialogPortal data-slot="dialog-portal"> 56 + <DialogOverlay /> 57 + <DialogPrimitive.Content 58 + data-slot="dialog-content" 59 + className={cn( 60 + "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 data-[state=closed]:animate-out data-[state=open]:animate-in sm:max-w-lg", 61 + className, 62 + )} 63 + {...props} 64 + > 65 + {children} 66 + <DialogPrimitive.Close className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"> 67 + <XIcon /> 68 + <span className="sr-only">Close</span> 69 + </DialogPrimitive.Close> 70 + </DialogPrimitive.Content> 71 + </DialogPortal> 72 + ); 73 + } 74 + 75 + function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { 76 + return ( 77 + <div 78 + data-slot="dialog-header" 79 + className={cn("flex flex-col gap-2 text-center sm:text-left", className)} 80 + {...props} 81 + /> 82 + ); 83 + } 84 + 85 + function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { 86 + return ( 87 + <div 88 + data-slot="dialog-footer" 89 + className={cn( 90 + "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", 91 + className, 92 + )} 93 + {...props} 94 + /> 95 + ); 96 + } 97 + 98 + function DialogTitle({ 99 + className, 100 + ...props 101 + }: React.ComponentProps<typeof DialogPrimitive.Title>) { 102 + return ( 103 + <DialogPrimitive.Title 104 + data-slot="dialog-title" 105 + className={cn("font-semibold text-lg leading-none", className)} 106 + {...props} 107 + /> 108 + ); 109 + } 110 + 111 + function DialogDescription({ 112 + className, 113 + ...props 114 + }: React.ComponentProps<typeof DialogPrimitive.Description>) { 115 + return ( 116 + <DialogPrimitive.Description 117 + data-slot="dialog-description" 118 + className={cn("text-muted-foreground text-sm", className)} 119 + {...props} 120 + /> 121 + ); 122 + } 123 + 124 + export { 125 + Dialog, 126 + DialogClose, 127 + DialogContent, 128 + DialogDescription, 129 + DialogFooter, 130 + DialogHeader, 131 + DialogOverlay, 132 + DialogPortal, 133 + DialogTitle, 134 + DialogTrigger, 135 + };
+257
apps/status-page/src/components/ui/dropdown-menu.tsx
··· 1 + "use client"; 2 + 3 + import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; 4 + import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"; 5 + import type * as React from "react"; 6 + 7 + import { cn } from "@/lib/utils"; 8 + 9 + function DropdownMenu({ 10 + ...props 11 + }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) { 12 + return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />; 13 + } 14 + 15 + function DropdownMenuPortal({ 16 + ...props 17 + }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) { 18 + return ( 19 + <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} /> 20 + ); 21 + } 22 + 23 + function DropdownMenuTrigger({ 24 + ...props 25 + }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) { 26 + return ( 27 + <DropdownMenuPrimitive.Trigger 28 + data-slot="dropdown-menu-trigger" 29 + {...props} 30 + /> 31 + ); 32 + } 33 + 34 + function DropdownMenuContent({ 35 + className, 36 + sideOffset = 4, 37 + ...props 38 + }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) { 39 + return ( 40 + <DropdownMenuPrimitive.Portal> 41 + <DropdownMenuPrimitive.Content 42 + data-slot="dropdown-menu-content" 43 + sideOffset={sideOffset} 44 + className={cn( 45 + "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=closed]:animate-out data-[state=open]:animate-in", 46 + className, 47 + )} 48 + {...props} 49 + /> 50 + </DropdownMenuPrimitive.Portal> 51 + ); 52 + } 53 + 54 + function DropdownMenuGroup({ 55 + ...props 56 + }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) { 57 + return ( 58 + <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} /> 59 + ); 60 + } 61 + 62 + function DropdownMenuItem({ 63 + className, 64 + inset, 65 + variant = "default", 66 + ...props 67 + }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & { 68 + inset?: boolean; 69 + variant?: "default" | "destructive"; 70 + }) { 71 + return ( 72 + <DropdownMenuPrimitive.Item 73 + data-slot="dropdown-menu-item" 74 + data-inset={inset} 75 + data-variant={variant} 76 + className={cn( 77 + "data-[variant=destructive]:*:[svg]:!text-destructive relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[disabled]:opacity-50 data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0", 78 + className, 79 + )} 80 + {...props} 81 + /> 82 + ); 83 + } 84 + 85 + function DropdownMenuCheckboxItem({ 86 + className, 87 + children, 88 + checked, 89 + ...props 90 + }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) { 91 + return ( 92 + <DropdownMenuPrimitive.CheckboxItem 93 + data-slot="dropdown-menu-checkbox-item" 94 + className={cn( 95 + "relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", 96 + className, 97 + )} 98 + checked={checked} 99 + {...props} 100 + > 101 + <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> 102 + <DropdownMenuPrimitive.ItemIndicator> 103 + <CheckIcon className="size-4" /> 104 + </DropdownMenuPrimitive.ItemIndicator> 105 + </span> 106 + {children} 107 + </DropdownMenuPrimitive.CheckboxItem> 108 + ); 109 + } 110 + 111 + function DropdownMenuRadioGroup({ 112 + ...props 113 + }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) { 114 + return ( 115 + <DropdownMenuPrimitive.RadioGroup 116 + data-slot="dropdown-menu-radio-group" 117 + {...props} 118 + /> 119 + ); 120 + } 121 + 122 + function DropdownMenuRadioItem({ 123 + className, 124 + children, 125 + ...props 126 + }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) { 127 + return ( 128 + <DropdownMenuPrimitive.RadioItem 129 + data-slot="dropdown-menu-radio-item" 130 + className={cn( 131 + "relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", 132 + className, 133 + )} 134 + {...props} 135 + > 136 + <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> 137 + <DropdownMenuPrimitive.ItemIndicator> 138 + <CircleIcon className="size-2 fill-current" /> 139 + </DropdownMenuPrimitive.ItemIndicator> 140 + </span> 141 + {children} 142 + </DropdownMenuPrimitive.RadioItem> 143 + ); 144 + } 145 + 146 + function DropdownMenuLabel({ 147 + className, 148 + inset, 149 + ...props 150 + }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & { 151 + inset?: boolean; 152 + }) { 153 + return ( 154 + <DropdownMenuPrimitive.Label 155 + data-slot="dropdown-menu-label" 156 + data-inset={inset} 157 + className={cn( 158 + "px-2 py-1.5 font-medium text-sm data-[inset]:pl-8", 159 + className, 160 + )} 161 + {...props} 162 + /> 163 + ); 164 + } 165 + 166 + function DropdownMenuSeparator({ 167 + className, 168 + ...props 169 + }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) { 170 + return ( 171 + <DropdownMenuPrimitive.Separator 172 + data-slot="dropdown-menu-separator" 173 + className={cn("-mx-1 my-1 h-px bg-border", className)} 174 + {...props} 175 + /> 176 + ); 177 + } 178 + 179 + function DropdownMenuShortcut({ 180 + className, 181 + ...props 182 + }: React.ComponentProps<"span">) { 183 + return ( 184 + <span 185 + data-slot="dropdown-menu-shortcut" 186 + className={cn( 187 + "ml-auto text-muted-foreground text-xs tracking-widest", 188 + className, 189 + )} 190 + {...props} 191 + /> 192 + ); 193 + } 194 + 195 + function DropdownMenuSub({ 196 + ...props 197 + }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) { 198 + return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />; 199 + } 200 + 201 + function DropdownMenuSubTrigger({ 202 + className, 203 + inset, 204 + children, 205 + ...props 206 + }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & { 207 + inset?: boolean; 208 + }) { 209 + return ( 210 + <DropdownMenuPrimitive.SubTrigger 211 + data-slot="dropdown-menu-sub-trigger" 212 + data-inset={inset} 213 + className={cn( 214 + "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[inset]:pl-8 data-[state=open]:text-accent-foreground", 215 + className, 216 + )} 217 + {...props} 218 + > 219 + {children} 220 + <ChevronRightIcon className="ml-auto size-4" /> 221 + </DropdownMenuPrimitive.SubTrigger> 222 + ); 223 + } 224 + 225 + function DropdownMenuSubContent({ 226 + className, 227 + ...props 228 + }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) { 229 + return ( 230 + <DropdownMenuPrimitive.SubContent 231 + data-slot="dropdown-menu-sub-content" 232 + className={cn( 233 + "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=closed]:animate-out data-[state=open]:animate-in", 234 + className, 235 + )} 236 + {...props} 237 + /> 238 + ); 239 + } 240 + 241 + export { 242 + DropdownMenu, 243 + DropdownMenuPortal, 244 + DropdownMenuTrigger, 245 + DropdownMenuContent, 246 + DropdownMenuGroup, 247 + DropdownMenuLabel, 248 + DropdownMenuItem, 249 + DropdownMenuCheckboxItem, 250 + DropdownMenuRadioGroup, 251 + DropdownMenuRadioItem, 252 + DropdownMenuSeparator, 253 + DropdownMenuShortcut, 254 + DropdownMenuSub, 255 + DropdownMenuSubTrigger, 256 + DropdownMenuSubContent, 257 + };
+168
apps/status-page/src/components/ui/form.tsx
··· 1 + "use client"; 2 + 3 + import type * as LabelPrimitive from "@radix-ui/react-label"; 4 + import { Slot } from "@radix-ui/react-slot"; 5 + import * as React from "react"; 6 + import { 7 + Controller, 8 + type ControllerProps, 9 + type FieldPath, 10 + type FieldValues, 11 + FormProvider, 12 + useFormContext, 13 + useFormState, 14 + } from "react-hook-form"; 15 + 16 + import { Label } from "@/components/ui/label"; 17 + import { cn } from "@/lib/utils"; 18 + 19 + const Form = FormProvider; 20 + 21 + type FormFieldContextValue< 22 + TFieldValues extends FieldValues = FieldValues, 23 + TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, 24 + > = { 25 + name: TName; 26 + }; 27 + 28 + const FormFieldContext = React.createContext<FormFieldContextValue>( 29 + {} as FormFieldContextValue, 30 + ); 31 + 32 + const FormField = < 33 + TFieldValues extends FieldValues = FieldValues, 34 + TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, 35 + >({ 36 + ...props 37 + }: ControllerProps<TFieldValues, TName>) => { 38 + return ( 39 + <FormFieldContext.Provider value={{ name: props.name }}> 40 + <Controller {...props} /> 41 + </FormFieldContext.Provider> 42 + ); 43 + }; 44 + 45 + const useFormField = () => { 46 + const fieldContext = React.useContext(FormFieldContext); 47 + const itemContext = React.useContext(FormItemContext); 48 + const { getFieldState } = useFormContext(); 49 + const formState = useFormState({ name: fieldContext.name }); 50 + const fieldState = getFieldState(fieldContext.name, formState); 51 + 52 + if (!fieldContext) { 53 + throw new Error("useFormField should be used within <FormField>"); 54 + } 55 + 56 + const { id } = itemContext; 57 + 58 + return { 59 + id, 60 + name: fieldContext.name, 61 + formItemId: `${id}-form-item`, 62 + formDescriptionId: `${id}-form-item-description`, 63 + formMessageId: `${id}-form-item-message`, 64 + ...fieldState, 65 + }; 66 + }; 67 + 68 + type FormItemContextValue = { 69 + id: string; 70 + }; 71 + 72 + const FormItemContext = React.createContext<FormItemContextValue>( 73 + {} as FormItemContextValue, 74 + ); 75 + 76 + function FormItem({ className, ...props }: React.ComponentProps<"div">) { 77 + const id = React.useId(); 78 + 79 + return ( 80 + <FormItemContext.Provider value={{ id }}> 81 + <div 82 + data-slot="form-item" 83 + className={cn("grid gap-2", className)} 84 + {...props} 85 + /> 86 + </FormItemContext.Provider> 87 + ); 88 + } 89 + 90 + function FormLabel({ 91 + className, 92 + ...props 93 + }: React.ComponentProps<typeof LabelPrimitive.Root>) { 94 + const { error, formItemId } = useFormField(); 95 + 96 + return ( 97 + <Label 98 + data-slot="form-label" 99 + data-error={!!error} 100 + className={cn("data-[error=true]:text-destructive", className)} 101 + htmlFor={formItemId} 102 + {...props} 103 + /> 104 + ); 105 + } 106 + 107 + function FormControl({ ...props }: React.ComponentProps<typeof Slot>) { 108 + const { error, formItemId, formDescriptionId, formMessageId } = 109 + useFormField(); 110 + 111 + return ( 112 + <Slot 113 + data-slot="form-control" 114 + id={formItemId} 115 + aria-describedby={ 116 + !error 117 + ? `${formDescriptionId}` 118 + : `${formDescriptionId} ${formMessageId}` 119 + } 120 + aria-invalid={!!error} 121 + {...props} 122 + /> 123 + ); 124 + } 125 + 126 + function FormDescription({ className, ...props }: React.ComponentProps<"p">) { 127 + const { formDescriptionId } = useFormField(); 128 + 129 + return ( 130 + <p 131 + data-slot="form-description" 132 + id={formDescriptionId} 133 + className={cn("text-muted-foreground text-sm", className)} 134 + {...props} 135 + /> 136 + ); 137 + } 138 + 139 + function FormMessage({ className, ...props }: React.ComponentProps<"p">) { 140 + const { error, formMessageId } = useFormField(); 141 + const body = error ? String(error?.message ?? "") : props.children; 142 + 143 + if (!body) { 144 + return null; 145 + } 146 + 147 + return ( 148 + <p 149 + data-slot="form-message" 150 + id={formMessageId} 151 + className={cn("text-destructive text-sm", className)} 152 + {...props} 153 + > 154 + {body} 155 + </p> 156 + ); 157 + } 158 + 159 + export { 160 + useFormField, 161 + Form, 162 + FormItem, 163 + FormLabel, 164 + FormControl, 165 + FormDescription, 166 + FormMessage, 167 + FormField, 168 + };
+44
apps/status-page/src/components/ui/hover-card.tsx
··· 1 + "use client"; 2 + 3 + import * as HoverCardPrimitive from "@radix-ui/react-hover-card"; 4 + import type * as React from "react"; 5 + 6 + import { cn } from "@/lib/utils"; 7 + 8 + function HoverCard({ 9 + ...props 10 + }: React.ComponentProps<typeof HoverCardPrimitive.Root>) { 11 + return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />; 12 + } 13 + 14 + function HoverCardTrigger({ 15 + ...props 16 + }: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) { 17 + return ( 18 + <HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} /> 19 + ); 20 + } 21 + 22 + function HoverCardContent({ 23 + className, 24 + align = "center", 25 + sideOffset = 4, 26 + ...props 27 + }: React.ComponentProps<typeof HoverCardPrimitive.Content>) { 28 + return ( 29 + <HoverCardPrimitive.Portal data-slot="hover-card-portal"> 30 + <HoverCardPrimitive.Content 31 + data-slot="hover-card-content" 32 + align={align} 33 + sideOffset={sideOffset} 34 + className={cn( 35 + "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[state=closed]:animate-out data-[state=open]:animate-in", 36 + className, 37 + )} 38 + {...props} 39 + /> 40 + </HoverCardPrimitive.Portal> 41 + ); 42 + } 43 + 44 + export { HoverCard, HoverCardTrigger, HoverCardContent };
+21
apps/status-page/src/components/ui/input.tsx
··· 1 + import type * as React from "react"; 2 + 3 + import { cn } from "@/lib/utils"; 4 + 5 + function Input({ className, type, ...props }: React.ComponentProps<"input">) { 6 + return ( 7 + <input 8 + type={type} 9 + data-slot="input" 10 + className={cn( 11 + "flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs outline-none transition-[color,box-shadow] selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:font-medium file:text-foreground file:text-sm placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30", 12 + "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50", 13 + "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40", 14 + className, 15 + )} 16 + {...props} 17 + /> 18 + ); 19 + } 20 + 21 + export { Input };
+24
apps/status-page/src/components/ui/label.tsx
··· 1 + "use client"; 2 + 3 + import * as LabelPrimitive from "@radix-ui/react-label"; 4 + import type * as React from "react"; 5 + 6 + import { cn } from "@/lib/utils"; 7 + 8 + function Label({ 9 + className, 10 + ...props 11 + }: React.ComponentProps<typeof LabelPrimitive.Root>) { 12 + return ( 13 + <LabelPrimitive.Root 14 + data-slot="label" 15 + className={cn( 16 + "flex select-none items-center gap-2 font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50 group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50", 17 + className, 18 + )} 19 + {...props} 20 + /> 21 + ); 22 + } 23 + 24 + export { Label };
+48
apps/status-page/src/components/ui/popover.tsx
··· 1 + "use client"; 2 + 3 + import * as PopoverPrimitive from "@radix-ui/react-popover"; 4 + import type * as React from "react"; 5 + 6 + import { cn } from "@/lib/utils"; 7 + 8 + function Popover({ 9 + ...props 10 + }: React.ComponentProps<typeof PopoverPrimitive.Root>) { 11 + return <PopoverPrimitive.Root data-slot="popover" {...props} />; 12 + } 13 + 14 + function PopoverTrigger({ 15 + ...props 16 + }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) { 17 + return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />; 18 + } 19 + 20 + function PopoverContent({ 21 + className, 22 + align = "center", 23 + sideOffset = 4, 24 + ...props 25 + }: React.ComponentProps<typeof PopoverPrimitive.Content>) { 26 + return ( 27 + <PopoverPrimitive.Portal> 28 + <PopoverPrimitive.Content 29 + data-slot="popover-content" 30 + align={align} 31 + sideOffset={sideOffset} 32 + className={cn( 33 + "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[state=closed]:animate-out data-[state=open]:animate-in", 34 + className, 35 + )} 36 + {...props} 37 + /> 38 + </PopoverPrimitive.Portal> 39 + ); 40 + } 41 + 42 + function PopoverAnchor({ 43 + ...props 44 + }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) { 45 + return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />; 46 + } 47 + 48 + export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
+31
apps/status-page/src/components/ui/progress.tsx
··· 1 + "use client"; 2 + 3 + import * as ProgressPrimitive from "@radix-ui/react-progress"; 4 + import type * as React from "react"; 5 + 6 + import { cn } from "@/lib/utils"; 7 + 8 + function Progress({ 9 + className, 10 + value, 11 + ...props 12 + }: React.ComponentProps<typeof ProgressPrimitive.Root>) { 13 + return ( 14 + <ProgressPrimitive.Root 15 + data-slot="progress" 16 + className={cn( 17 + "relative h-2 w-full overflow-hidden rounded-full bg-primary/20", 18 + className, 19 + )} 20 + {...props} 21 + > 22 + <ProgressPrimitive.Indicator 23 + data-slot="progress-indicator" 24 + className="h-full w-full flex-1 bg-primary transition-all" 25 + style={{ transform: `translateX(-${100 - (value || 0)}%)` }} 26 + /> 27 + </ProgressPrimitive.Root> 28 + ); 29 + } 30 + 31 + export { Progress };
+45
apps/status-page/src/components/ui/radio-group.tsx
··· 1 + "use client"; 2 + 3 + import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"; 4 + import { CircleIcon } from "lucide-react"; 5 + import type * as React from "react"; 6 + 7 + import { cn } from "@/lib/utils"; 8 + 9 + function RadioGroup({ 10 + className, 11 + ...props 12 + }: React.ComponentProps<typeof RadioGroupPrimitive.Root>) { 13 + return ( 14 + <RadioGroupPrimitive.Root 15 + data-slot="radio-group" 16 + className={cn("grid gap-3", className)} 17 + {...props} 18 + /> 19 + ); 20 + } 21 + 22 + function RadioGroupItem({ 23 + className, 24 + ...props 25 + }: React.ComponentProps<typeof RadioGroupPrimitive.Item>) { 26 + return ( 27 + <RadioGroupPrimitive.Item 28 + data-slot="radio-group-item" 29 + className={cn( 30 + "aspect-square size-4 shrink-0 rounded-full border border-input text-primary shadow-xs outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:aria-invalid:ring-destructive/40", 31 + className, 32 + )} 33 + {...props} 34 + > 35 + <RadioGroupPrimitive.Indicator 36 + data-slot="radio-group-indicator" 37 + className="relative flex items-center justify-center" 38 + > 39 + <CircleIcon className="-translate-x-1/2 -translate-y-1/2 absolute top-1/2 left-1/2 size-2 fill-primary" /> 40 + </RadioGroupPrimitive.Indicator> 41 + </RadioGroupPrimitive.Item> 42 + ); 43 + } 44 + 45 + export { RadioGroup, RadioGroupItem };
+185
apps/status-page/src/components/ui/select.tsx
··· 1 + "use client"; 2 + 3 + import * as SelectPrimitive from "@radix-ui/react-select"; 4 + import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; 5 + import type * as React from "react"; 6 + 7 + import { cn } from "@/lib/utils"; 8 + 9 + function Select({ 10 + ...props 11 + }: React.ComponentProps<typeof SelectPrimitive.Root>) { 12 + return <SelectPrimitive.Root data-slot="select" {...props} />; 13 + } 14 + 15 + function SelectGroup({ 16 + ...props 17 + }: React.ComponentProps<typeof SelectPrimitive.Group>) { 18 + return <SelectPrimitive.Group data-slot="select-group" {...props} />; 19 + } 20 + 21 + function SelectValue({ 22 + ...props 23 + }: React.ComponentProps<typeof SelectPrimitive.Value>) { 24 + return <SelectPrimitive.Value data-slot="select-value" {...props} />; 25 + } 26 + 27 + function SelectTrigger({ 28 + className, 29 + size = "default", 30 + children, 31 + ...props 32 + }: React.ComponentProps<typeof SelectPrimitive.Trigger> & { 33 + size?: "sm" | "default"; 34 + }) { 35 + return ( 36 + <SelectPrimitive.Trigger 37 + data-slot="select-trigger" 38 + data-size={size} 39 + className={cn( 40 + "flex w-fit items-center justify-between gap-2 whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[size=default]:h-9 data-[size=sm]:h-8 data-[placeholder]:text-muted-foreground *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:aria-invalid:ring-destructive/40 dark:hover:bg-input/50 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0", 41 + className, 42 + )} 43 + {...props} 44 + > 45 + {children} 46 + <SelectPrimitive.Icon asChild> 47 + <ChevronDownIcon className="size-4 opacity-50" /> 48 + </SelectPrimitive.Icon> 49 + </SelectPrimitive.Trigger> 50 + ); 51 + } 52 + 53 + function SelectContent({ 54 + className, 55 + children, 56 + position = "popper", 57 + ...props 58 + }: React.ComponentProps<typeof SelectPrimitive.Content>) { 59 + return ( 60 + <SelectPrimitive.Portal> 61 + <SelectPrimitive.Content 62 + data-slot="select-content" 63 + className={cn( 64 + "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=closed]:animate-out data-[state=open]:animate-in", 65 + position === "popper" && 66 + "data-[side=left]:-translate-x-1 data-[side=top]:-translate-y-1 data-[side=right]:translate-x-1 data-[side=bottom]:translate-y-1", 67 + className, 68 + )} 69 + position={position} 70 + {...props} 71 + > 72 + <SelectScrollUpButton /> 73 + <SelectPrimitive.Viewport 74 + className={cn( 75 + "p-1", 76 + position === "popper" && 77 + "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1", 78 + )} 79 + > 80 + {children} 81 + </SelectPrimitive.Viewport> 82 + <SelectScrollDownButton /> 83 + </SelectPrimitive.Content> 84 + </SelectPrimitive.Portal> 85 + ); 86 + } 87 + 88 + function SelectLabel({ 89 + className, 90 + ...props 91 + }: React.ComponentProps<typeof SelectPrimitive.Label>) { 92 + return ( 93 + <SelectPrimitive.Label 94 + data-slot="select-label" 95 + className={cn("px-2 py-1.5 text-muted-foreground text-xs", className)} 96 + {...props} 97 + /> 98 + ); 99 + } 100 + 101 + function SelectItem({ 102 + className, 103 + children, 104 + ...props 105 + }: React.ComponentProps<typeof SelectPrimitive.Item>) { 106 + return ( 107 + <SelectPrimitive.Item 108 + data-slot="select-item" 109 + className={cn( 110 + "relative flex w-full cursor-default select-none items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", 111 + className, 112 + )} 113 + {...props} 114 + > 115 + <span className="absolute right-2 flex size-3.5 items-center justify-center"> 116 + <SelectPrimitive.ItemIndicator> 117 + <CheckIcon className="size-4" /> 118 + </SelectPrimitive.ItemIndicator> 119 + </span> 120 + <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> 121 + </SelectPrimitive.Item> 122 + ); 123 + } 124 + 125 + function SelectSeparator({ 126 + className, 127 + ...props 128 + }: React.ComponentProps<typeof SelectPrimitive.Separator>) { 129 + return ( 130 + <SelectPrimitive.Separator 131 + data-slot="select-separator" 132 + className={cn("-mx-1 pointer-events-none my-1 h-px bg-border", className)} 133 + {...props} 134 + /> 135 + ); 136 + } 137 + 138 + function SelectScrollUpButton({ 139 + className, 140 + ...props 141 + }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) { 142 + return ( 143 + <SelectPrimitive.ScrollUpButton 144 + data-slot="select-scroll-up-button" 145 + className={cn( 146 + "flex cursor-default items-center justify-center py-1", 147 + className, 148 + )} 149 + {...props} 150 + > 151 + <ChevronUpIcon className="size-4" /> 152 + </SelectPrimitive.ScrollUpButton> 153 + ); 154 + } 155 + 156 + function SelectScrollDownButton({ 157 + className, 158 + ...props 159 + }: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) { 160 + return ( 161 + <SelectPrimitive.ScrollDownButton 162 + data-slot="select-scroll-down-button" 163 + className={cn( 164 + "flex cursor-default items-center justify-center py-1", 165 + className, 166 + )} 167 + {...props} 168 + > 169 + <ChevronDownIcon className="size-4" /> 170 + </SelectPrimitive.ScrollDownButton> 171 + ); 172 + } 173 + 174 + export { 175 + Select, 176 + SelectContent, 177 + SelectGroup, 178 + SelectItem, 179 + SelectLabel, 180 + SelectScrollDownButton, 181 + SelectScrollUpButton, 182 + SelectSeparator, 183 + SelectTrigger, 184 + SelectValue, 185 + };
+28
apps/status-page/src/components/ui/separator.tsx
··· 1 + "use client"; 2 + 3 + import * as SeparatorPrimitive from "@radix-ui/react-separator"; 4 + import type * as React from "react"; 5 + 6 + import { cn } from "@/lib/utils"; 7 + 8 + function Separator({ 9 + className, 10 + orientation = "horizontal", 11 + decorative = true, 12 + ...props 13 + }: React.ComponentProps<typeof SeparatorPrimitive.Root>) { 14 + return ( 15 + <SeparatorPrimitive.Root 16 + data-slot="separator-root" 17 + decorative={decorative} 18 + orientation={orientation} 19 + className={cn( 20 + "shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px", 21 + className, 22 + )} 23 + {...props} 24 + /> 25 + ); 26 + } 27 + 28 + export { Separator };
+139
apps/status-page/src/components/ui/sheet.tsx
··· 1 + "use client"; 2 + 3 + import * as SheetPrimitive from "@radix-ui/react-dialog"; 4 + import { XIcon } from "lucide-react"; 5 + import type * as React from "react"; 6 + 7 + import { cn } from "@/lib/utils"; 8 + 9 + function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) { 10 + return <SheetPrimitive.Root data-slot="sheet" {...props} />; 11 + } 12 + 13 + function SheetTrigger({ 14 + ...props 15 + }: React.ComponentProps<typeof SheetPrimitive.Trigger>) { 16 + return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />; 17 + } 18 + 19 + function SheetClose({ 20 + ...props 21 + }: React.ComponentProps<typeof SheetPrimitive.Close>) { 22 + return <SheetPrimitive.Close data-slot="sheet-close" {...props} />; 23 + } 24 + 25 + function SheetPortal({ 26 + ...props 27 + }: React.ComponentProps<typeof SheetPrimitive.Portal>) { 28 + return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />; 29 + } 30 + 31 + function SheetOverlay({ 32 + className, 33 + ...props 34 + }: React.ComponentProps<typeof SheetPrimitive.Overlay>) { 35 + return ( 36 + <SheetPrimitive.Overlay 37 + data-slot="sheet-overlay" 38 + className={cn( 39 + "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=open]:animate-in", 40 + className, 41 + )} 42 + {...props} 43 + /> 44 + ); 45 + } 46 + 47 + function SheetContent({ 48 + className, 49 + children, 50 + side = "right", 51 + ...props 52 + }: React.ComponentProps<typeof SheetPrimitive.Content> & { 53 + side?: "top" | "right" | "bottom" | "left"; 54 + }) { 55 + return ( 56 + <SheetPortal> 57 + <SheetOverlay /> 58 + <SheetPrimitive.Content 59 + data-slot="sheet-content" 60 + className={cn( 61 + "fixed z-50 flex flex-col gap-4 bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=open]:animate-in data-[state=closed]:duration-300 data-[state=open]:duration-500", 62 + side === "right" && 63 + "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm", 64 + side === "left" && 65 + "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm", 66 + side === "top" && 67 + "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b", 68 + side === "bottom" && 69 + "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t", 70 + className, 71 + )} 72 + {...props} 73 + > 74 + {children} 75 + <SheetPrimitive.Close className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary"> 76 + <XIcon className="size-4" /> 77 + <span className="sr-only">Close</span> 78 + </SheetPrimitive.Close> 79 + </SheetPrimitive.Content> 80 + </SheetPortal> 81 + ); 82 + } 83 + 84 + function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { 85 + return ( 86 + <div 87 + data-slot="sheet-header" 88 + className={cn("flex flex-col gap-1.5 p-4", className)} 89 + {...props} 90 + /> 91 + ); 92 + } 93 + 94 + function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { 95 + return ( 96 + <div 97 + data-slot="sheet-footer" 98 + className={cn("mt-auto flex flex-col gap-2 p-4", className)} 99 + {...props} 100 + /> 101 + ); 102 + } 103 + 104 + function SheetTitle({ 105 + className, 106 + ...props 107 + }: React.ComponentProps<typeof SheetPrimitive.Title>) { 108 + return ( 109 + <SheetPrimitive.Title 110 + data-slot="sheet-title" 111 + className={cn("font-semibold text-foreground", className)} 112 + {...props} 113 + /> 114 + ); 115 + } 116 + 117 + function SheetDescription({ 118 + className, 119 + ...props 120 + }: React.ComponentProps<typeof SheetPrimitive.Description>) { 121 + return ( 122 + <SheetPrimitive.Description 123 + data-slot="sheet-description" 124 + className={cn("text-muted-foreground text-sm", className)} 125 + {...props} 126 + /> 127 + ); 128 + } 129 + 130 + export { 131 + Sheet, 132 + SheetTrigger, 133 + SheetClose, 134 + SheetContent, 135 + SheetHeader, 136 + SheetFooter, 137 + SheetTitle, 138 + SheetDescription, 139 + };
+731
apps/status-page/src/components/ui/sidebar.tsx
··· 1 + "use client"; 2 + 3 + import { Slot } from "@radix-ui/react-slot"; 4 + import { type VariantProps, cva } from "class-variance-authority"; 5 + import { PanelLeftIcon } from "lucide-react"; 6 + import * as React from "react"; 7 + 8 + import { Button } from "@/components/ui/button"; 9 + import { Input } from "@/components/ui/input"; 10 + import { Separator } from "@/components/ui/separator"; 11 + import { 12 + Sheet, 13 + SheetContent, 14 + SheetDescription, 15 + SheetHeader, 16 + SheetTitle, 17 + } from "@/components/ui/sheet"; 18 + import { Skeleton } from "@/components/ui/skeleton"; 19 + import { 20 + Tooltip, 21 + TooltipContent, 22 + TooltipProvider, 23 + TooltipTrigger, 24 + } from "@/components/ui/tooltip"; 25 + import { useIsMobile } from "@/hooks/use-mobile"; 26 + import { cn } from "@/lib/utils"; 27 + 28 + const SIDEBAR_COOKIE_NAME = "sidebar_state"; 29 + const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; 30 + const SIDEBAR_WIDTH = "16rem"; 31 + const SIDEBAR_WIDTH_MOBILE = "18rem"; 32 + const SIDEBAR_WIDTH_ICON = "3rem"; 33 + const SIDEBAR_KEYBOARD_SHORTCUT = "b"; 34 + 35 + type SidebarContextProps = { 36 + state: "expanded" | "collapsed"; 37 + open: boolean; 38 + setOpen: (open: boolean) => void; 39 + openMobile: boolean; 40 + setOpenMobile: (open: boolean) => void; 41 + isMobile: boolean; 42 + toggleSidebar: () => void; 43 + }; 44 + 45 + const SidebarContext = React.createContext<SidebarContextProps | null>(null); 46 + 47 + function useSidebar() { 48 + const context = React.useContext(SidebarContext); 49 + if (!context) { 50 + throw new Error("useSidebar must be used within a SidebarProvider."); 51 + } 52 + 53 + return context; 54 + } 55 + 56 + function SidebarProvider({ 57 + defaultOpen = true, 58 + open: openProp, 59 + onOpenChange: setOpenProp, 60 + className, 61 + style, 62 + children, 63 + cookieName, 64 + ...props 65 + }: React.ComponentProps<"div"> & { 66 + defaultOpen?: boolean; 67 + open?: boolean; 68 + onOpenChange?: (open: boolean) => void; 69 + // NOTE: change from default shadcn sidebar 70 + cookieName?: string; 71 + }) { 72 + const isMobile = useIsMobile(); 73 + const [openMobile, setOpenMobile] = React.useState(false); 74 + 75 + // This is the internal state of the sidebar. 76 + // We use openProp and setOpenProp for control from outside the component. 77 + const [_open, _setOpen] = React.useState(defaultOpen); 78 + const open = openProp ?? _open; 79 + const setOpen = React.useCallback( 80 + (value: boolean | ((value: boolean) => boolean)) => { 81 + const openState = typeof value === "function" ? value(open) : value; 82 + if (setOpenProp) { 83 + setOpenProp(openState); 84 + } else { 85 + _setOpen(openState); 86 + } 87 + 88 + // This sets the cookie to keep the sidebar state. 89 + document.cookie = `${cookieName ?? SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; 90 + }, 91 + [setOpenProp, open, cookieName], 92 + ); 93 + 94 + // Helper to toggle the sidebar. 95 + // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation> 96 + const toggleSidebar = React.useCallback(() => { 97 + return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open); 98 + }, [isMobile, setOpen, setOpenMobile]); 99 + 100 + // Adds a keyboard shortcut to toggle the sidebar. 101 + React.useEffect(() => { 102 + const handleKeyDown = (event: KeyboardEvent) => { 103 + if ( 104 + event.key === SIDEBAR_KEYBOARD_SHORTCUT && 105 + (event.metaKey || event.ctrlKey) 106 + ) { 107 + event.preventDefault(); 108 + toggleSidebar(); 109 + } 110 + }; 111 + 112 + window.addEventListener("keydown", handleKeyDown); 113 + return () => window.removeEventListener("keydown", handleKeyDown); 114 + }, [toggleSidebar]); 115 + 116 + // We add a state so that we can do data-state="expanded" or "collapsed". 117 + // This makes it easier to style the sidebar with Tailwind classes. 118 + const state = open ? "expanded" : "collapsed"; 119 + 120 + // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation> 121 + const contextValue = React.useMemo<SidebarContextProps>( 122 + () => ({ 123 + state, 124 + open, 125 + setOpen, 126 + isMobile, 127 + openMobile, 128 + setOpenMobile, 129 + toggleSidebar, 130 + }), 131 + [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar], 132 + ); 133 + 134 + return ( 135 + <SidebarContext.Provider value={contextValue}> 136 + <TooltipProvider delayDuration={0}> 137 + <div 138 + data-slot="sidebar-wrapper" 139 + style={ 140 + { 141 + "--sidebar-width": SIDEBAR_WIDTH, 142 + "--sidebar-width-icon": SIDEBAR_WIDTH_ICON, 143 + ...style, 144 + } as React.CSSProperties 145 + } 146 + className={cn( 147 + "group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar", 148 + className, 149 + )} 150 + {...props} 151 + > 152 + {children} 153 + </div> 154 + </TooltipProvider> 155 + </SidebarContext.Provider> 156 + ); 157 + } 158 + 159 + function Sidebar({ 160 + side = "left", 161 + variant = "sidebar", 162 + collapsible = "offcanvas", 163 + className, 164 + children, 165 + ...props 166 + }: React.ComponentProps<"div"> & { 167 + side?: "left" | "right"; 168 + variant?: "sidebar" | "floating" | "inset"; 169 + collapsible?: "offcanvas" | "icon" | "none"; 170 + }) { 171 + const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); 172 + 173 + if (collapsible === "none") { 174 + return ( 175 + <div 176 + data-slot="sidebar" 177 + className={cn( 178 + "flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground", 179 + className, 180 + )} 181 + {...props} 182 + > 183 + {children} 184 + </div> 185 + ); 186 + } 187 + 188 + if (isMobile) { 189 + return ( 190 + <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}> 191 + <SheetContent 192 + data-sidebar="sidebar" 193 + data-slot="sidebar" 194 + data-mobile="true" 195 + className="w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden" 196 + style={ 197 + { 198 + "--sidebar-width": SIDEBAR_WIDTH_MOBILE, 199 + } as React.CSSProperties 200 + } 201 + side={side} 202 + > 203 + <SheetHeader className="sr-only"> 204 + <SheetTitle>Sidebar</SheetTitle> 205 + <SheetDescription>Displays the mobile sidebar.</SheetDescription> 206 + </SheetHeader> 207 + <div className="flex h-full w-full flex-col">{children}</div> 208 + </SheetContent> 209 + </Sheet> 210 + ); 211 + } 212 + 213 + return ( 214 + <div 215 + className="group peer hidden text-sidebar-foreground md:block" 216 + data-state={state} 217 + data-collapsible={state === "collapsed" ? collapsible : ""} 218 + data-variant={variant} 219 + data-side={side} 220 + data-slot="sidebar" 221 + > 222 + {/* This is what handles the sidebar gap on desktop */} 223 + <div 224 + data-slot="sidebar-gap" 225 + className={cn( 226 + "relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear", 227 + "group-data-[collapsible=offcanvas]:w-0", 228 + "group-data-[side=right]:rotate-180", 229 + variant === "floating" || variant === "inset" 230 + ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]" 231 + : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)", 232 + )} 233 + /> 234 + <div 235 + data-slot="sidebar-container" 236 + className={cn( 237 + "fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex", 238 + side === "left" 239 + ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]" 240 + : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]", 241 + // Adjust the padding for floating and inset variants. 242 + variant === "floating" || variant === "inset" 243 + ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]" 244 + : "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l", 245 + className, 246 + )} 247 + {...props} 248 + > 249 + <div 250 + data-sidebar="sidebar" 251 + data-slot="sidebar-inner" 252 + className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow-sm" 253 + > 254 + {children} 255 + </div> 256 + </div> 257 + </div> 258 + ); 259 + } 260 + 261 + function SidebarTrigger({ 262 + className, 263 + onClick, 264 + ...props 265 + }: React.ComponentProps<typeof Button>) { 266 + const { toggleSidebar } = useSidebar(); 267 + 268 + return ( 269 + <Button 270 + data-sidebar="trigger" 271 + data-slot="sidebar-trigger" 272 + variant="ghost" 273 + size="icon" 274 + className={cn("size-7", className)} 275 + onClick={(event) => { 276 + onClick?.(event); 277 + toggleSidebar(); 278 + }} 279 + {...props} 280 + > 281 + <PanelLeftIcon /> 282 + <span className="sr-only">Toggle Sidebar</span> 283 + </Button> 284 + ); 285 + } 286 + 287 + function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { 288 + const { toggleSidebar } = useSidebar(); 289 + 290 + return ( 291 + <button 292 + data-sidebar="rail" 293 + data-slot="sidebar-rail" 294 + aria-label="Toggle Sidebar" 295 + tabIndex={-1} 296 + onClick={toggleSidebar} 297 + title="Toggle Sidebar" 298 + className={cn( 299 + "-translate-x-1/2 group-data-[side=left]:-right-4 absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=right]:left-0 sm:flex", 300 + "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize", 301 + "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize", 302 + "group-data-[collapsible=offcanvas]:translate-x-0 hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:after:left-full", 303 + "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2", 304 + "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2", 305 + className, 306 + )} 307 + {...props} 308 + /> 309 + ); 310 + } 311 + 312 + function SidebarInset({ className, ...props }: React.ComponentProps<"main">) { 313 + return ( 314 + <main 315 + data-slot="sidebar-inset" 316 + className={cn( 317 + "relative flex w-full flex-1 flex-col bg-background", 318 + "md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2 md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm", 319 + className, 320 + )} 321 + {...props} 322 + /> 323 + ); 324 + } 325 + 326 + function SidebarInput({ 327 + className, 328 + ...props 329 + }: React.ComponentProps<typeof Input>) { 330 + return ( 331 + <Input 332 + data-slot="sidebar-input" 333 + data-sidebar="input" 334 + className={cn("h-8 w-full bg-background shadow-none", className)} 335 + {...props} 336 + /> 337 + ); 338 + } 339 + 340 + function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) { 341 + return ( 342 + <div 343 + data-slot="sidebar-header" 344 + data-sidebar="header" 345 + className={cn("flex flex-col gap-2 p-2", className)} 346 + {...props} 347 + /> 348 + ); 349 + } 350 + 351 + function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) { 352 + return ( 353 + <div 354 + data-slot="sidebar-footer" 355 + data-sidebar="footer" 356 + className={cn("flex flex-col gap-2 p-2", className)} 357 + {...props} 358 + /> 359 + ); 360 + } 361 + 362 + function SidebarSeparator({ 363 + className, 364 + ...props 365 + }: React.ComponentProps<typeof Separator>) { 366 + return ( 367 + <Separator 368 + data-slot="sidebar-separator" 369 + data-sidebar="separator" 370 + className={cn("mx-2 w-auto bg-sidebar-border", className)} 371 + {...props} 372 + /> 373 + ); 374 + } 375 + 376 + function SidebarContent({ className, ...props }: React.ComponentProps<"div">) { 377 + return ( 378 + <div 379 + data-slot="sidebar-content" 380 + data-sidebar="content" 381 + className={cn( 382 + "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden", 383 + className, 384 + )} 385 + {...props} 386 + /> 387 + ); 388 + } 389 + 390 + function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) { 391 + return ( 392 + <div 393 + data-slot="sidebar-group" 394 + data-sidebar="group" 395 + className={cn("relative flex w-full min-w-0 flex-col p-2", className)} 396 + {...props} 397 + /> 398 + ); 399 + } 400 + 401 + function SidebarGroupLabel({ 402 + className, 403 + asChild = false, 404 + ...props 405 + }: React.ComponentProps<"div"> & { asChild?: boolean }) { 406 + const Comp = asChild ? Slot : "div"; 407 + 408 + return ( 409 + <Comp 410 + data-slot="sidebar-group-label" 411 + data-sidebar="group-label" 412 + className={cn( 413 + "flex h-8 shrink-0 items-center rounded-md px-2 font-medium text-sidebar-foreground/70 text-xs outline-hidden ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", 414 + "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0", 415 + className, 416 + )} 417 + {...props} 418 + /> 419 + ); 420 + } 421 + 422 + function SidebarGroupAction({ 423 + className, 424 + asChild = false, 425 + ...props 426 + }: React.ComponentProps<"button"> & { asChild?: boolean }) { 427 + const Comp = asChild ? Slot : "button"; 428 + 429 + return ( 430 + <Comp 431 + data-slot="sidebar-group-action" 432 + data-sidebar="group-action" 433 + className={cn( 434 + "absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-hidden ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", 435 + // Increases the hit area of the button on mobile. 436 + "after:-inset-2 after:absolute md:after:hidden", 437 + "group-data-[collapsible=icon]:hidden", 438 + className, 439 + )} 440 + {...props} 441 + /> 442 + ); 443 + } 444 + 445 + function SidebarGroupContent({ 446 + className, 447 + ...props 448 + }: React.ComponentProps<"div">) { 449 + return ( 450 + <div 451 + data-slot="sidebar-group-content" 452 + data-sidebar="group-content" 453 + className={cn("w-full text-sm", className)} 454 + {...props} 455 + /> 456 + ); 457 + } 458 + 459 + function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) { 460 + return ( 461 + <ul 462 + data-slot="sidebar-menu" 463 + data-sidebar="menu" 464 + className={cn("flex w-full min-w-0 flex-col gap-1", className)} 465 + {...props} 466 + /> 467 + ); 468 + } 469 + 470 + function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) { 471 + return ( 472 + <li 473 + data-slot="sidebar-menu-item" 474 + data-sidebar="menu-item" 475 + className={cn("group/menu-item relative", className)} 476 + {...props} 477 + /> 478 + ); 479 + } 480 + 481 + const sidebarMenuButtonVariants = cva( 482 + "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", 483 + { 484 + variants: { 485 + variant: { 486 + default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", 487 + outline: 488 + "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]", 489 + }, 490 + size: { 491 + default: "h-8 text-sm", 492 + sm: "h-7 text-xs", 493 + lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!", 494 + }, 495 + }, 496 + defaultVariants: { 497 + variant: "default", 498 + size: "default", 499 + }, 500 + }, 501 + ); 502 + 503 + function SidebarMenuButton({ 504 + asChild = false, 505 + isActive = false, 506 + variant = "default", 507 + size = "default", 508 + tooltip, 509 + className, 510 + ...props 511 + }: React.ComponentProps<"button"> & { 512 + asChild?: boolean; 513 + isActive?: boolean; 514 + tooltip?: string | React.ComponentProps<typeof TooltipContent>; 515 + } & VariantProps<typeof sidebarMenuButtonVariants>) { 516 + const Comp = asChild ? Slot : "button"; 517 + const { isMobile, state } = useSidebar(); 518 + 519 + const button = ( 520 + <Comp 521 + data-slot="sidebar-menu-button" 522 + data-sidebar="menu-button" 523 + data-size={size} 524 + data-active={isActive} 525 + className={cn(sidebarMenuButtonVariants({ variant, size }), className)} 526 + {...props} 527 + /> 528 + ); 529 + 530 + if (!tooltip) { 531 + return button; 532 + } 533 + 534 + if (typeof tooltip === "string") { 535 + tooltip = { 536 + children: tooltip, 537 + }; 538 + } 539 + 540 + return ( 541 + <Tooltip> 542 + <TooltipTrigger asChild>{button}</TooltipTrigger> 543 + <TooltipContent 544 + side="right" 545 + align="center" 546 + hidden={state !== "collapsed" || isMobile} 547 + {...tooltip} 548 + /> 549 + </Tooltip> 550 + ); 551 + } 552 + 553 + function SidebarMenuAction({ 554 + className, 555 + asChild = false, 556 + showOnHover = false, 557 + ...props 558 + }: React.ComponentProps<"button"> & { 559 + asChild?: boolean; 560 + showOnHover?: boolean; 561 + }) { 562 + const Comp = asChild ? Slot : "button"; 563 + 564 + return ( 565 + <Comp 566 + data-slot="sidebar-menu-action" 567 + data-sidebar="menu-action" 568 + className={cn( 569 + "absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-hidden ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0", 570 + // Increases the hit area of the button on mobile. 571 + "after:-inset-2 after:absolute md:after:hidden", 572 + "peer-data-[size=sm]/menu-button:top-1", 573 + "peer-data-[size=default]/menu-button:top-1.5", 574 + "peer-data-[size=lg]/menu-button:top-2.5", 575 + "group-data-[collapsible=icon]:hidden", 576 + showOnHover && 577 + "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0", 578 + className, 579 + )} 580 + {...props} 581 + /> 582 + ); 583 + } 584 + 585 + function SidebarMenuBadge({ 586 + className, 587 + ...props 588 + }: React.ComponentProps<"div">) { 589 + return ( 590 + <div 591 + data-slot="sidebar-menu-badge" 592 + data-sidebar="menu-badge" 593 + className={cn( 594 + "pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 font-medium text-sidebar-foreground text-xs tabular-nums", 595 + "peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground", 596 + "peer-data-[size=sm]/menu-button:top-1", 597 + "peer-data-[size=default]/menu-button:top-1.5", 598 + "peer-data-[size=lg]/menu-button:top-2.5", 599 + "group-data-[collapsible=icon]:hidden", 600 + className, 601 + )} 602 + {...props} 603 + /> 604 + ); 605 + } 606 + 607 + function SidebarMenuSkeleton({ 608 + className, 609 + showIcon = false, 610 + ...props 611 + }: React.ComponentProps<"div"> & { 612 + showIcon?: boolean; 613 + }) { 614 + // Random width between 50 to 90%. 615 + const width = React.useMemo(() => { 616 + return `${Math.floor(Math.random() * 40) + 50}%`; 617 + }, []); 618 + 619 + return ( 620 + <div 621 + data-slot="sidebar-menu-skeleton" 622 + data-sidebar="menu-skeleton" 623 + className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)} 624 + {...props} 625 + > 626 + {showIcon && ( 627 + <Skeleton 628 + className="size-4 rounded-md" 629 + data-sidebar="menu-skeleton-icon" 630 + /> 631 + )} 632 + <Skeleton 633 + className="h-4 max-w-(--skeleton-width) flex-1" 634 + data-sidebar="menu-skeleton-text" 635 + style={ 636 + { 637 + "--skeleton-width": width, 638 + } as React.CSSProperties 639 + } 640 + /> 641 + </div> 642 + ); 643 + } 644 + 645 + function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) { 646 + return ( 647 + <ul 648 + data-slot="sidebar-menu-sub" 649 + data-sidebar="menu-sub" 650 + className={cn( 651 + "mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-sidebar-border border-l px-2.5 py-0.5", 652 + "group-data-[collapsible=icon]:hidden", 653 + className, 654 + )} 655 + {...props} 656 + /> 657 + ); 658 + } 659 + 660 + function SidebarMenuSubItem({ 661 + className, 662 + ...props 663 + }: React.ComponentProps<"li">) { 664 + return ( 665 + <li 666 + data-slot="sidebar-menu-sub-item" 667 + data-sidebar="menu-sub-item" 668 + className={cn("group/menu-sub-item relative", className)} 669 + {...props} 670 + /> 671 + ); 672 + } 673 + 674 + function SidebarMenuSubButton({ 675 + asChild = false, 676 + size = "md", 677 + isActive = false, 678 + className, 679 + ...props 680 + }: React.ComponentProps<"a"> & { 681 + asChild?: boolean; 682 + size?: "sm" | "md"; 683 + isActive?: boolean; 684 + }) { 685 + const Comp = asChild ? Slot : "a"; 686 + 687 + return ( 688 + <Comp 689 + data-slot="sidebar-menu-sub-button" 690 + data-sidebar="menu-sub-button" 691 + data-size={size} 692 + data-active={isActive} 693 + className={cn( 694 + "-translate-x-px flex h-7 min-w-0 items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-hidden ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground", 695 + "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground", 696 + size === "sm" && "text-xs", 697 + size === "md" && "text-sm", 698 + "group-data-[collapsible=icon]:hidden", 699 + className, 700 + )} 701 + {...props} 702 + /> 703 + ); 704 + } 705 + 706 + export { 707 + Sidebar, 708 + SidebarContent, 709 + SidebarFooter, 710 + SidebarGroup, 711 + SidebarGroupAction, 712 + SidebarGroupContent, 713 + SidebarGroupLabel, 714 + SidebarHeader, 715 + SidebarInput, 716 + SidebarInset, 717 + SidebarMenu, 718 + SidebarMenuAction, 719 + SidebarMenuBadge, 720 + SidebarMenuButton, 721 + SidebarMenuItem, 722 + SidebarMenuSkeleton, 723 + SidebarMenuSub, 724 + SidebarMenuSubButton, 725 + SidebarMenuSubItem, 726 + SidebarProvider, 727 + SidebarRail, 728 + SidebarSeparator, 729 + SidebarTrigger, 730 + useSidebar, 731 + };
+13
apps/status-page/src/components/ui/skeleton.tsx
··· 1 + import { cn } from "@/lib/utils"; 2 + 3 + function Skeleton({ className, ...props }: React.ComponentProps<"div">) { 4 + return ( 5 + <div 6 + data-slot="skeleton" 7 + className={cn("animate-pulse rounded-md bg-accent", className)} 8 + {...props} 9 + /> 10 + ); 11 + } 12 + 13 + export { Skeleton };
+63
apps/status-page/src/components/ui/slider.tsx
··· 1 + "use client"; 2 + 3 + import * as SliderPrimitive from "@radix-ui/react-slider"; 4 + import * as React from "react"; 5 + 6 + import { cn } from "@/lib/utils"; 7 + 8 + function Slider({ 9 + className, 10 + defaultValue, 11 + value, 12 + min = 0, 13 + max = 100, 14 + ...props 15 + }: React.ComponentProps<typeof SliderPrimitive.Root>) { 16 + const _values = React.useMemo( 17 + () => 18 + Array.isArray(value) 19 + ? value 20 + : Array.isArray(defaultValue) 21 + ? defaultValue 22 + : [min, max], 23 + [value, defaultValue, min, max], 24 + ); 25 + 26 + return ( 27 + <SliderPrimitive.Root 28 + data-slot="slider" 29 + defaultValue={defaultValue} 30 + value={value} 31 + min={min} 32 + max={max} 33 + className={cn( 34 + "relative flex w-full touch-none select-none items-center data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col data-[disabled]:opacity-50", 35 + className, 36 + )} 37 + {...props} 38 + > 39 + <SliderPrimitive.Track 40 + data-slot="slider-track" 41 + className={cn( 42 + "relative grow overflow-hidden rounded-full bg-muted data-[orientation=horizontal]:h-1.5 data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-1.5", 43 + )} 44 + > 45 + <SliderPrimitive.Range 46 + data-slot="slider-range" 47 + className={cn( 48 + "absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full", 49 + )} 50 + /> 51 + </SliderPrimitive.Track> 52 + {Array.from({ length: _values.length }, (_, index) => ( 53 + <SliderPrimitive.Thumb 54 + data-slot="slider-thumb" 55 + key={index} 56 + className="block size-4 shrink-0 rounded-full border border-primary bg-background shadow-sm ring-ring/50 transition-[color,box-shadow] hover:ring-4 focus-visible:outline-hidden focus-visible:ring-4 disabled:pointer-events-none disabled:opacity-50" 57 + /> 58 + ))} 59 + </SliderPrimitive.Root> 60 + ); 61 + } 62 + 63 + export { Slider };
+25
apps/status-page/src/components/ui/sonner.tsx
··· 1 + "use client"; 2 + 3 + import { useTheme } from "next-themes"; 4 + import { Toaster as Sonner, type ToasterProps } from "sonner"; 5 + 6 + const Toaster = ({ ...props }: ToasterProps) => { 7 + const { theme = "system" } = useTheme(); 8 + 9 + return ( 10 + <Sonner 11 + theme={theme as ToasterProps["theme"]} 12 + className="toaster group" 13 + style={ 14 + { 15 + "--normal-bg": "var(--popover)", 16 + "--normal-text": "var(--popover-foreground)", 17 + "--normal-border": "var(--border)", 18 + } as React.CSSProperties 19 + } 20 + {...props} 21 + /> 22 + ); 23 + }; 24 + 25 + export { Toaster };
+581
apps/status-page/src/components/ui/sortable.tsx
··· 1 + "use client"; 2 + 3 + import { 4 + type Announcements, 5 + DndContext, 6 + type DndContextProps, 7 + type DragEndEvent, 8 + DragOverlay, 9 + type DraggableSyntheticListeners, 10 + type DropAnimation, 11 + KeyboardSensor, 12 + MouseSensor, 13 + type ScreenReaderInstructions, 14 + TouchSensor, 15 + type UniqueIdentifier, 16 + closestCenter, 17 + closestCorners, 18 + defaultDropAnimationSideEffects, 19 + useSensor, 20 + useSensors, 21 + } from "@dnd-kit/core"; 22 + import { 23 + restrictToHorizontalAxis, 24 + restrictToParentElement, 25 + restrictToVerticalAxis, 26 + } from "@dnd-kit/modifiers"; 27 + import { 28 + SortableContext, 29 + type SortableContextProps, 30 + arrayMove, 31 + horizontalListSortingStrategy, 32 + sortableKeyboardCoordinates, 33 + useSortable, 34 + verticalListSortingStrategy, 35 + } from "@dnd-kit/sortable"; 36 + import { CSS } from "@dnd-kit/utilities"; 37 + import { Slot } from "@radix-ui/react-slot"; 38 + import * as React from "react"; 39 + import * as ReactDOM from "react-dom"; 40 + 41 + import { composeEventHandlers, useComposedRefs } from "@/lib/composition"; 42 + import { cn } from "@/lib/utils"; 43 + 44 + const orientationConfig = { 45 + vertical: { 46 + modifiers: [restrictToVerticalAxis, restrictToParentElement], 47 + strategy: verticalListSortingStrategy, 48 + collisionDetection: closestCenter, 49 + }, 50 + horizontal: { 51 + modifiers: [restrictToHorizontalAxis, restrictToParentElement], 52 + strategy: horizontalListSortingStrategy, 53 + collisionDetection: closestCenter, 54 + }, 55 + mixed: { 56 + modifiers: [restrictToParentElement], 57 + strategy: undefined, 58 + collisionDetection: closestCorners, 59 + }, 60 + }; 61 + 62 + const ROOT_NAME = "Sortable"; 63 + const CONTENT_NAME = "SortableContent"; 64 + const ITEM_NAME = "SortableItem"; 65 + const ITEM_HANDLE_NAME = "SortableItemHandle"; 66 + const OVERLAY_NAME = "SortableOverlay"; 67 + 68 + const SORTABLE_ERRORS = { 69 + [ROOT_NAME]: `\`${ROOT_NAME}\` components must be within \`${ROOT_NAME}\``, 70 + [CONTENT_NAME]: `\`${CONTENT_NAME}\` must be within \`${ROOT_NAME}\``, 71 + [ITEM_NAME]: `\`${ITEM_NAME}\` must be within \`${CONTENT_NAME}\``, 72 + [ITEM_HANDLE_NAME]: `\`${ITEM_HANDLE_NAME}\` must be within \`${ITEM_NAME}\``, 73 + [OVERLAY_NAME]: `\`${OVERLAY_NAME}\` must be within \`${ROOT_NAME}\``, 74 + } as const; 75 + 76 + interface SortableRootContextValue<T> { 77 + id: string; 78 + items: UniqueIdentifier[]; 79 + modifiers: DndContextProps["modifiers"]; 80 + strategy: SortableContextProps["strategy"]; 81 + activeId: UniqueIdentifier | null; 82 + setActiveId: (id: UniqueIdentifier | null) => void; 83 + getItemValue: (item: T) => UniqueIdentifier; 84 + flatCursor: boolean; 85 + } 86 + 87 + const SortableRootContext = 88 + React.createContext<SortableRootContextValue<unknown> | null>(null); 89 + SortableRootContext.displayName = ROOT_NAME; 90 + 91 + function useSortableContext(name: keyof typeof SORTABLE_ERRORS) { 92 + const context = React.useContext(SortableRootContext); 93 + if (!context) { 94 + throw new Error(SORTABLE_ERRORS[name]); 95 + } 96 + return context; 97 + } 98 + 99 + interface GetItemValue<T> { 100 + /** 101 + * Callback that returns a unique identifier for each sortable item. Required for array of objects. 102 + * @example getItemValue={(item) => item.id} 103 + */ 104 + getItemValue: (item: T) => UniqueIdentifier; 105 + } 106 + 107 + type SortableProps<T> = DndContextProps & { 108 + value: T[]; 109 + onValueChange?: (items: T[]) => void; 110 + onMove?: ( 111 + event: DragEndEvent & { activeIndex: number; overIndex: number }, 112 + ) => void; 113 + strategy?: SortableContextProps["strategy"]; 114 + orientation?: "vertical" | "horizontal" | "mixed"; 115 + flatCursor?: boolean; 116 + } & (T extends object ? GetItemValue<T> : Partial<GetItemValue<T>>); 117 + 118 + function Sortable<T>(props: SortableProps<T>) { 119 + const { 120 + value, 121 + onValueChange, 122 + collisionDetection, 123 + modifiers, 124 + strategy, 125 + onMove, 126 + orientation = "vertical", 127 + flatCursor = false, 128 + getItemValue: getItemValueProp, 129 + accessibility, 130 + ...sortableProps 131 + } = props; 132 + const id = React.useId(); 133 + const [activeId, setActiveId] = React.useState<UniqueIdentifier | null>(null); 134 + 135 + const sensors = useSensors( 136 + useSensor(MouseSensor), 137 + useSensor(TouchSensor), 138 + useSensor(KeyboardSensor, { 139 + coordinateGetter: sortableKeyboardCoordinates, 140 + }), 141 + ); 142 + const config = React.useMemo( 143 + () => orientationConfig[orientation], 144 + [orientation], 145 + ); 146 + 147 + const getItemValue = React.useCallback( 148 + (item: T): UniqueIdentifier => { 149 + if (typeof item === "object" && !getItemValueProp) { 150 + throw new Error( 151 + "getItemValue is required when using array of objects.", 152 + ); 153 + } 154 + return getItemValueProp 155 + ? getItemValueProp(item) 156 + : (item as UniqueIdentifier); 157 + }, 158 + [getItemValueProp], 159 + ); 160 + 161 + const items = React.useMemo(() => { 162 + return value.map((item) => getItemValue(item)); 163 + }, [value, getItemValue]); 164 + 165 + const onDragEnd = React.useCallback( 166 + (event: DragEndEvent) => { 167 + const { active, over } = event; 168 + if (over && active.id !== over?.id) { 169 + const activeIndex = value.findIndex( 170 + (item) => getItemValue(item) === active.id, 171 + ); 172 + const overIndex = value.findIndex( 173 + (item) => getItemValue(item) === over.id, 174 + ); 175 + 176 + if (onMove) { 177 + onMove({ ...event, activeIndex, overIndex }); 178 + } else { 179 + onValueChange?.(arrayMove(value, activeIndex, overIndex)); 180 + } 181 + } 182 + setActiveId(null); 183 + }, 184 + [value, onValueChange, onMove, getItemValue], 185 + ); 186 + 187 + const announcements: Announcements = React.useMemo( 188 + () => ({ 189 + onDragStart({ active }) { 190 + const activeValue = active.id.toString(); 191 + return `Grabbed sortable item "${activeValue}". Current position is ${ 192 + active.data.current?.sortable.index + 1 193 + } of ${value.length}. Use arrow keys to move, space to drop.`; 194 + }, 195 + onDragOver({ active, over }) { 196 + if (over) { 197 + const overIndex = over.data.current?.sortable.index ?? 0; 198 + const activeIndex = active.data.current?.sortable.index ?? 0; 199 + const moveDirection = overIndex > activeIndex ? "down" : "up"; 200 + const activeValue = active.id.toString(); 201 + return `Sortable item "${activeValue}" moved ${moveDirection} to position ${ 202 + overIndex + 1 203 + } of ${value.length}.`; 204 + } 205 + return "Sortable item is no longer over a droppable area. Press escape to cancel."; 206 + }, 207 + onDragEnd({ active, over }) { 208 + const activeValue = active.id.toString(); 209 + if (over) { 210 + const overIndex = over.data.current?.sortable.index ?? 0; 211 + return `Sortable item "${activeValue}" dropped at position ${ 212 + overIndex + 1 213 + } of ${value.length}.`; 214 + } 215 + return `Sortable item "${activeValue}" dropped. No changes were made.`; 216 + }, 217 + onDragCancel({ active }) { 218 + const activeIndex = active.data.current?.sortable.index ?? 0; 219 + const activeValue = active.id.toString(); 220 + return `Sorting cancelled. Sortable item "${activeValue}" returned to position ${ 221 + activeIndex + 1 222 + } of ${value.length}.`; 223 + }, 224 + onDragMove({ active, over }) { 225 + if (over) { 226 + const overIndex = over.data.current?.sortable.index ?? 0; 227 + const activeIndex = active.data.current?.sortable.index ?? 0; 228 + const moveDirection = overIndex > activeIndex ? "down" : "up"; 229 + const activeValue = active.id.toString(); 230 + return `Sortable item "${activeValue}" is moving ${moveDirection} to position ${ 231 + overIndex + 1 232 + } of ${value.length}.`; 233 + } 234 + return "Sortable item is no longer over a droppable area. Press escape to cancel."; 235 + }, 236 + }), 237 + [value], 238 + ); 239 + 240 + const screenReaderInstructions: ScreenReaderInstructions = React.useMemo( 241 + () => ({ 242 + draggable: ` 243 + To pick up a sortable item, press space or enter. 244 + While dragging, use the ${ 245 + orientation === "vertical" 246 + ? "up and down" 247 + : orientation === "horizontal" 248 + ? "left and right" 249 + : "arrow" 250 + } keys to move the item. 251 + Press space or enter again to drop the item in its new position, or press escape to cancel. 252 + `, 253 + }), 254 + [orientation], 255 + ); 256 + 257 + const contextValue = React.useMemo( 258 + () => ({ 259 + id, 260 + items, 261 + modifiers: modifiers ?? config.modifiers, 262 + strategy: strategy ?? config.strategy, 263 + activeId, 264 + setActiveId, 265 + getItemValue, 266 + flatCursor, 267 + }), 268 + [ 269 + id, 270 + items, 271 + modifiers, 272 + strategy, 273 + config.modifiers, 274 + config.strategy, 275 + activeId, 276 + getItemValue, 277 + flatCursor, 278 + ], 279 + ); 280 + 281 + return ( 282 + <SortableRootContext.Provider 283 + value={contextValue as SortableRootContextValue<unknown>} 284 + > 285 + <DndContext 286 + collisionDetection={collisionDetection ?? config.collisionDetection} 287 + modifiers={modifiers ?? config.modifiers} 288 + sensors={sensors} 289 + {...sortableProps} 290 + id={id} 291 + onDragStart={composeEventHandlers( 292 + sortableProps.onDragStart, 293 + ({ active }) => setActiveId(active.id), 294 + )} 295 + onDragEnd={composeEventHandlers(sortableProps.onDragEnd, onDragEnd)} 296 + onDragCancel={composeEventHandlers(sortableProps.onDragCancel, () => 297 + setActiveId(null), 298 + )} 299 + accessibility={{ 300 + announcements, 301 + screenReaderInstructions, 302 + ...accessibility, 303 + }} 304 + /> 305 + </SortableRootContext.Provider> 306 + ); 307 + } 308 + 309 + const SortableContentContext = React.createContext<boolean>(false); 310 + SortableContentContext.displayName = CONTENT_NAME; 311 + 312 + interface SortableContentProps extends React.ComponentPropsWithoutRef<"div"> { 313 + strategy?: SortableContextProps["strategy"]; 314 + children: React.ReactNode; 315 + asChild?: boolean; 316 + withoutSlot?: boolean; 317 + } 318 + 319 + const SortableContent = React.forwardRef<HTMLDivElement, SortableContentProps>( 320 + (props, forwardedRef) => { 321 + const { 322 + strategy: strategyProp, 323 + asChild, 324 + withoutSlot, 325 + children, 326 + ...contentProps 327 + } = props; 328 + const context = useSortableContext(CONTENT_NAME); 329 + 330 + const ContentPrimitive = asChild ? Slot : "div"; 331 + 332 + return ( 333 + <SortableContentContext.Provider value={true}> 334 + <SortableContext 335 + items={context.items} 336 + strategy={strategyProp ?? context.strategy} 337 + > 338 + {withoutSlot ? ( 339 + children 340 + ) : ( 341 + <ContentPrimitive {...contentProps} ref={forwardedRef}> 342 + {children} 343 + </ContentPrimitive> 344 + )} 345 + </SortableContext> 346 + </SortableContentContext.Provider> 347 + ); 348 + }, 349 + ); 350 + SortableContent.displayName = CONTENT_NAME; 351 + 352 + interface SortableItemContextValue { 353 + id: string; 354 + attributes: React.HTMLAttributes<HTMLElement>; 355 + listeners: DraggableSyntheticListeners | undefined; 356 + setActivatorNodeRef: (node: HTMLElement | null) => void; 357 + isDragging?: boolean; 358 + disabled?: boolean; 359 + } 360 + 361 + const SortableItemContext = 362 + React.createContext<SortableItemContextValue | null>(null); 363 + SortableItemContext.displayName = ITEM_NAME; 364 + 365 + interface SortableItemProps extends React.ComponentPropsWithoutRef<"div"> { 366 + value: UniqueIdentifier; 367 + asHandle?: boolean; 368 + asChild?: boolean; 369 + disabled?: boolean; 370 + } 371 + 372 + const SortableItem = React.forwardRef<HTMLDivElement, SortableItemProps>( 373 + (props, forwardedRef) => { 374 + const { 375 + value, 376 + style, 377 + asHandle, 378 + asChild, 379 + disabled, 380 + className, 381 + ...itemProps 382 + } = props; 383 + const inSortableContent = React.useContext(SortableContentContext); 384 + const inSortableOverlay = React.useContext(SortableOverlayContext); 385 + 386 + if (!inSortableContent && !inSortableOverlay) { 387 + throw new Error(SORTABLE_ERRORS[ITEM_NAME]); 388 + } 389 + 390 + if (value === "") { 391 + throw new Error(`\`${ITEM_NAME}\` value cannot be an empty string`); 392 + } 393 + 394 + const context = useSortableContext(ITEM_NAME); 395 + const id = React.useId(); 396 + const { 397 + attributes, 398 + listeners, 399 + setNodeRef, 400 + setActivatorNodeRef, 401 + transform, 402 + transition, 403 + isDragging, 404 + } = useSortable({ id: value, disabled }); 405 + 406 + const composedRef = useComposedRefs(forwardedRef, (node) => { 407 + if (disabled) return; 408 + setNodeRef(node); 409 + if (asHandle) setActivatorNodeRef(node); 410 + }); 411 + 412 + const composedStyle = React.useMemo<React.CSSProperties>(() => { 413 + return { 414 + transform: CSS.Translate.toString(transform), 415 + transition, 416 + ...style, 417 + }; 418 + }, [transform, transition, style]); 419 + 420 + const itemContext = React.useMemo<SortableItemContextValue>( 421 + () => ({ 422 + id, 423 + attributes, 424 + listeners, 425 + setActivatorNodeRef, 426 + isDragging, 427 + disabled, 428 + }), 429 + [id, attributes, listeners, setActivatorNodeRef, isDragging, disabled], 430 + ); 431 + 432 + const ItemPrimitive = asChild ? Slot : "div"; 433 + 434 + return ( 435 + <SortableItemContext.Provider value={itemContext}> 436 + <ItemPrimitive 437 + id={id} 438 + data-dragging={isDragging ? "" : undefined} 439 + {...itemProps} 440 + {...(asHandle ? attributes : {})} 441 + {...(asHandle ? listeners : {})} 442 + tabIndex={disabled ? undefined : 0} 443 + ref={composedRef} 444 + style={composedStyle} 445 + className={cn( 446 + "focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1", 447 + { 448 + "touch-none select-none": asHandle, 449 + "cursor-default": context.flatCursor, 450 + "data-dragging:cursor-grabbing": !context.flatCursor, 451 + "cursor-grab": !isDragging && asHandle && !context.flatCursor, 452 + "opacity-50": isDragging, 453 + "pointer-events-none opacity-50": disabled, 454 + }, 455 + className, 456 + )} 457 + /> 458 + </SortableItemContext.Provider> 459 + ); 460 + }, 461 + ); 462 + SortableItem.displayName = ITEM_NAME; 463 + 464 + interface SortableItemHandleProps 465 + extends React.ComponentPropsWithoutRef<"button"> { 466 + asChild?: boolean; 467 + } 468 + 469 + const SortableItemHandle = React.forwardRef< 470 + HTMLButtonElement, 471 + SortableItemHandleProps 472 + >((props, forwardedRef) => { 473 + const { asChild, disabled, className, ...itemHandleProps } = props; 474 + const itemContext = React.useContext(SortableItemContext); 475 + if (!itemContext) { 476 + throw new Error(SORTABLE_ERRORS[ITEM_HANDLE_NAME]); 477 + } 478 + const context = useSortableContext(ITEM_HANDLE_NAME); 479 + 480 + const isDisabled = disabled ?? itemContext.disabled; 481 + 482 + const composedRef = useComposedRefs(forwardedRef, (node) => { 483 + if (!isDisabled) return; 484 + itemContext.setActivatorNodeRef(node); 485 + }); 486 + 487 + const HandlePrimitive = asChild ? Slot : "button"; 488 + 489 + return ( 490 + <HandlePrimitive 491 + type="button" 492 + aria-controls={itemContext.id} 493 + data-dragging={itemContext.isDragging ? "" : undefined} 494 + {...itemHandleProps} 495 + {...itemContext.attributes} 496 + {...itemContext.listeners} 497 + ref={composedRef} 498 + className={cn( 499 + "select-none disabled:pointer-events-none disabled:opacity-50", 500 + context.flatCursor 501 + ? "cursor-default" 502 + : "cursor-grab data-dragging:cursor-grabbing", 503 + className, 504 + )} 505 + disabled={isDisabled} 506 + /> 507 + ); 508 + }); 509 + SortableItemHandle.displayName = ITEM_HANDLE_NAME; 510 + 511 + const SortableOverlayContext = React.createContext(false); 512 + SortableOverlayContext.displayName = OVERLAY_NAME; 513 + 514 + const dropAnimation: DropAnimation = { 515 + sideEffects: defaultDropAnimationSideEffects({ 516 + styles: { 517 + active: { 518 + opacity: "0.4", 519 + }, 520 + }, 521 + }), 522 + }; 523 + 524 + interface SortableOverlayProps 525 + extends Omit<React.ComponentPropsWithoutRef<typeof DragOverlay>, "children"> { 526 + container?: Element | DocumentFragment | null; 527 + children?: 528 + | ((params: { value: UniqueIdentifier }) => React.ReactNode) 529 + | React.ReactNode; 530 + } 531 + 532 + function SortableOverlay(props: SortableOverlayProps) { 533 + const { container: containerProp, children, ...overlayProps } = props; 534 + const context = useSortableContext(OVERLAY_NAME); 535 + 536 + const [mounted, setMounted] = React.useState(false); 537 + React.useLayoutEffect(() => setMounted(true), []); 538 + 539 + const container = 540 + containerProp ?? (mounted ? globalThis.document?.body : null); 541 + 542 + if (!container) return null; 543 + 544 + return ReactDOM.createPortal( 545 + <DragOverlay 546 + dropAnimation={dropAnimation} 547 + modifiers={context.modifiers} 548 + className={cn(!context.flatCursor && "cursor-grabbing")} 549 + {...overlayProps} 550 + > 551 + <SortableOverlayContext.Provider value={true}> 552 + {context.activeId 553 + ? typeof children === "function" 554 + ? children({ value: context.activeId }) 555 + : children 556 + : null} 557 + </SortableOverlayContext.Provider> 558 + </DragOverlay>, 559 + container, 560 + ); 561 + } 562 + 563 + const Root = Sortable; 564 + const Content = SortableContent; 565 + const Item = SortableItem; 566 + const ItemHandle = SortableItemHandle; 567 + const Overlay = SortableOverlay; 568 + 569 + export { 570 + Root, 571 + Content, 572 + Item, 573 + ItemHandle, 574 + Overlay, 575 + // 576 + Sortable, 577 + SortableContent, 578 + SortableItem, 579 + SortableItemHandle, 580 + SortableOverlay, 581 + };
+31
apps/status-page/src/components/ui/switch.tsx
··· 1 + "use client"; 2 + 3 + import * as SwitchPrimitive from "@radix-ui/react-switch"; 4 + import type * as React from "react"; 5 + 6 + import { cn } from "@/lib/utils"; 7 + 8 + function Switch({ 9 + className, 10 + ...props 11 + }: React.ComponentProps<typeof SwitchPrimitive.Root>) { 12 + return ( 13 + <SwitchPrimitive.Root 14 + data-slot="switch" 15 + className={cn( 16 + "peer inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-input/80", 17 + className, 18 + )} 19 + {...props} 20 + > 21 + <SwitchPrimitive.Thumb 22 + data-slot="switch-thumb" 23 + className={cn( 24 + "pointer-events-none block size-4 rounded-full bg-background ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0 dark:data-[state=checked]:bg-primary-foreground dark:data-[state=unchecked]:bg-foreground", 25 + )} 26 + /> 27 + </SwitchPrimitive.Root> 28 + ); 29 + } 30 + 31 + export { Switch };
+116
apps/status-page/src/components/ui/table.tsx
··· 1 + "use client"; 2 + 3 + import type * as React from "react"; 4 + 5 + import { cn } from "@/lib/utils"; 6 + 7 + function Table({ className, ...props }: React.ComponentProps<"table">) { 8 + return ( 9 + <div 10 + data-slot="table-container" 11 + className="relative w-full overflow-x-auto" 12 + > 13 + <table 14 + data-slot="table" 15 + className={cn("w-full caption-bottom text-sm", className)} 16 + {...props} 17 + /> 18 + </div> 19 + ); 20 + } 21 + 22 + function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { 23 + return ( 24 + <thead 25 + data-slot="table-header" 26 + className={cn("[&_tr]:border-b", className)} 27 + {...props} 28 + /> 29 + ); 30 + } 31 + 32 + function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { 33 + return ( 34 + <tbody 35 + data-slot="table-body" 36 + className={cn("[&_tr:last-child]:border-0", className)} 37 + {...props} 38 + /> 39 + ); 40 + } 41 + 42 + function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { 43 + return ( 44 + <tfoot 45 + data-slot="table-footer" 46 + className={cn( 47 + "border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", 48 + className, 49 + )} 50 + {...props} 51 + /> 52 + ); 53 + } 54 + 55 + function TableRow({ className, ...props }: React.ComponentProps<"tr">) { 56 + return ( 57 + <tr 58 + data-slot="table-row" 59 + className={cn( 60 + "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", 61 + className, 62 + )} 63 + {...props} 64 + /> 65 + ); 66 + } 67 + 68 + function TableHead({ className, ...props }: React.ComponentProps<"th">) { 69 + return ( 70 + <th 71 + data-slot="table-head" 72 + className={cn( 73 + "h-10 whitespace-nowrap px-2 text-left align-middle font-medium text-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", 74 + className, 75 + )} 76 + {...props} 77 + /> 78 + ); 79 + } 80 + 81 + function TableCell({ className, ...props }: React.ComponentProps<"td">) { 82 + return ( 83 + <td 84 + data-slot="table-cell" 85 + className={cn( 86 + "whitespace-nowrap p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", 87 + className, 88 + )} 89 + {...props} 90 + /> 91 + ); 92 + } 93 + 94 + function TableCaption({ 95 + className, 96 + ...props 97 + }: React.ComponentProps<"caption">) { 98 + return ( 99 + <caption 100 + data-slot="table-caption" 101 + className={cn("mt-4 text-muted-foreground text-sm", className)} 102 + {...props} 103 + /> 104 + ); 105 + } 106 + 107 + export { 108 + Table, 109 + TableHeader, 110 + TableBody, 111 + TableFooter, 112 + TableHead, 113 + TableRow, 114 + TableCell, 115 + TableCaption, 116 + };
+66
apps/status-page/src/components/ui/tabs.tsx
··· 1 + "use client"; 2 + 3 + import * as TabsPrimitive from "@radix-ui/react-tabs"; 4 + import type * as React from "react"; 5 + 6 + import { cn } from "@/lib/utils"; 7 + 8 + function Tabs({ 9 + className, 10 + ...props 11 + }: React.ComponentProps<typeof TabsPrimitive.Root>) { 12 + return ( 13 + <TabsPrimitive.Root 14 + data-slot="tabs" 15 + className={cn("flex flex-col gap-2", className)} 16 + {...props} 17 + /> 18 + ); 19 + } 20 + 21 + function TabsList({ 22 + className, 23 + ...props 24 + }: React.ComponentProps<typeof TabsPrimitive.List>) { 25 + return ( 26 + <TabsPrimitive.List 27 + data-slot="tabs-list" 28 + className={cn( 29 + "inline-flex h-9 w-fit items-center justify-center rounded-lg bg-muted p-[3px] text-muted-foreground", 30 + className, 31 + )} 32 + {...props} 33 + /> 34 + ); 35 + } 36 + 37 + function TabsTrigger({ 38 + className, 39 + ...props 40 + }: React.ComponentProps<typeof TabsPrimitive.Trigger>) { 41 + return ( 42 + <TabsPrimitive.Trigger 43 + data-slot="tabs-trigger" 44 + className={cn( 45 + "inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 whitespace-nowrap rounded-md border border-transparent px-2 py-1 font-medium text-foreground text-sm transition-[color,box-shadow] focus-visible:border-ring focus-visible:outline-1 focus-visible:outline-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:shadow-sm dark:text-muted-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", 46 + className, 47 + )} 48 + {...props} 49 + /> 50 + ); 51 + } 52 + 53 + function TabsContent({ 54 + className, 55 + ...props 56 + }: React.ComponentProps<typeof TabsPrimitive.Content>) { 57 + return ( 58 + <TabsPrimitive.Content 59 + data-slot="tabs-content" 60 + className={cn("flex-1 outline-none", className)} 61 + {...props} 62 + /> 63 + ); 64 + } 65 + 66 + export { Tabs, TabsList, TabsTrigger, TabsContent };
+18
apps/status-page/src/components/ui/textarea.tsx
··· 1 + import type * as React from "react"; 2 + 3 + import { cn } from "@/lib/utils"; 4 + 5 + function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { 6 + return ( 7 + <textarea 8 + data-slot="textarea" 9 + className={cn( 10 + "field-sizing-content flex min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs outline-none transition-[color,box-shadow] placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-destructive/40", 11 + className, 12 + )} 13 + {...props} 14 + /> 15 + ); 16 + } 17 + 18 + export { Textarea };
+61
apps/status-page/src/components/ui/tooltip.tsx
··· 1 + "use client"; 2 + 3 + import * as TooltipPrimitive from "@radix-ui/react-tooltip"; 4 + import type * as React from "react"; 5 + 6 + import { cn } from "@/lib/utils"; 7 + 8 + function TooltipProvider({ 9 + delayDuration = 0, 10 + ...props 11 + }: React.ComponentProps<typeof TooltipPrimitive.Provider>) { 12 + return ( 13 + <TooltipPrimitive.Provider 14 + data-slot="tooltip-provider" 15 + delayDuration={delayDuration} 16 + {...props} 17 + /> 18 + ); 19 + } 20 + 21 + function Tooltip({ 22 + ...props 23 + }: React.ComponentProps<typeof TooltipPrimitive.Root>) { 24 + return ( 25 + <TooltipProvider> 26 + <TooltipPrimitive.Root data-slot="tooltip" {...props} /> 27 + </TooltipProvider> 28 + ); 29 + } 30 + 31 + function TooltipTrigger({ 32 + ...props 33 + }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) { 34 + return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />; 35 + } 36 + 37 + function TooltipContent({ 38 + className, 39 + sideOffset = 0, 40 + children, 41 + ...props 42 + }: React.ComponentProps<typeof TooltipPrimitive.Content>) { 43 + return ( 44 + <TooltipPrimitive.Portal> 45 + <TooltipPrimitive.Content 46 + data-slot="tooltip-content" 47 + sideOffset={sideOffset} 48 + className={cn( 49 + "fade-in-0 zoom-in-95 data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in text-balance rounded-md bg-primary px-3 py-1.5 text-primary-foreground text-xs data-[state=closed]:animate-out", 50 + className, 51 + )} 52 + {...props} 53 + > 54 + {children} 55 + <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-primary fill-primary" /> 56 + </TooltipPrimitive.Content> 57 + </TooltipPrimitive.Portal> 58 + ); 59 + } 60 + 61 + export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
+40
apps/status-page/src/data/audit-logs.client.ts
··· 1 + import { 2 + CircleAlert, 3 + CircleCheck, 4 + CircleMinus, 5 + Send, 6 + Siren, 7 + } from "lucide-react"; 8 + 9 + export const config = { 10 + "incident.created": { 11 + icon: Siren, 12 + color: "text-destructive", 13 + title: "Incident Created", 14 + }, 15 + "incident.resolved": { 16 + icon: CircleCheck, 17 + color: "text-success", 18 + title: "Incident Resolved", 19 + }, 20 + "monitor.failed": { 21 + icon: CircleMinus, 22 + color: "text-destructive", 23 + title: "Monitor Failed", 24 + }, 25 + "notification.sent": { 26 + icon: Send, 27 + color: "text-info", 28 + title: "Notification Sent", 29 + }, 30 + "monitor.recovered": { 31 + icon: CircleCheck, 32 + color: "text-success", 33 + title: "Monitor Recovered", 34 + }, 35 + "monitor.degraded": { 36 + icon: CircleAlert, 37 + color: "text-warning", 38 + title: "Monitor Degraded", 39 + }, 40 + } as const;
+90
apps/status-page/src/data/audit-logs.ts
··· 1 + export const auditLogs = [ 2 + { 3 + id: 3, 4 + timestamp: new Date("2025-05-05 12:00:00"), 5 + action: "incident.created" as const, 6 + }, 7 + { 8 + id: 2, 9 + timestamp: new Date("2025-05-05 12:00:00"), 10 + action: "monitor.failed" as const, 11 + metadata: { 12 + region: "ams", 13 + status: 500, 14 + latency: 1400, 15 + } as const, 16 + }, 17 + { 18 + id: 1, 19 + timestamp: new Date("2025-05-05 12:00:00"), 20 + action: "notification.sent" as const, 21 + metadata: { 22 + provider: "slack", 23 + } as const, 24 + }, 25 + { 26 + id: 0, 27 + timestamp: new Date("2025-05-05 12:00:00"), 28 + action: "monitor.recovered" as const, 29 + metadata: { 30 + region: "ams", 31 + latency: 140, 32 + } as const, 33 + }, 34 + { 35 + id: -1, 36 + timestamp: new Date("2025-05-05 12:00:00"), 37 + action: "monitor.degraded" as const, 38 + metadata: { 39 + region: "ams", 40 + latency: 30_000, 41 + } as const, 42 + }, 43 + { 44 + id: -2, 45 + timestamp: new Date("2025-05-05 12:00:00"), 46 + action: "incident.resolved" as const, 47 + }, 48 + { 49 + id: -3, 50 + timestamp: new Date("2025-05-05 12:00:00"), 51 + action: "incident.created" as const, 52 + }, 53 + { 54 + id: -4, 55 + timestamp: new Date("2025-05-05 12:00:00"), 56 + action: "monitor.degraded" as const, 57 + metadata: { 58 + region: "ams", 59 + latency: 30_000, 60 + } as const, 61 + }, 62 + { 63 + id: -5, 64 + timestamp: new Date("2025-05-05 12:00:00"), 65 + action: "monitor.degraded" as const, 66 + metadata: { 67 + region: "ams", 68 + latency: 32_000, 69 + } as const, 70 + }, 71 + { 72 + id: -6, 73 + timestamp: new Date("2025-05-05 12:00:00"), 74 + action: "monitor.degraded" as const, 75 + metadata: { 76 + region: "ams", 77 + latency: 33_000, 78 + } as const, 79 + }, 80 + { 81 + id: -7, 82 + timestamp: new Date("2025-05-05 12:00:00"), 83 + action: "monitor.degraded" as const, 84 + metadata: { 85 + region: "ams", 86 + latency: 34_000, 87 + } as const, 88 + }, 89 + ]; 90 + export type AuditLog = (typeof auditLogs)[number];
+14
apps/status-page/src/data/icons.ts
··· 1 + "use client"; 2 + 3 + import { Activity, AlertCircle, SearchCheck } from "lucide-react"; 4 + 5 + export const status = { 6 + operational: SearchCheck, 7 + investigating: AlertCircle, 8 + identified: AlertCircle, 9 + monitoring: Activity, 10 + } as const; 11 + 12 + export const icons = { 13 + status, 14 + };
+33
apps/status-page/src/data/incidents.client.ts
··· 1 + import { Bookmark, Check, Trash2 } from "lucide-react"; 2 + 3 + export const actions = [ 4 + { 5 + id: "acknowledge", 6 + label: "Acknowledge", 7 + icon: Bookmark, 8 + variant: "default" as const, 9 + }, 10 + { 11 + id: "resolve", 12 + label: "Resolve", 13 + icon: Check, 14 + variant: "default" as const, 15 + }, 16 + { 17 + id: "delete", 18 + label: "Delete", 19 + icon: Trash2, 20 + variant: "destructive" as const, 21 + }, 22 + ] as const; 23 + 24 + export type IncidentAction = (typeof actions)[number]; 25 + 26 + export const getActions = ( 27 + props: Partial<Record<IncidentAction["id"], () => Promise<void> | void>>, 28 + ): (IncidentAction & { onClick?: () => Promise<void> | void })[] => { 29 + return actions.map((action) => ({ 30 + ...action, 31 + onClick: props[action.id as keyof typeof props], 32 + })); 33 + };
+11
apps/status-page/src/data/incidents.ts
··· 1 + export const incidents = [ 2 + { 3 + id: 1, 4 + startedAt: new Date("2025-05-05 12:00:00"), 5 + acknowledged: null, 6 + resolvedAt: new Date("2025-05-05 14:00:00"), 7 + monitor: "OpenStatus API", 8 + }, 9 + ]; 10 + 11 + export type Incident = (typeof incidents)[number];
+12
apps/status-page/src/data/invitations.ts
··· 1 + export const invitations = [ 2 + { 3 + id: 1, 4 + email: "thibault@openstatus.dev", 5 + role: "member", 6 + createdAt: "2021-01-01", 7 + expiresAt: "2021-01-07", 8 + acceptedAt: "2021-01-02", 9 + }, 10 + ]; 11 + 12 + export type Invitation = (typeof invitations)[number];
+27
apps/status-page/src/data/maintenances.client.ts
··· 1 + import { Pencil, Trash2 } from "lucide-react"; 2 + 3 + export const actions = [ 4 + { 5 + id: "edit", 6 + label: "Edit", 7 + icon: Pencil, 8 + variant: "default" as const, 9 + }, 10 + { 11 + id: "delete", 12 + label: "Delete", 13 + icon: Trash2, 14 + variant: "destructive" as const, 15 + }, 16 + ] as const; 17 + 18 + export type MaintenanceAction = (typeof actions)[number]; 19 + 20 + export const getActions = ( 21 + props: Partial<Record<MaintenanceAction["id"], () => Promise<void> | void>>, 22 + ): (MaintenanceAction & { onClick?: () => Promise<void> | void })[] => { 23 + return actions.map((action) => ({ 24 + ...action, 25 + onClick: props[action.id as keyof typeof props], 26 + })); 27 + };
+28
apps/status-page/src/data/maintenances.ts
··· 1 + const today = new Date(); 2 + const week = new Date(today); 3 + week.setDate(week.getDate() + 7); 4 + const hour = new Date(week); 5 + hour.setHours(hour.getHours() + 1); 6 + 7 + export const maintenances = [ 8 + { 9 + id: 1, 10 + title: "DB Migration", 11 + message: 12 + "We are performing a db migration on our system and will be down for a an hour.", 13 + startDate: week, 14 + endDate: hour, 15 + affected: ["OpenStatus API"], 16 + }, 17 + { 18 + id: 2, 19 + title: "System Upgrade", 20 + message: 21 + "We will be upgrading our core infrastructure to improve performance and reliability. Service interruptions may occur.", 22 + startDate: new Date("2025-03-01 11:00:00"), 23 + endDate: new Date("2025-03-01 15:30:00"), 24 + affected: ["OpenStatus API", "OpenStatus Web"], 25 + }, 26 + ]; 27 + 28 + export type Maintenance = (typeof maintenances)[number];
+11
apps/status-page/src/data/members.ts
··· 1 + export const members = [ 2 + { 3 + id: 1, 4 + name: "Maximilian Kaske", 5 + email: "max@openstatus.dev", 6 + role: "admin", 7 + createdAt: "2021-01-01", 8 + }, 9 + ]; 10 + 11 + export type Member = (typeof members)[number];
+34
apps/status-page/src/data/monitor-tags.ts
··· 1 + export const monitorTags = [ 2 + { 3 + value: "production", 4 + label: "Production", 5 + color: "bg-green-500", 6 + }, 7 + { 8 + value: "development", 9 + label: "Development", 10 + color: "bg-blue-500", 11 + }, 12 + { 13 + value: "staging", 14 + label: "Staging", 15 + color: "bg-yellow-500", 16 + }, 17 + { 18 + value: "testing", 19 + label: "Testing", 20 + color: "bg-purple-500", 21 + }, 22 + { 23 + value: "api", 24 + label: "API", 25 + color: "bg-red-500", 26 + }, 27 + { 28 + value: "database", 29 + label: "Database", 30 + color: "bg-orange-500", 31 + }, 32 + ]; 33 + 34 + export type MonitorTag = (typeof monitorTags)[number];
+45
apps/status-page/src/data/monitors.client.ts
··· 1 + import { Code, Copy, CopyPlus, Pencil, Trash2 } from "lucide-react"; 2 + 3 + export const actions = [ 4 + { 5 + id: "edit", 6 + label: "Edit", 7 + icon: Pencil, 8 + variant: "default" as const, 9 + }, 10 + { 11 + id: "copy-id", 12 + label: "Copy ID", 13 + icon: Copy, 14 + variant: "default" as const, 15 + }, 16 + { 17 + id: "clone", 18 + label: "Clone", 19 + icon: CopyPlus, 20 + variant: "default" as const, 21 + }, 22 + { 23 + id: "export", 24 + label: "Export Code", 25 + icon: Code, 26 + variant: "default" as const, 27 + }, 28 + { 29 + id: "delete", 30 + label: "Delete", 31 + icon: Trash2, 32 + variant: "destructive" as const, 33 + }, 34 + ] as const; 35 + 36 + export type MonitorAction = (typeof actions)[number]; 37 + 38 + export const getActions = ( 39 + props: Partial<Record<MonitorAction["id"], () => Promise<void> | void>>, 40 + ): (MonitorAction & { onClick?: () => Promise<void> | void })[] => { 41 + return actions.map((action) => ({ 42 + ...action, 43 + onClick: props[action.id as keyof typeof props], 44 + })); 45 + };
+88
apps/status-page/src/data/monitors.ts
··· 1 + export const monitors = [ 2 + { 3 + id: 1, 4 + name: "OpenStatus Marketing", 5 + description: "Marketing website for OpenStatus", 6 + public: true, 7 + active: true, 8 + status: "Normal" as const, 9 + url: "https://openstatus.dev", 10 + tags: ["Production"], 11 + lastIncident: undefined, 12 + p50: 110, 13 + p90: 200, 14 + p99: 250, 15 + }, 16 + { 17 + id: 2, 18 + name: "OpenStatus API", 19 + description: "API for OpenStatus", 20 + public: true, 21 + active: true, 22 + status: "Normal" as const, 23 + url: "https://api.openstatus.dev/v1/ping", 24 + tags: ["Production", "API"], 25 + lastIncident: undefined, 26 + p50: 34, 27 + p90: 201, 28 + p99: 530, 29 + }, 30 + { 31 + id: 3, 32 + name: "OpenStatus App", 33 + description: "Dashboard for OpenStatus", 34 + public: true, 35 + active: true, 36 + status: "Failing" as const, 37 + url: "https://openstatus.dev/app", 38 + tags: ["Production"], 39 + lastIncident: "10 minutes ago", 40 + p50: 130, 41 + p90: 200, 42 + p99: 250, 43 + }, 44 + { 45 + id: 4, 46 + name: "Lightweight OS", 47 + description: "Lightweight Operations System", 48 + public: false, 49 + active: false, 50 + status: "Inactive" as const, 51 + url: "https://data-table.openstatus.dev/light", 52 + tags: ["Development"], 53 + lastIncident: undefined, 54 + p50: undefined, 55 + p90: undefined, 56 + p99: undefined, 57 + }, 58 + { 59 + id: 5, 60 + name: "Astro Status Page", 61 + description: "Status page for Astro", 62 + public: false, 63 + active: true, 64 + status: "Degraded" as const, 65 + url: "https://status.openstat.us", 66 + tags: ["Development"], 67 + lastIncident: undefined, 68 + p50: 130, 69 + p90: 201, 70 + p99: 250, 71 + }, 72 + { 73 + id: 6, 74 + name: "Vercel Edge Ping", 75 + description: "Ping for Vercel Edge", 76 + public: false, 77 + active: true, 78 + status: "Normal" as const, 79 + url: "https://light.openstatus.dev", 80 + tags: ["Staging"], 81 + lastIncident: "15 days ago", 82 + p50: 30, 83 + p90: 240, 84 + p99: 400, 85 + }, 86 + ]; 87 + 88 + export type Monitor = (typeof monitors)[number];
+78
apps/status-page/src/data/notifiers.client.ts
··· 1 + import { FormDiscord } from "@/components/forms/notifier/form-discord"; 2 + import { FormEmail } from "@/components/forms/notifier/form-email"; 3 + import { FormSlack } from "@/components/forms/notifier/form-slack"; 4 + import { FormSms } from "@/components/forms/notifier/form-sms"; 5 + import { FormWebhook } from "@/components/forms/notifier/form-webhook"; 6 + import { DiscordIcon } from "@/components/icons/discord"; 7 + import { OpsGenieIcon } from "@/components/icons/opsgenie"; 8 + import { PagerDutyIcon } from "@/components/icons/pagerduty"; 9 + import { SlackIcon } from "@/components/icons/slack"; 10 + import { Mail, MessageCircle, Pencil, Trash2, Webhook } from "lucide-react"; 11 + 12 + export const actions = [ 13 + { 14 + id: "edit", 15 + label: "Edit", 16 + icon: Pencil, 17 + variant: "default" as const, 18 + }, 19 + { 20 + id: "delete", 21 + label: "Delete", 22 + icon: Trash2, 23 + variant: "destructive" as const, 24 + }, 25 + ] as const; 26 + 27 + export type NotifierAction = (typeof actions)[number]; 28 + 29 + export const getActions = ( 30 + props: Partial<Record<NotifierAction["id"], () => Promise<void> | void>>, 31 + ): (NotifierAction & { onClick?: () => Promise<void> | void })[] => { 32 + return actions.map((action) => ({ 33 + ...action, 34 + onClick: props[action.id as keyof typeof props], 35 + })); 36 + }; 37 + 38 + // List of the notifiers 39 + 40 + export const config = { 41 + slack: { 42 + icon: SlackIcon, 43 + label: "Slack", 44 + form: FormSlack, 45 + }, 46 + discord: { 47 + icon: DiscordIcon, 48 + label: "Discord", 49 + form: FormDiscord, 50 + }, 51 + email: { 52 + icon: Mail, 53 + label: "Email", 54 + form: FormEmail, 55 + }, 56 + sms: { 57 + icon: MessageCircle, 58 + label: "SMS", 59 + form: FormSms, 60 + }, 61 + webhook: { 62 + icon: Webhook, 63 + label: "Webhook", 64 + form: FormWebhook, 65 + }, 66 + opsgenie: { 67 + icon: OpsGenieIcon, 68 + label: "OpsGenie", 69 + form: undefined, 70 + }, 71 + pagerduty: { 72 + icon: PagerDutyIcon, 73 + label: "PagerDuty", 74 + form: undefined, 75 + }, 76 + }; 77 + 78 + export type NotifierProvider = keyof typeof config;
+10
apps/status-page/src/data/notifiers.ts
··· 1 + export const notifiers = [ 2 + { 3 + id: 1, 4 + name: "Email", 5 + provider: "email", 6 + value: "max@openstatus.dev", 7 + }, 8 + ]; 9 + 10 + export type Notifier = (typeof notifiers)[number];
+56
apps/status-page/src/data/plans.ts
··· 1 + export const plans = [ 2 + { 3 + title: "Hobby", 4 + id: "hobby", 5 + description: "Perfect for personal projects", 6 + price: 0, 7 + limits: { 8 + monitors: 1, 9 + regions: 35, 10 + periodicity: "10m", 11 + "status-pages": 1, 12 + members: 1, 13 + "notification-channels": 1, 14 + "custom-domain": false, 15 + "password-protection": false, 16 + "status-subscribers": false, 17 + "audit-log": false, 18 + }, 19 + }, 20 + { 21 + title: "Starter", 22 + id: "starter", 23 + description: "Perfect for uptime monitoring", 24 + price: 30, 25 + limits: { 26 + monitors: 10, 27 + regions: 35, 28 + periodicity: "1m", 29 + "status-pages": 1, 30 + members: Number.POSITIVE_INFINITY, 31 + "notification-channels": 10, 32 + "custom-domain": true, 33 + "password-protection": true, 34 + "status-subscribers": true, 35 + "audit-log": false, 36 + }, 37 + }, 38 + { 39 + title: "Pro", 40 + id: "team", 41 + description: "Perfect for global synthetic monitoring", 42 + price: 100, 43 + limits: { 44 + monitors: 100, 45 + regions: 35, 46 + periodicity: "30s", 47 + "status-pages": 5, 48 + members: Number.POSITIVE_INFINITY, 49 + "notification-channels": 20, 50 + "custom-domain": true, 51 + "password-protection": true, 52 + "status-subscribers": true, 53 + "audit-log": true, 54 + }, 55 + }, 56 + ];
+27
apps/status-page/src/data/region-metrics.client.ts
··· 1 + import { Filter, Zap } from "lucide-react"; 2 + 3 + export const actions = [ 4 + { 5 + id: "filter", 6 + label: "Filter", 7 + icon: Filter, 8 + variant: "default" as const, 9 + }, 10 + { 11 + id: "trigger", 12 + label: "Trigger", 13 + icon: Zap, 14 + variant: "default" as const, 15 + }, 16 + ] as const; 17 + 18 + export type RegionMetricAction = (typeof actions)[number]; 19 + 20 + export const getActions = ( 21 + props: Partial<Record<RegionMetricAction["id"], () => Promise<void> | void>>, 22 + ): (RegionMetricAction & { onClick?: () => Promise<void> | void })[] => { 23 + return actions.map((action) => ({ 24 + ...action, 25 + onClick: props[action.id as keyof typeof props], 26 + })); 27 + };
+27
apps/status-page/src/data/region-metrics.ts
··· 1 + import type { Region } from "./regions"; 2 + 3 + export const regionMetrics = [ 4 + { 5 + region: "ams" as const satisfies Region, 6 + p50: 100, 7 + p90: 150, 8 + p99: 200, 9 + trend: [{ latency: 100, timestamp: 1716729600 }], 10 + }, 11 + { 12 + region: "fra" as const satisfies Region, 13 + p50: 110, 14 + p90: 155, 15 + p99: 220, 16 + trend: [{ latency: 100, timestamp: 1716729600 }], 17 + }, 18 + { 19 + region: "gru" as const satisfies Region, 20 + p50: 120, 21 + p90: 160, 22 + p99: 230, 23 + trend: [{ latency: 100, timestamp: 1716729600 }], 24 + }, 25 + ]; 26 + 27 + export type RegionMetric = (typeof regionMetrics)[number];
+28
apps/status-page/src/data/region-percentile.ts
··· 1 + const randomizer = Math.random() * 10; 2 + 3 + export const regionPercentile = Array.from({ length: 30 }, (_, i) => ({ 4 + timestamp: new Date( 5 + new Date().setMinutes(new Date().getMinutes() - i), 6 + ).toLocaleString("default", { 7 + hour: "numeric", 8 + minute: "numeric", 9 + }), 10 + latency: Math.floor(Math.random() * randomizer + 1) * 100, 11 + })).map((item, i) => { 12 + const baseLatency = item.latency; 13 + const randomFactor = () => 0.85 + Math.random() * 0.3; // Random factor between 0.85-1.15 14 + 15 + return { 16 + ...item, 17 + // More realistic percentile distribution with randomness 18 + p50: Math.round(baseLatency * 0.7 * randomFactor()), 19 + p75: Math.round(baseLatency * 0.85 * randomFactor()), 20 + p90: Math.round(baseLatency * 1.1 * randomFactor()), 21 + p95: Math.round(baseLatency * 1.3 * randomFactor()), 22 + p99: Math.round(baseLatency * 1.8 * randomFactor()), 23 + // REMINDER: for error bars 24 + error: [4, 5, 6].includes(i) ? 1 : undefined, 25 + }; 26 + }); 27 + 28 + export type RegionPercentile = (typeof regionPercentile)[number];
+226
apps/status-page/src/data/regions.ts
··· 1 + export const regions = [ 2 + { 3 + code: "ams", 4 + location: "Amsterdam, Netherlands", 5 + flag: "🇳🇱", 6 + continent: "Europe", 7 + }, 8 + { 9 + code: "arn", 10 + location: "Stockholm, Sweden", 11 + flag: "🇸🇪", 12 + continent: "Europe", 13 + }, 14 + { 15 + code: "atl", 16 + location: "Atlanta, Georgia, USA", 17 + flag: "🇺🇸", 18 + continent: "North America", 19 + }, 20 + { 21 + code: "bog", 22 + location: "Bogotá, Colombia", 23 + flag: "🇨🇴", 24 + continent: "South America", 25 + }, 26 + { 27 + code: "bom", 28 + location: "Mumbai, India", 29 + flag: "🇮🇳", 30 + continent: "Asia", 31 + }, 32 + { 33 + code: "bos", 34 + location: "Boston, Massachusetts, USA", 35 + flag: "🇺🇸", 36 + continent: "North America", 37 + }, 38 + { 39 + code: "cdg", 40 + location: "Paris, France", 41 + flag: "🇫🇷", 42 + continent: "Europe", 43 + }, 44 + { 45 + code: "den", 46 + location: "Denver, Colorado, USA", 47 + flag: "🇺🇸", 48 + continent: "North America", 49 + }, 50 + { 51 + code: "dfw", 52 + location: "Dallas, Texas, USA", 53 + flag: "🇺🇸", 54 + continent: "North America", 55 + }, 56 + { 57 + code: "ewr", 58 + location: "Secaucus, New Jersey, USA", 59 + flag: "🇺🇸", 60 + continent: "North America", 61 + }, 62 + { 63 + code: "eze", 64 + location: "Ezeiza, Argentina", 65 + flag: "🇦🇷", 66 + continent: "South America", 67 + }, 68 + { 69 + code: "fra", 70 + location: "Frankfurt, Germany", 71 + flag: "🇩🇪", 72 + continent: "Europe", 73 + }, 74 + { 75 + code: "gdl", 76 + location: "Guadalajara, Mexico", 77 + flag: "🇲🇽", 78 + continent: "North America", 79 + }, 80 + { 81 + code: "gig", 82 + location: "Rio de Janeiro, Brazil", 83 + flag: "🇧🇷", 84 + continent: "South America", 85 + }, 86 + { 87 + code: "gru", 88 + location: "Sao Paulo, Brazil", 89 + flag: "🇧🇷", 90 + continent: "South America", 91 + }, 92 + { 93 + code: "hkg", 94 + location: "Hong Kong, Hong Kong", 95 + flag: "🇭🇰", 96 + continent: "Asia", 97 + }, 98 + { 99 + code: "iad", 100 + location: "Ashburn, Virginia, USA", 101 + flag: "🇺🇸", 102 + continent: "North America", 103 + }, 104 + { 105 + code: "jnb", 106 + location: "Johannesburg, South Africa", 107 + flag: "🇿🇦", 108 + continent: "Africa", 109 + }, 110 + { 111 + code: "lax", 112 + location: "Los Angeles, California, USA", 113 + flag: "🇺🇸", 114 + continent: "North America", 115 + }, 116 + { 117 + code: "lhr", 118 + location: "London, United Kingdom", 119 + flag: "🇬🇧", 120 + continent: "Europe", 121 + }, 122 + { 123 + code: "mad", 124 + location: "Madrid, Spain", 125 + flag: "🇪🇸", 126 + continent: "Europe", 127 + }, 128 + { 129 + code: "mia", 130 + location: "Miami, Florida, USA", 131 + flag: "🇺🇸", 132 + continent: "North America", 133 + }, 134 + { 135 + code: "nrt", 136 + location: "Tokyo, Japan", 137 + flag: "🇯🇵", 138 + continent: "Asia", 139 + }, 140 + { 141 + code: "ord", 142 + location: "Chicago, Illinois, USA", 143 + flag: "🇺🇸", 144 + continent: "North America", 145 + }, 146 + { 147 + code: "otp", 148 + location: "Bucharest, Romania", 149 + flag: "🇷🇴", 150 + continent: "Europe", 151 + }, 152 + { 153 + code: "phx", 154 + location: "Phoenix, Arizona, USA", 155 + flag: "🇺🇸", 156 + continent: "North America", 157 + }, 158 + { 159 + code: "qro", 160 + location: "Querétaro, Mexico", 161 + flag: "🇲🇽", 162 + continent: "North America", 163 + }, 164 + { 165 + code: "scl", 166 + location: "Santiago, Chile", 167 + flag: "🇨🇱", 168 + continent: "South America", 169 + }, 170 + { 171 + code: "sjc", 172 + location: "San Jose, California, USA", 173 + flag: "🇺🇸", 174 + continent: "North America", 175 + }, 176 + { 177 + code: "sea", 178 + location: "Seattle, Washington, USA", 179 + flag: "🇺🇸", 180 + continent: "North America", 181 + }, 182 + { 183 + code: "sin", 184 + location: "Singapore, Singapore", 185 + flag: "🇸🇬", 186 + continent: "Asia", 187 + }, 188 + { 189 + code: "syd", 190 + location: "Sydney, Australia", 191 + flag: "🇦🇺", 192 + continent: "Oceania", 193 + }, 194 + { 195 + code: "waw", 196 + location: "Warsaw, Poland", 197 + flag: "🇵🇱", 198 + continent: "Europe", 199 + }, 200 + { 201 + code: "yul", 202 + location: "Montreal, Canada", 203 + flag: "🇨🇦", 204 + continent: "North America", 205 + }, 206 + { 207 + code: "yyz", 208 + location: "Toronto, Canada", 209 + flag: "🇨🇦", 210 + continent: "North America", 211 + }, 212 + ] as const; 213 + 214 + export type Region = (typeof regions)[number]["code"]; 215 + 216 + export const groupedRegions = regions.reduce( 217 + (acc, region) => { 218 + const continent = region.continent; 219 + if (!acc[continent]) { 220 + acc[continent] = []; 221 + } 222 + acc[continent].push(region.code); 223 + return acc; 224 + }, 225 + {} as Record<string, Region[]>, 226 + );
+60
apps/status-page/src/data/response-logs.ts
··· 1 + export const responseLogs = [ 2 + { 3 + id: 1, 4 + url: "https://api.openstatus.dev", 5 + method: "GET", 6 + status: 200 as const, 7 + latency: 150, 8 + timing: { 9 + dns: 10, 10 + connect: 20, 11 + tls: 30, 12 + ttfb: 40, 13 + transfer: 50, 14 + }, 15 + assertions: [], 16 + region: "ams" as const, 17 + error: false, 18 + timestamp: new Date().getTime(), 19 + headers: { 20 + "Cache-Control": 21 + "private, no-cache, no-store, max-age=0, must-revalidate", 22 + "Content-Type": "text/html; charset=utf-8", 23 + Date: "Sun, 28 Jan 2024 08:50:13 GMT", 24 + Server: "Vercel", 25 + }, 26 + type: "scheduled" as const satisfies "scheduled" | "on-demand", 27 + }, 28 + { 29 + id: 2, 30 + url: "https://api.openstatus.dev", 31 + method: "GET", 32 + status: 500 as const, 33 + latency: 150, 34 + timing: { 35 + dns: 4, 36 + connect: 120, 37 + tls: 12, 38 + ttfb: 20, 39 + transfer: 40, 40 + }, 41 + assertions: [], 42 + region: "ams" as const, 43 + error: true, 44 + timestamp: new Date().getTime(), 45 + headers: { 46 + "Cache-Control": 47 + "private, no-cache, no-store, max-age=0, must-revalidate", 48 + "Content-Type": "text/html; charset=utf-8", 49 + Date: "Sun, 28 Jan 2024 08:50:13 GMT", 50 + Server: "Vercel", 51 + }, 52 + type: "scheduled" as const satisfies "scheduled" | "on-demand", 53 + // error message 54 + message: 55 + "Environment variable 'NEXT_PUBLIC_TEST_KEY' is missing. Please add and redeploy your project.", 56 + }, 57 + ]; 58 + 59 + export type ResponseLog = (typeof responseLogs)[number]; 60 + export type Timing = ResponseLog["timing"];
+16
apps/status-page/src/data/status-codes.ts
··· 1 + export const statusCodes = [ 2 + { 3 + code: 200 as const, 4 + bg: "bg-success", 5 + text: "text-success", 6 + name: "OK", 7 + }, 8 + { 9 + code: 500 as const, 10 + bg: "bg-destructive", 11 + text: "text-destructive", 12 + name: "Internal Server Error", 13 + }, 14 + ]; 15 + 16 + export type StatusCode = (typeof statusCodes)[number]["code"];
+39
apps/status-page/src/data/status-pages.client.ts
··· 1 + import { Copy, Pencil, Tag, Trash2 } from "lucide-react"; 2 + 3 + export const actions = [ 4 + { 5 + id: "edit", 6 + label: "Edit", 7 + icon: Pencil, 8 + variant: "default" as const, 9 + }, 10 + { 11 + id: "copy-id", 12 + label: "Copy ID", 13 + icon: Copy, 14 + variant: "default" as const, 15 + }, 16 + { 17 + id: "create-badge", 18 + label: "Create Badge", 19 + icon: Tag, 20 + variant: "default" as const, 21 + }, 22 + { 23 + id: "delete", 24 + label: "Delete", 25 + icon: Trash2, 26 + variant: "destructive" as const, 27 + }, 28 + ] as const; 29 + 30 + export type StatusPageAction = (typeof actions)[number]; 31 + 32 + export const getActions = ( 33 + props: Partial<Record<StatusPageAction["id"], () => Promise<void> | void>>, 34 + ): (StatusPageAction & { onClick?: () => Promise<void> | void })[] => { 35 + return actions.map((action) => ({ 36 + ...action, 37 + onClick: props[action.id as keyof typeof props], 38 + })); 39 + };
+17
apps/status-page/src/data/status-pages.ts
··· 1 + export const statusPages = [ 2 + { 3 + id: 1, 4 + name: "OpenStatus Status", 5 + description: "See our uptime history and status reports.", 6 + slug: "status", 7 + favicon: "https://openstatus.dev/favicon.ico", 8 + domain: "status.openstatus.dev", 9 + protected: true, 10 + showValues: false, 11 + // NOTE: the worst status of a report 12 + status: "degraded" as const, 13 + monitors: [], 14 + }, 15 + ]; 16 + 17 + export type StatusPage = (typeof statusPages)[number];
+29
apps/status-page/src/data/status-report-updates.client.ts
··· 1 + import { Pencil, Trash2 } from "lucide-react"; 2 + 3 + export const actions = [ 4 + { 5 + id: "edit", 6 + label: "Edit", 7 + icon: Pencil, 8 + variant: "default" as const, 9 + }, 10 + { 11 + id: "delete", 12 + label: "Delete", 13 + icon: Trash2, 14 + variant: "destructive" as const, 15 + }, 16 + ] as const; 17 + 18 + export type StatusReportUpdateAction = (typeof actions)[number]; 19 + 20 + export const getActions = ( 21 + props: Partial< 22 + Record<StatusReportUpdateAction["id"], () => Promise<void> | void> 23 + >, 24 + ): (StatusReportUpdateAction & { onClick?: () => Promise<void> | void })[] => { 25 + return actions.map((action) => ({ 26 + ...action, 27 + onClick: props[action.id as keyof typeof props], 28 + })); 29 + };
+35
apps/status-page/src/data/status-reports.client.ts
··· 1 + import { Pencil, Plus, Trash2 } from "lucide-react"; 2 + 3 + export const actions = [ 4 + { 5 + id: "edit", 6 + label: "Edit", 7 + icon: Pencil, 8 + variant: "default" as const, 9 + }, 10 + { 11 + id: "create-update", 12 + label: "Create Update", 13 + icon: Plus, 14 + variant: "default" as const, 15 + }, 16 + { 17 + id: "delete", 18 + label: "Delete", 19 + icon: Trash2, 20 + variant: "destructive" as const, 21 + }, 22 + ] as const; 23 + 24 + export type StatusReportUpdateAction = (typeof actions)[number]; 25 + 26 + export const getActions = ( 27 + props: Partial< 28 + Record<StatusReportUpdateAction["id"], () => Promise<void> | void> 29 + >, 30 + ): (StatusReportUpdateAction & { onClick?: () => Promise<void> | void })[] => { 31 + return actions.map((action) => ({ 32 + ...action, 33 + onClick: props[action.id as keyof typeof props], 34 + })); 35 + };
+84
apps/status-page/src/data/status-reports.ts
··· 1 + const today = new Date(); 2 + const lastHour = new Date(new Date().setHours(new Date().getHours() - 1)); 3 + const yesterday = new Date(new Date().setDate(new Date().getDate() - 1)); 4 + 5 + console.log({ today, lastHour, yesterday }); 6 + 7 + export const statusReports = [ 8 + { 9 + id: 1, 10 + name: "Downtime API due to hosting provider with 400 errors", 11 + startedAt: yesterday, 12 + updatedAt: today, 13 + status: "operational", 14 + updates: [ 15 + { 16 + id: 2, 17 + status: "operational" as const, 18 + message: 19 + "Everything is under control, we continue to monitor the situation.", 20 + date: today, 21 + updatedAt: today, 22 + monitors: [1], 23 + }, 24 + { 25 + id: 1, 26 + status: "investigating" as const, 27 + message: 28 + "Our hosting provider is having an increase of 400 errors. We are aware of the dependency and will be working on a solution to reduce the risk.", 29 + date: lastHour, 30 + updatedAt: lastHour, 31 + monitors: [1], 32 + }, 33 + ], 34 + affected: ["OpenStatus API"], 35 + }, 36 + { 37 + id: 3, 38 + name: "Downtime API due to hosting provider with 400 errors", 39 + startedAt: new Date("2025-08-05 12:10:00"), 40 + updatedAt: new Date("2025-08-05 12:30:00"), 41 + status: "operational", 42 + updates: [ 43 + { 44 + id: 4, 45 + status: "operational" as const, 46 + message: 47 + "Everything is under control, we continue to monitor the situation.", 48 + date: new Date("2025-08-06 03:30:00"), 49 + updatedAt: new Date("2025-08-06 03:30:00"), 50 + monitors: [1], 51 + }, 52 + { 53 + id: 3, 54 + status: "monitoring" as const, 55 + message: 56 + "We are continuing to monitor the situation to ensure that the issue is resolved.", 57 + date: new Date("2025-08-05 16:00:00"), 58 + updatedAt: new Date("2025-08-05 16:00:00"), 59 + monitors: [1], 60 + }, 61 + { 62 + id: 2, 63 + status: "identified" as const, 64 + message: 65 + "We have identified the root cause of the issue. It is due to a configuration error on our part.", 66 + date: new Date("2025-08-05 14:00:00"), 67 + updatedAt: new Date("2025-08-05 14:00:00"), 68 + monitors: [1], 69 + }, 70 + { 71 + id: 1, 72 + status: "investigating" as const, 73 + message: 74 + "Our hosting provider is having an increase of 400 errors. We are working on a solution to reduce the risk.", 75 + date: new Date("2025-08-05 12:00:00"), 76 + updatedAt: new Date("2025-08-05 12:00:00"), 77 + monitors: [1], 78 + }, 79 + ], 80 + affected: ["OpenStatus API"], 81 + }, 82 + ]; 83 + 84 + export type StatusReport = (typeof statusReports)[number];
+16
apps/status-page/src/data/subscribers.ts
··· 1 + export const subscribers = [ 2 + { 3 + id: "1", 4 + email: "max@openstatus.dev", 5 + createdAt: "2025-05-20", 6 + validatedAt: "2025-05-20", 7 + }, 8 + { 9 + id: "2", 10 + email: "thibault@openstatus.dev", 11 + createdAt: "2025-05-20", 12 + validatedAt: "2025-05-20", 13 + }, 14 + ]; 15 + 16 + export type Subscriber = (typeof subscribers)[number];
+52
apps/status-page/src/hooks/use-copy-to-clipboard.ts
··· 1 + "use client"; 2 + 3 + import { useCallback, useState } from "react"; 4 + import { toast } from "sonner"; 5 + 6 + export function useCopyToClipboard() { 7 + const [text, setText] = useState<string | null>(null); 8 + 9 + const copy = useCallback( 10 + async ( 11 + text: string, 12 + { 13 + timeout = 3000, 14 + withToast = true, 15 + successMessage = "Copied to clipboard", 16 + }: { 17 + timeout?: number; 18 + withToast?: boolean; 19 + successMessage?: string; 20 + }, 21 + ) => { 22 + if (!navigator?.clipboard) { 23 + console.warn("Clipboard not supported"); 24 + return false; 25 + } 26 + 27 + try { 28 + await navigator.clipboard.writeText(text); 29 + setText(text); 30 + 31 + if (timeout) { 32 + setTimeout(() => { 33 + setText(null); 34 + }, timeout); 35 + } 36 + 37 + if (withToast) { 38 + toast.success(successMessage); 39 + } 40 + 41 + return true; 42 + } catch (error) { 43 + console.warn("Copy failed", error); 44 + setText(null); 45 + return false; 46 + } 47 + }, 48 + [], 49 + ); 50 + 51 + return { text, copy, isCopied: text !== null }; 52 + }
+16
apps/status-page/src/hooks/use-debounce.ts
··· 1 + import * as React from "react"; 2 + 3 + // consider using https://github.com/xnimorz/use-debounce 4 + export function useDebounce<T>(value: T, delay?: number): T { 5 + const [debouncedValue, setDebouncedValue] = React.useState<T>(value); 6 + 7 + React.useEffect(() => { 8 + const timer = setTimeout(() => setDebouncedValue(value), delay ?? 500); 9 + 10 + return () => { 11 + clearTimeout(timer); 12 + }; 13 + }, [value, delay]); 14 + 15 + return debouncedValue; 16 + }
+19
apps/status-page/src/hooks/use-media-query.ts
··· 1 + import * as React from "react"; 2 + 3 + export function useMediaQuery(query: string) { 4 + const [value, setValue] = React.useState(false); 5 + 6 + React.useEffect(() => { 7 + function onChange(event: MediaQueryListEvent) { 8 + setValue(event.matches); 9 + } 10 + 11 + const result = matchMedia(query); 12 + result.addEventListener("change", onChange); 13 + setValue(result.matches); 14 + 15 + return () => result.removeEventListener("change", onChange); 16 + }, [query]); 17 + 18 + return value; 19 + }
+21
apps/status-page/src/hooks/use-mobile.ts
··· 1 + import * as React from "react"; 2 + 3 + const MOBILE_BREAKPOINT = 768; 4 + 5 + export function useIsMobile() { 6 + const [isMobile, setIsMobile] = React.useState<boolean | undefined>( 7 + undefined, 8 + ); 9 + 10 + React.useEffect(() => { 11 + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); 12 + const onChange = () => { 13 + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); 14 + }; 15 + mql.addEventListener("change", onChange); 16 + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); 17 + return () => mql.removeEventListener("change", onChange); 18 + }, []); 19 + 20 + return !!isMobile; 21 + }
+13
apps/status-page/src/instrumentation.ts
··· 1 + import * as Sentry from "@sentry/nextjs"; 2 + 3 + export async function register() { 4 + if (process.env.NEXT_RUNTIME === "nodejs") { 5 + await import("../sentry.server.config"); 6 + } 7 + 8 + if (process.env.NEXT_RUNTIME === "edge") { 9 + await import("../sentry.edge.config"); 10 + } 11 + } 12 + 13 + export const onRequestError = Sentry.captureRequestError;
+87
apps/status-page/src/lib/composition.ts
··· 1 + import * as React from "react"; 2 + 3 + /** 4 + * A utility to compose multiple event handlers into a single event handler. 5 + * Run originalEventHandler first, then ourEventHandler unless prevented. 6 + */ 7 + function composeEventHandlers<E>( 8 + originalEventHandler?: (event: E) => void, 9 + ourEventHandler?: (event: E) => void, 10 + { checkForDefaultPrevented = true } = {}, 11 + ) { 12 + return function handleEvent(event: E) { 13 + originalEventHandler?.(event); 14 + 15 + if ( 16 + checkForDefaultPrevented === false || 17 + !(event as unknown as Event).defaultPrevented 18 + ) { 19 + return ourEventHandler?.(event); 20 + } 21 + }; 22 + } 23 + 24 + /** 25 + * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx 26 + */ 27 + 28 + type PossibleRef<T> = React.Ref<T> | undefined; 29 + 30 + /** 31 + * Set a given ref to a given value. 32 + * This utility takes care of different types of refs: callback refs and RefObject(s). 33 + */ 34 + function setRef<T>(ref: PossibleRef<T>, value: T) { 35 + if (typeof ref === "function") { 36 + return ref(value); 37 + } 38 + 39 + if (ref !== null && ref !== undefined) { 40 + ref.current = value; 41 + } 42 + } 43 + 44 + /** 45 + * A utility to compose multiple refs together. 46 + * Accepts callback refs and RefObject(s). 47 + */ 48 + function composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> { 49 + return (node) => { 50 + let hasCleanup = false; 51 + const cleanups = refs.map((ref) => { 52 + const cleanup = setRef(ref, node); 53 + if (!hasCleanup && typeof cleanup === "function") { 54 + hasCleanup = true; 55 + } 56 + return cleanup; 57 + }); 58 + 59 + // React <19 will log an error to the console if a callback ref returns a 60 + // value. We don't use ref cleanups internally so this will only happen if a 61 + // user's ref callback returns a value, which we only expect if they are 62 + // using the cleanup functionality added in React 19. 63 + if (hasCleanup) { 64 + return () => { 65 + for (let i = 0; i < cleanups.length; i++) { 66 + const cleanup = cleanups[i]; 67 + if (typeof cleanup === "function") { 68 + cleanup(); 69 + } else { 70 + setRef(refs[i], null); 71 + } 72 + } 73 + }; 74 + } 75 + }; 76 + } 77 + 78 + /** 79 + * A custom hook that composes multiple refs. 80 + * Accepts callback refs and RefObject(s). 81 + */ 82 + function useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> { 83 + // eslint-disable-next-line react-hooks/exhaustive-deps 84 + return React.useCallback(composeRefs(...refs), refs); 85 + } 86 + 87 + export { composeEventHandlers, composeRefs, useComposedRefs };
+98
apps/status-page/src/lib/formatter.ts
··· 1 + import { endOfDay, isSameDay, startOfDay } from "date-fns"; 2 + 3 + export function formatMilliseconds(ms: number) { 4 + if (ms > 1000) { 5 + return `${Intl.NumberFormat("en-US", { 6 + style: "unit", 7 + unit: "second", 8 + maximumFractionDigits: 2, 9 + }).format(ms / 1000)}`; 10 + } 11 + 12 + return `${Intl.NumberFormat("en-US", { 13 + style: "unit", 14 + unit: "millisecond", 15 + }).format(ms)}`; 16 + } 17 + 18 + export function formatPercentage(value: number) { 19 + if (Number.isNaN(value)) return "100%"; 20 + return `${Intl.NumberFormat("en-US", { 21 + style: "percent", 22 + minimumFractionDigits: 2, 23 + maximumFractionDigits: 2, 24 + }).format(value)}`; 25 + } 26 + 27 + export function formatNumber( 28 + value: number, 29 + options?: Intl.NumberFormatOptions, 30 + ) { 31 + return `${Intl.NumberFormat("en-US", options).format(value)}`; 32 + } 33 + 34 + // TODO: think of supporting custom formats 35 + 36 + export function formatDate(date: Date, options?: Intl.DateTimeFormatOptions) { 37 + return date.toLocaleDateString("en-US", { 38 + year: "numeric", 39 + month: "long", 40 + day: "numeric", 41 + ...options, 42 + }); 43 + } 44 + 45 + export function formatDateTime(date: Date) { 46 + return date.toLocaleDateString("en-US", { 47 + month: "long", 48 + day: "numeric", 49 + hour: "numeric", 50 + minute: "numeric", 51 + }); 52 + } 53 + 54 + export function formatTime(date: Date) { 55 + return date.toLocaleTimeString("en-US", { 56 + hour: "numeric", 57 + minute: "numeric", 58 + }); 59 + } 60 + 61 + export function formatDateRange(from?: Date, to?: Date) { 62 + const sameDay = from && to && isSameDay(from, to); 63 + const isFromStartDay = from && startOfDay(from).getTime() === from.getTime(); 64 + const isToEndDay = to && endOfDay(to).getTime() === to.getTime(); 65 + 66 + if (sameDay) { 67 + if (from && to) { 68 + return `${formatDateTime(from)} - ${formatTime(to)}`; 69 + } 70 + } 71 + 72 + if (from && to) { 73 + if (isFromStartDay && isToEndDay) { 74 + return `${formatDate(from)} - ${formatDate(to)}`; 75 + } 76 + return `${formatDateTime(from)} - ${formatDateTime(to)}`; 77 + } 78 + 79 + if (to) { 80 + return `Until ${formatDateTime(to)}`; 81 + } 82 + 83 + if (from) { 84 + return `Since ${formatDateTime(from)}`; 85 + } 86 + 87 + return "All time"; 88 + } 89 + 90 + export function formatDateForInput(date: Date): string { 91 + const year = date.getFullYear(); 92 + const month = String(date.getMonth() + 1).padStart(2, "0"); 93 + const day = String(date.getDate()).padStart(2, "0"); 94 + const hours = String(date.getHours()).padStart(2, "0"); 95 + const minutes = String(date.getMinutes()).padStart(2, "0"); 96 + 97 + return `${year}-${month}-${day}T${hours}:${minutes}`; 98 + }
+65
apps/status-page/src/lib/trpc/client.tsx
··· 1 + "use client"; 2 + 3 + import { endingLink } from "@/lib/trpc/shared"; 4 + import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 5 + import { createTRPCClient, loggerLink } from "@trpc/client"; 6 + import { createTRPCContext } from "@trpc/tanstack-react-query"; 7 + import { useState } from "react"; 8 + 9 + import type { AppRouter } from "@openstatus/api"; 10 + 11 + export const { TRPCProvider, useTRPC, useTRPCClient } = 12 + createTRPCContext<AppRouter>(); 13 + 14 + function makeQueryClient() { 15 + return new QueryClient({ 16 + defaultOptions: { 17 + queries: { 18 + // With SSR, we usually want to set some default staleTime 19 + // above 0 to avoid refetching immediately on the client 20 + staleTime: 60 * 1000, 21 + }, 22 + }, 23 + }); 24 + } 25 + let browserQueryClient: QueryClient | undefined = undefined; 26 + function getQueryClient() { 27 + if (typeof window === "undefined") { 28 + // Server: always make a new query client 29 + return makeQueryClient(); 30 + } 31 + // Browser: make a new query client if we don't already have one 32 + // This is very important, so we don't re-make a new client if React 33 + // suspends during the initial render. This may not be needed if we 34 + // have a suspense boundary BELOW the creation of the query client 35 + if (!browserQueryClient) browserQueryClient = makeQueryClient(); 36 + return browserQueryClient; 37 + } 38 + 39 + export function TRPCReactProvider({ children }: { children: React.ReactNode }) { 40 + const queryClient = getQueryClient(); 41 + const [trpcClient] = useState(() => 42 + createTRPCClient<AppRouter>({ 43 + links: [ 44 + loggerLink({ 45 + enabled: (opts) => 46 + process.env.NODE_ENV === "development" || 47 + (opts.direction === "down" && opts.result instanceof Error), 48 + }), 49 + endingLink({ 50 + headers: { 51 + "x-trpc-source": "client", 52 + }, 53 + }), 54 + ], 55 + }), 56 + ); 57 + 58 + return ( 59 + <QueryClientProvider client={queryClient}> 60 + <TRPCProvider trpcClient={trpcClient} queryClient={queryClient}> 61 + {children} 62 + </TRPCProvider> 63 + </QueryClientProvider> 64 + ); 65 + }
+20
apps/status-page/src/lib/trpc/query-client.ts
··· 1 + import { 2 + QueryClient, 3 + defaultShouldDehydrateQuery, 4 + } from "@tanstack/react-query"; 5 + 6 + export function makeQueryClient() { 7 + return new QueryClient({ 8 + defaultOptions: { 9 + queries: { 10 + staleTime: 60 * 1000, 11 + }, 12 + dehydrate: { 13 + shouldDehydrateQuery: (query) => 14 + defaultShouldDehydrateQuery(query) || 15 + query.state.status === "pending", 16 + }, 17 + hydrate: {}, 18 + }, 19 + }); 20 + }
+88
apps/status-page/src/lib/trpc/server.tsx
··· 1 + import "server-only"; 2 + 3 + import type { AppRouter } from "@openstatus/api"; 4 + 5 + import { HydrationBoundary } from "@tanstack/react-query"; 6 + import { dehydrate } from "@tanstack/react-query"; 7 + import { createTRPCClient, loggerLink } from "@trpc/client"; 8 + import { 9 + type TRPCQueryOptions, 10 + createTRPCOptionsProxy, 11 + } from "@trpc/tanstack-react-query"; 12 + import { cookies } from "next/headers"; 13 + import { cache } from "react"; 14 + import { makeQueryClient } from "./query-client"; 15 + import { endingLink } from "./shared"; 16 + 17 + // IMPORTANT: Create a stable getter for the query client that 18 + // will return the same client during the same request. 19 + export const getQueryClient = cache(makeQueryClient); 20 + 21 + export const trpc = createTRPCOptionsProxy<AppRouter>({ 22 + queryClient: getQueryClient, 23 + client: createTRPCClient({ 24 + links: [ 25 + loggerLink({ 26 + enabled: (opts) => 27 + process.env.NODE_ENV === "development" || 28 + (opts.direction === "down" && opts.result instanceof Error), 29 + }), 30 + endingLink({ 31 + headers: { 32 + "x-trpc-source": "server", 33 + }, 34 + async fetch(url, options) { 35 + const cookieStore = await cookies(); 36 + return fetch(url, { 37 + ...options, 38 + credentials: "include", 39 + headers: { 40 + ...options?.headers, 41 + cookie: cookieStore.toString(), 42 + }, 43 + }); 44 + }, 45 + }), 46 + ], 47 + }), 48 + }); 49 + 50 + export function HydrateClient(props: { children: React.ReactNode }) { 51 + const queryClient = getQueryClient(); 52 + 53 + return ( 54 + <HydrationBoundary state={dehydrate(queryClient)}> 55 + {props.children} 56 + </HydrationBoundary> 57 + ); 58 + } 59 + 60 + // biome-ignore lint/suspicious/noExplicitAny: FIXME: remove any 61 + export function prefetch<T extends ReturnType<TRPCQueryOptions<any>>>( 62 + queryOptions: T, 63 + ) { 64 + const queryClient = getQueryClient(); 65 + 66 + if (queryOptions.queryKey[1]?.type === "infinite") { 67 + // biome-ignore lint/suspicious/noExplicitAny: FIXME: remove any 68 + void queryClient.prefetchInfiniteQuery(queryOptions as any); 69 + } else { 70 + void queryClient.prefetchQuery(queryOptions); 71 + } 72 + } 73 + 74 + // biome-ignore lint/suspicious/noExplicitAny: FIXME: remove any 75 + export function batchPrefetch<T extends ReturnType<TRPCQueryOptions<any>>>( 76 + queryOptionsArray: T[], 77 + ) { 78 + const queryClient = getQueryClient(); 79 + 80 + for (const queryOptions of queryOptionsArray) { 81 + if (queryOptions.queryKey[1]?.type === "infinite") { 82 + // biome-ignore lint/suspicious/noExplicitAny: FIXME: remove any 83 + void queryClient.prefetchInfiniteQuery(queryOptions as any); 84 + } else { 85 + void queryClient.prefetchQuery(queryOptions); 86 + } 87 + } 88 + }
+48
apps/status-page/src/lib/trpc/shared.ts
··· 1 + import type { HTTPBatchLinkOptions, HTTPHeaders, TRPCLink } from "@trpc/client"; 2 + import { httpBatchLink } from "@trpc/client"; 3 + 4 + import type { AppRouter } from "@openstatus/api"; 5 + import superjson from "superjson"; 6 + 7 + const getBaseUrl = () => { 8 + if (typeof window !== "undefined") return ""; 9 + const vc = process.env.VERCEL_URL; 10 + // if (vc) return `https://${vc}`; 11 + if (vc) return "https://app.openstatus.dev"; 12 + return "http://localhost:3000"; 13 + }; 14 + 15 + const lambdas = ["stripeRouter", "emailRouter"]; 16 + 17 + export const endingLink = (opts?: { 18 + fetch?: typeof fetch; 19 + headers?: HTTPHeaders | (() => HTTPHeaders | Promise<HTTPHeaders>); 20 + }) => 21 + ((runtime) => { 22 + const sharedOpts = { 23 + headers: opts?.headers, 24 + fetch: opts?.fetch, 25 + transformer: superjson, 26 + // biome-ignore lint/suspicious/noExplicitAny: FIXME: remove any 27 + } satisfies Partial<HTTPBatchLinkOptions<any>>; 28 + 29 + const edgeLink = httpBatchLink({ 30 + ...sharedOpts, 31 + url: `${getBaseUrl()}/api/trpc/edge`, 32 + })(runtime); 33 + const lambdaLink = httpBatchLink({ 34 + ...sharedOpts, 35 + url: `${getBaseUrl()}/api/trpc/lambda`, 36 + })(runtime); 37 + 38 + return (ctx) => { 39 + const path = ctx.op.path.split(".") as [string, ...string[]]; 40 + const endpoint = lambdas.includes(path[0]) ? "lambda" : "edge"; 41 + 42 + const newCtx = { 43 + ...ctx, 44 + op: { ...ctx.op, path: path.join(".") }, 45 + }; 46 + return endpoint === "edge" ? edgeLink(newCtx) : lambdaLink(newCtx); 47 + }; 48 + }) satisfies TRPCLink<AppRouter>;
+6
apps/status-page/src/lib/utils.ts
··· 1 + import { type ClassValue, clsx } from "clsx"; 2 + import { twMerge } from "tailwind-merge"; 3 + 4 + export function cn(...inputs: ClassValue[]) { 5 + return twMerge(clsx(inputs)); 6 + }
+28
apps/status-page/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ES2017", 4 + "lib": ["dom", "dom.iterable", "esnext"], 5 + "allowJs": true, 6 + "skipLibCheck": true, 7 + "strict": true, 8 + "strictNullChecks": true, 9 + "noEmit": true, 10 + "esModuleInterop": true, 11 + "module": "esnext", 12 + "moduleResolution": "bundler", 13 + "resolveJsonModule": true, 14 + "isolatedModules": true, 15 + "jsx": "preserve", 16 + "incremental": true, 17 + "plugins": [ 18 + { 19 + "name": "next" 20 + } 21 + ], 22 + "paths": { 23 + "@/*": ["./src/*"] 24 + } 25 + }, 26 + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 + "exclude": ["node_modules"] 28 + }
+1
package.json
··· 6 6 "env": "bun env.ts", 7 7 "lint": "biome lint .", 8 8 "format": "pnpm biome format . --write && pnpm biome check . --write ", 9 + "format:fix": "pnpm biome check --fix --unsafe .", 9 10 "lint:fix": "pnpm biome lint --write --unsafe .", 10 11 "lint:turbo": "turbo run lint", 11 12 "dev:web": "turbo run dev --filter='./apps/web' --filter='./packages/db'",
+304 -188
pnpm-lock.yaml
··· 64 64 version: 0.15.9(bufferutil@4.0.8)(utf-8-validate@6.0.5) 65 65 '@openpanel/nextjs': 66 66 specifier: 1.0.8 67 - version: 1.0.8(next@15.4.7(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 67 + version: 1.0.8(next@15.4.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 68 68 '@openstatus/analytics': 69 69 specifier: workspace:* 70 70 version: link:../../packages/analytics ··· 181 181 version: 1.2.7(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 182 182 '@sentry/nextjs': 183 183 specifier: 8.46.0 184 - version: 8.46.0(@opentelemetry/core@1.30.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.4.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1)(webpack@5.97.1) 184 + version: 8.46.0(@opentelemetry/core@1.30.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.4.7(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1)(webpack@5.97.1) 185 185 '@stripe/stripe-js': 186 186 specifier: 2.1.6 187 187 version: 2.1.6 ··· 196 196 version: 11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2) 197 197 '@trpc/next': 198 198 specifier: 11.4.4 199 - version: 11.4.4(@tanstack/react-query@5.81.5(react@19.1.1))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2))(@trpc/react-query@11.4.4(@tanstack/react-query@5.81.5(react@19.1.1))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.4.4(typescript@5.7.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.2))(@trpc/server@11.4.4(typescript@5.7.2))(next@15.4.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.2) 199 + version: 11.4.4(@tanstack/react-query@5.81.5(react@19.1.1))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2))(@trpc/react-query@11.4.4(@tanstack/react-query@5.81.5(react@19.1.1))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.4.4(typescript@5.7.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.2))(@trpc/server@11.4.4(typescript@5.7.2))(next@15.4.7(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.2) 200 200 '@trpc/react-query': 201 201 specifier: 11.4.4 202 202 version: 11.4.4(@tanstack/react-query@5.81.5(react@19.1.1))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.4.4(typescript@5.7.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.2) ··· 484 484 specifier: 16.3.1 485 485 version: 16.3.1 486 486 487 + apps/status-page: 488 + dependencies: 489 + '@date-fns/tz': 490 + specifier: 1.2.0 491 + version: 1.2.0 492 + '@date-fns/utc': 493 + specifier: 2.1.0 494 + version: 2.1.0 495 + '@dnd-kit/core': 496 + specifier: 6.3.1 497 + version: 6.3.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 498 + '@dnd-kit/modifiers': 499 + specifier: 9.0.0 500 + version: 9.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) 501 + '@dnd-kit/sortable': 502 + specifier: 10.0.0 503 + version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) 504 + '@dnd-kit/utilities': 505 + specifier: 3.2.2 506 + version: 3.2.2(react@19.1.1) 507 + '@hookform/devtools': 508 + specifier: 4.4.0 509 + version: 4.4.0(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 510 + '@hookform/resolvers': 511 + specifier: 3.9.1 512 + version: 3.9.1(react-hook-form@7.54.1(react@19.1.1)) 513 + '@libsql/client': 514 + specifier: 0.15.9 515 + version: 0.15.9(bufferutil@4.0.8)(utf-8-validate@6.0.5) 516 + '@openpanel/nextjs': 517 + specifier: 1.0.8 518 + version: 1.0.8(next@15.4.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 519 + '@openstatus/analytics': 520 + specifier: workspace:* 521 + version: link:../../packages/analytics 522 + '@openstatus/api': 523 + specifier: workspace:* 524 + version: link:../../packages/api 525 + '@openstatus/assertions': 526 + specifier: workspace:* 527 + version: link:../../packages/assertions 528 + '@openstatus/db': 529 + specifier: workspace:* 530 + version: link:../../packages/db 531 + '@openstatus/emails': 532 + specifier: workspace:* 533 + version: link:../../packages/emails 534 + '@openstatus/error': 535 + specifier: workspace:* 536 + version: link:../../packages/error 537 + '@openstatus/header-analysis': 538 + specifier: workspace:* 539 + version: link:../../packages/header-analysis 540 + '@openstatus/notification-discord': 541 + specifier: workspace:* 542 + version: link:../../packages/notifications/discord 543 + '@openstatus/notification-emails': 544 + specifier: workspace:* 545 + version: link:../../packages/notifications/email 546 + '@openstatus/notification-ntfy': 547 + specifier: workspace:* 548 + version: link:../../packages/notifications/ntfy 549 + '@openstatus/notification-opsgenie': 550 + specifier: workspace:* 551 + version: link:../../packages/notifications/opsgenie 552 + '@openstatus/notification-pagerduty': 553 + specifier: workspace:* 554 + version: link:../../packages/notifications/pagerduty 555 + '@openstatus/notification-slack': 556 + specifier: workspace:* 557 + version: link:../../packages/notifications/slack 558 + '@openstatus/notification-webhook': 559 + specifier: workspace:* 560 + version: link:../../packages/notifications/webhook 561 + '@openstatus/react': 562 + specifier: workspace:* 563 + version: link:../../packages/react 564 + '@openstatus/tinybird': 565 + specifier: workspace:* 566 + version: link:../../packages/tinybird 567 + '@openstatus/tracker': 568 + specifier: workspace:* 569 + version: link:../../packages/tracker 570 + '@openstatus/upstash': 571 + specifier: workspace:* 572 + version: link:../../packages/upstash 573 + '@openstatus/utils': 574 + specifier: workspace:* 575 + version: link:../../packages/utils 576 + '@radix-ui/react-alert-dialog': 577 + specifier: 1.1.14 578 + version: 1.1.14(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 579 + '@radix-ui/react-avatar': 580 + specifier: 1.1.10 581 + version: 1.1.10(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 582 + '@radix-ui/react-checkbox': 583 + specifier: 1.3.2 584 + version: 1.3.2(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 585 + '@radix-ui/react-collapsible': 586 + specifier: 1.1.11 587 + version: 1.1.11(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 588 + '@radix-ui/react-dialog': 589 + specifier: 1.1.14 590 + version: 1.1.14(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 591 + '@radix-ui/react-dropdown-menu': 592 + specifier: 2.1.15 593 + version: 2.1.15(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 594 + '@radix-ui/react-hover-card': 595 + specifier: 1.1.14 596 + version: 1.1.14(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 597 + '@radix-ui/react-label': 598 + specifier: 2.1.7 599 + version: 2.1.7(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 600 + '@radix-ui/react-popover': 601 + specifier: 1.1.14 602 + version: 1.1.14(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 603 + '@radix-ui/react-portal': 604 + specifier: 1.1.9 605 + version: 1.1.9(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 606 + '@radix-ui/react-progress': 607 + specifier: 1.1.7 608 + version: 1.1.7(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 609 + '@radix-ui/react-radio-group': 610 + specifier: 1.3.7 611 + version: 1.3.7(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 612 + '@radix-ui/react-select': 613 + specifier: 2.2.5 614 + version: 2.2.5(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 615 + '@radix-ui/react-separator': 616 + specifier: 1.1.7 617 + version: 1.1.7(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 618 + '@radix-ui/react-slider': 619 + specifier: 1.3.5 620 + version: 1.3.5(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 621 + '@radix-ui/react-slot': 622 + specifier: 1.2.3 623 + version: 1.2.3(@types/react@19.1.10)(react@19.1.1) 624 + '@radix-ui/react-switch': 625 + specifier: 1.2.5 626 + version: 1.2.5(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 627 + '@radix-ui/react-tabs': 628 + specifier: 1.1.12 629 + version: 1.1.12(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 630 + '@radix-ui/react-tooltip': 631 + specifier: 1.2.7 632 + version: 1.2.7(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 633 + '@sentry/nextjs': 634 + specifier: 8.46.0 635 + version: 8.46.0(@opentelemetry/core@1.30.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.4.7(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1)(webpack@5.97.1) 636 + '@tanstack/react-query': 637 + specifier: 5.81.5 638 + version: 5.81.5(react@19.1.1) 639 + '@tanstack/react-table': 640 + specifier: 8.21.3 641 + version: 8.21.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 642 + '@trpc/client': 643 + specifier: 11.4.4 644 + version: 11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2) 645 + '@trpc/next': 646 + specifier: 11.4.4 647 + version: 11.4.4(@tanstack/react-query@5.81.5(react@19.1.1))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2))(@trpc/react-query@11.4.4(@tanstack/react-query@5.81.5(react@19.1.1))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.4.4(typescript@5.7.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.2))(@trpc/server@11.4.4(typescript@5.7.2))(next@15.4.7(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.2) 648 + '@trpc/react-query': 649 + specifier: 11.4.4 650 + version: 11.4.4(@tanstack/react-query@5.81.5(react@19.1.1))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.4.4(typescript@5.7.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.2) 651 + '@trpc/server': 652 + specifier: 11.4.4 653 + version: 11.4.4(typescript@5.7.2) 654 + '@trpc/tanstack-react-query': 655 + specifier: 11.4.4 656 + version: 11.4.4(@tanstack/react-query@5.81.5(react@19.1.1))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.4.4(typescript@5.7.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.2) 657 + class-variance-authority: 658 + specifier: 0.7.1 659 + version: 0.7.1 660 + clsx: 661 + specifier: 2.1.1 662 + version: 2.1.1 663 + cmdk: 664 + specifier: 1.1.1 665 + version: 1.1.1(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 666 + date-fns: 667 + specifier: 4.1.0 668 + version: 4.1.0 669 + lucide-react: 670 + specifier: 0.525.0 671 + version: 0.525.0(react@19.1.1) 672 + next: 673 + specifier: 15.4.7 674 + version: 15.4.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 675 + next-themes: 676 + specifier: 0.4.6 677 + version: 0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 678 + nuqs: 679 + specifier: 2.4.3 680 + version: 2.4.3(next@15.4.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) 681 + react: 682 + specifier: 19.1.1 683 + version: 19.1.1 684 + react-day-picker: 685 + specifier: 8.10.1 686 + version: 8.10.1(date-fns@4.1.0)(react@19.1.1) 687 + react-dom: 688 + specifier: 19.1.1 689 + version: 19.1.1(react@19.1.1) 690 + react-hook-form: 691 + specifier: 7.54.1 692 + version: 7.54.1(react@19.1.1) 693 + recharts: 694 + specifier: 2.15.0 695 + version: 2.15.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 696 + rehype-react: 697 + specifier: 8.0.0 698 + version: 8.0.0 699 + remark-gfm: 700 + specifier: 4.0.1 701 + version: 4.0.1 702 + remark-parse: 703 + specifier: 11.0.0 704 + version: 11.0.0 705 + remark-rehype: 706 + specifier: 11.1.2 707 + version: 11.1.2 708 + sonner: 709 + specifier: 2.0.5 710 + version: 2.0.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 711 + superjson: 712 + specifier: 2.2.2 713 + version: 2.2.2 714 + tailwind-merge: 715 + specifier: 3.3.1 716 + version: 3.3.1 717 + unified: 718 + specifier: 11.0.5 719 + version: 11.0.5 720 + zod: 721 + specifier: 3.24.1 722 + version: 3.24.1 723 + devDependencies: 724 + '@tailwindcss/postcss': 725 + specifier: 4.1.11 726 + version: 4.1.11 727 + '@types/node': 728 + specifier: 24.0.8 729 + version: 24.0.8 730 + '@types/react': 731 + specifier: 19.1.10 732 + version: 19.1.10 733 + '@types/react-dom': 734 + specifier: 19.1.7 735 + version: 19.1.7(@types/react@19.1.10) 736 + shadcn: 737 + specifier: 2.7.0 738 + version: 2.7.0(@types/node@24.0.8)(typescript@5.7.2) 739 + tailwindcss: 740 + specifier: 4.1.11 741 + version: 4.1.11 742 + tw-animate-css: 743 + specifier: 1.3.4 744 + version: 1.3.4 745 + typescript: 746 + specifier: 5.7.2 747 + version: 5.7.2 748 + 487 749 apps/web: 488 750 dependencies: 489 751 '@auth/core': ··· 503 765 version: 0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.5) 504 766 '@openpanel/nextjs': 505 767 specifier: ^1.0.8 506 - version: 1.0.8(next@15.4.7(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 768 + version: 1.0.8(next@15.4.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 507 769 '@openstatus/analytics': 508 770 specifier: workspace:* 509 771 version: link:../../packages/analytics ··· 1886 2148 resolution: {integrity: sha512-VXAq/Jz8KIrU84+HqsOJhIKZqG0PNTdi6n6PFQ4xJf44ZQHD/5C7ouH4qCFX5XgZXcgbRIcMVVYGC6Jye0dRng==} 1887 2149 engines: {node: '>=14.0.0'} 1888 2150 1889 - '@babel/code-frame@7.26.2': 1890 - resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} 1891 - engines: {node: '>=6.9.0'} 1892 - 1893 2151 '@babel/code-frame@7.27.1': 1894 2152 resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} 1895 2153 engines: {node: '>=6.9.0'} ··· 1902 2160 resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} 1903 2161 engines: {node: '>=6.9.0'} 1904 2162 1905 - '@babel/generator@7.26.2': 1906 - resolution: {integrity: sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==} 1907 - engines: {node: '>=6.9.0'} 1908 - 1909 2163 '@babel/generator@7.27.5': 1910 2164 resolution: {integrity: sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==} 1911 2165 engines: {node: '>=6.9.0'} ··· 1989 2243 engines: {node: '>=6.0.0'} 1990 2244 hasBin: true 1991 2245 1992 - '@babel/parser@7.27.5': 1993 - resolution: {integrity: sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==} 1994 - engines: {node: '>=6.0.0'} 1995 - hasBin: true 1996 - 1997 2246 '@babel/parser@7.27.7': 1998 2247 resolution: {integrity: sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==} 1999 2248 engines: {node: '>=6.0.0'} ··· 2031 2280 resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} 2032 2281 engines: {node: '>=6.9.0'} 2033 2282 2034 - '@babel/template@7.25.9': 2035 - resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} 2036 - engines: {node: '>=6.9.0'} 2037 - 2038 2283 '@babel/template@7.27.2': 2039 2284 resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} 2040 2285 engines: {node: '>=6.9.0'} 2041 2286 2042 - '@babel/traverse@7.25.9': 2043 - resolution: {integrity: sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==} 2044 - engines: {node: '>=6.9.0'} 2045 - 2046 - '@babel/traverse@7.27.4': 2047 - resolution: {integrity: sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==} 2048 - engines: {node: '>=6.9.0'} 2049 - 2050 2287 '@babel/traverse@7.27.7': 2051 2288 resolution: {integrity: sha512-X6ZlfR/O/s5EQ/SnUSLzr+6kGnkg8HXGMzpgsMsrJVcfDtH1vIp6ctCN4eZ1LS5c0+te5Cb6Y514fASjMRJ1nw==} 2052 2289 engines: {node: '>=6.9.0'} 2053 2290 2054 2291 '@babel/types@7.26.3': 2055 2292 resolution: {integrity: sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==} 2056 - engines: {node: '>=6.9.0'} 2057 - 2058 - '@babel/types@7.27.6': 2059 - resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==} 2060 2293 engines: {node: '>=6.9.0'} 2061 2294 2062 2295 '@babel/types@7.27.7': ··· 3451 3684 3452 3685 '@jridgewell/gen-mapping@0.3.5': 3453 3686 resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} 3454 - engines: {node: '>=6.0.0'} 3455 - 3456 - '@jridgewell/gen-mapping@0.3.8': 3457 - resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} 3458 3687 engines: {node: '>=6.0.0'} 3459 3688 3460 3689 '@jridgewell/resolve-uri@3.1.1': ··· 6988 7217 6989 7218 brotli@1.3.3: 6990 7219 resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} 6991 - 6992 - browserslist@4.24.2: 6993 - resolution: {integrity: sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==} 6994 - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} 6995 - hasBin: true 6996 7220 6997 7221 browserslist@4.25.1: 6998 7222 resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==} ··· 7062 7286 resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} 7063 7287 engines: {node: '>=16'} 7064 7288 7065 - caniuse-lite@1.0.30001721: 7066 - resolution: {integrity: sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==} 7067 - 7068 7289 caniuse-lite@1.0.30001726: 7069 7290 resolution: {integrity: sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==} 7070 7291 ··· 7082 7303 chalk@4.1.2: 7083 7304 resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 7084 7305 engines: {node: '>=10'} 7085 - 7086 - chalk@5.3.0: 7087 - resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} 7088 - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} 7089 7306 7090 7307 chalk@5.4.1: 7091 7308 resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} ··· 7756 7973 7757 7974 electron-to-chromium@1.5.178: 7758 7975 resolution: {integrity: sha512-wObbz/ar3Bc6e4X5vf0iO8xTN8YAjN/tgiAOJLr7yjYFtP9wAjq8Mb5h0yn6kResir+VYx2DXBj9NNobs0ETSA==} 7759 - 7760 - electron-to-chromium@1.5.64: 7761 - resolution: {integrity: sha512-IXEuxU+5ClW2IGEYFC2T7szbyVgehupCWQe5GNh+H065CD6U6IFN0s4KeAMFGNmQolRU4IV7zGBWSYMmZ8uuqQ==} 7762 7976 7763 7977 emmet@2.4.11: 7764 7978 resolution: {integrity: sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==} ··· 9399 9613 micromark@4.0.2: 9400 9614 resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} 9401 9615 9402 - micromatch@4.0.5: 9403 - resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} 9404 - engines: {node: '>=8.6'} 9405 - 9406 9616 micromatch@4.0.8: 9407 9617 resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} 9408 9618 engines: {node: '>=8.6'} ··· 9701 9911 resolution: {integrity: sha512-Cov028YhBZ5aB7MdMWJEmwyBig43aGL5WT4vdoB28Oitau1zZAcHUn8Sgfk9HM33TqhtLJ9PlM/O0Mv+QpV/4Q==} 9702 9912 engines: {node: '>=8.9.4'} 9703 9913 9704 - node-releases@2.0.18: 9705 - resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} 9706 - 9707 9914 node-releases@2.0.19: 9708 9915 resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} 9709 9916 ··· 11587 11794 uploadthing: 11588 11795 optional: true 11589 11796 11590 - update-browserslist-db@1.1.1: 11591 - resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} 11592 - hasBin: true 11593 - peerDependencies: 11594 - browserslist: '>= 4.21.0' 11595 - 11596 11797 update-browserslist-db@1.1.3: 11597 11798 resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} 11598 11799 hasBin: true ··· 12909 13110 '@smithy/types': 2.12.0 12910 13111 tslib: 2.6.2 12911 13112 12912 - '@babel/code-frame@7.26.2': 12913 - dependencies: 12914 - '@babel/helper-validator-identifier': 7.25.9 12915 - js-tokens: 4.0.0 12916 - picocolors: 1.1.1 12917 - 12918 13113 '@babel/code-frame@7.27.1': 12919 13114 dependencies: 12920 13115 '@babel/helper-validator-identifier': 7.27.1 ··· 12926 13121 '@babel/core@7.26.0': 12927 13122 dependencies: 12928 13123 '@ampproject/remapping': 2.3.0 12929 - '@babel/code-frame': 7.26.2 12930 - '@babel/generator': 7.26.2 13124 + '@babel/code-frame': 7.27.1 13125 + '@babel/generator': 7.27.5 12931 13126 '@babel/helper-compilation-targets': 7.25.9 12932 13127 '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) 12933 13128 '@babel/helpers': 7.26.0 12934 - '@babel/parser': 7.26.3 12935 - '@babel/template': 7.25.9 12936 - '@babel/traverse': 7.25.9 12937 - '@babel/types': 7.26.3 13129 + '@babel/parser': 7.27.7 13130 + '@babel/template': 7.27.2 13131 + '@babel/traverse': 7.27.7 13132 + '@babel/types': 7.27.7 12938 13133 convert-source-map: 2.0.0 12939 13134 debug: 4.4.1 12940 13135 gensync: 1.0.0-beta.2 ··· 12943 13138 transitivePeerDependencies: 12944 13139 - supports-color 12945 13140 12946 - '@babel/generator@7.26.2': 12947 - dependencies: 12948 - '@babel/parser': 7.26.3 12949 - '@babel/types': 7.26.3 12950 - '@jridgewell/gen-mapping': 0.3.8 12951 - '@jridgewell/trace-mapping': 0.3.25 12952 - jsesc: 3.0.2 12953 - 12954 13141 '@babel/generator@7.27.5': 12955 13142 dependencies: 12956 - '@babel/parser': 7.27.5 12957 - '@babel/types': 7.27.6 13143 + '@babel/parser': 7.27.7 13144 + '@babel/types': 7.27.7 12958 13145 '@jridgewell/gen-mapping': 0.3.11 12959 13146 '@jridgewell/trace-mapping': 0.3.28 12960 13147 jsesc: 3.0.2 12961 13148 12962 13149 '@babel/helper-annotate-as-pure@7.27.3': 12963 13150 dependencies: 12964 - '@babel/types': 7.27.6 13151 + '@babel/types': 7.27.7 12965 13152 12966 13153 '@babel/helper-compilation-targets@7.25.9': 12967 13154 dependencies: 12968 13155 '@babel/compat-data': 7.26.2 12969 13156 '@babel/helper-validator-option': 7.25.9 12970 - browserslist: 4.24.2 13157 + browserslist: 4.25.1 12971 13158 lru-cache: 5.1.1 12972 13159 semver: 6.3.1 12973 13160 ··· 12979 13166 '@babel/helper-optimise-call-expression': 7.27.1 12980 13167 '@babel/helper-replace-supers': 7.27.1(@babel/core@7.26.0) 12981 13168 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 12982 - '@babel/traverse': 7.27.4 13169 + '@babel/traverse': 7.27.7 12983 13170 semver: 6.3.1 12984 13171 transitivePeerDependencies: 12985 13172 - supports-color 12986 13173 12987 13174 '@babel/helper-member-expression-to-functions@7.27.1': 12988 13175 dependencies: 12989 - '@babel/traverse': 7.27.4 12990 - '@babel/types': 7.27.6 13176 + '@babel/traverse': 7.27.7 13177 + '@babel/types': 7.27.7 12991 13178 transitivePeerDependencies: 12992 13179 - supports-color 12993 13180 12994 13181 '@babel/helper-module-imports@7.25.9': 12995 13182 dependencies: 12996 - '@babel/traverse': 7.25.9 12997 - '@babel/types': 7.26.3 13183 + '@babel/traverse': 7.27.7 13184 + '@babel/types': 7.27.7 12998 13185 transitivePeerDependencies: 12999 13186 - supports-color 13000 13187 ··· 13002 13189 dependencies: 13003 13190 '@babel/core': 7.26.0 13004 13191 '@babel/helper-module-imports': 7.25.9 13005 - '@babel/helper-validator-identifier': 7.25.9 13006 - '@babel/traverse': 7.25.9 13192 + '@babel/helper-validator-identifier': 7.27.1 13193 + '@babel/traverse': 7.27.7 13007 13194 transitivePeerDependencies: 13008 13195 - supports-color 13009 13196 13010 13197 '@babel/helper-optimise-call-expression@7.27.1': 13011 13198 dependencies: 13012 - '@babel/types': 7.27.6 13199 + '@babel/types': 7.27.7 13013 13200 13014 13201 '@babel/helper-plugin-utils@7.26.5': {} 13015 13202 ··· 13020 13207 '@babel/core': 7.26.0 13021 13208 '@babel/helper-member-expression-to-functions': 7.27.1 13022 13209 '@babel/helper-optimise-call-expression': 7.27.1 13023 - '@babel/traverse': 7.27.4 13210 + '@babel/traverse': 7.27.7 13024 13211 transitivePeerDependencies: 13025 13212 - supports-color 13026 13213 13027 13214 '@babel/helper-skip-transparent-expression-wrappers@7.27.1': 13028 13215 dependencies: 13029 - '@babel/traverse': 7.27.4 13030 - '@babel/types': 7.27.6 13216 + '@babel/traverse': 7.27.7 13217 + '@babel/types': 7.27.7 13031 13218 transitivePeerDependencies: 13032 13219 - supports-color 13033 13220 ··· 13043 13230 13044 13231 '@babel/helpers@7.26.0': 13045 13232 dependencies: 13046 - '@babel/template': 7.25.9 13047 - '@babel/types': 7.26.3 13233 + '@babel/template': 7.27.2 13234 + '@babel/types': 7.27.7 13048 13235 13049 13236 '@babel/parser@7.26.3': 13050 13237 dependencies: 13051 13238 '@babel/types': 7.26.3 13052 - 13053 - '@babel/parser@7.27.5': 13054 - dependencies: 13055 - '@babel/types': 7.27.6 13056 13239 13057 13240 '@babel/parser@7.27.7': 13058 13241 dependencies: ··· 13093 13276 dependencies: 13094 13277 regenerator-runtime: 0.14.1 13095 13278 13096 - '@babel/template@7.25.9': 13097 - dependencies: 13098 - '@babel/code-frame': 7.26.2 13099 - '@babel/parser': 7.26.3 13100 - '@babel/types': 7.26.3 13101 - 13102 13279 '@babel/template@7.27.2': 13103 13280 dependencies: 13104 13281 '@babel/code-frame': 7.27.1 13105 - '@babel/parser': 7.27.5 13106 - '@babel/types': 7.27.6 13107 - 13108 - '@babel/traverse@7.25.9': 13109 - dependencies: 13110 - '@babel/code-frame': 7.26.2 13111 - '@babel/generator': 7.26.2 13112 - '@babel/parser': 7.26.3 13113 - '@babel/template': 7.25.9 13114 - '@babel/types': 7.26.3 13115 - debug: 4.4.1 13116 - globals: 11.12.0 13117 - transitivePeerDependencies: 13118 - - supports-color 13119 - 13120 - '@babel/traverse@7.27.4': 13121 - dependencies: 13122 - '@babel/code-frame': 7.27.1 13123 - '@babel/generator': 7.27.5 13124 - '@babel/parser': 7.27.5 13125 - '@babel/template': 7.27.2 13126 - '@babel/types': 7.27.6 13127 - debug: 4.4.1 13128 - globals: 11.12.0 13129 - transitivePeerDependencies: 13130 - - supports-color 13282 + '@babel/parser': 7.27.7 13283 + '@babel/types': 7.27.7 13131 13284 13132 13285 '@babel/traverse@7.27.7': 13133 13286 dependencies: ··· 13146 13299 '@babel/helper-string-parser': 7.25.9 13147 13300 '@babel/helper-validator-identifier': 7.25.9 13148 13301 13149 - '@babel/types@7.27.6': 13150 - dependencies: 13151 - '@babel/helper-string-parser': 7.27.1 13152 - '@babel/helper-validator-identifier': 7.27.1 13153 - 13154 13302 '@babel/types@7.27.7': 13155 13303 dependencies: 13156 13304 '@babel/helper-string-parser': 7.27.1 ··· 14212 14360 '@jridgewell/sourcemap-codec': 1.5.3 14213 14361 '@jridgewell/trace-mapping': 0.3.25 14214 14362 14215 - '@jridgewell/gen-mapping@0.3.8': 14216 - dependencies: 14217 - '@jridgewell/set-array': 1.2.1 14218 - '@jridgewell/sourcemap-codec': 1.5.3 14219 - '@jridgewell/trace-mapping': 0.3.25 14220 - 14221 14363 '@jridgewell/resolve-uri@3.1.1': {} 14222 14364 14223 14365 '@jridgewell/resolve-uri@3.1.2': {} ··· 14521 14663 '@openpanel/web': 1.0.1 14522 14664 astro: 5.12.2(@types/node@24.0.8)(encoding@0.1.13)(jiti@2.4.2)(lightningcss@1.30.1)(rollup@4.45.1)(terser@5.43.1)(typescript@5.7.2)(yaml@2.6.1) 14523 14665 14524 - '@openpanel/nextjs@1.0.8(next@15.4.7(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': 14666 + '@openpanel/nextjs@1.0.8(next@15.4.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': 14525 14667 dependencies: 14526 14668 '@openpanel/web': 1.0.1 14527 14669 next: 15.4.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) ··· 16541 16683 '@sentry/types': 8.9.2 16542 16684 '@sentry/utils': 8.9.2 16543 16685 16544 - '@sentry/nextjs@8.46.0(@opentelemetry/core@1.30.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.4.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1)(webpack@5.97.1)': 16686 + '@sentry/nextjs@8.46.0(@opentelemetry/core@1.30.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.4.7(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1)(webpack@5.97.1(esbuild@0.21.5))': 16545 16687 dependencies: 16546 16688 '@opentelemetry/api': 1.9.0 16547 16689 '@opentelemetry/semantic-conventions': 1.28.0 ··· 16552 16694 '@sentry/opentelemetry': 8.46.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.30.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.28.0) 16553 16695 '@sentry/react': 8.46.0(react@19.1.1) 16554 16696 '@sentry/vercel-edge': 8.46.0 16555 - '@sentry/webpack-plugin': 2.22.7(encoding@0.1.13)(webpack@5.97.1) 16697 + '@sentry/webpack-plugin': 2.22.7(encoding@0.1.13)(webpack@5.97.1(esbuild@0.21.5)) 16556 16698 chalk: 3.0.0 16557 16699 next: 15.4.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 16558 16700 resolve: 1.22.8 ··· 16567 16709 - supports-color 16568 16710 - webpack 16569 16711 16570 - '@sentry/nextjs@8.46.0(@opentelemetry/core@1.30.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.4.7(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1)(webpack@5.97.1(esbuild@0.21.5))': 16712 + '@sentry/nextjs@8.46.0(@opentelemetry/core@1.30.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.4.7(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1)(webpack@5.97.1)': 16571 16713 dependencies: 16572 16714 '@opentelemetry/api': 1.9.0 16573 16715 '@opentelemetry/semantic-conventions': 1.28.0 ··· 16578 16720 '@sentry/opentelemetry': 8.46.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.30.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.28.0) 16579 16721 '@sentry/react': 8.46.0(react@19.1.1) 16580 16722 '@sentry/vercel-edge': 8.46.0 16581 - '@sentry/webpack-plugin': 2.22.7(encoding@0.1.13)(webpack@5.97.1(esbuild@0.21.5)) 16723 + '@sentry/webpack-plugin': 2.22.7(encoding@0.1.13)(webpack@5.97.1) 16582 16724 chalk: 3.0.0 16583 16725 next: 15.4.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 16584 16726 resolve: 1.22.8 ··· 17290 17432 '@tanstack/react-query': 5.80.7(react@19.1.1) 17291 17433 '@trpc/react-query': 11.4.4(@tanstack/react-query@5.80.7(react@19.1.1))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.4.4(typescript@5.7.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.2) 17292 17434 17293 - '@trpc/next@11.4.4(@tanstack/react-query@5.81.5(react@19.1.1))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2))(@trpc/react-query@11.4.4(@tanstack/react-query@5.81.5(react@19.1.1))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.4.4(typescript@5.7.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.2))(@trpc/server@11.4.4(typescript@5.7.2))(next@15.4.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.2)': 17435 + '@trpc/next@11.4.4(@tanstack/react-query@5.81.5(react@19.1.1))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2))(@trpc/react-query@11.4.4(@tanstack/react-query@5.81.5(react@19.1.1))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.4.4(typescript@5.7.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.2))(@trpc/server@11.4.4(typescript@5.7.2))(next@15.4.7(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.2)': 17294 17436 dependencies: 17295 17437 '@trpc/client': 11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2) 17296 17438 '@trpc/server': 11.4.4(typescript@5.7.2) ··· 17401 17543 17402 17544 '@types/babel__template@7.4.4': 17403 17545 dependencies: 17404 - '@babel/parser': 7.26.3 17546 + '@babel/parser': 7.27.7 17405 17547 '@babel/types': 7.26.3 17406 17548 17407 17549 '@types/babel__traverse@7.20.6': ··· 18250 18392 dependencies: 18251 18393 base64-js: 1.5.1 18252 18394 18253 - browserslist@4.24.2: 18254 - dependencies: 18255 - caniuse-lite: 1.0.30001721 18256 - electron-to-chromium: 1.5.64 18257 - node-releases: 2.0.18 18258 - update-browserslist-db: 1.1.1(browserslist@4.24.2) 18259 - 18260 18395 browserslist@4.25.1: 18261 18396 dependencies: 18262 18397 caniuse-lite: 1.0.30001726 ··· 18323 18458 18324 18459 camelcase@8.0.0: {} 18325 18460 18326 - caniuse-lite@1.0.30001721: {} 18327 - 18328 18461 caniuse-lite@1.0.30001726: {} 18329 18462 18330 18463 ccount@2.0.1: {} ··· 18344 18477 dependencies: 18345 18478 ansi-styles: 4.3.0 18346 18479 supports-color: 7.2.0 18347 - 18348 - chalk@5.3.0: {} 18349 18480 18350 18481 chalk@5.4.1: {} 18351 18482 ··· 18472 18603 18473 18604 cmdk@1.0.4(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): 18474 18605 dependencies: 18475 - '@radix-ui/react-dialog': 1.1.4(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 18606 + '@radix-ui/react-dialog': 1.1.14(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 18476 18607 '@radix-ui/react-id': 1.1.0(@types/react@19.1.10)(react@19.1.1) 18477 18608 '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 18478 18609 react: 19.1.1 ··· 18919 19050 18920 19051 electron-to-chromium@1.5.178: {} 18921 19052 18922 - electron-to-chromium@1.5.64: {} 18923 - 18924 19053 emmet@2.4.11: 18925 19054 dependencies: 18926 19055 '@emmetio/abbreviation': 2.3.3 ··· 19338 19467 '@nodelib/fs.walk': 1.2.8 19339 19468 glob-parent: 5.1.2 19340 19469 merge2: 1.4.1 19341 - micromatch: 4.0.5 19470 + micromatch: 4.0.8 19342 19471 19343 19472 fast-json-stable-stringify@2.1.0: {} 19344 19473 ··· 21364 21493 transitivePeerDependencies: 21365 21494 - supports-color 21366 21495 21367 - micromatch@4.0.5: 21368 - dependencies: 21369 - braces: 3.0.2 21370 - picomatch: 2.3.1 21371 - 21372 21496 micromatch@4.0.8: 21373 21497 dependencies: 21374 21498 braces: 3.0.3 ··· 21628 21752 mkdirp: 0.5.6 21629 21753 resolve: 1.22.10 21630 21754 21631 - node-releases@2.0.18: {} 21632 - 21633 21755 node-releases@2.0.19: {} 21634 21756 21635 21757 nopt@7.2.1: ··· 21747 21869 21748 21870 ora@6.3.1: 21749 21871 dependencies: 21750 - chalk: 5.3.0 21872 + chalk: 5.4.1 21751 21873 cli-cursor: 4.0.0 21752 - cli-spinners: 2.9.1 21874 + cli-spinners: 2.9.2 21753 21875 is-interactive: 2.0.0 21754 21876 is-unicode-supported: 1.3.0 21755 21877 log-symbols: 5.1.0 ··· 21859 21981 21860 21982 parse-json@5.2.0: 21861 21983 dependencies: 21862 - '@babel/code-frame': 7.26.2 21984 + '@babel/code-frame': 7.27.1 21863 21985 error-ex: 1.3.2 21864 21986 json-parse-even-better-errors: 2.3.1 21865 21987 lines-and-columns: 1.2.4 ··· 22938 23060 dependencies: 22939 23061 '@antfu/ni': 23.3.1 22940 23062 '@babel/core': 7.26.0 22941 - '@babel/parser': 7.27.5 23063 + '@babel/parser': 7.27.7 22942 23064 '@babel/plugin-transform-typescript': 7.27.1(@babel/core@7.26.0) 22943 23065 '@modelcontextprotocol/sdk': 1.12.1 22944 23066 commander: 10.0.1 ··· 23916 24038 node-fetch-native: 1.6.6 23917 24039 ofetch: 1.4.1 23918 24040 ufo: 1.5.4 23919 - 23920 - update-browserslist-db@1.1.1(browserslist@4.24.2): 23921 - dependencies: 23922 - browserslist: 4.24.2 23923 - escalade: 3.2.0 23924 - picocolors: 1.1.1 23925 24041 23926 24042 update-browserslist-db@1.1.3(browserslist@4.25.1): 23927 24043 dependencies: