chore: add update progress in modal

chore/update-progress
Arik Chakma 1 year ago
parent 29d91be094
commit eefce5c6a5
  1. 22
      src/components/ReactIcons/CloseIcon.tsx
  2. 1
      src/components/RoadmapHint.astro
  3. 6
      src/components/TeamProgress/MemberProgressItem.tsx
  4. 174
      src/components/TeamProgress/MemberProgressModal.tsx
  5. 70
      src/components/TeamProgress/ProgressHint.tsx
  6. 10
      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';

@ -60,9 +60,11 @@ export function MemberProgressItem(props: MemberProgressItemProps) {
> >
<span className="relative z-10 flex items-center justify-between text-sm"> <span className="relative z-10 flex items-center justify-between text-sm">
<span className="inline-grid"> <span className="inline-grid">
<span className={'truncate'}>{progress.resourceTitle}</span> <span className={'truncate'}>
{progress.resourceTitle}
</span> </span>
<span className="text-xs text-gray-400 shrink-0 ml-1.5"> </span>
<span className="ml-1.5 shrink-0 text-xs text-gray-400">
{progress.done} / {progress.total} {progress.done} / {progress.total}
</span> </span>
</span> </span>

@ -6,9 +6,18 @@ 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';
export type ProgressMapProps = { export type ProgressMapProps = {
member: TeamMember; member: TeamMember;
@ -27,10 +36,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 +87,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 +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 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,6 +270,13 @@ 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"> <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'}>
@ -165,16 +308,13 @@ export function MemberProgressModal(props: ProgressMapProps) {
</a> </a>
</p> </p>
</div> </div>
<p class="-mx-4 mb-3 flex items-center justify-start border-b border-t py-2 text-sm sm:hidden px-4"> <div class="-mx-4 mb-3 flex items-center justify-between border-b border-t py-2 text-sm">
<span class="mr-2.5 block rounded-sm bg-yellow-200 px-1 py-0.5 text-xs font-medium uppercase text-yellow-900"> <div className="flex items-center pl-4 sm:hidden">
<span>{progressPercentage}</span>% Done
</span>
<span> <span>
<span>{memberDone}</span> of <span>{memberTotal}</span> done <span>{memberDone}</span> of <span>{memberTotal}</span> done
</span> </span>
</p> </div>
<p class="-mx-4 mb-3 hidden items-center justify-center border-b border-t py-2 text-sm sm:flex"> <div className="hidden items-center pl-4 sm:flex">
<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>
@ -202,7 +342,17 @@ export function MemberProgressModal(props: ProgressMapProps) {
<span> <span>
<span data-progress-total="">{memberTotal}</span> Total <span data-progress-total="">{memberTotal}</span> Total
</span> </span>
</p> </div>
<button
onClick={() => {
setShowProgressHint(true);
}}
class="mr-4 flex items-center gap-1 text-sm font-medium text-black opacity-60 transition-opacity hover:opacity-100"
>
<img src={QuestionIcon} className="h-4 w-4" />
Track Progress
</button>
</div>
</div> </div>
<div ref={containerEl} className="px-4 pb-2"></div> <div ref={containerEl} className="px-4 pb-2"></div>

@ -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>
);
}

@ -9,7 +9,6 @@ import { $currentTeam } from '../../stores/team';
import { GroupRoadmapItem } from './GroupRoadmapItem'; import { GroupRoadmapItem } from './GroupRoadmapItem';
import { setUrlParams } from '../../lib/browser'; import { setUrlParams } from '../../lib/browser';
import { getUrlParams } from '../../lib/browser'; import { getUrlParams } from '../../lib/browser';
import { $toastMessage } from '../../stores/toast';
export type UserProgress = { export type UserProgress = {
resourceTitle: string; resourceTitle: string;
@ -78,10 +77,12 @@ export function TeamProgressPage() {
return; return;
} }
getTeamProgress().finally(() => { getTeamProgress().then(
() => {
pageProgressMessage.set(''); pageProgressMessage.set('');
setIsLoading(false); setIsLoading(false);
}); }
);
}, [teamId]); }, [teamId]);
if (isLoading) { if (isLoading) {
@ -134,8 +135,7 @@ 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 '
: '' : ''
}`} }`}

Loading…
Cancel
Save