From eefce5c6a5cf155ccb0b6a283e92bad0eee750d5 Mon Sep 17 00:00:00 2001 From: Arik Chakma Date: Tue, 25 Jul 2023 21:27:18 +0600 Subject: [PATCH] chore: add update progress in modal --- src/components/ReactIcons/CloseIcon.tsx | 22 ++ src/components/RoadmapHint.astro | 1 - .../TeamProgress/MemberProgressItem.tsx | 6 +- .../TeamProgress/MemberProgressModal.tsx | 234 ++++++++++++++---- src/components/TeamProgress/ProgressHint.tsx | 70 ++++++ .../TeamProgress/TeamProgressPage.tsx | 20 +- 6 files changed, 298 insertions(+), 55 deletions(-) create mode 100644 src/components/ReactIcons/CloseIcon.tsx create mode 100644 src/components/TeamProgress/ProgressHint.tsx diff --git a/src/components/ReactIcons/CloseIcon.tsx b/src/components/ReactIcons/CloseIcon.tsx new file mode 100644 index 000000000..0aa35759b --- /dev/null +++ b/src/components/ReactIcons/CloseIcon.tsx @@ -0,0 +1,22 @@ +type CloseIconProps = { + className?: string; +}; + +export function CloseIcon(props: CloseIconProps) { + const { className } = props; + + return ( + + + + ); +} diff --git a/src/components/RoadmapHint.astro b/src/components/RoadmapHint.astro index 0334705fd..6eee26ef3 100644 --- a/src/components/RoadmapHint.astro +++ b/src/components/RoadmapHint.astro @@ -1,5 +1,4 @@ --- -import { ClearProgress } from './Activity/ClearProgress'; import AstroIcon from './AstroIcon.astro'; import Icon from './AstroIcon.astro'; import ResourceProgressStats from './ResourceProgressStats.astro'; diff --git a/src/components/TeamProgress/MemberProgressItem.tsx b/src/components/TeamProgress/MemberProgressItem.tsx index 99d5a1c53..3971870d9 100644 --- a/src/components/TeamProgress/MemberProgressItem.tsx +++ b/src/components/TeamProgress/MemberProgressItem.tsx @@ -60,9 +60,11 @@ export function MemberProgressItem(props: MemberProgressItemProps) { > - {progress.resourceTitle} + + {progress.resourceTitle} + - + {progress.done} / {progress.total} diff --git a/src/components/TeamProgress/MemberProgressModal.tsx b/src/components/TeamProgress/MemberProgressModal.tsx index 88d7b3e67..36ffd71b8 100644 --- a/src/components/TeamProgress/MemberProgressModal.tsx +++ b/src/components/TeamProgress/MemberProgressModal.tsx @@ -6,9 +6,18 @@ import { useOutsideClick } from '../../hooks/use-outside-click'; import { useKeydown } from '../../hooks/use-keydown'; import type { TeamMember } from './TeamProgressPage'; 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 { 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'; export type ProgressMapProps = { member: TeamMember; @@ -27,10 +36,13 @@ type MemberProgressResponse = { export function MemberProgressModal(props: ProgressMapProps) { const { resourceId, member, resourceType, teamId, onClose } = props; + const user = useAuth(); + const isCurrentUser = user?.email === member.email; const containerEl = useRef(null); const popupBodyEl = useRef(null); + const [showProgressHint, setShowProgressHint] = useState(false); const [memberProgress, setMemberProgress] = useState(); const [isLoading, setIsLoading] = useState(true); @@ -75,10 +87,16 @@ export function MemberProgressModal(props: ProgressMapProps) { } useKeydown('Escape', () => { + if (showProgressHint) { + return; + } onClose(); }); useOutsideClick(popupBodyEl, () => { + if (showProgressHint) { + return; + } onClose(); }); @@ -119,10 +137,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 memberDone = currProgress?.done || 0; - const memberLearning = currProgress?.learning || 0; - const memberSkipped = currProgress?.skipped || 0; const memberTotal = currProgress?.total || 0; const progressPercentage = Math.round((memberDone / memberTotal) * 100); @@ -134,6 +270,13 @@ export function MemberProgressModal(props: ProgressMapProps) { ref={popupBodyEl} class="popup-body relative rounded-lg bg-white shadow" > + {showProgressHint && ( + { + setShowProgressHint(false); + }} + /> + )}

@@ -165,44 +308,51 @@ export function MemberProgressModal(props: ProgressMapProps) {

-

- - {progressPercentage}% Done - - - - {memberDone} of {memberTotal} done - -

- +
+
+ + {memberDone} of {memberTotal} done + +
+
+ + {progressPercentage}% Done + + + + {memberDone} completed + + · + + {memberLearning} in + progress + + + {memberSkipped > 0 && ( + <> + · + + {memberSkipped}{' '} + skipped + + + )} + + · + + {memberTotal} Total + +
+ +
diff --git a/src/components/TeamProgress/ProgressHint.tsx b/src/components/TeamProgress/ProgressHint.tsx new file mode 100644 index 000000000..ade88dc57 --- /dev/null +++ b/src/components/TeamProgress/ProgressHint.tsx @@ -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(null); + + useOutsideClick(containerEl, onClose); + useKeydown('Escape', () => { + onClose(); + }); + return ( +
+
+
+ + Update Progress + +

Use the keyboard shortcuts listed below.

+ +
    +
  • + + Right Mouse Click + {' '} + to mark as Done. +
  • +
  • + + Shift + {' '} + +{' '} + + Click + {' '} + to mark as in progress. +
  • +
  • + + Option / Alt + {' '} + +{' '} + + Click + {' '} + to mark as skipped. +
  • +
+ +
+
+
+ ); +} diff --git a/src/components/TeamProgress/TeamProgressPage.tsx b/src/components/TeamProgress/TeamProgressPage.tsx index 51188d101..4c86c77ed 100644 --- a/src/components/TeamProgress/TeamProgressPage.tsx +++ b/src/components/TeamProgress/TeamProgressPage.tsx @@ -9,7 +9,6 @@ import { $currentTeam } from '../../stores/team'; import { GroupRoadmapItem } from './GroupRoadmapItem'; import { setUrlParams } from '../../lib/browser'; import { getUrlParams } from '../../lib/browser'; -import { $toastMessage } from '../../stores/toast'; export type UserProgress = { resourceTitle: string; @@ -78,10 +77,12 @@ export function TeamProgressPage() { return; } - getTeamProgress().finally(() => { - pageProgressMessage.set(''); - setIsLoading(false); - }); + getTeamProgress().then( + () => { + pageProgressMessage.set(''); + setIsLoading(false); + } + ); }, [teamId]); if (isLoading) { @@ -134,11 +135,10 @@ export function TeamProgressPage() {
{groupingTypes.map((grouping) => (