diff --git a/src/components/TopicDetail/TopicDetail.tsx b/src/components/TopicDetail/TopicDetail.tsx index 272d07453..f01e4b88c 100644 --- a/src/components/TopicDetail/TopicDetail.tsx +++ b/src/components/TopicDetail/TopicDetail.tsx @@ -21,7 +21,7 @@ import type { RoadmapContentDocument, } from '../CustomRoadmap/CustomRoadmap'; import { markdownToHtml, sanitizeMarkdown } from '../../lib/markdown'; -import { Ban, FileText, HeartHandshake, Star, X } from 'lucide-react'; +import { Ban, BookOpen, FileText, HeartHandshake, Star, X } from 'lucide-react'; import { getUrlParams, parseUrl } from '../../lib/browser'; import { Spinner } from '../ReactIcons/Spinner'; import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx'; @@ -42,6 +42,11 @@ import { TopicProgressButton } from './TopicProgressButton.tsx'; import { CreateCourseModal } from './CreateCourseModal.tsx'; import { useChat } from '@ai-sdk/react'; import { topicDetailAiChatTransport } from '../../lib/ai.ts'; +import { useQuery } from '@tanstack/react-query'; +import { queryClient } from '../../stores/query-client.ts'; +import { roadmapTreeMappingOptions } from '../../queries/roadmap-tree.ts'; +import { aiLimitOptions } from '../../queries/ai-course.ts'; +import { billingDetailsOptions } from '../../queries/billing.ts'; type PaidResourceType = { _id?: string; @@ -121,16 +126,14 @@ export function TopicDetail(props: TopicDetailProps) { defaultActiveTab = 'content', } = props; - const [hasEnoughLinks, setHasEnoughLinks] = useState(false); const [contributionUrl, setContributionUrl] = useState(''); const [isActive, setIsActive] = useState(false); - const [isLoading, setIsLoading] = useState(false); + const [isTopicLoading, setIsTopicLoading] = useState(false); const [isContributing, setIsContributing] = useState(false); const [error, setError] = useState(''); const [topicHtml, setTopicHtml] = useState(''); const [hasContent, setHasContent] = useState(false); const [topicTitle, setTopicTitle] = useState(''); - const [topicHtmlTitle, setTopicHtmlTitle] = useState(''); const [links, setLinks] = useState([]); const [activeTab, setActiveTab] = useState(defaultActiveTab); @@ -160,6 +163,40 @@ export function TopicDetail(props: TopicDetailProps) { transport: topicDetailAiChatTransport, }); + const sanitizedTopicId = topicId?.includes('@') + ? topicId?.split('@')?.[1] + : topicId; + const { data: roadmapTreeMapping, isLoading: isRoadmapTreeMappingLoading } = + useQuery( + { + ...roadmapTreeMappingOptions(resourceId), + select: (data) => { + const node = data.find( + (mapping) => mapping.nodeId === sanitizedTopicId, + ); + return node; + }, + enabled: !!sanitizedTopicId && !isCustomResource, + }, + queryClient, + ); + const { data: tokenUsage, isLoading: isTokenUsageLoading } = useQuery( + aiLimitOptions(), + queryClient, + ); + + const { data: userBillingDetails, isLoading: isBillingDetailsLoading } = + useQuery(billingDetailsOptions(), queryClient); + + const isLimitExceeded = (tokenUsage?.used || 0) >= (tokenUsage?.limit || 0); + const isPaidUser = userBillingDetails?.status === 'active'; + + const isLoading = + isTopicLoading || + isRoadmapTreeMappingLoading || + isTokenUsageLoading || + isBillingDetailsLoading; + const handleClose = () => { onClose?.(); setIsActive(false); @@ -237,7 +274,7 @@ export function TopicDetail(props: TopicDetailProps) { // Load the topic detail when the topic detail is active useLoadTopic(({ topicId, resourceType, resourceId, isCustomResource }) => { setError(''); - setIsLoading(true); + setIsTopicLoading(true); setIsActive(true); setTopicId(topicId); @@ -273,7 +310,7 @@ export function TopicDetail(props: TopicDetailProps) { .then(({ response }) => { if (!response) { setError('Topic not found.'); - setIsLoading(false); + setIsTopicLoading(false); return; } let topicHtml = ''; @@ -353,8 +390,6 @@ export function TopicDetail(props: TopicDetailProps) { setLinks(listLinks); setHasContent(topicHasContent); setContributionUrl(contributionUrl); - setHasEnoughLinks(links.length >= 3); - setTopicHtmlTitle(titleElem?.textContent || ''); if (!topicHasContent && renderer === 'editor') { setActiveTab('ai'); @@ -371,12 +406,12 @@ export function TopicDetail(props: TopicDetailProps) { topicHtml = markdownToHtml(sanitizedMarkdown, false); } - setIsLoading(false); + setIsTopicLoading(false); setTopicHtml(topicHtml); }) .catch((err) => { setError('Something went wrong. Please try again later.'); - setIsLoading(false); + setIsTopicLoading(false); }); }); @@ -391,9 +426,6 @@ export function TopicDetail(props: TopicDetailProps) { return null; } - 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; @@ -401,6 +433,8 @@ export function TopicDetail(props: TopicDetailProps) { }); const shouldShowAiTab = !isCustomResource && resourceType === 'roadmap'; + const subjects = roadmapTreeMapping?.subjects || []; + const hasSubjects = subjects.length > 0; const hasDataCampResources = paidResources.some((resource) => resource.title.toLowerCase().includes('datacamp'), @@ -615,6 +649,45 @@ export function TopicDetail(props: TopicDetailProps) { )} + {hasSubjects && ( + <> + + + + )} + {paidResourcesForTopic.length > 0 && ( <>