Openstatus www.openstatus.dev
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 364 lines 13 kB view raw
1"use client"; 2 3import { Link } from "@/components/common/link"; 4import { 5 Section, 6 SectionDescription, 7 SectionGroup, 8 SectionGroupHeader, 9 SectionHeader, 10 SectionTitle, 11} from "@/components/content/section"; 12import { recomputeStyles } from "@/components/status-page/floating-button"; 13import { 14 Status, 15 StatusContent, 16 StatusDescription, 17 StatusHeader, 18 StatusTitle, 19} from "@/components/status-page/status"; 20import { StatusBanner } from "@/components/status-page/status-banner"; 21import { 22 StatusEvent, 23 StatusEventAffected, 24 StatusEventAffectedBadge, 25 StatusEventContent, 26 StatusEventDate, 27 StatusEventTimelineReport, 28 StatusEventTitle, 29} from "@/components/status-page/status-events"; 30import { StatusMonitor } from "@/components/status-page/status-monitor"; 31import { ThemePalettePicker } from "@/components/themes/theme-palette-picker"; 32import { ThemeSelect } from "@/components/themes/theme-select"; 33import { Button } from "@/components/ui/button"; 34import { Input } from "@/components/ui/input"; 35import { Separator } from "@/components/ui/separator"; 36import { useSidebar } from "@/components/ui/sidebar"; 37import { Skeleton } from "@/components/ui/skeleton"; 38import { 39 Tooltip, 40 TooltipContent, 41 TooltipProvider, 42 TooltipTrigger, 43} from "@/components/ui/tooltip"; 44import { monitors } from "@/data/monitors"; 45import { useTRPC } from "@/lib/trpc/client"; 46import { cn } from "@/lib/utils"; 47import { THEMES, THEME_KEYS } from "@openstatus/theme-store"; 48import { useQuery } from "@tanstack/react-query"; 49import { useTheme } from "next-themes"; 50import { useQueryStates } from "nuqs"; 51import { useEffect, useState } from "react"; 52import { searchParamsParsers } from "./search-params"; 53 54const MAIN_COLORS = [ 55 { key: "--primary", label: "Primary" }, 56 { key: "--success", label: "Operational" }, 57 { key: "--destructive", label: "Error" }, 58 { key: "--warning", label: "Degraded" }, 59 { key: "--info", label: "Maintenance" }, 60] as const; 61 62// TODO: add keyboard navigation for selection? 63 64export function Client() { 65 const { resolvedTheme } = useTheme(); 66 const [isMounted, setIsMounted] = useState(false); 67 const [{ q, t }, setSearchParams] = useQueryStates(searchParamsParsers); 68 const theme = t ? THEMES[t as keyof typeof THEMES] : undefined; 69 const { toggleSidebar } = useSidebar(); 70 71 useEffect(() => { 72 setIsMounted(true); 73 }, []); 74 75 useEffect(() => { 76 if (isMounted && t) { 77 recomputeStyles(t); 78 } 79 }, [t, isMounted]); 80 81 return ( 82 <SectionGroup> 83 <SectionGroupHeader> 84 <h1 className="font-bold text-2xl md:text-4xl"> 85 Status Page Theme Explorer 86 </h1> 87 <h2 className="font-medium text-muted-foreground md:text-lg"> 88 View all the openstatus themes for your status page and learn how to 89 create your own theme. 90 </h2> 91 </SectionGroupHeader> 92 <Section> 93 <SectionHeader> 94 <SectionTitle>Explorer</SectionTitle> 95 <SectionDescription> 96 Search for your favorite status page theme.{" "} 97 <Link href="#contribute-theme">Contribute your own?</Link> 98 </SectionDescription> 99 </SectionHeader> 100 <div className="sticky top-0 z-10 overflow-hidden rounded-lg border border-border bg-background outline-[3px] outline-background sm:relative"> 101 <div className="relative"> 102 <div className="absolute top-0 right-0 rounded-bl-lg border-border border-b border-l bg-muted/50 px-2 py-0.5 text-[10px]"> 103 {theme?.name} 104 </div> 105 <div className="sm:p-8"> 106 <ThemePlaygroundStatus className="scale-80 sm:scale-100" /> 107 </div> 108 </div> 109 </div> 110 <div className="flex gap-3"> 111 <ThemeSelect className="min-w-[125px] max-w-[125px]" /> 112 <Input 113 placeholder={`Search from ${THEME_KEYS.length} themes`} 114 value={q ?? ""} 115 onChange={(e) => { 116 if (e.target.value.length === 0) { 117 setSearchParams({ q: null }); 118 } 119 setSearchParams({ q: e.target.value.trim().toLowerCase() }); 120 }} 121 /> 122 <ThemePalettePicker /> 123 </div> 124 <ul className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3"> 125 {THEME_KEYS.filter((k) => { 126 const theme = THEMES[k]; 127 return ( 128 theme.author.name 129 .toLowerCase() 130 .includes(q?.toLowerCase() ?? "") || 131 theme.name.toLowerCase().includes(q?.toLowerCase() ?? "") 132 ); 133 }).map((k) => { 134 const theme = THEMES[k]; 135 const style = isMounted 136 ? theme[resolvedTheme as "dark" | "light"] 137 : undefined; 138 139 return ( 140 <li key={k} className="group/theme-card space-y-1.5"> 141 <div 142 data-active={k === t} 143 data-slot="theme-card" 144 data-theme={k} 145 className="relative h-40 cursor-pointer overflow-hidden rounded-md border border-border outline-none transition-all focus:outline-ring/50 focus:ring-2 focus:ring-ring/50 data-[active=true]:border-ring data-[active=true]:outline-[3px] data-[active=true]:outline-ring/50" 146 onClick={() => setSearchParams({ t: k })} 147 role="button" 148 tabIndex={0} 149 onKeyDown={(e) => { 150 if (e.key === "Enter" || e.key === " ") { 151 setSearchParams({ t: k }); 152 } 153 }} 154 > 155 {isMounted ? ( 156 <div 157 className="absolute h-full w-full bg-background text-foreground" 158 style={style as React.CSSProperties} 159 inert 160 > 161 <ThemePlaygroundStatus className="pointer-events-none scale-80" /> 162 </div> 163 ) : ( 164 <Skeleton className="absolute h-full w-full" /> 165 )} 166 </div> 167 <div className="flex items-start justify-between gap-2"> 168 <div className="space-y-0.5 truncate"> 169 <div className="truncate font-medium text-foreground text-sm leading-none"> 170 {theme.name} 171 </div> 172 <div className="font-mono text-xs"> 173 <Link 174 href={theme.author.url} 175 target="_blank" 176 rel="noopener noreferrer" 177 className="text-muted-foreground" 178 > 179 by {theme.author.name} 180 </Link> 181 </div> 182 </div> 183 <div className="flex gap-0.5"> 184 {MAIN_COLORS.map((color) => { 185 const backgroundColor = style 186 ? style[color.key] 187 : undefined; 188 189 if (!isMounted) { 190 return ( 191 <Skeleton 192 key={color.key} 193 className="size-3.5 rounded-sm" 194 /> 195 ); 196 } 197 return ( 198 <TooltipProvider key={color.key}> 199 <Tooltip> 200 <TooltipTrigger> 201 <div 202 className="size-3.5 rounded-sm border bg-muted-foreground" 203 style={{ backgroundColor }} 204 /> 205 </TooltipTrigger> 206 <TooltipContent>{color.label}</TooltipContent> 207 </Tooltip> 208 </TooltipProvider> 209 ); 210 })} 211 </div> 212 </div> 213 </li> 214 ); 215 })} 216 </ul> 217 </Section> 218 <Separator /> 219 <Section> 220 <SectionHeader id="contribute-theme"> 221 <SectionTitle>Contribute Theme</SectionTitle> 222 <SectionDescription> 223 Contribute your own theme to the community. 224 </SectionDescription> 225 </SectionHeader> 226 <div className="prose dark:prose-invert prose-sm max-w-none"> 227 <p> 228 You can contribute your own theme by creating a new file in the{" "} 229 <code>@openstatus/theme-store</code> package. You&apos;ll only need 230 to override css variables. If you are familiar with shadcn, you'll 231 know the trick (it also allows you to override `--radius`). Make 232 sure your object is satisfying the <code>Theme</code> interface. We 233 provide a theme builder to help you with the process. 234 </p> 235 <Button onClick={toggleSidebar}>Toggle Theme Builder</Button> 236 <p> 237 Go to the{" "} 238 <Link href="https://github.com/openstatusHQ/openstatus/tree/main/packages/theme-store"> 239 GitHub directory 240 </Link>{" "} 241 to see the existing themes and create a new one by forking and 242 creating a pull request. 243 </p> 244 <p> 245 Once you're done, you can test it by adding the following snippet to 246 your status page: 247 </p> 248 <pre> 249 <code>sessionStorage.setItem("community-theme", "true");</code> 250 </pre> 251 <p> 252 Or use the following button to test it on the `status` page slug: 253 </p> 254 <Button 255 onClick={() => { 256 // NOTE: we use it to display the 'floating-theme' component 257 sessionStorage.setItem("community-theme", "true"); 258 window.location.href = "/status"; 259 }} 260 > 261 Test it 262 </Button> 263 {/* TODO: OR go to the status-page config and click on the View and Configure button */} 264 </div> 265 </Section> 266 <Separator /> 267 <Section> 268 <div className="prose dark:prose-invert prose-sm max-w-none"> 269 <p> 270 Why don't we allow custom css styles to be overridden and only 271 support themes? 272 </p> 273 <ul> 274 <li>Keep it simple for the user</li> 275 <li>Don't end up with a xmas tree</li> 276 <li>Keep the theme consistent</li> 277 <li>Avoid conflicts with other styles</li> 278 <li> 279 Keep the theme maintainable (but this will also mean, a change 280 will affect all users) 281 </li> 282 </ul> 283 </div> 284 </Section> 285 </SectionGroup> 286 ); 287} 288 289function ThemePlaygroundStatus({ 290 className, 291 ...props 292}: React.ComponentProps<"div"> & {}) { 293 const trpc = useTRPC(); 294 const { data: uptimeData, isLoading } = useQuery( 295 trpc.statusPage.getNoopUptime.queryOptions(), 296 ); 297 return ( 298 // NOTE: we use pointer-events-none to prevent the hover card or tooltip from being interactive - the Portal container is document body and we loose the styles 299 <div className={cn("h-full w-full", className)} {...props}> 300 <Status variant="success"> 301 <StatusHeader> 302 <StatusTitle>Acme Inc.</StatusTitle> 303 <StatusDescription> 304 Get informed about our services. 305 </StatusDescription> 306 </StatusHeader> 307 <StatusBanner status="success" /> 308 <StatusContent> 309 {/* TODO: create mock data */} 310 <StatusMonitor 311 status="success" 312 data={uptimeData?.data || []} 313 monitor={monitors[0]} 314 showUptime={true} 315 uptime={uptimeData?.uptime} 316 isLoading={isLoading} 317 /> 318 </StatusContent> 319 </Status> 320 </div> 321 ); 322} 323 324// NOTE: we could add a tabs component here to switch between status and events 325function ThemePlaygroundEvents({ 326 className, 327 ...props 328}: React.ComponentProps<"div"> & {}) { 329 const trpc = useTRPC(); 330 const { data: report } = useQuery( 331 trpc.statusPage.getNoopReport.queryOptions(), 332 ); 333 const firstUpdate = report?.statusReportUpdates[0]; 334 335 if (!firstUpdate || !report) return null; 336 337 return ( 338 <div className={cn("h-full w-full", className)} {...props}> 339 <Status variant="success"> 340 <StatusEvent> 341 <StatusEventDate date={firstUpdate.date} className="lg:flex-row" /> 342 <StatusEventContent hoverable={false}> 343 <StatusEventTitle className="inline-flex gap-1"> 344 {report.title} 345 </StatusEventTitle> 346 {report.statusReportsToPageComponents.length > 0 ? ( 347 <StatusEventAffected> 348 {report.statusReportsToPageComponents.map((affected) => ( 349 <StatusEventAffectedBadge key={affected.pageComponent.id}> 350 {affected.pageComponent.name} 351 </StatusEventAffectedBadge> 352 ))} 353 </StatusEventAffected> 354 ) : null} 355 <StatusEventTimelineReport 356 updates={report.statusReportUpdates} 357 reportId={report.id} 358 /> 359 </StatusEventContent> 360 </StatusEvent> 361 </Status> 362 </div> 363 ); 364}