Skip to content
Open
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
134 changes: 79 additions & 55 deletions app/(dashboard)/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import {
CardFooter
} from '@/components/ui/card';
import { customerPortalAction } from '@/lib/payments/actions';
import { useActionState } from 'react';
import { TeamDataWithMembers, User } from '@/lib/db/schema';
import { useActionState, useState } from 'react';
import { TeamDataWithMembers, TeamMemberWithUser, User } from '@/lib/db/schema';
import { removeTeamMember, inviteTeamMember } from '@/app/(login)/actions';
import useSWR from 'swr';
import { Suspense } from 'react';
Expand Down Expand Up @@ -95,14 +95,6 @@ function TeamMembersSkeleton() {

function TeamMembers() {
const { data: teamData } = useSWR<TeamDataWithMembers>('/api/team', fetcher);
const [removeState, removeAction, isRemovePending] = useActionState<
ActionState,
FormData
>(removeTeamMember, {});

const getUserDisplayName = (user: Pick<User, 'id' | 'name' | 'email'>) => {
return user.name || user.email || 'Unknown User';
};

if (!teamData?.teamMembers?.length) {
return (
Expand All @@ -125,58 +117,90 @@ function TeamMembers() {
<CardContent>
<ul className="space-y-4">
{teamData.teamMembers.map((member, index) => (
<li key={member.id} className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<Avatar>
{/*
This app doesn't save profile images, but here
is how you'd show them:

<AvatarImage
src={member.user.image || ''}
alt={getUserDisplayName(member.user)}
/>
*/}
<AvatarFallback>
{getUserDisplayName(member.user)
.split(' ')
.map((n) => n[0])
.join('')}
</AvatarFallback>
</Avatar>
<div>
<p className="font-medium">
{getUserDisplayName(member.user)}
</p>
<p className="text-sm text-muted-foreground capitalize">
{member.role}
</p>
</div>
</div>
{index > 1 ? (
<form action={removeAction}>
<input type="hidden" name="memberId" value={member.id} />
<Button
type="submit"
variant="outline"
size="sm"
disabled={isRemovePending}
>
{isRemovePending ? 'Removing...' : 'Remove'}
</Button>
</form>
) : null}
</li>
<TeamMemberRow key={member.id} member={member} index={index} />
))}
</ul>
{removeState?.error && (
<p className="text-red-500 mt-4">{removeState.error}</p>
)}
</CardContent>
</Card>
);
}

function TeamMemberRow({
member,
index,
}: {
member: TeamMemberWithUser;
index: number;
}) {
const [visible, setVisible] = useState(true);
const [removeState, removeAction, isRemovePending] = useActionState<
ActionState,
FormData
>(async (actionState,formData) => {
const result = await removeTeamMember(actionState,formData);
if ('success' in result && typeof result.success !== 'undefined') {
setVisible(false);
}
return result;
}, {});

const getUserDisplayName = (user: Pick<User, "id" | "name" | "email">) => {
return user.name || user.email || "Unknown User";
};

if (!visible) return null;

return (
<li className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<Avatar>
{/*
This app doesn't save profile images, but here
is how you'd show them:

<AvatarImage
src={member.user.image || ''}
alt={getUserDisplayName(member.user)}
/>
*/}
<AvatarFallback>
{getUserDisplayName(member.user)
.split(' ')
.map((n) => n[0])
.join('')}
</AvatarFallback>
</Avatar>
<div>
<p className="font-medium">
{getUserDisplayName(member.user)}
</p>
<p className="text-sm text-muted-foreground capitalize">
{member.role}
</p>
</div>
</div>
{index > 0 && (
<div className="flex flex-col items-end">
<form action={removeAction}>
<input type="hidden" name="memberId" value={member.id} />
<Button
type="submit"
variant="outline"
size="sm"
disabled={isRemovePending}
>
{isRemovePending ? 'Removing...' : 'Remove'}
</Button>
</form>
{removeState?.error && (
<p className="text-red-500 text-xs">{removeState.error}</p>
)}
</div>
)}
</li>
);
}

function InviteTeamMemberSkeleton() {
return (
<Card className="h-[260px]">
Expand Down
2 changes: 1 addition & 1 deletion app/(login)/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ export const updateAccount = validatedActionWithUser(
);

const removeTeamMemberSchema = z.object({
memberId: z.number()
memberId: z.string().transform((val) => Number(val)),
});

export const removeTeamMember = validatedActionWithUser(
Expand Down
7 changes: 4 additions & 3 deletions lib/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,11 @@ export type ActivityLog = typeof activityLogs.$inferSelect;
export type NewActivityLog = typeof activityLogs.$inferInsert;
export type Invitation = typeof invitations.$inferSelect;
export type NewInvitation = typeof invitations.$inferInsert;
export type TeamMemberWithUser = TeamMember & {
user: Pick<User, "id" | "name" | "email">;
};
export type TeamDataWithMembers = Team & {
teamMembers: (TeamMember & {
user: Pick<User, 'id' | 'name' | 'email'>;
})[];
teamMembers: TeamMemberWithUser[];
};

export enum ActivityType {
Expand Down