import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; import CheckIcon from '../../icons/check.svg'; import CloseIcon from '../../icons/close.svg'; import ResetIcon from '../../icons/reset.svg'; import SpinnerIcon from '../../icons/spinner.svg'; import { useKeydown } from '../../hooks/use-keydown'; import { useLoadTopic } from '../../hooks/use-load-topic'; import { useOutsideClick } from '../../hooks/use-outside-click'; import { useToggleTopic } from '../../hooks/use-toggle-topic'; import { httpGet } from '../../lib/http'; import { isLoggedIn } from '../../lib/jwt'; import { isTopicDone, renderTopicProgress, ResourceType, toggleMarkTopicDone as toggleMarkTopicDoneApi, } from '../../lib/resource-progress'; import { pageLoadingMessage, sponsorHidden } from '../../stores/page'; export function TopicDetail() { const [isActive, setIsActive] = useState(false); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(''); const [topicHtml, setTopicHtml] = useState(''); const [isDone, setIsDone] = useState(); const [isUpdatingProgress, setIsUpdatingProgress] = useState(true); const isGuest = useMemo(() => !isLoggedIn(), []); const topicRef = useRef(null); // Details of the currently loaded topic const [topicId, setTopicId] = useState(''); const [resourceId, setResourceId] = useState(''); const [resourceType, setResourceType] = useState('roadmap'); const showLoginPopup = () => { const popupEl = document.querySelector(`#login-popup`); if (!popupEl) { return; } popupEl.classList.remove('hidden'); popupEl.classList.add('flex'); const focusEl = popupEl.querySelector('[autofocus]'); if (focusEl) { focusEl.focus(); } }; const toggleMarkTopicDone = (isDone: boolean) => { setIsUpdatingProgress(true); toggleMarkTopicDoneApi({ topicId, resourceId, resourceType }, isDone) .then(() => { setIsDone(isDone); setIsActive(false); renderTopicProgress(topicId, isDone); }) .catch((err) => { alert(err.message); console.error(err); }) .finally(() => { setIsUpdatingProgress(false); }); }; // Load the topic status when the topic detail is active useEffect(() => { if (!topicId || !resourceId || !resourceType) { return; } setIsUpdatingProgress(true); isTopicDone({ topicId, resourceId, resourceType }) .then((status: boolean) => { setIsUpdatingProgress(false); setIsDone(status); }) .catch(console.error); }, [topicId, resourceId, resourceType]); // Close the topic detail when user clicks outside the topic detail useOutsideClick(topicRef, () => { setIsActive(false); }); useKeydown('Escape', () => { setIsActive(false); }); // Toggle topic is available even if the component UI is not active // This is used on the best practice screen where we have the checkboxes // to mark the topic as done/undone. useToggleTopic(({ topicId, resourceType, resourceId }) => { if (isGuest) { showLoginPopup(); return; } pageLoadingMessage.set('Updating'); // Toggle the topic status isTopicDone({ topicId, resourceId, resourceType }) .then((oldIsDone) => { return toggleMarkTopicDoneApi( { topicId, resourceId, resourceType, }, !oldIsDone ); }) .then((newIsDone) => renderTopicProgress(topicId, newIsDone)) .catch((err) => { alert(err.message); console.error(err); }) .finally(() => { pageLoadingMessage.set(''); }); }); // Load the topic detail when the topic detail is active useLoadTopic(({ topicId, resourceType, resourceId }) => { setIsLoading(true); setIsActive(true); sponsorHidden.set(true); setTopicId(topicId); setResourceType(resourceType); setResourceId(resourceId); const topicPartial = topicId.replaceAll(':', '/'); const topicUrl = resourceType === 'roadmap' ? `/${resourceId}/${topicPartial}` : `/best-practices/${resourceId}/${topicPartial}`; httpGet( topicUrl, {}, { headers: { Accept: 'text/html', }, } ) .then(({ response }) => { if (!response) { setError('Topic not found.'); return; } // It's full HTML with page body, head etc. // We only need the inner HTML of the #main-content const node = new DOMParser().parseFromString(response, 'text/html'); const topicHtml = node?.getElementById('main-content')?.outerHTML || ''; setIsLoading(false); setTopicHtml(topicHtml); }) .catch((err) => { setError('Something went wrong. Please try again later.'); setIsLoading(false); }); }); if (!isActive) { return null; } return (
{isLoading && (
Loading
)} {!isLoading && !error && ( <> {/* Actions for the topic */}
{isGuest && ( )} {!isGuest && ( <> {isUpdatingProgress && ( )} {!isUpdatingProgress && !isDone && ( )} {!isUpdatingProgress && isDone && ( )} )}
{/* Topic Content */}
)}
); }