Skip to content

Commit

Permalink
feat: rete-limit store and product creation
Browse files Browse the repository at this point in the history
  • Loading branch information
sadmann7 committed Mar 10, 2024
1 parent 54711a8 commit a600d47
Show file tree
Hide file tree
Showing 18 changed files with 363 additions and 269 deletions.
30 changes: 4 additions & 26 deletions src/app/(dashboard)/dashboard/_components/dashboard-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,23 @@
"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>>
}>
children: React.ReactNode
}

export function DashboardSidebar({
promises,
children,
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">
<div className="pr-6 pt-4 lg:pt-6">{children}</div>
<ScrollArea className="h-[calc(100vh-8rem)] py-2.5 pr-6">
<SidebarNav items={dashboardConfig.sidebarNav} className="p-1 pt-4" />
</ScrollArea>
</aside>
Expand Down
123 changes: 60 additions & 63 deletions src/app/(dashboard)/dashboard/_components/store-switcher.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
"use client"

import * as React from "react"
import { usePathname, useRouter } from "next/navigation"
import { type Store } from "@/db/schema"
import type { UserSubscriptionPlan } from "@/types"
import { useParams, usePathname, useRouter } from "next/navigation"
import {
CaretSortIcon,
CheckIcon,
CircleIcon,
PlusCircledIcon,
} from "@radix-ui/react-icons"

import type { getStoresByUserId } from "@/lib/actions/store"
import { type getStoresByUserId } from "@/lib/actions/store"
import { type getProgress } from "@/lib/actions/user"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
Expand All @@ -23,111 +22,109 @@ import {
CommandList,
CommandSeparator,
} from "@/components/ui/command"
import { Dialog, DialogTrigger } from "@/components/ui/dialog"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"

import { AddStoreDialog } from "../stores/_components/add-store-dialog"

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

export function StoreSwitcher({
currentStore,
stores,
subscriptionPlan,
userId,
storesPromise,
progressPromise,
className,
...props
}: StoreSwitcherProps) {
const { storeId } = useParams<{ storeId: string }>()
const router = useRouter()
const pathname = usePathname()
const [open, setOpen] = React.useState(false)
const [showStoreDialog, setShowStoreDialog] = React.useState(false)
const [showNewStoreDialog, setShowNewStoreDialog] = React.useState(false)

const stores = React.use(storesPromise)

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

return (
<Dialog open={showStoreDialog} onOpenChange={setShowStoreDialog}>
<>
<AddStoreDialog
userId={userId}
progressPromise={progressPromise}
open={showNewStoreDialog}
onOpenChange={setShowNewStoreDialog}
showTrigger={false}
/>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={cn("w-full justify-between px-3", className)}
aria-label="Select a team"
className={cn("w-full justify-between", className)}
{...props}
>
<CircleIcon className="mr-2 size-4" aria-hidden="true" />
<span className="line-clamp-1">
{currentStore?.name ? currentStore?.name : "Select a store"}
</span>
{selectedStore?.name ?? "Select a store"}
<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">
<Command>
<CommandList>
<CommandInput placeholder="Search store..." />
<CommandEmpty>No store found.</CommandEmpty>
<CommandGroup>
{stores.map((store) => (
<CommandItem
key={store.id}
onSelect={() => {
router.push(
pathname.replace(
String(currentStore?.id),
String(store.id)
)
)
setOpen(false)
}}
className="text-sm"
>
<CircleIcon className="mr-2 size-4" aria-hidden="true" />
<span className="line-clamp-1">{store.name}</span>
<CheckIcon
className={cn(
"ml-auto size-4",
currentStore?.id === store.id
? "opacity-100"
: "opacity-0"
)}
aria-hidden="true"
/>
</CommandItem>
))}
</CommandGroup>
{stores.map((store) => (
<CommandItem
key={store.id}
onSelect={() => {
setOpen(false)
router.push(`/dashboard/stores/${store.id}`)
}}
className="text-sm"
>
<CircleIcon className="mr-2 size-5" aria-hidden="true" />
{store.name}
<CheckIcon
className={cn(
"ml-auto size-4",
selectedStore?.id === store.id
? "opacity-100"
: "opacity-0"
)}
aria-hidden="true"
/>
</CommandItem>
))}
</CommandList>
<CommandSeparator />
<CommandList>
<CommandGroup>
<DialogTrigger asChild>
<CommandItem
onSelect={() => {
setOpen(false)
setShowStoreDialog(true)
}}
>
<PlusCircledIcon
className="mr-2 size-4"
aria-hidden="true"
/>
Create store
</CommandItem>
</DialogTrigger>
<CommandItem
onSelect={() => {
setOpen(false)
setShowNewStoreDialog(true)
}}
>
<PlusCircledIcon className="mr-2 size-5" aria-hidden="true" />
Create store
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</Dialog>
</>
)
}
16 changes: 8 additions & 8 deletions src/app/(dashboard)/dashboard/billing/_components/billing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Link from "next/link"
import type { SubscriptionPlanWithPrice, UserSubscriptionPlan } from "@/types"
import { CheckIcon } from "@radix-ui/react-icons"

import { getPlanLimits } from "@/lib/subscription"
import { getUsageWithProgress } from "@/lib/subscription"
import { cn, formatDate } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
Expand All @@ -14,8 +14,8 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { ManageSubscriptionForm } from "@/components/manage-subscription-form"

import { ManageSubscriptionForm } from "./manage-subscription-form"
import { UsageCard } from "./usage-card"

interface BillingProps {
Expand All @@ -38,12 +38,12 @@ export async function Billing({
usagePromise,
])

const { storeLimit, productLimit } = getPlanLimits({
planTitle: subscriptionPlan?.title ?? "free",
})

const storeProgress = Math.floor((usage.storeCount / storeLimit) * 100)
const productProgress = Math.floor((usage.productCount / productLimit) * 100)
const { storeLimit, storeProgress, productLimit, productProgress } =
getUsageWithProgress({
planTitle: subscriptionPlan?.title ?? "free",
storeCount: usage.storeCount,
productCount: usage.productCount,
})

return (
<>
Expand Down
32 changes: 17 additions & 15 deletions src/app/(dashboard)/dashboard/billing/_components/usage-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ interface UsageCardProps {
usage: number
limit: number
progress: number
moreInfo: string
moreInfo?: string
}

export function UsageCard({
Expand All @@ -33,21 +33,23 @@ export function UsageCard({
return (
<Card>
<CardHeader>
<div className="flex items-center space-x-2">
<div className="flex items-center gap-2">
<CardTitle>{title}</CardTitle>
<HoverCard>
<HoverCardTrigger asChild>
<Button variant="ghost" size="icon" className="size-4">
<QuestionMarkCircledIcon
className="size-full"
aria-hidden="true"
/>
</Button>
</HoverCardTrigger>
<HoverCardContent className="w-80" sideOffset={8}>
<p className="text-sm">{moreInfo}</p>
</HoverCardContent>
</HoverCard>
{moreInfo && (
<HoverCard>
<HoverCardTrigger asChild>
<Button variant="ghost" size="icon" className="size-4">
<QuestionMarkCircledIcon
className="size-full"
aria-hidden="true"
/>
</Button>
</HoverCardTrigger>
<HoverCardContent className="w-80" sideOffset={8}>
<p className="text-sm">{moreInfo}</p>
</HoverCardContent>
</HoverCard>
)}
</div>
<CardDescription>
{usage} / {limit} stores ({progress}%)
Expand Down
3 changes: 1 addition & 2 deletions src/app/(dashboard)/dashboard/billing/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import { redirect } from "next/navigation"
import { env } from "@/env.js"
import { RocketIcon } from "@radix-ui/react-icons"

import { getCacheduser } from "@/lib/actions/auth"
import { getUsage } from "@/lib/actions/store"
import { getSubscriptionPlan, getSubscriptionPlans } from "@/lib/actions/stripe"
import { getCacheduser, getUsage } from "@/lib/actions/user"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import {
PageHeader,
Expand Down
27 changes: 18 additions & 9 deletions src/app/(dashboard)/dashboard/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { redirect } from "next/navigation"

import { getCacheduser } from "@/lib/actions/auth"
import { getStoresByUserId } from "@/lib/actions/store"
import { getSubscriptionPlan } from "@/lib/actions/stripe"
import { getCacheduser, getProgress } from "@/lib/actions/user"
import { SiteFooter } from "@/components/layouts/site-footer"

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"
import { StoreSwitcher } from "./_components/store-switcher"

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

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

return (
<SidebarProvider>
<div className="flex min-h-screen flex-col">
<DashboardHeader user={user}>
<DashboardSidebarSheet className="lg:hidden">
<DashboardSidebar className="pl-4" promises={promises} />
<DashboardSidebar className="pl-4">
<StoreSwitcher
userId={user.id}
storesPromise={storesPromise}
progressPromise={progressPromise}
/>
</DashboardSidebar>
</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}
/>
>
<StoreSwitcher
userId={user.id}
storesPromise={storesPromise}
progressPromise={progressPromise}
/>
</DashboardSidebar>
<main className="flex min-h-[200vh] w-full flex-col overflow-hidden">
{children}
</main>
Expand Down
Loading

0 comments on commit a600d47

Please sign in to comment.