Openstatus www.openstatus.dev

feat: nerd mode (#1375)

* feat: nerd mode

* fix: tabs width

* chore: enable floating button

* feat: font commit mono

* fix: format

* chore: small stuff

* chore: leading

* chore: check

* chore: title truncate

authored by

Maximilian Kaske and committed by
GitHub
fdacaad8 2fa2f9ef

+266 -79
+1
apps/dashboard/next-env.d.ts
··· 1 1 /// <reference types="next" /> 2 2 /// <reference types="next/image-types/global" /> 3 + /// <reference path="./.next/types/routes.d.ts" /> 3 4 4 5 // NOTE: This file should not be edited 5 6 // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+1
apps/status-page/next-env.d.ts
··· 1 1 /// <reference types="next" /> 2 2 /// <reference types="next/image-types/global" /> 3 + /// <reference path="./.next/types/routes.d.ts" /> 3 4 4 5 // NOTE: This file should not be edited 5 6 // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
apps/status-page/public/fonts/CommitMono-400-Italic.otf

This is a binary file and will not be displayed.

apps/status-page/public/fonts/CommitMono-400-Regular.otf

This is a binary file and will not be displayed.

apps/status-page/public/fonts/CommitMono-700-Italic.otf

This is a binary file and will not be displayed.

apps/status-page/public/fonts/CommitMono-700-Regular.otf

This is a binary file and will not be displayed.

+6 -2
apps/status-page/src/app/(status-page)/[domain]/(public)/events/(list)/page.tsx
··· 21 21 import { Badge } from "@/components/ui/badge"; 22 22 import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 23 23 import { formatDate } from "@/lib/formatter"; 24 - import { CircleCheck } from "lucide-react"; 24 + import { Check } from "lucide-react"; 25 25 import Link from "next/link"; 26 26 27 27 // TODO: include ?filter=maintenance/reports ··· 71 71 <StatusEventTitle className="inline-flex gap-1"> 72 72 {report.title} 73 73 {isReportResolvedOnly ? ( 74 - <CircleCheck className="size-4 text-success shrink-0 mt-1 ml-1.5" /> 74 + <div className="mt-1 ml-1.5"> 75 + <div className="rounded-full border border-success/20 bg-success/10 p-0.5 text-success"> 76 + <Check className="size-3 shrink-0" /> 77 + </div> 78 + </div> 75 79 ) : null} 76 80 </StatusEventTitle> 77 81 {report.monitorsToStatusReports.length > 0 ? (
+71 -51
apps/status-page/src/app/(status-page)/[domain]/(public)/page.tsx
··· 12 12 StatusBanner, 13 13 StatusBannerContainer, 14 14 StatusBannerContent, 15 - StatusBannerTitle, 15 + StatusBannerTabs, 16 + StatusBannerTabsContent, 17 + StatusBannerTabsList, 18 + StatusBannerTabsTrigger, 16 19 } from "@/components/status-page/status-banner"; 17 20 import { 18 21 StatusEventTimelineMaintenance, ··· 22 25 import { StatusMonitor } from "@/components/status-page/status-monitor"; 23 26 import { Separator } from "@/components/ui/separator"; 24 27 import { useTRPC } from "@/lib/trpc/client"; 28 + import { cn } from "@/lib/utils"; 25 29 import { useQuery } from "@tanstack/react-query"; 26 - import Link from "next/link"; 27 30 import { useParams } from "next/navigation"; 28 31 29 32 export default function Page() { ··· 55 58 </StatusHeader> 56 59 {page.openEvents.length > 0 ? ( 57 60 <StatusContent> 58 - {page.openEvents.map((e) => { 59 - if (e.type === "maintenance") { 60 - const maintenance = page.maintenances.find( 61 - (maintenance) => maintenance.id === e.id, 62 - ); 63 - if (!maintenance) return null; 64 - return ( 65 - <Link 66 - href={`./events/maintenance/${e.id}`} 67 - key={e.id} 68 - className="rounded-lg" 69 - > 70 - <StatusBannerContainer status={e.status}> 71 - <StatusBannerTitle>{e.name}</StatusBannerTitle> 72 - <StatusBannerContent> 73 - <StatusEventTimelineMaintenance 74 - maintenance={maintenance} 75 - withDot={false} 76 - /> 77 - </StatusBannerContent> 78 - </StatusBannerContainer> 79 - </Link> 80 - ); 81 - } 82 - if (e.type === "report") { 83 - const report = page.statusReports.find( 84 - (report) => report.id === e.id, 85 - ); 86 - if (!report) return null; 87 - return ( 88 - <Link 89 - href={`./events/report/${e.id}`} 90 - key={e.id} 91 - className="rounded-lg" 92 - > 93 - <StatusBannerContainer status={e.status}> 94 - <StatusBannerTitle>{e.name}</StatusBannerTitle> 95 - <StatusBannerContent> 96 - <StatusEventTimelineReport 97 - updates={report.statusReportUpdates} 98 - withDot={false} 99 - /> 100 - </StatusBannerContent> 101 - </StatusBannerContainer> 102 - </Link> 103 - ); 104 - } 105 - return null; 106 - })} 61 + <StatusBannerTabs 62 + defaultValue={`${page.openEvents[0].type}-${page.openEvents[0].id}`} 63 + > 64 + <StatusBannerTabsList> 65 + {page.openEvents.map((e, i) => { 66 + return ( 67 + <StatusBannerTabsTrigger 68 + value={`${e.type}-${e.id}`} 69 + status={e.status} 70 + key={e.id} 71 + className={cn( 72 + i === 0 && "rounded-tl-lg", 73 + i === page.openEvents.length - 1 && "rounded-tr-lg", 74 + )} 75 + > 76 + {e.name} 77 + </StatusBannerTabsTrigger> 78 + ); 79 + })} 80 + </StatusBannerTabsList> 81 + {page.openEvents.map((e) => { 82 + if (e.type === "report") { 83 + const report = page.statusReports.find( 84 + (report) => report.id === e.id, 85 + ); 86 + if (!report) return null; 87 + return ( 88 + <StatusBannerTabsContent 89 + value={`${e.type}-${e.id}`} 90 + key={e.id} 91 + > 92 + <StatusBannerContainer status={e.status}> 93 + <StatusBannerContent> 94 + <StatusEventTimelineReport 95 + updates={report.statusReportUpdates} 96 + withDot={false} 97 + /> 98 + </StatusBannerContent> 99 + </StatusBannerContainer> 100 + </StatusBannerTabsContent> 101 + ); 102 + } 103 + if (e.type === "maintenance") { 104 + const maintenance = page.maintenances.find( 105 + (maintenance) => maintenance.id === e.id, 106 + ); 107 + if (!maintenance) return null; 108 + return ( 109 + <StatusBannerTabsContent 110 + value={`${e.type}-${e.id}`} 111 + key={e.id} 112 + > 113 + <StatusBannerContainer status={e.status}> 114 + <StatusBannerContent> 115 + <StatusEventTimelineMaintenance 116 + maintenance={maintenance} 117 + withDot={false} 118 + /> 119 + </StatusBannerContent> 120 + </StatusBannerContainer> 121 + </StatusBannerTabsContent> 122 + ); 123 + } 124 + return null; 125 + })} 126 + </StatusBannerTabs> 107 127 </StatusContent> 108 128 ) : ( 109 129 <StatusBanner status={page.status} />
+1 -1
apps/status-page/src/app/(status-page)/[domain]/layout.tsx
··· 19 19 20 20 const DISPLAY_FLOATING_BUTTON = 21 21 process.env.NODE_ENV === "development" || 22 - process.env.ENABLE_FLOATING_BUTTON === "true"; 22 + process.env.NEXT_PUBLIC_ENABLE_FLOATING_BUTTON === "true"; 23 23 24 24 export default async function Layout({ 25 25 children,
+10 -2
apps/status-page/src/app/globals.css
··· 14 14 --color-background: var(--background); 15 15 --color-foreground: var(--foreground); 16 16 --font-sans: var(--font-geist-sans); 17 - --font-mono: var(--font-geist-mono); 17 + --font-mono: var(--font-commit-mono, var(--font-geist-mono)); 18 18 --color-sidebar-ring: var(--sidebar-ring); 19 19 --color-sidebar-border: var(--sidebar-border); 20 20 --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); ··· 55 55 } 56 56 57 57 :root { 58 - --radius: 0.625rem; 58 + /* --radius: 0.625rem; */ 59 + --radius: 0px; 59 60 --background: oklch(1 0 0); 60 61 --foreground: oklch(0.145 0 0); 61 62 --card: oklch(1 0 0); ··· 175 176 @apply bg-background text-foreground; 176 177 } 177 178 } 179 + 180 + @layer utilities { 181 + /* NOTE: allows us to --radius: 0px and avoid rounding issues - otherwise it is 'infinite * 1px' */ 182 + .rounded-full { 183 + border-radius: calc(var(--radius) * 99999999); 184 + } 185 + }
+27
apps/status-page/src/app/layout.tsx
··· 24 24 subsets: ["latin"], 25 25 }); 26 26 27 + const commitMono = LocalFont({ 28 + src: [ 29 + { 30 + path: "../../public/fonts/CommitMono-400-Regular.otf", 31 + weight: "400", 32 + style: "normal", 33 + }, 34 + { 35 + path: "../../public/fonts/CommitMono-400-Italic.otf", 36 + weight: "400", 37 + style: "italic", 38 + }, 39 + { 40 + path: "../../public/fonts/CommitMono-700-Regular.otf", 41 + weight: "700", 42 + style: "normal", 43 + }, 44 + { 45 + path: "../../public/fonts/CommitMono-700-Italic.otf", 46 + weight: "700", 47 + style: "italic", 48 + }, 49 + ], 50 + variable: "--font-commit-mono", 51 + }); 52 + 27 53 export const metadata: Metadata = { 28 54 ...defaultMetadata, 29 55 twitter: { ··· 48 74 geistSans.variable, 49 75 geistMono.variable, 50 76 cal.variable, 77 + commitMono.variable, 51 78 "antialiased", 52 79 )} 53 80 >
+3 -3
apps/status-page/src/components/nav/footer.tsx
··· 20 20 return ( 21 21 <footer {...props}> 22 22 <div className="mx-auto flex max-w-2xl items-center justify-between gap-4 px-3 py-2"> 23 - <div className="leading-[0.9]"> 24 - <p className="text-muted-foreground text-sm"> 25 - Powered by <Link href="#">OpenStatus</Link> 23 + <div> 24 + <p className="font-mono text-muted-foreground text-sm leading-none"> 25 + powered by <Link href="#">openstatus</Link> 26 26 </p> 27 27 <TimestampHoverCard date={new Date(dataUpdatedAt)} side="top"> 28 28 <span className="text-muted-foreground/70 text-xs">
+1 -1
apps/status-page/src/components/status-page/floating-button.tsx
··· 154 154 <Button 155 155 size="icon" 156 156 variant="outline" 157 - className="size-12 rounded-full" 157 + className="size-12 rounded-full dark:bg-background" 158 158 > 159 159 <Settings className="size-5" /> 160 160 <span className="sr-only">Open status page settings</span>
+99 -5
apps/status-page/src/components/status-page/status-banner.tsx
··· 1 + import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 1 2 import { cn } from "@/lib/utils"; 2 3 import { 3 4 AlertCircleIcon, ··· 48 49 data-status={status} 49 50 className={cn( 50 51 "group/status-banner overflow-hidden rounded-lg border", 51 - "data-[status=success]:border-success", 52 - "data-[status=degraded]:border-warning", 53 - "data-[status=error]:border-destructive", 54 - "data-[status=info]:border-info", 52 + "data-[status=success]:border-success data-[status=success]:bg-success/5 dark:data-[status=success]:bg-success/10", 53 + "data-[status=degraded]:border-warning data-[status=degraded]:bg-warning/5 dark:data-[status=degraded]:bg-warning/10", 54 + "data-[status=error]:border-destructive data-[status=error]:bg-destructive/5 dark:data-[status=error]:bg-destructive/10", 55 + "data-[status=info]:border-info data-[status=info]:bg-info/5 dark:data-[status=info]:bg-info/10", 55 56 className, 56 57 )} 57 58 > ··· 90 91 return ( 91 92 <div 92 93 className={cn( 93 - "px-3 py-2 font-medium text-background sm:px-4 sm:py-3", 94 + "px-3 py-2 font-medium text-background", 94 95 "group-data-[status=success]/status-banner:bg-success", 95 96 "group-data-[status=degraded]/status-banner:bg-warning", 96 97 "group-data-[status=error]/status-banner:bg-destructive", ··· 139 140 </div> 140 141 ); 141 142 } 143 + 144 + // Tabs Components 145 + 146 + export function StatusBannerTabs({ 147 + className, 148 + children, 149 + status, 150 + ...props 151 + }: React.ComponentProps<typeof Tabs> & { 152 + status?: "success" | "degraded" | "error" | "info"; 153 + }) { 154 + return ( 155 + <Tabs 156 + data-slot="status-banner-tabs" 157 + data-status={status} 158 + className={cn( 159 + "gap-0", 160 + "data-[status=success]:bg-success/20", 161 + "data-[status=degraded]:bg-warning/20", 162 + "data-[status=error]:bg-destructive/20", 163 + "data-[status=info]:bg-info/20", 164 + className, 165 + )} 166 + {...props} 167 + > 168 + {children} 169 + </Tabs> 170 + ); 171 + } 172 + 173 + export function StatusBannerTabsList({ 174 + className, 175 + children, 176 + ...props 177 + }: React.ComponentProps<typeof TabsList>) { 178 + return ( 179 + <div className={cn("rounded-t-lg", "w-full overflow-x-auto")}> 180 + <TabsList 181 + className={cn( 182 + "rounded-none rounded-t-lg p-0", 183 + "border-none", 184 + className, 185 + )} 186 + {...props} 187 + > 188 + {children} 189 + </TabsList> 190 + </div> 191 + ); 192 + } 193 + 194 + export function StatusBannerTabsTrigger({ 195 + className, 196 + children, 197 + status, 198 + ...props 199 + }: React.ComponentProps<typeof TabsTrigger> & { 200 + status?: "success" | "degraded" | "error" | "info"; 201 + }) { 202 + return ( 203 + <TabsTrigger 204 + data-slot="status-banner-tabs-trigger" 205 + data-status={status} 206 + className={cn( 207 + "font-mono", 208 + "rounded-none border-none focus-visible:ring-inset", 209 + "h-full text-foreground data-[state=active]:text-background dark:text-foreground dark:data-[state=active]:text-background", 210 + "data-[state=active]:data-[status=success]:bg-success data-[status=success]:bg-success/50 dark:data-[state=active]:data-[status=success]:bg-success dark:data-[status=success]:bg-success/50", 211 + "data-[state=active]:data-[status=degraded]:bg-warning data-[status=degraded]:bg-warning/50 dark:data-[state=active]:data-[status=degraded]:bg-warning dark:data-[status=degraded]:bg-warning/50", 212 + "data-[state=active]:data-[status=error]:bg-destructive data-[status=error]:bg-destructive/50 dark:data-[state=active]:data-[status=error]:bg-destructive dark:data-[status=error]:bg-destructive/50", 213 + "data-[state=active]:data-[status=info]:bg-info data-[status=info]:bg-info/50 dark:data-[state=active]:data-[status=info]:bg-info dark:data-[status=info]:bg-info/50", 214 + "data-[state=active]:shadow-none", 215 + className, 216 + )} 217 + {...props} 218 + > 219 + {children} 220 + </TabsTrigger> 221 + ); 222 + } 223 + 224 + // NOTE: tabing into content is not being highlighted 225 + export function StatusBannerTabsContent({ 226 + className, 227 + children, 228 + ...props 229 + }: React.ComponentProps<typeof TabsContent>) { 230 + return ( 231 + <TabsContent className={cn("-mx-3", className)} {...props}> 232 + {children} 233 + </TabsContent> 234 + ); 235 + }
+10 -3
apps/status-page/src/components/status-page/status-events.tsx
··· 124 124 } 125 125 withSeparator={index !== updates.length - 1} 126 126 withDot={withDot} 127 + isLast={index === updates.length - 1} 127 128 /> 128 129 ))} 129 130 </div> ··· 135 136 duration, 136 137 withSeparator = true, 137 138 withDot = true, 139 + isLast = false, 138 140 }: { 139 141 report: { 140 142 date: Date; ··· 144 146 withSeparator?: boolean; 145 147 duration?: string; 146 148 withDot?: boolean; 149 + isLast?: boolean; 147 150 }) { 148 151 return ( 149 152 <div data-variant={report.status} className="group"> ··· 157 160 {withSeparator ? <StatusEventTimelineSeparator /> : null} 158 161 </div> 159 162 ) : null} 160 - <div className="mb-2"> 163 + <div className={cn(isLast ? "mb-0" : "mb-2")}> 161 164 <StatusEventTimelineTitle> 162 165 <span>{status[report.status]}</span>{" "} 163 166 {/* underline decoration-dashed underline-offset-2 decoration-muted-foreground/30 */} ··· 209 212 </div> 210 213 </div> 211 214 ) : null} 212 - <div className="mb-2"> 215 + {/* NOTE: is always last, no need for className="mb-2" */} 216 + <div> 213 217 <StatusEventTimelineTitle> 214 218 <span>Maintenance</span>{" "} 215 219 <span className="font-mono text-muted-foreground/70 text-xs"> ··· 259 263 ...props 260 264 }: React.ComponentProps<"div">) { 261 265 return ( 262 - <div className={cn("text-muted-foreground text-sm", className)} {...props}> 266 + <div 267 + className={cn("font-mono text-muted-foreground text-sm", className)} 268 + {...props} 269 + > 263 270 {children} 264 271 </div> 265 272 );
+1 -1
apps/status-page/src/components/status-page/status-feed.tsx
··· 82 82 return ( 83 83 <StatusEmptyState> 84 84 <Newspaper className="size-4 text-muted-foreground" /> 85 - <StatusEmptyStateTitle>No recent reports</StatusEmptyStateTitle> 85 + <StatusEmptyStateTitle>No recent notifications</StatusEmptyStateTitle> 86 86 <StatusEmptyStateDescription> 87 87 There have been no reports within the last 7 days. 88 88 </StatusEmptyStateDescription>
+12 -5
apps/status-page/src/components/status-page/status-monitor.tsx
··· 56 56 {...props} 57 57 > 58 58 <div className="flex flex-row items-center justify-between gap-4"> 59 - <div className="flex flex-row items-center gap-2"> 59 + <div className="flex flex-row min-w-0 items-center gap-2"> 60 60 <StatusMonitorTitle>{monitor.name}</StatusMonitorTitle> 61 61 <StatusMonitorDescription> 62 62 {monitor.description} ··· 90 90 ...props 91 91 }: React.ComponentProps<"div">) { 92 92 return ( 93 - <div className={cn("font-medium", className)} {...props}> 93 + <div 94 + className={cn( 95 + "flex-1 truncate font-medium font-mono text-foreground leading-none", 96 + className, 97 + )} 98 + {...props} 99 + > 94 100 {children} 95 101 </div> 96 102 ); ··· 134 140 return ( 135 141 <div 136 142 className={cn( 137 - "flex size-4 items-center justify-center rounded-full bg-muted text-background [&>svg]:size-2.5", 143 + "flex size-[12.5px] items-center justify-center rounded-full bg-muted text-background [&>svg]:size-[9px]", 138 144 "group-data-[variant=success]/monitor:bg-success", 139 145 "group-data-[variant=degraded]/monitor:bg-warning", 140 146 "group-data-[variant=error]/monitor:bg-destructive", ··· 159 165 isLoading?: boolean; 160 166 }) { 161 167 return ( 162 - <div className="flex flex-row items-center justify-between text-muted-foreground text-xs"> 168 + <div className="flex flex-row items-center justify-between font-mono text-muted-foreground text-xs"> 163 169 <div> 164 170 {isLoading ? ( 165 171 <Skeleton className="h-4 w-18" /> ··· 185 191 return ( 186 192 <div 187 193 {...props} 188 - className={cn("font-mono text-muted-foreground text-sm", className)} 194 + className={cn("font-mono text-foreground text-sm leading-non", className)} 189 195 > 190 196 {children} 191 197 </div> ··· 206 212 return ( 207 213 <div 208 214 className={cn( 215 + "font-mono", 209 216 "group-data-[variant=success]/monitor:text-success", 210 217 "group-data-[variant=degraded]/monitor:text-warning", 211 218 "group-data-[variant=error]/monitor:text-destructive",
+1 -1
apps/status-page/src/components/status-page/status-tracker.tsx
··· 189 189 <HoverCardTrigger asChild> 190 190 <div 191 191 className={cn( 192 - "group relative flex h-full w-full cursor-pointer flex-col px-px outline-none hover:opacity-80 focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 data-[aria-pressed=true]:opacity-80", 192 + "group relative flex h-full w-full cursor-pointer flex-col px-px outline-none first:pl-0 last:pr-0 hover:opacity-80 focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 data-[aria-pressed=true]:opacity-80", 193 193 )} 194 194 onClick={() => handleBarClick(index)} 195 195 onFocus={() => handleBarFocus(index)}
+4 -1
apps/status-page/src/components/status-page/status.tsx
··· 181 181 ...props 182 182 }: React.ComponentProps<"div">) { 183 183 return ( 184 - <div className={cn("text-muted-foreground text-sm", className)} {...props}> 184 + <div 185 + className={cn("font-mono text-muted-foreground text-sm", className)} 186 + {...props} 187 + > 185 188 {children} 186 189 </div> 187 190 );
+18 -3
apps/web/next.config.js
··· 45 45 async headers() { 46 46 return [{ source: "/(.*)", headers: securityHeaders }]; 47 47 }, 48 - trailingSlash: true, 49 48 async redirects() { 50 49 return [ 51 50 { ··· 66 65 ]; 67 66 }, 68 67 async rewrites() { 68 + const HOST = 69 + process.env.NODE_ENV === "development" ? "localhost:3001" : "stpg.dev"; 70 + const PROTOCOL = process.env.NODE_ENV === "development" ? "http" : "https"; 69 71 return { 70 72 beforeFiles: [ 71 73 // Proxy app subdomain to /app ··· 79 81 ], 80 82 destination: "/app/:path*", 81 83 }, 84 + // New design: proxy Next.js assets from external host when cookie indicates "new" 85 + { 86 + source: "/_next/:path*", 87 + has: [ 88 + { type: "cookie", key: "sp_mode", value: "new" }, 89 + { 90 + type: "host", 91 + value: "(?<slug>[^.]+)\\.(openstatus\\.dev|localhost)", 92 + }, 93 + ], 94 + destination: `${PROTOCOL}://${HOST}/_next/:path*`, 95 + }, 82 96 // New design: proxy app routes to external host with slug prefix 83 97 { 84 - source: "/:path*", 98 + source: "/:path((?!_next/).*)", 85 99 has: [ 86 100 { type: "cookie", key: "sp_mode", value: "new" }, 87 101 { ··· 89 103 value: "(?<slug>[^.]+)\\.(openstatus\\.dev|localhost)", 90 104 }, 91 105 ], 92 - destination: "https://:slug.stpg.dev/:path*", 106 + // NOTE: might be different on prod and localhost (without :slug) 107 + destination: `${PROTOCOL}://${HOST}/:slug/:path*`, 93 108 }, 94 109 ], 95 110 };