Add update progress functionality in modal (#4256)

* chore: add update progress in modal

* chore: show tracking for current user

* chore: current user header

---------

Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
pull/4259/head
Arik Chakma 1 year ago committed by GitHub
parent 14f9ad9530
commit b85639d876
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 22
      src/components/ReactIcons/CloseIcon.tsx
  2. 1
      src/components/RoadmapHint.astro
  3. 232
      src/components/TeamProgress/MemberProgressModal.tsx
  4. 70
      src/components/TeamProgress/ProgressHint.tsx
  5. 19
      src/components/TeamProgress/TeamProgressPage.tsx

@ -0,0 +1,22 @@
type CloseIconProps = {
className?: string;
};
export function CloseIcon(props: CloseIconProps) {
const { className } = props;
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
className={className}
>
<path d="M18 6 6 18" /><path d="m6 6 12 12" />
</svg>
);
}

@ -1,5 +1,4 @@
--- ---
import { ClearProgress } from './Activity/ClearProgress';
import AstroIcon from './AstroIcon.astro'; import AstroIcon from './AstroIcon.astro';
import Icon from './AstroIcon.astro'; import Icon from './AstroIcon.astro';
import ResourceProgressStats from './ResourceProgressStats.astro'; import ResourceProgressStats from './ResourceProgressStats.astro';

@ -6,9 +6,19 @@ import { useOutsideClick } from '../../hooks/use-outside-click';
import { useKeydown } from '../../hooks/use-keydown'; 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 { renderTopicProgress } from '../../lib/resource-progress'; import {
ResourceProgressType,
ResourceType,
renderTopicProgress,
updateResourceProgress,
} from '../../lib/resource-progress';
import CloseIcon from '../../icons/close.svg'; import CloseIcon from '../../icons/close.svg';
import { useToast } from '../../hooks/use-toast'; import { useToast } from '../../hooks/use-toast';
import { useAuth } from '../../hooks/use-auth';
import { pageProgressMessage } from '../../stores/page';
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;
@ -27,10 +37,13 @@ type MemberProgressResponse = {
export function MemberProgressModal(props: ProgressMapProps) { export function MemberProgressModal(props: ProgressMapProps) {
const { resourceId, member, resourceType, teamId, onClose } = props; const { resourceId, member, resourceType, teamId, onClose } = props;
const user = useAuth();
const isCurrentUser = user?.email === member.email;
const containerEl = useRef<HTMLDivElement>(null); const containerEl = useRef<HTMLDivElement>(null);
const popupBodyEl = useRef<HTMLDivElement>(null); const popupBodyEl = useRef<HTMLDivElement>(null);
const [showProgressHint, setShowProgressHint] = useState(false);
const [memberProgress, setMemberProgress] = const [memberProgress, setMemberProgress] =
useState<MemberProgressResponse>(); useState<MemberProgressResponse>();
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@ -75,10 +88,16 @@ export function MemberProgressModal(props: ProgressMapProps) {
} }
useKeydown('Escape', () => { useKeydown('Escape', () => {
if (showProgressHint) {
return;
}
onClose(); onClose();
}); });
useOutsideClick(popupBodyEl, () => { useOutsideClick(popupBodyEl, () => {
if (showProgressHint) {
return;
}
onClose(); onClose();
}); });
@ -119,10 +138,128 @@ export function MemberProgressModal(props: ProgressMapProps) {
}); });
}, []); }, []);
function updateTopicStatus(topicId: string, newStatus: ResourceProgressType) {
if (!resourceId || !resourceType || !isCurrentUser) {
return;
}
pageProgressMessage.set('Updating progress');
updateResourceProgress(
{
resourceId: resourceId,
resourceType: resourceType as ResourceType,
topicId,
},
newStatus
)
.then(() => {
renderTopicProgress(topicId, newStatus);
getMemberProgress(teamId, member._id, resourceType, resourceId).then(
(data) => {
setMemberProgress(data);
}
);
})
.catch((err) => {
alert('Something went wrong, please try again.');
console.error(err);
})
.finally(() => {
pageProgressMessage.set('');
});
return;
}
async function handleRightClick(e: MouseEvent) {
const targetGroup = (e.target as HTMLElement)?.closest('g');
if (!targetGroup) {
return;
}
const groupId = targetGroup.dataset ? targetGroup.dataset.groupId : '';
if (!groupId) {
return;
}
if (targetGroup.classList.contains('removed')) {
return;
}
e.preventDefault();
const isCurrentStatusDone = targetGroup.classList.contains('done');
const normalizedGroupId = groupId.replace(/^\d+-/, '');
updateTopicStatus(
normalizedGroupId,
!isCurrentStatusDone ? 'done' : 'pending'
);
}
async function handleClick(e: MouseEvent) {
const targetGroup = (e.target as HTMLElement)?.closest('g');
if (!targetGroup) {
return;
}
const groupId = targetGroup.dataset ? targetGroup.dataset.groupId : '';
if (!groupId) {
return;
}
if (targetGroup.classList.contains('removed')) {
return;
}
e.preventDefault();
const normalizedGroupId = groupId.replace(/^\d+-/, '');
const isCurrentStatusLearning = targetGroup.classList.contains('learning');
const isCurrentStatusSkipped = targetGroup.classList.contains('skipped');
if (e.shiftKey) {
e.preventDefault();
updateTopicStatus(
normalizedGroupId,
!isCurrentStatusLearning ? 'learning' : 'pending'
);
return;
}
if (e.altKey) {
e.preventDefault();
updateTopicStatus(
normalizedGroupId,
!isCurrentStatusSkipped ? 'skipped' : 'pending'
);
return;
}
}
useEffect(() => {
if (!isCurrentUser || !containerEl.current) {
return;
}
containerEl.current?.addEventListener('contextmenu', handleRightClick);
containerEl.current?.addEventListener('click', handleClick);
return () => {
containerEl.current?.removeEventListener('contextmenu', handleRightClick);
containerEl.current?.removeEventListener('click', handleClick);
};
}, []);
const removedTopics = memberProgress?.removed || [];
const memberDone =
memberProgress?.done.filter((id) => !removedTopics.includes(id)).length ||
0;
const memberLearning =
memberProgress?.learning.filter((id) => !removedTopics.includes(id))
.length || 0;
const memberSkipped =
memberProgress?.skipped.filter((id) => !removedTopics.includes(id))
.length || 0;
const currProgress = member.progress.find((p) => p.resourceId === resourceId); const currProgress = member.progress.find((p) => p.resourceId === resourceId);
const memberDone = currProgress?.done || 0;
const memberLearning = currProgress?.learning || 0;
const memberSkipped = currProgress?.skipped || 0;
const memberTotal = currProgress?.total || 0; const memberTotal = currProgress?.total || 0;
const progressPercentage = Math.round((memberDone / memberTotal) * 100); const progressPercentage = Math.round((memberDone / memberTotal) * 100);
@ -134,38 +271,65 @@ export function MemberProgressModal(props: ProgressMapProps) {
ref={popupBodyEl} ref={popupBodyEl}
class="popup-body relative rounded-lg bg-white shadow" class="popup-body relative rounded-lg bg-white shadow"
> >
{showProgressHint && (
<ProgressHint
onClose={() => {
setShowProgressHint(false);
}}
/>
)}
<div className="p-4"> <div className="p-4">
<div className="mb-5 mt-0 text-left md:mt-4 md:text-center"> {isCurrentUser ? (
<h2 className={'mb-1 text-lg font-bold md:text-2xl'}> <div className="mb-5 mt-0 text-left md:mt-4 md:text-center">
{member.name}'s Progress <h2 className={'mb-1 text-lg font-bold md:text-2xl'}>
</h2> Your Progress
<p </h2>
className={ <p className={'text-gray-500'}>
'hidden text-xs text-gray-500 sm:text-sm md:block md:text-base' You can{' '}
} <button
> className="inline-flex items-center text-blue-600 underline"
You are looking at {member.name}'s progress.{' '} onClick={() => {
<a setShowProgressHint(true);
target={'_blank'} }}
href={`/${resourceId}?t=${teamId}`} >
className="text-blue-600 underline" follow these instructions
> </button>{' '}
View your progress to update your progress below.
</a> </p>
. </div>
</p> ) : (
<p className={'block text-gray-500 md:hidden'}> <div className="mb-5 mt-0 text-left md:mt-4 md:text-center">
View your progress&nbsp; <h2 className={'mb-1 text-lg font-bold md:text-2xl'}>
<a {member.name}'s Progress
target={'_blank'} </h2>
href={`/${resourceId}?t=${teamId}`} <p
className="text-blue-600 underline" className={
'hidden text-xs text-gray-500 sm:text-sm md:block md:text-base'
}
> >
on the roadmap page. You are looking at {member.name}'s progress.{' '}
</a> <a
</p> target={'_blank'}
</div> href={`/${resourceId}?t=${teamId}`}
<p class="-mx-4 mb-3 flex items-center justify-start border-b border-t py-2 text-sm sm:hidden px-4"> className="text-blue-600 underline"
>
View your progress
</a>
.
</p>
<p className={'block text-gray-500 md:hidden'}>
View your progress&nbsp;
<a
target={'_blank'}
href={`/${resourceId}?t=${teamId}`}
className="text-blue-600 underline"
>
on the roadmap page.
</a>
</p>
</div>
)}
<p class="-mx-4 mb-3 flex items-center justify-start border-b border-t px-4 py-2 text-sm sm:hidden">
<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>

@ -0,0 +1,70 @@
import { useRef } from 'preact/hooks';
import { useOutsideClick } from '../../hooks/use-outside-click';
import { useKeydown } from '../../hooks/use-keydown';
import { CloseIcon } from '../ReactIcons/CloseIcon';
type ProgressHintProps = {
onClose: () => void;
};
export function ProgressHint(props: ProgressHintProps) {
const { onClose } = props;
const containerEl = useRef<HTMLDivElement>(null);
useOutsideClick(containerEl, onClose);
useKeydown('Escape', () => {
onClose();
});
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="relative flex h-full w-full items-center justify-center">
<div
className="relative w-full max-w-lg rounded-md border border-yellow-300 bg-yellow-50 px-3 py-3 text-gray-500"
ref={containerEl}
>
<span className="mb-1.5 block text-xs font-medium uppercase text-green-600">
Update Progress
</span>
<p className="text-sm">Use the keyboard shortcuts listed below.</p>
<ul className="mb-1.5 mt-3 flex flex-col gap-1">
<li className="text-sm leading-loose">
<kbd className="rounded-md bg-gray-900 px-2 py-1.5 text-xs text-white">
Right Mouse Click
</kbd>{' '}
to mark as Done.
</li>
<li className="text-sm leading-loose">
<kbd className="rounded-md bg-gray-900 px-2 py-1.5 text-xs text-white">
Shift
</kbd>{' '}
+{' '}
<kbd className="rounded-md bg-gray-900 px-2 py-1.5 text-xs text-white">
Click
</kbd>{' '}
to mark as in progress.
</li>
<li className="text-sm leading-loose">
<kbd className="rounded-md bg-gray-900 px-2 py-1.5 text-xs text-white">
Option / Alt
</kbd>{' '}
+{' '}
<kbd className="rounded-md bg-gray-900 px-2 py-1.5 text-xs text-white">
Click
</kbd>{' '}
to mark as skipped.
</li>
</ul>
<button
type="button"
className="absolute right-1.5 top-1.5 ml-auto inline-flex items-center rounded-lg bg-transparent p-1.5 text-sm text-gray-400 hover:bg-yellow-200 hover:text-yellow-900"
onClick={onClose}
>
<CloseIcon />
<span class="sr-only">Close modal</span>
</button>
</div>
</div>
</div>
);
}

@ -88,10 +88,12 @@ export function TeamProgressPage() {
return; return;
} }
getTeamProgress().finally(() => { getTeamProgress().then(
pageProgressMessage.set(''); () => {
setIsLoading(false); pageProgressMessage.set('');
}); setIsLoading(false);
}
);
}, [teamId]); }, [teamId]);
if (isLoading) { if (isLoading) {
@ -144,11 +146,10 @@ export function TeamProgressPage() {
<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 ${ className={`rounded-md border p-1 px-2 text-sm ${selectedGrouping === grouping.value
selectedGrouping === grouping.value ? ' border-gray-400 bg-gray-200 '
? ' border-gray-400 bg-gray-200 ' : ''
: '' }`}
}`}
onClick={() => setSelectedGrouping(grouping.value)} onClick={() => setSelectedGrouping(grouping.value)}
> >
{grouping.label} {grouping.label}

Loading…
Cancel
Save