Files
GibSend/apps/web/src/components/team/JoinTeam.tsx
KM Koushik f1186f875c enable teams for self-hosted (#137)
* enable teams for self-hosted

* remove console
2025-03-29 00:56:06 +11:00

160 lines
4.7 KiB
TypeScript

"use client";
import { Button } from "@unsend/ui/src/button";
import { Spinner } from "@unsend/ui/src/spinner";
import { api } from "~/trpc/react";
import { useRouter, useSearchParams } 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];
export default function JoinTeam({
showCreateTeam = false,
}: {
showCreateTeam?: boolean;
}) {
const searchParams = useSearchParams();
const inviteId = searchParams.get("inviteId");
const { data: invites, status: invitesStatus } =
api.invitation.getUserInvites.useQuery({
inviteId,
});
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 !showCreateTeam ? (
<div className="text-center text-xl">No invites found</div>
) : null;
}
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>
);
}