diff --git a/src/components/Activity/ActivityStream.tsx b/src/components/Activity/ActivityStream.tsx index 9cdae7ef4..f262093a7 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 05953c842..4bca95a05 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 6e40e30ac..5419c78a4 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} - {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 29df4e3f0..f2200572c 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 000000000..3f81831bd --- /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?.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 000000000..b3537c34d --- /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 18612b5cc..f42c81e79 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 1405ae3e5..3545dabfa 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 b1f6e26ec..b8b5d0582 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 1a5605474..6f386d55e 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 000000000..43fd9dea7 --- /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'; +--- + + + + + +