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/MemberProgressModal.tsx b/src/components/TeamProgress/MemberProgressModal.tsx index 88d7b3e67..181e136db 100644 --- a/src/components/TeamProgress/MemberProgressModal.tsx +++ b/src/components/TeamProgress/MemberProgressModal.tsx @@ -6,9 +6,19 @@ 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'; +import { InfoIcon } from '../ReactIcons/InfoIcon'; export type ProgressMapProps = { member: TeamMember; @@ -27,10 +37,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 +88,16 @@ export function MemberProgressModal(props: ProgressMapProps) { } useKeydown('Escape', () => { + if (showProgressHint) { + return; + } onClose(); }); useOutsideClick(popupBodyEl, () => { + if (showProgressHint) { + return; + } 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 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,38 +271,65 @@ export function MemberProgressModal(props: ProgressMapProps) { ref={popupBodyEl} class="popup-body relative rounded-lg bg-white shadow" > + {showProgressHint && ( + { + setShowProgressHint(false); + }} + /> + )}
- + ) : ( + -

+ You are looking at {member.name}'s progress.{' '} + + View your progress + + . +

+

+ View your progress  + + on the roadmap page. + +

+
+ )} +

{progressPercentage}% Done 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 af827c23d..424fa1d2b 100644 --- a/src/components/TeamProgress/TeamProgressPage.tsx +++ b/src/components/TeamProgress/TeamProgressPage.tsx @@ -88,10 +88,12 @@ export function TeamProgressPage() { return; } - getTeamProgress().finally(() => { - pageProgressMessage.set(''); - setIsLoading(false); - }); + getTeamProgress().then( + () => { + pageProgressMessage.set(''); + setIsLoading(false); + } + ); }, [teamId]); if (isLoading) { @@ -144,11 +146,10 @@ export function TeamProgressPage() {
{groupingTypes.map((grouping) => (