Openstatus www.openstatus.dev

feat: create dialog form example (#43)

authored by

Maximilian Kaske and committed by
GitHub
c6c4d741 200db5ab

+177 -25
+12
README.md
··· 40 40 - [Next.js](https://nextjs.org/) 41 41 - [Tailwind CSS](https://tailwindcss.com/) 42 42 - [shadcn/ui](https://ui.shadcn.com/) 43 + - [tinybird](http://tinybird.co/) 44 + - [planetscale](http://planetscale.com/) 45 + - [drizzle](https://orm.drizzle.team/) 46 + - [clerk](https://clerk.com/) 43 47 44 48 ## Getting Started 45 49 ··· 74 78 75 79 5. Open [http://localhost:3000](http://localhost:3000) with your browser to see 76 80 the result. 81 + 82 + For [clerk](https://clerk.com), you will need to create a webhook endpoint. To 83 + access the link via ngrok (free), after login, append `/api/webhook/clerk` to 84 + the link you get after entering: 85 + 86 + ``` 87 + $ ngrok http 3000 88 + ``` 77 89 78 90 ## Roadmap 79 91
+1
apps/web/package.json
··· 18 18 "@radix-ui/react-dialog": "^1.0.4", 19 19 "@radix-ui/react-dropdown-menu": "^2.0.5", 20 20 "@radix-ui/react-hover-card": "^1.0.6", 21 + "@radix-ui/react-label": "^2.0.2", 21 22 "@radix-ui/react-select": "^1.2.2", 22 23 "@radix-ui/react-slot": "^1.0.2", 23 24 "@radix-ui/react-toast": "^1.1.4",
+6 -1
apps/web/src/app/app/(dashboard)/[workspaceId]/loading.tsx
··· 1 1 import { Container } from "@/components/dashboard/container"; 2 2 import { Header } from "@/components/dashboard/header"; 3 + import { Skeleton } from "@/components/ui/skeleton"; 3 4 4 5 export default function Loading() { 5 6 return ( 6 7 <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 7 - <Header.Skeleton /> 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> 8 13 <Container.Skeleton /> 9 14 <Container.Skeleton /> 10 15 <Container.Skeleton />
+8 -1
apps/web/src/app/app/(dashboard)/[workspaceId]/page.tsx
··· 2 2 3 3 import { Container } from "@/components/dashboard/container"; 4 4 import { Header } from "@/components/dashboard/header"; 5 + import { DialogForm } from "@/components/forms/dialog-form"; 6 + import { Button } from "@/components/ui/button"; 5 7 import { wait } from "@/lib/utils"; 6 8 7 9 export default async function DashboardPage() { 8 10 await wait(1000); 9 11 return ( 10 12 <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 11 - <Header title="Dashboard" description="Overview of all your websites" /> 13 + <div className="col-span-full flex w-full justify-between"> 14 + <Header title="Dashboard" description="Overview of all your websites"> 15 + {/* <Button>Create</Button> */} 16 + <DialogForm /> 17 + </Header> 18 + </div> 12 19 <Container title="Hello"></Container> 13 20 <Container title="World"></Container> 14 21 </div>
+1
apps/web/src/app/app/(dashboard)/layout.tsx
··· 18 18 </Shell> 19 19 <main className="z-10 flex w-full flex-1 flex-col items-start justify-center"> 20 20 <Shell className="relative flex-1"> 21 + {/* The `top-4` is represented in Shell with a `py-4` class */} 21 22 <nav className="absolute right-4 top-4 block md:hidden"> 22 23 <AppMenu /> 23 24 </nav>
+24 -10
apps/web/src/components/dashboard/header.tsx
··· 6 6 description?: string | null; 7 7 } 8 8 9 - function Header({ title, description, className }: HeaderProps) { 9 + /** 10 + * use `children` to include a Button e.g. 11 + */ 12 + function Header({ title, description, className, children }: HeaderProps) { 10 13 return ( 11 - <div className={cn("col-span-full grid gap-1", className)}> 12 - <h1 className="font-cal text-3xl">{title}</h1> 13 - {description ? ( 14 - <p className="text-muted-foreground">{description}</p> 15 - ) : null} 14 + <div 15 + className={cn( 16 + "col-span-full mr-12 flex w-full justify-between md:mr-0", 17 + className, 18 + )} 19 + > 20 + <div className="grid w-full gap-1"> 21 + <h1 className="font-cal text-3xl">{title}</h1> 22 + {description ? ( 23 + <p className="text-muted-foreground">{description}</p> 24 + ) : null} 25 + </div> 26 + {children} 16 27 </div> 17 28 ); 18 29 } 19 30 20 - function HeaderSkeleton() { 31 + function HeaderSkeleton({ children }: { children?: React.ReactNode }) { 21 32 return ( 22 - <div className="col-span-full grid gap-3"> 23 - <Skeleton className="h-8 w-[200px]" /> 24 - <Skeleton className="h-4 w-[300px]" /> 33 + <div className="col-span-full mr-12 flex w-full justify-between md:mr-0"> 34 + <div className="grid w-full gap-3"> 35 + <Skeleton className="h-8 w-full max-w-[200px]" /> 36 + <Skeleton className="h-4 w-full max-w-[300px]" /> 37 + </div> 38 + {children} 25 39 </div> 26 40 ); 27 41 }
+1 -1
apps/web/src/components/dashboard/shell.tsx
··· 6 6 return ( 7 7 <div 8 8 className={cn( 9 - "border-border w-full rounded-lg border p-3 backdrop-blur-[2px] md:p-6", 9 + "border-border w-full rounded-lg border px-3 py-4 backdrop-blur-[2px] md:p-6", 10 10 className, 11 11 )} 12 12 >
+71
apps/web/src/components/forms/dialog-form.tsx
··· 1 + "use client"; 2 + 3 + import * as React from "react"; 4 + import { Loader2 } from "lucide-react"; 5 + 6 + import { 7 + Dialog, 8 + DialogContent, 9 + DialogDescription, 10 + DialogFooter, 11 + DialogHeader, 12 + DialogTitle, 13 + DialogTrigger, 14 + } from "@/components/ui/dialog"; 15 + import { wait } from "@/lib/utils"; 16 + import { Button } from "../ui/button"; 17 + import { Input } from "../ui/input"; 18 + import { Label } from "../ui/label"; 19 + 20 + // EXAMPLE 21 + export function DialogForm() { 22 + const [saving, setSaving] = React.useState(false); 23 + const [open, setOpen] = React.useState(false); 24 + 25 + // either like that or with a user action 26 + async function handleSubmit(e: React.FormEvent<HTMLFormElement>) { 27 + e.preventDefault(); 28 + setSaving(true); 29 + const data = Object.fromEntries(new FormData(e.currentTarget)); 30 + 31 + console.log(data); // { url: "" } 32 + await wait(1500); 33 + // save data 34 + 35 + setOpen(false); 36 + setSaving(false); 37 + } 38 + 39 + return ( 40 + <Dialog open={open} onOpenChange={(value) => setOpen(value)}> 41 + <DialogTrigger asChild> 42 + <Button>Create</Button> 43 + </DialogTrigger> 44 + <DialogContent> 45 + <DialogHeader> 46 + <DialogTitle>Create Monitor</DialogTitle> 47 + <DialogDescription> 48 + Type an URL that you want to ping periodically. 49 + </DialogDescription> 50 + </DialogHeader> 51 + <form onSubmit={handleSubmit} id="monitor"> 52 + <div className="grid w-full items-center gap-1.5"> 53 + <Label htmlFor="url">Link</Label> 54 + <Input 55 + id="url" 56 + name="url" 57 + type="url" 58 + placeholder="https://" 59 + required 60 + /> 61 + </div> 62 + </form> 63 + <DialogFooter> 64 + <Button type="submit" form="monitor" disabled={saving}> 65 + {!saving ? "Confirm" : <Loader2 className="h-4 w-4 animate-spin" />} 66 + </Button> 67 + </DialogFooter> 68 + </DialogContent> 69 + </Dialog> 70 + ); 71 + }
+1 -5
apps/web/src/components/layout/app-menu.tsx
··· 8 8 import { 9 9 Sheet, 10 10 SheetContent, 11 - SheetDescription, 12 11 SheetHeader, 13 12 SheetTitle, 14 13 SheetTrigger, 15 14 } from "../ui/sheet"; 16 - // your navigation 17 15 import { AppSidebar } from "./app-sidebar"; 18 16 19 17 export function AppMenu() { ··· 35 33 <SheetContent> 36 34 <SheetHeader> 37 35 <SheetTitle className="text-left">Navigation</SheetTitle> 38 - <SheetDescription> 39 - <AppSidebar /> {/* is here */} 40 - </SheetDescription> 36 + <AppSidebar /> 41 37 </SheetHeader> 42 38 </SheetContent> 43 39 </Sheet>
-7
apps/web/src/components/layout/app-sidebar.tsx
··· 7 7 import { cn } from "@/lib/utils"; 8 8 import { Icons } from "../icons"; 9 9 10 - type PageConfig = { 11 - title: string; 12 - href: string; 13 - icon: string; 14 - disabled: boolean; 15 - }; 16 - 17 10 export function AppSidebar() { 18 11 const pathname = usePathname(); 19 12 const params = useParams();
+28
apps/web/src/components/ui/label.tsx
··· 1 + // THIS IS **NOT** a shadcn ui component. TBD if we want to keep `import/consistent-type-specifier-style` 2 + // https://ui.shadcn.com/docs/components/label 3 + 4 + "use client"; 5 + 6 + import * as React from "react"; 7 + import { cva } from "class-variance-authority"; 8 + import type { VariantProps } from "class-variance-authority"; 9 + 10 + import { cn } from "@/lib/utils"; 11 + 12 + const labelVariants = cva( 13 + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", 14 + ); 15 + 16 + export interface LabelProps 17 + extends React.LabelHTMLAttributes<HTMLLabelElement>, 18 + VariantProps<typeof labelVariants> {} 19 + 20 + const Label = React.forwardRef<HTMLLabelElement, LabelProps>( 21 + ({ className, ...props }, ref) => ( 22 + <label ref={ref} className={cn(labelVariants(), className)} {...props} /> 23 + ), 24 + ); 25 + 26 + Label.displayName = "Label"; 27 + 28 + export { Label };
+24
pnpm-lock.yaml
··· 62 62 '@radix-ui/react-hover-card': 63 63 specifier: ^1.0.6 64 64 version: 1.0.6(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) 65 + '@radix-ui/react-label': 66 + specifier: ^2.0.2 67 + version: 2.0.2(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) 65 68 '@radix-ui/react-select': 66 69 specifier: ^1.2.2 67 70 version: 1.2.2(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) ··· 2021 2024 '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.12)(react@18.2.0) 2022 2025 '@types/react': 18.2.12 2023 2026 react: 18.2.0 2027 + dev: false 2028 + 2029 + /@radix-ui/react-label@2.0.2(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0): 2030 + resolution: {integrity: sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==} 2031 + peerDependencies: 2032 + '@types/react': '*' 2033 + '@types/react-dom': '*' 2034 + react: ^16.8 || ^17.0 || ^18.0 2035 + react-dom: ^16.8 || ^17.0 || ^18.0 2036 + peerDependenciesMeta: 2037 + '@types/react': 2038 + optional: true 2039 + '@types/react-dom': 2040 + optional: true 2041 + dependencies: 2042 + '@babel/runtime': 7.22.5 2043 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) 2044 + '@types/react': 18.2.12 2045 + '@types/react-dom': 18.2.5 2046 + react: 18.2.0 2047 + react-dom: 18.2.0(react@18.2.0) 2024 2048 dev: false 2025 2049 2026 2050 /@radix-ui/react-menu@2.0.5(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0):