diff --git a/src/components/Activity/ActivityStream.tsx b/src/components/Activity/ActivityStream.tsx
index 9cdae7ef48c3..f262093a7c17 100644
--- a/src/components/Activity/ActivityStream.tsx
+++ b/src/components/Activity/ActivityStream.tsx
@@ -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',
@@ -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] =
@@ -48,7 +50,7 @@ export function ActivityStream(props: ActivityStreamProps) {
.slice(0, showAll ? activities.length : 10);
return (
-
+
Learning Activity
@@ -78,6 +80,7 @@ export function ActivityStream(props: ActivityStreamProps) {
updatedAt,
topicTitles,
isCustomResource,
+ resourceSlug,
} = activity;
const resourceUrl =
@@ -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 = (
diff --git a/src/components/Activity/ResourceProgress.tsx b/src/components/Activity/ResourceProgress.tsx
index 05953c842e6c..4bca95a055bd 100644
--- a/src/components/Activity/ResourceProgress.tsx
+++ b/src/components/Activity/ResourceProgress.tsx
@@ -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';
@@ -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;
@@ -52,7 +58,10 @@ export function ResourceProgress(props: ResourceProgressType) {
{title}
@@ -67,16 +76,18 @@ export function ResourceProgress(props: ResourceProgressType) {
>
-
-
-
+ {showActions && (
+
+
+
+ )}
);
}
diff --git a/src/components/TeamActivity/TeamActivityItem.tsx b/src/components/TeamActivity/TeamActivityItem.tsx
index 6e40e30acbdf..5419c78a4360 100644
--- a/src/components/TeamActivity/TeamActivityItem.tsx
+++ b/src/components/TeamActivity/TeamActivityItem.tsx
@@ -14,6 +14,7 @@ type TeamActivityItemProps = {
name: string;
avatar?: string | undefined;
username?: string | undefined;
+ memberId?: string;
};
};
@@ -62,14 +63,17 @@ export function TeamActivityItem(props: TeamActivityItemProps) {
: '/images/default-avatar.png';
const username = (
- <>
+
- {user?.name || 'Unknown'}
- >
+ {user?.name || 'Unknown'}
+
);
if (activities.length === 1) {
@@ -137,9 +141,9 @@ export function TeamActivityItem(props: TeamActivityItemProps) {
return (
-
- {username} has {activities.length} updates in {uniqueResourcesCount}{' '}
- resource(s)
+
+ {username} has {activities.length} updates in {uniqueResourcesCount}
+ resource(s)
diff --git a/src/components/TeamActivity/TeamActivityPage.tsx b/src/components/TeamActivity/TeamActivityPage.tsx
index 29df4e3f0961..f2200572cb45 100644
--- a/src/components/TeamActivity/TeamActivityPage.tsx
+++ b/src/components/TeamActivity/TeamActivityPage.tsx
@@ -39,6 +39,7 @@ type GetTeamActivityResponse = {
name: string;
avatar?: string;
username?: string;
+ memberId?: string;
}[];
activities: TeamActivityStreamDocument[];
};
diff --git a/src/components/TeamMemberDetails/TeamMemberDetailsPage.tsx b/src/components/TeamMemberDetails/TeamMemberDetailsPage.tsx
new file mode 100644
index 000000000000..3f81831bdd10
--- /dev/null
+++ b/src/components/TeamMemberDetails/TeamMemberDetailsPage.tsx
@@ -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(null);
+ const [memberActivity, setMemberActivity] =
+ useState(null);
+ const [currPage, setCurrPage] = useState(1);
+
+ const loadMemberProgress = async () => {
+ const { response, error } = await httpGet(
+ `${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(
+ `${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 (
+ <>
+
+
+

+
+
+
+ {memberProgress?.name}
+
+
{memberProgress?.email}
+
+
+
+
+
+
+ {memberProgress?.progresses && memberProgress?.progresses?.length > 0 ? (
+ <>
+
+ Progress Overview
+
+
+ {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 (
+ 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}
+ />
+ );
+ })}
+
+ >
+ ) : (
+
+ )}
+
+ {memberActivity?.data && memberActivity?.data?.length > 0 ? (
+ <>
+ act.activity) || []
+ }
+ />
+ {
+ pageProgressMessage.set('Loading Activity');
+ loadMemberActivity(page).finally(() => {
+ pageProgressMessage.set('');
+ });
+ }}
+ />
+ >
+ ) : null}
+ >
+ );
+}
diff --git a/src/components/TeamMemberDetails/TeamMemberEmptyPage.tsx b/src/components/TeamMemberDetails/TeamMemberEmptyPage.tsx
new file mode 100644
index 000000000000..b3537c34da32
--- /dev/null
+++ b/src/components/TeamMemberDetails/TeamMemberEmptyPage.tsx
@@ -0,0 +1,29 @@
+import { RoadmapIcon } from '../ReactIcons/RoadmapIcon';
+
+type TeamMemberEmptyPageProps = {
+ teamId: string;
+};
+
+export function TeamMemberEmptyPage(props: TeamMemberEmptyPageProps) {
+ const { teamId } = props;
+
+ return (
+
+
+
+
+
No Progress
+
+ Progress will appear here as they start tracking their{' '}
+
+ Roadmaps
+ {' '}
+ progress.
+
+
+
+ );
+}
diff --git a/src/components/TeamMembers/RoleBadge.tsx b/src/components/TeamMembers/RoleBadge.tsx
index 18612b5cc556..f42c81e79173 100644
--- a/src/components/TeamMembers/RoleBadge.tsx
+++ b/src/components/TeamMembers/RoleBadge.tsx
@@ -1,12 +1,23 @@
+import { cn } from '../../lib/classname';
import type { AllowedRoles } from '../CreateTeam/RoleDropdown';
-export function MemberRoleBadge({ role }: { role: AllowedRoles }) {
+type RoleBadgeProps = {
+ role: AllowedRoles;
+ className?: string;
+};
+export function MemberRoleBadge(props: RoleBadgeProps) {
+ const { role, className } = props;
+
return (
{role}
diff --git a/src/components/TeamMembers/TeamMemberItem.tsx b/src/components/TeamMembers/TeamMemberItem.tsx
index 1405ae3e56b2..3545dabfa52d 100644
--- a/src/components/TeamMembers/TeamMemberItem.tsx
+++ b/src/components/TeamMembers/TeamMemberItem.tsx
@@ -59,7 +59,12 @@ export function TeamMemberItem(props: TeamMemberProps) {
- {member.name}
+
+ {member.name}
+
{showNoProgressBadge && (
No Progress
@@ -109,4 +114,4 @@ export function TeamMemberItem(props: TeamMemberProps) {
);
-}
\ No newline at end of file
+}
diff --git a/src/components/TeamProgress/MemberProgressItem.tsx b/src/components/TeamProgress/MemberProgressItem.tsx
index b1f6e26ec15f..b8b5d058266a 100644
--- a/src/components/TeamProgress/MemberProgressItem.tsx
+++ b/src/components/TeamProgress/MemberProgressItem.tsx
@@ -5,12 +5,18 @@ type MemberProgressItemProps = {
member: TeamMember;
onShowResourceProgress: (
resourceId: string,
- isCustomResource: boolean
+ isCustomResource: boolean,
) => void;
isMyProgress?: boolean;
+ teamId: string;
};
export function MemberProgressItem(props: MemberProgressItemProps) {
- const { member, onShowResourceProgress, isMyProgress = false } = props;
+ const {
+ member,
+ onShowResourceProgress,
+ isMyProgress = false,
+ teamId,
+ } = props;
const memberProgress = member?.progress?.sort((a, b) => {
return b.done - a.done;
@@ -18,6 +24,8 @@ export function MemberProgressItem(props: MemberProgressItemProps) {
const [showAll, setShowAll] = useState(false);
+ const memberDetailsUrl = `/team/member?t=${teamId}&m=${member._id}`;
+
return (
<>
{!isMyProgress && (
-
{member.name}
+
+ {member.name}
+
)}
{isMyProgress && (
-
{member.name}
+
+ {member.name}
+
You
@@ -57,7 +69,7 @@ export function MemberProgressItem(props: MemberProgressItemProps) {
onClick={() =>
onShowResourceProgress(
progress.resourceId,
- progress.isCustomResource!
+ progress.isCustomResource!,
)
}
className="group relative overflow-hidden rounded-md border p-2 hover:border-gray-300 hover:text-black focus:outline-none"
@@ -81,7 +93,7 @@ export function MemberProgressItem(props: MemberProgressItemProps) {
/>
);
- }
+ },
)}
{memberProgress.length > 4 && !showAll && (
diff --git a/src/components/TeamProgress/TeamProgressPage.tsx b/src/components/TeamProgress/TeamProgressPage.tsx
index 1a5605474c53..6f386d55ecf7 100644
--- a/src/components/TeamProgress/TeamProgressPage.tsx
+++ b/src/components/TeamProgress/TeamProgressPage.tsx
@@ -227,6 +227,7 @@ export function TeamProgressPage() {
{
setShowMemberProgress({
diff --git a/src/pages/team/member.astro b/src/pages/team/member.astro
new file mode 100644
index 000000000000..43fd9dea71d8
--- /dev/null
+++ b/src/pages/team/member.astro
@@ -0,0 +1,15 @@
+---
+import { TeamSidebar } from '../../components/TeamSidebar';
+import AccountLayout from '../../layouts/AccountLayout.astro';
+import { TeamMemberDetailsPage } from '../../components/TeamMemberDetails/TeamMemberDetailsPage';
+---
+
+
+
+
+
+