diff --git a/.astro/types.d.ts b/.astro/types.d.ts index 03d7cc43f..f964fe0cf 100644 --- a/.astro/types.d.ts +++ b/.astro/types.d.ts @@ -1,2 +1 @@ /// -/// \ No newline at end of file diff --git a/src/components/RoadmapAIChat/ChatRoadmapRenderer.tsx b/src/components/RoadmapAIChat/ChatRoadmapRenderer.tsx index 70ae79bf7..7a701c509 100644 --- a/src/components/RoadmapAIChat/ChatRoadmapRenderer.tsx +++ b/src/components/RoadmapAIChat/ChatRoadmapRenderer.tsx @@ -1,6 +1,6 @@ import './ChatRoadmapRenderer.css'; -import { lazy, useCallback, useEffect, useRef } from 'react'; +import { lazy, useCallback, useEffect, useRef, useState } from 'react'; import { renderResourceProgress, updateResourceProgress, @@ -17,6 +17,7 @@ import { showLoginPopup } from '../../lib/popup'; import { queryClient } from '../../stores/query-client'; import { userResourceProgressOptions } from '../../queries/resource-progress'; import { useQuery } from '@tanstack/react-query'; +import { TopicResourcesModal } from './TopicResourcesModal'; const Renderer = lazy(() => import('@roadmapsh/editor').then((mod) => ({ @@ -68,6 +69,7 @@ export function ChatRoadmapRenderer(props: ChatRoadmapRendererProps) { const toast = useToast(); + const [selectedTopicId, setSelectedTopicId] = useState(null); const { data: userResourceProgressData } = useQuery( userResourceProgressOptions('roadmap', roadmapId), queryClient, @@ -190,6 +192,8 @@ export function ChatRoadmapRenderer(props: ChatRoadmapRendererProps) { if (!title) { return; } + + setSelectedTopicId(nodeId); }, []); const handleSvgRightClick = useCallback((e: MouseEvent) => { @@ -230,29 +234,39 @@ export function ChatRoadmapRenderer(props: ChatRoadmapRendererProps) { }, []); return ( - { - roadmapRef.current?.setAttribute('data-renderer', 'editor'); + <> + {selectedTopicId && ( + setSelectedTopicId(null)} + /> + )} - if (!userResourceProgressData) { - return; - } + { + roadmapRef.current?.setAttribute('data-renderer', 'editor'); - const { done, learning, skipped } = userResourceProgressData; - done.forEach((topicId) => { - renderTopicProgress(topicId, 'done'); - }); + if (!userResourceProgressData) { + return; + } - learning.forEach((topicId) => { - renderTopicProgress(topicId, 'learning'); - }); + const { done, learning, skipped } = userResourceProgressData; + done.forEach((topicId) => { + renderTopicProgress(topicId, 'done'); + }); - skipped.forEach((topicId) => { - renderTopicProgress(topicId, 'skipped'); - }); - }} - /> + learning.forEach((topicId) => { + renderTopicProgress(topicId, 'learning'); + }); + + skipped.forEach((topicId) => { + renderTopicProgress(topicId, 'skipped'); + }); + }} + /> + ); } diff --git a/src/components/RoadmapAIChat/TopicResourcesModal.tsx b/src/components/RoadmapAIChat/TopicResourcesModal.tsx index 95bbe2ba4..8961dbc0b 100644 --- a/src/components/RoadmapAIChat/TopicResourcesModal.tsx +++ b/src/components/RoadmapAIChat/TopicResourcesModal.tsx @@ -5,6 +5,9 @@ import { queryClient } from '../../stores/query-client'; import { roadmapContentOptions } from '../../queries/roadmap'; import { ModalLoader } from '../UserProgress/ModalLoader'; import { TopicDetailLink } from '../TopicDetail/TopicDetailLink'; +import { Spinner } from '../ReactIcons/Spinner'; +import { ErrorIcon } from '../ReactIcons/ErrorIcon'; +import { markdownToHtml } from '../../lib/markdown'; type TopicResourcesModalProps = { roadmapId: string; @@ -24,34 +27,62 @@ export function TopicResourcesModal(props: TopicResourcesModalProps) { const topicContent = roadmapContentData?.[topicId]; const links = topicContent?.links || []; - if (isLoadingRoadmapContent || error) { - return ( - - ); - } - return ( - -
-

{topicContent?.title}

-
    - {links.map((link, index) => { - return ( -
  • - -
  • - ); - })} -
-
+ + {!isLoadingRoadmapContent && !error && topicContent && ( +
+

{topicContent?.title}

+ +
+ + {links.length > 0 && ( +
    + {links.map((link, index) => { + return ( +
  • + +
  • + ); + })} +
+ )} +
+ )} + + {(isLoadingRoadmapContent || error || !topicContent) && ( +
+
+ {isLoadingRoadmapContent && ( + <> + + + Loading Topic Resources... + + + )} + + {(error || !topicContent) && !isLoadingRoadmapContent && ( + <> + + + {!topicContent + ? 'No resources found' + : (error?.message ?? 'Something went wrong')} + + + )} +
+
+ )} ); } diff --git a/src/components/RoadmapAIChat/UserProgressActionList.tsx b/src/components/RoadmapAIChat/UserProgressActionList.tsx index 3f350f1fc..d0ed21f42 100644 --- a/src/components/RoadmapAIChat/UserProgressActionList.tsx +++ b/src/components/RoadmapAIChat/UserProgressActionList.tsx @@ -1,7 +1,12 @@ -import { useMutation, useQuery } from '@tanstack/react-query'; +import { + useIsMutating, + useMutation, + useMutationState, + useQuery, +} from '@tanstack/react-query'; import { roadmapTreeMappingOptions } from '../../queries/roadmap-tree'; import { queryClient } from '../../stores/query-client'; -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; import { renderTopicProgress } from '../../lib/resource-progress'; import { updateResourceProgress } from '../../lib/resource-progress'; import { pageProgressMessage } from '../../stores/page'; @@ -10,6 +15,7 @@ import { userResourceProgressOptions } from '../../queries/resource-progress'; import { useToast } from '../../hooks/use-toast'; import { Loader2Icon } from 'lucide-react'; import { CheckIcon } from '../ReactIcons/CheckIcon'; +import { httpPost } from '../../lib/query-http'; type UpdateUserProgress = { id: string; @@ -47,6 +53,19 @@ function parseUserProgress(content: string): UpdateUserProgress[] { return items; } +type BulkUpdateResourceProgressBody = { + done: string[]; + learning: string[]; + skipped: string[]; + pending: string[]; +}; + +type BulkUpdateResourceProgressResponse = { + done: string[]; + learning: string[]; + skipped: string[]; +}; + type UserProgressActionListProps = { roadmapId: string; content: string; @@ -55,6 +74,7 @@ type UserProgressActionListProps = { export function UserProgressActionList(props: UserProgressActionListProps) { const { roadmapId, content } = props; + const toast = useToast(); const updateUserProgress = parseUserProgress(content); const { data: roadmapTreeData } = useQuery( @@ -62,6 +82,38 @@ export function UserProgressActionList(props: UserProgressActionListProps) { queryClient, ); + const { + mutate: bulkUpdateResourceProgress, + isPending: isBulkUpdating, + isSuccess: isBulkUpdateSuccess, + } = useMutation( + { + mutationFn: (body: BulkUpdateResourceProgressBody) => { + return httpPost( + `/v1-bulk-update-resource-progress/${roadmapId}`, + body, + ); + }, + onSuccess: () => { + return queryClient.invalidateQueries( + userResourceProgressOptions('roadmap', roadmapId), + ); + }, + onMutate: () => { + pageProgressMessage.set('Updating progress'); + }, + onSettled: () => { + pageProgressMessage.set(''); + }, + onError: (error) => { + toast.error( + error?.message ?? 'Something went wrong, please try again.', + ); + }, + }, + queryClient, + ); + const progressItemWithText = useMemo(() => { return updateUserProgress.map((item) => { const roadmapTreeItem = roadmapTreeData?.find( @@ -78,17 +130,74 @@ export function UserProgressActionList(props: UserProgressActionListProps) { }); }, [updateUserProgress, roadmapTreeData]); + const [showAll, setShowAll] = useState(false); + const itemCountToShow = 3; + const itemsToShow = showAll + ? progressItemWithText + : progressItemWithText.slice(0, itemCountToShow); + return ( -
- {progressItemWithText.map((item) => ( - - ))} +
+
+ {itemsToShow.map((item) => ( + + ))} + + {progressItemWithText.length > itemCountToShow && ( +
+
+ +
+
+ )} +
+ +
+ +
); } @@ -98,12 +207,22 @@ type ProgressItemProps = { topicId: string; text: string; action: UpdateUserProgress['action']; + isBulkUpdating: boolean; + isBulkUpdateSuccess: boolean; }; function ProgressItem(props: ProgressItemProps) { - const { roadmapId, topicId, text, action } = props; + const { + roadmapId, + topicId, + text, + action, + isBulkUpdating, + isBulkUpdateSuccess, + } = props; const toast = useToast(); + const { mutate: updateTopicStatus, isSuccess, @@ -142,11 +261,11 @@ function ProgressItem(props: ProgressItemProps) { return (
{text} - {!isSuccess && ( + {!isSuccess && !isBulkUpdateSuccess && ( )} - {isSuccess && ( + {(isSuccess || isBulkUpdateSuccess) && ( diff --git a/src/components/UserProgress/ModalLoader.tsx b/src/components/UserProgress/ModalLoader.tsx index d69c0ce35..7ddd98165 100644 --- a/src/components/UserProgress/ModalLoader.tsx +++ b/src/components/UserProgress/ModalLoader.tsx @@ -11,7 +11,7 @@ export function ModalLoader(props: ModalLoaderProps) { const { isLoading, text, error } = props; return ( -
+
diff --git a/src/pages/ai/chat/[roadmapId].astro b/src/pages/ai/chat/[roadmapId].astro index e46a24faf..8c53b5259 100644 --- a/src/pages/ai/chat/[roadmapId].astro +++ b/src/pages/ai/chat/[roadmapId].astro @@ -21,7 +21,8 @@ const { roadmapId } = Astro.params as Props; wrapperClassName='flex-row p-0 lg:p-0 overflow-hidden' client:load > - + +