From a2440f31ecaa83fe8f1369fecf4dcc9299b16755 Mon Sep 17 00:00:00 2001 From: Arik Chakma Date: Fri, 23 Aug 2024 05:44:07 +0600 Subject: [PATCH] wip: add resource api --- src/components/TopicDetail/TopicDetail.tsx | 326 ++++++++++++--------- 1 file changed, 185 insertions(+), 141 deletions(-) diff --git a/src/components/TopicDetail/TopicDetail.tsx b/src/components/TopicDetail/TopicDetail.tsx index da9000c06..fcdf011f2 100644 --- a/src/components/TopicDetail/TopicDetail.tsx +++ b/src/components/TopicDetail/TopicDetail.tsx @@ -32,6 +32,18 @@ import { YouTubeIcon } from '../ReactIcons/YouTubeIcon.tsx'; import { resourceTitleFromId } from '../../lib/roadmap.ts'; import { lockBodyScroll } from '../../lib/dom.ts'; +export const allowedRoadmapResourceTypes = ['course', 'book', 'other'] as const; +export type AllowedRoadmapResourceType = + (typeof allowedRoadmapResourceTypes)[number]; + +type TopicResource = { + _id?: string; + title: string; + type: AllowedRoadmapResourceType; + url: string; + topicIds: string[]; +}; + type TopicDetailProps = { resourceTitle?: string; resourceType?: ResourceType; @@ -50,7 +62,7 @@ const linkTypes: Record = { video: 'bg-purple-300', website: 'bg-blue-300', official: 'bg-blue-600 text-white', - feed: "bg-[#ce3df3] text-white" + feed: 'bg-[#ce3df3] text-white', }; export function TopicDetail(props: TopicDetailProps) { @@ -69,6 +81,8 @@ export function TopicDetail(props: TopicDetailProps) { const [links, setLinks] = useState([]); const toast = useToast(); + const [topicResources, setTopicResources] = useState([]); + const { secret } = getUrlParams() as { secret: string }; const isGuest = useMemo(() => !isLoggedIn(), []); const topicRef = useRef(null); @@ -87,6 +101,20 @@ export function TopicDetail(props: TopicDetailProps) { setIsActive(false); }); + const loadTopicResources = async (roadmapId: string, topicId: string) => { + const sanitizedTopicId = topicId.split('@')?.[1] || topicId; + const { response, error } = await httpGet( + `${import.meta.env.PUBLIC_API_URL}/v1-list-topic-resources/${roadmapId}?t=${sanitizedTopicId}`, + ); + + if (error) { + toast.error(error?.message || 'Failed to load topic resources'); + return; + } + + setTopicResources(response || []); + }; + // 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. @@ -151,114 +179,125 @@ export function TopicDetail(props: TopicDetailProps) { }`; } - httpGet( - topicUrl, - {}, - { - ...(!isCustomResource && { - headers: { - Accept: 'text/html', - }, - }), - }, - ) - .then(({ response }) => { - if (!response) { - setError('Topic not found.'); - setIsLoading(false); - return; - } - let topicHtml = ''; - if (!isCustomResource) { - const topicDom = new DOMParser().parseFromString( - response as string, - 'text/html', - ); - - const links = topicDom.querySelectorAll('a'); - const urlElem: HTMLElement = - topicDom.querySelector('[data-github-url]')!; - const contributionUrl = urlElem?.dataset?.githubUrl || ''; - - const titleElem: HTMLElement = topicDom.querySelector('h1')!; - const otherElems = topicDom.querySelectorAll('body > *:not(h1, div)'); - - let ulWithLinks: HTMLUListElement = document.createElement('ul'); - - // we need to remove the `ul` with just links (i.e. resource links) - // and show them separately. - topicDom.querySelectorAll('ul').forEach((ul) => { - const lisWithJustLinks = Array.from( - ul.querySelectorAll('li'), - ).filter((li) => { - return ( - li.children.length === 1 && - li.children[0].tagName === 'A' && - li.children[0].textContent === li.textContent - ); + Promise.all([ + httpGet( + topicUrl, + {}, + { + ...(!isCustomResource && { + headers: { + Accept: 'text/html', + }, + }), + }, + ) + .then(({ response }) => { + if (!response) { + setError('Topic not found.'); + setIsLoading(false); + return; + } + let topicHtml = ''; + if (!isCustomResource) { + const topicDom = new DOMParser().parseFromString( + response as string, + 'text/html', + ); + + const links = topicDom.querySelectorAll('a'); + const urlElem: HTMLElement = + topicDom.querySelector('[data-github-url]')!; + const contributionUrl = urlElem?.dataset?.githubUrl || ''; + + const titleElem: HTMLElement = topicDom.querySelector('h1')!; + const otherElems = topicDom.querySelectorAll( + 'body > *:not(h1, div)', + ); + + let ulWithLinks: HTMLUListElement = document.createElement('ul'); + + // we need to remove the `ul` with just links (i.e. resource links) + // and show them separately. + topicDom.querySelectorAll('ul').forEach((ul) => { + const lisWithJustLinks = Array.from( + ul.querySelectorAll('li'), + ).filter((li) => { + return ( + li.children.length === 1 && + li.children[0].tagName === 'A' && + li.children[0].textContent === li.textContent + ); + }); + + if (lisWithJustLinks.length > 0) { + ulWithLinks = ul; + } }); - if (lisWithJustLinks.length > 0) { - ulWithLinks = ul; + const listLinks = Array.from(ulWithLinks.querySelectorAll('li > a')) + .map((link, counter) => { + const typePattern = /@([a-z.]+)@/; + let linkText = link.textContent || ''; + const linkHref = link.getAttribute('href') || ''; + const linkType = linkText.match(typePattern)?.[1] || 'article'; + + linkText = linkText.replace(typePattern, ''); + + return { + id: `link-${linkHref}-${counter}`, + title: linkText, + url: linkHref, + type: linkType as AllowedLinkTypes, + }; + }) + .sort((a, b) => { + // official at the top + // opensource at second + // article at third + // videos at fourth + // rest at last + const order = [ + 'official', + 'opensource', + 'article', + 'video', + 'feed', + ]; + return order.indexOf(a.type) - order.indexOf(b.type); + }); + + if (ulWithLinks) { + ulWithLinks.remove(); } - }); - - const listLinks = Array.from(ulWithLinks.querySelectorAll('li > a')) - .map((link, counter) => { - const typePattern = /@([a-z.]+)@/; - let linkText = link.textContent || ''; - const linkHref = link.getAttribute('href') || ''; - const linkType = linkText.match(typePattern)?.[1] || 'article'; - - linkText = linkText.replace(typePattern, ''); - - return { - id: `link-${linkHref}-${counter}`, - title: linkText, - url: linkHref, - type: linkType as AllowedLinkTypes, - }; - }) - .sort((a, b) => { - // official at the top - // opensource at second - // article at third - // videos at fourth - // rest at last - const order = ['official', 'opensource', 'article', 'video', 'feed']; - return order.indexOf(a.type) - order.indexOf(b.type); - }); - if (ulWithLinks) { - ulWithLinks.remove(); - } - - topicHtml = topicDom.body.innerHTML; + topicHtml = topicDom.body.innerHTML; - setLinks(listLinks); - setHasContent(otherElems.length > 0); - setContributionUrl(contributionUrl); - setHasEnoughLinks(links.length >= 3); - setTopicHtmlTitle(titleElem?.textContent || ''); - } else { - setLinks((response as RoadmapContentDocument)?.links || []); - setTopicTitle((response as RoadmapContentDocument)?.title || ''); + setLinks(listLinks); + setHasContent(otherElems.length > 0); + setContributionUrl(contributionUrl); + setHasEnoughLinks(links.length >= 3); + setTopicHtmlTitle(titleElem?.textContent || ''); + } else { + setLinks((response as RoadmapContentDocument)?.links || []); + setTopicTitle((response as RoadmapContentDocument)?.title || ''); - const sanitizedMarkdown = sanitizeMarkdown( - (response as RoadmapContentDocument).description || '', - ); + const sanitizedMarkdown = sanitizeMarkdown( + (response as RoadmapContentDocument).description || '', + ); - setHasContent(sanitizedMarkdown?.length > 0); - topicHtml = markdownToHtml(sanitizedMarkdown, false); - } + setHasContent(sanitizedMarkdown?.length > 0); + topicHtml = markdownToHtml(sanitizedMarkdown, false); + } - setIsLoading(false); - setTopicHtml(topicHtml); - }) - .catch((err) => { - setError('Something went wrong. Please try again later.'); - setIsLoading(false); - }); + setIsLoading(false); + setTopicHtml(topicHtml); + }) + .catch((err) => { + setError('Something went wrong. Please try again later.'); + setIsLoading(false); + }), + loadTopicResources(resourceId, topicId), + ]); }); useEffect(() => { @@ -424,47 +463,52 @@ export function TopicDetail(props: TopicDetailProps) { )} {/* Contribution */} - {canSubmitContribution && !hasEnoughLinks && contributionUrl && hasContent && ( -
-
-

- Find more resources using these pre-filled search queries: -

-
- - - Google - - - - YouTube - + {canSubmitContribution && + !hasEnoughLinks && + contributionUrl && + hasContent && ( +
+
+

+ Find more resources using these pre-filled search + queries: +

+
-
-

- This popup should be a brief introductory paragraph for the topic and a few links - to good articles, videos, or any other self-vetted resources. Please consider - submitting a PR to improve this content. -

- - - Help us Improve this Content - -
- )} +

+ This popup should be a brief introductory paragraph for + the topic and a few links to good articles, videos, or any + other self-vetted resources. Please consider submitting a + PR to improve this content. +

+ + + Help us Improve this Content + +
+ )}
{resourceId === 'devops' && (
@@ -528,4 +572,4 @@ export function TopicDetail(props: TopicDetailProps) {
); -} \ No newline at end of file +}