mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-07-31 14:30:13 +02:00
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>
This commit is contained in:
22
src/components/ReactIcons/CloseIcon.tsx
Normal file
22
src/components/ReactIcons/CloseIcon.tsx
Normal file
@@ -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 Icon from './AstroIcon.astro';
|
||||
import ResourceProgressStats from './ResourceProgressStats.astro';
|
||||
|
@@ -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<HTMLDivElement>(null);
|
||||
const popupBodyEl = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [showProgressHint, setShowProgressHint] = useState(false);
|
||||
const [memberProgress, setMemberProgress] =
|
||||
useState<MemberProgressResponse>();
|
||||
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 && (
|
||||
<ProgressHint
|
||||
onClose={() => {
|
||||
setShowProgressHint(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="p-4">
|
||||
<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'}>
|
||||
{member.name}'s Progress
|
||||
</h2>
|
||||
<p
|
||||
className={
|
||||
'hidden text-xs text-gray-500 sm:text-sm md:block md:text-base'
|
||||
}
|
||||
>
|
||||
You are looking at {member.name}'s progress.{' '}
|
||||
<a
|
||||
target={'_blank'}
|
||||
href={`/${resourceId}?t=${teamId}`}
|
||||
className="text-blue-600 underline"
|
||||
{isCurrentUser ? (
|
||||
<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'}>
|
||||
Your Progress
|
||||
</h2>
|
||||
<p className={'text-gray-500'}>
|
||||
You can{' '}
|
||||
<button
|
||||
className="inline-flex items-center text-blue-600 underline"
|
||||
onClick={() => {
|
||||
setShowProgressHint(true);
|
||||
}}
|
||||
>
|
||||
follow these instructions
|
||||
</button>{' '}
|
||||
to update your progress below.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<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'}>
|
||||
{member.name}'s Progress
|
||||
</h2>
|
||||
<p
|
||||
className={
|
||||
'hidden text-xs text-gray-500 sm:text-sm md:block md:text-base'
|
||||
}
|
||||
>
|
||||
View your progress
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<p className={'block text-gray-500 md:hidden'}>
|
||||
View your progress
|
||||
<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 py-2 text-sm sm:hidden px-4">
|
||||
You are looking at {member.name}'s progress.{' '}
|
||||
<a
|
||||
target={'_blank'}
|
||||
href={`/${resourceId}?t=${teamId}`}
|
||||
className="text-blue-600 underline"
|
||||
>
|
||||
View your progress
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<p className={'block text-gray-500 md:hidden'}>
|
||||
View your progress
|
||||
<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>{progressPercentage}</span>% Done
|
||||
</span>
|
||||
|
70
src/components/TeamProgress/ProgressHint.tsx
Normal file
70
src/components/TeamProgress/ProgressHint.tsx
Normal file
@@ -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;
|
||||
}
|
||||
|
||||
getTeamProgress().finally(() => {
|
||||
pageProgressMessage.set('');
|
||||
setIsLoading(false);
|
||||
});
|
||||
getTeamProgress().then(
|
||||
() => {
|
||||
pageProgressMessage.set('');
|
||||
setIsLoading(false);
|
||||
}
|
||||
);
|
||||
}, [teamId]);
|
||||
|
||||
if (isLoading) {
|
||||
@@ -144,11 +146,10 @@ export function TeamProgressPage() {
|
||||
<div className="flex items-center gap-2">
|
||||
{groupingTypes.map((grouping) => (
|
||||
<button
|
||||
className={`rounded-md border p-1 px-2 text-sm ${
|
||||
selectedGrouping === grouping.value
|
||||
? ' border-gray-400 bg-gray-200 '
|
||||
: ''
|
||||
}`}
|
||||
className={`rounded-md border p-1 px-2 text-sm ${selectedGrouping === grouping.value
|
||||
? ' border-gray-400 bg-gray-200 '
|
||||
: ''
|
||||
}`}
|
||||
onClick={() => setSelectedGrouping(grouping.value)}
|
||||
>
|
||||
{grouping.label}
|
||||
|
Reference in New Issue
Block a user