Skip to content

Commit 579ddbb

Browse files
authored
Merge pull request #369 from mfts/feat/usergroups
feat: add viewer and invite to view
2 parents 64b8c23 + 68b6781 commit 579ddbb

File tree

20 files changed

+1173
-25
lines changed

20 files changed

+1173
-25
lines changed
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import { Button } from "@/components/ui/button";
2+
import {
3+
Dialog,
4+
DialogContent,
5+
DialogDescription,
6+
DialogFooter,
7+
DialogHeader,
8+
DialogTitle,
9+
DialogTrigger,
10+
} from "@/components/ui/dialog";
11+
import { Input } from "@/components/ui/input";
12+
import { Label } from "@/components/ui/label";
13+
import { useTeam } from "@/context/team-context";
14+
import { useAnalytics } from "@/lib/analytics";
15+
import { useRouter } from "next/router";
16+
import { useState } from "react";
17+
import { toast } from "sonner";
18+
import { mutate } from "swr";
19+
20+
export function AddViewerModal({
21+
dataroomId,
22+
open,
23+
setOpen,
24+
children,
25+
}: {
26+
dataroomId: string;
27+
open: boolean;
28+
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
29+
children?: React.ReactNode;
30+
}) {
31+
const [emails, setEmails] = useState<string[]>([]);
32+
const [inputValue, setInputValue] = useState<string>("");
33+
const [loading, setLoading] = useState<boolean>(false);
34+
const teamInfo = useTeam();
35+
const analytics = useAnalytics();
36+
37+
// Email validation regex pattern
38+
const validateEmail = (email: string) => {
39+
return email.match(
40+
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
41+
);
42+
};
43+
44+
// const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
45+
// const val = e.target.value;
46+
// setInputValue(val);
47+
48+
// if (val.endsWith(",")) {
49+
// const newEmail = val.slice(0, -1).trim(); // Remove the comma and trim whitespace
50+
// if (validateEmail(newEmail) && !emails.includes(newEmail)) {
51+
// setEmails([...emails, newEmail]); // Add the new email if it's valid and not already in the list
52+
// setInputValue(""); // Reset input field
53+
// }
54+
// }
55+
// };
56+
57+
// const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
58+
// const inputValue = e.target.value;
59+
// setInputValue(inputValue);
60+
61+
// // Split the input value by commas to support pasting comma-separated emails
62+
// const potentialEmails = inputValue
63+
// .split(",")
64+
// .map((email) => email.trim())
65+
// .filter((email) => email);
66+
67+
// const newEmails: string[] = [];
68+
// potentialEmails.forEach((email) => {
69+
// // Check if the email is valid and not already included
70+
// if (validateEmail(email) && !emails.includes(email)) {
71+
// newEmails.push(email);
72+
// }
73+
// });
74+
75+
// // If there are new valid emails, update the state
76+
// if (newEmails.length > 0) {
77+
// setEmails([...emails, ...newEmails]);
78+
// setInputValue(""); // Reset input field only if new emails were added
79+
// }
80+
// };
81+
82+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
83+
const inputValue = e.target.value;
84+
setInputValue(inputValue);
85+
};
86+
87+
const handleInputBlurOrComma = () => {
88+
// Split the current input value by commas in case of pasting multiple emails
89+
const potentialEmails = inputValue
90+
.split(",")
91+
.map((email) => email.trim())
92+
.filter((email) => email);
93+
94+
const newEmails = potentialEmails.filter((email) => {
95+
// Validate each email and check it's not already in the list
96+
return validateEmail(email) && !emails.includes(email);
97+
});
98+
99+
if (newEmails.length > 0) {
100+
setEmails([...emails, ...newEmails]); // Add new valid emails to the list
101+
setInputValue(""); // Clear input field
102+
}
103+
};
104+
105+
const removeEmail = (index: number) => {
106+
setEmails(emails.filter((_, i) => i !== index));
107+
};
108+
109+
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
110+
event.preventDefault();
111+
event.stopPropagation();
112+
113+
if (emails.length === 0) return;
114+
115+
setLoading(true);
116+
117+
// POST request with multiple emails
118+
const response = await fetch(
119+
`/api/teams/${teamInfo?.currentTeam?.id}/datarooms/${dataroomId}/users`,
120+
{
121+
method: "POST",
122+
headers: {
123+
"Content-Type": "application/json",
124+
},
125+
body: JSON.stringify({
126+
emails: emails,
127+
}),
128+
},
129+
);
130+
131+
if (!response.ok) {
132+
const error = await response.json();
133+
setLoading(false);
134+
setOpen(false);
135+
toast.error(error.message || "Failed to send invitations.");
136+
return;
137+
}
138+
139+
analytics.capture("Dataroom View Invitation Sent", {
140+
inviteeCount: emails.length,
141+
teamId: teamInfo?.currentTeam?.id,
142+
dataroomId: dataroomId,
143+
});
144+
145+
mutate(
146+
`/api/teams/${teamInfo?.currentTeam?.id}/datarooms/${dataroomId}/viewers`,
147+
);
148+
149+
toast.success("Invitation emails have been sent!");
150+
setOpen(false);
151+
setLoading(false);
152+
setEmails([]); // Reset emails state
153+
};
154+
155+
return (
156+
<Dialog open={open} onOpenChange={setOpen}>
157+
<DialogTrigger asChild>{children}</DialogTrigger>
158+
<DialogContent className="sm:max-w-[425px]">
159+
<DialogHeader className="text-start">
160+
<DialogTitle>Invite Visitors</DialogTitle>
161+
<DialogDescription>
162+
Enter email addresses, separated by commas.
163+
</DialogDescription>
164+
</DialogHeader>
165+
<form onSubmit={handleSubmit}>
166+
<Label htmlFor="email" className="opacity-80">
167+
Emails
168+
</Label>
169+
<div className="flex flex-wrap gap-2 py-2 text-sm">
170+
{emails.map((email, index) => (
171+
<div
172+
key={index}
173+
className="flex items-center gap-2 bg-gray-100 px-2 py-1 rounded"
174+
>
175+
{email}
176+
<button type="button" onClick={() => removeEmail(index)}>
177+
×
178+
</button>
179+
</div>
180+
))}
181+
<Input
182+
id="email"
183+
value={inputValue}
184+
placeholder="[email protected]"
185+
className="flex-1"
186+
onChange={handleInputChange}
187+
onBlur={handleInputBlurOrComma} // Handle when input loses focus
188+
onKeyDown={(e) => {
189+
if (e.key === ",") {
190+
e.preventDefault(); // Prevent the comma from being added to the input
191+
handleInputBlurOrComma(); // Act as if the input lost focus to process the email
192+
}
193+
}}
194+
/>
195+
</div>
196+
197+
<DialogFooter>
198+
<Button type="submit" className="w-full h-9 mt-8" loading={loading}>
199+
{loading ? "Sending emails..." : "Add members"}
200+
</Button>
201+
</DialogFooter>
202+
</form>
203+
</DialogContent>
204+
</Dialog>
205+
);
206+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import React from "react";
2+
import {
3+
Body,
4+
Container,
5+
Head,
6+
Heading,
7+
Html,
8+
Preview,
9+
Text,
10+
Tailwind,
11+
Section,
12+
Button,
13+
Hr,
14+
} from "@react-email/components";
15+
export default function DataroomViewerInvitation({
16+
dataroomName,
17+
senderEmail,
18+
url,
19+
}: {
20+
dataroomName: string;
21+
senderEmail: string;
22+
url: string;
23+
}) {
24+
return (
25+
<Html>
26+
<Head />
27+
<Preview>View dataroom on Papermark</Preview>
28+
<Tailwind>
29+
<Body className="bg-white my-auto mx-auto font-sans">
30+
<Container className="my-10 mx-auto p-5 w-[465px]">
31+
<Heading className="text-2xl font-normal text-center p-0 mt-4 mb-8 mx-0">
32+
<span className="font-bold tracking-tighter">Papermark</span>
33+
</Heading>
34+
<Heading className="text-xl font-seminbold text-center p-0 mt-4 mb-8 mx-0">
35+
{`View ${dataroomName}`}
36+
</Heading>
37+
<Text className="text-sm leading-6 text-black">Hey!</Text>
38+
<Text className="text-sm leading-6 text-black">
39+
You have been invited to view the{" "}
40+
<span className="font-semibold">{dataroomName}</span> dataroom on{" "}
41+
<span className="font-semibold">Papermark</span>.
42+
<br />
43+
The invitation was sent by{" "}
44+
<span className="font-semibold">{senderEmail}</span>.
45+
</Text>
46+
<Section className="text-center mt-[32px] mb-[32px]">
47+
<Button
48+
className="bg-black rounded text-white text-xs font-semibold no-underline text-center"
49+
href={`${url}`}
50+
style={{ padding: "12px 20px" }}
51+
>
52+
View the dataroom
53+
</Button>
54+
</Section>
55+
<Text className="text-sm text-black">
56+
or copy and paste this URL into your browser: <br />
57+
{`${url}`}
58+
</Text>
59+
<Text className="text-sm text-gray-400">Papermark</Text>
60+
<Hr />
61+
<Section className="mt-8 text-gray-400">
62+
<Text className="text-xs">
63+
© {new Date().getFullYear()}{" "}
64+
<a
65+
href="https://www.papermark.io"
66+
className="no-underline text-gray-400 hover:text-gray-400 visited:text-gray-400"
67+
target="_blank"
68+
>
69+
papermark.io
70+
</a>
71+
</Text>
72+
<Text className="text-xs">
73+
If you have any feedback or questions about this email, simply
74+
reply to it.
75+
</Text>
76+
</Section>
77+
</Container>
78+
</Body>
79+
</Tailwind>
80+
</Html>
81+
);
82+
}

0 commit comments

Comments
 (0)