Skip to content

Commit

Permalink
refactor: dashboard layout
Browse files Browse the repository at this point in the history
  • Loading branch information
sadmann7 committed Mar 10, 2024
1 parent 0933fe9 commit 54711a8
Show file tree
Hide file tree
Showing 13 changed files with 411 additions and 222 deletions.
33 changes: 33 additions & 0 deletions src/app/(dashboard)/dashboard/_components/dashboard-header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Link from "next/link"
import type { User } from "@clerk/nextjs/server"

import { siteConfig } from "@/config/site"
import { Icons } from "@/components/icons"
import { AuthDropdown } from "@/components/layouts/auth-dropdown"

interface DashboardHeaderProps {
user: User | null
children: React.ReactNode
}

export function DashboardHeader({ user, children }: DashboardHeaderProps) {
return (
<header className="sticky top-0 z-50 w-full border-b bg-background">
<div className="container flex h-16 items-center">
<Link href="/" className="hidden items-center space-x-2 lg:flex">
<Icons.logo className="size-6" aria-hidden="true" />
<span className="hidden font-bold lg:inline-block">
{siteConfig.name}
</span>
<span className="sr-only">Home</span>
</Link>
{children}
<div className="flex flex-1 items-center justify-end space-x-4">
<nav className="flex items-center space-x-2">
<AuthDropdown user={user} />
</nav>
</div>
</div>
</header>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"use client"

import Link from "next/link"

import { siteConfig } from "@/config/site"
import { cn } from "@/lib/utils"
import { useMediaQuery } from "@/hooks/use-media-query"
import { Button, type ButtonProps } from "@/components/ui/button"
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
import { Icons } from "@/components/icons"

import { useSidebar } from "./sidebar-provider"

export interface SidebarSheetProps extends ButtonProps {}

export function DashboardSidebarSheet({
children,
className,
...props
}: SidebarSheetProps) {
const { open, setOpen } = useSidebar()
const isDesktop = useMediaQuery("(min-width: 1024px)")

if (isDesktop) return null

return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button
variant="ghost"
size="icon"
className={cn(
"size-5 hover:bg-transparent focus-visible:bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0",
className
)}
{...props}
>
<Icons.menu aria-hidden="true" />
<span className="sr-only">Toggle Menu</span>
</Button>
</SheetTrigger>
<SheetContent
side="left"
className="inset-y-0 flex h-auto w-[300px] flex-col items-center px-0 pt-9"
>
<div className="w-full self-start px-7">
<Link
href="/"
className="flex items-center"
onClick={() => setOpen(false)}
>
<Icons.logo className="mr-2 size-4" aria-hidden="true" />
<span className="font-bold">{siteConfig.name}</span>
<span className="sr-only">Home</span>
</Link>
</div>
{children}
</SheetContent>
</Sheet>
)
}
47 changes: 47 additions & 0 deletions src/app/(dashboard)/dashboard/_components/dashboard-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"use client"

import * as React from "react"
import { useParams } from "next/navigation"

import { dashboardConfig } from "@/config/dashboard"
import { type getStoresByUserId } from "@/lib/actions/store"
import { type getSubscriptionPlan } from "@/lib/actions/stripe"
import { cn } from "@/lib/utils"
import { ScrollArea } from "@/components/ui/scroll-area"
import { SidebarNav } from "@/components/layouts/sidebar-nav"

import { StoreSwitcher } from "./store-switcher"

interface DashboardSidebarProps extends React.HTMLAttributes<HTMLElement> {
promises: Promise<{
stores: Awaited<ReturnType<typeof getStoresByUserId>>
subscriptionPlan: Awaited<ReturnType<typeof getSubscriptionPlan>>
}>
}

export function DashboardSidebar({
promises,
className,
...props
}: DashboardSidebarProps) {
const { stores, subscriptionPlan } = React.use(promises)

const { storeId } = useParams<{ storeId: string }>()

const currentStore = stores.find((store) => store.id === storeId)

return (
<aside className={cn("w-full", className)} {...props}>
<div className="pr-6 pt-4 lg:pt-6">
<StoreSwitcher
currentStore={currentStore}
stores={stores}
subscriptionPlan={subscriptionPlan}
/>
</div>
<ScrollArea className="h-[calc(100vh-8rem)] py-4 pr-6">
<SidebarNav items={dashboardConfig.sidebarNav} className="p-1 pt-4" />
</ScrollArea>
</aside>
)
}
38 changes: 38 additions & 0 deletions src/app/(dashboard)/dashboard/_components/sidebar-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"use client"

import * as React from "react"

interface SidebarContextProps {
open: boolean
setOpen: React.Dispatch<React.SetStateAction<boolean>>
}

const SidebarContext = React.createContext<SidebarContextProps>({
open: false,
setOpen: () => {},
})

export const SidebarProvider = ({ children }: React.PropsWithChildren) => {
const [open, setOpen] = React.useState(false)

return (
<SidebarContext.Provider
value={{
open,
setOpen,
}}
>
{children}
</SidebarContext.Provider>
)
}

export const useSidebar = () => {
const context = React.useContext(SidebarContext)

if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider")
}

return context
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
import * as React from "react"
import { usePathname, useRouter } from "next/navigation"
import { type Store } from "@/db/schema"
import type { UserSubscriptionPlan } from "@/types"
import {
CaretSortIcon,
CheckIcon,
CircleIcon,
PlusCircledIcon,
} from "@radix-ui/react-icons"

import type { getStoresByUserId } from "@/lib/actions/store"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
Expand All @@ -30,44 +32,43 @@ import {

interface StoreSwitcherProps
extends React.ComponentPropsWithoutRef<typeof PopoverTrigger> {
currentStore: Pick<Store, "id" | "name">
currentStore?: Awaited<ReturnType<typeof getStoresByUserId>>[number]
stores: Pick<Store, "id" | "name">[]
dashboardRedirectPath: string
subscriptionPlan: UserSubscriptionPlan | null
}

export function StoreSwitcher({
currentStore,
stores,
dashboardRedirectPath,
subscriptionPlan,
className,
...props
}: StoreSwitcherProps) {
const router = useRouter()
const pathname = usePathname()
const [isOpen, setIsOpen] = React.useState(false)
const [isDialogOpen, setIsDialogOpen] = React.useState(false)
const [open, setOpen] = React.useState(false)
const [showStoreDialog, setShowStoreDialog] = React.useState(false)

return (
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<Popover open={isOpen} onOpenChange={setIsOpen}>
<Dialog open={showStoreDialog} onOpenChange={setShowStoreDialog}>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={isOpen}
aria-label="Select a store"
className={cn(
"w-full justify-between px-3 xxs:w-[180px]",
className
)}
aria-expanded={open}
className={cn("w-full justify-between px-3", className)}
{...props}
>
<CircleIcon className="mr-2 size-4" aria-hidden="true" />
<span className="line-clamp-1">{currentStore.name}</span>
<span className="line-clamp-1">
{currentStore?.name ? currentStore?.name : "Select a store"}
</span>
<CaretSortIcon
className="ml-auto size-4 shrink-0 opacity-50"
aria-hidden="true"
/>
<span className="sr-only">Select a store</span>
</Button>
</PopoverTrigger>
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
Expand All @@ -82,11 +83,11 @@ export function StoreSwitcher({
onSelect={() => {
router.push(
pathname.replace(
String(currentStore.id),
String(currentStore?.id),
String(store.id)
)
)
setIsOpen(false)
setOpen(false)
}}
className="text-sm"
>
Expand All @@ -95,7 +96,7 @@ export function StoreSwitcher({
<CheckIcon
className={cn(
"ml-auto size-4",
currentStore.id === store.id
currentStore?.id === store.id
? "opacity-100"
: "opacity-0"
)}
Expand All @@ -111,9 +112,8 @@ export function StoreSwitcher({
<DialogTrigger asChild>
<CommandItem
onSelect={() => {
router.push(dashboardRedirectPath)
setIsOpen(false)
setIsDialogOpen(true)
setOpen(false)
setShowStoreDialog(true)
}}
>
<PlusCircledIcon
Expand Down
46 changes: 31 additions & 15 deletions src/app/(dashboard)/dashboard/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { redirect } from "next/navigation"

import { dashboardConfig } from "@/config/dashboard"
import { getCacheduser } from "@/lib/actions/auth"
import { ScrollArea } from "@/components/ui/scroll-area"
import { SidebarNav } from "@/components/layouts/sidebar-nav"
import { getStoresByUserId } from "@/lib/actions/store"
import { getSubscriptionPlan } from "@/lib/actions/stripe"
import { SiteFooter } from "@/components/layouts/site-footer"
import { SiteHeader } from "@/components/layouts/site-header"

import { DashboardHeader } from "./_components/dashboard-header"
import { DashboardSidebar } from "./_components/dashboard-sidebar"
import { DashboardSidebarSheet } from "./_components/dashboard-sidebar-sheet"
import { SidebarProvider } from "./_components/sidebar-provider"

export default async function DashboardLayout({
children,
Expand All @@ -16,18 +19,31 @@ export default async function DashboardLayout({
redirect("/signin")
}

const promises = Promise.all([
getStoresByUserId({ userId: user.id }),
getSubscriptionPlan({ userId: user.id }),
]).then(([stores, subscriptionPlan]) => ({ stores, subscriptionPlan }))

return (
<div className="flex min-h-screen flex-col">
<SiteHeader user={user} />
<div className="container flex-1 items-start md:grid md:grid-cols-[220px_minmax(0,1fr)] md:gap-6 lg:grid-cols-[240px_minmax(0,1fr)] lg:gap-10">
<aside className="fixed top-14 z-30 -ml-2 hidden h-[calc(100vh-3.5rem)] w-full shrink-0 overflow-y-auto border-r md:sticky md:block">
<ScrollArea className="py-6 pr-6 lg:py-8">
<SidebarNav items={dashboardConfig.sidebarNav} className="p-1" />
</ScrollArea>
</aside>
<main className="flex w-full flex-col overflow-hidden">{children}</main>
<SidebarProvider>
<div className="flex min-h-screen flex-col">
<DashboardHeader user={user}>
<DashboardSidebarSheet className="lg:hidden">
<DashboardSidebar className="pl-4" promises={promises} />
</DashboardSidebarSheet>
</DashboardHeader>
<div className="container flex-1 items-start lg:grid lg:grid-cols-[240px_minmax(0,1fr)] lg:gap-10">
<DashboardSidebar
// the top-16 class is used for the dashboard-header of h-16, added extra 0.1rem to fix the sticky layout shift issue
className="top-[calc(theme('spacing.16')_+_0.1rem)] z-30 hidden border-r lg:sticky lg:block"
promises={promises}
/>
<main className="flex min-h-[200vh] w-full flex-col overflow-hidden">
{children}
</main>
</div>
<SiteFooter />
</div>
<SiteFooter />
</div>
</SidebarProvider>
)
}
Loading

0 comments on commit 54711a8

Please sign in to comment.