From fc8ce296be5898524434f866a2824f0f098b8c85 Mon Sep 17 00:00:00 2001 From: Arik Chakma Date: Fri, 28 Jul 2023 20:11:58 +0600 Subject: [PATCH] Team Member listing and Progress Reminder (#4264) * wip: team member listing * wip: no progress alert * wip: mail icon * feat: Send progress reminder * fix: guard clause * chore: resend invite --- src/components/ReactIcons/MailIcon.tsx | 23 ++ .../TeamMembers/MemberActionDropdown.tsx | 24 +- src/components/TeamMembers/RoleBadge.tsx | 9 +- src/components/TeamMembers/TeamMemberItem.tsx | 132 ++++++++++ .../TeamMembers/TeamMembersPage.tsx | 237 ++++++++++++------ 5 files changed, 329 insertions(+), 96 deletions(-) create mode 100644 src/components/ReactIcons/MailIcon.tsx create mode 100644 src/components/TeamMembers/TeamMemberItem.tsx diff --git a/src/components/ReactIcons/MailIcon.tsx b/src/components/ReactIcons/MailIcon.tsx new file mode 100644 index 000000000..ef1c82f56 --- /dev/null +++ b/src/components/ReactIcons/MailIcon.tsx @@ -0,0 +1,23 @@ +interface MailIconProps { + className?: string; +} +export function MailIcon(props: MailIconProps) { + const { className } = props; + return ( + + + + + ); +} diff --git a/src/components/TeamMembers/MemberActionDropdown.tsx b/src/components/TeamMembers/MemberActionDropdown.tsx index 403e4cf4f..029972ea7 100644 --- a/src/components/TeamMembers/MemberActionDropdown.tsx +++ b/src/components/TeamMembers/MemberActionDropdown.tsx @@ -9,10 +9,12 @@ export function MemberActionDropdown({ member, onUpdateMember, onDeleteMember, + onResendInvite, isDisabled = false, }: { onDeleteMember: () => void; onUpdateMember: () => void; + onResendInvite: () => void; isDisabled: boolean; member: TeamMemberDocument; }) { @@ -25,23 +27,6 @@ export function MemberActionDropdown({ setIsOpen(false); }); - async function resendInvite() { - const { response, error } = await httpPatch( - `${import.meta.env.PUBLIC_API_URL}/v1-resend-invite/${member.teamId}/${ - member._id - }`, - {} - ); - - if (error || !response) { - setIsLoading(false); - toast.error(error?.message || 'Something went wrong'); - return; - } - - window.location.reload(); - } - const actions = [ { name: 'Delete', @@ -61,7 +46,10 @@ export function MemberActionDropdown({ ? [ { name: 'Resend Invite', - handleClick: resendInvite, + handleClick: () => { + onResendInvite(); + setIsOpen(false); + }, }, ] : []), diff --git a/src/components/TeamMembers/RoleBadge.tsx b/src/components/TeamMembers/RoleBadge.tsx index f65e8e9f1..18612b5cc 100644 --- a/src/components/TeamMembers/RoleBadge.tsx +++ b/src/components/TeamMembers/RoleBadge.tsx @@ -3,11 +3,10 @@ import type { AllowedRoles } from '../CreateTeam/RoleDropdown'; export function MemberRoleBadge({ role }: { role: AllowedRoles }) { return ( {role} diff --git a/src/components/TeamMembers/TeamMemberItem.tsx b/src/components/TeamMembers/TeamMemberItem.tsx new file mode 100644 index 000000000..93e41a568 --- /dev/null +++ b/src/components/TeamMembers/TeamMemberItem.tsx @@ -0,0 +1,132 @@ +import { MailIcon } from '../ReactIcons/MailIcon'; +import { MemberActionDropdown } from './MemberActionDropdown'; +import { MemberRoleBadge } from './RoleBadge'; +import type { TeamMemberItem } from './TeamMembersPage'; + +type TeamMemberProps = { + member: TeamMemberItem; + userId: string; + index: number; + teamId: string; + canManageCurrentTeam: boolean; + handleDeleteMember: () => void; + onUpdateMember: () => void; + handleSendReminder: () => void; + onResendInvite: () => void; +}; + +export function TeamMemberItem(props: TeamMemberProps) { + const { + member, + index, + onResendInvite, + onUpdateMember, + canManageCurrentTeam, + userId, + handleDeleteMember, + handleSendReminder, + } = props; + + const showNoProgress = + member.progress.length === 0 && member.status === 'joined'; + const showReminder = + member.progress.length === 0 && + member.status === 'joined' && + !(member.userId === userId); + + return ( +
+
+ {member.name +
+
+ + {showReminder && ( + + )} +
+
+

+ {member.name} + {showNoProgress && ( + + No Progress + + )} + {member.userId === userId && ( + + You + + )} +

+
+ {member.status === 'invited' && ( + + Invited + + )} + {member.status === 'rejected' && ( + + Rejected + + )} +
+
+

+ {member.invitedEmail} +

+
+
+ +
+ {showReminder && ( + + + + )} + + + + {canManageCurrentTeam && ( + + )} +
+
+ ); +} + +type SendProgressReminderProps = { + handleSendReminder: () => void; +}; + +function SendProgressReminder(props: SendProgressReminderProps) { + const { handleSendReminder } = props; + + return ( + + ); +} diff --git a/src/components/TeamMembers/TeamMembersPage.tsx b/src/components/TeamMembers/TeamMembersPage.tsx index 9133de018..ea1153c15 100644 --- a/src/components/TeamMembers/TeamMembersPage.tsx +++ b/src/components/TeamMembers/TeamMembersPage.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'preact/hooks'; -import { httpDelete, httpGet } from '../../lib/http'; +import { httpDelete, httpGet, httpPatch } from '../../lib/http'; import { MemberActionDropdown } from './MemberActionDropdown'; import { useAuth } from '../../hooks/use-auth'; import { pageProgressMessage } from '../../stores/page'; @@ -14,6 +14,7 @@ import { useStore } from '@nanostores/preact'; import { $canManageCurrentTeam } from '../../stores/team'; import { useToast } from '../../hooks/use-toast'; import { MemberRoleBadge } from './RoleBadge'; +import { TeamMemberItem } from './TeamMemberItem'; export interface TeamMemberDocument { _id?: string; @@ -26,9 +27,23 @@ export interface TeamMemberDocument { updatedAt: Date; } -interface TeamMemberItem extends TeamMemberDocument { +export interface UserResourceProgressDocument { + _id?: string; + userId: string; + resourceId: string; + resourceType: 'roadmap' | 'best-practice'; + isFavorite?: boolean; + done: string[]; + learning: string[]; + skipped: string[]; + createdAt: Date; + updatedAt: Date; +} + +export interface TeamMemberItem extends TeamMemberDocument { name: string; avatar: string; + progress: UserResourceProgressDocument[]; } export function TeamMembersPage() { @@ -79,7 +94,6 @@ export function TeamMembersPage() { pageProgressMessage.set(''); }); }, [teamId]); - async function deleteMember(teamId: string, memberId: string) { pageProgressMessage.set('Deleting member'); const { response, error } = await httpDelete( @@ -98,6 +112,50 @@ export function TeamMembersPage() { await getTeamMemberList(); } + async function resendInvite(teamId: string, memberId: string) { + pageProgressMessage.set('Resending Invite'); + const { response, error } = await httpPatch( + `${ + import.meta.env.PUBLIC_API_URL + }/v1-resend-invite/${teamId}/${memberId}`, + {} + ); + + if (error || !response) { + toast.error(error?.message || 'Something went wrong'); + return; + } + + toast.success('Invite has been sent'); + } + + async function handleSendReminder(teamId: string, memberId: string) { + pageProgressMessage.set('Sending Reminder'); + const { response, error } = await httpPatch( + `${ + import.meta.env.PUBLIC_API_URL + }/v1-send-progress-reminder/${teamId}/${memberId}`, + {} + ); + + if (error || !response) { + toast.error(error?.message || 'Something went wrong'); + return; + } + + toast.success('Reminder has been sent'); + } + + const joinedMembers = teamMembers.filter( + (member) => member.status === 'joined' + ); + const invitedMembers = teamMembers.filter( + (member) => member.status === 'invited' + ); + const rejectedMembers = teamMembers.filter( + (member) => member.status === 'rejected' + ); + return (
{memberToUpdate && ( @@ -139,80 +197,113 @@ export function TeamMembersPage() {

- {teamMembers.map((member, index) => { + {joinedMembers.map((member, index) => { return ( -
-
- {member.name -
- - - -
-

- {member.name} - {member.userId === user?.id && ( - - You - - )} -

-
- {member.status === 'invited' && ( - - Invited - - )} - {member.status === 'rejected' && ( - - Rejected - - )} -
-
-

- {member.invitedEmail} -

-
-
- -
- - - - {canManageCurrentTeam && ( - { - deleteMember(teamId, member._id!).finally(() => { - pageProgressMessage.set(''); - }); - }} - isDisabled={member.userId === user?.id} - onUpdateMember={() => { - setMemberToUpdate(member); - }} - member={member} - /> - )} -
-
+ { + resendInvite(teamId, member._id!).finally(() => { + pageProgressMessage.set(''); + }); + }} + canManageCurrentTeam={canManageCurrentTeam} + handleDeleteMember={() => { + deleteMember(teamId, member._id!).finally(() => { + pageProgressMessage.set(''); + }); + }} + onUpdateMember={() => { + setMemberToUpdate(member); + }} + handleSendReminder={() => { + handleSendReminder(teamId, member._id!).finally(() => { + pageProgressMessage.set(''); + }); + }} + /> ); })} + + {invitedMembers.length > 0 && ( +
+

Invited Members

+
+ {invitedMembers.map((member, index) => { + return ( + { + resendInvite(teamId, member._id!).finally(() => { + pageProgressMessage.set(''); + }); + }} + canManageCurrentTeam={canManageCurrentTeam} + handleDeleteMember={() => { + deleteMember(teamId, member._id!).finally(() => { + pageProgressMessage.set(''); + }); + }} + onUpdateMember={() => { + setMemberToUpdate(member); + }} + handleSendReminder={() => { + handleSendReminder(teamId, member._id!).finally(() => { + pageProgressMessage.set(''); + }); + }} + /> + ); + })} +
+
+ )} + + {rejectedMembers.length > 0 && ( +
+

Rejected Members

+
+ {rejectedMembers.map((member, index) => { + return ( + { + resendInvite(teamId, member._id!).finally(() => { + pageProgressMessage.set(''); + }); + }} + canManageCurrentTeam={canManageCurrentTeam} + handleDeleteMember={() => { + deleteMember(teamId, member._id!).finally(() => { + pageProgressMessage.set(''); + }); + }} + onUpdateMember={() => { + setMemberToUpdate(member); + }} + handleSendReminder={() => { + handleSendReminder(teamId, member._id!).finally(() => { + pageProgressMessage.set(''); + }); + }} + /> + ); + })} +
+
+ )} {canManageCurrentTeam && (