Openstatus www.openstatus.dev

feat: pro banner (#510)

authored by

Maximilian Kaske and committed by
GitHub
d56f329b 71928479

+72 -2
+1
apps/web/src/app/app/(dashboard)/[workspaceSlug]/layout.tsx
··· 22 22 <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"> 23 23 <AppHeader /> 24 24 <div className="flex w-full flex-1 gap-6 lg:gap-8"> 25 + {/* FIXME: max-h-[] results into weird behavior when shrinking window height */} 25 26 <Shell className="hidden max-h-[calc(100vh-9rem)] max-w-min shrink-0 lg:sticky lg:top-28 lg:block"> 26 27 <AppSidebar /> 27 28 </Shell>
+65
apps/web/src/components/billing/pro-banner.tsx
··· 1 + "use client"; 2 + 3 + import { useEffect, useState } from "react"; 4 + import Link from "next/link"; 5 + import { useParams } from "next/navigation"; 6 + import { ArrowRight, Rocket, X } from "lucide-react"; 7 + 8 + import { Button } from "@openstatus/ui"; 9 + 10 + import { api } from "@/trpc/client"; 11 + 12 + export function ProBanner() { 13 + const [hidden, setHidden] = useState(true); 14 + const params = useParams(); 15 + const workspaceSlug = params?.workspaceSlug; 16 + 17 + function onClick() { 18 + if (document) { 19 + const expires = new Date(); 20 + expires.setDate(expires.getDate() + 7); 21 + // store the cookie for 7 days and only for a specific workspace 22 + document.cookie = `hide-pro-banner=true; expires=${expires}; path=/app/${workspaceSlug}`; 23 + } 24 + setHidden(true); 25 + } 26 + 27 + useEffect(() => { 28 + async function configureProBanner() { 29 + const workspace = await api.workspace.getWorkspace.query(); 30 + // make sure to display the banner only for free plans 31 + if (document && workspace?.plan === "free") { 32 + const cookie = document.cookie 33 + .split("; ") 34 + .find((row) => row.startsWith("hide-pro-banner")); 35 + if (!cookie) { 36 + setHidden(false); 37 + } 38 + } 39 + } 40 + configureProBanner(); 41 + }, []); 42 + 43 + if (hidden) return null; 44 + 45 + return ( 46 + <div className="border-border grid gap-2 rounded-md border p-2"> 47 + <div className="flex items-center justify-between"> 48 + <p className="inline-flex items-center text-sm font-medium"> 49 + OpenStatus Pro <Rocket className="ml-2 h-4 w-4" /> 50 + </p> 51 + <Button variant="ghost" size="icon" onClick={onClick}> 52 + <X className="h-4 w-4" /> 53 + </Button> 54 + </div> 55 + <p className="text-muted-foreground text-xs"> 56 + Unlock custom domains, teams, 1 min. checks and more. 57 + </p> 58 + <Button variant="secondary" size="sm" asChild> 59 + <Link href={`/app/${workspaceSlug}/settings/billing`}> 60 + Upgrade <ArrowRight className="ml-2 h-4 w-4" /> 61 + </Link> 62 + </Button> 63 + </div> 64 + ); 65 + }
+6 -2
apps/web/src/components/layout/app-sidebar.tsx
··· 5 5 6 6 import { pagesConfig } from "@/config/pages"; 7 7 import { cn } from "@/lib/utils"; 8 + import { ProBanner } from "../billing/pro-banner"; 8 9 import { Icons } from "../icons"; 9 10 10 11 export function AppSidebar() { ··· 12 13 const params = useParams(); 13 14 14 15 return ( 15 - <div className="flex h-full flex-col justify-between"> 16 + <div className="flex h-full flex-col justify-between gap-6"> 16 17 <ul className="grid gap-1"> 17 18 {pagesConfig.map(({ title, href, icon, disabled }) => { 18 19 const Icon = Icons[icon]; ··· 35 36 ); 36 37 })} 37 38 </ul> 38 - <ul> 39 + <ul className="grid gap-2"> 40 + <li className="w-full"> 41 + <ProBanner /> 42 + </li> 39 43 <li className="w-full"> 40 44 <Link 41 45 href={`/app/${params?.workspaceSlug}/settings`}