From 54711a8d8d1e41a62dd3833bbddd46c81cb45aca Mon Sep 17 00:00:00 2001 From: sadmann7 Date: Sun, 10 Mar 2024 14:19:02 +0600 Subject: [PATCH] refactor: dashboard layout --- .../_components/dashboard-header.tsx | 33 ++++++ .../_components/dashboard-sidebar-sheet.tsx | 61 ++++++++++ .../_components/dashboard-sidebar.tsx | 47 ++++++++ .../_components/sidebar-provider.tsx | 38 +++++++ .../dashboard/_components}/store-switcher.tsx | 40 +++---- src/app/(dashboard)/dashboard/layout.tsx | 46 +++++--- .../dashboard/stores/[storeId]/layout.tsx | 50 ++------- .../stores/_components/add-store-dialog.tsx | 7 +- src/components/icons.tsx | 31 ++++++ src/components/layouts/auth-dropdown.tsx | 101 +++++++++++++++++ src/components/layouts/mobile-nav.tsx | 67 +++++------ src/components/layouts/sidebar-nav.tsx | 8 +- src/components/layouts/site-header.tsx | 104 +----------------- 13 files changed, 411 insertions(+), 222 deletions(-) create mode 100644 src/app/(dashboard)/dashboard/_components/dashboard-header.tsx create mode 100644 src/app/(dashboard)/dashboard/_components/dashboard-sidebar-sheet.tsx create mode 100644 src/app/(dashboard)/dashboard/_components/dashboard-sidebar.tsx create mode 100644 src/app/(dashboard)/dashboard/_components/sidebar-provider.tsx rename src/{components => app/(dashboard)/dashboard/_components}/store-switcher.tsx (75%) create mode 100644 src/components/layouts/auth-dropdown.tsx diff --git a/src/app/(dashboard)/dashboard/_components/dashboard-header.tsx b/src/app/(dashboard)/dashboard/_components/dashboard-header.tsx new file mode 100644 index 00000000..220cdb84 --- /dev/null +++ b/src/app/(dashboard)/dashboard/_components/dashboard-header.tsx @@ -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 ( +
+
+ +
+
+ ) +} diff --git a/src/app/(dashboard)/dashboard/_components/dashboard-sidebar-sheet.tsx b/src/app/(dashboard)/dashboard/_components/dashboard-sidebar-sheet.tsx new file mode 100644 index 00000000..0d998dde --- /dev/null +++ b/src/app/(dashboard)/dashboard/_components/dashboard-sidebar-sheet.tsx @@ -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 ( + + + + + +
+ setOpen(false)} + > +
+ {children} +
+
+ ) +} diff --git a/src/app/(dashboard)/dashboard/_components/dashboard-sidebar.tsx b/src/app/(dashboard)/dashboard/_components/dashboard-sidebar.tsx new file mode 100644 index 00000000..05f9d192 --- /dev/null +++ b/src/app/(dashboard)/dashboard/_components/dashboard-sidebar.tsx @@ -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 { + promises: Promise<{ + stores: Awaited> + subscriptionPlan: Awaited> + }> +} + +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 ( + + ) +} diff --git a/src/app/(dashboard)/dashboard/_components/sidebar-provider.tsx b/src/app/(dashboard)/dashboard/_components/sidebar-provider.tsx new file mode 100644 index 00000000..f20e0d85 --- /dev/null +++ b/src/app/(dashboard)/dashboard/_components/sidebar-provider.tsx @@ -0,0 +1,38 @@ +"use client" + +import * as React from "react" + +interface SidebarContextProps { + open: boolean + setOpen: React.Dispatch> +} + +const SidebarContext = React.createContext({ + open: false, + setOpen: () => {}, +}) + +export const SidebarProvider = ({ children }: React.PropsWithChildren) => { + const [open, setOpen] = React.useState(false) + + return ( + + {children} + + ) +} + +export const useSidebar = () => { + const context = React.useContext(SidebarContext) + + if (!context) { + throw new Error("useSidebar must be used within a SidebarProvider") + } + + return context +} diff --git a/src/components/store-switcher.tsx b/src/app/(dashboard)/dashboard/_components/store-switcher.tsx similarity index 75% rename from src/components/store-switcher.tsx rename to src/app/(dashboard)/dashboard/_components/store-switcher.tsx index 4aae191b..a6f4b630 100644 --- a/src/components/store-switcher.tsx +++ b/src/app/(dashboard)/dashboard/_components/store-switcher.tsx @@ -3,6 +3,7 @@ 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, @@ -10,6 +11,7 @@ import { 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 { @@ -30,44 +32,43 @@ import { interface StoreSwitcherProps extends React.ComponentPropsWithoutRef { - currentStore: Pick + currentStore?: Awaited>[number] stores: Pick[] - 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 ( - - + + @@ -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" > @@ -95,7 +96,7 @@ export function StoreSwitcher({ { - router.push(dashboardRedirectPath) - setIsOpen(false) - setIsDialogOpen(true) + setOpen(false) + setShowStoreDialog(true) }} > ({ stores, subscriptionPlan })) + return ( -
- -
- -
{children}
+ +
+ + + + + +
+ +
+ {children} +
+
+
- -
+ ) } diff --git a/src/app/(dashboard)/dashboard/stores/[storeId]/layout.tsx b/src/app/(dashboard)/dashboard/stores/[storeId]/layout.tsx index 2b97d1a6..2e5cc134 100644 --- a/src/app/(dashboard)/dashboard/stores/[storeId]/layout.tsx +++ b/src/app/(dashboard)/dashboard/stores/[storeId]/layout.tsx @@ -1,18 +1,12 @@ -import { notFound, redirect } from "next/navigation" -import { db } from "@/db" -import { stores } from "@/db/schema" -import { eq } from "drizzle-orm" +import { redirect } from "next/navigation" import { getCacheduser } from "@/lib/actions/auth" -import { getSubscriptionPlan } from "@/lib/actions/stripe" -import { getDashboardRedirectPath } from "@/lib/subscription" import { PageHeader, PageHeaderDescription, PageHeaderHeading, } from "@/components/page-header" import { Shell } from "@/components/shell" -import { StoreSwitcher } from "@/components/store-switcher" import { StoreTabs } from "@/components/store-tabs" interface StoreLayoutProps extends React.PropsWithChildren { @@ -33,44 +27,14 @@ export default async function StoreLayout({ redirect("/signin") } - const allStores = await db - .select({ - id: stores.id, - name: stores.name, - }) - .from(stores) - .where(eq(stores.userId, user.id)) - - const store = allStores.find((store) => store.id === storeId) - - if (!store) { - notFound() - } - - const subscriptionPlan = await getSubscriptionPlan({ userId: user.id }) - - const redirectPath = getDashboardRedirectPath({ - subscriptionPlan, - storeCount: allStores.length, - }) - return ( -
- - Dashboard - - Manage your store - - - {allStores.length > 1 ? ( - - ) : null} -
+ + Dashboard + + Manage your store + +
{children}
diff --git a/src/app/(dashboard)/dashboard/stores/_components/add-store-dialog.tsx b/src/app/(dashboard)/dashboard/stores/_components/add-store-dialog.tsx index 335e0afa..d71a2080 100644 --- a/src/app/(dashboard)/dashboard/stores/_components/add-store-dialog.tsx +++ b/src/app/(dashboard)/dashboard/stores/_components/add-store-dialog.tsx @@ -43,7 +43,8 @@ import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" import { Icons } from "@/components/icons" -interface AddStoreDialogProps { +interface AddStoreDialogProps + extends React.ComponentPropsWithRef { userId: string subscriptionPlanPromise: ReturnType } @@ -53,6 +54,7 @@ type Inputs = z.infer export function AddStoreDialog({ userId, subscriptionPlanPromise, + ...props }: AddStoreDialogProps) { const subscriptionPlan = React.use(subscriptionPlanPromise) @@ -103,6 +105,7 @@ export function AddStoreDialog({ } setOpen(open) }} + {...props} > @@ -148,6 +151,7 @@ export function AddStoreDialog({ } setOpen(open) }} + {...props} > @@ -203,6 +207,7 @@ function AddStoreForm({
), + menu: ({ ...props }: IconProps) => ( + + + + + + ), nextjs: (props: IconProps) => ( + {user ? ( + + + + + + +
+

+ {user.firstName} {user.lastName} +

+

+ {email} +

+
+ + + + + + + + + + + + + + + + + + + + ) : ( + + )} + + ) +} diff --git a/src/components/layouts/mobile-nav.tsx b/src/components/layouts/mobile-nav.tsx index 60af6474..b23ee124 100644 --- a/src/components/layouts/mobile-nav.tsx +++ b/src/components/layouts/mobile-nav.tsx @@ -3,11 +3,11 @@ import * as React from "react" import Link from "next/link" import { useSelectedLayoutSegment } from "next/navigation" -import type { MainNavItem, SidebarNavItem } from "@/types" -import { ViewVerticalIcon } from "@radix-ui/react-icons" +import type { MainNavItem } from "@/types" import { siteConfig } from "@/config/site" import { cn } from "@/lib/utils" +import { useMediaQuery } from "@/hooks/use-media-query" import { Accordion, AccordionContent, @@ -20,47 +20,34 @@ import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet" import { Icons } from "@/components/icons" interface MobileNavProps { - mainNavItems?: MainNavItem[] - sidebarNavItems: SidebarNavItem[] + items?: MainNavItem[] } -export function MobileNav({ mainNavItems, sidebarNavItems }: MobileNavProps) { +export function MobileNav({ items }: MobileNavProps) { + const isDesktop = useMediaQuery("(min-width: 1024px)") const segment = useSelectedLayoutSegment() - const [isOpen, setIsOpen] = React.useState(false) + const [open, setOpen] = React.useState(false) - const navItems = React.useMemo(() => { - const items = mainNavItems ?? [] - const myAccountItem = { - title: "My Account", - items: sidebarNavItems, - } - const myAccountIndex = items.findIndex( - (item) => item.title === "My Account" - ) - if (myAccountIndex !== -1) { - items.splice(myAccountIndex, 1) - } - items.splice(1, 0, myAccountItem) - return items - }, [mainNavItems, sidebarNavItems]) + if (isDesktop) return null return ( - + - -
+ +
setIsOpen(false)} + onClick={() => setOpen(false)} >
- item.title)} - className="w-full" - > - {navItems?.map((item, index) => ( + + {items?.map((item, index) => ( {item.title} @@ -87,8 +70,9 @@ export function MobileNav({ mainNavItems, sidebarNavItems }: MobileNavProps) { key={index} href={String(subItem.href)} segment={String(segment)} - setIsOpen={setIsOpen} + setOpen={setOpen} disabled={subItem.disabled} + className="m-1" > {subItem.title} @@ -113,11 +97,12 @@ export function MobileNav({ mainNavItems, sidebarNavItems }: MobileNavProps) { ) } -interface MobileLinkProps extends React.PropsWithChildren { +interface MobileLinkProps + extends React.AnchorHTMLAttributes { href: string disabled?: boolean segment: string - setIsOpen: React.Dispatch> + setOpen: React.Dispatch> } function MobileLink({ @@ -125,7 +110,9 @@ function MobileLink({ href, disabled, segment, - setIsOpen, + setOpen, + className, + ...props }: MobileLinkProps) { return ( setIsOpen(false)} + onClick={() => setOpen(false)} + {...props} > {children} diff --git a/src/components/layouts/sidebar-nav.tsx b/src/components/layouts/sidebar-nav.tsx index 6251efc7..daef1144 100644 --- a/src/components/layouts/sidebar-nav.tsx +++ b/src/components/layouts/sidebar-nav.tsx @@ -6,8 +6,7 @@ import type { SidebarNavItem } from "@/types" import { ChevronLeftIcon } from "@radix-ui/react-icons" import { cn } from "@/lib/utils" - -import { Icons } from "../icons" +import { Icons } from "@/components/icons" export interface SidebarNavProps extends React.HTMLAttributes { items: SidebarNavItem[] @@ -19,7 +18,10 @@ export function SidebarNav({ items, className, ...props }: SidebarNavProps) { if (!items?.length) return null return ( -
+
{items.map((item, index) => { const Icon = item.icon ? Icons[item.icon] : ChevronLeftIcon diff --git a/src/components/layouts/site-header.tsx b/src/components/layouts/site-header.tsx index e516be76..585a849d 100644 --- a/src/components/layouts/site-header.tsx +++ b/src/components/layouts/site-header.tsx @@ -1,24 +1,8 @@ -import Link from "next/link" import type { User } from "@clerk/nextjs/server" -import { DashboardIcon, ExitIcon, GearIcon } from "@radix-ui/react-icons" -import { dashboardConfig } from "@/config/dashboard" import { siteConfig } from "@/config/site" -import { getUserEmail } from "@/lib/utils" -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" -import { Button } from "@/components/ui/button" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" import { CartSheet } from "@/components/checkout/cart-sheet" -import { Icons } from "@/components/icons" +import { AuthDropdown } from "@/components/layouts/auth-dropdown" import { MainNav } from "@/components/layouts/main-nav" import { MobileNav } from "@/components/layouts/mobile-nav" import { ProductsCommandMenu } from "@/components/products-command-menu" @@ -28,98 +12,16 @@ interface SiteHeaderProps { } export function SiteHeader({ user }: SiteHeaderProps) { - const initials = `${user?.firstName?.charAt(0) ?? ""} ${ - user?.lastName?.charAt(0) ?? "" - }` - const email = getUserEmail(user) - return (
- +