Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: rate limit users based on subscription plan #131

Merged
merged 3 commits into from
Apr 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 10 additions & 10 deletions src/app/(dashboard)/dashboard/_components/store-switcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from "@radix-ui/react-icons"

import { type getStoresByUserId } from "@/lib/actions/store"
import { type getProgress } from "@/lib/queries/user"
import { type getUserPlanMetrics } from "@/lib/queries/user"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
Expand All @@ -28,19 +28,19 @@ import {
PopoverTrigger,
} from "@/components/ui/popover"

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

interface StoreSwitcherProps
extends React.ComponentPropsWithoutRef<typeof PopoverTrigger> {
userId: string
storesPromise: ReturnType<typeof getStoresByUserId>
progressPromise: ReturnType<typeof getProgress>
planMetricsPromise: ReturnType<typeof getUserPlanMetrics>
}

export function StoreSwitcher({
userId,
storesPromise,
progressPromise,
planMetricsPromise,
className,
...props
}: StoreSwitcherProps) {
Expand All @@ -51,15 +51,15 @@ export function StoreSwitcher({
const [showNewStoreDialog, setShowNewStoreDialog] = React.useState(false)

const stores = React.use(storesPromise)
const progress = React.use(progressPromise)
const planMetrics = React.use(planMetricsPromise)

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

return (
<>
<AddStoreDialog
<CreateStoreDialog
userId={userId}
progressPromise={progressPromise}
planMetricsPromise={planMetricsPromise}
open={showNewStoreDialog}
onOpenChange={setShowNewStoreDialog}
showTrigger={false}
Expand All @@ -70,13 +70,13 @@ export function StoreSwitcher({
variant="outline"
role="combobox"
aria-expanded={open}
aria-label="Select a team"
aria-label="Select a store"
className={cn("w-full justify-between", className)}
{...props}
>
{selectedStore?.name ?? "Select a store"}
<CaretSortIcon
className="ml-auto size-4 shrink-0 opacity-50"
className="ml-auto size-3.5 shrink-0 opacity-50"
aria-hidden="true"
/>
</Button>
Expand Down Expand Up @@ -121,7 +121,7 @@ export function StoreSwitcher({
setOpen(false)
setShowNewStoreDialog(true)
}}
disabled={progress.storeCount >= progress.storeLimit}
disabled={planMetrics.storeLimitExceeded}
>
<PlusCircledIcon className="mr-2 size-5" aria-hidden="true" />
Create store
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,28 @@

import { UserProfile as ClerkUserProfile } from "@clerk/nextjs"
import { dark } from "@clerk/themes"
import { type Theme } from "@clerk/types"
import type { Theme, UserProfileProps } from "@clerk/types"
import { useTheme } from "next-themes"

const appearance: Theme = {
baseTheme: undefined,
variables: {
borderRadius: "0.25rem",
},
elements: {
card: "shadow-none",
navbar: "hidden",
navbarMobileMenuButton: "hidden",
headerTitle: "hidden",
headerSubtitle: "hidden",
},
}

export function UserProfile() {
export function UserProfile({ ...props }: UserProfileProps) {
const { theme } = useTheme()

return (
<ClerkUserProfile
appearance={{
...appearance,
baseTheme: theme === "dark" ? dark : appearance.baseTheme,
baseTheme: theme === "light" ? appearance.baseTheme : dark,
variables: {
...appearance.variables,
colorBackground: theme === "light" ? "#fafafa" : undefined,
},
}}
{...props}
/>
)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from "next"
import { env } from "@/env.js"

import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"
import {
PageHeader,
PageHeaderDescription,
Expand All @@ -18,16 +19,17 @@ export const metadata: Metadata = {

export default function AccountPage() {
return (
<Shell variant="sidebar">
<Shell variant="sidebar" className="overflow-hidden">
<PageHeader>
<PageHeaderHeading size="sm">Account</PageHeaderHeading>
<PageHeaderDescription size="sm">
Manage your account settings
</PageHeaderDescription>
</PageHeader>
<section className="w-full overflow-hidden">
<ScrollArea className="w-full pb-3.5">
<UserProfile />
</section>
<ScrollBar orientation="horizontal" />
</ScrollArea>
</Shell>
)
}
20 changes: 8 additions & 12 deletions src/app/(dashboard)/dashboard/billing/_components/billing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Link from "next/link"
import type { SubscriptionPlanWithPrice, UserSubscriptionPlan } from "@/types"
import { CheckIcon } from "@radix-ui/react-icons"

import { type getUserUsageMetrics } from "@/lib/queries/user"
import { getPlanLimits } from "@/lib/subscription"
import { cn, formatDate } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
Expand All @@ -20,22 +21,17 @@ import { ManageSubscriptionForm } from "@/components/manage-subscription-form"
interface BillingProps {
subscriptionPlanPromise: Promise<UserSubscriptionPlan | null>
subscriptionPlansPromise: Promise<SubscriptionPlanWithPrice[]>
usagePromise: Promise<{
storeCount: number
productCount: number
}>
usageMetricsPromise: ReturnType<typeof getUserUsageMetrics>
}

export async function Billing({
subscriptionPlanPromise,
subscriptionPlansPromise,
usagePromise,
usageMetricsPromise,
}: BillingProps) {
const [subscriptionPlan, subscriptionPlans, usage] = await Promise.all([
subscriptionPlanPromise,
subscriptionPlansPromise,
usagePromise,
])
const [subscriptionPlan, subscriptionPlans, usageMetrics] = await Promise.all(
[subscriptionPlanPromise, subscriptionPlansPromise, usageMetricsPromise]
)

const { storeLimit, productLimit } = getPlanLimits({
planTitle: subscriptionPlan?.title,
Expand Down Expand Up @@ -68,13 +64,13 @@ export async function Billing({
<CardContent className="grid gap-6 sm:grid-cols-2">
<UsageCard
title="Stores"
count={usage.storeCount}
count={usageMetrics.storeCount}
limit={storeLimit}
moreInfo="The number of stores you can create on the current plan."
/>
<UsageCard
title="Products"
count={usage.productCount}
count={usageMetrics.productCount}
limit={productLimit}
moreInfo="The number of products you can create on the current plan."
/>
Expand Down
6 changes: 3 additions & 3 deletions src/app/(dashboard)/dashboard/billing/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { env } from "@/env.js"
import { RocketIcon } from "@radix-ui/react-icons"

import { getSubscriptionPlan, getSubscriptionPlans } from "@/lib/actions/stripe"
import { getCachedUser, getUsage } from "@/lib/queries/user"
import { getCachedUser, getUserUsageMetrics } from "@/lib/queries/user"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import {
PageHeader,
Expand All @@ -32,7 +32,7 @@ export default async function BillingPage() {

const subscriptionPlanPromise = getSubscriptionPlan({ userId: user.id })
const subscriptionPlansPromise = getSubscriptionPlans()
const usagePromise = getUsage({ userId: user.id })
const usageMetricsPromise = getUserUsageMetrics({ userId: user.id })

return (
<Shell variant="sidebar">
Expand Down Expand Up @@ -63,7 +63,7 @@ export default async function BillingPage() {
<Billing
subscriptionPlanPromise={subscriptionPlanPromise}
subscriptionPlansPromise={subscriptionPlansPromise}
usagePromise={usagePromise}
usageMetricsPromise={usageMetricsPromise}
/>
</React.Suspense>
</Shell>
Expand Down
8 changes: 4 additions & 4 deletions src/app/(dashboard)/dashboard/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { redirect } from "next/navigation"

import { getStoresByUserId } from "@/lib/actions/store"
import { getCachedUser, getProgress } from "@/lib/queries/user"
import { getCachedUser, getUserPlanMetrics } from "@/lib/queries/user"
import { SiteFooter } from "@/components/layouts/site-footer"

import { DashboardHeader } from "./_components/dashboard-header"
Expand All @@ -20,7 +20,7 @@ export default async function DashboardLayout({
}

const storesPromise = getStoresByUserId({ userId: user.id })
const progressPromise = getProgress({ userId: user.id })
const planMetricsPromise = getUserPlanMetrics({ userId: user.id })

return (
<SidebarProvider>
Expand All @@ -31,7 +31,7 @@ export default async function DashboardLayout({
<StoreSwitcher
userId={user.id}
storesPromise={storesPromise}
progressPromise={progressPromise}
planMetricsPromise={planMetricsPromise}
/>
</DashboardSidebar>
</DashboardSidebarSheet>
Expand All @@ -44,7 +44,7 @@ export default async function DashboardLayout({
<StoreSwitcher
userId={user.id}
storesPromise={storesPromise}
progressPromise={progressPromise}
planMetricsPromise={planMetricsPromise}
/>
</DashboardSidebar>
<main className="flex w-full flex-col overflow-hidden">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import {
} from "@/lib/actions/product"
import { getErrorMessage } from "@/lib/handle-error"
import {
addProductSchema,
type AddProductSchema,
createProductSchema,
type CreateProductSchema,
} from "@/lib/validations/product"
import { useUploadFile } from "@/hooks/use-upload-file"
import { Button } from "@/components/ui/button"
Expand Down Expand Up @@ -48,23 +48,26 @@ import { FilesCard } from "@/components/cards/FilesCard"
import { FileUploader } from "@/components/file-uploader"
import { Icons } from "@/components/icons"

interface AddProductFormProps {
interface CreateProductFormProps {
storeId: string
promises: Promise<{
categories: Awaited<ReturnType<typeof getCategories>>
subcategories: Awaited<ReturnType<typeof getSubcategories>>
}>
}

export function AddProductForm({ storeId, promises }: AddProductFormProps) {
export function CreateProductForm({
storeId,
promises,
}: CreateProductFormProps) {
const { categories, subcategories } = React.use(promises)

const [loading, setLoading] = React.useState(false)
const { uploadFiles, progresses, uploadedFiles, isUploading } =
useUploadFile("productImage")

const form = useForm<AddProductSchema>({
resolver: zodResolver(addProductSchema),
const form = useForm<CreateProductSchema>({
resolver: zodResolver(createProductSchema),
defaultValues: {
name: "",
description: "",
Expand All @@ -76,7 +79,7 @@ export function AddProductForm({ storeId, promises }: AddProductFormProps) {
},
})

function onSubmit(input: AddProductSchema) {
function onSubmit(input: CreateProductSchema) {
setLoading(true)

toast.promise(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
CardTitle,
} from "@/components/ui/card"

import { AddProductForm } from "./_components/add-product-form"
import { CreateProductForm } from "./_components/create-product-form"

export const metadata: Metadata = {
metadataBase: new URL(env.NEXT_PUBLIC_APP_URL),
Expand Down Expand Up @@ -46,7 +46,7 @@ export default async function NewProductPage({ params }: NewProductPageProps) {
<CardDescription>Add a new product to your store</CardDescription>
</CardHeader>
<CardContent>
<AddProductForm storeId={storeId} promises={promises} />
<CreateProductForm storeId={storeId} promises={promises} />
</CardContent>
</Card>
)
Expand Down
Loading