From 3d72c49c3fae206c7b1585de7de02a3439068476 Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Wed, 28 Aug 2024 01:00:49 +0100 Subject: [PATCH] Add resource separation --- .../CustomRoadmap/CustomRoadmap.tsx | 1 + .../TopicDetail/ResourceListSeparator.tsx | 33 +++ src/components/TopicDetail/TopicDetail.tsx | 256 +++++++++++------- .../TopicDetail/TopicDetailLink.tsx | 57 ++++ src/pages/[roadmapId]/courses.astro | 1 - src/pages/[roadmapId]/index.astro | 1 + src/pages/[roadmapId]/projects.astro | 1 - .../[bestPracticeId]/index.astro | 1 + src/stores/roadmap.ts | 1 + 9 files changed, 252 insertions(+), 100 deletions(-) create mode 100644 src/components/TopicDetail/ResourceListSeparator.tsx create mode 100644 src/components/TopicDetail/TopicDetailLink.tsx diff --git a/src/components/CustomRoadmap/CustomRoadmap.tsx b/src/components/CustomRoadmap/CustomRoadmap.tsx index fe3d66f45..f28e98545 100644 --- a/src/components/CustomRoadmap/CustomRoadmap.tsx +++ b/src/components/CustomRoadmap/CustomRoadmap.tsx @@ -122,6 +122,7 @@ export function CustomRoadmap(props: CustomRoadmapProps) { {!isEmbed && } + + {Icon && } + {text} + +
+

+ ); +} diff --git a/src/components/TopicDetail/TopicDetail.tsx b/src/components/TopicDetail/TopicDetail.tsx index da9000c06..8336a7adc 100644 --- a/src/components/TopicDetail/TopicDetail.tsx +++ b/src/components/TopicDetail/TopicDetail.tsx @@ -22,8 +22,7 @@ import type { RoadmapContentDocument, } from '../CustomRoadmap/CustomRoadmap'; import { markdownToHtml, sanitizeMarkdown } from '../../lib/markdown'; -import { cn } from '../../lib/classname'; -import { Ban, FileText, HeartHandshake, X } from 'lucide-react'; +import { Ban, FileText, HeartHandshake, Star, X } from 'lucide-react'; import { getUrlParams, parseUrl } from '../../lib/browser'; import { Spinner } from '../ReactIcons/Spinner'; import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx'; @@ -31,8 +30,11 @@ import { GoogleIcon } from '../ReactIcons/GoogleIcon.tsx'; import { YouTubeIcon } from '../ReactIcons/YouTubeIcon.tsx'; import { resourceTitleFromId } from '../../lib/roadmap.ts'; import { lockBodyScroll } from '../../lib/dom.ts'; +import { TopicDetailLink } from './TopicDetailLink.tsx'; +import { ResourceListSeparator } from './ResourceListSeparator.tsx'; type TopicDetailProps = { + resourceId?: string; resourceTitle?: string; resourceType?: ResourceType; @@ -40,21 +42,42 @@ type TopicDetailProps = { canSubmitContribution: boolean; }; -const linkTypes: Record = { - article: 'bg-yellow-300', - course: 'bg-green-400', - opensource: 'bg-black text-white', - 'roadmap.sh': 'bg-black text-white', - roadmap: 'bg-black text-white', - podcast: 'bg-purple-300', - video: 'bg-purple-300', - website: 'bg-blue-300', - official: 'bg-blue-600 text-white', - feed: "bg-[#ce3df3] text-white" +type PaidResourceType = { + _id?: string; + title: string; + type: 'course' | 'book' | 'other'; + url: string; + topicIds: string[]; }; +const paidResourcesCache: Record = {}; + +async function fetchRoadmapPaidResources(roadmapId: string) { + if (paidResourcesCache[roadmapId]) { + return paidResourcesCache[roadmapId]; + } + + const { response, error } = await httpGet( + `${import.meta.env.PUBLIC_API_URL}/v1-list-roadmap-paid-resources/${roadmapId}`, + ); + + if (!response || error) { + console.error(error); + return []; + } + + paidResourcesCache[roadmapId] = response; + + return response; +} + export function TopicDetail(props: TopicDetailProps) { - const { canSubmitContribution, isEmbed = false, resourceTitle } = props; + const { + canSubmitContribution, + resourceId: defaultResourceId, + isEmbed = false, + resourceTitle, + } = props; const [hasEnoughLinks, setHasEnoughLinks] = useState(false); const [contributionUrl, setContributionUrl] = useState(''); @@ -77,6 +100,7 @@ export function TopicDetail(props: TopicDetailProps) { const [topicId, setTopicId] = useState(''); const [resourceId, setResourceId] = useState(''); const [resourceType, setResourceType] = useState('roadmap'); + const [paidResources, setPaidResources] = useState([]); // Close the topic detail when user clicks outside the topic detail useOutsideClick(topicRef, () => { @@ -87,6 +111,16 @@ export function TopicDetail(props: TopicDetailProps) { setIsActive(false); }); + useEffect(() => { + if (resourceType !== 'roadmap' || !defaultResourceId) { + return; + } + + fetchRoadmapPaidResources(defaultResourceId).then((resources) => { + setPaidResources(resources); + }); + }, [defaultResourceId]); + // 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. @@ -225,7 +259,13 @@ export function TopicDetail(props: TopicDetailProps) { // article at third // videos at fourth // rest at last - const order = ['official', 'opensource', 'article', 'video', 'feed']; + const order = [ + 'official', + 'opensource', + 'article', + 'video', + 'feed', + ]; return order.indexOf(a.type) - order.indexOf(b.type); }); @@ -280,6 +320,12 @@ export function TopicDetail(props: TopicDetailProps) { const tnsLink = 'https://thenewstack.io/devops/?utm_source=roadmap.sh&utm_medium=Referral&utm_campaign=Topic'; + const paidResourcesForTopic = paidResources.filter((resource) => { + const normalizedTopicId = + topicId.indexOf('@') !== -1 ? topicId.split('@')[1] : topicId; + return resource.topicIds.includes(normalizedTopicId); + }); + return (
0 && ( - + + )} + + {paidResourcesForTopic.length > 0 && ( + <> + + +
    + {paidResourcesForTopic.map((resource) => { + return ( +
  • + +
  • + ); + })} +
+ )} {/* 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 +588,4 @@ export function TopicDetail(props: TopicDetailProps) {
); -} \ No newline at end of file +} diff --git a/src/components/TopicDetail/TopicDetailLink.tsx b/src/components/TopicDetail/TopicDetailLink.tsx new file mode 100644 index 000000000..9d4b01978 --- /dev/null +++ b/src/components/TopicDetail/TopicDetailLink.tsx @@ -0,0 +1,57 @@ +import { cn } from '../../lib/classname.ts'; +import type { AllowedLinkTypes } from '../CustomRoadmap/CustomRoadmap.tsx'; + +const linkTypes: Record = { + article: 'bg-yellow-300', + course: 'bg-green-400', + opensource: 'bg-black text-white', + 'roadmap.sh': 'bg-black text-white', + roadmap: 'bg-black text-white', + podcast: 'bg-purple-300', + video: 'bg-purple-300', + website: 'bg-blue-300', + official: 'bg-blue-600 text-white', + feed: 'bg-[#ce3df3] text-white', +}; + +const paidLinkTypes: Record = { + course: 'bg-yellow-300', +}; + +type TopicDetailLinkProps = { + url: string; + onClick?: () => void; + type: AllowedLinkTypes; + title: string; + isPaid?: boolean; +}; + +export function TopicDetailLink(props: TopicDetailLinkProps) { + const { url, onClick, type, title, isPaid = false } = props; + + return ( + + + {type === 'opensource' ? ( + <> + {url.includes('github') && 'GitHub'} + {url.includes('gitlab') && 'GitLab'} + + ) : ( + type + )} + + {title} + + ); +} diff --git a/src/pages/[roadmapId]/courses.astro b/src/pages/[roadmapId]/courses.astro index a1726b405..d7a606f85 100644 --- a/src/pages/[roadmapId]/courses.astro +++ b/src/pages/[roadmapId]/courses.astro @@ -7,7 +7,6 @@ import RoadmapHeader from '../../components/RoadmapHeader.astro'; import { FolderKanbanIcon } from 'lucide-react'; import { EmptyProjects } from '../../components/Projects/EmptyProjects'; import ShareIcons from '../../components/ShareIcons/ShareIcons.astro'; -import { TopicDetail } from '../../components/TopicDetail/TopicDetail'; import { UserProgressModal } from '../../components/UserProgress/UserProgressModal'; import BaseLayout from '../../layouts/BaseLayout.astro'; import { getProjectsByRoadmapId } from '../../lib/project'; diff --git a/src/pages/[roadmapId]/index.astro b/src/pages/[roadmapId]/index.astro index 4aa157968..184bb9eed 100644 --- a/src/pages/[roadmapId]/index.astro +++ b/src/pages/[roadmapId]/index.astro @@ -96,6 +96,7 @@ const projects = await getProjectsByRoadmapId(roadmapId); (); export const totalRoadmapNodes = atom(); +