Updating personal progress from popup

pull/4259/head
Kamran Ahmed 1 year ago
parent b85639d876
commit f48a351c99
  1. 38
      src/components/TeamProgress/GroupRoadmapItem.tsx
  2. 22
      src/components/TeamProgress/MemberProgressItem.tsx
  3. 62
      src/components/TeamProgress/MemberProgressModal.tsx
  4. 3
      src/components/TeamProgress/ProgressHint.tsx
  5. 63
      src/components/TeamProgress/TeamProgressPage.tsx

@ -1,37 +1,25 @@
import { useState } from 'preact/hooks'; import { useState } from 'preact/hooks';
import type { GroupByRoadmap, TeamMember } from './TeamProgressPage'; import type { GroupByRoadmap, TeamMember } from './TeamProgressPage';
import { MemberProgressModal } from './MemberProgressModal';
import { getUrlParams } from '../../lib/browser'; import { getUrlParams } from '../../lib/browser';
import ExternalLinkIcon from '../../icons/external-link.svg'; import ExternalLinkIcon from '../../icons/external-link.svg';
import { useAuth } from '../../hooks/use-auth'; import { useAuth } from '../../hooks/use-auth';
type GroupRoadmapItemProps = { type GroupRoadmapItemProps = {
roadmap: GroupByRoadmap; roadmap: GroupByRoadmap;
onShowResourceProgress: (member: TeamMember, resourceId: string) => void;
}; };
export function GroupRoadmapItem(props: GroupRoadmapItemProps) { export function GroupRoadmapItem(props: GroupRoadmapItemProps) {
const { onShowResourceProgress } = props;
const { members, resourceTitle, resourceId } = props.roadmap; const { members, resourceTitle, resourceId } = props.roadmap;
const { t: teamId } = getUrlParams(); const { t: teamId } = getUrlParams();
const user = useAuth(); const user = useAuth();
const [showAll, setShowAll] = useState(false); const [showAll, setShowAll] = useState(false);
const [detailResourceId, setDetailResourceId] = useState<string | null>(null);
const [selectedMember, setSelectedMember] = useState<TeamMember | null>(null);
return ( return (
<> <>
{detailResourceId && (
<MemberProgressModal
member={selectedMember!}
teamId={teamId}
resourceId={detailResourceId}
resourceType={'roadmap'}
onClose={() => {
setDetailResourceId(null);
setSelectedMember(null);
}}
/>
)}
<div className="flex h-full min-h-[270px] flex-col rounded-md border"> <div className="flex h-full min-h-[270px] flex-col rounded-md border">
<div className="flex items-center gap-3 border-b p-3"> <div className="flex items-center gap-3 border-b p-3">
<div className="flex min-w-0 flex-grow items-center justify-between"> <div className="flex min-w-0 flex-grow items-center justify-between">
@ -59,11 +47,15 @@ export function GroupRoadmapItem(props: GroupRoadmapItemProps) {
return ( return (
<button <button
className={`group relative w-full overflow-hidden rounded-md border p-2 hover:border-gray-300 hover:text-black focus:outline-none ${isMyProgress ? 'border-green-500 hover:border-green-600' : ''}`} className={`group relative w-full overflow-hidden rounded-md border p-2 hover:border-gray-300 hover:text-black focus:outline-none ${
isMyProgress ? 'border-green-500 hover:border-green-600' : ''
}`}
key={member?.member._id} key={member?.member._id}
onClick={() => { onClick={() => {
setDetailResourceId(member?.progress?.resourceId!); onShowResourceProgress(
setSelectedMember(member.member); member.member,
member.progress?.resourceId!
);
}} }}
> >
<span className="relative z-10 flex items-center justify-between gap-1 text-sm"> <span className="relative z-10 flex items-center justify-between gap-1 text-sm">
@ -80,7 +72,9 @@ export function GroupRoadmapItem(props: GroupRoadmapItemProps) {
className="h-5 w-5 shrink-0 rounded-full" className="h-5 w-5 shrink-0 rounded-full"
/> />
<span className="inline-grid grid-cols-[auto,32px] items-center"> <span className="inline-grid grid-cols-[auto,32px] items-center">
<span className="truncate mr-[5px]">{member?.member?.name}</span> <span className="mr-[5px] truncate">
{member?.member?.name}
</span>
</span> </span>
</span> </span>
<span className="shrink-0 text-xs text-gray-400"> <span className="shrink-0 text-xs text-gray-400">
@ -88,7 +82,11 @@ export function GroupRoadmapItem(props: GroupRoadmapItemProps) {
</span> </span>
</span> </span>
<span <span
className={`absolute inset-0 ${isMyProgress ? 'bg-green-100 group-hover:bg-green-200' : 'bg-gray-100 group-hover:bg-gray-200'}`} className={`absolute inset-0 ${
isMyProgress
? 'bg-green-100 group-hover:bg-green-200'
: 'bg-gray-100 group-hover:bg-gray-200'
}`}
style={{ style={{
width: `${ width: `${
(member?.progress?.done / member?.progress?.total) * 100 (member?.progress?.done / member?.progress?.total) * 100

@ -1,36 +1,22 @@
import type { TeamMember } from './TeamProgressPage'; import type { TeamMember } from './TeamProgressPage';
import { useState } from 'preact/hooks'; import { useState } from 'preact/hooks';
import { MemberProgressModal } from './MemberProgressModal';
type MemberProgressItemProps = { type MemberProgressItemProps = {
teamId: string;
member: TeamMember; member: TeamMember;
onShowResourceProgress: (resourceId: string) => void;
isMyProgress?: boolean; isMyProgress?: boolean;
}; };
export function MemberProgressItem(props: MemberProgressItemProps) { export function MemberProgressItem(props: MemberProgressItemProps) {
const { member, teamId, isMyProgress = false } = props; const { member, onShowResourceProgress, isMyProgress = false } = props;
const memberProgress = member?.progress?.sort((a, b) => { const memberProgress = member?.progress?.sort((a, b) => {
return b.done - a.done; return b.done - a.done;
}); });
const [detailResourceId, setDetailResourceId] = useState<string | null>(null);
const [showAll, setShowAll] = useState(false); const [showAll, setShowAll] = useState(false);
return ( return (
<> <>
{detailResourceId && (
<MemberProgressModal
member={member}
teamId={teamId}
resourceId={detailResourceId}
resourceType={'roadmap'}
onClose={() => {
setDetailResourceId(null);
}}
/>
)}
<div <div
className={`flex h-full min-h-[270px] flex-col overflow-hidden rounded-md border`} className={`flex h-full min-h-[270px] flex-col overflow-hidden rounded-md border`}
key={member._id} key={member._id}
@ -52,7 +38,7 @@ export function MemberProgressItem(props: MemberProgressItemProps) {
{isMyProgress && ( {isMyProgress && (
<div className="inline-grid grid-cols-[auto,32px] items-center gap-1.5"> <div className="inline-grid grid-cols-[auto,32px] items-center gap-1.5">
<h3 className="truncate font-medium">{member.name}</h3> <h3 className="truncate font-medium">{member.name}</h3>
<span className="rounded-md bg-red-500 py-0.5 px-1 text-xs text-white"> <span className="rounded-md bg-red-500 px-1 py-0.5 text-xs text-white">
You You
</span> </span>
</div> </div>
@ -65,7 +51,7 @@ export function MemberProgressItem(props: MemberProgressItemProps) {
(progress) => { (progress) => {
return ( return (
<button <button
onClick={() => setDetailResourceId(progress.resourceId)} onClick={() => onShowResourceProgress(progress.resourceId)}
className="group relative overflow-hidden rounded-md border p-2 hover:border-gray-300 hover:text-black focus:outline-none" className="group relative overflow-hidden rounded-md border p-2 hover:border-gray-300 hover:text-black focus:outline-none"
key={progress.resourceId} key={progress.resourceId}
> >

@ -7,9 +7,9 @@ import { useKeydown } from '../../hooks/use-keydown';
import type { TeamMember } from './TeamProgressPage'; import type { TeamMember } from './TeamProgressPage';
import { httpGet } from '../../lib/http'; import { httpGet } from '../../lib/http';
import { import {
renderTopicProgress,
ResourceProgressType, ResourceProgressType,
ResourceType, ResourceType,
renderTopicProgress,
updateResourceProgress, updateResourceProgress,
} from '../../lib/resource-progress'; } from '../../lib/resource-progress';
import CloseIcon from '../../icons/close.svg'; import CloseIcon from '../../icons/close.svg';
@ -17,8 +17,6 @@ import { useToast } from '../../hooks/use-toast';
import { useAuth } from '../../hooks/use-auth'; import { useAuth } from '../../hooks/use-auth';
import { pageProgressMessage } from '../../stores/page'; import { pageProgressMessage } from '../../stores/page';
import { ProgressHint } from './ProgressHint'; import { ProgressHint } from './ProgressHint';
import QuestionIcon from '../../icons/question.svg';
import { InfoIcon } from '../ReactIcons/InfoIcon';
export type ProgressMapProps = { export type ProgressMapProps = {
member: TeamMember; member: TeamMember;
@ -26,6 +24,7 @@ export type ProgressMapProps = {
resourceId: string; resourceId: string;
resourceType: 'roadmap' | 'best-practice'; resourceType: 'roadmap' | 'best-practice';
onClose: () => void; onClose: () => void;
onShowMyProgress: () => void;
}; };
type MemberProgressResponse = { type MemberProgressResponse = {
@ -36,7 +35,14 @@ type MemberProgressResponse = {
}; };
export function MemberProgressModal(props: ProgressMapProps) { export function MemberProgressModal(props: ProgressMapProps) {
const { resourceId, member, resourceType, teamId, onClose } = props; const {
resourceId,
member,
resourceType,
onShowMyProgress,
teamId,
onClose,
} = props;
const user = useAuth(); const user = useAuth();
const isCurrentUser = user?.email === member.email; const isCurrentUser = user?.email === member.email;
@ -112,6 +118,7 @@ export function MemberProgressModal(props: ProgressMapProps) {
return; return;
} }
setIsLoading(true);
Promise.all([ Promise.all([
renderResource(resourceJsonUrl), renderResource(resourceJsonUrl),
getMemberProgress(teamId, member._id, resourceType, resourceId), getMemberProgress(teamId, member._id, resourceType, resourceId),
@ -136,7 +143,7 @@ export function MemberProgressModal(props: ProgressMapProps) {
.finally(() => { .finally(() => {
setIsLoading(false); setIsLoading(false);
}); });
}, []); }, [member]);
function updateTopicStatus(topicId: string, newStatus: ResourceProgressType) { function updateTopicStatus(topicId: string, newStatus: ResourceProgressType) {
if (!resourceId || !resourceType || !isCurrentUser) { if (!resourceId || !resourceType || !isCurrentUser) {
@ -176,18 +183,21 @@ export function MemberProgressModal(props: ProgressMapProps) {
if (!targetGroup) { if (!targetGroup) {
return; return;
} }
const groupId = targetGroup.dataset ? targetGroup.dataset.groupId : ''; const groupId = targetGroup.dataset ? targetGroup.dataset.groupId : '';
if (!groupId) { if (!groupId) {
return; return;
} }
if (targetGroup.classList.contains('removed')) { if (targetGroup.classList.contains('removed')) {
e.preventDefault();
return; return;
} }
e.preventDefault(); e.preventDefault();
const isCurrentStatusDone = targetGroup.classList.contains('done'); const isCurrentStatusDone = targetGroup.classList.contains('done');
const normalizedGroupId = groupId.replace(/^\d+-/, ''); const normalizedGroupId = groupId.replace(/^\d+-/, '');
updateTopicStatus( updateTopicStatus(
normalizedGroupId, normalizedGroupId,
!isCurrentStatusDone ? 'done' : 'pending' !isCurrentStatusDone ? 'done' : 'pending'
@ -235,7 +245,7 @@ export function MemberProgressModal(props: ProgressMapProps) {
} }
useEffect(() => { useEffect(() => {
if (!isCurrentUser || !containerEl.current) { if (!member || !containerEl.current) {
return; return;
} }
@ -246,7 +256,7 @@ export function MemberProgressModal(props: ProgressMapProps) {
containerEl.current?.removeEventListener('contextmenu', handleRightClick); containerEl.current?.removeEventListener('contextmenu', handleRightClick);
containerEl.current?.removeEventListener('click', handleClick); containerEl.current?.removeEventListener('click', handleClick);
}; };
}, []); }, [member]);
const removedTopics = memberProgress?.removed || []; const removedTopics = memberProgress?.removed || [];
const memberDone = const memberDone =
@ -282,19 +292,18 @@ export function MemberProgressModal(props: ProgressMapProps) {
{isCurrentUser ? ( {isCurrentUser ? (
<div className="mb-5 mt-0 text-left md:mt-4 md:text-center"> <div className="mb-5 mt-0 text-left md:mt-4 md:text-center">
<h2 className={'mb-1 text-lg font-bold md:text-2xl'}> <h2 className={'mb-1 text-lg font-bold md:text-2xl'}>
Your Progress Update Your Progress
</h2> </h2>
<p className={'text-gray-500'}> <p className={'text-gray-500'}>
You can{' '} Follow the{' '}
<button <button
className="inline-flex items-center text-blue-600 underline" className="inline-flex items-center text-blue-600 underline"
onClick={() => { onClick={() => {
setShowProgressHint(true); setShowProgressHint(true);
}} }}
> >
follow these instructions instructions to update your progress
</button>{' '} </button>{' '}
to update your progress below.
</p> </p>
</div> </div>
) : ( ) : (
@ -308,28 +317,29 @@ export function MemberProgressModal(props: ProgressMapProps) {
} }
> >
You are looking at {member.name}'s progress.{' '} You are looking at {member.name}'s progress.{' '}
<a <button
target={'_blank'}
href={`/${resourceId}?t=${teamId}`}
className="text-blue-600 underline" className="text-blue-600 underline"
onClick={onShowMyProgress}
> >
View your progress View your progress
</a> </button>
. .
</p> </p>
<p className={'block text-gray-500 md:hidden'}> <p className={'block text-gray-500 md:hidden'}>
View your progress&nbsp; <button
<a
target={'_blank'}
href={`/${resourceId}?t=${teamId}`}
className="text-blue-600 underline" className="text-blue-600 underline"
onClick={onShowMyProgress}
> >
on the roadmap page. View your progress.
</a> </button>
</p> </p>
</div> </div>
)} )}
<p class="-mx-4 mb-3 flex items-center justify-start border-b border-t px-4 py-2 text-sm sm:hidden"> <p
class={`-mx-4 mb-3 flex items-center justify-start border-b border-t px-4 py-2 text-sm sm:hidden ${
isLoading ? 'striped-loader' : ''
}`}
>
<span class="mr-2.5 block rounded-sm bg-yellow-200 px-1 py-0.5 text-xs font-medium uppercase text-yellow-900"> <span class="mr-2.5 block rounded-sm bg-yellow-200 px-1 py-0.5 text-xs font-medium uppercase text-yellow-900">
<span>{progressPercentage}</span>% Done <span>{progressPercentage}</span>% Done
</span> </span>
@ -338,7 +348,11 @@ export function MemberProgressModal(props: ProgressMapProps) {
<span>{memberDone}</span> of <span>{memberTotal}</span> done <span>{memberDone}</span> of <span>{memberTotal}</span> done
</span> </span>
</p> </p>
<p class="-mx-4 mb-3 hidden items-center justify-center border-b border-t py-2 text-sm sm:flex"> <p
class={`-mx-4 mb-3 hidden items-center justify-center border-b border-t py-2 text-sm sm:flex ${
isLoading ? 'striped-loader' : ''
}`}
>
<span class="mr-2.5 block rounded-sm bg-yellow-200 px-1 py-0.5 text-xs font-medium uppercase text-yellow-900"> <span class="mr-2.5 block rounded-sm bg-yellow-200 px-1 py-0.5 text-xs font-medium uppercase text-yellow-900">
<span>{progressPercentage}</span>% Done <span>{progressPercentage}</span>% Done
</span> </span>
@ -385,7 +399,7 @@ export function MemberProgressModal(props: ProgressMapProps) {
className="popup-close absolute right-2.5 top-3 ml-auto inline-flex items-center rounded-lg bg-transparent p-1.5 text-sm text-gray-400 hover:bg-gray-200 hover:text-gray-900 sm:hidden" className="popup-close absolute right-2.5 top-3 ml-auto inline-flex items-center rounded-lg bg-transparent p-1.5 text-sm text-gray-400 hover:bg-gray-200 hover:text-gray-900 sm:hidden"
onClick={onClose} onClick={onClose}
> >
<img src={CloseIcon} className="h-4 w-4" /> <img alt={'close'} src={CloseIcon} className="h-4 w-4" />
<span class="sr-only">Close modal</span> <span class="sr-only">Close modal</span>
</button> </button>
</div> </div>

@ -15,11 +15,12 @@ export function ProgressHint(props: ProgressHintProps) {
useKeydown('Escape', () => { useKeydown('Escape', () => {
onClose(); onClose();
}); });
return ( return (
<div className="fixed left-0 right-0 top-0 z-50 h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50"> <div className="fixed left-0 right-0 top-0 z-50 h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50">
<div className="relative flex h-full w-full items-center justify-center"> <div className="relative flex h-full w-full items-center justify-center">
<div <div
className="relative w-full max-w-lg rounded-md border border-yellow-300 bg-yellow-50 px-3 py-3 text-gray-500" className="relative w-full max-w-lg rounded-md bg-yellow-50 px-3 py-3 text-gray-500"
ref={containerEl} ref={containerEl}
> >
<span className="mb-1.5 block text-xs font-medium uppercase text-green-600"> <span className="mb-1.5 block text-xs font-medium uppercase text-green-600">

@ -9,6 +9,7 @@ import { $currentTeam } from '../../stores/team';
import { GroupRoadmapItem } from './GroupRoadmapItem'; import { GroupRoadmapItem } from './GroupRoadmapItem';
import { getUrlParams, setUrlParams } from '../../lib/browser'; import { getUrlParams, setUrlParams } from '../../lib/browser';
import { useAuth } from '../../hooks/use-auth'; import { useAuth } from '../../hooks/use-auth';
import { MemberProgressModal } from './MemberProgressModal';
export type UserProgress = { export type UserProgress = {
resourceTitle: string; resourceTitle: string;
@ -56,6 +57,11 @@ export function TeamProgressPage() {
const currentTeam = useStore($currentTeam); const currentTeam = useStore($currentTeam);
const user = useAuth(); const user = useAuth();
const [showMemberProgress, setShowMemberProgress] = useState<{
resourceId: string;
member: TeamMember;
}>();
const [teamMembers, setTeamMembers] = useState<TeamMember[]>([]); const [teamMembers, setTeamMembers] = useState<TeamMember[]>([]);
const [selectedGrouping, setSelectedGrouping] = useState< const [selectedGrouping, setSelectedGrouping] = useState<
'roadmap' | 'member' 'roadmap' | 'member'
@ -88,12 +94,10 @@ export function TeamProgressPage() {
return; return;
} }
getTeamProgress().then( getTeamProgress().then(() => {
() => { pageProgressMessage.set('');
pageProgressMessage.set(''); setIsLoading(false);
setIsLoading(false); });
}
);
}, [teamId]); }, [teamId]);
if (isLoading) { if (isLoading) {
@ -143,13 +147,34 @@ export function TeamProgressPage() {
return ( return (
<div> <div>
{showMemberProgress && (
<MemberProgressModal
member={showMemberProgress.member}
teamId={teamId}
resourceId={showMemberProgress.resourceId}
resourceType={'roadmap'}
onClose={() => {
setShowMemberProgress(undefined);
}}
onShowMyProgress={() => {
setShowMemberProgress({
resourceId: showMemberProgress.resourceId,
member: teamMembers.find(
(member) => member.email === user?.email
)!,
});
}}
/>
)}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{groupingTypes.map((grouping) => ( {groupingTypes.map((grouping) => (
<button <button
className={`rounded-md border p-1 px-2 text-sm ${selectedGrouping === grouping.value className={`rounded-md border p-1 px-2 text-sm ${
? ' border-gray-400 bg-gray-200 ' selectedGrouping === grouping.value
: '' ? ' border-gray-400 bg-gray-200 '
}`} : ''
}`}
onClick={() => setSelectedGrouping(grouping.value)} onClick={() => setSelectedGrouping(grouping.value)}
> >
{grouping.label} {grouping.label}
@ -162,7 +187,16 @@ export function TeamProgressPage() {
<div className="grid gap-4 sm:grid-cols-2"> <div className="grid gap-4 sm:grid-cols-2">
{groupByRoadmap.map((roadmap) => { {groupByRoadmap.map((roadmap) => {
return ( return (
<GroupRoadmapItem key={roadmap.resourceId} roadmap={roadmap} /> <GroupRoadmapItem
key={roadmap.resourceId}
roadmap={roadmap}
onShowResourceProgress={(member, resourceId) => {
setShowMemberProgress({
resourceId,
member,
});
}}
/>
); );
})} })}
</div> </div>
@ -171,9 +205,14 @@ export function TeamProgressPage() {
<div className="grid gap-4 sm:grid-cols-2"> <div className="grid gap-4 sm:grid-cols-2">
{teamMembers.map((member) => ( {teamMembers.map((member) => (
<MemberProgressItem <MemberProgressItem
teamId={teamId}
member={member} member={member}
isMyProgress={member?.email === user?.email} isMyProgress={member?.email === user?.email}
onShowResourceProgress={(resourceId) => {
setShowMemberProgress({
resourceId,
member,
});
}}
/> />
))} ))}
</div> </div>

Loading…
Cancel
Save