Openstatus www.openstatus.dev
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

wip: landing page design

mxkaske f277999d cd99a2e6

+186 -11
+4 -1
apps/web/next.config.mjs
··· 4 4 const nextConfig = { 5 5 reactStrictMode: true, 6 6 transpilePackages: ["ui"], 7 + experimental: { 8 + serverActions: true, 9 + }, 7 10 }; 8 11 9 - export default nextConfig; 12 + export default nextConfig;
+1
apps/web/package.json
··· 15 15 "class-variance-authority": "^0.6.0", 16 16 "clsx": "^1.2.1", 17 17 "lucide-react": "^0.244.0", 18 + "next-plausible": "3.7.2", 18 19 "next": "13.4.6", 19 20 "react": "18.2.0", 20 21 "react-dom": "18.2.0",
+18
apps/web/src/app/action.ts
··· 1 + "use server"; 2 + 3 + export async function waitlist(data: FormData) { 4 + const email = data.get("email"); 5 + if (email) { 6 + await wait(3000); 7 + // TODO: save email to Highstorm 8 + } 9 + return; 10 + } 11 + 12 + const wait = (ms: number): Promise<void> => { 13 + return new Promise((resolve) => { 14 + setTimeout(() => { 15 + resolve(); 16 + }, ms); 17 + }); 18 + };
+58
apps/web/src/app/components/background.tsx
··· 1 + "use client"; 2 + import React from "react"; 3 + 4 + export default function Background({ 5 + children, 6 + }: { 7 + children: React.ReactNode; 8 + }) { 9 + React.useEffect(() => { 10 + function mouseMoveEvent(e: MouseEvent) { 11 + const body = document.body; 12 + 13 + const targetX = e.clientX; 14 + const targetY = e.clientY; 15 + 16 + body.style.setProperty("--x", `${targetX}px`); 17 + body.style.setProperty("--y", `${targetY}px`); 18 + } 19 + 20 + document.addEventListener("mousemove", mouseMoveEvent); 21 + return () => document.removeEventListener("mousemove", mouseMoveEvent); 22 + }, []); 23 + 24 + return ( 25 + <> 26 + <div className="absolute inset-0 z-[-1]"> 27 + <div className="absolute inset-0 z-[-1] bg-muted-foreground/20" /> 28 + <div 29 + className="absolute z-[-1] h-56 w-56 rounded-full -translate-y-1/2 -translate-x-1/2 bg-gradient-radial from-muted-foreground/70 from-0% to-transparent to-90% blur-md" 30 + style={{ left: "var(--x)", top: "var(--y)" }} 31 + /> 32 + <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%"> 33 + <defs> 34 + <pattern 35 + id="dotted-pattern" 36 + width="16" 37 + height="16" 38 + patternUnits="userSpaceOnUse" 39 + > 40 + <circle cx="2" cy="2" r="1" fill="black" /> 41 + </pattern> 42 + <mask id="dots-mask"> 43 + <rect width="100%" height="100%" fill="white" /> 44 + <rect width="100%" height="100%" fill="url(#dotted-pattern)" /> 45 + </mask> 46 + </defs> 47 + <rect 48 + width="100%" 49 + height="100%" 50 + fill="hsl(var(--background))" 51 + mask="url(#dots-mask)" 52 + /> 53 + </svg> 54 + </div> 55 + {children} 56 + </> 57 + ); 58 + }
+25
apps/web/src/app/components/submit-button.tsx
··· 1 + "use client"; 2 + 3 + import { Button } from "@/components/ui/button"; 4 + import { experimental_useFormStatus as useFormStatus } from "react-dom"; 5 + 6 + export function SubmitButton() { 7 + const { pending } = useFormStatus(); 8 + return ( 9 + <Button 10 + type="submit" 11 + disabled={pending} 12 + className="w-20 disabled:opacity-100" 13 + > 14 + {pending ? ( 15 + <div className="flex items-center justify-center gap-1"> 16 + <div className="h-1 w-1 animate-pulse direction-alternate duration-700 rounded-full bg-primary-foreground" /> 17 + <div className="h-1 w-1 animate-pulse direction-alternate duration-700 delay-150 rounded-full bg-primary-foreground" /> 18 + <div className="h-1 w-1 animate-pulse direction-alternate duration-700 delay-300 rounded-full bg-primary-foreground" /> 19 + </div> 20 + ) : ( 21 + "Join" 22 + )} 23 + </Button> 24 + ); 25 + }
+12 -5
apps/web/src/app/layout.tsx
··· 3 3 import { Inter } from "next/font/google"; 4 4 import LocalFont from "next/font/local"; 5 5 6 + import PlausibleProvider from "next-plausible"; 7 + import Background from "./components/background"; 8 + 6 9 const inter = Inter({ subsets: ["latin"] }); 7 10 8 11 const calSans = LocalFont({ ··· 16 19 children: React.ReactNode; 17 20 }) { 18 21 return ( 19 - <ClerkProvider> 20 - <html lang="en"> 21 - <body className={`${inter.className} ${calSans.variable}`}>{children}</body> 22 - </html> 23 - </ClerkProvider> 22 + <html lang="en"> 23 + <ClerkProvider> 24 + <PlausibleProvider domain="openstatus.dev"> 25 + <body className={`${inter.className} ${calSans.variable}`}> 26 + <Background>{children}</Background> 27 + </body> 28 + </PlausibleProvider> 29 + </ClerkProvider> 30 + </html> 24 31 ); 25 32 }
+49 -5
apps/web/src/app/page.tsx
··· 1 - import { Button, Header } from "ui"; 1 + import { Badge } from "@/components/ui/badge"; 2 + import { Input } from "@/components/ui/input"; 3 + import { waitlist } from "./action"; 4 + import { SubmitButton } from "./components/submit-button"; 2 5 3 6 export default function Page() { 4 7 return ( 5 - <> 6 - <Header text="Web" /> 7 - <Button /> 8 - </> 8 + <main className="min-h-screen w-full flex flex-col p-4 md:p-8 space-y-6"> 9 + <div className="flex-1 flex flex-col justify-center"> 10 + <div className="mx-auto max-w-xl text-center"> 11 + <div className="rounded-lg border border-border backdrop-blur-[2px] p-8"> 12 + <Badge>Announcing Post</Badge> 13 + <h1 className="text-3xl text-foreground font-cal mb-6 mt-2"> 14 + OpenStatus 15 + </h1> 16 + <p className="text-muted-foreground mb-4"> 17 + Your Open Source Status Page. 18 + </p> 19 + <form action={waitlist} className="flex gap-1.5"> 20 + <Input 21 + id="email" 22 + name="email" 23 + type="email" 24 + placeholder="me@domain.com" 25 + required 26 + /> 27 + <SubmitButton /> 28 + </form> 29 + </div> 30 + </div> 31 + </div> 32 + <footer className="text-center text-sm text-muted-foreground mx-auto rounded-full px-4 py-2 border border-border backdrop-blur-[2px]"> 33 + Creating by{" "} 34 + <a 35 + href="https://twitter.com/mxkaske" 36 + target="_blank" 37 + rel="noreferrer" 38 + className="underline underline-offset-4 hover:no-underline" 39 + > 40 + @mxkaske 41 + </a>{" "} 42 + and{" "} 43 + <a 44 + href="https://twitter.com/thibaultleouay" 45 + target="_blank" 46 + rel="noreferrer" 47 + className="underline underline-offset-4 hover:no-underline" 48 + > 49 + @thibaultleouay 50 + </a> 51 + </footer> 52 + </main> 9 53 ); 10 54 }
+4
apps/web/tailwind.config.ts
··· 13 13 }, 14 14 }, 15 15 extend: { 16 + // REMINDER: added for background.tsx 17 + backgroundImage: { 18 + "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 19 + }, 16 20 colors: { 17 21 border: "hsl(var(--border))", 18 22 input: "hsl(var(--input))",
+15
pnpm-lock.yaml
··· 84 84 next: 85 85 specifier: 13.4.6 86 86 version: 13.4.6(react-dom@18.2.0)(react@18.2.0) 87 + next-plausible: 88 + specifier: 3.7.2 89 + version: 3.7.2(next@13.4.6)(react-dom@18.2.0)(react@18.2.0) 87 90 react: 88 91 specifier: 18.2.0 89 92 version: 18.2.0 ··· 2701 2704 /neo-async@2.6.2: 2702 2705 resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} 2703 2706 dev: true 2707 + 2708 + /next-plausible@3.7.2(next@13.4.6)(react-dom@18.2.0)(react@18.2.0): 2709 + resolution: {integrity: sha512-9PqFiVtD1kZO5gHFYTcgilHhg2WhMzD6I4NK/RUh9DGavD1N11IhNAvyGLFmvB3f4FtHC9IoAsauYDtQBt+riA==} 2710 + peerDependencies: 2711 + next: ^11.1.0 || ^12.0.0 || ^13.0.0 2712 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 2713 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 2714 + dependencies: 2715 + next: 13.4.6(react-dom@18.2.0)(react@18.2.0) 2716 + react: 18.2.0 2717 + react-dom: 18.2.0(react@18.2.0) 2718 + dev: false 2704 2719 2705 2720 /next@13.4.6(react-dom@18.2.0)(react@18.2.0): 2706 2721 resolution: {integrity: sha512-sjVqjxU+U2aXZnYt4Ud6CTLNNwWjdSfMgemGpIQJcN3Z7Jni9xRWbR0ie5fQzCg87aLqQVhKA2ud2gPoqJ9lGw==}