Skip to content

Commit

Permalink
feat: team member details (#5598)
Browse files Browse the repository at this point in the history
* fix: change `topicIds` to `topicTitles`

* fix: comma and gap

* wip: member details page

* fix: team member empty state

* feat: add pagination

* fix: add loading screen
  • Loading branch information
arikchakma committed May 10, 2024
1 parent fb7136e commit 63ad6fe
Show file tree
Hide file tree
Showing 11 changed files with 303 additions and 35 deletions.
11 changes: 7 additions & 4 deletions src/components/Activity/ActivityStream.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { useMemo, useState } from 'react';
import { useState } from 'react';
import { getRelativeTimeString } from '../../lib/date';
import type { ResourceType } from '../../lib/resource-progress';
import { EmptyStream } from './EmptyStream';
import { ActivityTopicsModal } from './ActivityTopicsModal.tsx';
import { ChevronsDown, ChevronsUp } from 'lucide-react';
import { ActivityTopicTitles } from './ActivityTopicTitles.tsx';
import { cn } from '../../lib/classname.ts';

export const allowedActivityActionType = [
'in_progress',
Expand All @@ -29,10 +30,11 @@ export type UserStreamActivity = {

type ActivityStreamProps = {
activities: UserStreamActivity[];
className?: string;
};

export function ActivityStream(props: ActivityStreamProps) {
const { activities } = props;
const { activities, className } = props;

const [showAll, setShowAll] = useState(false);
const [selectedActivity, setSelectedActivity] =
Expand All @@ -48,7 +50,7 @@ export function ActivityStream(props: ActivityStreamProps) {
.slice(0, showAll ? activities.length : 10);

return (
<div className="mx-0 px-0 py-5 md:-mx-10 md:px-8 md:py-8">
<div className={cn('mx-0 px-0 py-5 md:-mx-10 md:px-8 md:py-8', className)}>
<h2 className="mb-3 text-xs uppercase text-gray-400">
Learning Activity
</h2>
Expand Down Expand Up @@ -78,6 +80,7 @@ export function ActivityStream(props: ActivityStreamProps) {
updatedAt,
topicTitles,
isCustomResource,
resourceSlug,
} = activity;

const resourceUrl =
Expand All @@ -86,7 +89,7 @@ export function ActivityStream(props: ActivityStreamProps) {
: resourceType === 'best-practice'
? `/best-practices/${resourceId}`
: isCustomResource && resourceType === 'roadmap'
? `/r/${resourceId}`
? `/r/${resourceSlug}`
: `/${resourceId}`;

const resourceLinkComponent = (
Expand Down
35 changes: 23 additions & 12 deletions src/components/Activity/ResourceProgress.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getUser } from '../../lib/jwt';
import { getPercentage } from '../../helper/number';
import { ResourceProgressActions } from './ResourceProgressActions';
import { cn } from '../../lib/classname';

type ResourceProgressType = {
resourceType: 'roadmap' | 'best-practice';
Expand All @@ -15,10 +16,15 @@ type ResourceProgressType = {
showClearButton?: boolean;
isCustomResource: boolean;
roadmapSlug?: string;
showActions?: boolean;
};

export function ResourceProgress(props: ResourceProgressType) {
const { showClearButton = true, isCustomResource } = props;
const {
showClearButton = true,
isCustomResource,
showActions = true,
} = props;

const userId = getUser()?.id;

Expand Down Expand Up @@ -52,7 +58,10 @@ export function ResourceProgress(props: ResourceProgressType) {
<a
target="_blank"
href={url}
className="group relative flex items-center justify-between overflow-hidden rounded-md border border-gray-300 bg-white px-3 py-2 pr-7 text-left text-sm transition-all hover:border-gray-400"
className={cn(
'group relative flex items-center justify-between overflow-hidden rounded-md border border-gray-300 bg-white px-3 py-2 text-left text-sm transition-all hover:border-gray-400',
showActions ? 'pr-7' : '',
)}
>
<span className="flex-grow truncate">{title}</span>
<span className="text-xs text-gray-400">
Expand All @@ -67,16 +76,18 @@ export function ResourceProgress(props: ResourceProgressType) {
></span>
</a>

<div className="absolute right-2 top-0 flex h-full items-center">
<ResourceProgressActions
userId={userId!}
resourceType={resourceType}
resourceId={resourceId}
isCustomResource={isCustomResource}
onCleared={onCleared}
showClearButton={showClearButton}
/>
</div>
{showActions && (
<div className="absolute right-2 top-0 flex h-full items-center">
<ResourceProgressActions
userId={userId!}
resourceType={resourceType}
resourceId={resourceId}
isCustomResource={isCustomResource}
onCleared={onCleared}
showClearButton={showClearButton}
/>
</div>
)}
</div>
);
}
16 changes: 10 additions & 6 deletions src/components/TeamActivity/TeamActivityItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type TeamActivityItemProps = {
name: string;
avatar?: string | undefined;
username?: string | undefined;
memberId?: string;
};
};

Expand Down Expand Up @@ -62,14 +63,17 @@ export function TeamActivityItem(props: TeamActivityItemProps) {
: '/images/default-avatar.png';

const username = (
<>
<a
href={`/team/member?t=${teamId}&m=${user?.memberId}`}
className="inline-flex items-center underline underline-offset-2 hover:no-underline"
>
<img
className="mr-1 inline-block h-5 w-5 rounded-full"
src={userAvatar}
alt={user.name}
/>
<span className="font-medium">{user?.name || 'Unknown'}</span>
</>
<span className="font-medium">{user?.name || 'Unknown'}</span>&nbsp;
</a>
);

if (activities.length === 1) {
Expand Down Expand Up @@ -137,9 +141,9 @@ export function TeamActivityItem(props: TeamActivityItemProps) {

return (
<li key={user._id} className="overflow-hidden rounded-md border">
<h3 className="flex flex-wrap items-center gap-1 bg-gray-100 px-2 py-2.5 text-sm">
{username} has {activities.length} updates in {uniqueResourcesCount}{' '}
resource(s)
<h3 className="flex flex-wrap items-center bg-gray-100 px-2 py-2.5 text-sm">
{username} has {activities.length} updates in {uniqueResourcesCount}
&nbsp;resource(s)
</h3>
<div className="py-3">
<ul className="ml-2 flex flex-col divide-y pr-2 sm:ml-[36px]">
Expand Down
1 change: 1 addition & 0 deletions src/components/TeamActivity/TeamActivityPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type GetTeamActivityResponse = {
name: string;
avatar?: string;
username?: string;
memberId?: string;
}[];
activities: TeamActivityStreamDocument[];
};
Expand Down
176 changes: 176 additions & 0 deletions src/components/TeamMemberDetails/TeamMemberDetailsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { useEffect, useState } from 'react';
import { httpGet } from '../../lib/http';
import { pageProgressMessage } from '../../stores/page';
import { getUrlParams } from '../../lib/browser';
import { useToast } from '../../hooks/use-toast';
import type { TeamMemberDocument } from '../TeamMembers/TeamMembersPage';
import type { UserProgress } from '../TeamProgress/TeamProgressPage';
import type { TeamActivityStreamDocument } from '../TeamActivity/TeamActivityPage';
import { ResourceProgress } from '../Activity/ResourceProgress';
import { ActivityStream } from '../Activity/ActivityStream';
import { MemberRoleBadge } from '../TeamMembers/RoleBadge';
import { TeamMemberEmptyPage } from './TeamMemberEmptyPage';
import { Pagination } from '../Pagination/Pagination';

type GetTeamMemberProgressesResponse = TeamMemberDocument & {
name: string;
avatar: string;
email: string;
progresses: UserProgress[];
};

type GetTeamMemberActivityResponse = {
data: TeamActivityStreamDocument[];
totalCount: number;
totalPages: number;
currPage: number;
perPage: number;
};

export function TeamMemberDetailsPage() {
const { t: teamId, m: memberId } = getUrlParams() as { t: string; m: string };

const toast = useToast();

const [memberProgress, setMemberProgress] =
useState<GetTeamMemberProgressesResponse | null>(null);
const [memberActivity, setMemberActivity] =
useState<GetTeamMemberActivityResponse | null>(null);
const [currPage, setCurrPage] = useState(1);

const loadMemberProgress = async () => {
const { response, error } = await httpGet<GetTeamMemberProgressesResponse>(
`${import.meta.env.PUBLIC_API_URL}/v1-get-team-member-progresses/${teamId}/${memberId}`,
);
if (error || !response) {
pageProgressMessage.set('');
toast.error(error?.message || 'Failed to load team member');
return;
}

setMemberProgress(response);
};

const loadMemberActivity = async (currPage: number = 1) => {
const { response, error } = await httpGet<GetTeamMemberActivityResponse>(
`${import.meta.env.PUBLIC_API_URL}/v1-get-team-member-activity/${teamId}/${memberId}`,
{
currPage,
},
);
if (error || !response) {
pageProgressMessage.set('');
toast.error(error?.message || 'Failed to load team member activity');
return;
}

setMemberActivity(response);
setCurrPage(response?.currPage || 1);
};

useEffect(() => {
if (!teamId) {
return;
}

Promise.allSettled([loadMemberProgress(), loadMemberActivity()]).finally(
() => {
pageProgressMessage.set('');
},
);
}, [teamId]);

if (!teamId || !memberId || !memberProgress || !memberActivity) {
return null;
}

const avatarUrl = memberProgress?.avatar
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${memberProgress?.avatar}`
: '/images/default-avatar.png';

return (
<>
<div>
<div className="flex items-center gap-4">
<img
src={avatarUrl}
alt={memberProgress?.name}
className="h-24 w-24 rounded-full"
/>
<div>
<MemberRoleBadge
className="sm:inline-flex"
role={memberProgress?.role!}
/>
<h1 className="mt-1 text-2xl font-medium">
{memberProgress?.name}
</h1>
<p className="text-sm text-gray-500">{memberProgress?.email}</p>
</div>
</div>
</div>

<hr className="my-8 border-gray-200" />

{memberProgress?.progresses && memberProgress?.progresses?.length > 0 ? (
<>
<h2 className="mb-3 text-xs uppercase text-gray-400">
Progress Overview
</h2>
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-2">
{memberProgress?.progresses?.map((progress) => {
const learningCount = progress.learning || 0;
const doneCount = progress.done || 0;
const totalCount = progress.total || 0;
const skippedCount = progress.skipped || 0;

return (
<ResourceProgress
key={progress.resourceId}
isCustomResource={progress.isCustomResource!}
doneCount={doneCount > totalCount ? totalCount : doneCount}
learningCount={
learningCount > totalCount ? totalCount : learningCount
}
totalCount={totalCount}
skippedCount={skippedCount}
resourceId={progress.resourceId}
resourceType={'roadmap'}
updatedAt={progress.updatedAt}
title={progress.resourceTitle}
roadmapSlug={progress.roadmapSlug}
showActions={false}
/>
);
})}
</div>
</>
) : (
<TeamMemberEmptyPage teamId={teamId} />
)}

{memberActivity?.data && memberActivity?.data?.length > 0 ? (
<>
<ActivityStream
className="mt-8 p-0 md:m-0 md:mb-4 md:mt-8 md:p-0"
activities={
memberActivity?.data?.flatMap((act) => act.activity) || []
}
/>
<Pagination
currPage={currPage}
totalPages={memberActivity?.totalPages || 1}
totalCount={memberActivity?.totalCount || 0}
perPage={memberActivity?.perPage || 10}
onPageChange={(page) => {
pageProgressMessage.set('Loading Activity');
loadMemberActivity(page).finally(() => {
pageProgressMessage.set('');
});
}}
/>
</>
) : null}
</>
);
}
29 changes: 29 additions & 0 deletions src/components/TeamMemberDetails/TeamMemberEmptyPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { RoadmapIcon } from '../ReactIcons/RoadmapIcon';

type TeamMemberEmptyPageProps = {
teamId: string;
};

export function TeamMemberEmptyPage(props: TeamMemberEmptyPageProps) {
const { teamId } = props;

return (
<div className="rounded-md">
<div className="flex flex-col items-center p-7 text-center">
<RoadmapIcon className="mb-2 h-[60px] w-[60px] opacity-10 sm:h-[120px] sm:w-[120px]" />

<h2 className="text-lg font-bold sm:text-xl">No Progress</h2>
<p className="my-1 max-w-[400px] text-balance text-sm text-gray-500 sm:my-2 sm:text-base">
Progress will appear here as they start tracking their{' '}
<a
href={`/team/roadmaps?t=${teamId}`}
className="mt-4 text-blue-500 hover:underline"
>
Roadmaps
</a>{' '}
progress.
</p>
</div>
</div>
);
}
Loading

0 comments on commit 63ad6fe

Please sign in to comment.