add team management (#131)
* add team management * add more team management * add join team page
This commit is contained in:
@@ -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
|
||||
|
159
apps/web/src/components/team/JoinTeam.tsx
Normal file
159
apps/web/src/components/team/JoinTeam.tsx
Normal 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>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user