import { useMemo, useRef, useState } from 'preact/hooks'; import CloseIcon from '../../icons/close.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, refreshProgressCounters, renderTopicProgress, ResourceType, updateResourceProgress as updateResourceProgressApi, } from '../../lib/resource-progress'; import { pageProgressMessage, sponsorHidden } from '../../stores/page'; import { TopicProgressButton } from './TopicProgressButton'; import { ContributionForm } from './ContributionForm'; export function TopicDetail() { const [contributionAlertMessage, setContributionAlertMessage] = useState(''); const [isActive, setIsActive] = useState(false); const [isLoading, setIsLoading] = useState(false); const [isContributing, setIsContributing] = useState(false); const [error, setError] = useState(''); const [topicHtml, setTopicHtml] = useState(''); 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(); } }; // Close the topic detail when user clicks outside the topic detail useOutsideClick(topicRef, () => { setIsActive(false); setIsContributing(false); }); useKeydown('Escape', () => { setIsActive(false); setIsContributing(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; } pageProgressMessage.set('Updating'); // Toggle the topic status isTopicDone({ topicId, resourceId, resourceType }) .then((oldIsDone) => updateResourceProgressApi( { topicId, resourceId, resourceType, }, oldIsDone ? 'pending' : 'done' ) ) .then(({ done = [] }) => { renderTopicProgress( topicId, done.includes(topicId) ? 'done' : 'pending' ); refreshProgressCounters(); }) .catch((err) => { alert(err.message); console.error(err); }) .finally(() => { pageProgressMessage.set(''); }); }); // Load the topic detail when the topic detail is active useLoadTopic(({ topicId, resourceType, resourceId }) => { setIsLoading(true); setIsActive(true); sponsorHidden.set(true); setContributionAlertMessage(''); 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 && isContributing && ( { if (message) { setContributionAlertMessage(message); } setIsContributing(false); }} /> )} {!isContributing && !isLoading && !error && ( <> {/* Actions for the topic */}
{ setIsActive(false); setIsContributing(false); }} />
{/* Topic Content */}
{/* Contribution */}

Help others learn by submitting links to learn more about this topic{' '}

)}
); }