Roadmap to becoming a developer in 2022
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

243 lines
6.5 KiB

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 { ModalLoader } from './ModalLoader.tsx';
import { UserProgressModalHeader } from './UserProgressModalHeader';
import { X } from 'lucide-react';
import type { PageType } from '../CommandMenu/CommandMenu.tsx';
import type { AllowedRoadmapRenderer } from '../../lib/roadmap.ts';
import { renderFlowJSON } from '../../../editor/renderer/renderer.ts';
export type ProgressMapProps = {
userId?: string;
resourceId: string;
resourceType: ResourceType;
onClose?: () => void;
Allow creating custom roadmaps (#4486) * wip: custom roadmap renderer * wip: custom roadmap events * wip: roadmap content * wip: svg styles * wip: custom roadmap progress * Render progress * Shortcut progress * Progress Tracking styles * wip: edit and share button * fix: disabled the share button * wip: content links rendering * Fix progress share * Replace disabled with `canShare` * wip: show custom roadmaps * wip: users all roadmaps * fix: create roadmap api * chore: roadmap sidebar icon * wip: content links * Update links color * Create roadmap home * Create Roadmap button * Roadmap type * chore: share progress modal * wip: share roadmap * wip: change visibility * chore: custom roadmap progress in activity * wip: custom roadmap share progress * chore: friend's roadmap * wip: custom roadmap skeleton * chore: roadmap title * Restricted Page * fix: skeleton loading width * Fix create roadmap button * chore: remove user id * chore: pick roadmap and share * chore: open new tab on create roadmap * chore: change share title * chore: use team id from params * chore: team roadmap create modal * chore: create team roadmap * chore: custom roadmap modal * chore: placeholde roadmaps * chore: roadmap hint * chore: visibility label * chore: public roadmap * chore: empty screen * chore: team progress * chore: create roadmap responsive * chore: form error * chore: multi user history * wip: manage custom roadmap * chore: empty roadmap list * chore: custom roadmap visit * chore: shared roadmaps * chore: shared roadmaps * chore: empty screen and topic title * chore: show progress bar * Implement Error in topic details * Add Modal close button * fix: link groups * Refactor roadmap creation * Refactor roadmap creation * Refactor team creation * Refactor team roadmaps * Refactor team creation roadmap selection * Refactor * Refactor team roadmap loading * Refactor team roadmaps * Refactor team roadmaps listing * Refactor Account dropdown * Updates * Refactor Account dropdown * Fix Team name overflow * Change Icon color * Update team dropdown * Minor UI fixes * Fix minor UI * Flicker fix in team dropdown * Roadmap action dropdown with responsiveness * Team roadmaps listing * Update team settings * Team roadmaps listing * fix: remove visibility change * Update roadmap options modal * Add dummy renderer * Add renderer script * Add generate renderer script * Add generate renderer * wip: add share settings * Update * Update UI * Update Minor UI * Fix team issue * Update Personal roadmaps UI * Add Roadmap Secret * Update teams type * Rearrange sections * Change Secret name * Add action button on roadmap detail page --------- Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
1 year ago
isCustomResource?: boolean;
renderer?: AllowedRoadmapRenderer;
};
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,
renderer = 'balsamiq',
} = props;
const { s: userId = propUserId } = getUrlParams();
if (!userId) {
return null;
}
const resourceSvgEl = useRef<HTMLDivElement>(null);
const popupBodyEl = useRef<HTMLDivElement>(null);
const currentUser = useAuth();
const [showModal, setShowModal] = useState(!!userId);
const [resourceSvg, setResourceSvg] = useState<SVGElement | null>(null);
const [progressResponse, setProgressResponse] =
useState<UserProgressResponse>();
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<UserProgressResponse | undefined> {
const { error, response } = await httpGet<UserProgressResponse>(
`${
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,
renderer: AllowedRoadmapRenderer = 'balsamiq',
): Promise<SVGElement | undefined> {
const { error, response: roadmapJson } = await httpGet(jsonUrl);
if (error || !roadmapJson) {
throw error || new Error('Something went wrong. Please try again!');
}
return renderer === 'editor'
? await renderFlowJSON(roadmapJson as any)
: 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);
setError('');
Promise.all([
getRoadmapSVG(resourceJsonUrl, renderer),
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 (
<ModalLoader
text={'Loading user progress..'}
isLoading={isLoading}
error={error}
/>
);
}
return (
<div
id={'user-progress-modal'}
9 months ago
className="fixed left-0 right-0 top-0 z-[100] h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50"
>
<div className="relative mx-auto h-full w-full max-w-4xl p-4 md:h-auto">
<div
ref={popupBodyEl}
className={`popup-body relative rounded-lg bg-white pt-[1px] shadow`}
>
<UserProgressModalHeader
isLoading={isLoading}
progressResponse={progressResponse}
/>
<div
ref={resourceSvgEl}
className="px-4 pb-2"
dangerouslySetInnerHTML={{ __html: resourceSvg?.outerHTML || '' }}
></div>
<button
type="button"
className={`absolute right-2.5 top-3 ml-auto inline-flex items-center rounded-lg bg-gray-100 bg-transparent p-1.5 text-sm text-gray-400 hover:text-gray-900 lg:hidden`}
onClick={onClose}
>
<X className="h-4 w-4" />
<span className="sr-only">Close modal</span>
</button>
</div>
</div>
</div>
);
}