Skip to content

Commit 4dd9a26

Browse files
authored
Merge pull request #622 from mfts/fix/limit-links
2 parents 05ada08 + 8dc0b71 commit 4dd9a26

File tree

11 files changed

+275
-32
lines changed

11 files changed

+275
-32
lines changed

Diff for: components/Sidebar.tsx

+72-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import MenuIcon from "@/components/shared/icons/menu";
1717
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
1818

1919
import { usePlan } from "@/lib/swr/use-billing";
20-
import { cn } from "@/lib/utils";
20+
import useLimits from "@/lib/swr/use-limits";
21+
import { cn, nFormatter } from "@/lib/utils";
2122

2223
import Banner from "./banner";
2324
import ProBanner from "./billing/pro-banner";
@@ -26,6 +27,7 @@ import ProfileMenu from "./profile-menu";
2627
import SiderbarFolders from "./sidebar-folders";
2728
import { AddTeamModal } from "./teams/add-team-modal";
2829
import SelectTeam from "./teams/select-team";
30+
import { Progress } from "./ui/progress";
2931
import { ScrollArea } from "./ui/scroll-area";
3032

3133
export default function Sidebar() {
@@ -66,6 +68,9 @@ export const SidebarComponent = ({ className }: { className?: string }) => {
6668
const { data: session, status } = useSession();
6769
const { plan: userPlan, trial: userTrial, loading } = usePlan();
6870
const isTrial = !!userTrial;
71+
const { limits } = useLimits();
72+
const linksLimit = limits?.links;
73+
const documentsLimit = limits?.documents;
6974

7075
const router = useRouter();
7176
const { currentTeam, teams, isLoading }: TeamContextType =
@@ -281,6 +286,25 @@ export const SidebarComponent = ({ className }: { className?: string }) => {
281286
<ProBanner setShowProBanner={setShowProBanner} />
282287
) : null}
283288

289+
<div className="mb-2">
290+
{linksLimit ? (
291+
<UsageProgress
292+
title="Links"
293+
unit="links"
294+
usage={limits?.usage?.links}
295+
usageLimit={linksLimit}
296+
/>
297+
) : null}
298+
{documentsLimit ? (
299+
<UsageProgress
300+
title="Documents"
301+
unit="documents"
302+
usage={limits?.usage?.documents}
303+
usageLimit={documentsLimit}
304+
/>
305+
) : null}
306+
</div>
307+
284308
<div className="hidden w-full lg:block">
285309
<ProfileMenu size="large" />
286310
</div>
@@ -289,3 +313,50 @@ export const SidebarComponent = ({ className }: { className?: string }) => {
289313
</div>
290314
);
291315
};
316+
317+
function UsageProgress(data: {
318+
title: string;
319+
unit: string;
320+
usage?: number;
321+
usageLimit?: number;
322+
}) {
323+
let { title, unit, usage, usageLimit } = data;
324+
let usagePercentage = 0;
325+
if (usage !== undefined && usageLimit !== undefined) {
326+
usagePercentage = (usage / usageLimit) * 100;
327+
}
328+
329+
return (
330+
<div className="p-2">
331+
{/* <div className="flex items-center space-x-2">
332+
<h3 className="font-medium">{title}</h3>
333+
</div> */}
334+
335+
<div className="mt-2 flex flex-col space-y-2">
336+
{usage !== undefined && usageLimit !== undefined ? (
337+
<p className="text-sm text-foreground">
338+
<span
339+
className={cn(
340+
usagePercentage > 100 && "text-destructive dark:text-red-600",
341+
)}
342+
>
343+
{nFormatter(usage)}
344+
</span>{" "}
345+
/ {nFormatter(usageLimit)} {unit}{" "}
346+
<span
347+
className={cn(
348+
usagePercentage > 100 && "text-destructive dark:text-red-600",
349+
)}
350+
>
351+
({usagePercentage.toFixed(0)}
352+
%)
353+
</span>
354+
</p>
355+
) : (
356+
<div className="h-5 w-32 animate-pulse rounded-md bg-muted" />
357+
)}
358+
<Progress value={usagePercentage} className="h-2 bg-muted" max={100} />
359+
</div>
360+
</div>
361+
);
362+
}

Diff for: components/documents/add-document-modal.tsx

+33
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,12 @@ import {
2828
createNewDocumentVersion,
2929
} from "@/lib/documents/create-document";
3030
import { putFile } from "@/lib/files/put-file";
31+
import useLimits from "@/lib/swr/use-limits";
3132
import { copyToClipboard } from "@/lib/utils";
3233
import { getSupportedContentType } from "@/lib/utils/get-content-type";
3334

35+
import { UpgradePlanModal } from "../billing/upgrade-plan-modal";
36+
3437
export function AddDocumentModal({
3538
newVersion,
3639
children,
@@ -52,6 +55,7 @@ export function AddDocumentModal({
5255
const [currentFile, setCurrentFile] = useState<File | null>(null);
5356
const [notionLink, setNotionLink] = useState<string | null>(null);
5457
const teamInfo = useTeam();
58+
const { canAddDocuments } = useLimits();
5559

5660
const teamId = teamInfo?.currentTeam?.id as string;
5761

@@ -69,6 +73,11 @@ export function AddDocumentModal({
6973
return; // prevent form from submitting
7074
}
7175

76+
if (!canAddDocuments) {
77+
toast.error("You have reached the maximum number of documents.");
78+
return;
79+
}
80+
7281
try {
7382
setUploading(true);
7483

@@ -251,6 +260,12 @@ export function AddDocumentModal({
251260
event: FormEvent<HTMLFormElement>,
252261
): Promise<void> => {
253262
event.preventDefault();
263+
264+
if (!canAddDocuments) {
265+
toast.error("You have reached the maximum number of documents.");
266+
return;
267+
}
268+
254269
const validateNotionPageURL = parsePageId(notionLink);
255270
// Check if it's a valid URL or not by Regx
256271
const isValidURL =
@@ -353,6 +368,24 @@ export function AddDocumentModal({
353368
setAddDocumentModalOpen && setAddDocumentModalOpen(!isOpen);
354369
};
355370

371+
if (!canAddDocuments && children) {
372+
if (newVersion) {
373+
return (
374+
<UpgradePlanModal
375+
clickedPlan="Pro"
376+
trigger={"limit_upload_document_version"}
377+
>
378+
{children}
379+
</UpgradePlanModal>
380+
);
381+
}
382+
return (
383+
<UpgradePlanModal clickedPlan="Pro" trigger={"limit_upload_documents"}>
384+
<Button>Upgrade to Add Documents</Button>
385+
</UpgradePlanModal>
386+
);
387+
}
388+
356389
return (
357390
<Dialog open={isOpen} onOpenChange={clearModelStates}>
358391
<DialogTrigger asChild>{children}</DialogTrigger>

Diff for: components/ui/progress.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ const Progress = React.forwardRef<
2727
{...props}
2828
>
2929
<ProgressPrimitive.Indicator
30-
className="h-full w-full flex-1 bg-primary transition-all"
30+
className={cn(
31+
"h-full w-full flex-1 bg-primary transition-all",
32+
value && value > 100 ? "bg-destructive dark:bg-red-600" : "",
33+
)}
3134
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
3235
/>
3336
{text && !error ? (

Diff for: components/upload-zone.tsx

+10
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import { useTeam } from "@/context/team-context";
66
import { DocumentStorageType } from "@prisma/client";
77
import { useSession } from "next-auth/react";
88
import { FileRejection, useDropzone } from "react-dropzone";
9+
import { toast } from "sonner";
910
import { mutate } from "swr";
1011

1112
import { useAnalytics } from "@/lib/analytics";
1213
import { DocumentData, createDocument } from "@/lib/documents/create-document";
1314
import { resumableUpload } from "@/lib/files/tus-upload";
1415
import { usePlan } from "@/lib/swr/use-billing";
16+
import useLimits from "@/lib/swr/use-limits";
1517
import { CustomUser } from "@/lib/types";
1618
import { cn } from "@/lib/utils";
1719
import { getSupportedContentType } from "@/lib/utils/get-content-type";
@@ -70,13 +72,21 @@ export default function UploadZone({
7072
const isTrial = !!trial;
7173
const maxSize = plan === "business" || plan === "datarooms" ? 250 : 30;
7274
const maxNumPages = plan === "business" || plan === "datarooms" ? 500 : 100;
75+
const { limits, canAddDocuments } = useLimits();
76+
const remainingDocuments = limits?.documents
77+
? limits?.documents - limits?.usage?.documents
78+
: 0;
7379

7480
const [progress, setProgress] = useState<number>(0);
7581
const [showProgress, setShowProgress] = useState(false);
7682
const uploadProgress = useRef<number[]>([]);
7783

7884
const onDrop = useCallback(
7985
(acceptedFiles: FileWithPath[]) => {
86+
if (!canAddDocuments && acceptedFiles.length > remainingDocuments) {
87+
toast.error("You have reached the maximum number of documents.");
88+
return;
89+
}
8090
const newUploads = acceptedFiles.map((file) => ({
8191
fileName: file.name,
8292
progress: 0,

Diff for: ee/limits/constants.ts

+22-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
1+
// INFO: for numeric values,`null` means unlimited
2+
3+
export type TPlanLimits = {
4+
users: number;
5+
links: number | null;
6+
documents: number | null;
7+
domains: number;
8+
datarooms: number;
9+
customDomainOnPro: boolean;
10+
customDomainInDataroom: boolean;
11+
advancedLinkControlsOnPro: boolean | null;
12+
};
13+
114
export const FREE_PLAN_LIMITS = {
215
users: 1,
16+
links: 10,
17+
documents: 10,
318
domains: 0,
419
datarooms: 0,
520
customDomainOnPro: false,
@@ -9,7 +24,9 @@ export const FREE_PLAN_LIMITS = {
924

1025
export const PRO_PLAN_LIMITS = {
1126
users: 2,
12-
domains: 5,
27+
links: null,
28+
documents: 100,
29+
domains: 2,
1330
datarooms: 0,
1431
customDomainOnPro: false,
1532
customDomainInDataroom: false,
@@ -18,6 +35,8 @@ export const PRO_PLAN_LIMITS = {
1835

1936
export const BUSINESS_PLAN_LIMITS = {
2037
users: 3,
38+
links: null,
39+
documents: null,
2140
domains: 5,
2241
datarooms: 1,
2342
customDomainOnPro: true,
@@ -27,6 +46,8 @@ export const BUSINESS_PLAN_LIMITS = {
2746

2847
export const DATAROOMS_PLAN_LIMITS = {
2948
users: 3,
49+
links: null,
50+
documents: null,
3051
domains: 10,
3152
datarooms: 100,
3253
customDomainOnPro: true,

Diff for: ee/limits/server.ts

+83-13
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,44 @@ import { z } from "zod";
22

33
import prisma from "@/lib/prisma";
44

5+
import {
6+
BUSINESS_PLAN_LIMITS,
7+
DATAROOMS_PLAN_LIMITS,
8+
FREE_PLAN_LIMITS,
9+
PRO_PLAN_LIMITS,
10+
TPlanLimits,
11+
} from "./constants";
12+
13+
// Function to determine if a plan is free or free+drtrial
14+
const isFreePlan = (plan: string) => plan === "free" || plan === "free+drtrial";
15+
16+
// Function to get the base plan from a plan string
17+
const getBasePlan = (plan: string) => plan.split("+")[0];
18+
19+
const planLimitsMap: Record<string, TPlanLimits> = {
20+
free: FREE_PLAN_LIMITS,
21+
pro: PRO_PLAN_LIMITS,
22+
business: BUSINESS_PLAN_LIMITS,
23+
datarooms: DATAROOMS_PLAN_LIMITS,
24+
};
25+
26+
const configSchema = z.object({
27+
datarooms: z.number(),
28+
links: z
29+
.preprocess((v) => (v === null ? Infinity : Number(v)), z.number())
30+
.optional()
31+
.default(10),
32+
documents: z
33+
.preprocess((v) => (v === null ? Infinity : Number(v)), z.number())
34+
.optional()
35+
.default(10),
36+
users: z.number(),
37+
domains: z.number(),
38+
customDomainOnPro: z.boolean(),
39+
customDomainInDataroom: z.boolean(),
40+
advancedLinkControlsOnPro: z.boolean().nullish(),
41+
});
42+
543
export async function getLimits({
644
teamId,
745
userId,
@@ -21,31 +59,63 @@ export async function getLimits({
2159
select: {
2260
plan: true,
2361
limits: true,
62+
_count: {
63+
select: {
64+
documents: true,
65+
},
66+
},
67+
documents: {
68+
select: {
69+
_count: {
70+
select: {
71+
links: true,
72+
},
73+
},
74+
},
75+
},
2476
},
2577
});
2678

2779
if (!team) {
2880
throw new Error("Team not found");
2981
}
3082

83+
const documentCount = team._count.documents;
84+
const linkCount = team.documents.reduce(
85+
(sum, doc) => sum + doc._count.links,
86+
0,
87+
);
88+
3189
// parse the limits json with zod and return the limits
3290
// {datarooms: 1, users: 1, domains: 1, customDomainOnPro: boolean, customDomainInDataroom: boolean}
3391

34-
const configSchema = z.object({
35-
datarooms: z.number(),
36-
users: z.number(),
37-
domains: z.number(),
38-
customDomainOnPro: z.boolean(),
39-
customDomainInDataroom: z.boolean(),
40-
advancedLinkControlsOnPro: z.boolean().nullish(),
41-
});
42-
4392
try {
44-
const parsedData = configSchema.parse(team.limits);
93+
let parsedData = configSchema.parse(team.limits);
4594

46-
return parsedData;
95+
// Adjust limits based on the plan if they're at the default value
96+
if (isFreePlan(team.plan)) {
97+
return {
98+
...parsedData,
99+
usage: { documents: documentCount, links: linkCount },
100+
};
101+
} else {
102+
return {
103+
...parsedData,
104+
// if account is paid, but link and document limits are not set, then set them to Infinity
105+
links: parsedData.links === 10 ? Infinity : parsedData.links,
106+
documents:
107+
parsedData.documents === 10 ? Infinity : parsedData.documents,
108+
usage: { documents: documentCount, links: linkCount },
109+
};
110+
}
47111
} catch (error) {
48-
// if no limits set, then return null and don't block the team
49-
return null;
112+
// if no limits set or parsing fails, return default limits based on the plan
113+
const basePlan = getBasePlan(team.plan);
114+
const defaultLimits = planLimitsMap[basePlan] || FREE_PLAN_LIMITS;
115+
116+
return {
117+
...defaultLimits,
118+
usage: { documents: documentCount, links: linkCount },
119+
};
50120
}
51121
}

0 commit comments

Comments
 (0)