diff --git a/src/components/Dashboard/DashboardBookmarkCard.tsx b/src/components/Dashboard/DashboardBookmarkCard.tsx new file mode 100644 index 000000000..92eded421 --- /dev/null +++ b/src/components/Dashboard/DashboardBookmarkCard.tsx @@ -0,0 +1,38 @@ +import { Bookmark } from 'lucide-react'; +import type { UserProgress } from '../TeamProgress/TeamProgressPage'; + +type DashboardBookmarkCardProps = { + bookmark: UserProgress; +}; + +export function DashboardBookmarkCard(props: DashboardBookmarkCardProps) { + const { + resourceType, + resourceId, + resourceTitle, + roadmapSlug, + isCustomResource, + } = props.bookmark; + + let url = + resourceType === 'roadmap' + ? `/${resourceId}` + : `/best-practices/${resourceId}`; + + if (isCustomResource) { + url = `/r/${roadmapSlug}`; + } + + return ( + + +

+ {resourceTitle} +

+
+ ); +} diff --git a/src/components/Dashboard/DashboardCustomProgressCard.tsx b/src/components/Dashboard/DashboardCustomProgressCard.tsx new file mode 100644 index 000000000..b84b3d3a6 --- /dev/null +++ b/src/components/Dashboard/DashboardCustomProgressCard.tsx @@ -0,0 +1,64 @@ +import { getPercentage } from '../../helper/number'; +import { getRelativeTimeString } from '../../lib/date'; +import type { UserProgress } from '../TeamProgress/TeamProgressPage'; + +type DashboardCustomProgressCardProps = { + progress: UserProgress; +}; + +export function DashboardCustomProgressCard(props: DashboardCustomProgressCardProps) { + const { progress } = props; + + const { + resourceType, + resourceId, + resourceTitle, + total: totalCount, + done: doneCount, + skipped: skippedCount, + roadmapSlug, + isCustomResource, + updatedAt, + } = progress; + + let url = + resourceType === 'roadmap' + ? `/${resourceId}` + : `/best-practices/${resourceId}`; + + if (isCustomResource) { + url = `/r/${roadmapSlug}`; + } + + const totalMarked = doneCount + skippedCount; + const progressPercentage = getPercentage(totalMarked, totalCount); + + return ( + +

{resourceTitle}

+ +
+
+
+
+ + {Math.floor(+progressPercentage)}% + +
+ +

+ {isCustomResource ? ( + <>Last updated {getRelativeTimeString(updatedAt)} + ) : ( + <>Last practiced {getRelativeTimeString(updatedAt)} + )} +

+
+ ); +} diff --git a/src/components/Dashboard/DashboardProgressCard.tsx b/src/components/Dashboard/DashboardProgressCard.tsx index 376fc3871..738237cfd 100644 --- a/src/components/Dashboard/DashboardProgressCard.tsx +++ b/src/components/Dashboard/DashboardProgressCard.tsx @@ -1,5 +1,4 @@ import { getPercentage } from '../../helper/number'; -import { getRelativeTimeString } from '../../lib/date'; import type { UserProgress } from '../TeamProgress/TeamProgressPage'; type DashboardProgressCardProps = { @@ -8,7 +7,6 @@ type DashboardProgressCardProps = { export function DashboardProgressCard(props: DashboardProgressCardProps) { const { progress } = props; - const { resourceType, resourceId, @@ -36,29 +34,24 @@ export function DashboardProgressCard(props: DashboardProgressCardProps) { return ( -

{resourceTitle}

+

+ {resourceTitle} +

-
-
+
+
- + {Math.floor(+progressPercentage)}%
- -

- {isCustomResource ? ( - <>Last updated {getRelativeTimeString(updatedAt)} - ) : ( - <>Last practiced {getRelativeTimeString(updatedAt)} - )} -

); } diff --git a/src/components/Dashboard/DashboardProjectCard.tsx b/src/components/Dashboard/DashboardProjectCard.tsx new file mode 100644 index 000000000..14a033bb0 --- /dev/null +++ b/src/components/Dashboard/DashboardProjectCard.tsx @@ -0,0 +1,34 @@ +import { CircleCheck, CircleDashed } from 'lucide-react'; +import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions'; + +type DashboardProjectCardProps = { + project: ProjectStatusDocument & { + title: string; + }; +}; + +export function DashboardProjectCard(props: DashboardProjectCardProps) { + const { project } = props; + + const { title, projectId, submittedAt, repositoryUrl } = project; + + const url = `/projects/${projectId}`; + const status = submittedAt && repositoryUrl ? 'submitted' : 'started'; + + return ( + + {status === 'submitted' ? ( + + ) : ( + + )} +

+ {title} +

+
+ ); +} diff --git a/src/components/Dashboard/ListDashboardCustomProgress.tsx b/src/components/Dashboard/ListDashboardCustomProgress.tsx index 4915f3b7f..0f52e7518 100644 --- a/src/components/Dashboard/ListDashboardCustomProgress.tsx +++ b/src/components/Dashboard/ListDashboardCustomProgress.tsx @@ -1,6 +1,5 @@ import type { UserProgress } from '../TeamProgress/TeamProgressPage'; -import { DashboardProgressCard } from './DashboardProgressCard'; -import { DashboardProgressCardSkeleton } from './ListDashboardProgress'; +import { DashboardCustomProgressCard } from './DashboardCustomProgressCard'; import { DashboardCardLink } from './DashboardCardLink'; import { useState } from 'react'; import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal'; @@ -67,13 +66,13 @@ export function ListDashboardCustomProgress( {isLoading ? ( <> {Array.from({ length: 8 }).map((_, index) => ( - + ))} ) : ( <> {progresses.map((progress) => ( - @@ -97,3 +96,13 @@ export function ListDashboardCustomProgress( ); } + +type CustomProgressCardSkeletonProps = {}; + +export function CustomProgressCardSkeleton( + props: CustomProgressCardSkeletonProps, +) { + return ( +
+ ); +} diff --git a/src/components/Dashboard/ListDashboardProgress.tsx b/src/components/Dashboard/ListDashboardProgress.tsx deleted file mode 100644 index 4fd9609f2..000000000 --- a/src/components/Dashboard/ListDashboardProgress.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { getPercentage } from '../../helper/number'; -import { getUser } from '../../lib/jwt'; -import type { UserProgress } from '../TeamProgress/TeamProgressPage'; -import { DashboardProgressCard } from './DashboardProgressCard'; - -type ListDashboardProgressProps = { - progresses: UserProgress[]; - isLoading?: boolean; - isCustomResources?: boolean; -}; - -export function ListDashboardProgress(props: ListDashboardProgressProps) { - const { progresses, isLoading = false } = props; - - if (!isLoading && progresses.length === 0) { - return null; - } - - return ( - <> -

- Progress and Bookmarks -

- -
- {isLoading ? ( - <> - {Array.from({ length: 8 }).map((_, index) => ( - - ))} - - ) : ( - <> - {progresses.map((progress) => ( - - ))} - - )} -
- - ); -} - -type DashboardProgressCardSkeletonProps = {}; - -export function DashboardProgressCardSkeleton( - props: DashboardProgressCardSkeletonProps, -) { - return ( -
- ); -} diff --git a/src/components/Dashboard/PersonalDashboard.tsx b/src/components/Dashboard/PersonalDashboard.tsx index a3399906a..7b948e161 100644 --- a/src/components/Dashboard/PersonalDashboard.tsx +++ b/src/components/Dashboard/PersonalDashboard.tsx @@ -2,19 +2,12 @@ import { useEffect, useState, type ReactNode } from 'react'; import { httpGet } from '../../lib/http'; import type { UserProgress } from '../TeamProgress/TeamProgressPage'; import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions'; -import { ResourceProgress } from '../Activity/ResourceProgress'; -import { ProjectProgress } from '../Activity/ProjectProgress'; import type { PageType } from '../CommandMenu/CommandMenu'; import { useToast } from '../../hooks/use-toast'; -import { LoadingProgress } from './LoadingProgress'; -import { ArrowUpRight, Pencil, Plus } from 'lucide-react'; -import { MarkFavorite } from '../FeaturedItems/MarkFavorite'; -import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal'; import { getCurrentPeriod } from '../../lib/date'; -import { ListDashboardProgress } from './ListDashboardProgress'; import { ListDashboardCustomProgress } from './ListDashboardCustomProgress'; -import { DashboardCardLink } from './DashboardCardLink'; import { RecommendedRoadmaps } from './RecommendedRoadmaps'; +import { ProgressStack } from './ProgressStack'; type UserDashboardResponse = { name: string; @@ -48,9 +41,11 @@ export function PersonalDashboard(props: PersonalDashboardProps) { builtInSkillRoadmaps = [], } = props; + const toast = useToast(); const [isLoading, setIsLoading] = useState(true); const [personalDashboardDetails, setPersonalDashboardDetails] = useState(); + const [projectDetails, setProjectDetails] = useState([]); async function loadProgress() { const { response: progressList, error } = @@ -76,8 +71,26 @@ export function PersonalDashboard(props: PersonalDashboardProps) { setPersonalDashboardDetails(progressList); } + async function loadAllProjectDetails() { + const { error, response } = await httpGet(`/pages.json`); + + if (error) { + toast.error(error.message || 'Something went wrong'); + return; + } + + if (!response) { + return []; + } + + const allProjects = response.filter((page) => page.group === 'Projects'); + setProjectDetails(allProjects); + } + useEffect(() => { - loadProgress().finally(() => setIsLoading(false)); + Promise.allSettled([loadProgress(), loadAllProjectDetails()]).finally(() => + setIsLoading(false), + ); }, []); useEffect(() => { @@ -152,6 +165,29 @@ export function PersonalDashboard(props: PersonalDashboardProps) { recommendedRoadmapIds.has(roadmap.id), ); + const enrichedProjects = personalDashboardDetails?.projects + .map((project) => { + const projectDetail = projectDetails.find( + (page) => page.id === project.projectId, + ); + + return { + ...project, + title: projectDetail?.title || 'N/A', + }; + }) + .sort((a, b) => { + if (a.repositoryUrl && !b.repositoryUrl) { + return 1; + } + + if (!a.repositoryUrl && b.repositoryUrl) { + return -1; + } + + return 0; + }); + return (
{isLoading ? ( @@ -212,13 +248,9 @@ export function PersonalDashboard(props: PersonalDashboardProps) { )}
- - - @@ -231,6 +263,11 @@ export function PersonalDashboard(props: PersonalDashboardProps) { isLoading={isLoading} isAIGeneratedRoadmaps={true} /> + + ); } diff --git a/src/components/Dashboard/ProgressStack.tsx b/src/components/Dashboard/ProgressStack.tsx new file mode 100644 index 000000000..818725cc9 --- /dev/null +++ b/src/components/Dashboard/ProgressStack.tsx @@ -0,0 +1,231 @@ +import { + ArrowUpRight, + Bookmark, + Check, + CheckCircle, + CheckIcon, + CircleCheck, + CircleDashed, +} from 'lucide-react'; +import { ResourceProgress } from '../Activity/ResourceProgress'; +import type { UserProgress } from '../TeamProgress/TeamProgressPage'; +import { getPercentage } from '../../helper/number'; +import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions'; +import { DashboardBookmarkCard } from './DashboardBookmarkCard'; +import { DashboardProjectCard } from './DashboardProjectCard'; +import { useState } from 'react'; +import { cn } from '../../lib/classname'; +import { DashboardProgressCard } from './DashboardProgressCard'; + +type ProgressStackProps = { + progresses: UserProgress[]; + projects: (ProjectStatusDocument & { + title: string; + })[]; + isLoading: boolean; +}; + +const MAX_PROGRESS_TO_SHOW = 5; +const MAX_PROJECTS_TO_SHOW = 8; +const MAX_BOOKMARKS_TO_SHOW = 8; + +export function ProgressStack(props: ProgressStackProps) { + const { progresses, projects, isLoading } = props; + + const bookmarkedProgresses = progresses.filter( + (progress) => + progress?.isFavorite && + progress?.done === 0 && + progress?.learning === 0 && + progress?.skipped === 0, + ); + + const userProgresses = progresses.filter((progress) => !progress?.isFavorite); + + const [showAllProgresses, setShowAllProgresses] = useState(false); + const userProgressesToShow = showAllProgresses + ? userProgresses + : userProgresses.slice(0, MAX_PROGRESS_TO_SHOW); + + const [showAllProjects, setShowAllProjects] = useState(false); + const projectsToShow = showAllProjects + ? projects + : projects.slice(0, MAX_PROJECTS_TO_SHOW); + + const [showAllBookmarks, setShowAllBookmarks] = useState(false); + const bookmarksToShow = showAllBookmarks + ? bookmarkedProgresses + : bookmarkedProgresses.slice(0, MAX_BOOKMARKS_TO_SHOW); + + return ( +
+
+

Your Progress

+ +
+ {isLoading ? ( + <> + + + + + + + ) : ( + <> + {userProgressesToShow.map((progress) => { + return ( + + ); + })} + + )} +
+ + {userProgresses.length > MAX_PROGRESS_TO_SHOW && ( + + )} +
+ +
+

Projects

+ +
+ {isLoading ? ( + <> + + + + + + + + + + ) : ( + <> + {projectsToShow.map((project) => { + return ( + + ); + })} + + )} +
+ + {projects.length > MAX_PROJECTS_TO_SHOW && ( + + )} +
+ +
+
+

Bookmarks

+ + + + Explore + +
+ +
+ {isLoading ? ( + <> + + + + + + + + + + ) : ( + <> + {bookmarksToShow.map((progress) => { + return ( + + ); + })} + + )} +
+ + {bookmarkedProgresses.length > MAX_BOOKMARKS_TO_SHOW && ( + + )} +
+
+ ); +} + +type ShowAllButtonProps = { + showAll: boolean; + setShowAll: (showAll: boolean) => void; + count: number; + maxCount: number; + className?: string; +}; + +function ShowAllButton(props: ShowAllButtonProps) { + const { showAll, setShowAll, count, maxCount, className } = props; + + return ( + + ); +} + +type CardSkeletonProps = { + className?: string; +}; + +function CardSkeleton(props: CardSkeletonProps) { + const { className } = props; + + return ( +
+ ); +} diff --git a/src/components/Dashboard/RecommendedRoadmaps.tsx b/src/components/Dashboard/RecommendedRoadmaps.tsx index 1c18338e5..73c380171 100644 --- a/src/components/Dashboard/RecommendedRoadmaps.tsx +++ b/src/components/Dashboard/RecommendedRoadmaps.tsx @@ -11,9 +11,19 @@ export function RecommendedRoadmaps(props: RecommendedRoadmapsProps) { return ( <> -

- Recommended Roadmaps -

+
+

+ Recommended Roadmaps +

+ + + + All Roadmaps + +
{isLoading ? (