Openstatus www.openstatus.dev

feat: testing card gradient (#529)

* feat: testing card gradient

* 🔥

* 🧑‍🎨

* wip: landing page

* wip: landing page

* chore: add transition to globe

* fix: typos

* fix: preprod sqlite

* fix: replace grid with flex

* fix: keep grid for columns

* wip: landing page

* fix: tb pipe

---------

Co-authored-by: Thibault Le Ouay <thibaultleouay@gmail.Com>

authored by

Maximilian Kaske
Thibault Le Ouay
and committed by
GitHub
75e7db10 c4d39274

+538 -196
+1 -1
.vscode/settings.json
··· 1 1 { 2 2 "editor.codeActionsOnSave": { 3 - "source.fixAll.eslint": true 3 + "source.fixAll.eslint": "explicit" 4 4 }, 5 5 "editor.defaultFormatter": "esbenp.prettier-vscode", 6 6 "editor.formatOnSave": true,
+1
apps/web/package.json
··· 46 46 "class-variance-authority": "0.7.0", 47 47 "clsx": "2.0.0", 48 48 "cmdk": "0.2.0", 49 + "cobe": "0.6.3", 49 50 "contentlayer": "0.3.4", 50 51 "date-fns": "2.30.0", 51 52 "lucide-react": "0.279.0",
+8 -14
apps/web/src/app/page.tsx
··· 1 - import { Shell } from "@/components/dashboard/shell"; 2 1 import { MarketingLayout } from "@/components/layout/marketing-layout"; 3 - import { Cards, SpecialCard } from "@/components/marketing/cards"; 4 - import { Example } from "@/components/marketing/example"; 2 + import { AlertCard } from "@/components/marketing/alert/card"; 5 3 import { FAQs } from "@/components/marketing/faqs"; 6 4 import { Hero } from "@/components/marketing/hero"; 5 + import { MonitoringCard } from "@/components/marketing/monitor/card"; 7 6 import { Partners } from "@/components/marketing/partners"; 8 7 import { Plans } from "@/components/marketing/plans"; 9 8 import { Stats } from "@/components/marketing/stats"; 10 - import { cardConfig, specialCardConfig } from "@/config/features"; 9 + import { StatusPageCard } from "@/components/marketing/status-page/card"; 11 10 12 11 export const revalidate = 600; 13 12 ··· 16 15 <MarketingLayout> 17 16 <div className="grid gap-8"> 18 17 <Hero /> 19 - <Example /> 20 - <Cards {...cardConfig.monitors} /> 21 - <Stats /> 22 - {/* TODO: rename to `reports` */} 23 - <Cards {...cardConfig.incidents} /> 24 18 <Partners /> 25 - <Cards {...cardConfig.pages} /> 26 - <SpecialCard {...specialCardConfig} /> 19 + <MonitoringCard /> 20 + <Stats /> 21 + <StatusPageCard /> 22 + <AlertCard /> 27 23 <Plans /> 28 - <Shell> 29 - <FAQs /> 30 - </Shell> 24 + <FAQs /> 31 25 </div> 32 26 </MarketingLayout> 33 27 );
+4
apps/web/src/components/icons.tsx
··· 25 25 Search, 26 26 SearchCheck, 27 27 Siren, 28 + Sparkles, 28 29 SunMedium, 29 30 Table, 30 31 Tag, 31 32 ToyBrick, 32 33 Trash, 33 34 TwitterIcon, 35 + Webhook, 34 36 Zap, 35 37 } from "lucide-react"; 36 38 import type { LucideIcon, LucideProps } from "lucide-react"; ··· 68 70 zap: Zap, 69 71 "alert-triangle": AlertTriangle, 70 72 megaphone: Megaphone, 73 + webhook: Webhook, 71 74 minus: Minus, 72 75 sun: SunMedium, 73 76 moon: Moon, 74 77 laptop: Laptop, 78 + sparkles: Sparkles, 75 79 discord: ({ ...props }: LucideProps) => ( 76 80 <svg viewBox="0 0 640 512" {...props}> 77 81 <path
+29
apps/web/src/components/marketing/alert/card.tsx
··· 1 + import { cardConfig } from "@/config/features"; 2 + import { 3 + CardContainer, 4 + CardContent, 5 + CardFeature, 6 + CardFeatureContainer, 7 + CardHeader, 8 + CardIcon, 9 + CardTitle, 10 + } from "../card"; 11 + import { Timeline } from "./timeline"; 12 + 13 + export function AlertCard() { 14 + const { icon, title, features } = cardConfig.alerts; 15 + return ( 16 + <CardContainer> 17 + <CardHeader> 18 + <CardIcon icon={icon} /> 19 + <CardTitle>{title}</CardTitle> 20 + </CardHeader> 21 + <CardContent> 22 + <Timeline /> 23 + <CardFeatureContainer> 24 + {features?.map((feature, i) => <CardFeature key={i} {...feature} />)} 25 + </CardFeatureContainer> 26 + </CardContent> 27 + </CardContainer> 28 + ); 29 + }
+117
apps/web/src/components/marketing/alert/timeline.tsx
··· 1 + import * as React from "react"; 2 + import { format } from "date-fns"; 3 + 4 + import type { ValidIcon } from "@/components/icons"; 5 + import { Icons } from "@/components/icons"; 6 + import { cn } from "@/lib/utils"; 7 + 8 + export function TimelineContainer({ children }: { children: React.ReactNode }) { 9 + return ( 10 + // first:md:order-2 does not work 11 + <div className="mx-auto flex max-w-md flex-col justify-center gap-3 md:order-2"> 12 + {children} 13 + </div> 14 + ); 15 + } 16 + 17 + export function TimelineEvent({ 18 + label, 19 + date, 20 + message, 21 + icon, 22 + isLast = false, 23 + }: Event & { 24 + isLast?: boolean; 25 + }) { 26 + const Icon = Icons[icon.name]; 27 + return ( 28 + <div className="group relative -m-2 flex gap-4 border border-transparent p-2"> 29 + <div className="relative"> 30 + <div 31 + className={cn( 32 + "bg-background rounded-full border p-2", 33 + icon.borderColor, 34 + )} 35 + > 36 + <Icon className={cn("h-4 w-4", icon.textColor)} /> 37 + </div> 38 + {!isLast ? ( 39 + <div className="bg-muted absolute inset-x-0 mx-auto h-full w-[2px]" /> 40 + ) : null} 41 + </div> 42 + <div className="mt-1 flex flex-1 flex-col gap-1"> 43 + <div className="flex items-center justify-between gap-4"> 44 + <p className="text-sm font-semibold">{label}</p> 45 + <p className="text-muted-foreground mt-px text-right text-[10px]"> 46 + <code>{format(new Date(date), "LLL dd, y HH:mm:ss")}</code> 47 + </p> 48 + </div> 49 + <p className="text-muted-foreground text-sm">{message}</p> 50 + </div> 51 + </div> 52 + ); 53 + } 54 + 55 + export function Timeline() { 56 + return ( 57 + <TimelineContainer> 58 + {timeline.map((event, i) => ( 59 + <TimelineEvent key={i} isLast={i === timeline.length - 1} {...event} /> 60 + ))} 61 + </TimelineContainer> 62 + ); 63 + } 64 + 65 + type Event = { 66 + label: string; 67 + date: Date; 68 + message: string; 69 + icon: { 70 + name: ValidIcon; 71 + textColor: string; 72 + borderColor: string; 73 + }; 74 + }; 75 + 76 + const timeline = [ 77 + { 78 + label: "Monitor down", 79 + date: new Date("03.12.2023, 19:14:45"), 80 + message: "We couldn't reach your endpoint in Amsterdam.", 81 + icon: { 82 + name: "alert-triangle", 83 + textColor: "text-orange-500", 84 + borderColor: "border-orange-500/40", 85 + }, 86 + }, 87 + { 88 + label: "Grafana alert", 89 + date: new Date("03.12.2023, 19:14:55"), 90 + message: "3 incoming notifications from Grafana.", 91 + icon: { 92 + name: "webhook", 93 + textColor: "text-yellow-500", 94 + borderColor: "border-yellow-500/40", 95 + }, 96 + }, 97 + { 98 + label: "Notification sent", 99 + date: new Date("03.12.2023, 19:15:01"), 100 + message: "Smart notification with summary sent to your Slack channel.", 101 + icon: { 102 + name: "sparkles", 103 + textColor: "text-blue-500", 104 + borderColor: "border-blue-500/40", 105 + }, 106 + }, 107 + { 108 + label: "Monitor recovered", 109 + date: new Date("03.12.2023, 19:21:30"), 110 + message: "The enpoint response is back.", 111 + icon: { 112 + name: "check", 113 + textColor: "text-green-500", 114 + borderColor: "border-green-500/40", 115 + }, 116 + }, 117 + ] satisfies Event[];
+105
apps/web/src/components/marketing/card.tsx
··· 1 + import { Badge } from "@openstatus/ui"; 2 + 3 + import type { FeatureDescription } from "@/config/features"; 4 + import { cn } from "@/lib/utils"; 5 + import { Shell } from "../dashboard/shell"; 6 + import type { ValidIcon } from "../icons"; 7 + import { Icons } from "../icons"; 8 + 9 + export function CardContainer({ children }: { children: React.ReactNode }) { 10 + return ( 11 + <Shell className="grid gap-6 bg-gradient-to-br from-[hsl(var(--muted))] from-0% to-transparent to-20%"> 12 + {children} 13 + </Shell> 14 + ); 15 + } 16 + 17 + export function CardHeader({ children }: { children: React.ReactNode }) { 18 + return ( 19 + <div className="flex flex-col items-center justify-center gap-3"> 20 + {children} 21 + </div> 22 + ); 23 + } 24 + 25 + export function CardIcon({ icon }: { icon: ValidIcon }) { 26 + const Icon = Icons[icon]; 27 + return ( 28 + <div className="border-border rounded-full border p-2"> 29 + <Icon className="h-5 w-5" /> 30 + </div> 31 + ); 32 + } 33 + 34 + export function CardTitle({ children }: { children: React.ReactNode }) { 35 + return ( 36 + <h3 className="font-cal bg-gradient-to-tl from-[hsl(var(--muted))] from-0% to-[hsl(var(--foreground))] to-40% bg-clip-text text-center text-3xl text-transparent"> 37 + {children} 38 + </h3> 39 + ); 40 + } 41 + 42 + export function CardDescription({ children }: { children: React.ReactNode }) { 43 + return <p className="text-muted-foreground mt-2">{children}</p>; 44 + } 45 + 46 + export function CardContent({ 47 + children, 48 + dir = "cols", 49 + }: { 50 + children: React.ReactNode; 51 + dir?: "rows" | "cols"; 52 + }) { 53 + return ( 54 + <div 55 + className={cn("grid gap-10", { 56 + "grid-cols-none md:grid-cols-2": dir === "cols", 57 + "grid-rows-none md:grid-rows-2": dir === "rows", 58 + })} 59 + > 60 + {children} 61 + </div> 62 + ); 63 + } 64 + 65 + export function CardFeatureContainer({ 66 + children, 67 + dir = "rows", 68 + }: { 69 + children: React.ReactNode; 70 + dir?: "rows" | "cols"; 71 + }) { 72 + return ( 73 + <ul 74 + className={cn("gap-4 md:gap-6", { 75 + "grid md:grid-cols-3": dir === "cols", 76 + "flex flex-col": dir === "rows", 77 + })} 78 + > 79 + {children} 80 + </ul> 81 + ); 82 + } 83 + 84 + // TODO: rename type a bit appropriately 85 + export function CardFeature(props: FeatureDescription) { 86 + const FeatureIcon = Icons[props.icon]; 87 + return ( 88 + <li> 89 + <p className="flex flex-col"> 90 + <span> 91 + <FeatureIcon className="text-foreground/80 mb-1 mr-1.5 inline-flex h-4 w-4" /> 92 + <span className="text-foreground font-medium"> 93 + {props.catchline.replace(".", "")} 94 + </span>{" "} 95 + </span> 96 + <span className="text-muted-foreground">{props.description}</span> 97 + </p> 98 + {props.badge ? ( 99 + <Badge variant="secondary" className="-ml-2 mt-1"> 100 + {props.badge} 101 + </Badge> 102 + ) : null} 103 + </li> 104 + ); 105 + }
-62
apps/web/src/components/marketing/cards.tsx
··· 1 - import { Badge } from "@openstatus/ui"; 2 - 3 - import type { Feature, SpecialFeature } from "@/config/features"; 4 - import { Shell } from "../dashboard/shell"; 5 - import { Icons } from "../icons"; 6 - 7 - export function Cards(props: Feature) { 8 - const Icon = Icons[props.icon]; 9 - return ( 10 - <Shell className="grid gap-6"> 11 - <div className="flex flex-col items-center justify-center gap-3"> 12 - <div className="border-border rounded-full border p-2"> 13 - <Icon className="h-5 w-5" /> 14 - </div> 15 - <h3 className="font-cal text-center text-3xl">{props.title}</h3> 16 - </div> 17 - <ul className="grid gap-4 md:grid-cols-3 md:gap-6"> 18 - {props.features?.map((feature, i) => { 19 - const FeatureIcon = Icons[feature.icon]; 20 - return ( 21 - <li key={i}> 22 - <p className="flex flex-col"> 23 - <span> 24 - <FeatureIcon className="text-foreground/80 mb-1 mr-1.5 inline-flex h-4 w-4" /> 25 - <span className="text-foreground font-medium"> 26 - {feature.catchline.replace(".", "")} 27 - </span>{" "} 28 - </span> 29 - <span className="text-muted-foreground"> 30 - {feature.description} 31 - </span> 32 - </p> 33 - {feature.badge ? ( 34 - <Badge variant="secondary" className="-ml-2 mt-1"> 35 - {feature.badge} 36 - </Badge> 37 - ) : null} 38 - </li> 39 - ); 40 - })} 41 - </ul> 42 - </Shell> 43 - ); 44 - } 45 - 46 - export function SpecialCard(props: SpecialFeature) { 47 - const Icon = Icons[props.icon]; 48 - return ( 49 - <Shell className="relative flex items-center justify-between"> 50 - <div> 51 - <div className="flex items-center gap-3"> 52 - <h3 className="font-cal text-3xl">{props.title}</h3> 53 - <div className="border-border rounded-full border p-2"> 54 - <Icon className="h-5 w-5" /> 55 - </div> 56 - </div> 57 - <p className="text-muted-foreground mt-2">{props.description}</p> 58 - </div> 59 - <div /> 60 - </Shell> 61 - ); 62 - }
+7 -9
apps/web/src/components/marketing/example.tsx apps/web/src/components/marketing/status-page/tracker-example.tsx
··· 4 4 5 5 import { Button } from "@openstatus/ui"; 6 6 7 - import { Shell } from "@/components/dashboard/shell"; 8 7 import { Tracker } from "@/components/tracker"; 9 8 import { getHomeMonitorListData } from "@/lib/tb"; 10 9 11 10 const currentTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; 12 11 13 - export async function Example() { 12 + export async function TrackerExample() { 14 13 return ( 15 - <Shell className="text-center"> 16 - <h2 className="font-cal mb-3 text-2xl">Status</h2> 17 - <Button asChild variant="outline" className="rounded-full"> 18 - <Link href="/play">Playground</Link> 19 - </Button> 20 - <div className="mx-auto max-w-md"> 14 + <div className="flex w-full flex-col items-center justify-center gap-8"> 15 + <div className="mx-auto w-full max-w-md"> 21 16 <Suspense fallback={<ExampleTrackerFallback />}> 22 17 <ExampleTracker /> 23 18 </Suspense> 24 19 </div> 25 - </Shell> 20 + <Button asChild variant="outline" className="rounded-full"> 21 + <Link href="/play">Playground</Link> 22 + </Button> 23 + </div> 26 24 ); 27 25 } 28 26
+4 -2
apps/web/src/components/marketing/faqs.tsx
··· 5 5 AccordionTrigger, 6 6 } from "@openstatus/ui"; 7 7 8 + import { Shell } from "@/components/dashboard/shell"; 9 + 8 10 // REMINDER: we can create a contentlayer document and the faq into it 9 11 const faqsConfig: Record<"q" | "a", string>[] = [ 10 12 { ··· 31 33 32 34 export function FAQs() { 33 35 return ( 34 - <div className="grid gap-1"> 36 + <Shell className="grid gap-1"> 35 37 <h2 className="text-foreground font-cal text-center text-2xl">FAQ</h2> 36 38 <Accordion type="single" collapsible className="w-full"> 37 39 {faqsConfig.map(({ q, a }, i) => ( ··· 46 48 </AccordionItem> 47 49 ))} 48 50 </Accordion> 49 - </div> 51 + </Shell> 50 52 ); 51 53 }
+29 -21
apps/web/src/components/marketing/hero.tsx
··· 4 4 5 5 import { Badge, Button } from "@openstatus/ui"; 6 6 7 - import { Shell } from "@/components/dashboard/shell"; 8 7 import { getGitHubStars } from "@/lib/github"; 9 - import { numberFormatter } from "@/lib/utils"; 8 + import { cn, numberFormatter } from "@/lib/utils"; 10 9 11 10 export function Hero() { 12 11 return ( 13 - <Shell className="text-center"> 14 - <Link 15 - href="https://github.com/openstatusHQ/openstatus/stargazers" 16 - target="_blank" 17 - rel="noreferrer" 18 - > 19 - <Badge variant="outline"> 20 - Proudly Open Source - Support us on GitHub 21 - <ChevronRight className="ml-1 h-3 w-3" /> 12 + <div className="my-10 flex w-full flex-col justify-center gap-1 px-3 py-4 text-center md:my-20 md:p-6"> 13 + <div> 14 + <Badge variant="outline" className="backdrop-blur-[2px]"> 15 + <Link 16 + href="https://github.com/openstatusHQ/openstatus/stargazers" 17 + target="_blank" 18 + rel="noreferrer" 19 + className="flex items-center" 20 + > 21 + Proudly Open Source 22 + <ChevronRight className="ml-1 h-3 w-3" /> 23 + </Link> 22 24 </Badge> 23 - </Link> 24 - <h1 className="text-foreground font-cal mb-6 mt-2 text-3xl"> 25 - A better way to monitor your services. 26 - </h1> 27 - <p className="text-muted-foreground mx-auto mb-6 max-w-lg text-lg"> 28 - Reduce alert fatigue by triggering only relevant alerts when your 29 - services experience downtime. 30 - </p> 31 - {/* much better than using flex without text alignment, text stays center even thought not same length */} 25 + </div> 26 + <div className="flex flex-col gap-6"> 27 + <h1 28 + className={cn( 29 + "text-foreground font-cal text-4xl md:text-6xl", 30 + "bg-gradient-to-tl from-[hsl(var(--muted))] from-0% to-[hsl(var(--foreground))] to-40% bg-clip-text text-transparent", 31 + )} 32 + > 33 + A better way to monitor your services. 34 + </h1> 35 + <p className="text-muted-foreground mx-auto max-w-md text-lg md:max-w-lg md:text-xl"> 36 + Reduce alert fatigue by triggering only relevant alerts when your 37 + services experience downtime. 38 + </p> 39 + </div> 32 40 <div className="my-4 grid gap-2 sm:grid-cols-2"> 33 41 <div className="text-center sm:block sm:text-right"> 34 42 <Button className="w-48 rounded-full sm:w-auto" asChild> ··· 50 58 </Button> 51 59 </div> 52 60 </div> 53 - </Shell> 61 + </div> 54 62 ); 55 63 } 56 64
+29
apps/web/src/components/marketing/monitor/card.tsx
··· 1 + import { cardConfig } from "@/config/features"; 2 + import { 3 + CardContainer, 4 + CardContent, 5 + CardFeature, 6 + CardFeatureContainer, 7 + CardHeader, 8 + CardIcon, 9 + CardTitle, 10 + } from "../card"; 11 + import { Globe } from "./globe"; 12 + 13 + export function MonitoringCard() { 14 + const { icon, title, features } = cardConfig.monitors; 15 + return ( 16 + <CardContainer> 17 + <CardHeader> 18 + <CardIcon icon={icon} /> 19 + <CardTitle>{title}</CardTitle> 20 + </CardHeader> 21 + <CardContent> 22 + <Globe /> 23 + <CardFeatureContainer> 24 + {features?.map((feature, i) => <CardFeature key={i} {...feature} />)} 25 + </CardFeatureContainer> 26 + </CardContent> 27 + </CardContainer> 28 + ); 29 + }
+76
apps/web/src/components/marketing/monitor/globe.tsx
··· 1 + "use client"; 2 + 3 + import { useEffect, useRef } from "react"; 4 + import createGlobe from "cobe"; 5 + 6 + const SIZE = 350; 7 + 8 + export function Globe() { 9 + const canvasRef = useRef<HTMLCanvasElement>(null); 10 + 11 + useEffect(() => { 12 + let phi = 0; 13 + if (!canvasRef.current) return; 14 + 15 + const globe = createGlobe(canvasRef.current, { 16 + devicePixelRatio: 2, 17 + width: SIZE * 2, 18 + height: SIZE * 2, 19 + phi: 0, 20 + theta: 0, 21 + dark: 1, 22 + diffuse: 1.2, 23 + mapSamples: 16000, 24 + mapBrightness: 6, 25 + baseColor: [0.3, 0.3, 0.3], 26 + markerColor: [1, 1, 1], 27 + glowColor: [1, 1, 1], 28 + markers: [ 29 + // AMS 30 + { location: [52.3676, 4.9041], size: 0.05 }, 31 + // IAD 32 + { location: [39.0438, -77.4874], size: 0.05 }, 33 + // JNB 34 + { location: [-26.2041, 28.0473], size: 0.05 }, 35 + // HKG 36 + { location: [22.3193, 114.1694], size: 0.05 }, 37 + // SYD 38 + { location: [-33.8688, 151.2093], size: 0.05 }, 39 + // GRU 40 + { location: [-23.5558, -46.6396], size: 0.05 }, 41 + ], 42 + onRender: (state) => { 43 + // Called on every animation frame. 44 + // `state` will be an empty object, return updated params. 45 + state.phi = phi; 46 + phi += 0.003; 47 + }, 48 + }); 49 + 50 + setTimeout(() => { 51 + const canvas = canvasRef.current; 52 + if (!canvas) return; 53 + canvas.style.opacity = "1"; 54 + }); 55 + 56 + return () => { 57 + globe.destroy(); 58 + }; 59 + }, []); 60 + 61 + return ( 62 + <div className="flex justify-center"> 63 + <canvas 64 + ref={canvasRef} 65 + style={{ 66 + width: SIZE, 67 + height: SIZE, 68 + maxWidth: "100%", 69 + aspectRatio: 1, 70 + opacity: 0, 71 + transition: "opacity 1s ease", 72 + }} 73 + /> 74 + </div> 75 + ); 76 + }
+4 -4
apps/web/src/components/marketing/partners.tsx
··· 10 10 <h3 className="text-muted-foreground font-cal text-center text-sm"> 11 11 Trusted By 12 12 </h3> 13 - <div className="grid grid-cols-1 gap-8 sm:grid-cols-4 sm:gap-16"> 13 + <div className="grid grid-cols-2 gap-8 sm:grid-cols-4 sm:gap-16"> 14 14 <div className="flex items-center justify-center"> 15 15 <a 16 16 href="https://status.hanko.io" ··· 18 18 rel="noreferrer" 19 19 className="group inline-flex items-center justify-center underline underline-offset-4 hover:no-underline" 20 20 > 21 - <Hanko className="h-8 w-24" /> 21 + <Hanko className="h-6 w-[3.75rem] sm:h-8 sm:w-24" /> 22 22 <ArrowUpRight className="text-muted-foreground group-hover:text-foreground ml-1 h-4 w-4 flex-shrink-0" /> 23 23 </a> 24 24 </div> ··· 29 29 rel="noreferrer" 30 30 className="group inline-flex items-center justify-center underline underline-offset-4 hover:no-underline" 31 31 > 32 - <Documenso className="h-8 w-32" /> 32 + <Documenso className="h-6 w-24 sm:h-8 sm:w-32" /> 33 33 <ArrowUpRight className="text-muted-foreground group-hover:text-foreground ml-1 h-4 w-4 flex-shrink-0" /> 34 34 </a> 35 35 </div> ··· 40 40 rel="noreferrer" 41 41 className="group inline-flex items-center justify-center underline underline-offset-4 hover:no-underline" 42 42 > 43 - <Trigger className="h-8 w-32" /> 43 + <Trigger className="h-6 w-24 sm:h-8 sm:w-32" /> 44 44 <ArrowUpRight className="text-muted-foreground group-hover:text-foreground ml-1 h-4 w-4 flex-shrink-0" /> 45 45 </a> 46 46 </div>
+12 -14
apps/web/src/components/marketing/plans.tsx
··· 3 3 4 4 import { Button } from "@openstatus/ui"; 5 5 6 + import { Shell } from "@/components/dashboard/shell"; 7 + import { LoadingAnimation } from "@/components/loading-animation"; 6 8 import type { PlanProps } from "@/config/plans"; 7 9 import { plansConfig } from "@/config/plans"; 8 10 import { cn } from "@/lib/utils"; 9 - import { Shell } from "../dashboard/shell"; 10 - import { LoadingAnimation } from "../loading-animation"; 11 11 12 12 export function Plans() { 13 13 return ( 14 - <Shell> 15 - <div className="grid gap-4 md:grid-cols-2 md:gap-0"> 16 - <Plan 17 - {...plansConfig.free} 18 - className="md:border-border/50 md:border-r md:pr-4" 19 - /> 20 - <Plan {...plansConfig.pro} className="md:pl-4" /> 21 - <Plan 22 - {...plansConfig.enterprise} 23 - className="md:border-border/50 col-span-full md:mt-4 md:border-t md:pt-4" 24 - /> 25 - </div> 14 + <Shell className="grid gap-4 md:grid-cols-2 md:gap-0"> 15 + <Plan 16 + {...plansConfig.free} 17 + className="md:border-border/50 md:border-r md:pr-4" 18 + /> 19 + <Plan {...plansConfig.pro} className="md:pl-4" /> 20 + <Plan 21 + {...plansConfig.enterprise} 22 + className="md:border-border/50 col-span-full md:mt-4 md:border-t md:pt-4" 23 + /> 26 24 </Shell> 27 25 ); 28 26 }
+4 -12
apps/web/src/components/marketing/stats.tsx
··· 1 1 import { Shell } from "@/components/dashboard/shell"; 2 2 import { getHomeStatsData } from "@/lib/tb"; 3 3 import { numberFormatter } from "@/lib/utils"; 4 - 5 - // import { api } from "@/trpc/server"; 4 + import { api } from "@/trpc/server"; 6 5 7 6 export async function Stats() { 8 7 const tbTotalStats = await getHomeStatsData({}); 9 8 const tbLastHourStats = await getHomeStatsData({ period: "1h" }); 10 - // FIXME: is it time? 11 - // const totalActiveMonitors = await api.monitor.getTotalActiveMonitors.query( 12 - // {}, 13 - // ); 9 + // const totalActiveMonitors = await api.monitor.getTotalActiveMonitors.query(); 14 10 15 11 return ( 16 12 <Shell> 17 13 <div className="grid grid-cols-1 gap-8 sm:grid-cols-3 sm:gap-16"> 18 14 <div className="text-center"> 19 15 <h3 className="font-cal text-3xl"> 20 - {tbTotalStats && tbTotalStats?.length > 0 21 - ? numberFormatter(tbTotalStats[0].count) 22 - : 0} 16 + {numberFormatter(tbTotalStats?.[0].count || 0)} 23 17 </h3> 24 18 <p className="text-muted-foreground font-light">Total pings</p> 25 19 </div> 26 20 <div className="text-center"> 27 21 <h3 className="font-cal text-3xl"> 28 - {tbLastHourStats && tbLastHourStats?.length > 0 29 - ? numberFormatter(tbLastHourStats[0].count) 30 - : 0} 22 + {numberFormatter(tbLastHourStats?.[0].count || 0)} 31 23 </h3> 32 24 <p className="text-muted-foreground font-light"> 33 25 Pings in the last hour
+29
apps/web/src/components/marketing/status-page/card.tsx
··· 1 + import { cardConfig } from "@/config/features"; 2 + import { 3 + CardContainer, 4 + CardContent, 5 + CardFeature, 6 + CardFeatureContainer, 7 + CardHeader, 8 + CardIcon, 9 + CardTitle, 10 + } from "../card"; 11 + import { TrackerExample } from "./tracker-example"; 12 + 13 + export function StatusPageCard() { 14 + const { icon, title, features } = cardConfig.pages; 15 + return ( 16 + <CardContainer> 17 + <CardHeader> 18 + <CardIcon icon={icon} /> 19 + <CardTitle>{title}</CardTitle> 20 + </CardHeader> 21 + <CardContent dir="rows"> 22 + <TrackerExample /> 23 + <CardFeatureContainer dir="cols"> 24 + {features?.map((feature, i) => <CardFeature key={i} {...feature} />)} 25 + </CardFeatureContainer> 26 + </CardContent> 27 + </CardContainer> 28 + ); 29 + }
+24 -24
apps/web/src/components/svg/trigger.tsx
··· 62 62 y2="13.9297" 63 63 gradientUnits="userSpaceOnUse" 64 64 > 65 - <stop stop-color="#41FF54"></stop> 66 - <stop offset="1" stop-color="#E7FF52"></stop> 65 + <stop stopColor="#41FF54"></stop> 66 + <stop offset="1" stopColor="#E7FF52"></stop> 67 67 </linearGradient> 68 68 <linearGradient 69 69 id="paint1_linear_228_1439" ··· 73 73 y2="13.9297" 74 74 gradientUnits="userSpaceOnUse" 75 75 > 76 - <stop stop-color="#41FF54"></stop> 77 - <stop offset="1" stop-color="#E7FF52"></stop> 76 + <stop stopColor="#41FF54"></stop> 77 + <stop offset="1" stopColor="#E7FF52"></stop> 78 78 </linearGradient> 79 79 <linearGradient 80 80 id="paint2_linear_228_1439" ··· 84 84 y2="13.9297" 85 85 gradientUnits="userSpaceOnUse" 86 86 > 87 - <stop stop-color="#41FF54"></stop> 88 - <stop offset="1" stop-color="#E7FF52"></stop> 87 + <stop stopColor="#41FF54"></stop> 88 + <stop offset="1" stopColor="#E7FF52"></stop> 89 89 </linearGradient> 90 90 <linearGradient 91 91 id="paint3_linear_228_1439" ··· 95 95 y2="13.9297" 96 96 gradientUnits="userSpaceOnUse" 97 97 > 98 - <stop stop-color="#41FF54"></stop> 99 - <stop offset="1" stop-color="#E7FF52"></stop> 98 + <stop stopColor="#41FF54"></stop> 99 + <stop offset="1" stopColor="#E7FF52"></stop> 100 100 </linearGradient> 101 101 <linearGradient 102 102 id="paint4_linear_228_1439" ··· 106 106 y2="13.9297" 107 107 gradientUnits="userSpaceOnUse" 108 108 > 109 - <stop stop-color="#41FF54"></stop> 110 - <stop offset="1" stop-color="#E7FF52"></stop> 109 + <stop stopColor="#41FF54"></stop> 110 + <stop offset="1" stopColor="#E7FF52"></stop> 111 111 </linearGradient> 112 112 <linearGradient 113 113 id="paint5_linear_228_1439" ··· 117 117 y2="13.9297" 118 118 gradientUnits="userSpaceOnUse" 119 119 > 120 - <stop stop-color="#41FF54"></stop> 121 - <stop offset="1" stop-color="#E7FF52"></stop> 120 + <stop stopColor="#41FF54"></stop> 121 + <stop offset="1" stopColor="#E7FF52"></stop> 122 122 </linearGradient> 123 123 <linearGradient 124 124 id="paint6_linear_228_1439" ··· 128 128 y2="13.9297" 129 129 gradientUnits="userSpaceOnUse" 130 130 > 131 - <stop stop-color="#41FF54"></stop> 132 - <stop offset="1" stop-color="#E7FF52"></stop> 131 + <stop stopColor="#41FF54"></stop> 132 + <stop offset="1" stopColor="#E7FF52"></stop> 133 133 </linearGradient> 134 134 <linearGradient 135 135 id="paint7_linear_228_1439" ··· 139 139 y2="25.9719" 140 140 gradientUnits="userSpaceOnUse" 141 141 > 142 - <stop stop-color="#2563EB"></stop> 143 - <stop offset="1" stop-color="#A855F7"></stop> 142 + <stop stopColor="#2563EB"></stop> 143 + <stop offset="1" stopColor="#A855F7"></stop> 144 144 </linearGradient> 145 145 <linearGradient 146 146 id="paint8_linear_228_1439" ··· 150 150 y2="25.9719" 151 151 gradientUnits="userSpaceOnUse" 152 152 > 153 - <stop stop-color="#2563EB"></stop> 154 - <stop offset="1" stop-color="#A855F7"></stop> 153 + <stop stopColor="#2563EB"></stop> 154 + <stop offset="1" stopColor="#A855F7"></stop> 155 155 </linearGradient> 156 156 <linearGradient 157 157 id="paint9_linear_228_1439" ··· 161 161 y2="25.9719" 162 162 gradientUnits="userSpaceOnUse" 163 163 > 164 - <stop stop-color="#2563EB"></stop> 165 - <stop offset="1" stop-color="#A855F7"></stop> 164 + <stop stopColor="#2563EB"></stop> 165 + <stop offset="1" stopColor="#A855F7"></stop> 166 166 </linearGradient> 167 167 <linearGradient 168 168 id="paint10_linear_228_1439" ··· 172 172 y2="25.9719" 173 173 gradientUnits="userSpaceOnUse" 174 174 > 175 - <stop stop-color="#2563EB"></stop> 176 - <stop offset="1" stop-color="#A855F7"></stop> 175 + <stop stopColor="#2563EB"></stop> 176 + <stop offset="1" stopColor="#A855F7"></stop> 177 177 </linearGradient> 178 178 <linearGradient 179 179 id="paint11_linear_228_1439" ··· 183 183 y2="31.2381" 184 184 gradientUnits="userSpaceOnUse" 185 185 > 186 - <stop stop-color="#41FF54"></stop> 187 - <stop offset="1" stop-color="#E7FF52"></stop> 186 + <stop stopColor="#41FF54"></stop> 187 + <stop offset="1" stopColor="#E7FF52"></stop> 188 188 </linearGradient> 189 189 </defs> 190 190 </svg>
+38 -30
apps/web/src/config/features.ts
··· 4 4 icon: ValidIcon; 5 5 title: string; 6 6 // description?: string; 7 - features?: { 8 - icon: ValidIcon; 9 - catchline: string; 10 - description: string; 11 - badge?: "Coming soon" | "New"; 12 - }[]; 7 + features?: FeatureDescription[]; 8 + }; 9 + 10 + export type FeatureDescription = { 11 + icon: ValidIcon; 12 + catchline: string; 13 + description: string; 14 + badge?: "Coming soon" | "New"; 13 15 }; 14 16 15 17 export type SpecialFeature = { ··· 29 31 export const cardConfig = { 30 32 monitors: { 31 33 icon: "activity", 32 - title: "Monitors", 34 + title: "Monitoring", 33 35 features: [ 34 36 { 35 - icon: "cog", 36 - catchline: "Custom Headers.", 37 + icon: "globe", 38 + catchline: "Global Monitoring.", 37 39 description: 38 - "Add your own headers to the request and access secured endpoints.", 40 + "Monitor your endpoints from all over the world. We currently support all the continents.", 39 41 }, 40 42 { 41 43 icon: "play", 42 - catchline: "Run on demand.", 43 - description: "Check if your endpoint is up and running with one click.", 44 + catchline: "Monitor anything.", 45 + description: 46 + "API, DNS, domain, SLL, SMTP, ping, webpage,... , we can monitor it all.", 44 47 }, 45 48 { 46 49 icon: "bot", 47 - catchline: "Automatic checks.", 50 + catchline: "Cron Monitoring.", 51 + badge: "Coming soon", 48 52 description: 49 - "Define the region where to call the request and the frequency how often to call it.", 53 + "Never let a cron job fail you. Get notified when a jobs did not run successfully.", 50 54 }, 51 55 ], 52 56 }, ··· 56 60 features: [ 57 61 { 58 62 icon: "puzzle", 59 - catchline: "Custom slug.", 63 + catchline: "Build trust", 60 64 description: 61 - "Create your own sudomain and inform your users about the uptime of your endpoints.", 65 + "Showcase your reliability to your users, and reduce the numbers of customers service tickets.", 62 66 }, 63 67 { 64 68 icon: "globe", 65 69 catchline: "Custom domain.", 66 70 description: 67 - "Give the status page a personal touch. Including the favicon.", 71 + "Bring your own domain, give the status page a personal touch.", 68 72 }, 69 73 { 70 74 icon: "image", 71 - catchline: "OG images.", 75 + catchline: "Subscription", 72 76 description: 73 - "Get a custom image with the current uptime of your first monitor.", 77 + "Let your users subscribe to your status page, to automatically receive updates about the status of your services.", 74 78 }, 75 79 ], 76 80 }, 77 - incidents: { 81 + alerts: { 78 82 icon: "siren", 79 - title: "Incidents", 83 + title: "Alerting", 80 84 features: [ 81 85 { 82 - icon: "bell", 83 - catchline: "Alerts.", 84 - description: "Be informed if your endpoint fails.", 86 + icon: "sparkles", 87 + catchline: "Connect.", 88 + description: 89 + "Aggregate alerts from all your monitoring services (Grafana, Datadog) and use our AI to make them actionnable.", 90 + badge: "Coming soon", 85 91 }, 86 92 { 87 - icon: "message-circle", 88 - catchline: "Status Reports.", 89 - description: "Keep your teams and users updated with status updates.", 93 + icon: "zap", 94 + catchline: "Escalatation.", 95 + description: "Notify and escalate an alert to the right team member.", 96 + badge: "Coming soon", 90 97 }, 91 98 { 92 - icon: "zap", 93 - catchline: "Blazingly Fast.", 94 - description: "Respond to incident faster than ever.", 99 + icon: "bell", 100 + catchline: "Get alerted.", 101 + description: 102 + "Get notified via Email, SMS, Slack, Discord,... before your users do.", 95 103 }, 96 104 ], 97 105 },
+4 -1
apps/web/src/config/plans.ts
··· 28 28 "1 status page", 29 29 "subdomain", 30 30 "10m, 30m, 1h checks", 31 + "email, slack, discord notifications", 31 32 ], 32 33 action: { 33 34 text: "Start Now", ··· 40 41 cost: 29, 41 42 features: [ 42 43 "20 monitors", 43 - "5 status page", 44 + "5 status pages", 44 45 "custom domain", 45 46 "1m, 5m, 10m, 30m, 1h checks", 47 + "email, slack, discord, sms notifications", 48 + "status page subscription", 46 49 "5 team members", 47 50 ], 48 51 action: {
-2
packages/tinybird/pipes/home_stats.pipe
··· 14 14 {% else %} 15 15 WHERE cronTimestamp > 0 16 16 {% end %} 17 - {% else %} 18 - WHERE cronTimestamp > {{ Int64(cronTimestamp, 0) }} 19 17 {% end %} 20 18
+13
pnpm-lock.yaml
··· 212 212 cmdk: 213 213 specifier: 0.2.0 214 214 version: 0.2.0(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0) 215 + cobe: 216 + specifier: 0.6.3 217 + version: 0.6.3 215 218 contentlayer: 216 219 specifier: 0.3.4 217 220 version: 0.3.4(esbuild@0.19.5) ··· 6267 6270 - '@types/react' 6268 6271 dev: false 6269 6272 6273 + /cobe@0.6.3: 6274 + resolution: {integrity: sha512-WHr7X4o1ym94GZ96h7b1pNemZJacbOzd02dZtnVwuC4oWBaLg96PBmp2rIS1SAhUDhhC/QyS9WEqkpZIs/ZBTg==} 6275 + dependencies: 6276 + phenomenon: 1.6.0 6277 + dev: false 6278 + 6270 6279 /color-convert@1.9.3: 6271 6280 resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} 6272 6281 dependencies: ··· 10713 10722 '@types/estree': 1.0.3 10714 10723 estree-walker: 3.0.3 10715 10724 is-reference: 3.0.2 10725 + dev: false 10726 + 10727 + /phenomenon@1.6.0: 10728 + resolution: {integrity: sha512-7h9/fjPD3qNlgggzm88cY58l9sudZ6Ey+UmZsizfhtawO6E3srZQXywaNm2lBwT72TbpHYRPy7ytIHeBUD/G0A==} 10716 10729 dev: false 10717 10730 10718 10731 /picocolors@1.0.0: