import { useEffect, useRef, useState } from 'react'; import { wireframeJSONToSVG } from 'roadmap-renderer'; import '../FrameRenderer/FrameRenderer.css'; import { useOutsideClick } from '../../hooks/use-outside-click'; import { useKeydown } from '../../hooks/use-keydown'; import { httpGet } from '../../lib/http'; import type { ResourceType } from '../../lib/resource-progress'; import { topicSelectorAll } from '../../lib/resource-progress'; import { deleteUrlParam, getUrlParams } from '../../lib/browser'; import { useAuth } from '../../hooks/use-auth'; import { ProgressLoadingError } from './ProgressLoadingError'; import { UserProgressModalHeader } from './UserProgressModalHeader'; import { X } from 'lucide-react'; export type ProgressMapProps = { userId?: string; resourceId: string; resourceType: ResourceType; onClose?: () => void; isCustomResource?: boolean; }; export type UserProgressResponse = { user: { _id: string; name: string; }; progress: { total: number; done: string[]; learning: string[]; skipped: string[]; }; }; export function UserProgressModal(props: ProgressMapProps) { const { resourceId, resourceType, userId: propUserId, onClose: onModalClose, } = props; const { s: userId = propUserId } = getUrlParams(); if (!userId) { return null; } const resourceSvgEl = useRef(null); const popupBodyEl = useRef(null); const currentUser = useAuth(); const [showModal, setShowModal] = useState(!!userId); const [resourceSvg, setResourceSvg] = useState(null); const [progressResponse, setProgressResponse] = useState(); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(''); let resourceJsonUrl = import.meta.env.DEV ? 'http://localhost:3000' : 'https://roadmap.sh'; if (resourceType === 'roadmap') { resourceJsonUrl += `/${resourceId}.json`; } else { resourceJsonUrl += `/best-practices/${resourceId}.json`; } async function getUserProgress( userId: string, resourceType: string, resourceId: string, ): Promise { const { error, response } = await httpGet( `${ import.meta.env.PUBLIC_API_URL }/v1-get-user-progress/${userId}?resourceType=${resourceType}&resourceId=${resourceId}`, ); if (error || !response) { throw error || new Error('Something went wrong. Please try again!'); } return response; } async function getRoadmapSVG( jsonUrl: string, ): Promise { const { error, response: roadmapJson } = await httpGet(jsonUrl); if (error || !roadmapJson) { throw error || new Error('Something went wrong. Please try again!'); } return await wireframeJSONToSVG(roadmapJson, { fontURL: '/fonts/balsamiq.woff2', }); } function onClose() { deleteUrlParam('s'); setError(''); setShowModal(false); if (onModalClose) { onModalClose(); } else { window.location.reload(); } } useKeydown('Escape', () => { onClose(); }); useOutsideClick(popupBodyEl, () => { onClose(); }); useEffect(() => { if (!resourceJsonUrl || !resourceId || !resourceType || !userId) { return; } setIsLoading(true); Promise.all([ getRoadmapSVG(resourceJsonUrl), getUserProgress(userId, resourceType, resourceId), ]) .then(([svg, user]) => { if (!user || !svg) { return; } const { progress } = user; const { done, learning, skipped } = progress || { done: [], learning: [], skipped: [], }; done?.forEach((topicId: string) => { topicSelectorAll(topicId, svg).forEach((el) => { el.classList.add('done'); }); }); learning?.forEach((topicId: string) => { topicSelectorAll(topicId, svg).forEach((el) => { el.classList.add('learning'); }); }); skipped?.forEach((topicId: string) => { topicSelectorAll(topicId, svg).forEach((el) => { el.classList.add('skipped'); }); }); svg.querySelectorAll('.clickable-group').forEach((el) => { el.classList.remove('clickable-group'); }); svg.querySelectorAll('[data-group-id]').forEach((el) => { el.removeAttribute('data-group-id'); }); setResourceSvg(svg); setProgressResponse(user); }) .catch((err) => { setError(err?.message || 'Something went wrong. Please try again!'); }) .finally(() => { setIsLoading(false); }); }, [userId]); if (currentUser?.id === userId) { deleteUrlParam('s'); return null; } if (!showModal) { return null; } if (isLoading || error) { return ; } return (
); }