add team management (#131)
* add team management * add more team management * add join team page
This commit is contained in:
@@ -9,8 +9,9 @@ import { useTeam } from "~/providers/team-context";
|
||||
import { api } from "~/trpc/react";
|
||||
import { PlanDetails } from "~/components/payments/PlanDetails";
|
||||
import { UpgradeButton } from "~/components/payments/UpgradeButton";
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { currentTeam } = useTeam();
|
||||
const { currentTeam, currentIsAdmin } = useTeam();
|
||||
const manageSessionUrl = api.billing.getManageSessionUrl.useMutation();
|
||||
const updateBillingEmailMutation =
|
||||
api.billing.updateBillingEmail.useMutation();
|
||||
@@ -47,6 +48,10 @@ export default function SettingsPage() {
|
||||
|
||||
const paymentMethod = JSON.parse(subscription?.paymentMethod || "{}");
|
||||
|
||||
if (!currentIsAdmin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!currentTeam?.plan) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-full">
|
||||
|
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useTeam } from "~/providers/team-context";
|
||||
import { SettingsNavButton } from "../dev-settings/settings-nav-button";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
@@ -9,12 +10,19 @@ export default function ApiKeysPage({
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { currentIsAdmin } = useTeam();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="font-bold text-lg">Settings</h1>
|
||||
<div className="flex gap-4 mt-4">
|
||||
<SettingsNavButton href="/settings">Usage</SettingsNavButton>
|
||||
<SettingsNavButton href="/settings/billing">Billing</SettingsNavButton>
|
||||
{currentIsAdmin ? (
|
||||
<SettingsNavButton href="/settings/billing">
|
||||
Billing
|
||||
</SettingsNavButton>
|
||||
) : null}
|
||||
<SettingsNavButton href="/settings/team">Team</SettingsNavButton>
|
||||
</div>
|
||||
<div className="mt-8">{children}</div>
|
||||
</div>
|
||||
|
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
import { api } from "~/trpc/react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { Trash2 } from "lucide-react";
|
||||
|
||||
export const DeleteTeamInvite: React.FC<{
|
||||
invite: { id: string; email: string };
|
||||
}> = ({ invite }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const deleteInviteMutation = api.team.deleteTeamInvite.useMutation();
|
||||
|
||||
const utils = api.useUtils();
|
||||
|
||||
async function onInviteDelete() {
|
||||
deleteInviteMutation.mutate(
|
||||
{
|
||||
inviteId: invite.id,
|
||||
},
|
||||
{
|
||||
onSuccess: async () => {
|
||||
utils.team.getTeamInvites.invalidate();
|
||||
setOpen(false);
|
||||
toast.success("Invite cancelled successfully");
|
||||
},
|
||||
onError: async (error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(_open) => (_open !== open ? setOpen(_open) : null)}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Trash2 className="h-4 w-4 text-red-600/80" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Cancel Invite</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to cancel the invite for{" "}
|
||||
<span className="font-semibold text-primary">{invite.email}</span>?
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex justify-end gap-4 mt-6">
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
isLoading={deleteInviteMutation.isPending}
|
||||
onClick={onInviteDelete}
|
||||
className="w-[150px]"
|
||||
>
|
||||
Delete Invite
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteTeamInvite;
|
@@ -0,0 +1,88 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
import { api } from "~/trpc/react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { Role } from "@prisma/client";
|
||||
import { LogOut, Trash2 } from "lucide-react";
|
||||
|
||||
export const DeleteTeamMember: React.FC<{
|
||||
teamUser: { userId: string; role: Role; email: string };
|
||||
self: boolean;
|
||||
}> = ({ teamUser, self }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const deleteTeamUserMutation = api.team.deleteTeamUser.useMutation();
|
||||
|
||||
const utils = api.useUtils();
|
||||
|
||||
async function onTeamUserDelete() {
|
||||
deleteTeamUserMutation.mutate(
|
||||
{
|
||||
userId: teamUser.userId,
|
||||
},
|
||||
{
|
||||
onSuccess: async () => {
|
||||
utils.team.getTeamUsers.invalidate();
|
||||
setOpen(false);
|
||||
toast.success("Team member removed successfully");
|
||||
},
|
||||
onError: async (error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(_open) => (_open !== open ? setOpen(_open) : null)}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
{self ? (
|
||||
<LogOut className="h-4 w-4 text-red-600/80" />
|
||||
) : (
|
||||
<Trash2 className="h-4 w-4 text-red-600/80" />
|
||||
)}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{self ? "Leave Team" : "Remove Team Member"}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{self
|
||||
? "Are you sure you want to leave the team? This action cannot be undone."
|
||||
: 'Are you sure you want to remove"{teamUser.email}" from the team? This action cannot be undone.'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex justify-end gap-4 mt-6">
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={onTeamUserDelete}
|
||||
isLoading={deleteTeamUserMutation.isPending}
|
||||
className="w-[150px]"
|
||||
>
|
||||
{self ? "Leave" : "Remove"}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteTeamMember;
|
142
apps/web/src/app/(dashboard)/settings/team/edit-team-member.tsx
Normal file
142
apps/web/src/app/(dashboard)/settings/team/edit-team-member.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@unsend/ui/src/form";
|
||||
|
||||
import { api } from "~/trpc/react";
|
||||
import { useState } from "react";
|
||||
import { PencilIcon } from "lucide-react";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { Role } from "@prisma/client";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@unsend/ui/src/select";
|
||||
|
||||
const teamUserSchema = z.object({
|
||||
role: z.enum(["MEMBER", "ADMIN"]),
|
||||
});
|
||||
|
||||
export const EditTeamMember: React.FC<{
|
||||
teamUser: { userId: string; role: Role };
|
||||
}> = ({ teamUser }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const updateTeamUserMutation = api.team.updateTeamUserRole.useMutation();
|
||||
|
||||
const utils = api.useUtils();
|
||||
|
||||
const teamUserForm = useForm<z.infer<typeof teamUserSchema>>({
|
||||
resolver: zodResolver(teamUserSchema),
|
||||
defaultValues: {
|
||||
role: teamUser.role,
|
||||
},
|
||||
});
|
||||
|
||||
async function onTeamUserUpdate(values: z.infer<typeof teamUserSchema>) {
|
||||
updateTeamUserMutation.mutate(
|
||||
{
|
||||
userId: teamUser.userId,
|
||||
role: values.role,
|
||||
},
|
||||
{
|
||||
onSuccess: async () => {
|
||||
utils.team.getTeamUsers.invalidate();
|
||||
setOpen(false);
|
||||
toast.success("Team member role updated successfully");
|
||||
},
|
||||
onError: async (error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(_open) => (_open !== open ? setOpen(_open) : null)}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Team Member Role</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-2">
|
||||
<Form {...teamUserForm}>
|
||||
<form
|
||||
onSubmit={teamUserForm.handleSubmit(onTeamUserUpdate)}
|
||||
className="space-y-8"
|
||||
>
|
||||
<FormField
|
||||
control={teamUserForm.control}
|
||||
name="role"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Role</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a role" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="MEMBER">Member</SelectItem>
|
||||
<SelectItem value="ADMIN">Admin</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={updateTeamUserMutation.isPending}
|
||||
className="w-[150px]"
|
||||
>
|
||||
Update Role
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditTeamMember;
|
@@ -0,0 +1,178 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@unsend/ui/src/dialog";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@unsend/ui/src/select";
|
||||
import { Input } from "@unsend/ui/src/input";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { api } from "~/trpc/react";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@unsend/ui/src/form";
|
||||
import { useTeam } from "~/providers/team-context";
|
||||
|
||||
const inviteTeamMemberSchema = z.object({
|
||||
email: z
|
||||
.string({ required_error: "Email is required" })
|
||||
.email("Invalid email address"),
|
||||
role: z.enum(["ADMIN", "MEMBER"], {
|
||||
required_error: "Please select a role",
|
||||
}),
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof inviteTeamMemberSchema>;
|
||||
|
||||
export default function InviteTeamMember() {
|
||||
const { currentIsAdmin } = useTeam();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(inviteTeamMemberSchema),
|
||||
defaultValues: {
|
||||
email: "",
|
||||
role: "MEMBER",
|
||||
},
|
||||
});
|
||||
|
||||
const utils = api.useUtils();
|
||||
|
||||
const createInvite = api.team.createTeamInvite.useMutation({
|
||||
onSuccess: () => {
|
||||
form.reset();
|
||||
setOpen(false);
|
||||
void utils.team.getTeamInvites.invalidate();
|
||||
toast.success("Invitation sent successfully");
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message || "Failed to send invitation");
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(values: FormData) {
|
||||
createInvite.mutate({
|
||||
email: values.email,
|
||||
role: values.role,
|
||||
});
|
||||
}
|
||||
|
||||
if (!currentIsAdmin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm">
|
||||
<PlusIcon className="mr-2 h-4 w-4" />
|
||||
Invite Member
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Invite Team Member</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-4 pt-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field, formState }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="colleague@example.com" {...field} />
|
||||
</FormControl>
|
||||
{formState.errors.email ? (
|
||||
<FormMessage />
|
||||
) : (
|
||||
<FormDescription>
|
||||
Enter your colleague's email address
|
||||
</FormDescription>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="role"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Role</FormLabel>
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<div className="capitalize">
|
||||
{field.value.toLowerCase()}
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="ADMIN">
|
||||
<div>Admin</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Manage users, update payments
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="MEMBER">
|
||||
<div>Member</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Manage emails, domains and contacts
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={createInvite.isPending}
|
||||
showSpinner
|
||||
isLoading={createInvite.isPending}
|
||||
className="w-[150px]"
|
||||
>
|
||||
{createInvite.isPending ? "Sending..." : "Send Invitation"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
15
apps/web/src/app/(dashboard)/settings/team/page.tsx
Normal file
15
apps/web/src/app/(dashboard)/settings/team/page.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import InviteTeamMember from "./invite-team-member";
|
||||
import TeamMembersList from "./team-members-list";
|
||||
|
||||
export default function TeamsPage() {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-end ">
|
||||
<InviteTeamMember />
|
||||
</div>
|
||||
<TeamMembersList />
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { api } from "~/trpc/react";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
import { RotateCw } from "lucide-react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@unsend/ui/src/tooltip";
|
||||
|
||||
export const ResendTeamInvite: React.FC<{
|
||||
invite: { id: string; email: string };
|
||||
}> = ({ invite }) => {
|
||||
const resendInviteMutation = api.team.resendTeamInvite.useMutation();
|
||||
|
||||
async function onResendInvite() {
|
||||
resendInviteMutation.mutate(
|
||||
{
|
||||
inviteId: invite.id,
|
||||
},
|
||||
{
|
||||
onSuccess: async () => {
|
||||
toast.success(`Invite resent to ${invite.email}`);
|
||||
},
|
||||
onError: async (error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="sm" onClick={onResendInvite}>
|
||||
<RotateCw className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Resend invite</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResendTeamInvite;
|
153
apps/web/src/app/(dashboard)/settings/team/team-members-list.tsx
Normal file
153
apps/web/src/app/(dashboard)/settings/team/team-members-list.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableHead,
|
||||
TableBody,
|
||||
TableCell,
|
||||
} from "@unsend/ui/src/table";
|
||||
import { api } from "~/trpc/react";
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import Spinner from "@unsend/ui/src/spinner";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { Role } from "@prisma/client";
|
||||
import { EditTeamMember } from "./edit-team-member";
|
||||
import { DeleteTeamMember } from "./delete-team-member";
|
||||
import { ResendTeamInvite } from "./resend-team-invite";
|
||||
import { DeleteTeamInvite } from "./delete-team-invite";
|
||||
import { useTeam } from "~/providers/team-context";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
export default function TeamMembersList() {
|
||||
const { currentIsAdmin } = useTeam();
|
||||
const { data: session } = useSession();
|
||||
const teamUsersQuery = api.team.getTeamUsers.useQuery();
|
||||
const teamInvitesQuery = api.team.getTeamInvites.useQuery();
|
||||
|
||||
// Combine team users and invites for display
|
||||
const teamMembers = teamUsersQuery.data || [];
|
||||
const pendingInvites = teamInvitesQuery.data || [];
|
||||
|
||||
const isLoading = teamUsersQuery.isLoading || teamInvitesQuery.isLoading;
|
||||
|
||||
return (
|
||||
<div className="mt-10 flex flex-col gap-4">
|
||||
<div className="flex flex-col rounded-xl border border-border shadow">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-muted/30">
|
||||
<TableHead className="rounded-tl-xl">User</TableHead>
|
||||
<TableHead>Role</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Joined</TableHead>
|
||||
<TableHead className="rounded-tr-xl">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow className="h-32">
|
||||
<TableCell colSpan={5} className="text-center py-4">
|
||||
<Spinner
|
||||
className="w-6 h-6 mx-auto"
|
||||
innerSvgClass="stroke-primary"
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : teamMembers.length > 0 ? (
|
||||
teamMembers.map((member) => (
|
||||
<TableRow key={member.userId} className="">
|
||||
<TableCell className="font-medium">
|
||||
{member.user?.email || "Unknown user"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className=" rounded capitalize py-1 text-xs">
|
||||
{member.role.toLowerCase()}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-center w-[100px] rounded capitalize py-1 text-xs bg-green-500/15 dark:bg-green-600/10 text-green-700 dark:text-green-600/90 border border-green-500/25 dark:border-green-700/25">
|
||||
Active
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{formatDistanceToNow(new Date(member.user.createdAt), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2">
|
||||
{currentIsAdmin ? (
|
||||
<EditTeamMember
|
||||
teamUser={{
|
||||
...member,
|
||||
userId: String(member.userId),
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{currentIsAdmin || session?.user.id == member.userId ? (
|
||||
<DeleteTeamMember
|
||||
teamUser={{
|
||||
userId: String(member.userId),
|
||||
role: member.role,
|
||||
email: member.user?.email || "Unknown user",
|
||||
}}
|
||||
self={session?.user.id == member.userId}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow className="h-32">
|
||||
<TableCell colSpan={5} className="text-center py-4">
|
||||
No team members found
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
||||
{/* Pending invites section */}
|
||||
{pendingInvites.length > 0 && (
|
||||
<>
|
||||
{pendingInvites.map((invite) => (
|
||||
<TableRow key={invite.id} className="">
|
||||
<TableCell className="font-medium">
|
||||
{invite.email}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className=" w-[100px] rounded capitalize py-1 text-xs">
|
||||
{invite.role.toLowerCase()}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-center w-[100px] rounded capitalize py-1 text-xs bg-yellow-500/15 dark:bg-yellow-600/10 text-yellow-700 dark:text-yellow-600/90 border border-yellow-500/25 dark:border-yellow-700/25">
|
||||
Pending
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{formatDistanceToNow(new Date(invite.createdAt), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2">
|
||||
{currentIsAdmin ? (
|
||||
<ResendTeamInvite invite={invite} />
|
||||
) : null}
|
||||
{currentIsAdmin ? (
|
||||
<DeleteTeamInvite invite={invite} />
|
||||
) : null}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user