Openstatus www.openstatus.dev

feat: new app layout (#654)

authored by

Maximilian Kaske and committed by
GitHub
1bd55047 cd0c7454

+1718 -848
+6 -10
apps/web/next.config.js
··· 4 4 const nextConfig = { 5 5 reactStrictMode: true, 6 6 swcMinify: true, 7 - transpilePackages: [ 8 - "@openstatus/ui", 9 - "@openstatus/api", 10 - ], 11 - 7 + transpilePackages: ["@openstatus/ui", "@openstatus/api"], 12 8 experimental: { 13 - serverActions: true, 14 9 serverComponentsExternalPackages: [ 15 10 "libsql", 16 11 "@react-email/components", 17 12 "@react-email/render", 18 - '@google-cloud/tasks', 13 + "@google-cloud/tasks", 19 14 // "@libsql/client", 20 15 // "better-sqlite3" 21 16 ], 22 - logging: { 23 - level: "verbose", 17 + optimizePackageImports: ["@tremor/react"], 18 + }, 19 + logging: { 20 + fetches: { 24 21 fullUrl: true, 25 22 }, 26 - optimizePackageImports: ["@tremor/react"], 27 23 }, 28 24 images: { 29 25 remotePatterns: [
+4 -7
apps/web/src/app/app/[workspaceSlug]/(dashboard)/incidents/(overview)/layout.tsx
··· 1 1 import * as React from "react"; 2 2 3 3 import { Header } from "@/components/dashboard/header"; 4 - import { HelpCallout } from "@/components/dashboard/help-callout"; 4 + import AppPageLayout from "@/components/layout/app-page-layout"; 5 5 6 6 export default async function Layout({ 7 7 children, ··· 9 9 children: React.ReactNode; 10 10 }) { 11 11 return ( 12 - <div className="grid min-h-full grid-cols-1 grid-rows-[auto,1fr,auto] gap-6 md:grid-cols-2 md:gap-8"> 12 + <AppPageLayout withHelpCallout> 13 13 <Header 14 14 title="Incidents" 15 15 description="Overview of all your incidents." 16 16 actions={undefined} 17 17 /> 18 - <div className="col-span-full">{children}</div> 19 - <div className="mt-8 md:mt-12"> 20 - <HelpCallout /> 21 - </div> 22 - </div> 18 + {children} 19 + </AppPageLayout> 23 20 ); 24 21 }
+1 -1
apps/web/src/app/app/[workspaceSlug]/(dashboard)/incidents/(overview)/page.tsx
··· 12 12 return ( 13 13 <EmptyState 14 14 icon="activity" 15 - title="No Incidents " 15 + title="No Incidents" 16 16 description="Hopefully you will see this screen for a long time." 17 17 action={undefined} 18 18 />
+6 -1
apps/web/src/app/app/[workspaceSlug]/(dashboard)/incidents/[id]/layout.tsx
··· 1 1 import { notFound } from "next/navigation"; 2 2 3 + import AppPageWithSidebarLayout from "@/components/layout/app-page-with-sidebar-layout"; 3 4 import { api } from "@/trpc/server"; 4 5 5 6 export default async function Layout({ ··· 19 20 return notFound(); 20 21 } 21 22 22 - return <div className="grid grid-cols-1 gap-6 md:gap-8">{children}</div>; 23 + return ( 24 + <AppPageWithSidebarLayout id="incidents"> 25 + {children} 26 + </AppPageWithSidebarLayout> 27 + ); 23 28 }
+5 -18
apps/web/src/app/app/[workspaceSlug]/(dashboard)/layout.tsx
··· 2 2 import { notFound } from "next/navigation"; 3 3 4 4 import { Shell } from "@/components/dashboard/shell"; 5 - import { AppHeader } from "@/components/layout/app-header"; 6 - import { AppMenu } from "@/components/layout/app-menu"; 7 5 import { AppSidebar } from "@/components/layout/app-sidebar"; 6 + import { AppHeader } from "@/components/layout/header/app-header"; 8 7 import { api } from "@/trpc/server"; 9 8 import { WorkspaceClientCookie } from "../worskpace-client-cookie"; 10 9 ··· 24 23 return notFound(); 25 24 26 25 return ( 27 - <div className="container relative mx-auto flex min-h-screen w-full flex-col items-center justify-center gap-6 p-4 lg:p-8"> 26 + <div className="container relative mx-auto flex min-h-screen w-full flex-col items-center justify-center gap-6 p-4"> 28 27 <AppHeader /> 29 - <div className="flex w-full flex-1 gap-6 lg:gap-8"> 30 - {/* FIXME: max-h-[] results into weird behavior when shrinking window height */} 31 - <Shell className="hidden max-h-[calc(100vh-9rem)] max-w-min shrink-0 lg:sticky lg:top-28 lg:block"> 32 - <AppSidebar /> 33 - </Shell> 34 - <main className="z-10 flex w-full flex-1 flex-col items-start justify-center"> 35 - <Shell className="relative flex-1 overflow-x-hidden"> 36 - {/* The `top-4` is represented in Shell with a `py-4` class */} 37 - <nav className="absolute right-4 top-4 block md:right-6 md:top-6 lg:hidden"> 38 - <AppMenu /> 39 - </nav> 40 - {children} 41 - </Shell> 42 - </main> 43 - </div> 28 + <main className="z-10 flex w-full flex-1 flex-col items-start justify-center"> 29 + {children} 30 + </main> 44 31 <WorkspaceClientCookie {...{ workspaceSlug }} /> 45 32 </div> 46 33 );
+4 -7
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/(overview)/layout.tsx
··· 4 4 import { ButtonWithDisableTooltip } from "@openstatus/ui"; 5 5 6 6 import { Header } from "@/components/dashboard/header"; 7 - import { HelpCallout } from "@/components/dashboard/help-callout"; 7 + import AppPageLayout from "@/components/layout/app-page-layout"; 8 8 import { api } from "@/trpc/server"; 9 9 10 10 export default async function Layout({ ··· 15 15 const isLimitReached = await api.monitor.isMonitorLimitReached.query(); 16 16 17 17 return ( 18 - <div className="grid min-h-full grid-cols-1 grid-rows-[auto,1fr,auto] gap-6 md:grid-cols-2 md:gap-8"> 18 + <AppPageLayout withHelpCallout> 19 19 <Header 20 20 title="Monitors" 21 21 description="Overview of all your monitors." ··· 29 29 </ButtonWithDisableTooltip> 30 30 } 31 31 /> 32 - <div className="col-span-full">{children}</div> 33 - <div className="mt-8 md:mt-12"> 34 - <HelpCallout /> 35 - </div> 36 - </div> 32 + {children} 33 + </AppPageLayout> 37 34 ); 38 35 }
+1 -1
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/(overview)/page.tsx
··· 30 30 return ( 31 31 <> 32 32 <DataTable columns={columns} data={monitors} /> 33 - <div className="mt-3">{isLimitReached ? <Limit /> : null}</div> 33 + {isLimitReached ? <Limit /> : null} 34 34 </> 35 35 ); 36 36 }
-2
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/_components/metrics.tsx
··· 35 35 const failures = current.ok === 0 ? 0 : current.count - current.ok; 36 36 const lastFailures = last.count - last.ok; 37 37 38 - console.log(metrics); 39 - 40 38 const distance = current.lastTimestamp 41 39 ? formatDistanceToNowStrict(new Date(current.lastTimestamp)) 42 40 : undefined;
+3 -22
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/layout.tsx
··· 1 1 import { notFound } from "next/navigation"; 2 2 3 3 import { Header } from "@/components/dashboard/header"; 4 - import { Navbar } from "@/components/dashboard/navbar"; 4 + import AppPageWithSidebarLayout from "@/components/layout/app-page-with-sidebar-layout"; 5 5 import { api } from "@/trpc/server"; 6 6 7 7 export default async function Layout({ ··· 21 21 return notFound(); 22 22 } 23 23 24 - const navigation = [ 25 - { 26 - label: "Overview", 27 - href: `/app/${params.workspaceSlug}/monitors/${id}/overview`, 28 - segment: "overview", 29 - }, 30 - { 31 - label: "Requests Log", 32 - href: `/app/${params.workspaceSlug}/monitors/${id}/data`, 33 - segment: "data", 34 - }, 35 - { 36 - label: "Settings", 37 - href: `/app/${params.workspaceSlug}/monitors/${id}/edit`, 38 - segment: "edit", 39 - }, 40 - ]; 41 - 42 24 return ( 43 - <div className="grid grid-cols-1 gap-6 md:gap-8"> 25 + <AppPageWithSidebarLayout id="monitors"> 44 26 <Header title={monitor.name} description={monitor.url} /> 45 - <Navbar className="col-span-full" navigation={navigation} /> 46 27 {children} 47 - </div> 28 + </AppPageWithSidebarLayout> 48 29 ); 49 30 }
+4 -3
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/new/layout.tsx
··· 1 1 import { Header } from "@/components/dashboard/header"; 2 + import AppPageLayout from "@/components/layout/app-page-layout"; 2 3 3 4 export default async function Layout({ 4 5 children, ··· 6 7 children: React.ReactNode; 7 8 }) { 8 9 return ( 9 - <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 10 + <AppPageLayout> 10 11 <Header title="Monitor" description="Create your monitor" /> 11 - <div className="col-span-full">{children}</div> 12 - </div> 12 + {children} 13 + </AppPageLayout> 13 14 ); 14 15 }
+34
apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/(overview)/layout.tsx
··· 1 + import Link from "next/link"; 2 + 3 + import { ButtonWithDisableTooltip } from "@openstatus/ui"; 4 + 5 + import { Header } from "@/components/dashboard/header"; 6 + import AppPageLayout from "@/components/layout/app-page-layout"; 7 + import { api } from "@/trpc/server"; 8 + 9 + export default async function Layout({ 10 + children, 11 + }: { 12 + children: React.ReactNode; 13 + }) { 14 + const isLimitReached = 15 + await api.notification.isNotificationLimitReached.query(); 16 + return ( 17 + <AppPageLayout withHelpCallout> 18 + <Header 19 + title="Notifications" 20 + description="Overview of all your notification channels." 21 + actions={ 22 + <ButtonWithDisableTooltip 23 + tooltip="You reached the limits" 24 + asChild={!isLimitReached} 25 + disabled={isLimitReached} 26 + > 27 + <Link href="./notifications/new">Create</Link> 28 + </ButtonWithDisableTooltip> 29 + } 30 + /> 31 + {children} 32 + </AppPageLayout> 33 + ); 34 + }
+5
apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/(overview)/loading.tsx
··· 1 + import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; 2 + 3 + export default function Loading() { 4 + return <DataTableSkeleton />; 5 + }
+39
apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/(overview)/page.tsx
··· 1 + import * as React from "react"; 2 + import Link from "next/link"; 3 + 4 + import { Button } from "@openstatus/ui"; 5 + 6 + import { EmptyState } from "@/components/dashboard/empty-state"; 7 + import { Limit } from "@/components/dashboard/limit"; 8 + import { columns } from "@/components/data-table/notification/columns"; 9 + import { DataTable } from "@/components/data-table/notification/data-table"; 10 + import { api } from "@/trpc/server"; 11 + 12 + export default async function NotificationPage() { 13 + const notifications = 14 + await api.notification.getNotificationsByWorkspace.query(); 15 + const isLimitReached = 16 + await api.notification.isNotificationLimitReached.query(); 17 + 18 + if (notifications.length === 0) { 19 + return ( 20 + <EmptyState 21 + icon="bell" 22 + title="No notifications" 23 + description="Create your first notification channel" 24 + action={ 25 + <Button asChild> 26 + <Link href="./notifications/new">Create</Link> 27 + </Button> 28 + } 29 + /> 30 + ); 31 + } 32 + 33 + return ( 34 + <> 35 + <DataTable columns={columns} data={notifications} /> 36 + {isLimitReached ? <Limit /> : null} 37 + </> 38 + ); 39 + }
+21
apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/[id]/edit/page.tsx
··· 1 + import { NotificationForm } from "@/components/forms/notification-form"; 2 + import { api } from "@/trpc/server"; 3 + 4 + export default async function EditPage({ 5 + params, 6 + }: { 7 + params: { workspaceSlug: string; id: string }; 8 + }) { 9 + const workspace = await api.workspace.getWorkspace.query(); 10 + 11 + const notification = await api.notification.getNotificationById.query({ 12 + id: Number(params.id), 13 + }); 14 + 15 + return ( 16 + <NotificationForm 17 + defaultValues={notification} 18 + workspacePlan={workspace.plan} 19 + /> 20 + ); 21 + }
+30
apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/[id]/layout.tsx
··· 1 + import { notFound } from "next/navigation"; 2 + 3 + import { Header } from "@/components/dashboard/header"; 4 + import AppPageWithSidebarLayout from "@/components/layout/app-page-with-sidebar-layout"; 5 + import { api } from "@/trpc/server"; 6 + 7 + export default async function Layout({ 8 + children, 9 + params, 10 + }: { 11 + children: React.ReactNode; 12 + params: { workspaceSlug: string; id: string }; 13 + }) { 14 + const id = params.id; 15 + 16 + const notification = await api.notification.getNotificationById.query({ 17 + id: Number(id), 18 + }); 19 + 20 + if (!notification) { 21 + return notFound(); 22 + } 23 + 24 + return ( 25 + <AppPageWithSidebarLayout id="notifications"> 26 + <Header title={notification.name} description={notification.provider} /> 27 + {children} 28 + </AppPageWithSidebarLayout> 29 + ); 30 + }
+9
apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/[id]/page.tsx
··· 1 + import { redirect } from "next/navigation"; 2 + 3 + export default function Page({ 4 + params, 5 + }: { 6 + params: { workspaceSlug: string; id: string }; 7 + }) { 8 + return redirect(`./${params.id}/edit`); 9 + }
-20
apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/_components/empty-state.tsx
··· 1 - import Link from "next/link"; 2 - 3 - import { Button } from "@openstatus/ui"; 4 - 5 - import { EmptyState as DefaultEmptyState } from "@/components/dashboard/empty-state"; 6 - 7 - export function EmptyState() { 8 - return ( 9 - <DefaultEmptyState 10 - icon="bell" 11 - title="No notifications" 12 - description="Create your first notification channel" 13 - action={ 14 - <Button asChild> 15 - <Link href="./notifications/edit">Create</Link> 16 - </Button> 17 - } 18 - /> 19 - ); 20 - }
-19
apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/edit/loading.tsx
··· 1 - import { Skeleton } from "@openstatus/ui"; 2 - 3 - import { Header } from "@/components/dashboard/header"; 4 - import { SkeletonForm } from "@/components/forms/skeleton-form"; 5 - 6 - export default function Loading() { 7 - return ( 8 - <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 9 - <div className="col-span-full flex w-full justify-between"> 10 - <Header.Skeleton> 11 - <Skeleton className="h-9 w-20" /> 12 - </Header.Skeleton> 13 - </div> 14 - <div className="col-span-full"> 15 - <SkeletonForm /> 16 - </div> 17 - </div> 18 - ); 19 - }
-52
apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/edit/page.tsx
··· 1 - import { notFound } from "next/navigation"; 2 - import * as z from "zod"; 3 - 4 - import { Header } from "@/components/dashboard/header"; 5 - import { NotificationForm } from "@/components/forms/notification-form"; 6 - import { api } from "@/trpc/server"; 7 - 8 - /** 9 - * allowed URL search params 10 - */ 11 - const searchParamsSchema = z.object({ 12 - id: z.coerce.number().optional(), 13 - }); 14 - 15 - export default async function EditPage({ 16 - searchParams, 17 - }: { 18 - params: { workspaceSlug: string }; 19 - searchParams: { [key: string]: string | string[] | undefined }; 20 - }) { 21 - const search = searchParamsSchema.safeParse(searchParams); 22 - 23 - if (!search.success) { 24 - return notFound(); 25 - } 26 - const workspace = await api.workspace.getWorkspace.query(); 27 - 28 - const { id } = search.data; 29 - 30 - const notification = id 31 - ? await api.notification.getNotificationById.query({ id }) 32 - : undefined; 33 - 34 - return ( 35 - <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 36 - <Header 37 - title="Notification" 38 - description={ 39 - notification 40 - ? "Update your notification channel" 41 - : "Create your notification channel" 42 - } 43 - /> 44 - <div className="col-span-full"> 45 - <NotificationForm 46 - defaultValues={notification} 47 - workspacePlan={workspace.plan} 48 - /> 49 - </div> 50 - </div> 51 - ); 52 - }
-19
apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/loading.tsx
··· 1 - import { Skeleton } from "@openstatus/ui"; 2 - 3 - import { Header } from "@/components/dashboard/header"; 4 - import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; 5 - 6 - export default function Loading() { 7 - return ( 8 - <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 9 - <div className="col-span-full flex w-full justify-between"> 10 - <Header.Skeleton> 11 - <Skeleton className="h-9 w-20" /> 12 - </Header.Skeleton> 13 - </div> 14 - <div className="col-span-full w-full"> 15 - <DataTableSkeleton /> 16 - </div> 17 - </div> 18 - ); 19 - }
+15
apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/new/layout.tsx
··· 1 + import { Header } from "@/components/dashboard/header"; 2 + import AppPageLayout from "@/components/layout/app-page-layout"; 3 + 4 + export default async function Layout({ 5 + children, 6 + }: { 7 + children: React.ReactNode; 8 + }) { 9 + return ( 10 + <AppPageLayout> 11 + <Header title="Notifications" description="Create your notification" /> 12 + {children} 13 + </AppPageLayout> 14 + ); 15 + }
+5
apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/new/loading.tsx
··· 1 + import { SkeletonForm } from "@/components/forms/skeleton-form"; 2 + 3 + export default function Loading() { 4 + return <SkeletonForm />; 5 + }
+13
apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/new/page.tsx
··· 1 + import { notFound } from "next/navigation"; 2 + import * as z from "zod"; 3 + 4 + import { NotificationForm } from "@/components/forms/notification-form"; 5 + import { api } from "@/trpc/server"; 6 + 7 + export default async function EditPage({}: { 8 + params: { workspaceSlug: string }; 9 + }) { 10 + const workspace = await api.workspace.getWorkspace.query(); 11 + 12 + return <NotificationForm workspacePlan={workspace.plan} nextUrl="./" />; 13 + }
-48
apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/page.tsx
··· 1 - import * as React from "react"; 2 - import Link from "next/link"; 3 - 4 - import { ButtonWithDisableTooltip } from "@openstatus/ui"; 5 - 6 - import { Header } from "@/components/dashboard/header"; 7 - import { HelpCallout } from "@/components/dashboard/help-callout"; 8 - import { columns } from "@/components/data-table/notification/columns"; 9 - import { DataTable } from "@/components/data-table/notification/data-table"; 10 - import { api } from "@/trpc/server"; 11 - import { EmptyState } from "./_components/empty-state"; 12 - 13 - export default async function NotificationPage() { 14 - const notifications = 15 - await api.notification.getNotificationsByWorkspace.query(); 16 - const isLimitReached = 17 - await api.notification.isNotificationLimitReached.query(); 18 - 19 - return ( 20 - <div className="grid min-h-full grid-cols-1 grid-rows-[auto,1fr,auto] gap-6 md:grid-cols-2 md:gap-8"> 21 - <Header 22 - title="Notifications" 23 - description="Overview of all your notification channels." 24 - actions={ 25 - <ButtonWithDisableTooltip 26 - tooltip="You reached the limits" 27 - asChild={!isLimitReached} 28 - disabled={isLimitReached} 29 - > 30 - <Link href="./notifications/edit">Create</Link> 31 - </ButtonWithDisableTooltip> 32 - } 33 - /> 34 - {notifications && notifications.length > 0 ? ( 35 - <div className="col-span-full"> 36 - <DataTable columns={columns} data={notifications} /> 37 - </div> 38 - ) : ( 39 - <div className="col-span-full"> 40 - <EmptyState /> 41 - </div> 42 - )} 43 - <div className="mt-8 md:mt-12"> 44 - <HelpCallout /> 45 - </div> 46 - </div> 47 - ); 48 - }
+40 -26
apps/web/src/app/app/[workspaceSlug]/(dashboard)/settings/api-token/_components/card.tsx
··· 23 23 const key = data.result.keys?.[0] || undefined; 24 24 25 25 return ( 26 - <Container 27 - title="API Token" 28 - description="Use our API endpoints to create your monitors programmatically." 29 - actions={ 30 - <> 31 - {key ? ( 32 - <RevokeButton keyId={key.id} /> 33 - ) : ( 34 - <CreateForm ownerId={ownerId} /> 35 - )} 36 - </> 37 - } 38 - > 39 - {key ? ( 40 - <dl className="[&_dt]:text-muted-foreground grid gap-2 [&>*]:text-sm [&_dt]:font-light"> 41 - <div className="flex min-w-0 items-center justify-between gap-3"> 42 - <dt>Token</dt> 43 - <dd className="font-mono">{key.start}...</dd> 44 - </div> 45 - <div className="flex min-w-0 items-center justify-between gap-3"> 46 - <dt>Created At</dt> 47 - <dd>{formatDate(new Date(key.createdAt!))}</dd> 48 - </div> 49 - </dl> 50 - ) : null} 51 - </Container> 26 + <> 27 + <Container 28 + title="API Token" 29 + description="Use our API endpoints to create your monitors programmatically." 30 + actions={ 31 + <> 32 + {key ? ( 33 + <RevokeButton keyId={key.id} /> 34 + ) : ( 35 + <CreateForm ownerId={ownerId} /> 36 + )} 37 + </> 38 + } 39 + > 40 + {key ? ( 41 + <dl className="[&_dt]:text-muted-foreground grid gap-2 [&>*]:text-sm [&_dt]:font-light"> 42 + <div className="flex min-w-0 items-center justify-between gap-3"> 43 + <dt>Token</dt> 44 + <dd className="font-mono">{key.start}...</dd> 45 + </div> 46 + <div className="flex min-w-0 items-center justify-between gap-3"> 47 + <dt>Created At</dt> 48 + <dd>{formatDate(new Date(key.createdAt!))}</dd> 49 + </div> 50 + </dl> 51 + ) : null} 52 + </Container> 53 + <p className="text-foreground text-sm"> 54 + Read more about APIs in our{" "} 55 + <a 56 + className="text-foreground underline underline-offset-4 hover:no-underline" 57 + href="https://docs.openstatus.dev/api-reference/auth" 58 + target="_blank" 59 + rel="noreferrer" 60 + > 61 + docs 62 + </a> 63 + . 64 + </p> 65 + </> 52 66 ); 53 67 }
+13
apps/web/src/app/app/[workspaceSlug]/(dashboard)/settings/api-token/layout.tsx
··· 1 + import { Header } from "@/components/dashboard/header"; 2 + import { getPageBySegment } from "@/config/pages"; 3 + 4 + const page = getPageBySegment(["settings", "api-token"]); 5 + 6 + export default function Layout({ children }: { children: React.ReactNode }) { 7 + return ( 8 + <> 9 + <Header title={page?.title} description={page?.description} /> 10 + {children} 11 + </> 12 + ); 13 + }
+7
apps/web/src/app/app/[workspaceSlug]/(dashboard)/settings/api-token/loading.tsx
··· 1 + import { Skeleton } from "@openstatus/ui"; 2 + 3 + // TODO: can be improved... 4 + 5 + export default function Loading() { 6 + return <Skeleton className="h-72 w-full" />; 7 + }
+13
apps/web/src/app/app/[workspaceSlug]/(dashboard)/settings/appearance/layout.tsx
··· 1 + import { Header } from "@/components/dashboard/header"; 2 + import { getPageBySegment } from "@/config/pages"; 3 + 4 + const page = getPageBySegment(["settings", "appearance"]); 5 + 6 + export default function Layout({ children }: { children: React.ReactNode }) { 7 + return ( 8 + <> 9 + <Header title={page?.title} description={page?.description} /> 10 + {children} 11 + </> 12 + ); 13 + }
+7
apps/web/src/app/app/[workspaceSlug]/(dashboard)/settings/appearance/loading.tsx
··· 1 + import { Skeleton } from "@openstatus/ui"; 2 + 3 + // TODO: can be improved... 4 + 5 + export default function Loading() { 6 + return <Skeleton className="h-72 w-full" />; 7 + }
+13
apps/web/src/app/app/[workspaceSlug]/(dashboard)/settings/billing/layout.tsx
··· 1 + import { Header } from "@/components/dashboard/header"; 2 + import { getPageBySegment } from "@/config/pages"; 3 + 4 + const page = getPageBySegment(["settings", "billing"]); 5 + 6 + export default function Layout({ children }: { children: React.ReactNode }) { 7 + return ( 8 + <> 9 + <Header title={page?.title} description={page?.description} /> 10 + {children} 11 + </> 12 + ); 13 + }
+7
apps/web/src/app/app/[workspaceSlug]/(dashboard)/settings/billing/loading.tsx
··· 1 + import { Skeleton } from "@openstatus/ui"; 2 + 3 + // TODO: can be improved... 4 + 5 + export default function Loading() { 6 + return <Skeleton className="h-72 w-full" />; 7 + }
+13
apps/web/src/app/app/[workspaceSlug]/(dashboard)/settings/general/layout.tsx
··· 1 + import { Header } from "@/components/dashboard/header"; 2 + import { getPageBySegment } from "@/config/pages"; 3 + 4 + const page = getPageBySegment(["settings", "general"]); 5 + 6 + export default function Layout({ children }: { children: React.ReactNode }) { 7 + return ( 8 + <> 9 + <Header title={page?.title} description={page?.description} /> 10 + {children} 11 + </> 12 + ); 13 + }
+7
apps/web/src/app/app/[workspaceSlug]/(dashboard)/settings/general/loading.tsx
··· 1 + import { Skeleton } from "@openstatus/ui"; 2 + 3 + // TODO: can be improved... 4 + 5 + export default function Loading() { 6 + return <Skeleton className="h-72 w-full" />; 7 + }
+4 -40
apps/web/src/app/app/[workspaceSlug]/(dashboard)/settings/layout.tsx
··· 1 - import { Header } from "@/components/dashboard/header"; 2 - import { Navbar } from "@/components/dashboard/navbar"; 1 + import AppPageWithSidebarLayout from "@/components/layout/app-page-with-sidebar-layout"; 3 2 4 3 export default function SettingsLayout({ 5 - params, 6 4 children, 7 5 }: { 8 - params: { workspaceSlug: string }; 9 6 children: React.ReactNode; 10 7 }) { 11 - const navigation = [ 12 - { 13 - label: "General", 14 - href: `/app/${params.workspaceSlug}/settings/general`, 15 - segment: "general", 16 - }, 17 - { 18 - label: "Team", 19 - href: `/app/${params.workspaceSlug}/settings/team`, 20 - segment: "team", 21 - }, 22 - { 23 - label: "API Token", 24 - href: `/app/${params.workspaceSlug}/settings/api-token`, 25 - segment: "api-token", 26 - }, 27 - { 28 - label: "Billing", 29 - href: `/app/${params.workspaceSlug}/settings/billing`, 30 - segment: "billing", 31 - }, 32 - { 33 - label: "Appearance", 34 - href: `/app/${params.workspaceSlug}/settings/appearance`, 35 - segment: "appearance", 36 - }, 37 - ]; 38 - 39 8 return ( 40 - <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 41 - <Header 42 - title="Settings" 43 - description="Your OpenStatus workspace settings." 44 - /> 45 - <Navbar className="col-span-full" {...{ navigation }} /> 46 - <div className="col-span-full">{children}</div> 47 - </div> 9 + <AppPageWithSidebarLayout id="settings"> 10 + {children} 11 + </AppPageWithSidebarLayout> 48 12 ); 49 13 }
-11
apps/web/src/app/app/[workspaceSlug]/(dashboard)/settings/loading.tsx
··· 1 - import { Skeleton } from "@openstatus/ui"; 2 - 3 - // TODO: can be improved... 4 - 5 - export default function Loading() { 6 - return ( 7 - <div className="grid gap-6 md:grid-cols-1 md:gap-8"> 8 - <Skeleton className="h-72 w-full" /> 9 - </div> 10 - ); 11 - }
+13
apps/web/src/app/app/[workspaceSlug]/(dashboard)/settings/team/layout.tsx
··· 1 + import { Header } from "@/components/dashboard/header"; 2 + import { getPageBySegment } from "@/config/pages"; 3 + 4 + const page = getPageBySegment(["settings", "team"]); 5 + 6 + export default function Layout({ children }: { children: React.ReactNode }) { 7 + return ( 8 + <> 9 + <Header title={page?.title} description={page?.description} /> 10 + {children} 11 + </> 12 + ); 13 + }
+7
apps/web/src/app/app/[workspaceSlug]/(dashboard)/settings/team/loading.tsx
··· 1 + import { Skeleton } from "@openstatus/ui"; 2 + 3 + // TODO: can be improved... 4 + 5 + export default function Loading() { 6 + return <Skeleton className="h-72 w-full" />; 7 + }
+4 -7
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-pages/(overview)/layout.tsx
··· 4 4 import { ButtonWithDisableTooltip } from "@openstatus/ui"; 5 5 6 6 import { Header } from "@/components/dashboard/header"; 7 - import { HelpCallout } from "@/components/dashboard/help-callout"; 7 + import AppPageLayout from "@/components/layout/app-page-layout"; 8 8 import { api } from "@/trpc/server"; 9 9 10 10 export default async function Layout({ ··· 15 15 const isLimitReached = await api.page.isPageLimitReached.query(); 16 16 17 17 return ( 18 - <div className="grid min-h-full grid-cols-1 grid-rows-[auto,1fr,auto] gap-6 md:grid-cols-2 md:gap-8"> 18 + <AppPageLayout withHelpCallout> 19 19 <Header 20 20 title="Pages" 21 21 description="Overview of all your pages." ··· 29 29 </ButtonWithDisableTooltip> 30 30 } 31 31 /> 32 - <div className="col-span-full">{children}</div> 33 - <div className="mt-8 md:mt-12"> 34 - <HelpCallout /> 35 - </div> 36 - </div> 32 + {children} 33 + </AppPageLayout> 37 34 ); 38 35 }
+1 -1
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-pages/(overview)/page.tsx
··· 30 30 return ( 31 31 <> 32 32 <DataTable columns={columns} data={pages} /> 33 - <div className="mt-3">{isLimitReached ? <Limit /> : null}</div> 33 + {isLimitReached ? <Limit /> : null} 34 34 </> 35 35 ); 36 36 }
+3 -22
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-pages/[id]/layout.tsx
··· 4 4 import { Button } from "@openstatus/ui"; 5 5 6 6 import { Header } from "@/components/dashboard/header"; 7 - import { Navbar } from "@/components/dashboard/navbar"; 7 + import AppPageWithSidebarLayout from "@/components/layout/app-page-with-sidebar-layout"; 8 8 import { api } from "@/trpc/server"; 9 9 10 10 export default async function Layout({ ··· 24 24 return notFound(); 25 25 } 26 26 27 - const navigation = [ 28 - { 29 - label: "Settings", 30 - href: `/app/${params.workspaceSlug}/status-pages/${id}/edit`, 31 - segment: "edit", 32 - }, 33 - { 34 - label: "Domain", 35 - href: `/app/${params.workspaceSlug}/status-pages/${id}/domain`, 36 - segment: "domain", 37 - }, 38 - { 39 - label: "Subscribers", 40 - href: `/app/${params.workspaceSlug}/status-pages/${id}/subscribers`, 41 - segment: "subscribers", 42 - }, 43 - ]; 44 - 45 27 return ( 46 - <div className="grid grid-cols-1 gap-6 md:gap-8"> 28 + <AppPageWithSidebarLayout id="status-pages"> 47 29 <Header 48 30 title={page.title} 49 31 description={page.description} ··· 55 37 </Button> 56 38 } 57 39 /> 58 - <Navbar className="col-span-full" navigation={navigation} /> 59 40 {children} 60 - </div> 41 + </AppPageWithSidebarLayout> 61 42 ); 62 43 }
+4 -3
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-pages/new/layout.tsx
··· 1 1 import { Header } from "@/components/dashboard/header"; 2 + import AppPageLayout from "@/components/layout/app-page-layout"; 2 3 3 4 export default async function Layout({ 4 5 children, ··· 6 7 children: React.ReactNode; 7 8 }) { 8 9 return ( 9 - <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 10 + <AppPageLayout> 10 11 <Header title="Pages" description="Create your page." /> 11 - <div className="col-span-full">{children}</div> 12 - </div> 12 + {children} 13 + </AppPageLayout> 13 14 ); 14 15 }
+4 -7
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-reports/(overview)/layout.tsx
··· 4 4 import { Button } from "@openstatus/ui"; 5 5 6 6 import { Header } from "@/components/dashboard/header"; 7 - import { HelpCallout } from "@/components/dashboard/help-callout"; 7 + import AppPageLayout from "@/components/layout/app-page-layout"; 8 8 9 9 export default async function Layout({ 10 10 children, ··· 12 12 children: React.ReactNode; 13 13 }) { 14 14 return ( 15 - <div className="grid min-h-full grid-cols-1 grid-rows-[auto,1fr,auto] gap-6 md:grid-cols-2 md:gap-8"> 15 + <AppPageLayout withHelpCallout> 16 16 <Header 17 17 title="Status Reports" 18 18 description="Overview of all your status reports and updates." ··· 22 22 </Button> 23 23 } 24 24 /> 25 - <div className="col-span-full">{children}</div> 26 - <div className="mt-8 md:mt-12"> 27 - <HelpCallout /> 28 - </div> 29 - </div> 25 + {children} 26 + </AppPageLayout> 30 27 ); 31 28 }
+34 -12
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-reports/[id]/_components/status-update-button.tsx
··· 1 1 "use client"; 2 2 3 - import Link from "next/link"; 4 - import { usePathname } from "next/navigation"; 3 + import { useState } from "react"; 5 4 6 - import { Button } from "@openstatus/ui"; 5 + import { 6 + Button, 7 + Dialog, 8 + DialogContent, 9 + DialogDescription, 10 + DialogHeader, 11 + DialogTitle, 12 + DialogTrigger, 13 + } from "@openstatus/ui"; 7 14 8 - export function StatusUpdateButton() { 9 - const pathname = usePathname(); 15 + import { StatusReportUpdateForm } from "@/components/forms/status-report-update/form"; 10 16 11 - if (pathname.endsWith("/update/edit")) { 12 - return <Button disabled>Status Update</Button>; 13 - } 14 - 17 + export function StatusUpdateButton({ 18 + statusReportId, 19 + }: { 20 + statusReportId: number; 21 + }) { 22 + const [open, setOpen] = useState(false); 15 23 return ( 16 - <Button asChild> 17 - <Link href="./update/edit">Status Update</Link> 18 - </Button> 24 + <Dialog open={open} onOpenChange={setOpen}> 25 + <DialogTrigger asChild> 26 + <Button>Status Update</Button> 27 + </DialogTrigger> 28 + <DialogContent className="max-h-screen overflow-y-scroll sm:max-w-[650px]"> 29 + <DialogHeader> 30 + <DialogTitle>New Status Report</DialogTitle> 31 + <DialogDescription> 32 + Provide a status update and add it to the report history. 33 + </DialogDescription> 34 + </DialogHeader> 35 + <StatusReportUpdateForm 36 + statusReportId={statusReportId} 37 + onSubmit={() => setOpen(false)} 38 + /> 39 + </DialogContent> 40 + </Dialog> 19 41 ); 20 42 }
+7 -18
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-reports/[id]/layout.tsx
··· 1 1 import { notFound } from "next/navigation"; 2 2 3 3 import { Header } from "@/components/dashboard/header"; 4 - import { Navbar } from "@/components/dashboard/navbar"; 4 + import AppPageWithSidebarLayout from "@/components/layout/app-page-with-sidebar-layout"; 5 5 import { api } from "@/trpc/server"; 6 6 import { StatusUpdateButton } from "./_components/status-update-button"; 7 7 ··· 22 22 return notFound(); 23 23 } 24 24 25 - const navigation = [ 26 - { 27 - label: "Overview", 28 - href: `/app/${params.workspaceSlug}/status-reports/${id}/overview`, 29 - segment: "overview", 30 - }, 31 - { 32 - label: "Settings", 33 - href: `/app/${params.workspaceSlug}/status-reports/${id}/edit`, 34 - segment: "edit", 35 - }, 36 - ]; 37 - 38 25 return ( 39 - <div className="grid grid-cols-1 gap-6 md:gap-8"> 40 - <Header title={statusReport.title} actions={<StatusUpdateButton />} /> 41 - <Navbar className="col-span-full" navigation={navigation} /> 26 + <AppPageWithSidebarLayout id="status-reports"> 27 + <Header 28 + title={statusReport.title} 29 + actions={<StatusUpdateButton statusReportId={Number(id)} />} 30 + /> 42 31 {children} 43 - </div> 32 + </AppPageWithSidebarLayout> 44 33 ); 45 34 }
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-reports/[id]/update/edit/loading.tsx apps/web/src/app/app/[workspaceSlug]/(dashboard)/notifications/[id]/edit/loading.tsx
-42
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-reports/[id]/update/edit/page.tsx
··· 1 - import { notFound } from "next/navigation"; 2 - import * as z from "zod"; 3 - 4 - import { StatusReportUpdateForm } from "@/components/forms/status-report-update-form"; 5 - import { api } from "@/trpc/server"; 6 - 7 - /** 8 - * allowed URL search params 9 - */ 10 - const searchParamsSchema = z.object({ 11 - statusUpdate: z.coerce.number().optional(), // TODO: call it id as we do it everywhere else 12 - }); 13 - 14 - export default async function EditPage({ 15 - params, 16 - searchParams, 17 - }: { 18 - params: { workspaceSlug: string; id: string }; 19 - searchParams: { [key: string]: string | string[] | undefined }; 20 - }) { 21 - const search = searchParamsSchema.safeParse(searchParams); 22 - 23 - if (!search.success) { 24 - return notFound(); 25 - } 26 - 27 - const { statusUpdate } = search.data; 28 - 29 - const data = statusUpdate 30 - ? await api.statusReport.getStatusReportUpdateById.query({ 31 - id: statusUpdate, 32 - }) 33 - : undefined; 34 - 35 - return ( 36 - <StatusReportUpdateForm 37 - statusReportId={parseInt(params.id)} 38 - defaultValues={data || undefined} 39 - nextUrl={"../overview"} 40 - /> 41 - ); 42 - }
+1 -3
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-reports/_components/delete-status-update.tsx apps/web/src/components/status-update/delete-status-update.tsx
··· 1 - // TODO: move to components folder as not being used in any page.tsx or layout.tsx 2 - 3 1 "use client"; 4 2 5 3 import * as React from "react"; ··· 48 46 <Button 49 47 size="icon" 50 48 variant="outline" 51 - className="border-destructive/50 text-destructive/80 hover:text-destructive hover:bg-destructive/10 h-7 w-7 p-0" 49 + className="border-destructive/50 text-destructive/80 hover:text-destructive hover:bg-destructive/10" 52 50 > 53 51 <Icons.trash className="h-4 w-4" /> 54 52 </Button>
+24 -25
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-reports/edit/page.tsx
··· 3 3 4 4 import { Header } from "@/components/dashboard/header"; 5 5 import { StatusReportForm } from "@/components/forms/status-report-form"; 6 + import AppPageLayout from "@/components/layout/app-page-layout"; 6 7 import { api } from "@/trpc/server"; 7 8 8 9 /** ··· 38 39 const pages = await api.page.getPagesByWorkspace.query(); 39 40 40 41 return ( 41 - <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 42 + <AppPageLayout> 42 43 <Header 43 44 title="Status Report" 44 45 description="Create a public report for your incident" 45 46 /> 46 - <div className="col-span-full"> 47 - <StatusReportForm 48 - monitors={monitors} 49 - pages={pages} 50 - defaultValues={ 51 - statusUpdate 52 - ? // TODO: we should move the mapping to the trpc layer 53 - // so we don't have to do this in the UI 54 - // it should be something like defaultValues={statusReport} 55 - { 56 - ...statusUpdate, 57 - monitors: statusUpdate?.monitorsToStatusReports.map( 58 - ({ monitorId }) => monitorId, 59 - ), 60 - pages: statusUpdate?.pagesToStatusReports.map( 61 - ({ pageId }) => pageId, 62 - ), 63 - message: "", 64 - } 65 - : undefined 66 - } 67 - /> 68 - </div> 69 - </div> 47 + <StatusReportForm 48 + monitors={monitors} 49 + pages={pages} 50 + defaultValues={ 51 + statusUpdate 52 + ? // TODO: we should move the mapping to the trpc layer 53 + // so we don't have to do this in the UI 54 + // it should be something like defaultValues={statusReport} 55 + { 56 + ...statusUpdate, 57 + monitors: statusUpdate?.monitorsToStatusReports.map( 58 + ({ monitorId }) => monitorId, 59 + ), 60 + pages: statusUpdate?.pagesToStatusReports.map( 61 + ({ pageId }) => pageId, 62 + ), 63 + message: "", 64 + } 65 + : undefined 66 + } 67 + /> 68 + </AppPageLayout> 70 69 ); 71 70 }
+4 -3
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-reports/new/layout.tsx
··· 1 1 import { Header } from "@/components/dashboard/header"; 2 + import AppPageLayout from "@/components/layout/app-page-layout"; 2 3 3 4 export default async function Layout({ 4 5 children, ··· 6 7 children: React.ReactNode; 7 8 }) { 8 9 return ( 9 - <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 10 + <AppPageLayout> 10 11 <Header 11 12 title="Status Report" 12 13 description="Create a public report for your incident." 13 14 /> 14 - <div className="col-span-full">{children}</div> 15 - </div> 15 + {children} 16 + </AppPageLayout> 16 17 ); 17 18 }
+2 -2
apps/web/src/app/app/[workspaceSlug]/onboarding/layout.tsx
··· 1 1 import * as React from "react"; 2 2 3 3 import { Shell } from "@/components/dashboard/shell"; 4 - import { AppHeader } from "@/components/layout/app-header"; 4 + import { AppHeader } from "@/components/layout/header/app-header"; 5 5 import { WorkspaceClientCookie } from "../worskpace-client-cookie"; 6 6 7 7 // TODO: make the container min-h-screen and the footer below! ··· 14 14 }) { 15 15 const { workspaceSlug } = params; 16 16 return ( 17 - <div className="container relative mx-auto flex min-h-screen w-full flex-col items-center justify-center gap-6 p-4 lg:p-8"> 17 + <div className="container relative mx-auto flex min-h-screen w-full flex-col items-center justify-center gap-6 p-4"> 18 18 <AppHeader /> 19 19 <div className="flex w-full flex-1 gap-6 lg:gap-8"> 20 20 <main className="z-10 flex w-full flex-1 flex-col items-start justify-center">
+1 -1
apps/web/src/app/app/invite/layout.tsx
··· 1 1 import * as React from "react"; 2 2 3 3 import { Shell } from "@/components/dashboard/shell"; 4 - import { AppHeader } from "@/components/layout/app-header"; 4 + import { AppHeader } from "@/components/layout/header/app-header"; 5 5 6 6 export default async function AppLayout({ 7 7 children,
+6 -6
apps/web/src/app/app/page.tsx
··· 4 4 import { useRouter } from "next/navigation"; 5 5 6 6 import { Shell } from "@/components/dashboard/shell"; 7 - import { AppHeader } from "@/components/layout/app-header"; 7 + import { AppHeader } from "@/components/layout/header/app-header"; 8 8 import { LoadingAnimation } from "@/components/loading-animation"; 9 9 10 10 // TODO: discuss how to make that page a bit more enjoyable ··· 22 22 <Shell className="relative flex flex-1 flex-col items-center justify-center"> 23 23 <div className="grid gap-4"> 24 24 <div className="text-center"> 25 - <p className="mb-1 text-lg font-bold">Creating Workspace</p> 26 - <p className="text-muted-foreground mb-3"> 25 + <p className="font-cal mb-1 text-3xl">Creating Workspace</p> 26 + <p className="text-muted-foreground mb-5 text-xl"> 27 27 Should be done in a second. 28 28 </p> 29 - <p className="text-muted-foreground text-sm font-light"> 29 + <p className="text-muted-foreground"> 30 30 If you are stuck for longer, please contact us via{" "} 31 31 <a 32 32 href="https://openstatus.dev/discord" 33 33 target="_blank" 34 - className="font-normal" 34 + className="text-foreground underline underline-offset-4 hover:no-underline" 35 35 > 36 36 Discord 37 37 </a>{" "} 38 38 or{" "} 39 39 <a 40 40 href="mailto:thibault@openstatus.dev" 41 - className="font-normal" 41 + className="text-foreground underline underline-offset-4 hover:no-underline" 42 42 > 43 43 Mail 44 44 </a>
+24
apps/web/src/app/global-error.tsx
··· 1 + "use client"; 2 + 3 + import { useEffect } from "react"; 4 + import NextError from "next/error"; 5 + import * as Sentry from "@sentry/nextjs"; 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> 18 + <body> 19 + {/* This is the default Next.js error component but it doesn't allow omitting the statusCode property yet. */} 20 + <NextError statusCode={undefined as any} /> 21 + </body> 22 + </html> 23 + ); 24 + }
+2 -4
apps/web/src/app/play/checker/[id]/page.tsx
··· 17 17 import { MultiRegionTabs } from "./_components/multi-region-tabs"; 18 18 import { RegionInfo } from "./_components/region-info"; 19 19 import { ResponseDetailTabs } from "./_components/response-detail-tabs"; 20 - import { ResponseHeaderTable } from "./_components/response-header-table"; 21 - import { ResponseTimingTable } from "./_components/response-timing-table"; 22 20 import { SelectRegion } from "./_components/select-region"; 23 21 import { getCheckerDataById, timestampFormatter } from "./utils"; 24 22 ··· 51 49 return ( 52 50 <> 53 51 <BackButton href="/play/checker" /> 54 - <Shell className="grid gap-8"> 52 + <Shell className="flex flex-col gap-8"> 55 53 <div className="flex items-center justify-between gap-4"> 56 54 <div className="flex flex-col gap-1"> 57 55 <h1 className="text-3xl font-semibold"> ··· 67 65 </div> 68 66 <MultiRegionTabs regions={data.checks} /> 69 67 <Separator /> 70 - <div className="grid gap-8"> 68 + <div className="flex flex-col gap-8"> 71 69 <div className="grid gap-8 md:grid-cols-2"> 72 70 <div> 73 71 <SelectRegion defaultValue={region} />
+1 -1
apps/web/src/app/play/checker/_components/checker-play.tsx
··· 4 4 5 5 export default async function CheckerPlay() { 6 6 return ( 7 - <Shell className="grid gap-8"> 7 + <Shell className="flex flex-col gap-8"> 8 8 <HeaderPlay 9 9 title="Is your endpoint fast?" 10 10 description="Check the performance of your website, API from the different continents."
+20
apps/web/src/app/pricing/page.tsx
··· 1 + import type { Metadata } from "next"; 1 2 import Link from "next/link"; 2 3 3 4 import { Shell } from "@/components/dashboard/shell"; ··· 5 6 import { FAQs } from "@/components/marketing/faqs"; 6 7 import { EnterpricePlan } from "@/components/marketing/pricing/enterprice-plan"; 7 8 import { PricingWrapper } from "@/components/marketing/pricing/pricing-wrapper"; 9 + import { 10 + defaultMetadata, 11 + ogMetadata, 12 + twitterMetadata, 13 + } from "../shared-metadata"; 14 + 15 + export const metadata: Metadata = { 16 + ...defaultMetadata, 17 + title: "Pricing", 18 + openGraph: { 19 + ...ogMetadata, 20 + title: "Pricing", 21 + url: "https://www.openstatus.dev/pricing", 22 + }, 23 + twitter: { 24 + ...twitterMetadata, 25 + title: "Pricing", 26 + }, 27 + }; 8 28 9 29 export default function PricingPage() { 10 30 return (
+4 -4
apps/web/src/components/dashboard/header.tsx
··· 3 3 import { cn } from "@/lib/utils"; 4 4 5 5 interface HeaderProps extends React.HTMLAttributes<HTMLDivElement> { 6 - title: string; 6 + title?: string; 7 7 description?: string | null; 8 8 actions?: React.ReactNode | React.ReactNode[]; 9 9 } ··· 15 15 return ( 16 16 <div 17 17 className={cn( 18 - "col-span-full mr-12 flex items-start justify-between gap-1 lg:mr-0", 18 + "col-span-full flex items-start justify-between gap-1", 19 19 className, 20 20 )} 21 21 > 22 - <div className="flex flex-col gap-1"> 22 + <div className="flex min-w-0 flex-col gap-1"> 23 23 <h1 className="font-cal text-3xl">{title}</h1> 24 24 {description ? ( 25 25 <p className="text-muted-foreground">{description}</p> ··· 42 42 withDescription?: boolean; 43 43 }) { 44 44 return ( 45 - <div className="col-span-full mr-12 flex w-full justify-between lg:mr-0"> 45 + <div className="col-span-full flex w-full justify-between"> 46 46 <div className="grid w-full gap-3"> 47 47 <Skeleton className="h-8 w-full max-w-[200px]" /> 48 48 {withDescription && <Skeleton className="h-4 w-full max-w-[300px]" />}
+1 -1
apps/web/src/components/dashboard/help-callout.tsx
··· 4 4 5 5 export function HelpCallout() { 6 6 return ( 7 - <Alert className="max-w-xl"> 7 + <Alert className="max-w-md"> 8 8 <HelpCircle className="h-4 w-4" /> 9 9 <AlertTitle className="">Need help?</AlertTitle> 10 10 <AlertDescription className="text-muted-foreground">
+22 -5
apps/web/src/components/dashboard/tabs-link.tsx
··· 9 9 import { cn } from "@/lib/utils"; 10 10 11 11 export interface TabsContainerProps 12 - extends React.HTMLAttributes<HTMLDivElement> {} 12 + extends React.HTMLAttributes<HTMLDivElement> { 13 + hideSeparator?: boolean; 14 + } 13 15 14 - export function TabsContainer({ className, children }: TabsContainerProps) { 16 + export function TabsContainer({ 17 + className, 18 + children, 19 + hideSeparator = false, 20 + }: TabsContainerProps) { 15 21 return ( 16 22 <nav className={cn(className)}> 17 23 <div className="flex w-full items-center overflow-x-auto"> 18 24 <ul className="flex flex-row">{children}</ul> 19 25 </div> 20 - <Separator /> 26 + {/* TODO: move into border-b instead to allow overwrite via className `border-b-0`? */} 27 + {hideSeparator ? null : <Separator />} 21 28 </nav> 22 29 ); 23 30 } 24 31 25 32 export interface TabsLinkProps extends LinkProps { 26 33 children: React.ReactNode; 34 + className?: string; 27 35 active?: boolean; 36 + disabled?: boolean; 28 37 } 29 38 30 - export function TabsLink({ href, children, active }: TabsLinkProps) { 39 + export function TabsLink({ 40 + children, 41 + active, 42 + className, 43 + disabled, 44 + ...props 45 + }: TabsLinkProps) { 31 46 return ( 32 47 <li 33 48 className={cn("flex shrink-0 list-none border-b-2 border-transparent", { 34 49 "border-primary": active, 50 + "pointer-events-none opacity-70": disabled, 35 51 })} 36 52 > 37 53 <Link 38 - href={href} 39 54 className={cn( 40 55 "text-muted-foreground hover:text-primary rounded-md px-4 pb-3 pt-2 text-sm font-medium", 41 56 { 42 57 "text-primary": active, 43 58 }, 59 + className, 44 60 )} 61 + {...props} 45 62 > 46 63 {children} 47 64 </Link>
+1 -2
apps/web/src/components/data-table/notification/data-table-row-actions.tsx
··· 43 43 44 44 async function onDelete() { 45 45 startTransition(async () => { 46 - console.log({ notification }); 47 46 try { 48 47 if (!notification.id) return; 49 48 await api.notification.deleteNotification.mutate({ ··· 71 70 </Button> 72 71 </DropdownMenuTrigger> 73 72 <DropdownMenuContent align="end"> 74 - <Link href={`./notifications/edit?id=${notification.id}`}> 73 + <Link href={`./notifications/${notification.id}/edit`}> 75 74 <DropdownMenuItem>Edit</DropdownMenuItem> 76 75 </Link> 77 76 <AlertDialogTrigger asChild>
+12 -1
apps/web/src/components/forms/notification-form.tsx
··· 101 101 defaultValues?: InsertNotification; 102 102 onSubmit?: () => void; 103 103 workspacePlan: WorkspacePlan; 104 + nextUrl?: string; 104 105 } 105 106 106 107 export function NotificationForm({ 107 108 defaultValues, 108 109 onSubmit: onExternalSubmit, 109 110 workspacePlan, 111 + nextUrl, 110 112 }: Props) { 111 113 const [isPending, startTransition] = useTransition(); 112 114 const [isTestPending, startTestTransition] = useTransition(); ··· 147 149 ...rest, 148 150 }); 149 151 } 152 + if (nextUrl) { 153 + router.push(nextUrl); 154 + } 150 155 router.refresh(); 151 156 toast("saved"); 152 157 } catch { ··· 175 180 <form 176 181 onSubmit={form.handleSubmit(onSubmit)} 177 182 className="grid w-full gap-6" 183 + id="notification-form" // we use a form id to connect the submit button to the form (as we also have the form nested inside of `MonitorForm`) 178 184 > 179 185 <div className="grid gap-4 sm:grid-cols-3"> 180 186 <div className="my-1.5 flex flex-col gap-2"> ··· 300 306 )} 301 307 </Button> 302 308 )} 303 - <Button className="w-full sm:w-auto" size="lg" disabled={isPending}> 309 + <Button 310 + form="notification-form" 311 + className="w-full sm:w-auto" 312 + size="lg" 313 + disabled={isPending} 314 + > 304 315 {!isPending ? "Confirm" : <LoadingAnimation />} 305 316 </Button> 306 317 </div>
-1
apps/web/src/components/forms/status-page-form.tsx
··· 55 55 checkAllMonitors, 56 56 nextUrl, 57 57 }: Props) { 58 - console.log({ defaultValues }); 59 58 const form = useForm<InsertPage>({ 60 59 resolver: zodResolver(insertPageSchema), 61 60 defaultValues: {
+2 -2
apps/web/src/components/forms/status-report-form.tsx
··· 22 22 AccordionTrigger, 23 23 Button, 24 24 Checkbox, 25 - DateTimePicker, 25 + DateTimePickerPopover, 26 26 Form, 27 27 FormControl, 28 28 FormDescription, ··· 376 376 render={({ field }) => ( 377 377 <FormItem className="flex flex-col sm:col-span-full"> 378 378 <FormLabel>Date</FormLabel> 379 - <DateTimePicker 379 + <DateTimePickerPopover 380 380 date={ 381 381 field.value ? new Date(field.value) : new Date() 382 382 }
+5 -7
apps/web/src/components/forms/status-report-update-form.tsx
··· 40 40 interface Props { 41 41 defaultValues?: InsertStatusReportUpdate; 42 42 statusReportId: number; 43 - nextUrl?: string; 43 + onSubmit?: () => void; 44 44 } 45 45 46 46 export function StatusReportUpdateForm({ 47 47 defaultValues, 48 48 statusReportId, 49 - nextUrl, 49 + onSubmit, 50 50 }: Props) { 51 51 const form = useForm<InsertStatusReportUpdate>({ 52 52 resolver: zodResolver(insertStatusReportUpdateSchema), ··· 62 62 const [isPending, startTransition] = React.useTransition(); 63 63 const { toast } = useToastAction(); 64 64 65 - const onSubmit = ({ ...props }: InsertStatusReportUpdate) => { 65 + const handleSubmit = ({ ...props }: InsertStatusReportUpdate) => { 66 66 startTransition(async () => { 67 67 try { 68 68 if (defaultValues) { ··· 71 71 await api.statusReport.createStatusReportUpdate.mutate({ ...props }); 72 72 } 73 73 toast("saved"); 74 - if (nextUrl) { 75 - router.push(nextUrl); 76 - } 74 + onSubmit?.(); 77 75 router.refresh(); 78 76 } catch { 79 77 toast("error"); ··· 86 84 <form 87 85 onSubmit={async (e) => { 88 86 e.preventDefault(); 89 - form.handleSubmit(onSubmit)(e); 87 + form.handleSubmit(handleSubmit)(e); 90 88 }} 91 89 className="grid w-full gap-6" 92 90 >
+95
apps/web/src/components/forms/status-report-update/form.tsx
··· 1 + "use client"; 2 + 3 + import * as React from "react"; 4 + import { useRouter } from "next/navigation"; 5 + import { zodResolver } from "@hookform/resolvers/zod"; 6 + import { useForm } from "react-hook-form"; 7 + 8 + import type { InsertStatusReportUpdate } from "@openstatus/db/src/schema"; 9 + import { insertStatusReportUpdateSchema } from "@openstatus/db/src/schema"; 10 + import { Button, Form } from "@openstatus/ui"; 11 + 12 + import { 13 + Tabs, 14 + TabsContent, 15 + TabsList, 16 + TabsTrigger, 17 + } from "@/components/dashboard/tabs"; 18 + import { LoadingAnimation } from "@/components/loading-animation"; 19 + import { useToastAction } from "@/hooks/use-toast-action"; 20 + import { api } from "@/trpc/client"; 21 + import { General } from "./general"; 22 + import { SectionDate } from "./section-date"; 23 + import { SectionMessage } from "./section-message"; 24 + 25 + interface Props { 26 + defaultValues?: InsertStatusReportUpdate; 27 + statusReportId: number; 28 + onSubmit?: () => void; 29 + } 30 + 31 + export function StatusReportUpdateForm({ 32 + defaultValues, 33 + statusReportId, 34 + onSubmit, 35 + }: Props) { 36 + const form = useForm<InsertStatusReportUpdate>({ 37 + resolver: zodResolver(insertStatusReportUpdateSchema), 38 + defaultValues: { 39 + id: defaultValues?.id || 0, 40 + status: defaultValues?.status || "investigating", 41 + message: defaultValues?.message, 42 + date: defaultValues?.date || new Date(), 43 + statusReportId, 44 + }, 45 + }); 46 + const router = useRouter(); 47 + const [isPending, startTransition] = React.useTransition(); 48 + const { toast } = useToastAction(); 49 + 50 + const handleSubmit = ({ ...props }: InsertStatusReportUpdate) => { 51 + startTransition(async () => { 52 + try { 53 + if (defaultValues) { 54 + await api.statusReport.updateStatusReportUpdate.mutate({ ...props }); 55 + } else { 56 + await api.statusReport.createStatusReportUpdate.mutate({ ...props }); 57 + } 58 + toast("saved"); 59 + onSubmit?.(); 60 + router.refresh(); 61 + } catch { 62 + toast("error"); 63 + } 64 + }); 65 + }; 66 + 67 + return ( 68 + <Form {...form}> 69 + <form 70 + onSubmit={async (e) => { 71 + e.preventDefault(); 72 + form.handleSubmit(handleSubmit)(e); 73 + }} 74 + className="grid w-full gap-6" 75 + > 76 + <General form={form} /> 77 + <Tabs defaultValue="message"> 78 + <TabsList> 79 + <TabsTrigger value="message">Message</TabsTrigger> 80 + <TabsTrigger value="date">Date & Time</TabsTrigger> 81 + </TabsList> 82 + <TabsContent value="message"> 83 + <SectionMessage form={form} /> 84 + </TabsContent> 85 + <TabsContent value="date"> 86 + <SectionDate form={form} /> 87 + </TabsContent> 88 + </Tabs> 89 + <Button className="w-full sm:w-auto" size="lg"> 90 + {!isPending ? "Confirm" : <LoadingAnimation />} 91 + </Button> 92 + </form> 93 + </Form> 94 + ); 95 + }
+64
apps/web/src/components/forms/status-report-update/general.tsx
··· 1 + "use client"; 2 + 3 + import type { UseFormReturn } from "react-hook-form"; 4 + 5 + import { 6 + statusReportStatus, 7 + statusReportStatusSchema, 8 + } from "@openstatus/db/src/schema"; 9 + import type { InsertStatusReportUpdate } from "@openstatus/db/src/schema"; 10 + import { 11 + FormControl, 12 + FormField, 13 + FormItem, 14 + FormLabel, 15 + FormMessage, 16 + RadioGroup, 17 + RadioGroupItem, 18 + } from "@openstatus/ui"; 19 + 20 + import { Icons } from "@/components/icons"; 21 + import { statusDict } from "@/data/incidents-dictionary"; 22 + 23 + interface Props { 24 + form: UseFormReturn<InsertStatusReportUpdate>; 25 + } 26 + export function General({ form }: Props) { 27 + return ( 28 + <FormField 29 + control={form.control} 30 + name="status" 31 + render={({ field }) => ( 32 + <FormItem className="space-y-1 sm:col-span-full"> 33 + <FormLabel>Status</FormLabel> 34 + <FormMessage /> 35 + <RadioGroup 36 + onValueChange={(value) => 37 + field.onChange(statusReportStatusSchema.parse(value)) 38 + } // value is a string 39 + defaultValue={field.value} 40 + className="grid grid-cols-2 gap-4 sm:grid-cols-4" 41 + > 42 + {statusReportStatus.map((status) => { 43 + const { value, label, icon } = statusDict[status]; 44 + const Icon = Icons[icon]; 45 + return ( 46 + <FormItem key={value}> 47 + <FormLabel className="[&:has([data-state=checked])>div]:border-primary [&:has([data-state=checked])>div]:text-foreground"> 48 + <FormControl> 49 + <RadioGroupItem value={value} className="sr-only" /> 50 + </FormControl> 51 + <div className="border-border text-muted-foreground flex w-full items-center justify-center rounded-lg border px-3 py-2 text-center text-sm"> 52 + <Icon className="mr-2 h-4 w-4 shrink-0" /> 53 + <span className="truncate">{label}</span> 54 + </div> 55 + </FormLabel> 56 + </FormItem> 57 + ); 58 + })} 59 + </RadioGroup> 60 + </FormItem> 61 + )} 62 + /> 63 + ); 64 + }
+41
apps/web/src/components/forms/status-report-update/section-date.tsx
··· 1 + "use client"; 2 + 3 + import type { UseFormReturn } from "react-hook-form"; 4 + 5 + import type { InsertStatusReportUpdate } from "@openstatus/db/src/schema"; 6 + import { 7 + DateTimePicker, 8 + FormDescription, 9 + FormField, 10 + FormItem, 11 + FormLabel, 12 + FormMessage, 13 + } from "@openstatus/ui"; 14 + 15 + interface Props { 16 + form: UseFormReturn<InsertStatusReportUpdate>; 17 + } 18 + export function SectionDate({ form }: Props) { 19 + return ( 20 + <FormField 21 + control={form.control} 22 + name="date" 23 + render={({ field }) => ( 24 + <FormItem> 25 + <FormLabel>Date</FormLabel> 26 + <DateTimePicker 27 + className="max-w-min rounded-md border" 28 + date={new Date(field.value)} 29 + setDate={(date) => { 30 + field.onChange(date); 31 + }} 32 + /> 33 + <FormDescription> 34 + The date and time when the incident took place. 35 + </FormDescription> 36 + <FormMessage /> 37 + </FormItem> 38 + )} 39 + /> 40 + ); 41 + }
+60
apps/web/src/components/forms/status-report-update/section-message.tsx
··· 1 + "use client"; 2 + 3 + import type { UseFormReturn } from "react-hook-form"; 4 + 5 + import type { InsertStatusReportUpdate } from "@openstatus/db/src/schema"; 6 + import { 7 + FormControl, 8 + FormDescription, 9 + FormField, 10 + FormItem, 11 + FormLabel, 12 + FormMessage, 13 + Tabs, 14 + TabsContent, 15 + TabsList, 16 + TabsTrigger, 17 + Textarea, 18 + } from "@openstatus/ui"; 19 + 20 + import { Preview } from "@/components/content/preview"; 21 + 22 + interface Props { 23 + form: UseFormReturn<InsertStatusReportUpdate>; 24 + } 25 + export function SectionMessage({ form }: Props) { 26 + return ( 27 + <FormField 28 + control={form.control} 29 + name="message" 30 + render={({ field }) => ( 31 + <FormItem className="sm:col-span-full"> 32 + <FormLabel>Message</FormLabel> 33 + <Tabs defaultValue="write"> 34 + <TabsList> 35 + <TabsTrigger value="write">Write</TabsTrigger> 36 + <TabsTrigger value="preview">Preview</TabsTrigger> 37 + </TabsList> 38 + <TabsContent value="write"> 39 + <FormControl> 40 + <Textarea 41 + placeholder="We are encountering..." 42 + className="h-auto w-full resize-none" 43 + rows={9} 44 + {...field} 45 + /> 46 + </FormControl> 47 + </TabsContent> 48 + <TabsContent value="preview"> 49 + <Preview md={form.getValues("message")} /> 50 + </TabsContent> 51 + </Tabs> 52 + <FormDescription> 53 + Tell your user what&apos;s happening. Supports markdown. 54 + </FormDescription> 55 + <FormMessage /> 56 + </FormItem> 57 + )} 58 + /> 59 + ); 60 + }
+10
apps/web/src/components/icons.tsx
··· 2 2 Activity, 3 3 AlertTriangle, 4 4 Bell, 5 + Book, 5 6 Bot, 6 7 Calendar, 7 8 Check, 8 9 Clock, 9 10 Cog, 10 11 Copy, 12 + CreditCard, 11 13 Fingerprint, 12 14 Globe2, 13 15 Image, 16 + KeyRound, 14 17 Laptop, 15 18 LayoutDashboard, 16 19 LineChart, ··· 20 23 MessageCircle, 21 24 Minus, 22 25 Moon, 26 + Newspaper, 23 27 PanelTop, 24 28 Pencil, 25 29 Play, ··· 36 40 ToyBrick, 37 41 Trash, 38 42 TwitterIcon, 43 + Users, 39 44 Webhook, 40 45 Youtube, 41 46 Zap, ··· 73 78 image: Image, 74 79 bell: Bell, 75 80 zap: Zap, 81 + users: Users, 82 + key: KeyRound, 83 + "credit-card": CreditCard, 76 84 "alert-triangle": AlertTriangle, 77 85 megaphone: Megaphone, 78 86 webhook: Webhook, ··· 85 93 clock: Clock, 86 94 "line-chart": LineChart, 87 95 linkedin: Linkedin, 96 + book: Book, 97 + newspaper: Newspaper, 88 98 youtube: Youtube, 89 99 discord: ({ ...props }: LucideProps) => ( 90 100 <svg viewBox="0 0 640 512" {...props}>
-52
apps/web/src/components/layout/app-header.tsx
··· 1 - "use client"; 2 - 3 - import Link from "next/link"; 4 - import { UserButton, useUser } from "@clerk/nextjs"; 5 - import { ArrowUpRight } from "lucide-react"; 6 - 7 - import { Button, Skeleton } from "@openstatus/ui"; 8 - 9 - import { Shell } from "../dashboard/shell"; 10 - 11 - /** 12 - * TODO: work on a better breadcrumb navigation like Vercel 13 - * [workspace/project/deploymenents/deployment] 14 - * This will allow us to 'only' save, and not redirect the user to the other pages 15 - * and therefore, can after saving the monitor/page go to the next tab! 16 - * Probably, we will need to use useSegements() from vercel, but once done properly, it could be really nice to share 17 - */ 18 - 19 - export function AppHeader() { 20 - const { isLoaded, isSignedIn } = useUser(); 21 - 22 - return ( 23 - <header className="border-border sticky top-3 z-50 w-full md:top-6"> 24 - <Shell className="bg-background/70 flex w-full items-center justify-between px-3 py-3 backdrop-blur-lg md:px-6 md:py-3"> 25 - <Link 26 - href={`/${isSignedIn ? "app" : ""}`} 27 - className="font-cal text-muted-foreground hover:text-foreground text-lg" 28 - > 29 - OpenStatus 30 - </Link> 31 - <div className="flex items-center gap-4"> 32 - <ul className="flex gap-2"> 33 - <li className="w-full"> 34 - <Button variant="link" asChild> 35 - <Link href="/docs" target="_blank"> 36 - Docs 37 - <ArrowUpRight className="ml-1 h-4 w-4 flex-shrink-0" /> 38 - </Link> 39 - </Button> 40 - </li> 41 - </ul> 42 - <div className="relative"> 43 - <Skeleton className="h-8 w-8 rounded-full" /> 44 - <div className="absolute inset-0"> 45 - {isLoaded && isSignedIn && <UserButton />} 46 - </div> 47 - </div> 48 - </div> 49 - </Shell> 50 - </header> 51 - ); 52 - }
+5 -8
apps/web/src/components/layout/app-link.tsx
··· 2 2 3 3 import Link from "next/link"; 4 4 import type { LinkProps } from "next/link"; 5 - import { useSelectedLayoutSegment } from "next/navigation"; 6 5 import { cva } from "class-variance-authority"; 7 6 8 7 import { cn } from "@/lib/utils"; ··· 15 14 variants: { 16 15 variant: { 17 16 default: "hover:bg-muted/50 hover:text-foreground border-transparent", 18 - active: "bg-muted/50 border-border text-foreground", 19 - disabled: "pointer-events-none opacity-60", 17 + active: "bg-muted/50 border-border text-foreground font-medium", 18 + disabled: "pointer-events-none opacity-60 border-transparent", 20 19 }, 21 20 }, 22 21 defaultVariants: { ··· 27 26 28 27 interface AppLinkProps extends LinkProps { 29 28 label: string; 30 - segment?: string | null; 31 29 icon?: ValidIcon; 32 30 className?: string; 31 + active?: boolean; 33 32 disabled?: boolean; 34 33 } 35 34 ··· 39 38 icon, 40 39 disabled, 41 40 className, 42 - segment, 41 + active, 43 42 ...props 44 43 }: AppLinkProps) { 45 - const selectedSegment = useSelectedLayoutSegment(); 46 44 const Icon = icon && Icons[icon]; 47 45 48 - const isActive = segment === selectedSegment; 49 - const variant = disabled ? "disabled" : isActive ? "active" : "default"; 46 + const variant = disabled ? "disabled" : active ? "active" : "default"; 50 47 51 48 return ( 52 49 <Link
+31 -24
apps/web/src/components/layout/app-menu.tsx
··· 1 1 "use client"; 2 2 3 3 import * as React from "react"; 4 - import { usePathname, useSearchParams } from "next/navigation"; 5 - import { Menu } from "lucide-react"; 4 + import { 5 + usePathname, 6 + useSearchParams, 7 + useSelectedLayoutSegment, 8 + } from "next/navigation"; 9 + import { ChevronsUpDown } from "lucide-react"; 6 10 7 11 import { 8 - Button, 9 - Sheet, 10 - SheetContent, 11 - SheetHeader, 12 - SheetTitle, 13 - SheetTrigger, 12 + Collapsible, 13 + CollapsibleContent, 14 + CollapsibleTrigger, 14 15 } from "@openstatus/ui"; 15 16 17 + import type { Page } from "@/config/pages"; 16 18 import { AppSidebar } from "./app-sidebar"; 17 19 18 - export function AppMenu() { 20 + export function AppMenu({ page }: { page?: Page }) { 19 21 const [open, setOpen] = React.useState(false); 20 22 const pathname = usePathname(); 21 23 const searchParams = useSearchParams(); 24 + const selectedSegment = useSelectedLayoutSegment(); 22 25 23 26 React.useEffect(() => { 24 27 setOpen(false); 25 28 }, [pathname, searchParams]); // remove searchParams if not needed 26 29 30 + if (!page) return null; 31 + 32 + const activeChild = page?.children?.find( 33 + ({ segment }) => segment === selectedSegment, 34 + ); 35 + 27 36 return ( 28 - <Sheet open={open} onOpenChange={(value) => setOpen(value)}> 29 - <SheetTrigger asChild> 30 - <Button size="icon" variant="outline"> 31 - <Menu className="h-6 w-6" /> 32 - </Button> 33 - </SheetTrigger> 34 - <SheetContent side="top" className="flex flex-col"> 35 - <SheetHeader> 36 - <SheetTitle className="ml-2 text-left">Menu</SheetTitle> 37 - </SheetHeader> 38 - <div className="flex-1"> 39 - <AppSidebar /> 40 - </div> 41 - </SheetContent> 42 - </Sheet> 37 + <Collapsible open={open} onOpenChange={(value) => setOpen(value)}> 38 + <CollapsibleTrigger className="flex w-full items-center justify-between"> 39 + <span className="text-foreground font-medium"> 40 + {activeChild?.title} 41 + </span> 42 + <span className="focus-visible:ring-ring hover:bg-accent hover:text-accent-foreground inline-flex h-9 w-9 items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50"> 43 + <ChevronsUpDown className="h-4 w-4" /> 44 + </span> 45 + </CollapsibleTrigger> 46 + <CollapsibleContent className="mt-2"> 47 + <AppSidebar page={page} /> 48 + </CollapsibleContent> 49 + </Collapsible> 43 50 ); 44 51 }
+24
apps/web/src/components/layout/app-page-layout.tsx
··· 1 + import { Shell } from "@/components/dashboard/shell"; 2 + import { cn } from "@/lib/utils"; 3 + import { HelpCallout } from "../dashboard/help-callout"; 4 + 5 + export default function AppPageLayout({ 6 + children, 7 + className, 8 + withHelpCallout = false, 9 + }: { 10 + children: React.ReactNode; 11 + className?: string; 12 + withHelpCallout?: boolean; 13 + }) { 14 + return ( 15 + <Shell className="relative flex flex-1 flex-col overflow-x-hidden"> 16 + <div 17 + className={cn("flex h-full flex-1 flex-col gap-6 md:gap-8", className)} 18 + > 19 + {children} 20 + </div> 21 + {withHelpCallout ? <HelpCallout /> : null} 22 + </Shell> 23 + ); 24 + }
+39
apps/web/src/components/layout/app-page-with-sidebar-layout.tsx
··· 1 + import { Shell } from "@/components/dashboard/shell"; 2 + import type { PageId } from "@/config/pages"; 3 + import { pagesConfig } from "@/config/pages"; 4 + import { cn } from "@/lib/utils"; 5 + import { AppMenu } from "./app-menu"; 6 + import { AppSidebar } from "./app-sidebar"; 7 + 8 + export default function AppPageWithSidebarLayout({ 9 + id, 10 + className, 11 + children, 12 + }: { 13 + id?: PageId; 14 + className?: string; 15 + children: React.ReactNode; 16 + }) { 17 + const page = pagesConfig.find((page) => page.segment === id); 18 + 19 + return ( 20 + <div className="flex w-full flex-1 flex-col gap-6 lg:flex-row lg:gap-8"> 21 + <Shell className="block py-3 md:py-3 lg:hidden"> 22 + <AppMenu page={page} /> 23 + </Shell> 24 + <Shell className="hidden max-h-[calc(100vh-8rem)] max-w-min shrink-0 lg:sticky lg:top-28 lg:block"> 25 + <AppSidebar page={page} /> 26 + </Shell> 27 + <Shell className="relative flex-1 overflow-x-hidden"> 28 + <div 29 + className={cn( 30 + "flex h-full flex-1 flex-col gap-6 md:gap-8", 31 + className, 32 + )} 33 + > 34 + {children} 35 + </div> 36 + </Shell> 37 + </div> 38 + ); 39 + }
+29 -38
apps/web/src/components/layout/app-sidebar.tsx
··· 1 1 "use client"; 2 2 3 - import { useParams } from "next/navigation"; 3 + import { useSelectedLayoutSegment } from "next/navigation"; 4 4 5 - import { pagesConfig } from "@/config/pages"; 5 + import type { Page } from "@/config/pages"; 6 6 import { ProBanner } from "../billing/pro-banner"; 7 - import { SelectWorkspace } from "../workspace/select-workspace"; 8 7 import { AppLink } from "./app-link"; 9 8 10 - // import { socialsConfig } from "@/config/socials"; 11 - // import { SocialIconButton } from "./social-icon-button"; 9 + export function AppSidebar({ page }: { page?: Page }) { 10 + const selectedSegment = useSelectedLayoutSegment(); 12 11 13 - export function AppSidebar() { 14 - const params = useParams(); 12 + if (!page) return null; 15 13 16 14 return ( 17 - <div className="flex h-full flex-col justify-between gap-6"> 18 - <ul className="grid gap-1"> 19 - {pagesConfig.map(({ title, href, icon, disabled }) => { 20 - return ( 21 - <li key={title} className="w-full"> 22 - <AppLink 23 - label={title} 24 - href={`/app/${params?.workspaceSlug}${href}`} 25 - disabled={disabled} 26 - segment={href.replace("/", "")} 27 - icon={icon} 28 - /> 29 - </li> 30 - ); 31 - })} 32 - </ul> 33 - <ul className="grid gap-2"> 34 - {/* <li className="w-full">Help & Support</li> */} 35 - <li className="w-full"> 36 - <ProBanner /> 37 - </li> 38 - {/* <li className="flex w-full gap-1"> 39 - {socialsConfig.map((props, i) => ( 40 - <SocialIconButton key={i} {...props} /> 41 - ))} 42 - </li> */} 43 - {/* maybe add the icons of the socials in here... */} 44 - <li className="w-full"> 45 - <SelectWorkspace /> 46 - </li> 47 - </ul> 15 + <div className="flex h-full flex-col justify-between"> 16 + <div className="grid gap-2"> 17 + <p className="text-foreground hidden px-3 text-lg font-medium lg:block"> 18 + {page?.title} 19 + </p> 20 + <ul className="grid gap-2"> 21 + {page?.children?.map(({ title, segment, icon, disabled }) => { 22 + return ( 23 + <li key={title} className="w-full"> 24 + <AppLink 25 + label={title} 26 + href={`./${segment}`} 27 + disabled={disabled} 28 + active={segment === selectedSegment} 29 + icon={icon} 30 + /> 31 + </li> 32 + ); 33 + })} 34 + </ul> 35 + </div> 36 + <div className="hidden lg:block"> 37 + <ProBanner /> 38 + </div> 48 39 </div> 49 40 ); 50 41 }
+1 -1
apps/web/src/components/layout/brand-name.tsx
··· 17 17 <ContextMenuTrigger> 18 18 <Link 19 19 href="/" 20 - className="font-cal text-muted-foreground hover:text-foreground" 20 + className="font-cal text-muted-foreground hover:text-foreground text-lg" 21 21 > 22 22 OpenStatus 23 23 </Link>
-27
apps/web/src/components/layout/breadcrumbs.tsx
··· 1 - "use client"; 2 - 3 - import { Fragment } from "react"; 4 - import { usePathname } from "next/navigation"; 5 - import { ChevronRight } from "lucide-react"; 6 - 7 - // TODO: create place to put into layout.tsx 8 - 9 - export function Breadcrumbs() { 10 - const pathname = usePathname(); 11 - const result = pathname.split("/").slice(3); 12 - 13 - return ( 14 - <ul className="flex items-center"> 15 - {result.map((path, i) => { 16 - return ( 17 - <Fragment key={i}> 18 - <li>{path}</li> 19 - {i !== result.length - 1 ? ( 20 - <ChevronRight className="text-muted-foreground mx-2 h-3 w-3" /> 21 - ) : null} 22 - </Fragment> 23 - ); 24 - })} 25 - </ul> 26 - ); 27 - }
+49
apps/web/src/components/layout/header/app-header.tsx
··· 1 + "use client"; 2 + 3 + import Link from "next/link"; 4 + import { UserButton, useUser } from "@clerk/nextjs"; 5 + import { ArrowUpRight } from "lucide-react"; 6 + 7 + import { Button, Skeleton } from "@openstatus/ui"; 8 + 9 + import { Shell } from "@/components/dashboard/shell"; 10 + import { ChangelogViewedButton } from "@/components/workspace/changelog-viewed-button"; 11 + import { AppTabs } from "./app-tabs"; 12 + import { Breadcrumbs } from "./breadcrumbs"; 13 + 14 + export function AppHeader() { 15 + const { isLoaded, isSignedIn } = useUser(); 16 + return ( 17 + // TODO: discuss amount of top-3 and top-6 18 + <header className="border-border sticky top-2 z-50 w-full"> 19 + <Shell className="bg-background/70 px-3 py-3 backdrop-blur-lg md:px-6 md:py-3"> 20 + <div className="flex w-full items-center justify-between"> 21 + <Breadcrumbs /> 22 + {/* */} 23 + <div className="flex items-center gap-1"> 24 + <ul className="hidden gap-1 sm:flex"> 25 + <li className="w-full"> 26 + <ChangelogViewedButton /> 27 + </li> 28 + <li className="w-full"> 29 + <Button variant="link" asChild> 30 + <Link href="/docs" target="_blank"> 31 + Docs 32 + <ArrowUpRight className="ml-1 h-4 w-4 flex-shrink-0" /> 33 + </Link> 34 + </Button> 35 + </li> 36 + </ul> 37 + <div className="relative"> 38 + <Skeleton className="h-8 w-8 rounded-full" /> 39 + <div className="absolute inset-0"> 40 + {isLoaded && isSignedIn && <UserButton />} 41 + </div> 42 + </div> 43 + </div> 44 + </div> 45 + <AppTabs /> 46 + </Shell> 47 + </header> 48 + ); 49 + }
+30
apps/web/src/components/layout/header/app-tabs.tsx
··· 1 + import { useParams, useSelectedLayoutSegment } from "next/navigation"; 2 + 3 + import { TabsContainer, TabsLink } from "@/components/dashboard/tabs-link"; 4 + import { pagesConfig } from "@/config/pages"; 5 + 6 + export function AppTabs() { 7 + const params = useParams(); 8 + const selectedSegment = useSelectedLayoutSegment(); 9 + 10 + if (!params?.workspaceSlug) return null; 11 + 12 + return ( 13 + <div className="-mb-3"> 14 + <TabsContainer hideSeparator> 15 + {pagesConfig.map(({ title, segment, href }) => { 16 + const active = segment === selectedSegment; 17 + return ( 18 + <TabsLink 19 + key={title} 20 + active={active} 21 + href={`/app/${params?.workspaceSlug}${href}`} 22 + > 23 + {title} 24 + </TabsLink> 25 + ); 26 + })} 27 + </TabsContainer> 28 + </div> 29 + ); 30 + }
+107
apps/web/src/components/layout/header/breadcrumbs.tsx
··· 1 + "use client"; 2 + 3 + import { Fragment, useEffect, useState } from "react"; 4 + import Image from "next/image"; 5 + import Link from "next/link"; 6 + import { 7 + useParams, 8 + useSelectedLayoutSegment, 9 + useSelectedLayoutSegments, 10 + } from "next/navigation"; 11 + import { Slash } from "lucide-react"; 12 + 13 + import { SelectWorkspace } from "@/components/workspace/select-workspace"; 14 + import { pagesConfig } from "@/config/pages"; 15 + import { notEmpty } from "@/lib/utils"; 16 + import { api } from "@/trpc/client"; 17 + 18 + export function Breadcrumbs() { 19 + const params = useParams(); 20 + // const selectedSegment = useSelectedLayoutSegment(); 21 + // const selectedSegments = useSelectedLayoutSegments(); 22 + // const label = useIdLabel(); 23 + 24 + // // remove route groups like '(overview)' from the segments 25 + // const segmentsWithoutRouteGroup = selectedSegments.filter( 26 + // (segment) => !segment.startsWith("("), 27 + // ); 28 + 29 + // const isRoot = segmentsWithoutRouteGroup.length <= 1; 30 + 31 + // const page = pagesConfig.find(({ segment }) => segment === selectedSegment); 32 + const breadcrumbs = [ 33 + // !isRoot ? page?.title : null, 34 + // label, 35 + ].filter(notEmpty); 36 + 37 + const isWorkspaceSlug = params.workspaceSlug; 38 + 39 + return ( 40 + <div className="flex items-center"> 41 + <Link href="/app" className="shrink-0"> 42 + <Image 43 + src="/icon.png" 44 + alt="OpenStatus" 45 + height={30} 46 + width={30} 47 + className="border-border rounded-full border" 48 + /> 49 + </Link> 50 + <Slash className="text-muted-foreground ml-2.5 mr-0.5 h-4 w-4 -rotate-12" /> 51 + {params.workspaceSlug ? ( 52 + <div className="w-40"> 53 + <SelectWorkspace /> 54 + </div> 55 + ) : null} 56 + {breadcrumbs.map((breadcrumb) => ( 57 + <Fragment key={breadcrumb}> 58 + <Slash className="text-muted-foreground ml-0.5 mr-2.5 h-4 w-4 -rotate-12" /> 59 + <p className="text-primary rounded-md text-sm font-medium"> 60 + {breadcrumb} 61 + </p> 62 + </Fragment> 63 + ))} 64 + </div> 65 + ); 66 + } 67 + 68 + // This is a custom hook that returns the label of the current id 69 + function useIdLabel() { 70 + const params = useParams(); 71 + const selectedSegment = useSelectedLayoutSegment(); 72 + const selectedSegments = useSelectedLayoutSegments(); 73 + const [label, setLabel] = useState<string>(); 74 + 75 + // remove route groups like '(overview)' from the segments 76 + const segmentsWithoutRouteGroup = selectedSegments.filter( 77 + (segment) => !segment.startsWith("("), 78 + ); 79 + 80 + const isRoot = segmentsWithoutRouteGroup.length <= 1; 81 + 82 + useEffect(() => { 83 + async function getInfos() { 84 + const { id } = params; 85 + if (!isRoot && id) { 86 + if (selectedSegment === "monitors") { 87 + const monitor = await api.monitor.getMonitorById.query({ 88 + id: Number(id), 89 + }); 90 + if (monitor) setLabel(monitor.name); 91 + } 92 + if (selectedSegment === "status-pages") { 93 + const statusPage = await api.page.getPageById.query({ 94 + id: Number(id), 95 + }); 96 + if (statusPage) setLabel(statusPage.title); 97 + } 98 + } 99 + if (isRoot && label) { 100 + setLabel(undefined); 101 + } 102 + } 103 + getInfos(); 104 + }, [params, selectedSegment, selectedSegments]); 105 + 106 + return label; 107 + }
+20 -17
apps/web/src/components/layout/marketing-header.tsx
··· 1 1 "use client"; 2 2 3 3 import Link from "next/link"; 4 + import { usePathname } from "next/navigation"; 4 5 import { useUser } from "@clerk/nextjs"; 5 6 6 7 import { Button } from "@openstatus/ui"; 7 8 9 + import { marketingPagesConfig } from "@/config/pages"; 8 10 import { cn } from "@/lib/utils"; 9 11 import { BrandName } from "./brand-name"; 10 12 import { MarketingMenu } from "./marketing-menu"; ··· 15 17 16 18 export function MarketingHeader({ className }: Props) { 17 19 const { isSignedIn } = useUser(); 20 + const pathname = usePathname(); 18 21 19 22 return ( 20 23 <header ··· 24 27 <BrandName /> 25 28 </div> 26 29 <div className="border-border mx-auto hidden items-center justify-center rounded-full border px-2 backdrop-blur-[2px] md:col-span-3 md:flex md:gap-1"> 27 - <Button variant="link" asChild> 28 - <Link href="/blog">Blog</Link> 29 - </Button> 30 - <Button variant="link" asChild> 31 - <Link href="/play">Playground</Link> 32 - </Button> 33 - <Button variant="link" asChild> 34 - <Link href="/changelog">Changelog</Link> 35 - </Button> 36 - <Button variant="link" asChild> 37 - <Link href="/pricing">Pricing</Link> 38 - </Button> 39 - <Button variant="link" asChild> 40 - <Link href="https://docs.openstatus.dev" target="_blank"> 41 - Docs 42 - </Link> 43 - </Button> 30 + {marketingPagesConfig.map(({ href, title, segment }) => { 31 + const isExternal = href.startsWith("http"); 32 + const externalProps = isExternal ? { target: "_blank" } : {}; 33 + const isActive = pathname.startsWith(href); 34 + return ( 35 + <Button 36 + key={segment} 37 + variant="link" 38 + className={isActive ? "font-semibold" : undefined} 39 + asChild 40 + > 41 + <Link href={href} {...externalProps}> 42 + {title} 43 + </Link> 44 + </Button> 45 + ); 46 + })} 44 47 </div> 45 48 <div className="flex items-center justify-end gap-3 md:col-span-1"> 46 49 <div className="block md:hidden">
+6 -12
apps/web/src/components/layout/marketing-menu.tsx
··· 13 13 SheetTrigger, 14 14 } from "@openstatus/ui"; 15 15 16 + import { marketingPagesConfig } from "@/config/pages"; 16 17 import { socialsConfig } from "@/config/socials"; 17 18 import { AppLink } from "./app-link"; 18 19 import { SocialIconButton } from "./social-icon-button"; 19 - 20 - const pages = [ 21 - { href: "/blog", label: "Blog", segment: "blog" }, 22 - { href: "/play", label: "Playground", segment: "play" }, 23 - { href: "/changelog", label: "Changelog", segment: "changelog" }, 24 - { href: "/pricing", label: "Pricing", segment: "pricing" }, 25 - { href: "https://docs.openstatus.dev", label: "Documentation" }, 26 - ]; 27 20 28 21 export function MarketingMenu() { 29 22 const [open, setOpen] = React.useState(false); ··· 52 45 </SheetHeader> 53 46 <div className="flex flex-1 flex-col justify-between gap-4"> 54 47 <ul className="grid gap-1"> 55 - {pages.map(({ href, label, segment }) => { 48 + {marketingPagesConfig.map(({ href, title, segment }) => { 56 49 const isExternal = href.startsWith("http"); 57 50 const externalProps = isExternal ? { target: "_blank" } : {}; 51 + const isActive = pathname.startsWith(href); 58 52 return ( 59 53 <li key={href} className="w-full"> 60 54 <AppLink 61 55 href={href} 62 - label={label} 63 - segment={segment} 56 + label={title} 57 + active={isActive} 64 58 {...externalProps} 65 59 /> 66 60 </li> 67 61 ); 68 62 })} 69 63 </ul> 70 - <ul className="flex gap-1"> 64 + <ul className="flex gap-2"> 71 65 {socialsConfig.map((props, i) => ( 72 66 <li key={i}> 73 67 <SocialIconButton {...props} />
+49
apps/web/src/components/status-update/edit-status-update.tsx
··· 1 + "use client"; 2 + 3 + import { useState } from "react"; 4 + 5 + import type { InsertStatusReportUpdate } from "@openstatus/db/src/schema"; 6 + import { 7 + Button, 8 + Dialog, 9 + DialogContent, 10 + DialogDescription, 11 + DialogHeader, 12 + DialogTitle, 13 + DialogTrigger, 14 + } from "@openstatus/ui"; 15 + 16 + import { StatusReportUpdateForm } from "../forms/status-report-update/form"; 17 + import { Icons } from "../icons"; 18 + 19 + export function EditStatusReportUpdateIconButton({ 20 + statusReportId, 21 + statusReportUpdate, 22 + }: { 23 + statusReportId: number; 24 + statusReportUpdate?: InsertStatusReportUpdate; 25 + }) { 26 + const [open, setOpen] = useState(false); 27 + return ( 28 + <Dialog open={open} onOpenChange={setOpen}> 29 + <DialogTrigger asChild> 30 + <Button size="icon" variant="outline"> 31 + <Icons.pencil className="h-4 w-4" /> 32 + </Button> 33 + </DialogTrigger> 34 + <DialogContent className="max-h-screen overflow-y-scroll sm:max-w-[650px]"> 35 + <DialogHeader> 36 + <DialogTitle>Edit Status Report</DialogTitle> 37 + <DialogDescription> 38 + Update your status report with new information. 39 + </DialogDescription> 40 + </DialogHeader> 41 + <StatusReportUpdateForm 42 + statusReportId={statusReportId} 43 + defaultValues={statusReportUpdate} 44 + onSubmit={() => setOpen(false)} 45 + /> 46 + </DialogContent> 47 + </Dialog> 48 + ); 49 + }
+7 -11
apps/web/src/components/status-update/events.tsx
··· 1 1 "use client"; 2 2 3 3 import * as React from "react"; 4 + import Link from "next/link"; 4 5 import { useRouter } from "next/navigation"; 5 6 import { format } from "date-fns"; 6 7 7 8 import type { StatusReportUpdate } from "@openstatus/db/src/schema"; 8 9 import { Button } from "@openstatus/ui"; 9 10 10 - import { DeleteStatusReportUpdateButtonIcon } from "@/app/app/[workspaceSlug]/(dashboard)/status-reports/_components/delete-status-update"; 11 11 import { Icons } from "@/components/icons"; 12 12 import { statusDict } from "@/data/incidents-dictionary"; 13 13 import { useProcessor } from "@/hooks/use-preprocessor"; 14 14 import { cn } from "@/lib/utils"; 15 + import { DeleteStatusReportUpdateButtonIcon } from "./delete-status-update"; 16 + import { EditStatusReportUpdateIconButton } from "./edit-status-update"; 15 17 16 18 export function Events({ 17 19 statusReportUpdates, ··· 63 65 <div className="mt-1 grid flex-1"> 64 66 {editable ? ( 65 67 <div className="absolute right-2 top-2 hidden gap-2 group-hover:flex group-active:flex"> 66 - <Button 67 - size="icon" 68 - variant="outline" 69 - className="h-7 w-7 p-0" 70 - onClick={() => { 71 - router.push(`./update/edit?statusUpdate=${update.id}`); 72 - }} 73 - > 74 - <Icons.pencil className="h-4 w-4" /> 75 - </Button> 68 + <EditStatusReportUpdateIconButton 69 + statusReportId={update.statusReportId} 70 + statusReportUpdate={update} 71 + /> 76 72 <DeleteStatusReportUpdateButtonIcon id={update.id} /> 77 73 </div> 78 74 ) : undefined}
+57
apps/web/src/components/workspace/changelog-viewed-button.tsx
··· 1 + import { useEffect, useState } from "react"; 2 + import Link from "next/link"; 3 + import { allChangelogs } from "contentlayer/generated"; 4 + 5 + import { Button } from "@openstatus/ui"; 6 + 7 + const lastChangelog = allChangelogs 8 + .sort( 9 + (a, b) => 10 + new Date(a.publishedAt).getTime() - new Date(b.publishedAt).getTime(), 11 + ) 12 + .pop(); 13 + 14 + export function ChangelogViewedButton() { 15 + const [show, setShow] = useState(false); 16 + 17 + function onClick() { 18 + if (document) { 19 + const date = new Date().toISOString(); 20 + // store the last viewed changelog date in a cookie 21 + document.cookie = `last-viewed-changelog=${date}; path=/`; 22 + } 23 + setShow(false); 24 + } 25 + 26 + useEffect(() => { 27 + if (document) { 28 + const cookie = document.cookie 29 + .split("; ") 30 + .find((row) => row.startsWith("last-viewed-changelog")); 31 + if (!cookie) { 32 + setShow(true); 33 + return; 34 + } 35 + const lastViewed = new Date(cookie.split("=")[1]); 36 + if (lastChangelog && lastViewed < new Date(lastChangelog.publishedAt)) { 37 + setShow(true); 38 + } 39 + } 40 + }, []); 41 + 42 + return ( 43 + <Button variant="link" asChild> 44 + <Link 45 + href="/changelog" 46 + target="_blank" 47 + onClick={onClick} 48 + className="relative" 49 + > 50 + Changelog 51 + {show ? ( 52 + <span className="absolute right-1 top-1 h-2 w-2 rounded-full bg-green-500" /> 53 + ) : null} 54 + </Link> 55 + </Button> 56 + ); 57 + }
+7 -3
apps/web/src/components/workspace/select-workspace.tsx
··· 43 43 <DropdownMenu> 44 44 <DropdownMenuTrigger asChild> 45 45 <Button 46 - variant="outline" 46 + variant="ghost" 47 47 className="flex w-full items-center justify-between" 48 48 > 49 - {active ? <span>{active}</span> : <Skeleton className="h-5 w-full" />} 49 + {active ? ( 50 + <span className="truncate">{active}</span> 51 + ) : ( 52 + <Skeleton className="h-5 w-full" /> 53 + )} 50 54 <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> 51 55 </Button> 52 56 </DropdownMenuTrigger> ··· 65 69 }} 66 70 className="justify-between" 67 71 > 68 - {workspace.slug} 72 + <span className="truncate">{workspace.slug}</span> 69 73 {active === workspace.slug ? ( 70 74 <Check className="ml-2 h-4 w-4" /> 71 75 ) : null}
+188 -12
apps/web/src/config/pages.ts
··· 1 1 import type { ValidIcon } from "@/components/icons"; 2 2 3 - type Page = { 3 + export type Page = { 4 4 title: string; 5 5 description: string; 6 6 href: string; 7 7 icon: ValidIcon; 8 8 disabled?: boolean; 9 + segment: string; 10 + children?: Page[]; 9 11 }; 10 12 11 - // TODO: add to <Header id="dashboard" /> to easily access title and description - ideally allow both 12 - export const pagesConfig: Page[] = [ 13 + export const settingsPagesConfig: Page[] = [ 14 + { 15 + title: "General", 16 + description: "General settings for the workspace.", 17 + href: "/settings/general", 18 + icon: "cog", 19 + segment: "general", 20 + }, 21 + { 22 + title: "Team", 23 + description: "Team settings for the workspace.", 24 + href: "/settings/team", 25 + icon: "users", 26 + segment: "team", 27 + }, 28 + { 29 + title: "API Token", 30 + description: "API token settings for the workspace.", 31 + href: "/settings/api-token", 32 + icon: "key", 33 + segment: "api-token", 34 + }, 35 + { 36 + title: "Billing", 37 + description: "Billing settings for the workspace.", 38 + href: "/settings/billing", 39 + icon: "credit-card", 40 + segment: "billing", 41 + }, 42 + { 43 + title: "Appearance", 44 + description: "Appearance settings for the workspace.", 45 + href: "/settings/appearance", 46 + icon: "sun", 47 + segment: "appearance", 48 + }, 49 + ]; 50 + 51 + export const monitorPagesConfig: Page[] = [ 52 + { 53 + title: "Overview", 54 + description: "Dashboard with all the metrics and charts.", 55 + href: "/monitors/[id]/overview", 56 + icon: "line-chart", 57 + segment: "overview", 58 + }, 59 + { 60 + title: "Response logs", 61 + description: "Data table with all response details.", 62 + href: "/monitors/[id]/data", 63 + icon: "table", 64 + segment: "data", 65 + }, 66 + { 67 + title: "Settings", 68 + description: "Edit section for the monitor.", 69 + href: "/monitors/[id]/edit", 70 + icon: "cog", 71 + segment: "edit", 72 + }, 73 + ]; 74 + 75 + export const statusPagesPagesConfig: Page[] = [ 76 + { 77 + title: "Settings", 78 + description: "Edit section for the status page.", 79 + href: "/status-pages/[id]/edit", 80 + icon: "cog", 81 + segment: "edit", 82 + }, 83 + { 84 + title: "Domain", 85 + description: "Where you can see the domain settings.", 86 + href: "/status-pages/[id]/domain", 87 + icon: "globe", 88 + segment: "domain", 89 + }, 90 + { 91 + title: "Subscribers", 92 + description: "Where you can see all the subscribers.", 93 + href: "/status-pages/[id]/subscribers", 94 + icon: "users", 95 + segment: "subscribers", 96 + }, 97 + ]; 98 + 99 + export const statusReportsPagesConfig: Page[] = [ 100 + { 101 + title: "Overview", 102 + description: "Overview of the status report.", 103 + href: "/status-reports/[id]/overview", 104 + icon: "megaphone", 105 + segment: "overview", 106 + }, 107 + { 108 + title: "Settings", 109 + description: "Edit section for the status report.", 110 + href: "/status-reports/[id]/edit", 111 + icon: "cog", 112 + segment: "edit", 113 + }, 114 + ]; 115 + 116 + export const notificationsPagesConfig: Page[] = [ 117 + { 118 + title: "Settings", 119 + description: "Edit section for the notifications.", 120 + href: "/notifications/[id]/edit", 121 + icon: "cog", 122 + segment: "edit", 123 + }, 124 + ]; 125 + 126 + export type PageId = (typeof pagesConfig)[number]["segment"]; 127 + 128 + export const pagesConfig = [ 13 129 { 14 130 title: "Monitors", 15 131 description: "Check all the responses in one place.", 16 132 href: "/monitors", 17 133 icon: "activity", 134 + segment: "monitors", 135 + children: monitorPagesConfig, 18 136 }, 19 137 { 20 138 title: "Incidents", 21 139 description: "All your incidents.", 22 140 href: "/incidents", 23 141 icon: "siren", 142 + segment: "incidents", 24 143 }, 25 144 { 26 145 title: "Status Pages", 27 146 description: "Where you can see all the pages.", 28 147 href: "/status-pages", 29 148 icon: "panel-top", 149 + segment: "status-pages", 150 + children: statusPagesPagesConfig, 30 151 }, 31 152 { 32 153 title: "Status Reports", 33 154 description: "War room where you handle the incidents.", 34 155 href: "/status-reports", 35 156 icon: "megaphone", 157 + segment: "status-reports", 158 + children: statusReportsPagesConfig, 36 159 }, 37 160 { 38 161 title: "Notifications", 39 162 description: "Where you can see all the notifications.", 40 163 href: "/notifications", 41 164 icon: "bell", 165 + segment: "notifications", 166 + children: notificationsPagesConfig, 42 167 }, 43 168 { 44 169 title: "Settings", 45 170 description: "Your workspace settings", 46 - href: "/settings", 171 + href: "/settings/general", 47 172 icon: "cog", 173 + segment: "settings", 174 + children: settingsPagesConfig, 48 175 }, 49 - // { 50 - // title: "Integrations", 51 - // description: "Where you can see all the integrations.", 52 - // href: "/integrations", 53 - // icon: "plug", 54 - // }, 55 - // ... 56 - ]; 176 + ] as const satisfies readonly Page[]; 177 + 178 + export const marketingPagesConfig = [ 179 + { 180 + href: "/blog", 181 + title: "Blog", 182 + description: "All the latest articles and news from OpenStatus.", 183 + segment: "blog", 184 + icon: "book", 185 + }, 186 + { 187 + href: "/play", 188 + title: "Playground", 189 + description: "All the latest tools build by OpenStatus.", 190 + segment: "play", 191 + icon: "toy-brick", 192 + }, 193 + { 194 + href: "/changelog", 195 + title: "Changelog", 196 + description: "All the latest features, fixes and work to OpenStatus.", 197 + segment: "changelog", 198 + icon: "newspaper", 199 + }, 200 + { 201 + href: "/pricing", 202 + title: "Pricing", 203 + description: "The pricing for OpenStatus.", 204 + segment: "pricing", 205 + icon: "credit-card", 206 + }, 207 + { 208 + href: "https://docs.openstatus.dev", 209 + description: "The documentation for OpenStatus.", 210 + title: "Docs", 211 + segment: "docs", 212 + icon: "book", 213 + }, 214 + ] as const satisfies readonly Page[]; 215 + 216 + export function getPageBySegment( 217 + segment: string | string[], 218 + currentPage: readonly Page[] = pagesConfig, 219 + ): Page | undefined { 220 + if (typeof segment === "string") { 221 + const page = currentPage.find((page) => page.segment === segment); 222 + return page; 223 + } else if (Array.isArray(segment) && segment.length > 0) { 224 + const [firstSegment, ...restSegments] = segment; 225 + const childPage = currentPage.find((page) => page.segment === firstSegment); 226 + if (childPage && childPage.children) { 227 + return getPageBySegment(restSegments, childPage.children); 228 + } 229 + return childPage; 230 + } 231 + return undefined; 232 + }
+1
packages/ui/package.json
··· 23 23 "@radix-ui/react-alert-dialog": "1.0.5", 24 24 "@radix-ui/react-avatar": "1.0.4", 25 25 "@radix-ui/react-checkbox": "1.0.4", 26 + "@radix-ui/react-collapsible": "^1.0.3", 26 27 "@radix-ui/react-context-menu": "2.1.5", 27 28 "@radix-ui/react-dialog": "1.0.4", 28 29 "@radix-ui/react-dropdown-menu": "2.0.6",
+11
packages/ui/src/components/collabsible.tsx
··· 1 + "use client"; 2 + 3 + import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; 4 + 5 + const Collapsible = CollapsiblePrimitive.Root; 6 + 7 + const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; 8 + 9 + const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent; 10 + 11 + export { Collapsible, CollapsibleTrigger, CollapsibleContent };
+92
packages/ui/src/components/date-time-picker-popover.tsx
··· 1 + // CREDITS: https://gist.github.com/fernandops26/da681c4b12e52191803b4fcb040cdebb 2 + "use client"; 3 + 4 + import * as React from "react"; 5 + import { Calendar as CalendarIcon } from "lucide-react"; 6 + import { DateTime } from "luxon"; 7 + import type { SelectSingleEventHandler } from "react-day-picker"; 8 + 9 + import { cn } from "../lib/utils"; 10 + import { Button } from "./button"; 11 + import { Calendar } from "./calendar"; 12 + import { Input } from "./input"; 13 + import { Label } from "./label"; 14 + import { Popover, PopoverContent, PopoverTrigger } from "./popover"; 15 + 16 + interface DateTimePickerProps { 17 + date: Date; 18 + setDate: (date: Date) => void; 19 + className?: string; 20 + } 21 + 22 + export function DateTimePickerPopover({ 23 + date, 24 + setDate, 25 + className, 26 + }: DateTimePickerProps) { 27 + const [selectedDateTime, setSelectedDateTime] = React.useState<DateTime>( 28 + DateTime.fromJSDate(date), 29 + ); 30 + 31 + const handleSelect: SelectSingleEventHandler = (day, selected) => { 32 + const selectedDay = DateTime.fromJSDate(selected); 33 + const modifiedDay = selectedDay.set({ 34 + hour: selectedDateTime.hour, 35 + minute: selectedDateTime.minute, 36 + }); 37 + 38 + setSelectedDateTime(modifiedDay); 39 + setDate(modifiedDay.toJSDate()); 40 + }; 41 + 42 + const handleTimeChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { 43 + const { value } = e.target; 44 + const hours = Number.parseInt(value.split(":")[0] || "00", 10); 45 + const minutes = Number.parseInt(value.split(":")[1] || "00", 10); 46 + const modifiedDay = selectedDateTime.set({ hour: hours, minute: minutes }); 47 + 48 + setSelectedDateTime(modifiedDay); 49 + setDate(modifiedDay.toJSDate()); 50 + }; 51 + 52 + return ( 53 + <Popover> 54 + <PopoverTrigger asChild className="z-10"> 55 + <Button 56 + variant={"outline"} 57 + className={cn( 58 + "w-[280px] justify-start text-left font-normal", 59 + !date && "text-muted-foreground", 60 + className, 61 + )} 62 + suppressHydrationWarning // because timestamp is not same, server and client 63 + > 64 + <CalendarIcon className="mr-2 h-4 w-4" /> 65 + {date ? ( 66 + selectedDateTime.toFormat("DDD HH:mm") 67 + ) : ( 68 + <span>Pick a date</span> 69 + )} 70 + </Button> 71 + </PopoverTrigger> 72 + <PopoverContent className="w-auto p-0"> 73 + <Calendar 74 + mode="single" 75 + selected={selectedDateTime.toJSDate()} 76 + onSelect={handleSelect} 77 + initialFocus 78 + /> 79 + <div className="px-4 pb-4 pt-0"> 80 + <Label>Time</Label> 81 + {/* TODO: style it properly! */} 82 + <Input 83 + type="time" 84 + onChange={handleTimeChange} 85 + value={selectedDateTime.toFormat("HH:mm")} 86 + /> 87 + </div> 88 + {!selectedDateTime && <p>Please pick a day.</p>} 89 + </PopoverContent> 90 + </Popover> 91 + ); 92 + }
+17 -41
packages/ui/src/components/date-time-picker.tsx
··· 2 2 "use client"; 3 3 4 4 import * as React from "react"; 5 - import { Calendar as CalendarIcon } from "lucide-react"; 6 5 import { DateTime } from "luxon"; 7 6 import type { SelectSingleEventHandler } from "react-day-picker"; 8 7 9 - import { Button } from "../components/button"; 10 8 import { Calendar } from "../components/calendar"; 11 9 import { Input } from "../components/input"; 12 10 import { Label } from "../components/label"; 13 - import { Popover, PopoverContent, PopoverTrigger } from "../components/popover"; 14 - import { cn } from "../lib/utils"; 15 11 16 12 interface DateTimePickerProps { 17 13 date: Date; ··· 50 46 }; 51 47 52 48 return ( 53 - <Popover> 54 - <PopoverTrigger asChild className="z-10"> 55 - <Button 56 - variant={"outline"} 57 - className={cn( 58 - "w-[280px] justify-start text-left font-normal", 59 - !date && "text-muted-foreground", 60 - className, 61 - )} 62 - suppressHydrationWarning // because timestamp is not same, server and client 63 - > 64 - <CalendarIcon className="mr-2 h-4 w-4" /> 65 - {date ? ( 66 - selectedDateTime.toFormat("DDD HH:mm") 67 - ) : ( 68 - <span>Pick a date</span> 69 - )} 70 - </Button> 71 - </PopoverTrigger> 72 - <PopoverContent className="w-auto p-0"> 73 - <Calendar 74 - mode="single" 75 - selected={selectedDateTime.toJSDate()} 76 - onSelect={handleSelect} 77 - initialFocus 49 + <div className={className}> 50 + <Calendar 51 + mode="single" 52 + selected={selectedDateTime.toJSDate()} 53 + onSelect={handleSelect} 54 + initialFocus 55 + /> 56 + <div className="px-4 pb-4 pt-0"> 57 + <Label>Time</Label> 58 + {/* TODO: style it properly! */} 59 + <Input 60 + type="time" 61 + onChange={handleTimeChange} 62 + value={selectedDateTime.toFormat("HH:mm")} 78 63 /> 79 - <div className="px-4 pb-4 pt-0"> 80 - <Label>Time</Label> 81 - {/* TODO: style it properly! */} 82 - <Input 83 - type="time" 84 - onChange={handleTimeChange} 85 - value={selectedDateTime.toFormat("HH:mm")} 86 - /> 87 - </div> 88 - {!selectedDateTime && <p>Please pick a day.</p>} 89 - </PopoverContent> 90 - </Popover> 64 + </div> 65 + {!selectedDateTime && <p>Please pick a day.</p>} 66 + </div> 91 67 ); 92 68 }
+2
packages/ui/src/index.tsx
··· 33 33 export * from "./components/tooltip"; 34 34 export * from "./components/tabs"; 35 35 export * from "./components/date-time-picker"; 36 + export * from "./components/date-time-picker-popover"; 36 37 export * from "./components/sheet"; 37 38 export * from "./components/use-toast"; 38 39 export * from "./components/multi-select"; 39 40 export * from "./components/toggle"; 41 + export * from "./components/collabsible";
+138 -115
pnpm-lock.yaml
··· 16 16 version: link:packages/config/eslint 17 17 '@turbo/gen': 18 18 specifier: 1.10.12 19 - version: 1.10.12(@types/node@20.8.0)(typescript@5.2.2) 19 + version: 1.10.12(@types/node@20.11.17)(typescript@5.2.2) 20 20 eslint: 21 21 specifier: 8.50.0 22 22 version: 8.50.0 ··· 214 214 version: 0.6.3 215 215 contentlayer: 216 216 specifier: 0.3.4 217 - version: 0.3.4(esbuild@0.19.5) 217 + version: 0.3.4(esbuild@0.20.0) 218 218 date-fns: 219 219 specifier: 2.30.0 220 220 version: 2.30.0 ··· 238 238 version: 14.1.0(@babel/core@7.23.2)(@opentelemetry/api@1.4.1)(react-dom@18.2.0)(react@18.2.0) 239 239 next-contentlayer: 240 240 specifier: 0.3.4 241 - version: 0.3.4(contentlayer@0.3.4)(esbuild@0.19.5)(next@14.1.0)(react-dom@18.2.0)(react@18.2.0) 241 + version: 0.3.4(contentlayer@0.3.4)(esbuild@0.20.0)(next@14.1.0)(react-dom@18.2.0)(react@18.2.0) 242 242 next-plausible: 243 243 specifier: 3.12.0 244 244 version: 3.12.0(next@14.1.0)(react-dom@18.2.0)(react@18.2.0) ··· 347 347 dependencies: 348 348 '@jitsu/js': 349 349 specifier: 1.3.0 350 - version: 1.3.0(@types/dlv@1.1.3) 350 + version: 1.3.0(@types/dlv@1.1.4) 351 351 '@t3-oss/env-core': 352 352 specifier: 0.7.0 353 353 version: 0.7.0(typescript@5.2.2)(zod@3.22.2) ··· 755 755 '@radix-ui/react-checkbox': 756 756 specifier: 1.0.4 757 757 version: 1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0) 758 + '@radix-ui/react-collapsible': 759 + specifier: ^1.0.3 760 + version: 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0) 758 761 '@radix-ui/react-context-menu': 759 762 specifier: 2.1.5 760 763 version: 2.1.5(@types/react-dom@18.2.19)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0) ··· 922 925 '@analytics/global-storage-utils': 0.1.7 923 926 dev: false 924 927 925 - /@analytics/core@0.12.7(@types/dlv@1.1.3): 928 + /@analytics/core@0.12.7(@types/dlv@1.1.4): 926 929 resolution: {integrity: sha512-etmIPCoxWLoUZ/o1o2zvIk4cdVHa8I1xUQtTuLA+YXQ4SsFbm75ZoMXJBqWrNSENpqCJgoL6hizl5uTbkNN+1Q==} 927 930 dependencies: 928 931 '@analytics/global-storage-utils': 0.1.7 929 932 '@analytics/type-utils': 0.6.2 930 - analytics-utils: 1.0.12(@types/dlv@1.1.3) 933 + analytics-utils: 1.0.12(@types/dlv@1.1.4) 931 934 transitivePeerDependencies: 932 935 - '@types/dlv' 933 936 dev: false ··· 1259 1262 commander: 9.4.1 1260 1263 dev: false 1261 1264 1262 - /@contentlayer/cli@0.3.4(esbuild@0.19.5): 1265 + /@contentlayer/cli@0.3.4(esbuild@0.20.0): 1263 1266 resolution: {integrity: sha512-vNDwgLuhYNu+m70NZ3XK9kexKNguuxPXg7Yvzj3B34cEilQjjzSrcTY/i+AIQm9V7uT5GGshx9ukzPf+SmoszQ==} 1264 1267 dependencies: 1265 - '@contentlayer/core': 0.3.4(esbuild@0.19.5) 1268 + '@contentlayer/core': 0.3.4(esbuild@0.20.0) 1266 1269 '@contentlayer/utils': 0.3.4 1267 1270 clipanion: 3.2.1(typanion@3.14.0) 1268 1271 typanion: 3.14.0 ··· 1273 1276 - supports-color 1274 1277 dev: false 1275 1278 1276 - /@contentlayer/client@0.3.4(esbuild@0.19.5): 1279 + /@contentlayer/client@0.3.4(esbuild@0.20.0): 1277 1280 resolution: {integrity: sha512-QSlLyc3y4PtdC5lFw0L4wTZUH8BQnv2nk37hNCsPAqGf+dRO7TLAzdc+2/mVIRgK+vSH+pSOzjLsQpFxxXRTZA==} 1278 1281 dependencies: 1279 - '@contentlayer/core': 0.3.4(esbuild@0.19.5) 1282 + '@contentlayer/core': 0.3.4(esbuild@0.20.0) 1280 1283 transitivePeerDependencies: 1281 1284 - '@effect-ts/otel-node' 1282 1285 - esbuild ··· 1284 1287 - supports-color 1285 1288 dev: false 1286 1289 1287 - /@contentlayer/core@0.3.4(esbuild@0.19.5): 1290 + /@contentlayer/core@0.3.4(esbuild@0.20.0): 1288 1291 resolution: {integrity: sha512-o68oBLwfYZ+2vtgfk1lgHxOl3LoxvRNiUfeQ8IWFWy/L4wnIkKIqLZX01zlRE5IzYM+ZMMN5V0cKQlO7DsyR9g==} 1289 1292 peerDependencies: 1290 1293 esbuild: 0.17.x || 0.18.x ··· 1298 1301 '@contentlayer/utils': 0.3.4 1299 1302 camel-case: 4.1.2 1300 1303 comment-json: 4.2.3 1301 - esbuild: 0.19.5 1304 + esbuild: 0.20.0 1302 1305 gray-matter: 4.0.3 1303 - mdx-bundler: 9.2.1(esbuild@0.19.5) 1306 + mdx-bundler: 9.2.1(esbuild@0.20.0) 1304 1307 rehype-stringify: 9.0.4 1305 1308 remark-frontmatter: 4.0.1 1306 1309 remark-parse: 10.0.2 ··· 1313 1316 - supports-color 1314 1317 dev: false 1315 1318 1316 - /@contentlayer/source-files@0.3.4(esbuild@0.19.5): 1319 + /@contentlayer/source-files@0.3.4(esbuild@0.20.0): 1317 1320 resolution: {integrity: sha512-4njyn0OFPu7WY4tAjMxiJgWOKeiHuBOGdQ36EYE03iij/pPPRbiWbL+cmLccYXUFEW58mDwpqROZZm6pnxjRDQ==} 1318 1321 dependencies: 1319 - '@contentlayer/core': 0.3.4(esbuild@0.19.5) 1322 + '@contentlayer/core': 0.3.4(esbuild@0.20.0) 1320 1323 '@contentlayer/utils': 0.3.4 1321 1324 chokidar: 3.5.3 1322 1325 fast-glob: 3.3.1 ··· 1334 1337 - supports-color 1335 1338 dev: false 1336 1339 1337 - /@contentlayer/source-remote-files@0.3.4(esbuild@0.19.5): 1340 + /@contentlayer/source-remote-files@0.3.4(esbuild@0.20.0): 1338 1341 resolution: {integrity: sha512-cyiv4sNUySZvR0uAKlM+kSAELzNd2h2QT1R2e41dRKbwOUVxeLfmGiLugr0aVac6Q3xYcD99dbHyR1xWPV+w9w==} 1339 1342 dependencies: 1340 - '@contentlayer/core': 0.3.4(esbuild@0.19.5) 1341 - '@contentlayer/source-files': 0.3.4(esbuild@0.19.5) 1343 + '@contentlayer/core': 0.3.4(esbuild@0.20.0) 1344 + '@contentlayer/source-files': 0.3.4(esbuild@0.20.0) 1342 1345 '@contentlayer/utils': 0.3.4 1343 1346 transitivePeerDependencies: 1344 1347 - '@effect-ts/otel-node' ··· 1463 1466 get-tsconfig: 4.7.2 1464 1467 dev: true 1465 1468 1466 - /@esbuild-plugins/node-resolve@0.1.4(esbuild@0.19.5): 1469 + /@esbuild-plugins/node-resolve@0.1.4(esbuild@0.20.0): 1467 1470 resolution: {integrity: sha512-haFQ0qhxEpqtWWY0kx1Y5oE3sMyO1PcoSiWEPrAw6tm/ZOOLXjSs6Q+v1v9eyuVF0nNt50YEvrcrvENmyoMv5g==} 1468 1471 peerDependencies: 1469 1472 esbuild: '*' 1470 1473 dependencies: 1471 1474 '@types/resolve': 1.20.4 1472 1475 debug: 4.3.4 1473 - esbuild: 0.19.5 1476 + esbuild: 0.20.0 1474 1477 escape-string-regexp: 4.0.0 1475 1478 resolve: 1.22.8 1476 1479 transitivePeerDependencies: 1477 1480 - supports-color 1478 1481 dev: false 1482 + 1483 + /@esbuild/aix-ppc64@0.20.0: 1484 + resolution: {integrity: sha512-fGFDEctNh0CcSwsiRPxiaqX0P5rq+AqE0SRhYGZ4PX46Lg1FNR6oCxJghf8YgY0WQEgQuh3lErUFE4KxLeRmmw==} 1485 + engines: {node: '>=12'} 1486 + cpu: [ppc64] 1487 + os: [aix] 1488 + requiresBuild: true 1489 + dev: false 1490 + optional: true 1479 1491 1480 1492 /@esbuild/android-arm64@0.16.4: 1481 1493 resolution: {integrity: sha512-VPuTzXFm/m2fcGfN6CiwZTlLzxrKsWbPkG7ArRFpuxyaHUm/XFHQPD4xNwZT6uUmpIHhnSjcaCmcla8COzmZ5Q==} ··· 1495 1507 dev: true 1496 1508 optional: true 1497 1509 1498 - /@esbuild/android-arm64@0.19.5: 1499 - resolution: {integrity: sha512-5d1OkoJxnYQfmC+Zd8NBFjkhyCNYwM4n9ODrycTFY6Jk1IGiZ+tjVJDDSwDt77nK+tfpGP4T50iMtVi4dEGzhQ==} 1510 + /@esbuild/android-arm64@0.20.0: 1511 + resolution: {integrity: sha512-aVpnM4lURNkp0D3qPoAzSG92VXStYmoVPOgXveAUoQBWRSuQzt51yvSju29J6AHPmwY1BjH49uR29oyfH1ra8Q==} 1500 1512 engines: {node: '>=12'} 1501 1513 cpu: [arm64] 1502 1514 os: [android] ··· 1522 1534 dev: true 1523 1535 optional: true 1524 1536 1525 - /@esbuild/android-arm@0.19.5: 1526 - resolution: {integrity: sha512-bhvbzWFF3CwMs5tbjf3ObfGqbl/17ict2/uwOSfr3wmxDE6VdS2GqY/FuzIPe0q0bdhj65zQsvqfArI9MY6+AA==} 1537 + /@esbuild/android-arm@0.20.0: 1538 + resolution: {integrity: sha512-3bMAfInvByLHfJwYPJRlpTeaQA75n8C/QKpEaiS4HrFWFiJlNI0vzq/zCjBrhAYcPyVPG7Eo9dMrcQXuqmNk5g==} 1527 1539 engines: {node: '>=12'} 1528 1540 cpu: [arm] 1529 1541 os: [android] ··· 1549 1561 dev: true 1550 1562 optional: true 1551 1563 1552 - /@esbuild/android-x64@0.19.5: 1553 - resolution: {integrity: sha512-9t+28jHGL7uBdkBjL90QFxe7DVA+KGqWlHCF8ChTKyaKO//VLuoBricQCgwhOjA1/qOczsw843Fy4cbs4H3DVA==} 1564 + /@esbuild/android-x64@0.20.0: 1565 + resolution: {integrity: sha512-uK7wAnlRvjkCPzh8jJ+QejFyrP8ObKuR5cBIsQZ+qbMunwR8sbd8krmMbxTLSrDhiPZaJYKQAU5Y3iMDcZPhyQ==} 1554 1566 engines: {node: '>=12'} 1555 1567 cpu: [x64] 1556 1568 os: [android] ··· 1576 1588 dev: true 1577 1589 optional: true 1578 1590 1579 - /@esbuild/darwin-arm64@0.19.5: 1580 - resolution: {integrity: sha512-mvXGcKqqIqyKoxq26qEDPHJuBYUA5KizJncKOAf9eJQez+L9O+KfvNFu6nl7SCZ/gFb2QPaRqqmG0doSWlgkqw==} 1591 + /@esbuild/darwin-arm64@0.20.0: 1592 + resolution: {integrity: sha512-AjEcivGAlPs3UAcJedMa9qYg9eSfU6FnGHJjT8s346HSKkrcWlYezGE8VaO2xKfvvlZkgAhyvl06OJOxiMgOYQ==} 1581 1593 engines: {node: '>=12'} 1582 1594 cpu: [arm64] 1583 1595 os: [darwin] ··· 1603 1615 dev: true 1604 1616 optional: true 1605 1617 1606 - /@esbuild/darwin-x64@0.19.5: 1607 - resolution: {integrity: sha512-Ly8cn6fGLNet19s0X4unjcniX24I0RqjPv+kurpXabZYSXGM4Pwpmf85WHJN3lAgB8GSth7s5A0r856S+4DyiA==} 1618 + /@esbuild/darwin-x64@0.20.0: 1619 + resolution: {integrity: sha512-bsgTPoyYDnPv8ER0HqnJggXK6RyFy4PH4rtsId0V7Efa90u2+EifxytE9pZnsDgExgkARy24WUQGv9irVbTvIw==} 1608 1620 engines: {node: '>=12'} 1609 1621 cpu: [x64] 1610 1622 os: [darwin] ··· 1630 1642 dev: true 1631 1643 optional: true 1632 1644 1633 - /@esbuild/freebsd-arm64@0.19.5: 1634 - resolution: {integrity: sha512-GGDNnPWTmWE+DMchq1W8Sd0mUkL+APvJg3b11klSGUDvRXh70JqLAO56tubmq1s2cgpVCSKYywEiKBfju8JztQ==} 1645 + /@esbuild/freebsd-arm64@0.20.0: 1646 + resolution: {integrity: sha512-kQ7jYdlKS335mpGbMW5tEe3IrQFIok9r84EM3PXB8qBFJPSc6dpWfrtsC/y1pyrz82xfUIn5ZrnSHQQsd6jebQ==} 1635 1647 engines: {node: '>=12'} 1636 1648 cpu: [arm64] 1637 1649 os: [freebsd] ··· 1657 1669 dev: true 1658 1670 optional: true 1659 1671 1660 - /@esbuild/freebsd-x64@0.19.5: 1661 - resolution: {integrity: sha512-1CCwDHnSSoA0HNwdfoNY0jLfJpd7ygaLAp5EHFos3VWJCRX9DMwWODf96s9TSse39Br7oOTLryRVmBoFwXbuuQ==} 1672 + /@esbuild/freebsd-x64@0.20.0: 1673 + resolution: {integrity: sha512-uG8B0WSepMRsBNVXAQcHf9+Ko/Tr+XqmK7Ptel9HVmnykupXdS4J7ovSQUIi0tQGIndhbqWLaIL/qO/cWhXKyQ==} 1662 1674 engines: {node: '>=12'} 1663 1675 cpu: [x64] 1664 1676 os: [freebsd] ··· 1684 1696 dev: true 1685 1697 optional: true 1686 1698 1687 - /@esbuild/linux-arm64@0.19.5: 1688 - resolution: {integrity: sha512-o3vYippBmSrjjQUCEEiTZ2l+4yC0pVJD/Dl57WfPwwlvFkrxoSO7rmBZFii6kQB3Wrn/6GwJUPLU5t52eq2meA==} 1699 + /@esbuild/linux-arm64@0.20.0: 1700 + resolution: {integrity: sha512-uTtyYAP5veqi2z9b6Gr0NUoNv9F/rOzI8tOD5jKcCvRUn7T60Bb+42NDBCWNhMjkQzI0qqwXkQGo1SY41G52nw==} 1689 1701 engines: {node: '>=12'} 1690 1702 cpu: [arm64] 1691 1703 os: [linux] ··· 1711 1723 dev: true 1712 1724 optional: true 1713 1725 1714 - /@esbuild/linux-arm@0.19.5: 1715 - resolution: {integrity: sha512-lrWXLY/vJBzCPC51QN0HM71uWgIEpGSjSZZADQhq7DKhPcI6NH1IdzjfHkDQws2oNpJKpR13kv7/pFHBbDQDwQ==} 1726 + /@esbuild/linux-arm@0.20.0: 1727 + resolution: {integrity: sha512-2ezuhdiZw8vuHf1HKSf4TIk80naTbP9At7sOqZmdVwvvMyuoDiZB49YZKLsLOfKIr77+I40dWpHVeY5JHpIEIg==} 1716 1728 engines: {node: '>=12'} 1717 1729 cpu: [arm] 1718 1730 os: [linux] ··· 1738 1750 dev: true 1739 1751 optional: true 1740 1752 1741 - /@esbuild/linux-ia32@0.19.5: 1742 - resolution: {integrity: sha512-MkjHXS03AXAkNp1KKkhSKPOCYztRtK+KXDNkBa6P78F8Bw0ynknCSClO/ztGszILZtyO/lVKpa7MolbBZ6oJtQ==} 1753 + /@esbuild/linux-ia32@0.20.0: 1754 + resolution: {integrity: sha512-c88wwtfs8tTffPaoJ+SQn3y+lKtgTzyjkD8NgsyCtCmtoIC8RDL7PrJU05an/e9VuAke6eJqGkoMhJK1RY6z4w==} 1743 1755 engines: {node: '>=12'} 1744 1756 cpu: [ia32] 1745 1757 os: [linux] ··· 1765 1777 dev: true 1766 1778 optional: true 1767 1779 1768 - /@esbuild/linux-loong64@0.19.5: 1769 - resolution: {integrity: sha512-42GwZMm5oYOD/JHqHska3Jg0r+XFb/fdZRX+WjADm3nLWLcIsN27YKtqxzQmGNJgu0AyXg4HtcSK9HuOk3v1Dw==} 1780 + /@esbuild/linux-loong64@0.20.0: 1781 + resolution: {integrity: sha512-lR2rr/128/6svngnVta6JN4gxSXle/yZEZL3o4XZ6esOqhyR4wsKyfu6qXAL04S4S5CgGfG+GYZnjFd4YiG3Aw==} 1770 1782 engines: {node: '>=12'} 1771 1783 cpu: [loong64] 1772 1784 os: [linux] ··· 1792 1804 dev: true 1793 1805 optional: true 1794 1806 1795 - /@esbuild/linux-mips64el@0.19.5: 1796 - resolution: {integrity: sha512-kcjndCSMitUuPJobWCnwQ9lLjiLZUR3QLQmlgaBfMX23UEa7ZOrtufnRds+6WZtIS9HdTXqND4yH8NLoVVIkcg==} 1807 + /@esbuild/linux-mips64el@0.20.0: 1808 + resolution: {integrity: sha512-9Sycc+1uUsDnJCelDf6ZNqgZQoK1mJvFtqf2MUz4ujTxGhvCWw+4chYfDLPepMEvVL9PDwn6HrXad5yOrNzIsQ==} 1797 1809 engines: {node: '>=12'} 1798 1810 cpu: [mips64el] 1799 1811 os: [linux] ··· 1819 1831 dev: true 1820 1832 optional: true 1821 1833 1822 - /@esbuild/linux-ppc64@0.19.5: 1823 - resolution: {integrity: sha512-yJAxJfHVm0ZbsiljbtFFP1BQKLc8kUF6+17tjQ78QjqjAQDnhULWiTA6u0FCDmYT1oOKS9PzZ2z0aBI+Mcyj7Q==} 1834 + /@esbuild/linux-ppc64@0.20.0: 1835 + resolution: {integrity: sha512-CoWSaaAXOZd+CjbUTdXIJE/t7Oz+4g90A3VBCHLbfuc5yUQU/nFDLOzQsN0cdxgXd97lYW/psIIBdjzQIwTBGw==} 1824 1836 engines: {node: '>=12'} 1825 1837 cpu: [ppc64] 1826 1838 os: [linux] ··· 1846 1858 dev: true 1847 1859 optional: true 1848 1860 1849 - /@esbuild/linux-riscv64@0.19.5: 1850 - resolution: {integrity: sha512-5u8cIR/t3gaD6ad3wNt1MNRstAZO+aNyBxu2We8X31bA8XUNyamTVQwLDA1SLoPCUehNCymhBhK3Qim1433Zag==} 1861 + /@esbuild/linux-riscv64@0.20.0: 1862 + resolution: {integrity: sha512-mlb1hg/eYRJUpv8h/x+4ShgoNLL8wgZ64SUr26KwglTYnwAWjkhR2GpoKftDbPOCnodA9t4Y/b68H4J9XmmPzA==} 1851 1863 engines: {node: '>=12'} 1852 1864 cpu: [riscv64] 1853 1865 os: [linux] ··· 1873 1885 dev: true 1874 1886 optional: true 1875 1887 1876 - /@esbuild/linux-s390x@0.19.5: 1877 - resolution: {integrity: sha512-Z6JrMyEw/EmZBD/OFEFpb+gao9xJ59ATsoTNlj39jVBbXqoZm4Xntu6wVmGPB/OATi1uk/DB+yeDPv2E8PqZGw==} 1888 + /@esbuild/linux-s390x@0.20.0: 1889 + resolution: {integrity: sha512-fgf9ubb53xSnOBqyvWEY6ukBNRl1mVX1srPNu06B6mNsNK20JfH6xV6jECzrQ69/VMiTLvHMicQR/PgTOgqJUQ==} 1878 1890 engines: {node: '>=12'} 1879 1891 cpu: [s390x] 1880 1892 os: [linux] ··· 1900 1912 dev: true 1901 1913 optional: true 1902 1914 1903 - /@esbuild/linux-x64@0.19.5: 1904 - resolution: {integrity: sha512-psagl+2RlK1z8zWZOmVdImisMtrUxvwereIdyJTmtmHahJTKb64pAcqoPlx6CewPdvGvUKe2Jw+0Z/0qhSbG1A==} 1915 + /@esbuild/linux-x64@0.20.0: 1916 + resolution: {integrity: sha512-H9Eu6MGse++204XZcYsse1yFHmRXEWgadk2N58O/xd50P9EvFMLJTQLg+lB4E1cF2xhLZU5luSWtGTb0l9UeSg==} 1905 1917 engines: {node: '>=12'} 1906 1918 cpu: [x64] 1907 1919 os: [linux] ··· 1927 1939 dev: true 1928 1940 optional: true 1929 1941 1930 - /@esbuild/netbsd-x64@0.19.5: 1931 - resolution: {integrity: sha512-kL2l+xScnAy/E/3119OggX8SrWyBEcqAh8aOY1gr4gPvw76la2GlD4Ymf832UCVbmuWeTf2adkZDK+h0Z/fB4g==} 1942 + /@esbuild/netbsd-x64@0.20.0: 1943 + resolution: {integrity: sha512-lCT675rTN1v8Fo+RGrE5KjSnfY0x9Og4RN7t7lVrN3vMSjy34/+3na0q7RIfWDAj0e0rCh0OL+P88lu3Rt21MQ==} 1932 1944 engines: {node: '>=12'} 1933 1945 cpu: [x64] 1934 1946 os: [netbsd] ··· 1954 1966 dev: true 1955 1967 optional: true 1956 1968 1957 - /@esbuild/openbsd-x64@0.19.5: 1958 - resolution: {integrity: sha512-sPOfhtzFufQfTBgRnE1DIJjzsXukKSvZxloZbkJDG383q0awVAq600pc1nfqBcl0ice/WN9p4qLc39WhBShRTA==} 1969 + /@esbuild/openbsd-x64@0.20.0: 1970 + resolution: {integrity: sha512-HKoUGXz/TOVXKQ+67NhxyHv+aDSZf44QpWLa3I1lLvAwGq8x1k0T+e2HHSRvxWhfJrFxaaqre1+YyzQ99KixoA==} 1959 1971 engines: {node: '>=12'} 1960 1972 cpu: [x64] 1961 1973 os: [openbsd] ··· 1981 1993 dev: true 1982 1994 optional: true 1983 1995 1984 - /@esbuild/sunos-x64@0.19.5: 1985 - resolution: {integrity: sha512-dGZkBXaafuKLpDSjKcB0ax0FL36YXCvJNnztjKV+6CO82tTYVDSH2lifitJ29jxRMoUhgkg9a+VA/B03WK5lcg==} 1996 + /@esbuild/sunos-x64@0.20.0: 1997 + resolution: {integrity: sha512-GDwAqgHQm1mVoPppGsoq4WJwT3vhnz/2N62CzhvApFD1eJyTroob30FPpOZabN+FgCjhG+AgcZyOPIkR8dfD7g==} 1986 1998 engines: {node: '>=12'} 1987 1999 cpu: [x64] 1988 2000 os: [sunos] ··· 2008 2020 dev: true 2009 2021 optional: true 2010 2022 2011 - /@esbuild/win32-arm64@0.19.5: 2012 - resolution: {integrity: sha512-dWVjD9y03ilhdRQ6Xig1NWNgfLtf2o/STKTS+eZuF90fI2BhbwD6WlaiCGKptlqXlURVB5AUOxUj09LuwKGDTg==} 2023 + /@esbuild/win32-arm64@0.20.0: 2024 + resolution: {integrity: sha512-0vYsP8aC4TvMlOQYozoksiaxjlvUcQrac+muDqj1Fxy6jh9l9CZJzj7zmh8JGfiV49cYLTorFLxg7593pGldwQ==} 2013 2025 engines: {node: '>=12'} 2014 2026 cpu: [arm64] 2015 2027 os: [win32] ··· 2035 2047 dev: true 2036 2048 optional: true 2037 2049 2038 - /@esbuild/win32-ia32@0.19.5: 2039 - resolution: {integrity: sha512-4liggWIA4oDgUxqpZwrDhmEfAH4d0iljanDOK7AnVU89T6CzHon/ony8C5LeOdfgx60x5cnQJFZwEydVlYx4iw==} 2050 + /@esbuild/win32-ia32@0.20.0: 2051 + resolution: {integrity: sha512-p98u4rIgfh4gdpV00IqknBD5pC84LCub+4a3MO+zjqvU5MVXOc3hqR2UgT2jI2nh3h8s9EQxmOsVI3tyzv1iFg==} 2040 2052 engines: {node: '>=12'} 2041 2053 cpu: [ia32] 2042 2054 os: [win32] ··· 2062 2074 dev: true 2063 2075 optional: true 2064 2076 2065 - /@esbuild/win32-x64@0.19.5: 2066 - resolution: {integrity: sha512-czTrygUsB/jlM8qEW5MD8bgYU2Xg14lo6kBDXW6HdxKjh8M5PzETGiSHaz9MtbXBYDloHNUAUW2tMiKW4KM9Mw==} 2077 + /@esbuild/win32-x64@0.20.0: 2078 + resolution: {integrity: sha512-NgJnesu1RtWihtTtXGFMU5YSE6JyyHPMxCwBZK7a6/8d31GuSo9l0Ss7w1Jw5QnKUawG6UEehs883kcXf5fYwg==} 2067 2079 engines: {node: '>=12'} 2068 2080 cpu: [x64] 2069 2081 os: [win32] ··· 2353 2365 chalk: 4.1.2 2354 2366 dev: false 2355 2367 2356 - /@jitsu/js@1.3.0(@types/dlv@1.1.3): 2368 + /@jitsu/js@1.3.0(@types/dlv@1.1.4): 2357 2369 resolution: {integrity: sha512-7k3dM84xqf6Ivaama8VWwGYOcmGI/WWQTUHmHNXao+M/tneU6L72SCNlUE587vgtaxvoN2oROo4sBGO+sTh4ZQ==} 2358 2370 dependencies: 2359 - analytics: 0.8.9(@types/dlv@1.1.3) 2371 + analytics: 0.8.9(@types/dlv@1.1.4) 2360 2372 transitivePeerDependencies: 2361 2373 - '@types/dlv' 2362 2374 dev: false ··· 2535 2547 unist-util-visit: 1.4.1 2536 2548 dev: false 2537 2549 2538 - /@mdx-js/esbuild@2.3.0(esbuild@0.19.5): 2550 + /@mdx-js/esbuild@2.3.0(esbuild@0.20.0): 2539 2551 resolution: {integrity: sha512-r/vsqsM0E+U4Wr0DK+0EfmABE/eg+8ITW4DjvYdh3ve/tK2safaqHArNnaqbOk1DjYGrhxtoXoGaM3BY8fGBTA==} 2540 2552 peerDependencies: 2541 2553 esbuild: '>=0.11.0' 2542 2554 dependencies: 2543 2555 '@mdx-js/mdx': 2.3.0 2544 - esbuild: 0.19.5 2556 + esbuild: 0.20.0 2545 2557 node-fetch: 3.3.2 2546 2558 vfile: 5.3.7 2547 2559 transitivePeerDependencies: ··· 5064 5076 resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} 5065 5077 dev: true 5066 5078 5067 - /@turbo/gen@1.10.12(@types/node@20.8.0)(typescript@5.2.2): 5079 + /@turbo/gen@1.10.12(@types/node@20.11.17)(typescript@5.2.2): 5068 5080 resolution: {integrity: sha512-noop5+3MBFsgPQ7O2vQpS6YYiah+ZrOioa4cDDpZceUVsKVXvUHFmC2nEVyKSJZhO/8SLvbDE/esB/MGw5b2tw==} 5069 5081 hasBin: true 5070 5082 dependencies: ··· 5076 5088 node-plop: 0.26.3 5077 5089 proxy-agent: 6.3.1 5078 5090 semver: 7.5.4 5079 - ts-node: 10.9.1(@types/node@20.8.0)(typescript@5.2.2) 5091 + ts-node: 10.9.1(@types/node@20.11.17)(typescript@5.2.2) 5080 5092 update-check: 1.5.4 5081 5093 validate-npm-package-name: 5.0.0 5082 5094 transitivePeerDependencies: ··· 5166 5178 dependencies: 5167 5179 '@types/ms': 0.7.33 5168 5180 5169 - /@types/dlv@1.1.3: 5170 - resolution: {integrity: sha512-u6JdAm9wje4n7vqkK9F6nQI18xSO+MVfXz6RMgN4acJmSZMlV7ZRrEQg4d1wmiQ2txg2xArpc68RS6p4JecVNg==} 5181 + /@types/dlv@1.1.4: 5182 + resolution: {integrity: sha512-m8KmImw4Jt+4rIgupwfivrWEOnj1LzkmKkqbh075uG13eTQ1ZxHWT6T0vIdSQhLIjQCiR0n0lZdtyDOPO1x2Mw==} 5171 5183 dev: false 5172 5184 5173 5185 /@types/eslint@8.44.3: ··· 5324 5336 resolution: {integrity: sha512-vmYJF0REqDyyU0gviezF/KHq/fYaUbFhkcNbQCuPGFQj6VTbXuHZoxs/Y7mutWe73C8AC6l9fFu8mSYiBAqkGA==} 5325 5337 dev: false 5326 5338 5339 + /@types/node@20.11.17: 5340 + resolution: {integrity: sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==} 5341 + dependencies: 5342 + undici-types: 5.26.5 5343 + dev: true 5344 + 5327 5345 /@types/node@20.8.0: 5328 5346 resolution: {integrity: sha512-LzcWltT83s1bthcvjBmiBvGJiiUe84NWRHkw+ZV6Fr41z2FbIzvc815dk2nQ3RAKMuN2fkenM/z3Xv2QzEpYxQ==} 5329 5347 ··· 5717 5735 json-schema-traverse: 0.4.1 5718 5736 uri-js: 4.4.1 5719 5737 5720 - /analytics-utils@1.0.12(@types/dlv@1.1.3): 5738 + /analytics-utils@1.0.12(@types/dlv@1.1.4): 5721 5739 resolution: {integrity: sha512-WvV2YWgsnXLxaY0QYux0crpBAg/0JA763NmbMVz22jKhMPo7dpTBet8G2IlF7ixTjLDzGlkHk1ZaKqqQmjJ+4w==} 5722 5740 peerDependencies: 5723 5741 '@types/dlv': ^1.0.0 5724 5742 dependencies: 5725 5743 '@analytics/type-utils': 0.6.2 5726 - '@types/dlv': 1.1.3 5744 + '@types/dlv': 1.1.4 5727 5745 dlv: 1.1.3 5728 5746 dev: false 5729 5747 5730 - /analytics@0.8.9(@types/dlv@1.1.3): 5748 + /analytics@0.8.9(@types/dlv@1.1.4): 5731 5749 resolution: {integrity: sha512-oTbUzQpncMTslakqfK70GgB6bopk5hY+uuekwnadMkDyqNLgcD02KRzteTnO7q5Ko6wDECVtT8xi/6OuAMZykA==} 5732 5750 dependencies: 5733 - '@analytics/core': 0.12.7(@types/dlv@1.1.3) 5751 + '@analytics/core': 0.12.7(@types/dlv@1.1.4) 5734 5752 '@analytics/storage-utils': 0.4.2 5735 5753 transitivePeerDependencies: 5736 5754 - '@types/dlv' ··· 6452 6470 engines: {node: '>= 0.6'} 6453 6471 dev: false 6454 6472 6455 - /contentlayer@0.3.4(esbuild@0.19.5): 6473 + /contentlayer@0.3.4(esbuild@0.20.0): 6456 6474 resolution: {integrity: sha512-FYDdTUFaN4yqep0waswrhcXjmMJnPD5iXDTtxcUCGdklfuIrXM2xLx51xl748cHmGA6IsC+27YZFxU6Ym13QIA==} 6457 6475 engines: {node: '>=14.18'} 6458 6476 hasBin: true 6459 6477 requiresBuild: true 6460 6478 dependencies: 6461 - '@contentlayer/cli': 0.3.4(esbuild@0.19.5) 6462 - '@contentlayer/client': 0.3.4(esbuild@0.19.5) 6463 - '@contentlayer/core': 0.3.4(esbuild@0.19.5) 6464 - '@contentlayer/source-files': 0.3.4(esbuild@0.19.5) 6465 - '@contentlayer/source-remote-files': 0.3.4(esbuild@0.19.5) 6479 + '@contentlayer/cli': 0.3.4(esbuild@0.20.0) 6480 + '@contentlayer/client': 0.3.4(esbuild@0.20.0) 6481 + '@contentlayer/core': 0.3.4(esbuild@0.20.0) 6482 + '@contentlayer/source-files': 0.3.4(esbuild@0.20.0) 6483 + '@contentlayer/source-remote-files': 0.3.4(esbuild@0.20.0) 6466 6484 '@contentlayer/utils': 0.3.4 6467 6485 transitivePeerDependencies: 6468 6486 - '@effect-ts/otel-node' ··· 7302 7320 '@esbuild/win32-x64': 0.18.20 7303 7321 dev: true 7304 7322 7305 - /esbuild@0.19.5: 7306 - resolution: {integrity: sha512-bUxalY7b1g8vNhQKdB24QDmHeY4V4tw/s6Ak5z+jJX9laP5MoQseTOMemAr0gxssjNcH0MCViG8ONI2kksvfFQ==} 7323 + /esbuild@0.20.0: 7324 + resolution: {integrity: sha512-6iwE3Y2RVYCME1jLpBqq7LQWK3MW6vjV2bZy6gt/WrqkY+WE74Spyc0ThAOYpMtITvnjX09CrC6ym7A/m9mebA==} 7307 7325 engines: {node: '>=12'} 7308 7326 hasBin: true 7309 7327 requiresBuild: true 7310 7328 optionalDependencies: 7311 - '@esbuild/android-arm': 0.19.5 7312 - '@esbuild/android-arm64': 0.19.5 7313 - '@esbuild/android-x64': 0.19.5 7314 - '@esbuild/darwin-arm64': 0.19.5 7315 - '@esbuild/darwin-x64': 0.19.5 7316 - '@esbuild/freebsd-arm64': 0.19.5 7317 - '@esbuild/freebsd-x64': 0.19.5 7318 - '@esbuild/linux-arm': 0.19.5 7319 - '@esbuild/linux-arm64': 0.19.5 7320 - '@esbuild/linux-ia32': 0.19.5 7321 - '@esbuild/linux-loong64': 0.19.5 7322 - '@esbuild/linux-mips64el': 0.19.5 7323 - '@esbuild/linux-ppc64': 0.19.5 7324 - '@esbuild/linux-riscv64': 0.19.5 7325 - '@esbuild/linux-s390x': 0.19.5 7326 - '@esbuild/linux-x64': 0.19.5 7327 - '@esbuild/netbsd-x64': 0.19.5 7328 - '@esbuild/openbsd-x64': 0.19.5 7329 - '@esbuild/sunos-x64': 0.19.5 7330 - '@esbuild/win32-arm64': 0.19.5 7331 - '@esbuild/win32-ia32': 0.19.5 7332 - '@esbuild/win32-x64': 0.19.5 7329 + '@esbuild/aix-ppc64': 0.20.0 7330 + '@esbuild/android-arm': 0.20.0 7331 + '@esbuild/android-arm64': 0.20.0 7332 + '@esbuild/android-x64': 0.20.0 7333 + '@esbuild/darwin-arm64': 0.20.0 7334 + '@esbuild/darwin-x64': 0.20.0 7335 + '@esbuild/freebsd-arm64': 0.20.0 7336 + '@esbuild/freebsd-x64': 0.20.0 7337 + '@esbuild/linux-arm': 0.20.0 7338 + '@esbuild/linux-arm64': 0.20.0 7339 + '@esbuild/linux-ia32': 0.20.0 7340 + '@esbuild/linux-loong64': 0.20.0 7341 + '@esbuild/linux-mips64el': 0.20.0 7342 + '@esbuild/linux-ppc64': 0.20.0 7343 + '@esbuild/linux-riscv64': 0.20.0 7344 + '@esbuild/linux-s390x': 0.20.0 7345 + '@esbuild/linux-x64': 0.20.0 7346 + '@esbuild/netbsd-x64': 0.20.0 7347 + '@esbuild/openbsd-x64': 0.20.0 7348 + '@esbuild/sunos-x64': 0.20.0 7349 + '@esbuild/win32-arm64': 0.20.0 7350 + '@esbuild/win32-ia32': 0.20.0 7351 + '@esbuild/win32-x64': 0.20.0 7333 7352 dev: false 7334 7353 7335 7354 /escalade@3.1.1: ··· 9771 9790 dependencies: 9772 9791 '@types/mdast': 3.0.14 9773 9792 9774 - /mdx-bundler@9.2.1(esbuild@0.19.5): 9793 + /mdx-bundler@9.2.1(esbuild@0.20.0): 9775 9794 resolution: {integrity: sha512-hWEEip1KU9MCNqeH2rqwzAZ1pdqPPbfkx9OTJjADqGPQz4t9BO85fhI7AP9gVYrpmfArf9/xJZUN0yBErg/G/Q==} 9776 9795 engines: {node: '>=14', npm: '>=6'} 9777 9796 peerDependencies: 9778 9797 esbuild: 0.* 9779 9798 dependencies: 9780 9799 '@babel/runtime': 7.23.2 9781 - '@esbuild-plugins/node-resolve': 0.1.4(esbuild@0.19.5) 9800 + '@esbuild-plugins/node-resolve': 0.1.4(esbuild@0.20.0) 9782 9801 '@fal-works/esbuild-plugin-global-externals': 2.1.2 9783 - '@mdx-js/esbuild': 2.3.0(esbuild@0.19.5) 9784 - esbuild: 0.19.5 9802 + '@mdx-js/esbuild': 2.3.0(esbuild@0.20.0) 9803 + esbuild: 0.20.0 9785 9804 gray-matter: 4.0.3 9786 9805 remark-frontmatter: 4.0.1 9787 9806 remark-mdx-frontmatter: 1.1.1 ··· 10293 10312 engines: {node: '>= 0.4.0'} 10294 10313 dev: true 10295 10314 10296 - /next-contentlayer@0.3.4(contentlayer@0.3.4)(esbuild@0.19.5)(next@14.1.0)(react-dom@18.2.0)(react@18.2.0): 10315 + /next-contentlayer@0.3.4(contentlayer@0.3.4)(esbuild@0.20.0)(next@14.1.0)(react-dom@18.2.0)(react@18.2.0): 10297 10316 resolution: {integrity: sha512-UtUCwgAl159KwfhNaOwyiI7Lg6sdioyKMeh+E7jxx0CJ29JuXGxBEYmCI6+72NxFGIFZKx8lvttbbQhbnYWYSw==} 10298 10317 peerDependencies: 10299 10318 contentlayer: 0.3.4 ··· 10301 10320 react: '*' 10302 10321 react-dom: '*' 10303 10322 dependencies: 10304 - '@contentlayer/core': 0.3.4(esbuild@0.19.5) 10323 + '@contentlayer/core': 0.3.4(esbuild@0.20.0) 10305 10324 '@contentlayer/utils': 0.3.4 10306 - contentlayer: 0.3.4(esbuild@0.19.5) 10325 + contentlayer: 0.3.4(esbuild@0.20.0) 10307 10326 next: 14.1.0(@babel/core@7.23.2)(@opentelemetry/api@1.4.1)(react-dom@18.2.0)(react@18.2.0) 10308 10327 react: 18.2.0 10309 10328 react-dom: 18.2.0(react@18.2.0) ··· 12594 12613 /ts-interface-checker@0.1.13: 12595 12614 resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} 12596 12615 12597 - /ts-node@10.9.1(@types/node@20.8.0)(typescript@5.2.2): 12616 + /ts-node@10.9.1(@types/node@20.11.17)(typescript@5.2.2): 12598 12617 resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} 12599 12618 hasBin: true 12600 12619 peerDependencies: ··· 12613 12632 '@tsconfig/node12': 1.0.11 12614 12633 '@tsconfig/node14': 1.0.3 12615 12634 '@tsconfig/node16': 1.0.4 12616 - '@types/node': 20.8.0 12635 + '@types/node': 20.11.17 12617 12636 acorn: 8.10.0 12618 12637 acorn-walk: 8.2.0 12619 12638 arg: 4.1.0 ··· 12889 12908 has-symbols: 1.0.3 12890 12909 which-boxed-primitive: 1.0.2 12891 12910 dev: false 12911 + 12912 + /undici-types@5.26.5: 12913 + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} 12914 + dev: true 12892 12915 12893 12916 /undici@5.25.1: 12894 12917 resolution: {integrity: sha512-nTw6b2G2OqP6btYPyghCgV4hSwjJlL/78FMJatVLCa3otj6PCOQSt6dVtYt82OtNqFz8XsnJ+vsXLADPXjPhqw==}