diff --git a/src/components/Dashboard/DashboardCardLink.tsx b/src/components/Dashboard/DashboardCardLink.tsx new file mode 100644 index 000000000..654281261 --- /dev/null +++ b/src/components/Dashboard/DashboardCardLink.tsx @@ -0,0 +1,28 @@ +import { ArrowUpRight } from 'lucide-react'; +import { cn } from '../../lib/classname'; + +type DashboardCardLinkProps = { + href: string; + title: string; + description: string; + className?: string; +}; + +export function DashboardCardLink(props: DashboardCardLinkProps) { + const { href, title, description, className } = props; + + return ( + +

{title}

+

{description}

+ +
+ ); +} diff --git a/src/components/Dashboard/DashboardPage.tsx b/src/components/Dashboard/DashboardPage.tsx index b62e6c2f7..44eca404b 100644 --- a/src/components/Dashboard/DashboardPage.tsx +++ b/src/components/Dashboard/DashboardPage.tsx @@ -23,100 +23,19 @@ export function DashboardPage(props: DashboardPageProps) { const toast = useToast(); const teamList = useStore($teamList); - const [isLoading, setIsLoading] = useState(true); - const [selectedTeamId, setSelectedTeamId] = useState(); - - async function getAllTeams() { - if (teamList.length > 0) { - return; - } - - const { response, error } = await httpGet( - `${import.meta.env.PUBLIC_API_URL}/v1-get-user-teams`, - ); - if (error || !response) { - toast.error(error?.message || 'Something went wrong'); - return; - } - - $teamList.set(response); - } - - useEffect(() => { - getAllTeams().finally(() => setIsLoading(false)); - }, []); - - const userAvatar = - currentUser?.avatar && !isLoading - ? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${currentUser.avatar}` - : '/images/default-avatar.png'; + const userAvatar = currentUser?.avatar + ? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${currentUser.avatar}` + : '/images/default-avatar.png'; return ( -
-
- setSelectedTeamId(undefined)} - avatar={userAvatar} - /> - {isLoading && ( - <> - - - - - )} - - {!isLoading && ( - <> - {teamList.map((team) => { - const { avatar } = team; - const avatarUrl = avatar - ? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${avatar}` - : '/images/default-avatar.png'; - return ( - { - setSelectedTeamId(team._id); - }, - })} - avatar={avatarUrl} - /> - ); - })} - - - )} -
- - {!selectedTeamId && ( +
+
- )} - {selectedTeamId && } +
); } - -function DashboardTabLoading() { - return ( -
- ); -} diff --git a/src/components/Dashboard/DashboardProgressCard.tsx b/src/components/Dashboard/DashboardProgressCard.tsx new file mode 100644 index 000000000..b2041c523 --- /dev/null +++ b/src/components/Dashboard/DashboardProgressCard.tsx @@ -0,0 +1,49 @@ +import { getPercentage } from '../../helper/number'; +import type { UserProgress } from '../TeamProgress/TeamProgressPage'; + +type DashboardProgressCardProps = { + progress: UserProgress; +}; + +export function DashboardProgressCard(props: DashboardProgressCardProps) { + const { progress } = props; + + const { + resourceType, + resourceId, + resourceTitle, + total: totalCount, + done: doneCount, + skipped: skippedCount, + roadmapSlug, + isCustomResource, + } = 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}

+ +
+
+
+
+ ); +} diff --git a/src/components/Dashboard/ListDashboardCustomProgress.tsx b/src/components/Dashboard/ListDashboardCustomProgress.tsx new file mode 100644 index 000000000..d2638c81f --- /dev/null +++ b/src/components/Dashboard/ListDashboardCustomProgress.tsx @@ -0,0 +1,78 @@ +import type { UserProgress } from '../TeamProgress/TeamProgressPage'; +import { DashboardProgressCard } from './DashboardProgressCard'; +import { DashboardProgressCardSkeleton } from './ListDashboardProgress'; +import { DashboardCardLink } from './DashboardCardLink'; +import { useState } from 'react'; +import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal'; + +type ListDashboardCustomProgressProps = { + progresses: UserProgress[]; + isLoading?: boolean; + isCustomResources?: boolean; +}; + +export function ListDashboardCustomProgress( + props: ListDashboardCustomProgressProps, +) { + const { progresses, isLoading = false } = props; + const [isCreateCustomRoadmapModalOpen, setIsCreateCustomRoadmapModalOpen] = + useState(false); + + if (!isLoading && progresses.length === 0) { + return ( + + ); + } + + const customRoadmapModal = isCreateCustomRoadmapModalOpen ? ( + setIsCreateCustomRoadmapModalOpen(false)} + onCreated={(roadmap) => { + window.location.href = `${ + import.meta.env.PUBLIC_EDITOR_APP_URL + }/${roadmap?._id}`; + return; + }} + /> + ) : null; + + return ( + <> + {customRoadmapModal} +

+ Custom Roadmaps +

+ + {isLoading ? ( +
+ {Array.from({ length: 8 }).map((_, index) => ( + + ))} +
+ ) : ( +
+ {progresses.map((progress) => ( + + ))} + + +
+ )} + + ); +} diff --git a/src/components/Dashboard/ListDashboardProgress.tsx b/src/components/Dashboard/ListDashboardProgress.tsx new file mode 100644 index 000000000..258d7c665 --- /dev/null +++ b/src/components/Dashboard/ListDashboardProgress.tsx @@ -0,0 +1,53 @@ +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 038d12aa1..073e4510c 100644 --- a/src/components/Dashboard/PersonalDashboard.tsx +++ b/src/components/Dashboard/PersonalDashboard.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState, type ReactNode } from 'react'; import { httpGet } from '../../lib/http'; import type { UserProgress } from '../TeamProgress/TeamProgressPage'; import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions'; @@ -10,6 +10,11 @@ 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'; type UserDashboardResponse = { name: string; @@ -27,6 +32,7 @@ export type BuiltInRoadmap = { title: string; description: string; isFavorite?: boolean; + relatedRoadmapIds?: string[]; }; type PersonalDashboardProps = { @@ -44,8 +50,6 @@ export function PersonalDashboard(props: PersonalDashboardProps) { const toast = useToast(); const [isLoading, setIsLoading] = useState(true); - const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false); - const [projectDetails, setProjectDetails] = useState([]); const [personalDashboardDetails, setPersonalDashboardDetails] = useState(); @@ -73,26 +77,8 @@ 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(() => { - Promise.allSettled([loadProgress(), loadAllProjectDetails()]).finally(() => - setIsLoading(false), - ); + loadProgress().finally(() => setIsLoading(false)); }, []); useEffect(() => { @@ -125,294 +111,136 @@ export function PersonalDashboard(props: PersonalDashboardProps) { return updatedAtB.getTime() - updatedAtA.getTime(); }); - 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) => { - const isPendingA = !a.repositoryUrl && !a.submittedAt; - const isPendingB = !b.repositoryUrl && !b.submittedAt; - - if (isPendingA && !isPendingB) { - return -1; - } - - if (!isPendingA && isPendingB) { - return 1; - } - - return 0; - }) || []; - const { avatar, name, headline, email, username } = personalDashboardDetails || {}; const avatarLink = avatar ? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${avatar}` : '/images/default-avatar.png'; + const currentPeriod = getCurrentPeriod(); + + const relatedRoadmapIds = [...builtInRoleRoadmaps, ...builtInSkillRoadmaps] + .filter((roadmap) => + learningRoadmapsToShow?.some( + (learningRoadmap) => learningRoadmap.resourceId === roadmap.id, + ), + ) + .flatMap((roadmap) => roadmap.relatedRoadmapIds) + .filter(Boolean); + + const recommendedRoadmapIds = new Set( + relatedRoadmapIds.length === 0 + ? ['frontend', 'backend', 'devops', 'ai-data-scientist', 'full-stack'] + : relatedRoadmapIds, + ); + + const recommendedRoadmaps = [ + ...builtInRoleRoadmaps, + ...builtInSkillRoadmaps, + ].filter((roadmap) => recommendedRoadmapIds.has(roadmap.id)); + return ( -
- {isCreatingRoadmap && ( - { - setIsCreatingRoadmap(false); - }} - /> +
+ {isLoading ? ( +
+ ) : ( +

+ Hi {name}, good {currentPeriod}! +

)} - {isLoading && ( -
- )} - {!isLoading && ( - - )} - -

- Progress and Bookmarks -

- {isLoading && } - {!isLoading && learningRoadmapsToShow.length > 0 && ( -
- {learningRoadmapsToShow.map((roadmap) => { - const learningCount = roadmap.learning || 0; - const doneCount = roadmap.done || 0; - const totalCount = roadmap.total || 0; - const skippedCount = roadmap.skipped || 0; - - return ( - totalCount ? totalCount : doneCount} - learningCount={ - learningCount > totalCount ? totalCount : learningCount - } - totalCount={totalCount} - skippedCount={skippedCount} - resourceId={roadmap.resourceId} - resourceType={roadmap.resourceType} - updatedAt={roadmap.updatedAt} - title={roadmap.resourceTitle} - showActions={true} - roadmapSlug={roadmap.roadmapSlug} - /> - ); - })} -
- )} - -

- Custom Roadmaps -

- {isLoading && } - {!isLoading && customRoadmaps.length > 0 && ( -
- {customRoadmaps.map((roadmap) => { - const learningCount = roadmap.learning || 0; - const doneCount = roadmap.done || 0; - const totalCount = roadmap.total || 0; - const skippedCount = roadmap.skipped || 0; - return ( - totalCount ? totalCount : doneCount} - learningCount={ - learningCount > totalCount ? totalCount : learningCount - } - totalCount={totalCount} - skippedCount={skippedCount} - resourceId={roadmap.resourceId} - resourceType={roadmap.resourceType} - updatedAt={roadmap.updatedAt} - title={roadmap.resourceTitle} - showActions={true} - roadmapSlug={roadmap.roadmapSlug} - /> - ); - })} - - -
- )} -{!isLoading && customRoadmaps.length === 0 && ( -
- No custom roadmaps found. - - Start  - - . - -
- )} - - -

My Projects

- {isLoading && } - {!isLoading && enrichedProjects.length > 0 && ( -
- {enrichedProjects.map((project) => { - return ( - - ); - })} -
- )} - {!isLoading && enrichedProjects.length === 0 && ( -
- No projects found. - - Start  - - Backend Projects - - . - -
- )} - -

- Role Based Roadmaps -

- - -

- Skill Based Roadmaps -

- +
+

{name}

+

Setup your profile

+
+ + )} + + + + +
-

- Best Practices -

- + + + + + +
); } -type ListRoadmapsProps = { - roadmaps: BuiltInRoadmap[]; +type DashboardCardProps = { + icon: string | ReactNode; + title: string; + description: string; + href: string; }; -export function ListRoadmaps(props: ListRoadmapsProps) { - const { roadmaps } = props; - const [showAll, setShowAll] = useState(roadmaps.length <= 12); - const roadmapsToShow = showAll ? roadmaps : roadmaps.slice(0, 12); - - const [isMounted, setIsMounted] = useState(false); - useEffect(() => { - setIsMounted(true); - }, []); +function DashboardCard(props: DashboardCardProps) { + const { icon, title, description, href } = props; return ( -
-
- {roadmapsToShow.map((roadmap) => ( -
- - {roadmap.title} - - - {isMounted && ( - - )} -
- ))} + +
+ + {icon} +
- {!showAll && ( -
- -
- )} -
+
+

{title}

+

{description}

+
+
); } diff --git a/src/components/Dashboard/RecommendedRoadmaps.tsx b/src/components/Dashboard/RecommendedRoadmaps.tsx new file mode 100644 index 000000000..9b587ea73 --- /dev/null +++ b/src/components/Dashboard/RecommendedRoadmaps.tsx @@ -0,0 +1,92 @@ +import { useEffect, useState } from 'react'; +import type { BuiltInRoadmap } from './PersonalDashboard'; +import { MarkFavorite } from '../FeaturedItems/MarkFavorite'; + +type RecommendedRoadmapsProps = { + roadmaps: BuiltInRoadmap[]; + isLoading: boolean; +}; + +export function RecommendedRoadmaps(props: RecommendedRoadmapsProps) { + const { roadmaps, isLoading } = props; + + const [showAll, setShowAll] = useState(false); + const roadmapsToShow = showAll ? roadmaps : roadmaps.slice(0, 12); + + const [isMounted, setIsMounted] = useState(false); + + useEffect(() => { + setIsMounted(true); + }, []); + + useEffect(() => { + setShowAll(roadmaps.length < 12); + }, [roadmaps]); + + return ( + <> +

+ Recommended Roadmaps +

+ + {isLoading ? ( +
+ {Array.from({ length: 12 }).map((_, index) => ( + + ))} +
+ ) : ( +
+
+ {roadmapsToShow.map((roadmap) => ( +
+ + {roadmap.title} + + + {isMounted && ( + + )} +
+ ))} +
+ + {!showAll && ( +
+ +
+ )} +
+ )} + + ); +} + +function RecommendedCardSkeleton() { + return ( +
+ ); +} diff --git a/src/lib/date.ts b/src/lib/date.ts index 2adbc3a86..45b0b25c2 100644 --- a/src/lib/date.ts +++ b/src/lib/date.ts @@ -65,3 +65,15 @@ export function formatActivityDate(date: string): string { day: 'numeric', }); } + +export function getCurrentPeriod() { + const now = new Date(); + const hour = now.getHours(); + if (hour < 12) { + return 'morning'; + } else if (hour < 18) { + return 'afternoon'; + } else { + return 'evening'; + } +} diff --git a/src/pages/dashboard.astro b/src/pages/dashboard.astro index 01e2749fb..242e651a6 100644 --- a/src/pages/dashboard.astro +++ b/src/pages/dashboard.astro @@ -18,6 +18,7 @@ const enrichedRoleRoadmaps = roleRoadmaps url: `/${roadmap.id}`, title: frontmatter.briefTitle, description: frontmatter.briefDescription, + relatedRoadmapIds: frontmatter.relatedRoadmaps, }; }); const enrichedSkillRoadmaps = skillRoadmaps @@ -31,6 +32,7 @@ const enrichedSkillRoadmaps = skillRoadmaps title: frontmatter.briefTitle === 'Go' ? 'Go Roadmap' : frontmatter.briefTitle, description: frontmatter.briefDescription, + relatedRoadmapIds: frontmatter.relatedRoadmaps, }; });