Openstatus www.openstatus.dev

feat: loading and more (#41)

* feat: loading and more

* fix: css stuff

* chore: small style changes

* fix: mobile sheet close

* chore: rename link

authored by

Maximilian Kaske and committed by
GitHub
a285bace 80a42779

+307 -58
+13
apps/web/src/app/app/(dashboard)/[workspaceId]/endpoint/loading.tsx
··· 1 + import { Container } from "@/components/dashboard/container"; 2 + import { Header } from "@/components/dashboard/header"; 3 + 4 + export default function Loading() { 5 + return ( 6 + <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 7 + <Header.Skeleton /> 8 + <Container.Skeleton /> 9 + <Container.Skeleton /> 10 + <Container.Skeleton /> 11 + </div> 12 + ); 13 + }
+8 -3
apps/web/src/app/app/(dashboard)/[workspaceId]/endpoint/page.tsx
··· 1 1 import * as React from "react"; 2 2 3 - import { Header } from "@/components/header"; 3 + import { Container } from "@/components/dashboard/container"; 4 + import { Header } from "@/components/dashboard/header"; 5 + import { wait } from "@/lib/utils"; 4 6 5 - export default function EndpointPage() { 7 + export default async function EndpointPage() { 8 + await wait(1000); 6 9 return ( 7 - <div> 10 + <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 8 11 <Header title="Endpoint" description="Overview of all your sites." /> 12 + <Container title="Hello"></Container> 13 + <Container title="World"></Container> 9 14 </div> 10 15 ); 11 16 }
+13
apps/web/src/app/app/(dashboard)/[workspaceId]/incident/loading.tsx
··· 1 + import { Container } from "@/components/dashboard/container"; 2 + import { Header } from "@/components/dashboard/header"; 3 + 4 + export default function Loading() { 5 + return ( 6 + <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 7 + <Header.Skeleton /> 8 + <Container.Skeleton /> 9 + <Container.Skeleton /> 10 + <Container.Skeleton /> 11 + </div> 12 + ); 13 + }
+8 -3
apps/web/src/app/app/(dashboard)/[workspaceId]/incident/page.tsx
··· 1 1 import * as React from "react"; 2 2 3 - import { Header } from "@/components/header"; 3 + import { Container } from "@/components/dashboard/container"; 4 + import { Header } from "@/components/dashboard/header"; 5 + import { wait } from "@/lib/utils"; 4 6 5 - export default function IncidentPage() { 7 + export default async function IncidentPage() { 8 + await wait(1000); 6 9 return ( 7 - <div> 10 + <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 8 11 <Header title="Monitor" description="Overview of all the responses." /> 12 + <Container title="Hello"></Container> 13 + <Container title="World"></Container> 9 14 </div> 10 15 ); 11 16 }
+13
apps/web/src/app/app/(dashboard)/[workspaceId]/loading.tsx
··· 1 + import { Container } from "@/components/dashboard/container"; 2 + import { Header } from "@/components/dashboard/header"; 3 + 4 + export default function Loading() { 5 + return ( 6 + <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 7 + <Header.Skeleton /> 8 + <Container.Skeleton /> 9 + <Container.Skeleton /> 10 + <Container.Skeleton /> 11 + </div> 12 + ); 13 + }
+13
apps/web/src/app/app/(dashboard)/[workspaceId]/monitor/loading.tsx
··· 1 + import { Container } from "@/components/dashboard/container"; 2 + import { Header } from "@/components/dashboard/header"; 3 + 4 + export default function Loading() { 5 + return ( 6 + <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 7 + <Header.Skeleton /> 8 + <Container.Skeleton /> 9 + <Container.Skeleton /> 10 + <Container.Skeleton /> 11 + </div> 12 + ); 13 + }
+8 -3
apps/web/src/app/app/(dashboard)/[workspaceId]/monitor/page.tsx
··· 1 1 import * as React from "react"; 2 2 3 - import { Header } from "@/components/header"; 3 + import { Container } from "@/components/dashboard/container"; 4 + import { Header } from "@/components/dashboard/header"; 5 + import { wait } from "@/lib/utils"; 4 6 5 - export default function MonitorPage() { 7 + export default async function MonitorPage() { 8 + await wait(1000); 6 9 return ( 7 - <div> 10 + <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 8 11 <Header title="Monitor" description="Overview of all the responses." /> 12 + <Container title="Hello"></Container> 13 + <Container title="World"></Container> 9 14 </div> 10 15 ); 11 16 }
+9 -7
apps/web/src/app/app/(dashboard)/[workspaceId]/page.tsx
··· 1 1 import * as React from "react"; 2 - import { auth, currentUser } from "@clerk/nextjs"; 3 2 4 - import { Header } from "@/components/header"; 3 + import { Container } from "@/components/dashboard/container"; 4 + import { Header } from "@/components/dashboard/header"; 5 + import { wait } from "@/lib/utils"; 5 6 6 - export default async function AppPage() { 7 - const user = await currentUser(); 8 - console.log({ user }); 7 + export default async function DashboardPage() { 8 + await wait(1000); 9 9 return ( 10 - <div> 11 - <Header title="App" description="Overview of all your websites" /> 10 + <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 11 + <Header title="Dashboard" description="Overview of all your websites" /> 12 + <Container title="Hello"></Container> 13 + <Container title="World"></Container> 12 14 </div> 13 15 ); 14 16 }
+5 -4
apps/web/src/app/app/(dashboard)/layout.tsx
··· 1 1 import * as React from "react"; 2 2 3 + import { Shell } from "@/components/dashboard/shell"; 3 4 import { AppHeader } from "@/components/layout/app-header"; 4 5 import { AppMenu } from "@/components/layout/app-menu"; 5 6 import { AppSidebar } from "@/components/layout/app-sidebar"; ··· 10 11 <div className="container mx-auto flex min-h-screen w-full flex-col items-center justify-center space-y-6 p-4 md:p-8"> 11 12 <AppHeader /> 12 13 <div className="flex w-full flex-1 gap-6 md:gap-8"> 13 - <aside className="border-border hidden rounded-lg border p-3 backdrop-blur-[2px] md:block md:p-6"> 14 + <Shell className="hidden max-w-min md:block"> 14 15 <nav> 15 16 <AppSidebar /> 16 17 </nav> 17 - </aside> 18 + </Shell> 18 19 <main className="z-10 flex w-full flex-1 flex-col items-start justify-center"> 19 - <div className="border-border relative w-full flex-1 rounded-lg border p-3 backdrop-blur-[2px] md:p-6"> 20 + <Shell className="relative flex-1"> 20 21 <nav className="absolute right-4 top-4 block md:hidden"> 21 22 <AppMenu /> 22 23 </nav> 23 24 {children} 24 - </div> 25 + </Shell> 25 26 </main> 26 27 </div> 27 28 <Footer />
-14
apps/web/src/app/app/(dashboard)/page.tsx
··· 1 - import * as React from "react"; 2 - import { auth, currentUser } from "@clerk/nextjs"; 3 - 4 - import { Header } from "@/components/header"; 5 - 6 - export default async function AppPage() { 7 - const user = await currentUser(); 8 - console.log({ user }); 9 - return ( 10 - <div> 11 - <Header title="App" description="Overview of all your websites" /> 12 - </div> 13 - ); 14 - }
+38
apps/web/src/components/dashboard/container.tsx
··· 1 + import { 2 + Card, 3 + CardContent, 4 + CardDescription, 5 + CardFooter, 6 + CardHeader, 7 + CardTitle, 8 + } from "@/components/ui/card"; 9 + import { cn } from "@/lib/utils"; 10 + import { Skeleton } from "../ui/skeleton"; 11 + 12 + interface CardProps extends React.HTMLAttributes<HTMLDivElement> { 13 + title: string; 14 + description?: string; 15 + } 16 + 17 + function Container({ title, description, className, children }: CardProps) { 18 + return ( 19 + <Card className={cn("border-border/50 w-full shadow-none", className)}> 20 + <CardHeader> 21 + <CardTitle className="text-lg font-medium tracking-normal"> 22 + {title} 23 + </CardTitle> 24 + {description ? <CardDescription>{description}</CardDescription> : null} 25 + </CardHeader> 26 + {/* potentially `asChild` */} 27 + <CardContent>{children}</CardContent> 28 + </Card> 29 + ); 30 + } 31 + 32 + function ContainerSkeleton() { 33 + return <Skeleton className="h-24 w-full" />; 34 + } 35 + 36 + Container.Skeleton = ContainerSkeleton; 37 + 38 + export { Container };
+31
apps/web/src/components/dashboard/header.tsx
··· 1 + import { cn } from "@/lib/utils"; 2 + import { Skeleton } from "../ui/skeleton"; 3 + 4 + interface HeaderProps extends React.HTMLAttributes<HTMLDivElement> { 5 + title: string; 6 + description?: string | null; 7 + } 8 + 9 + function Header({ title, description, className }: HeaderProps) { 10 + 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} 16 + </div> 17 + ); 18 + } 19 + 20 + function HeaderSkeleton() { 21 + return ( 22 + <div className="col-span-full grid gap-3"> 23 + <Skeleton className="h-8 w-[200px]" /> 24 + <Skeleton className="h-4 w-[300px]" /> 25 + </div> 26 + ); 27 + } 28 + 29 + Header.Skeleton = HeaderSkeleton; 30 + 31 + export { Header };
+18
apps/web/src/components/dashboard/shell.tsx
··· 1 + import { cn } from "@/lib/utils"; 2 + 3 + type ShellProps = React.HTMLAttributes<HTMLDivElement>; 4 + 5 + function Shell({ children, className }: ShellProps) { 6 + return ( 7 + <div 8 + className={cn( 9 + "border-border w-full rounded-lg border p-3 backdrop-blur-[2px] md:p-6", 10 + className, 11 + )} 12 + > 13 + {children} 14 + </div> 15 + ); 16 + } 17 + 18 + export { Shell };
-17
apps/web/src/components/header.tsx
··· 1 - import { cn } from "@/lib/utils"; 2 - 3 - interface HeaderProps extends React.HTMLAttributes<HTMLDivElement> { 4 - title: string; 5 - description?: string | null; 6 - } 7 - 8 - export function Header({ title, description, className }: HeaderProps) { 9 - return ( 10 - <div className={cn("grid gap-1", className)}> 11 - <h1 className="font-cal text-3xl">{title}</h1> 12 - {description ? ( 13 - <p className="text-muted-foreground">{description}</p> 14 - ) : null} 15 - </div> 16 - ); 17 - }
+16 -3
apps/web/src/components/layout/app-menu.tsx
··· 1 + "use client"; 2 + 3 + import * as React from "react"; 4 + import { usePathname, useSearchParams } from "next/navigation"; 1 5 import { Menu } from "lucide-react"; 2 6 3 7 import { Button } from "../ui/button"; ··· 9 13 SheetTitle, 10 14 SheetTrigger, 11 15 } from "../ui/sheet"; 16 + // your navigation 12 17 import { AppSidebar } from "./app-sidebar"; 13 18 14 19 export function AppMenu() { 20 + const [open, setOpen] = React.useState(false); 21 + const pathname = usePathname(); 22 + const searchParams = useSearchParams(); 23 + 24 + React.useEffect(() => { 25 + setOpen(false); 26 + }, [pathname, searchParams]); // remove searchParams if not needed 27 + 15 28 return ( 16 - <Sheet> 29 + <Sheet open={open} onOpenChange={(value) => setOpen(value)}> 17 30 <SheetTrigger asChild> 18 31 <Button size="icon" variant="outline"> 19 32 <Menu className="h-6 w-6" /> ··· 21 34 </SheetTrigger> 22 35 <SheetContent> 23 36 <SheetHeader> 24 - <SheetTitle>Navigation</SheetTitle> 37 + <SheetTitle className="text-left">Navigation</SheetTitle> 25 38 <SheetDescription> 26 - <AppSidebar /> 39 + <AppSidebar /> {/* is here */} 27 40 </SheetDescription> 28 41 </SheetHeader> 29 42 </SheetContent>
+11 -3
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 + 10 17 export function AppSidebar() { 11 18 const pathname = usePathname(); 12 19 const params = useParams(); ··· 14 21 <ul className="grid gap-1"> 15 22 {pagesConfig.map(({ title, href, icon, disabled }) => { 16 23 const Icon = Icons[icon]; 24 + const link = `/app/${params.workspaceId}${href}`; // TODO: add 17 25 return ( 18 26 <li key={title} className="w-full"> 19 27 <Link 20 - href={`/app/${params.workspaceId}/${href}`} 28 + href={link} 21 29 className={cn( 22 - "hover:bg-muted/50 hover:text-foreground text-muted-foreground group -mx-2 flex w-full min-w-[200px] items-center rounded-md border border-transparent px-3 py-1", 23 - pathname === href && 30 + "hover:bg-muted/50 hover:text-foreground text-muted-foreground group flex w-full min-w-[200px] items-center rounded-md border border-transparent px-3 py-1", 31 + pathname === link && 24 32 "bg-muted/50 border-border text-foreground", 25 33 disabled && "pointer-events-none opacity-60", 26 34 )}
+83
apps/web/src/components/ui/card.tsx
··· 1 + import * as React from "react"; 2 + 3 + import { cn } from "@/lib/utils"; 4 + 5 + const Card = React.forwardRef< 6 + HTMLDivElement, 7 + React.HTMLAttributes<HTMLDivElement> 8 + >(({ className, ...props }, ref) => ( 9 + <div 10 + ref={ref} 11 + className={cn( 12 + "bg-card text-card-foreground rounded-xl border shadow", 13 + className, 14 + )} 15 + {...props} 16 + /> 17 + )); 18 + Card.displayName = "Card"; 19 + 20 + const CardHeader = React.forwardRef< 21 + HTMLDivElement, 22 + React.HTMLAttributes<HTMLDivElement> 23 + >(({ className, ...props }, ref) => ( 24 + <div 25 + ref={ref} 26 + className={cn("flex flex-col space-y-1.5 p-6", className)} 27 + {...props} 28 + /> 29 + )); 30 + CardHeader.displayName = "CardHeader"; 31 + 32 + const CardTitle = React.forwardRef< 33 + HTMLParagraphElement, 34 + React.HTMLAttributes<HTMLHeadingElement> 35 + >(({ className, ...props }, ref) => ( 36 + <h3 37 + ref={ref} 38 + className={cn("font-semibold leading-none tracking-tight", className)} 39 + {...props} 40 + /> 41 + )); 42 + CardTitle.displayName = "CardTitle"; 43 + 44 + const CardDescription = React.forwardRef< 45 + HTMLParagraphElement, 46 + React.HTMLAttributes<HTMLParagraphElement> 47 + >(({ className, ...props }, ref) => ( 48 + <p 49 + ref={ref} 50 + className={cn("text-muted-foreground text-sm", className)} 51 + {...props} 52 + /> 53 + )); 54 + CardDescription.displayName = "CardDescription"; 55 + 56 + const CardContent = React.forwardRef< 57 + HTMLDivElement, 58 + React.HTMLAttributes<HTMLDivElement> 59 + >(({ className, ...props }, ref) => ( 60 + <div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> 61 + )); 62 + CardContent.displayName = "CardContent"; 63 + 64 + const CardFooter = React.forwardRef< 65 + HTMLDivElement, 66 + React.HTMLAttributes<HTMLDivElement> 67 + >(({ className, ...props }, ref) => ( 68 + <div 69 + ref={ref} 70 + className={cn(" flex items-center p-6 pt-0", className)} 71 + {...props} 72 + /> 73 + )); 74 + CardFooter.displayName = "CardFooter"; 75 + 76 + export { 77 + Card, 78 + CardHeader, 79 + CardFooter, 80 + CardTitle, 81 + CardDescription, 82 + CardContent, 83 + };
+15
apps/web/src/components/ui/skeleton.tsx
··· 1 + import { cn } from "@/lib/utils"; 2 + 3 + function Skeleton({ 4 + className, 5 + ...props 6 + }: React.HTMLAttributes<HTMLDivElement>) { 7 + return ( 8 + <div 9 + className={cn("bg-primary/10 animate-pulse rounded-md", className)} 10 + {...props} 11 + /> 12 + ); 13 + } 14 + 15 + export { Skeleton };
+1 -1
apps/web/src/config/pages.ts
··· 12 12 { 13 13 title: "Dashboard", 14 14 description: "Get an overview of what's hot.", 15 - href: "/", 15 + href: "", 16 16 icon: "layout-dashboard", 17 17 }, 18 18 {
+4
apps/web/src/lib/utils.ts
··· 5 5 export function cn(...inputs: ClassValue[]) { 6 6 return twMerge(clsx(inputs)); 7 7 } 8 + 9 + export function wait(ms: number) { 10 + return new Promise((resolve) => setTimeout(resolve, ms)); 11 + }