feat: team member progress modal (#5651)

* feat: restrict members

* feat: member progress modal
pull/5675/head
Arik Chakma 6 months ago committed by GitHub
parent de89e56a47
commit 6804c6ec00
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 19
      src/components/Activity/ActivityStream.tsx
  2. 20
      src/components/Activity/ResourceProgress.tsx
  3. 20
      src/components/TeamActivity/TeamActivityItem.tsx
  4. 1
      src/components/TeamDropdown/TeamDropdown.tsx
  5. 41
      src/components/TeamMemberDetails/TeamMemberDetailsPage.tsx
  6. 22
      src/components/TeamMembers/TeamMemberItem.tsx
  7. 40
      src/components/TeamProgress/MemberProgressItem.tsx

@ -31,10 +31,15 @@ export type UserStreamActivity = {
type ActivityStreamProps = {
activities: UserStreamActivity[];
className?: string;
onResourceClick?: (
resourceId: string,
resourceType: ResourceType,
isCustomResource: boolean,
) => void;
};
export function ActivityStream(props: ActivityStreamProps) {
const { activities, className } = props;
const { activities, className, onResourceClick } = props;
const [showAll, setShowAll] = useState(false);
const [selectedActivity, setSelectedActivity] =
@ -92,7 +97,17 @@ export function ActivityStream(props: ActivityStreamProps) {
? `/r/${resourceSlug}`
: `/${resourceId}`;
const resourceLinkComponent = (
const resourceLinkComponent =
onResourceClick && resourceType !== 'question' ? (
<button
className="font-medium underline transition-colors hover:cursor-pointer hover:text-black"
onClick={() =>
onResourceClick(resourceId, resourceType, isCustomResource!)
}
>
{resourceTitle}
</button>
) : (
<a
className="font-medium underline transition-colors hover:cursor-pointer hover:text-black"
target="_blank"

@ -17,6 +17,7 @@ type ResourceProgressType = {
isCustomResource: boolean;
roadmapSlug?: string;
showActions?: boolean;
onResourceClick?: () => void;
};
export function ResourceProgress(props: ResourceProgressType) {
@ -24,6 +25,7 @@ export function ResourceProgress(props: ResourceProgressType) {
showClearButton = true,
isCustomResource,
showActions = true,
onResourceClick,
} = props;
const userId = getUser()?.id;
@ -53,13 +55,21 @@ export function ResourceProgress(props: ResourceProgressType) {
const totalMarked = doneCount + skippedCount;
const progressPercentage = getPercentage(totalMarked, totalCount);
const Slot = onResourceClick ? 'button' : 'a';
return (
<div className="relative">
<a
target="_blank"
href={url}
<Slot
{...(onResourceClick
? {
onClick: onResourceClick,
}
: {
href: url,
target: '_blank',
})}
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',
'group relative flex w-full 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' : '',
)}
>
@ -74,7 +84,7 @@ export function ResourceProgress(props: ResourceProgressType) {
width: `${progressPercentage}%`,
}}
></span>
</a>
</Slot>
{showActions && (
<div className="absolute right-2 top-0 flex h-full items-center">

@ -4,6 +4,8 @@ import type { TeamStreamActivity } from './TeamActivityPage';
import { ChevronsDown, ChevronsUp } from 'lucide-react';
import { ActivityTopicTitles } from '../Activity/ActivityTopicTitles';
import { cn } from '../../lib/classname';
import { useStore } from '@nanostores/react';
import { $currentTeam } from '../../stores/team';
type TeamActivityItemProps = {
onTopicClick?: (activity: TeamStreamActivity) => void;
@ -22,6 +24,7 @@ export function TeamActivityItem(props: TeamActivityItemProps) {
const { user, onTopicClick, teamId } = props;
const { activities } = user;
const currentTeam = useStore($currentTeam);
const [showAll, setShowAll] = useState(false);
const resourceLink = (activity: TeamStreamActivity) => {
@ -62,10 +65,25 @@ export function TeamActivityItem(props: TeamActivityItemProps) {
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${user.avatar}`
: '/images/default-avatar.png';
const isPersonalProgressOnly =
currentTeam?.personalProgressOnly &&
currentTeam.role === 'member' &&
user.memberId !== currentTeam.memberId;
const username = (
<a
href={`/team/member?t=${teamId}&m=${user?.memberId}`}
className="inline-flex items-center gap-1.5 underline underline-offset-2 hover:underline"
className={cn(
'inline-flex items-center gap-1.5 underline underline-offset-2 hover:underline',
isPersonalProgressOnly
? 'pointer-events-none cursor-default no-underline'
: '',
)}
onClick={(e) => {
if (isPersonalProgressOnly) {
e.preventDefault();
}
}}
aria-disabled={isPersonalProgressOnly}
>
<img
className="inline-block h-5 w-5 rounded-full"

@ -23,6 +23,7 @@ export type UserTeamItem = {
role: AllowedRoles;
status: AllowedMemberStatus;
memberId: string;
personalProgressOnly?: boolean;
};
export type TeamListResponse = UserTeamItem[];

@ -11,6 +11,10 @@ import { ActivityStream } from '../Activity/ActivityStream';
import { MemberRoleBadge } from '../TeamMembers/RoleBadge';
import { TeamMemberEmptyPage } from './TeamMemberEmptyPage';
import { Pagination } from '../Pagination/Pagination';
import type { ResourceType } from '../../lib/resource-progress';
import { MemberProgressModal } from '../TeamProgress/MemberProgressModal';
import { useStore } from '@nanostores/react';
import { $currentTeam } from '../../stores/team';
type GetTeamMemberProgressesResponse = TeamMemberDocument & {
name: string;
@ -31,6 +35,7 @@ export function TeamMemberDetailsPage() {
const { t: teamId, m: memberId } = getUrlParams() as { t: string; m: string };
const toast = useToast();
const currentTeam = useStore($currentTeam);
const [memberProgress, setMemberProgress] =
useState<GetTeamMemberProgressesResponse | null>(null);
@ -38,6 +43,12 @@ export function TeamMemberDetailsPage() {
useState<GetTeamMemberActivityResponse | null>(null);
const [currPage, setCurrPage] = useState(1);
const [selectedResource, setSelectedResource] = useState<{
resourceId: string;
resourceType: ResourceType;
isCustomResource?: boolean;
} | null>(null);
const loadMemberProgress = async () => {
const { response, error } = await httpGet<GetTeamMemberProgressesResponse>(
`${import.meta.env.PUBLIC_API_URL}/v1-get-team-member-progresses/${teamId}/${memberId}`,
@ -90,7 +101,25 @@ export function TeamMemberDetailsPage() {
return (
<>
<div className="flex items-center gap-3 mb-8">
{selectedResource && (
<MemberProgressModal
teamId={teamId}
member={{
...memberProgress,
_id: memberId,
updatedAt: new Date(memberProgress.updatedAt).toISOString(),
progress: memberProgress.progresses,
}}
resourceId={selectedResource.resourceId}
resourceType={selectedResource.resourceType}
isCustomResource={selectedResource.isCustomResource}
onClose={() => setSelectedResource(null)}
onShowMyProgress={() => {
window.location.href = `/team/member?t=${teamId}&m=${currentTeam?.memberId}`;
}}
/>
)}
<div className="mb-8 flex items-center gap-3">
<img
src={avatarUrl}
alt={memberProgress?.name}
@ -130,6 +159,13 @@ export function TeamMemberDetailsPage() {
title={progress.resourceTitle}
roadmapSlug={progress.roadmapSlug}
showActions={false}
onResourceClick={() => {
setSelectedResource({
resourceId: progress.resourceId,
resourceType: progress.resourceType,
isCustomResource: progress.isCustomResource,
});
}}
/>
);
})}
@ -146,6 +182,9 @@ export function TeamMemberDetailsPage() {
activities={
memberActivity?.data?.flatMap((act) => act.activity) || []
}
onResourceClick={(resourceId, resourceType, isCustomResource) => {
setSelectedResource({ resourceId, resourceType, isCustomResource });
}}
/>
<Pagination
currPage={currPage}

@ -2,8 +2,10 @@ import { MailIcon } from '../ReactIcons/MailIcon';
import { MemberActionDropdown } from './MemberActionDropdown';
import { MemberRoleBadge } from './RoleBadge';
import type { TeamMemberItem } from './TeamMembersPage';
import { $canManageCurrentTeam } from '../../stores/team';
import { $canManageCurrentTeam, $currentTeam } from '../../stores/team';
import { useStore } from '@nanostores/react';
import { useAuth } from '../../hooks/use-auth';
import { cn } from '../../lib/classname';
type TeamMemberProps = {
member: TeamMemberItem;
@ -29,6 +31,7 @@ export function TeamMemberItem(props: TeamMemberProps) {
onSendProgressReminder,
} = props;
const currentTeam = useStore($currentTeam);
const canManageTeam = useStore($canManageCurrentTeam);
const showNoProgressBadge = !member.hasProgress && member.status === 'joined';
const allowProgressReminder =
@ -36,6 +39,10 @@ export function TeamMemberItem(props: TeamMemberProps) {
!member.hasProgress &&
member.status === 'joined' &&
member.userId !== userId;
const isPersonalProgressOnly =
currentTeam?.personalProgressOnly &&
currentTeam.role === 'member' &&
String(member._id) !== currentTeam.memberId;
return (
<div
@ -61,7 +68,18 @@ export function TeamMemberItem(props: TeamMemberProps) {
<h3 className="inline-grid grid-cols-[auto_auto_auto] items-center font-medium">
<a
href={`/team/member?t=${member.teamId}&m=${member._id}`}
className="truncate"
className={cn(
'truncate',
isPersonalProgressOnly
? 'pointer-events-none cursor-default no-underline'
: '',
)}
onClick={(e) => {
if (isPersonalProgressOnly) {
e.preventDefault();
}
}}
aria-disabled={isPersonalProgressOnly}
>
{member.name}
</a>

@ -1,5 +1,8 @@
import { useStore } from '@nanostores/react';
import type { TeamMember } from './TeamProgressPage';
import { useState } from 'react';
import { $currentTeam } from '../../stores/team';
import { cn } from '../../lib/classname';
type MemberProgressItemProps = {
member: TeamMember;
@ -18,12 +21,17 @@ export function MemberProgressItem(props: MemberProgressItemProps) {
teamId,
} = props;
const currentTeam = useStore($currentTeam);
const memberProgress = member?.progress?.sort((a, b) => {
return b.done - a.done;
});
const [showAll, setShowAll] = useState(false);
const isPersonalProgressOnly =
currentTeam?.personalProgressOnly &&
currentTeam.role === 'member' &&
String(member._id) !== currentTeam.memberId;
const memberDetailsUrl = `/team/member?t=${teamId}&m=${member._id}`;
return (
@ -44,13 +52,41 @@ export function MemberProgressItem(props: MemberProgressItemProps) {
/>
<div className="inline-grid w-full">
{!isMyProgress && (
<a href={memberDetailsUrl} className="truncate font-medium">
<a
href={memberDetailsUrl}
className={cn(
'truncate font-medium',
isPersonalProgressOnly
? 'pointer-events-none cursor-default no-underline'
: '',
)}
onClick={(e) => {
if (isPersonalProgressOnly) {
e.preventDefault();
}
}}
aria-disabled={isPersonalProgressOnly}
>
{member.name}
</a>
)}
{isMyProgress && (
<div className="inline-grid grid-cols-[auto,32px] items-center gap-1.5">
<a href={memberDetailsUrl} className="truncate font-medium">
<a
href={memberDetailsUrl}
className={cn(
'truncate font-medium',
isPersonalProgressOnly
? 'pointer-events-none cursor-default no-underline'
: '',
)}
onClick={(e) => {
if (isPersonalProgressOnly) {
e.preventDefault();
}
}}
aria-disabled={isPersonalProgressOnly}
>
{member.name}
</a>
<span className="rounded-md bg-red-500 px-1 py-0.5 text-xs text-white">

Loading…
Cancel
Save