Openstatus www.openstatus.dev

feat: clean up (#186)

* feat: clean up

* chore: update footer

* fix: add missing docs

authored by

Maximilian Kaske and committed by
GitHub
ff49603c 5682dc59

+318 -239
+1
apps/web/src/app/app/(auth)/sign-in/[[...sign-in]]/page.tsx
··· 4 4 export default function Page() { 5 5 const { userId } = auth(); 6 6 7 + // TODO: we can improve the UX here. If user is logged in, (s)he will see the screen for a quick moment 7 8 if (userId) { 8 9 redirect("/app"); 9 10 }
+1
apps/web/src/app/app/(auth)/sign-up/[[...sign-up]]/page.tsx
··· 4 4 export default function Page() { 5 5 const { userId } = auth(); 6 6 7 + // TODO: we can improve the UX here. If user is logged in, (s)he will see the screen for a quick moment 7 8 if (userId) { 8 9 redirect("/app"); 9 10 }
+1 -2
apps/web/src/app/app/(dashboard)/[workspaceSlug]/layout.tsx
··· 4 4 import { AppHeader } from "@/components/layout/app-header"; 5 5 import { AppMenu } from "@/components/layout/app-menu"; 6 6 import { AppSidebar } from "@/components/layout/app-sidebar"; 7 - import { Footer } from "@/components/layout/footer"; 8 7 8 + // TODO: make the container min-h-screen and the footer below! 9 9 export default function AppLayout({ children }: { children: React.ReactNode }) { 10 10 return ( 11 11 <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"> ··· 24 24 </Shell> 25 25 </main> 26 26 </div> 27 - <Footer /> 28 27 </div> 29 28 ); 30 29 }
+18
apps/web/src/app/app/(dashboard)/[workspaceSlug]/monitors/loading.tsx
··· 1 + import { Container } from "@/components/dashboard/container"; 2 + import { Header } from "@/components/dashboard/header"; 3 + import { Skeleton } from "@/components/ui/skeleton"; 4 + 5 + export default function Loading() { 6 + return ( 7 + <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 8 + <div className="col-span-full flex w-full justify-between"> 9 + <Header.Skeleton> 10 + <Skeleton className="h-9 w-20" /> 11 + </Header.Skeleton> 12 + </div> 13 + <Container.Skeleton /> 14 + <Container.Skeleton /> 15 + <Container.Skeleton /> 16 + </div> 17 + ); 18 + }
+20 -20
apps/web/src/app/app/(dashboard)/[workspaceSlug]/monitors/page.tsx
··· 42 42 key={index} 43 43 title={monitor.name} 44 44 description={monitor.description} 45 - actions={ 46 - <ActionButton {...monitor} workspaceSlug={params.workspaceSlug} /> 47 - } 45 + actions={[ 46 + <Badge 47 + key="status-badge" 48 + variant={monitor.active ? "default" : "outline"} 49 + className="capitalize" 50 + > 51 + {monitor.active ? "active" : "inactive"} 52 + <span 53 + className={cn( 54 + "ml-1 h-1.5 w-1.5 rounded-full", 55 + monitor.active ? "bg-green-500" : "bg-red-500", 56 + )} 57 + /> 58 + </Badge>, 59 + <ActionButton 60 + key="action-button" 61 + {...monitor} 62 + workspaceSlug={params.workspaceSlug} 63 + />, 64 + ]} 48 65 > 49 66 <dl className="[&_dt]:text-muted-foreground grid gap-2 [&>*]:text-sm [&_dt]:font-light"> 50 - <div className="flex min-w-0 items-center justify-between gap-3"> 51 - <dt>Status</dt> 52 - <dd> 53 - <Badge 54 - variant={monitor.active ? "default" : "outline"} 55 - className="capitalize" 56 - > 57 - {monitor.active ? "active" : "inactive"} 58 - <span 59 - className={cn( 60 - "ml-1 h-1.5 w-1.5 rounded-full", 61 - monitor.active ? "bg-green-500" : "bg-red-500", 62 - )} 63 - /> 64 - </Badge> 65 - </dd> 66 - </div> 67 67 <div className="flex min-w-0 items-center justify-between gap-3"> 68 68 <dt>Frequency</dt> 69 69 <dd className="font-mono">{monitor.periodicity}</dd>
+39
apps/web/src/app/app/(dashboard)/[workspaceSlug]/settings/_components/customer-portal-button.tsx
··· 1 + "use client"; 2 + 3 + import { useTransition } from "react"; 4 + import { useRouter } from "next/navigation"; 5 + 6 + import { LoadingAnimation } from "@/components/loading-animation"; 7 + import { Button } from "@/components/ui/button"; 8 + import { api } from "@/trpc/client"; 9 + 10 + interface Props { 11 + workspaceSlug: string; 12 + } 13 + 14 + export function CustomerPortalButton({ workspaceSlug }: Props) { 15 + const router = useRouter(); 16 + const [isPending, startTransition] = useTransition(); 17 + 18 + const getUserCustomerPortal = () => { 19 + startTransition(async () => { 20 + const url = await api.stripeRouter.getUserCustomerPortal.mutate({ 21 + workspaceSlug, 22 + }); 23 + if (!url) return; 24 + router.push(url); 25 + return; 26 + }); 27 + }; 28 + 29 + return ( 30 + <Button 31 + size="sm" 32 + variant="outline" 33 + onClick={getUserCustomerPortal} 34 + disabled={isPending} 35 + > 36 + {isPending ? <LoadingAnimation /> : "Customer Portal"} 37 + </Button> 38 + ); 39 + }
+19 -92
apps/web/src/app/app/(dashboard)/[workspaceSlug]/settings/_components/plan.tsx
··· 2 2 3 3 import { useTransition } from "react"; 4 4 import { useRouter } from "next/navigation"; 5 - import { Check } from "lucide-react"; 6 5 import type { z } from "zod"; 7 6 8 7 import type { selectWorkspaceSchema } from "@openstatus/db/src/schema/workspace"; 9 8 10 9 import { Shell } from "@/components/dashboard/shell"; 11 - import { Button } from "@/components/ui/button"; 10 + import { Plan } from "@/components/marketing/plans"; 11 + import type { PlanProps } from "@/config/plans"; 12 + import { plansConfig } from "@/config/plans"; 12 13 import { getStripe } from "@/lib/stripe/client"; 13 - import { cn } from "@/lib/utils"; 14 14 import { api } from "@/trpc/client"; 15 15 16 - interface Plan { 17 - title: string; 18 - description: string; 19 - cost: number | string; 20 - features: string[]; 21 - action: { 22 - text: string; 23 - onClick: () => void; 24 - }; 25 - loading?: boolean; 26 - } 27 - 28 - interface Props extends Plan { 29 - className?: string; 30 - } 31 16 export const SettingsPlan = ({ 32 17 workspaceSlug, 33 18 workspaceData, ··· 36 21 workspaceData: z.infer<typeof selectWorkspaceSchema>; 37 22 }) => { 38 23 const router = useRouter(); 39 - 40 24 const [isPending, startTransition] = useTransition(); 25 + const [isPortalPending, startPortalTransition] = useTransition(); 41 26 42 27 const getCheckoutSession = () => { 43 28 startTransition(async () => { 44 29 const result = await api.stripeRouter.getCheckoutSession.mutate({ 45 - workspaceSlug: workspaceSlug, 30 + workspaceSlug, 46 31 }); 47 32 if (!result) return; 48 33 ··· 50 35 stripe?.redirectToCheckout({ sessionId: result.id }); 51 36 }); 52 37 }; 53 - const getUserCustomerPortal = async () => { 54 - const url = await api.stripeRouter.getUserCustomerPortal.mutate({ 55 - workspaceSlug: workspaceSlug, 38 + 39 + const getUserCustomerPortal = () => { 40 + startPortalTransition(async () => { 41 + const url = await api.stripeRouter.getUserCustomerPortal.mutate({ 42 + workspaceSlug, 43 + }); 44 + if (!url) return; 45 + router.push(url); 46 + return; 56 47 }); 57 - if (!url) return; 58 - router.push(url); 59 - return; 60 48 }; 61 - getUserCustomerPortal; 62 - const plans: Record<"free" | "pro", Plan> = { 49 + 50 + const plans: Record<"free" | "pro", PlanProps> = { 63 51 free: { 64 - title: "Free", 65 - description: "Get started now and upgrade once reaching the limits.", 66 - cost: 0, 67 - features: [ 68 - "5 monitors", 69 - "1 status page", 70 - "subdomain", 71 - "10m, 30m, 1h checks", 72 - ], 52 + ...plansConfig.free, 53 + loading: isPortalPending, 73 54 action: { 74 55 text: workspaceData?.plan === "free" ? "Current plan" : "Downgrade", 75 56 onClick: async () => { ··· 78 59 }, 79 60 }, 80 61 pro: { 81 - title: "Pro", 82 - description: "Scale and build monitors for all your services.", 83 - cost: 29, 84 - features: [ 85 - "20 monitors", 86 - "5 status page", 87 - "custom domain", 88 - "1m, 5m, 10m, 30m, 1h checks", 89 - "5 team members", 90 - ], 62 + ...plansConfig.pro, 91 63 loading: isPending, 92 64 action: { 93 65 text: workspaceData?.plan === "free" ? "Upgrade" : "Current plan", ··· 110 82 </Shell> 111 83 ); 112 84 }; 113 - 114 - export function Plan({ 115 - title, 116 - description, 117 - cost, 118 - features, 119 - action, 120 - loading, 121 - className, 122 - }: Props) { 123 - return ( 124 - <div key={title} className={cn("flex w-full flex-col", className)}> 125 - <div className="flex-1"> 126 - <div className="flex items-end justify-between gap-4"> 127 - <div> 128 - <p className="font-cal mb-2 text-xl">{title}</p> 129 - <p className="text-muted-foreground">{description}</p> 130 - </div> 131 - <p className="shrink-0"> 132 - <span className="font-cal text-2xl">{cost}</span> 133 - {typeof cost === "number" ? ( 134 - <span className="text-muted-foreground font-light">/month</span> 135 - ) : null} 136 - </p> 137 - </div> 138 - <ul className="border-border/50 grid divide-y py-2"> 139 - {features.map((item) => ( 140 - <li 141 - key={item} 142 - className="text-muted-foreground inline-flex items-center py-2 text-sm" 143 - > 144 - <Check className="mr-2 h-4 w-4 text-green-500" /> 145 - {item} 146 - </li> 147 - ))} 148 - </ul> 149 - </div> 150 - <div> 151 - <Button size="sm" onClick={action.onClick} disabled={loading}> 152 - {action.text} 153 - </Button> 154 - </div> 155 - </div> 156 - ); 157 - }
+14
apps/web/src/app/app/(dashboard)/[workspaceSlug]/settings/loading.tsx
··· 1 + import { Header } from "@/components/dashboard/header"; 2 + import { Skeleton } from "@/components/ui/skeleton"; 3 + 4 + export default function Loading() { 5 + return ( 6 + <div className="grid gap-6 md:grid-cols-1 md:gap-8"> 7 + <div className="col-span-full flex w-full justify-between"> 8 + <Header.Skeleton /> 9 + </div> 10 + <Skeleton className="h-4 w-24" /> 11 + <Skeleton className="h-72 w-full" /> 12 + </div> 13 + ); 14 + }
+9 -4
apps/web/src/app/app/(dashboard)/[workspaceSlug]/settings/page.tsx
··· 3 3 import { Header } from "@/components/dashboard/header"; 4 4 import { Badge } from "@/components/ui/badge"; 5 5 import { api } from "@/trpc/server"; 6 + import { CustomerPortalButton } from "./_components/customer-portal-button"; 6 7 import { SettingsPlan } from "./_components/plan"; 7 8 8 9 export default async function Page({ ··· 22 23 <Header 23 24 title="Settings" 24 25 description="Your OpenStatus workspace settings." 25 - ></Header> 26 - <div className="col-span-full"> 26 + /> 27 + <div className="col-span-full grid gap-4"> 27 28 <h3 className="text-lg font-medium">Plans</h3> 28 - 29 - <div className="mt-4 flex items-center space-x-2 text-sm"> 29 + <div className="text-muted-foreground flex items-center space-x-2 text-sm"> 30 30 Your current plan is{" "} 31 31 <Badge className="ml-2">{data?.plan || "free"}</Badge> 32 32 </div> 33 + {data?.plan === "pro" ? ( 34 + <div> 35 + <CustomerPortalButton workspaceSlug={params.workspaceSlug} /> 36 + </div> 37 + ) : null} 33 38 <SettingsPlan 34 39 workspaceSlug={params.workspaceSlug} 35 40 workspaceData={data}
+3 -1
apps/web/src/components/dashboard/container.tsx
··· 36 36 <CardDescription>{description}</CardDescription> 37 37 ) : null} 38 38 </div> 39 - {actions ? <div className="flex gap-2">{actions}</div> : null} 39 + {actions ? ( 40 + <div className="flex items-center gap-2">{actions}</div> 41 + ) : null} 40 42 </CardHeader> 41 43 {/* potentially `asChild` */} 42 44 <CardContent>{children}</CardContent>
+30
apps/web/src/components/layout/app-footer.tsx
··· 1 + import Link from "next/link"; 2 + 3 + import { Shell } from "../dashboard/shell"; 4 + 5 + export function AppFooter() { 6 + return ( 7 + <footer className="w-full"> 8 + <Shell className="flex items-center justify-between"> 9 + <div className="text-muted-foreground text-xs font-light"> 10 + All rights reserved &copy; 11 + </div> 12 + <div className="text-right text-xs"> 13 + <Link 14 + href="/legal/terms" 15 + className="text-foreground underline underline-offset-4 hover:no-underline" 16 + > 17 + Terms 18 + </Link> 19 + <span className="text-muted-foreground/70 mx-1">&bull;</span> 20 + <Link 21 + href="/legal/privacy" 22 + className="text-foreground underline underline-offset-4 hover:no-underline" 23 + > 24 + Privacy 25 + </Link> 26 + </div> 27 + </Shell> 28 + </footer> 29 + ); 30 + }
-47
apps/web/src/components/layout/footer.tsx
··· 1 - import Link from "next/link"; 2 - 3 - export function Footer() { 4 - return ( 5 - <footer className="text-muted-foreground mx-auto grid gap-4 text-sm"> 6 - <p className="border-border rounded-full border px-4 py-2 text-center backdrop-blur-[2px]"> 7 - A collaboration between{" "} 8 - <a 9 - href="https://twitter.com/mxkaske" 10 - target="_blank" 11 - rel="noreferrer" 12 - className="text-foreground underline underline-offset-4 hover:no-underline" 13 - > 14 - @mxkaske 15 - </a>{" "} 16 - and{" "} 17 - <a 18 - href="https://twitter.com/thibaultleouay" 19 - target="_blank" 20 - rel="noreferrer" 21 - className="text-foreground underline underline-offset-4 hover:no-underline" 22 - > 23 - @thibaultleouay 24 - </a> 25 - <span className="text-muted-foreground/70 mx-1">&bull;</span> 26 - See on{" "} 27 - <a 28 - href="https://github.com/openstatushq/openstatus" 29 - target="_blank" 30 - rel="noreferrer" 31 - className="text-foreground underline underline-offset-4 hover:no-underline" 32 - > 33 - GitHub 34 - </a> 35 - </p> 36 - <div className="flex items-center justify-center space-x-2"> 37 - <Link href="/legal/terms" className="text-xs"> 38 - Terms 39 - </Link>{" "} 40 - <span>{"/"}</span> 41 - <Link href="/legal/privacy" className="text-xs"> 42 - Privacy 43 - </Link> 44 - </div> 45 - </footer> 46 - ); 47 - }
+64
apps/web/src/components/layout/marketing-footer.tsx
··· 1 + import Link from "next/link"; 2 + import { ArrowUpRight } from "lucide-react"; 3 + 4 + import { cn } from "@/lib/utils"; 5 + import { Shell } from "../dashboard/shell"; 6 + 7 + interface Props { 8 + className?: string; 9 + } 10 + 11 + export function MarketingFooter({ className }: Props) { 12 + return ( 13 + <footer className={cn("w-full", className)}> 14 + <Shell className="grid grid-cols-2 gap-6 md:grid-cols-3"> 15 + <div className="flex flex-col gap-3 text-sm"> 16 + <p className="text-muted-foreground font-semibold">Twitter</p> 17 + <FooterLink 18 + href="https://twitter.com/thibaultleouay" 19 + label="@thibaultleouay" 20 + /> 21 + <FooterLink href="https://twitter.com/mxkaske" label="@mxkaske" /> 22 + </div> 23 + <div className="flex flex-col gap-3 text-sm"> 24 + <p className="text-muted-foreground font-semibold">Community</p> 25 + <FooterLink 26 + href="https://github.com/openstatushq/openstatus" 27 + label="GitHub" 28 + /> 29 + <FooterLink href="https://discord.gg/dHD4JtSfsn" label="Discord" /> 30 + </div> 31 + <div className="flex flex-col gap-3 text-sm"> 32 + <p className="text-muted-foreground font-semibold">More</p> 33 + <FooterLink href="https://docs.openstatus.dev" label="Docs" /> 34 + <FooterLink href="/legal/terms" label="Terms" /> 35 + <FooterLink href="/legal/privacy" label="Privacy" /> 36 + </div> 37 + </Shell> 38 + </footer> 39 + ); 40 + } 41 + 42 + function FooterLink({ href, label }: Record<"href" | "label", string>) { 43 + const isExternal = href.startsWith("http"); 44 + 45 + const LinkSlot = isExternal ? "a" : Link; 46 + 47 + const externalProps = isExternal 48 + ? { 49 + target: "_blank", 50 + rel: "noreferrer", 51 + } 52 + : {}; 53 + 54 + return ( 55 + <LinkSlot 56 + className="text-foreground inline-flex items-center underline underline-offset-4 hover:no-underline" 57 + href={href} 58 + {...{ externalProps }} 59 + > 60 + {label} 61 + {isExternal ? <ArrowUpRight className="ml-1 h-4 w-4" /> : null} 62 + </LinkSlot> 63 + ); 64 + }
+2 -2
apps/web/src/components/layout/marketing-layout.tsx
··· 1 - import { Footer } from "./footer"; 1 + import { MarketingFooter } from "./marketing-footer"; 2 2 import { MarketingHeader } from "./marketing-header"; 3 3 4 4 export function MarketingLayout({ children }: { children: React.ReactNode }) { ··· 8 8 <div className="mx-auto flex w-full max-w-[calc(65ch+8rem)] flex-1 flex-col items-start justify-center"> 9 9 {children} 10 10 </div> 11 - <Footer /> 11 + <MarketingFooter className="mx-auto w-full max-w-[calc(65ch+8rem)]" /> 12 12 </main> 13 13 ); 14 14 }
+34 -71
apps/web/src/components/marketing/plans.tsx
··· 1 1 import Link from "next/link"; 2 2 import { Check } from "lucide-react"; 3 3 4 + import type { PlanProps } from "@/config/plans"; 5 + import { plansConfig } from "@/config/plans"; 4 6 import { cn } from "@/lib/utils"; 5 7 import { Shell } from "../dashboard/shell"; 8 + import { LoadingAnimation } from "../loading-animation"; 6 9 import { Button } from "../ui/button"; 7 10 8 - interface Plan { 9 - title: string; 10 - description: string; 11 - cost: number | string; 12 - features: string[]; 13 - action?: { 14 - text: string; 15 - link: string; 16 - }; 17 - disabled?: boolean; 18 - } 19 - 20 - const plans: Record<"hobby" | "pro" | "enterprise", Plan> = { 21 - hobby: { 22 - title: "Hobby", 23 - description: "Get started now and upgrade once reaching the limits.", 24 - cost: 0, 25 - features: [ 26 - "5 monitors", 27 - "1 status page", 28 - "subdomain", 29 - "10m, 30m, 1h checks", 30 - ], 31 - action: { 32 - text: "Start Now", 33 - link: "/app/sign-up?plan=hobby", 34 - }, 35 - }, 36 - pro: { 37 - title: "Pro", 38 - description: "Scale and build monitors for all your services.", 39 - cost: 29, 40 - features: [ 41 - "20 monitors", 42 - "5 status page", 43 - "custom domain", 44 - "1m, 5m, 10m, 30m, 1h checks", 45 - "5 team members", 46 - ], 47 - action: { 48 - text: "Start Now", 49 - link: "/app/sign-up?plan=pro", 50 - }, 51 - }, 52 - enterprise: { 53 - title: "Enterprise", 54 - description: "Dedicated support and needs for your company.", 55 - cost: "Lets talk", 56 - features: [], 57 - action: { 58 - text: "Schedule call", 59 - link: "https://cal.com/thibault-openstatus/30min", 60 - }, 61 - }, 62 - }; 63 - 64 11 export function Plans() { 65 12 return ( 66 13 <Shell> 67 14 <div className="grid gap-4 md:grid-cols-2 md:gap-0"> 68 15 <Plan 69 - {...plans.hobby} 16 + {...plansConfig.free} 70 17 className="md:border-border/50 md:border-r md:pr-4" 71 18 /> 72 - <Plan {...plans.pro} className="md:pl-4" /> 19 + <Plan {...plansConfig.pro} className="md:pl-4" /> 73 20 <Plan 74 - {...plans.enterprise} 21 + {...plansConfig.enterprise} 75 22 className="md:border-border/50 col-span-full md:mt-4 md:border-t md:pt-4" 76 23 /> 77 24 </div> ··· 79 26 ); 80 27 } 81 28 82 - interface Props extends Plan { 29 + interface Props extends PlanProps { 83 30 className?: string; 84 31 } 85 32 86 - function Plan({ 33 + export function Plan({ 87 34 title, 88 35 description, 89 36 cost, ··· 91 38 action, 92 39 disabled, 93 40 className, 41 + loading, 94 42 }: Props) { 95 43 return ( 96 44 <div ··· 108 56 <p className="text-muted-foreground">{description}</p> 109 57 </div> 110 58 <p className="shrink-0"> 111 - <span className="font-cal text-2xl">{cost}</span> 112 59 {typeof cost === "number" ? ( 113 - <span className="text-muted-foreground font-light">/month</span> 114 - ) : null} 60 + <> 61 + <span className="font-cal text-2xl">{cost} €</span> 62 + <span className="text-muted-foreground font-light">/month</span> 63 + </> 64 + ) : ( 65 + <span className="font-cal text-2xl">{cost}</span> 66 + )} 115 67 </p> 116 68 </div> 117 69 <ul className="border-border/50 grid divide-y py-2"> ··· 126 78 ))} 127 79 </ul> 128 80 </div> 129 - <div> 130 - {action ? ( 131 - <Button asChild size="sm"> 132 - <Link href={action.link}>{action.text}</Link> 133 - </Button> 134 - ) : null} 135 - </div> 81 + {action ? ( 82 + <div> 83 + {"link" in action ? ( 84 + <Button asChild size="sm"> 85 + <Link href={action.link}>{action.text}</Link> 86 + </Button> 87 + ) : null} 88 + {"onClick" in action ? ( 89 + <Button 90 + onClick={action.onClick} 91 + size="sm" 92 + disabled={disabled || loading} 93 + > 94 + {loading ? <LoadingAnimation /> : action.text} 95 + </Button> 96 + ) : null} 97 + </div> 98 + ) : null} 136 99 </div> 137 100 ); 138 101 }
+63
apps/web/src/config/plans.ts
··· 1 + export type Plans = "free" | "pro" | "enterprise"; 2 + 3 + export interface PlanProps { 4 + title: string; 5 + description: string; 6 + cost: number | string; 7 + features: string[]; 8 + action?: 9 + | { 10 + text: string; 11 + link: string; 12 + } 13 + | { 14 + text: string; 15 + onClick: () => void; 16 + }; 17 + disabled?: boolean; 18 + loading?: boolean; 19 + } 20 + 21 + export const plansConfig: Record<Plans, PlanProps> = { 22 + free: { 23 + title: "Hobby", 24 + description: "Get started now and upgrade once reaching the limits.", 25 + cost: 0, 26 + features: [ 27 + "5 monitors", 28 + "1 status page", 29 + "subdomain", 30 + "10m, 30m, 1h checks", 31 + ], 32 + action: { 33 + text: "Start Now", 34 + link: "/app/sign-up?plan=hobby", 35 + }, 36 + }, 37 + pro: { 38 + title: "Pro", 39 + description: "Scale and build monitors for all your services.", 40 + cost: 29, 41 + features: [ 42 + "20 monitors", 43 + "5 status page", 44 + "custom domain", 45 + "1m, 5m, 10m, 30m, 1h checks", 46 + "5 team members", 47 + ], 48 + action: { 49 + text: "Start Now", 50 + link: "/app/sign-up?plan=pro", 51 + }, 52 + }, 53 + enterprise: { 54 + title: "Enterprise", 55 + description: "Dedicated support and needs for your company.", 56 + cost: "Lets talk", 57 + features: [], 58 + action: { 59 + text: "Schedule call", 60 + link: "https://cal.com/thibault-openstatus/30min", 61 + }, 62 + }, 63 + };