Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
15 changes: 6 additions & 9 deletions apps/dashboard/src/@/api/team-members.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import "server-only";
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
import { API_SERVER_URL } from "@/constants/env";
import { cookies } from "next/headers";
import { getAuthToken } from "../../app/api/lib/getAuthToken";

const TeamAccountRole = {
OWNER: "OWNER",
Expand All @@ -26,14 +25,10 @@ export type TeamMember = {
};

export async function getMembers(teamSlug: string) {
const cookiesManager = cookies();
const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
const token = activeAccount
? cookiesManager.get(COOKIE_PREFIX_TOKEN + activeAccount)?.value
: null;
const token = getAuthToken();

if (!token) {
return [];
return undefined;
}

const teamsRes = await fetch(
Expand All @@ -44,8 +39,10 @@ export async function getMembers(teamSlug: string) {
},
},
);

if (teamsRes.ok) {
return (await teamsRes.json())?.result as TeamMember[];
}
return [];

return undefined;
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,19 @@ export function InviteSection(props: {
}) {
const teamPlan = getValidTeamPlan(props.team);
let bottomSection: React.ReactNode = null;
const inviteEnabled = teamPlan === "pro" && props.userHasEditPermission;
const inviteEnabled = teamPlan !== "free" && props.userHasEditPermission;

if (teamPlan !== "pro") {
if (teamPlan === "free") {
bottomSection = (
<div className="lg:px6 flex items-center justify-between gap-4 border-border border-t px-4 py-4">
<p className="text-muted-foreground text-sm">
This feature is only available on the{" "}
This feature is not available on the Free Plan.{" "}
<Link
href="https://thirdweb.com/pricing"
target="_blank"
className="text-link-foreground hover:text-foreground"
>
Pro plan <ExternalLinkIcon className="inline size-3" />
View plans <ExternalLinkIcon className="inline size-3" />
</Link>
</p>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import {
SelectValue,
} from "@/components/ui/select";
import { EllipsisIcon, SearchIcon } from "lucide-react";
import { useState } from "react";
import { useMemo, useState } from "react";

type RoleFilterValue = "ALL ROLES" | TeamAccountRole;

export function ManageMembersSection(props: {
team: Team;
Expand All @@ -23,6 +25,36 @@ export function ManageMembersSection(props: {
}) {
let topSection: React.ReactNode = null;

const [role, setRole] = useState<RoleFilterValue>("ALL ROLES");
const [sortBy, setSortBy] = useState<MemberSortId>("date");

const membersToShow = useMemo(() => {
let value = props.members;
if (role !== "ALL ROLES") {
value = value.filter((m) => m.role === role);
}

switch (sortBy) {
case "date":
value = value.sort(
(a, b) => a.createdAt.getTime() - b.createdAt.getTime(),
);
break;
case "a-z":
value = value.sort((a, b) =>
a.account.name.localeCompare(b.account.name),
);
break;
case "z-a":
value = value.sort((a, b) =>
b.account.name.localeCompare(a.account.name),
);
break;
}

return value;
}, [role, props.members, sortBy]);

if (!props.userHasEditPermission) {
topSection = (
<div className="border-border border-b p-4">
Expand All @@ -39,22 +71,22 @@ export function ManageMembersSection(props: {
id="select-all"
className="border-muted-foreground data-[state=checked]:border-inverted"
disabled={
!props.userHasEditPermission || props.members.length === 0
!props.userHasEditPermission || membersToShow.length === 0
}
/>
<Label
htmlFor="select-all"
className="cursor-pointer text-muted-foreground"
>
Select All ({props.members.length})
Select All ({membersToShow.length})
</Label>
</div>

<Button
size="icon"
variant="ghost"
className="!h-auto !w-auto p-1.5"
disabled={!props.userHasEditPermission || props.members.length === 0}
disabled={!props.userHasEditPermission || membersToShow.length === 0}
>
<EllipsisIcon className="size-4 text-muted-foreground" />
</Button>
Expand All @@ -68,7 +100,14 @@ export function ManageMembersSection(props: {

<div className="h-3" />

<FiltersSection disabled={props.members.length === 0} />
<FiltersSection
// don't use membersToShow here
disabled={props.members.length === 0}
role={role}
setRole={setRole}
setSortBy={setSortBy}
sortBy={sortBy}
/>

<div className="h-4" />

Expand All @@ -77,11 +116,14 @@ export function ManageMembersSection(props: {
{/* Top section */}
{topSection}

{props.members.length > 0 && (
{membersToShow.length > 0 && (
<ul>
{props.members.map((member) => {
{membersToShow.map((member) => {
return (
<li key={member.accountId}>
<li
key={member.accountId}
className="border-border border-b last:border-b-0"
>
<MemberRow
member={member}
userHasEditPermission={props.userHasEditPermission}
Expand All @@ -93,7 +135,7 @@ export function ManageMembersSection(props: {
)}

{/* Empty state */}
{props.members.length === 0 && (
{membersToShow.length === 0 && (
<div className="flex justify-center px-4 py-10">
<p className="text-muted-foreground text-sm">No Members Found</p>
</div>
Expand All @@ -108,7 +150,7 @@ function MemberRow(props: {
userHasEditPermission: boolean;
}) {
return (
<div className="flex items-center justify-between border-border border-b px-4 py-4">
<div className="flex items-center justify-between px-4 py-4">
<div className="flex items-center gap-3 lg:gap-4">
{/* Checkbox */}
<Checkbox
Expand Down Expand Up @@ -150,7 +192,12 @@ function MemberRow(props: {

function FiltersSection(props: {
disabled: boolean;
role: RoleFilterValue;
setRole: (role: RoleFilterValue) => void;
setSortBy: (sortBy: MemberSortId) => void;
sortBy: MemberSortId;
}) {
const { role, setRole, setSortBy, sortBy } = props;
return (
<div className="flex flex-col gap-4 lg:flex-row lg:items-center">
{/* Search */}
Expand All @@ -164,8 +211,12 @@ function FiltersSection(props: {
</div>

<div className="grid grid-cols-2 items-center gap-3 lg:flex">
<RoleSelector disabled={props.disabled} />
<SortMembersBy disabled={props.disabled} />
<RoleSelector disabled={props.disabled} role={role} setRole={setRole} />
<SortMembersBy
disabled={props.disabled}
setSortBy={setSortBy}
sortBy={sortBy}
/>
</div>
</div>
);
Expand All @@ -175,8 +226,10 @@ type MemberSortId = "date" | "a-z" | "z-a";

function SortMembersBy(props: {
disabled?: boolean;
setSortBy: (sortBy: MemberSortId) => void;
sortBy: MemberSortId;
}) {
const [sortBy, setSortBy] = useState<MemberSortId>("date");
const { sortBy, setSortBy } = props;
const valueToLabel: Record<MemberSortId, string> = {
date: "Date",
"a-z": "Name (A-Z)",
Expand Down Expand Up @@ -211,19 +264,17 @@ function SortMembersBy(props: {

function RoleSelector(props: {
disabled?: boolean;
role: RoleFilterValue;
setRole: (role: RoleFilterValue) => void;
}) {
const roles: (TeamAccountRole | "ALL ROLES")[] = [
"OWNER",
"MEMBER",
"ALL ROLES",
];
const [role, setRole] = useState<TeamAccountRole | "ALL ROLES">("ALL ROLES");
const { role, setRole } = props;
const roles: RoleFilterValue[] = ["OWNER", "MEMBER", "ALL ROLES"];

return (
<Select
value={role}
onValueChange={(v) => {
setRole(v as TeamAccountRole);
setRole(v as RoleFilterValue);
}}
>
<SelectTrigger
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Team } from "@/api/team";
import type { TeamAccountRole, TeamMember } from "@/api/team-members";
import { Toaster } from "@/components/ui/sonner";
import type { Meta, StoryObj } from "@storybook/react";
import { teamStub } from "../../../../../../../stories/stubs";
import {
BadgeContainer,
mobileViewport,
Expand Down Expand Up @@ -34,37 +34,22 @@ export const Mobile: Story = {
},
};

const freeTeam: Team = {
id: "team-id-foo-bar",
name: "Team XYZ",
slug: "team-slug-foo-bar",
createdAt: "2023-07-07T19:21:33.604Z",
updatedAt: "2024-07-11T00:01:02.241Z",
billingStatus: "validPayment",
billingPlan: "free",
billingEmail: "[email protected]",
};

const proTeam: Team = {
id: "team-id-foo-bar",
name: "Team XYZ",
slug: "team-slug-foo-bar",
createdAt: "2023-07-07T19:21:33.604Z",
updatedAt: "2024-07-11T00:01:02.241Z",
billingStatus: "validPayment",
billingPlan: "pro",
billingEmail: "[email protected]",
};
const freeTeam = teamStub("foo", "free");
const proTeam = teamStub("bar", "pro");
const growthTeam = teamStub("bazz", "growth");

function createMemberStub(id: string, role: TeamAccountRole): TeamMember {
function createMemberStub(
id: string,
role: TeamAccountRole,
createdHours: number,
): TeamMember {
const date = new Date();
// add random time to the date
date.setHours(Math.floor(Math.random() * 24));
date.setHours(createdHours);

const member: TeamMember = {
account: {
email: `user-${id}@foo.com`,
name: `username-${id}`,
name: id,
},
accountId: `account-id-${id}`,
createdAt: date,
Expand All @@ -78,9 +63,9 @@ function createMemberStub(id: string, role: TeamAccountRole): TeamMember {
}

const membersStub: TeamMember[] = [
createMemberStub("1", "OWNER"),
createMemberStub("2", "MEMBER"),
createMemberStub("3", "OWNER"),
createMemberStub("first-member", "OWNER", 1),
createMemberStub("third-member", "MEMBER", 3),
createMemberStub("second-member", "OWNER", 2),
];

function Story() {
Expand All @@ -107,7 +92,7 @@ function CompVariants() {

{/* Invite */}
<div className="flex flex-col gap-10">
<BadgeContainer label="Not a Pro Team">
<BadgeContainer label="Free Team">
<InviteSection team={freeTeam} userHasEditPermission={false} />
</BadgeContainer>

Expand All @@ -118,35 +103,32 @@ function CompVariants() {
<BadgeContainer label="Pro, User has permission">
<InviteSection team={proTeam} userHasEditPermission={true} />
</BadgeContainer>

<BadgeContainer label="Growth, User has permission">
<InviteSection team={growthTeam} userHasEditPermission={true} />
</BadgeContainer>
</div>

<div className="my-10" />

{/* Invite */}
<h2 className="py-4 font-semibold text-3xl">Team Members Variants</h2>

<div className="flex flex-col gap-10">
<BadgeContainer label="Pro Team, has permission">
<BadgeContainer label="Has permission">
<ManageMembersSection
team={proTeam}
team={freeTeam}
userHasEditPermission={true}
members={membersStub}
/>
</BadgeContainer>

<BadgeContainer label="Not a Pro Team, No permission">
<BadgeContainer label="No permission">
<ManageMembersSection
team={freeTeam}
userHasEditPermission={false}
members={membersStub}
/>
</BadgeContainer>

<BadgeContainer label="Pro Team, No permission">
<ManageMembersSection
team={proTeam}
userHasEditPermission={false}
members={membersStub}
/>
</BadgeContainer>
</div>
</div>
</div>
Expand Down
Loading
Loading