import { useCallback, useEffect, useState, type MouseEvent, useRef, } from 'react'; import { Spinner } from '../ReactIcons/Spinner'; import '../FrameRenderer/FrameRenderer.css'; import type { TeamMember } from './TeamProgressPage'; import { httpGet } from '../../lib/http'; import { renderTopicProgress, type ResourceProgressType, type ResourceType, 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 type { GetRoadmapResponse } from '../CustomRoadmap/CustomRoadmap'; import { ReadonlyEditor } from '../../../editor/readonly-editor'; import type { Node } from 'reactflow'; import { useKeydown } from '../../hooks/use-keydown'; import { useOutsideClick } from '../../hooks/use-outside-click'; import { MemberProgressModalHeader } from './MemberProgressModalHeader'; import { X } from 'lucide-react'; export type ProgressMapProps = { member: TeamMember; teamId: string; resourceId: string; resourceType: 'roadmap' | 'best-practice'; onClose: () => void; onShowMyProgress: () => void; isCustomResource?: boolean; }; export type MemberProgressResponse = { removed: string[]; done: string[]; learning: string[]; skipped: string[]; }; export function MemberCustomProgressModal(props: ProgressMapProps) { const { resourceId, member, resourceType, onShowMyProgress, teamId, onClose, } = props; const user = useAuth(); const isCurrentUser = user?.email === member.email; const popupBodyEl = useRef(null); const [roadmap, setRoadmap] = useState(null); const [memberProgress, setMemberProgress] = useState(); const [isLoading, setIsLoading] = useState(true); const toast = useToast(); useKeydown('Escape', () => onClose()); useOutsideClick(popupBodyEl, () => onClose()); async function getMemberProgress( teamId: string, memberId: string, resourceType: string, resourceId: string, ) { const { error, response } = await httpGet( `${ import.meta.env.PUBLIC_API_URL }/v1-get-member-resource-progress/${teamId}/${memberId}?resourceType=${resourceType}&resourceId=${resourceId}`, ); if (error || !response) { toast.error(error?.message || 'Failed to get member progress'); return; } setMemberProgress(response); return response; } async function getRoadmap() { const { response, error } = await httpGet( `${import.meta.env.PUBLIC_API_URL}/v1-get-roadmap/${resourceId}`, ); if (error || !response) { toast.error(error?.message || 'Failed to load roadmap'); return; } setRoadmap(response); return response; } useEffect(() => { if (!resourceId || !resourceType || !teamId) { return; } setIsLoading(true); Promise.all([ getRoadmap(), getMemberProgress(teamId, member._id, resourceType, resourceId), ]) .then(() => {}) .catch((err) => { console.error(err); toast.error(err?.message || 'Something went wrong. Please try again!'); }) .finally(() => { setIsLoading(false); }); }, [member]); 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; } const handleTopicRightClick = useCallback((e: MouseEvent, node: Node) => { if (!isCurrentUser) { return; } const target = node?.type === 'todo' ? document.querySelector(`[data-id="${node.id}"]`) : (e?.currentTarget as HTMLDivElement); if (!target) { return; } const isCurrentStatusDone = target?.classList.contains('done'); updateTopicStatus(node.id, isCurrentStatusDone ? 'pending' : 'done'); }, []); const handleTopicShiftClick = useCallback((e: MouseEvent, node: Node) => { if (!isCurrentUser) { return; } const target = e?.currentTarget as HTMLDivElement; if (!target) { return; } const isCurrentStatusLearning = target?.classList.contains('learning'); updateTopicStatus( node.id, isCurrentStatusLearning ? 'pending' : 'learning', ); }, []); const handleTopicAltClick = useCallback((e: MouseEvent, node: Node) => { if (!isCurrentUser) { return; } const target = e?.currentTarget as HTMLDivElement; if (!target) { return; } const isCurrentStatusSkipped = target?.classList.contains('skipped'); updateTopicStatus(node.id, isCurrentStatusSkipped ? 'pending' : 'skipped'); }, []); const handleLinkClick = useCallback((linkId: string, href: string) => { if (!href || !isCurrentUser) { return; } const isExternalLink = href.startsWith('http'); if (isExternalLink) { window.open(href, '_blank'); } else { window.location.href = href; } }, []); return (
{!isLoading && roadmap && (
{ const { removed = [], done = [], learning = [], skipped = [], } = memberProgress || {}; done.forEach((id: string) => renderTopicProgress(id, 'done')); learning.forEach((id: string) => renderTopicProgress(id, 'learning'), ); skipped.forEach((id: string) => renderTopicProgress(id, 'skipped'), ); removed.forEach((id: string) => renderTopicProgress(id, 'removed'), ); }} onTopicRightClick={handleTopicRightClick} onTopicShiftClick={handleTopicShiftClick} onTopicAltClick={handleTopicAltClick} onButtonNodeClick={handleLinkClick} onLinkClick={handleLinkClick} fontFamily="Balsamiq Sans" fontURL="/fonts/balsamiq.woff2" />
)} {isLoading && (
)}
); }