add team management (#131)

* add team management

* add more team management

* add join team page
This commit is contained in:
KM Koushik
2025-03-26 22:02:49 +11:00
committed by GitHub
parent f8113e64b5
commit 1ed5c8009f
26 changed files with 1348 additions and 13 deletions

View File

@@ -18,6 +18,7 @@ import { Spinner } from "@unsend/ui/src/spinner";
import { api } from "~/trpc/react";
import { useRouter } from "next/navigation";
import { toast } from "@unsend/ui/src/toaster";
import JoinTeam from "./JoinTeam";
const FormSchema = z.object({
name: z.string().min(2, {
@@ -52,9 +53,10 @@ export default function CreateTeam() {
return (
<div className="flex items-center justify-center min-h-screen ">
<div className=" w-[300px] flex flex-col gap-8">
<div className=" w-[400px] flex flex-col gap-8">
<JoinTeam showCreateTeam />
<div>
<h1 className="text-2xl font-semibold text-center">Create Team</h1>
<h1 className=" font-semibold text-center">Create Team</h1>
</div>
<Form {...form}>
<form

View File

@@ -0,0 +1,159 @@
"use client";
import { z } from "zod";
import { Button } from "@unsend/ui/src/button";
import { Spinner } from "@unsend/ui/src/spinner";
import { api } from "~/trpc/react";
import { useRouter } from "next/navigation";
import { toast } from "@unsend/ui/src/toaster";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@unsend/ui/src/dialog";
import { useState } from "react";
import type { AppRouter } from "~/server/api/root";
import type { inferRouterOutputs } from "@trpc/server";
type RouterOutputs = inferRouterOutputs<AppRouter>;
type Invite = NonNullable<
RouterOutputs["invitation"]["getUserInvites"]
>[number];
const FormSchema = z.object({
name: z.string().min(2, {
message: "Team name must be at least 2 characters.",
}),
});
export default function JoinTeam({
showCreateTeam = false,
}: {
showCreateTeam?: boolean;
}) {
const { data: invites, status: invitesStatus } =
api.invitation.getUserInvites.useQuery();
const joinTeamMutation = api.invitation.acceptTeamInvite.useMutation();
const [selectedInvite, setSelectedInvite] = useState<Invite | null>(null);
const [dialogOpen, setDialogOpen] = useState(false);
const utils = api.useUtils();
const router = useRouter();
const handleAcceptInvite = (invite: Invite) => {
setSelectedInvite(invite);
setDialogOpen(true);
};
const confirmAcceptInvite = () => {
if (!selectedInvite) return;
joinTeamMutation.mutate(
{
inviteId: selectedInvite.id,
},
{
onSuccess: async () => {
toast.success(`Successfully joined ${selectedInvite.team.name}`);
await Promise.all([
utils.invitation.getUserInvites.invalidate(),
utils.team.getTeams.invalidate(),
]);
setDialogOpen(false);
router.replace("/dashboard");
},
onError: (error) => {
toast.error(`Failed to join team: ${error.message}`);
setDialogOpen(false);
},
}
);
};
if (!invites?.length)
return <div className="text-center text-xl">No invites found</div>;
return (
<div>
<div>You have been invited to join team</div>
<div className="space-y-2 mt-4">
{invites.map((invite) => (
<div
key={invite.id}
className="flex items-center gap-2 border rounded-lg p-2 px-4 shadow justify-between"
>
<div>
<div className="text-sm">{invite.team.name}</div>
<div className="flex items-center gap-2">
<div className="text-muted-foreground text-xs capitalize">
{invite.role.toLowerCase()}
</div>
<div className="text-muted-foreground text-xs">
{invite.createdAt.toLocaleDateString()}
</div>
</div>
</div>
<Button
onClick={() => handleAcceptInvite(invite)}
disabled={joinTeamMutation.isPending}
size="sm"
variant="ghost"
>
{joinTeamMutation.isPending ? (
<Spinner className="w-5 h-5" />
) : (
"Accept"
)}
</Button>
</div>
))}
</div>
{showCreateTeam ? (
<div className="mt-8 text-muted-foreground text-sm font-mono text-center">
OR
</div>
) : null}
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Accept Team Invitation</DialogTitle>
<DialogDescription>
Are you sure you want to join{" "}
<span className="font-semibold text-primary">
{selectedInvite?.team.name}
</span>
? You will be added as a{" "}
<span className="font-semibold text-primary lowercase">
{selectedInvite?.role.toLowerCase()}
</span>
.
</DialogDescription>
</DialogHeader>
<div className="flex justify-end gap-3 mt-4">
<Button
variant="outline"
onClick={() => setDialogOpen(false)}
disabled={joinTeamMutation.isPending}
>
Cancel
</Button>
<Button
onClick={confirmAcceptInvite}
disabled={joinTeamMutation.isPending}
>
{joinTeamMutation.isPending ? (
<Spinner className="w-5 h-5" />
) : (
"Accept Invitation"
)}
</Button>
</div>
</DialogContent>
</Dialog>
</div>
);
}