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: Add invite link functionality to team #1168

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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 components/search-box.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,9 @@ export function SearchBoxPersisted({
const [value, setValue] = useState(queryParams[urlParam] ?? "");
const [debouncedValue, setDebouncedValue] = useState(value);

console.log("queryParams", queryParams);
console.log("debouncedValue", debouncedValue);
console.log("value", value);
// console.log("queryParams", queryParams);
// console.log("debouncedValue", debouncedValue);
// console.log("value", value);

// Set URL param when debounced value changes
useEffect(() => {
Expand Down
131 changes: 95 additions & 36 deletions components/teams/add-team-member-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import { useRouter } from "next/router";

import { useState } from "react";

import { useEffect, useState } from "react";
import { useTeam } from "@/context/team-context";
import { toast } from "sonner";
import { mutate } from "swr";

import { Button } from "@/components/ui/button";
import {
Dialog,
Expand All @@ -18,7 +15,6 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";

import { useAnalytics } from "@/lib/analytics";

export function AddTeamMembers({
Expand All @@ -32,47 +28,88 @@ export function AddTeamMembers({
}) {
const [email, setEmail] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const [inviteLink, setInviteLink] = useState<string | null>(null);
const [inviteLinkLoading, setInviteLinkLoading] = useState<boolean>(true);
const teamInfo = useTeam();
const analytics = useAnalytics();

useEffect(() => {
const fetchInviteLink = async () => {
setInviteLinkLoading(true);
try {
const response = await fetch(`/api/teams/${teamInfo?.currentTeam?.id}/invite-link`);
if (response.ok) {
const data = await response.json();
setInviteLink(data.inviteLink || null);
} else {
console.error("Failed to fetch invite link:", response.status);
}
} catch (error) {
console.error("Error fetching invite link:", error);
} finally {
setInviteLinkLoading(false);
}
};
fetchInviteLink();
}, [teamInfo]);

const handleResetInviteLink = async () => {
setInviteLinkLoading(true);
try {
const linkResponse = await fetch(`/api/teams/${teamInfo?.currentTeam?.id}/invite-link`, {
method: "POST",
});

if (!linkResponse.ok) {
throw new Error("Failed to reset invite link");
}

const linkData = await linkResponse.json();
setInviteLink(linkData.inviteLink || null);
toast.success("Invite link has been reset!");
} catch (error) {
toast.error("Error resetting invite link.");
} finally {
setInviteLinkLoading(false);
}
};

const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
event.stopPropagation();

if (!email) return;

setLoading(true);
const response = await fetch(
`/api/teams/${teamInfo?.currentTeam?.id}/invite`,
{

try {
const response = await fetch(`/api/teams/${teamInfo?.currentTeam?.id}/invite`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: email,
}),
},
);
body: JSON.stringify({ email }),
});

if (!response.ok) {
const error = await response.json();
setLoading(false);
if (!response.ok) {
const error = await response.json();
throw new Error(error);
}

toast.success("An invitation email has been sent!");
setOpen(false);
toast.error(error);
return;
} catch (error: any) {
toast.error(error.message);
} finally {
setLoading(false);
}
};

analytics.capture("Team Member Invitation Sent", {
email: email,
teamId: teamInfo?.currentTeam?.id,
});

mutate(`/api/teams/${teamInfo?.currentTeam?.id}/invitations`);

toast.success("An invitation email has been sent!");
setOpen(false);
setLoading(false);
const handleCopyInviteLink = () => {
if (inviteLink) {
navigator.clipboard.writeText(inviteLink);
toast.success("Invite link copied to clipboard!");
}
};

return (
Expand All @@ -82,26 +119,48 @@ export function AddTeamMembers({
<DialogHeader className="text-start">
<DialogTitle>Add Member</DialogTitle>
<DialogDescription>
You can easily add team members.
Invite team members via email or share the invite link.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit}>
<Label htmlFor="domain" className="opacity-80">
<Label htmlFor="email" className="opacity-80">
Email
</Label>
<Input
id="email"
placeholder="team@member.com"
className="mb-8 mt-1 w-full"
className="mb-4 mt-1 w-full"
onChange={(e) => setEmail(e.target.value)}
/>
<Button type="submit" className="mb-6 w-full" disabled={loading}>
{loading ? "Sending invitation..." : "Send Invitation"}
</Button>
</form>

<DialogFooter>
<Button type="submit" className="h-9 w-full">
{loading ? "Sending email..." : "Add member"}
<div className="mb-4">
<Label className="opacity-80">Or share invite link</Label>
<Input
value={inviteLink || ""}
readOnly
className="mt-1 w-full"
/>
<div className="mt-2 flex justify-between">
<Button
onClick={handleCopyInviteLink}
disabled={!inviteLink || inviteLinkLoading}
className="flex-1 mr-2"
>
Copy Link
</Button>
</DialogFooter>
</form>
<Button
onClick={handleResetInviteLink}
disabled={inviteLinkLoading}
className="flex-1"
>
Reset Link
</Button>
</div>
</div>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add the invite by link as a separate dialog / modal

Copy link
Contributor Author

@ShreyasLakhani ShreyasLakhani Oct 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mfts
I made all changes but I did not understand last changes you told me.
"please add the invite by link as a separate dialog / modal".

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ShreyasLakhani instead of adding the button with the copy invite link on the add-team-member-modal.tsx, please create a new component only for copying the invite link. and that should be a separate button on the /people page.

</DialogContent>
</Dialog>
);
Expand Down
8 changes: 8 additions & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/ua-parser-js": "^0.7.39",
"@types/uuid": "^10.0.0",
ShreyasLakhani marked this conversation as resolved.
Show resolved Hide resolved
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.8",
"prisma": "^5.20.0",
Expand Down
54 changes: 54 additions & 0 deletions pages/api/teams/[teamId]/invite-link.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "next-auth/next";
import prisma from "@/lib/prisma";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import { v4 as uuidv4 } from 'uuid';

const generateUniqueInviteLink = (): string => {
return `https://papermark.com/teams/invite/${uuidv4()}`;
};
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you use nanoid() instead of uuid.


export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { teamId } = req.query as { teamId: string };

const session = await getServerSession(req, res, authOptions);
if (!session) {
return res.status(401).end("Unauthorized");
}

switch (req.method) {
case "POST":
// Generate a new invite link
const newInviteLink = generateUniqueInviteLink();
const updatedTeam = await prisma.team.update({
where: { id: teamId },
data: { inviteLink: newInviteLink },
select: { inviteLink: true },
});
return res.json({ inviteLink: updatedTeam.inviteLink });

case "GET":
// Get the current invite link
const team = await prisma.team.findUnique({
where: { id: teamId },
select: { inviteLink: true },
});

if (!team?.inviteLink) {
// If no invite link exists, create one
const newLink = generateUniqueInviteLink();
const updatedTeam = await prisma.team.update({
where: { id: teamId },
data: { inviteLink: newLink },
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also let's only store the unique code for each team, not the full URL

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this appears in several other places

select: { inviteLink: true },
});
return res.json({ inviteLink: updatedTeam.inviteLink });
}

return res.json({ inviteLink: team.inviteLink });

default:
res.setHeader("Allow", ["POST", "GET"]);
return res.status(405).end(`Method ${req.method} Not Allowed`);
}
}
Loading
Loading