From 33037f317b4d988e8d604e38060ebd6830f85319 Mon Sep 17 00:00:00 2001 From: Maximilian Kaske <56969857+mxkaske@users.noreply.github.com> Date: Sat, 29 Jul 2023 16:59:09 +0200 Subject: [PATCH] chore: marketing stuff (#101) * chore: marketing stuff * chore: extract header for blog layout * chore: add consistent layout to playground * chore: extract layout into marketing-layout * fix: thibault x link * chore: replace waitlist with get started and change badge * feat: plan components * marketing copy * wip: plans * fix: typo --------- Co-authored-by: Thibault Le Ouay --- apps/web/src/app/blog/layout.tsx | 11 +- apps/web/src/app/monitor/[id]/page.tsx | 4 +- apps/web/src/app/monitor/layout.tsx | 16 +- apps/web/src/app/page.tsx | 101 +++++++------ apps/web/src/app/play/layout.tsx | 20 ++- apps/web/src/app/play/page.tsx | 7 +- apps/web/src/components/icons.tsx | 2 + apps/web/src/components/layout/app-header.tsx | 2 +- .../components/layout/marketing-header.tsx | 39 +++++ .../components/layout/marketing-layout.tsx | 14 ++ apps/web/src/components/marketing/cards.tsx | 63 ++++++++ apps/web/src/components/marketing/faqs.tsx | 51 +++++++ apps/web/src/components/marketing/plans.tsx | 139 ++++++++++++++++++ apps/web/src/components/tracker.tsx | 45 +++--- 14 files changed, 411 insertions(+), 103 deletions(-) create mode 100644 apps/web/src/components/layout/marketing-header.tsx create mode 100644 apps/web/src/components/layout/marketing-layout.tsx create mode 100644 apps/web/src/components/marketing/cards.tsx create mode 100644 apps/web/src/components/marketing/faqs.tsx create mode 100644 apps/web/src/components/marketing/plans.tsx diff --git a/apps/web/src/app/blog/layout.tsx b/apps/web/src/app/blog/layout.tsx index dddfb8022..fe800ca92 100644 --- a/apps/web/src/app/blog/layout.tsx +++ b/apps/web/src/app/blog/layout.tsx @@ -1,18 +1,11 @@ import * as React from "react"; -import { Footer } from "@/components/layout/footer"; +import { MarketingLayout } from "@/components/layout/marketing-layout"; export default function BlogLayout({ children, }: { children: React.ReactNode; }) { - return ( -
-
-
{children}
-
-
-
- ); + return {children}; } diff --git a/apps/web/src/app/monitor/[id]/page.tsx b/apps/web/src/app/monitor/[id]/page.tsx index e4e8c3c8f..9946f1535 100644 --- a/apps/web/src/app/monitor/[id]/page.tsx +++ b/apps/web/src/app/monitor/[id]/page.tsx @@ -28,8 +28,8 @@ export default async function Monitor({ }) { const search = searchParamsSchema.safeParse(searchParams); const data = search.success - // TODO: lets hard-code our `monitorId` here - ? await getResponseListData({ monitorId: params.id, ...search.data }) + ? // TODO: lets hard-code our `monitorId` here + await getResponseListData({ monitorId: params.id, ...search.data }) : await getResponseListData({ monitorId: params.id }); if (!data || !search.success) return
Something went wrong
; return ; diff --git a/apps/web/src/app/monitor/layout.tsx b/apps/web/src/app/monitor/layout.tsx index f76220960..ff66cbefc 100644 --- a/apps/web/src/app/monitor/layout.tsx +++ b/apps/web/src/app/monitor/layout.tsx @@ -1,7 +1,8 @@ import * as React from "react"; +import { Shell } from "@/components/dashboard/shell"; import { BackButton } from "@/components/layout/back-button"; -import { Footer } from "@/components/layout/footer"; +import { MarketingLayout } from "@/components/layout/marketing-layout"; // same layout as /play export default function MonitorLayout({ @@ -10,14 +11,9 @@ export default function MonitorLayout({ children: React.ReactNode; }) { return ( -
-
- -
- {children} -
-
-
-
+ + + {children} + ); } diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 9dd9c6814..2fbde5c81 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -1,61 +1,70 @@ import Link from "next/link"; +import { ChevronRight } from "lucide-react"; -import { Footer } from "@/components/layout/footer"; +import { Shell } from "@/components/dashboard/shell"; +import { MarketingLayout } from "@/components/layout/marketing-layout"; +import { Cards } from "@/components/marketing/cards"; +import { FAQs } from "@/components/marketing/faqs"; +import { Plans } from "@/components/marketing/plans"; import { Tracker } from "@/components/tracker"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { getMonitorListData } from "@/lib/tb"; -import { HeroForm } from "./_components/hero-form"; export default async function Page() { const data = await getMonitorListData({ monitorId: "openstatusPing" }); return ( -
-
-
-
- Coming Soon -

- Open-source monitoring service -

-

- OpenStatus is an open source alternative to your current - monitoring service with a beautiful status page. -

- {/* think of using the `A total of X events as Link as well */} -
- - -
- + +
+ + + + Announcement Post + + +

+ Open-source monitoring service +

+

+ OpenStatus is an open source alternative to your current monitoring + service with a beautiful status page. +

+
+ +
-
-
-
-

Status

- {data && ( - - )} -
-
+ + +

Status

+ + {data && ( + + )} +
+ + + + +
-
-
+ ); } diff --git a/apps/web/src/app/play/layout.tsx b/apps/web/src/app/play/layout.tsx index 69f03b2e3..84b014ee0 100644 --- a/apps/web/src/app/play/layout.tsx +++ b/apps/web/src/app/play/layout.tsx @@ -1,8 +1,9 @@ import * as React from "react"; import type { Metadata } from "next"; +import { Shell } from "@/components/dashboard/shell"; import { BackButton } from "@/components/layout/back-button"; -import { Footer } from "@/components/layout/footer"; +import { MarketingLayout } from "@/components/layout/marketing-layout"; const TITLE = "OpenStatus"; const DESCRIPTION = @@ -34,15 +35,12 @@ export default function PlayLayout({ modal: React.ReactNode; }) { return ( -
-
- -
- {children} - {modal} -
-
-
-
+ + + + {children} + {modal} + + ); } diff --git a/apps/web/src/app/play/page.tsx b/apps/web/src/app/play/page.tsx index edd939f0f..9847a8bf1 100644 --- a/apps/web/src/app/play/page.tsx +++ b/apps/web/src/app/play/page.tsx @@ -30,9 +30,10 @@ export default async function PlayPage({
-

Status

-

Learn more on how to build your own.

- +

Status

+

+ Build your own within seconds. +

{data && ( ( openstatus diff --git a/apps/web/src/components/layout/marketing-header.tsx b/apps/web/src/components/layout/marketing-header.tsx new file mode 100644 index 000000000..6a3236843 --- /dev/null +++ b/apps/web/src/components/layout/marketing-header.tsx @@ -0,0 +1,39 @@ +import Link from "next/link"; + +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +interface Props { + className?: string; +} + +export function MarketingHeader({ className }: Props) { + return ( +
+ + openstatus + +
+ + + +
+ + ); +} diff --git a/apps/web/src/components/layout/marketing-layout.tsx b/apps/web/src/components/layout/marketing-layout.tsx new file mode 100644 index 000000000..cadd0d157 --- /dev/null +++ b/apps/web/src/components/layout/marketing-layout.tsx @@ -0,0 +1,14 @@ +import { Footer } from "./footer"; +import { MarketingHeader } from "./marketing-header"; + +export function MarketingLayout({ children }: { children: React.ReactNode }) { + return ( +
+ +
+ {children} +
+
+
+ ); +} diff --git a/apps/web/src/components/marketing/cards.tsx b/apps/web/src/components/marketing/cards.tsx new file mode 100644 index 000000000..07162cfa0 --- /dev/null +++ b/apps/web/src/components/marketing/cards.tsx @@ -0,0 +1,63 @@ +import { Shell } from "../dashboard/shell"; +import { Icons } from "../icons"; +import type { ValidIcon } from "../icons"; +import { Badge } from "../ui/badge"; + +const cardConfig: { + icon: ValidIcon; + title: string; + description: string; + badge?: "Coming soon" | "New"; +}[] = [ + { + icon: "activity", + title: "Monitors", + description: "Regularly monitor your website or API.", + }, + { + icon: "panel-top", + title: "Status Pages", + description: + "Create your own status page within seconds. Select your endpoints you wish to display.", + }, + { + icon: "siren", + title: "Incidents", + description: "Inform your users when something goes wrong.", + badge: "Coming soon", + }, + { + icon: "toy-brick", + title: "Integrations", + description: + "Create incident or received failure notification in the tools you already work with.", + badge: "Coming soon", + }, +]; + +// TBD: if we need it at the beginning +export function Cards() { + return ( +
+ {cardConfig.map(({ icon, title, description, badge }, i) => { + const Icon = Icons[icon]; + return ( + + {badge ? ( + + {badge} + + ) : null} +

+ {title} +

+

{description}

+
+ ); + })} +
+ ); +} diff --git a/apps/web/src/components/marketing/faqs.tsx b/apps/web/src/components/marketing/faqs.tsx new file mode 100644 index 000000000..3e9ce4ee6 --- /dev/null +++ b/apps/web/src/components/marketing/faqs.tsx @@ -0,0 +1,51 @@ +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; + +// REMINDER: we can create a contentlayer document and the faq into it +const faqsConfig: Record<"q" | "a", string>[] = [ + { + q: "What are the limits?", + a: "You will start with a free plan by default which includes a total of 5 monitors and 1 status page as well as cron jobs of either 10m, 30m or 1h.", + }, + { + q: "Who are we?", + a: "We are Thibault and Max. We both have a 9-to-5 job and we are doing that project for the purpose of open source and the community. Of course, it would be nice to make that project self sustainable. It's not a sprint, it's a marathon.", + }, + { + q: "How does it work?", + a: "We will ping your endpoint from multiple region to figure out if it's up or down. We will then display the status on your status page.", + }, + { + q: "What regions do we support?", + a: "We support all the vercel regions. Find the full list here.", + }, + { + q: "How can I help?", + a: "You can star our project on github, or contribute to it. Or you can also become a paid user.", + }, +]; + +export function FAQs() { + return ( +
+

FAQ

+ + {faqsConfig.map(({ q, a }, i) => ( + + {q} + +
+ + + ))} + +
+ ); +} diff --git a/apps/web/src/components/marketing/plans.tsx b/apps/web/src/components/marketing/plans.tsx new file mode 100644 index 000000000..f051abe5e --- /dev/null +++ b/apps/web/src/components/marketing/plans.tsx @@ -0,0 +1,139 @@ +import Link from "next/link"; +import { Check } from "lucide-react"; + +import { cn } from "@/lib/utils"; +import { Shell } from "../dashboard/shell"; +import { Button } from "../ui/button"; + +interface Plan { + title: string; + description: string; + cost: number | string; + features: string[]; + action?: { + text: string; + link: string; + }; + disabled?: boolean; +} + +const plans: Record<"hobby" | "pro" | "enterprise", Plan> = { + hobby: { + title: "Hobby", + description: "Get started now and upgrade once reaching the limits.", + cost: 0, + features: [ + "5 monitors", + "1 status page", + "subdomain", + "10m, 30m, 1h checks", + ], + action: { + text: "Start now", + link: "/app", + }, + }, + pro: { + title: "Pro", + description: "Scale and build monitors for all your services.", + cost: 29, + features: [ + "20 monitors", + "5 status page", + "custom domain", + "1m, 5m, 10m, 30m, 1h checks", + "5 team members", + ], + action: { + text: "Coming soon", + link: "/", + }, + disabled: true, + }, + enterprise: { + title: "Enterprise", + description: "Dedicated support and needs for your company.", + cost: "Lets talk", + features: [], + action: { + text: "Schedule call", + link: "https://cal.com/thibault-openstatus/30min", + }, + }, +}; + +export function Plans() { + return ( + +
+ + + +
+
+ ); +} + +interface Props extends Plan { + className?: string; +} + +function Plan({ + title, + description, + cost, + features, + action, + disabled, + className, +}: Props) { + return ( +
+
+
+
+

{title}

+

{description}

+
+

+ {cost} + {typeof cost === "number" ? ( + /month + ) : null} +

+
+
    + {features.map((item) => ( +
  • + + {item} +
  • + ))} +
+
+
+ {action ? ( + + ) : null} +
+
+ ); +} diff --git a/apps/web/src/components/tracker.tsx b/apps/web/src/components/tracker.tsx index 763e43e7c..b5ebe7854 100644 --- a/apps/web/src/components/tracker.tsx +++ b/apps/web/src/components/tracker.tsx @@ -13,6 +13,7 @@ import { HoverCardContent, HoverCardTrigger, } from "@/components/ui/hover-card"; +import { Separator } from "@/components/ui/separator"; import { Tooltip, TooltipContent, @@ -120,20 +121,15 @@ const MoreInfo = ({ - <> - {description && ( -

{description}

+

+ {context === "play" ? ( + + {link} + + ) : ( + description )} -

- {context === "play" ? ( - - {link} - - ) : ( - link - )} -

- +

@@ -165,7 +161,7 @@ const Bar = ({ setOpen(true)} asChild>
- +

{getStatus(ratio).label}

{context === "play" ? ( @@ -185,13 +181,20 @@ const Bar = ({ avg. {avgLatency}ms

-
-

Failed requests

-

{count - ok}

-
-
-

Total requests

-

{count}

+ +
+

+ {count}{" "} + + total requests + +

+

+ {count - ok}{" "} + + failed requests + +

-- 2.51.2