feat: update dashboard design

feat/dashboard
Arik Chakma 3 months ago
parent 57e957198e
commit 3a5fdb656f
  1. 38
      src/components/Dashboard/DashboardBookmarkCard.tsx
  2. 64
      src/components/Dashboard/DashboardCustomProgressCard.tsx
  3. 23
      src/components/Dashboard/DashboardProgressCard.tsx
  4. 34
      src/components/Dashboard/DashboardProjectCard.tsx
  5. 17
      src/components/Dashboard/ListDashboardCustomProgress.tsx
  6. 55
      src/components/Dashboard/ListDashboardProgress.tsx
  7. 67
      src/components/Dashboard/PersonalDashboard.tsx
  8. 231
      src/components/Dashboard/ProgressStack.tsx
  9. 12
      src/components/Dashboard/RecommendedRoadmaps.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 (
<a
href={url}
key={resourceId}
className="group relative flex w-full items-center gap-2 text-left text-sm"
>
<Bookmark className="size-4 fill-current text-gray-500 group-hover:text-gray-600" />
<h4 className="truncate font-medium text-gray-900 group-hover:text-gray-600">
{resourceTitle}
</h4>
</a>
);
}

@ -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 (
<a
href={url}
className="group relative flex min-h-[80px] w-full flex-col justify-between overflow-hidden rounded-md border bg-white p-3 text-left text-sm shadow-sm transition-all hover:border-gray-300"
>
<h4 className="truncate font-medium text-gray-900">{resourceTitle}</h4>
<div className="mt-6 flex items-center gap-2">
<div className="h-2 w-full overflow-hidden rounded-md bg-black/10">
<div
className="h-full bg-black/20"
style={{ width: `${progressPercentage}%` }}
></div>
</div>
<span className="text-xs text-gray-500">
{Math.floor(+progressPercentage)}%
</span>
</div>
<p className="mt-1 text-xs text-gray-400">
{isCustomResource ? (
<>Last updated {getRelativeTimeString(updatedAt)}</>
) : (
<>Last practiced {getRelativeTimeString(updatedAt)}</>
)}
</p>
</a>
);
}

@ -1,5 +1,4 @@
import { getPercentage } from '../../helper/number'; import { getPercentage } from '../../helper/number';
import { getRelativeTimeString } from '../../lib/date';
import type { UserProgress } from '../TeamProgress/TeamProgressPage'; import type { UserProgress } from '../TeamProgress/TeamProgressPage';
type DashboardProgressCardProps = { type DashboardProgressCardProps = {
@ -8,7 +7,6 @@ type DashboardProgressCardProps = {
export function DashboardProgressCard(props: DashboardProgressCardProps) { export function DashboardProgressCard(props: DashboardProgressCardProps) {
const { progress } = props; const { progress } = props;
const { const {
resourceType, resourceType,
resourceId, resourceId,
@ -36,29 +34,24 @@ export function DashboardProgressCard(props: DashboardProgressCardProps) {
return ( return (
<a <a
href={url} href={url}
className="group relative flex min-h-[80px] w-full flex-col justify-between overflow-hidden rounded-md border bg-white p-3 text-left text-sm shadow-sm transition-all hover:border-gray-300" key={resourceId}
className="group relative flex w-full flex-col justify-between overflow-hidden text-left text-sm"
> >
<h4 className="truncate font-medium text-gray-900">{resourceTitle}</h4> <h4 className="truncate font-medium text-gray-900 group-hover:text-gray-500">
{resourceTitle}
</h4>
<div className="mt-6 flex items-center gap-2"> <div className="mt-1 flex items-center gap-2">
<div className="h-2 w-full overflow-hidden rounded-md bg-black/10"> <div className="h-1.5 w-full overflow-hidden rounded-md bg-black/10">
<div <div
className="h-full bg-black/20" className="h-full bg-black/20"
style={{ width: `${progressPercentage}%` }} style={{ width: `${progressPercentage}%` }}
></div> ></div>
</div> </div>
<span className="text-xs text-gray-500"> <span className="min-w-7 text-xs text-gray-500">
{Math.floor(+progressPercentage)}% {Math.floor(+progressPercentage)}%
</span> </span>
</div> </div>
<p className="mt-1 text-xs text-gray-400">
{isCustomResource ? (
<>Last updated {getRelativeTimeString(updatedAt)}</>
) : (
<>Last practiced {getRelativeTimeString(updatedAt)}</>
)}
</p>
</a> </a>
); );
} }

@ -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 (
<a
href={url}
key={projectId}
className="group relative flex w-full items-center gap-2 text-left text-sm"
>
{status === 'submitted' ? (
<CircleCheck className="size-4" />
) : (
<CircleDashed className="size-4" />
)}
<h4 className="truncate font-medium text-gray-900 group-hover:text-gray-600">
{title}
</h4>
</a>
);
}

@ -1,6 +1,5 @@
import type { UserProgress } from '../TeamProgress/TeamProgressPage'; import type { UserProgress } from '../TeamProgress/TeamProgressPage';
import { DashboardProgressCard } from './DashboardProgressCard'; import { DashboardCustomProgressCard } from './DashboardCustomProgressCard';
import { DashboardProgressCardSkeleton } from './ListDashboardProgress';
import { DashboardCardLink } from './DashboardCardLink'; import { DashboardCardLink } from './DashboardCardLink';
import { useState } from 'react'; import { useState } from 'react';
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal'; import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal';
@ -67,13 +66,13 @@ export function ListDashboardCustomProgress(
{isLoading ? ( {isLoading ? (
<> <>
{Array.from({ length: 8 }).map((_, index) => ( {Array.from({ length: 8 }).map((_, index) => (
<DashboardProgressCardSkeleton key={index} /> <CustomProgressCardSkeleton key={index} />
))} ))}
</> </>
) : ( ) : (
<> <>
{progresses.map((progress) => ( {progresses.map((progress) => (
<DashboardProgressCard <DashboardCustomProgressCard
key={progress.resourceId} key={progress.resourceId}
progress={progress} progress={progress}
/> />
@ -97,3 +96,13 @@ export function ListDashboardCustomProgress(
</> </>
); );
} }
type CustomProgressCardSkeletonProps = {};
export function CustomProgressCardSkeleton(
props: CustomProgressCardSkeletonProps,
) {
return (
<div className="h-[106px] w-full animate-pulse rounded-md bg-gray-200" />
);
}

@ -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 (
<>
<h2 className="mb-3 mt-8 text-xs uppercase text-gray-400">
Progress and Bookmarks
</h2>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-4">
{isLoading ? (
<>
{Array.from({ length: 8 }).map((_, index) => (
<DashboardProgressCardSkeleton key={index} />
))}
</>
) : (
<>
{progresses.map((progress) => (
<DashboardProgressCard
key={progress.resourceId}
progress={progress}
/>
))}
</>
)}
</div>
</>
);
}
type DashboardProgressCardSkeletonProps = {};
export function DashboardProgressCardSkeleton(
props: DashboardProgressCardSkeletonProps,
) {
return (
<div className="h-[106px] w-full animate-pulse rounded-md bg-gray-200" />
);
}

@ -2,19 +2,12 @@ import { useEffect, useState, type ReactNode } from 'react';
import { httpGet } from '../../lib/http'; import { httpGet } from '../../lib/http';
import type { UserProgress } from '../TeamProgress/TeamProgressPage'; import type { UserProgress } from '../TeamProgress/TeamProgressPage';
import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions'; import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions';
import { ResourceProgress } from '../Activity/ResourceProgress';
import { ProjectProgress } from '../Activity/ProjectProgress';
import type { PageType } from '../CommandMenu/CommandMenu'; import type { PageType } from '../CommandMenu/CommandMenu';
import { useToast } from '../../hooks/use-toast'; 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 { getCurrentPeriod } from '../../lib/date';
import { ListDashboardProgress } from './ListDashboardProgress';
import { ListDashboardCustomProgress } from './ListDashboardCustomProgress'; import { ListDashboardCustomProgress } from './ListDashboardCustomProgress';
import { DashboardCardLink } from './DashboardCardLink';
import { RecommendedRoadmaps } from './RecommendedRoadmaps'; import { RecommendedRoadmaps } from './RecommendedRoadmaps';
import { ProgressStack } from './ProgressStack';
type UserDashboardResponse = { type UserDashboardResponse = {
name: string; name: string;
@ -48,9 +41,11 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
builtInSkillRoadmaps = [], builtInSkillRoadmaps = [],
} = props; } = props;
const toast = useToast();
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [personalDashboardDetails, setPersonalDashboardDetails] = const [personalDashboardDetails, setPersonalDashboardDetails] =
useState<UserDashboardResponse>(); useState<UserDashboardResponse>();
const [projectDetails, setProjectDetails] = useState<PageType[]>([]);
async function loadProgress() { async function loadProgress() {
const { response: progressList, error } = const { response: progressList, error } =
@ -76,8 +71,26 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
setPersonalDashboardDetails(progressList); setPersonalDashboardDetails(progressList);
} }
async function loadAllProjectDetails() {
const { error, response } = await httpGet<PageType[]>(`/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(() => { useEffect(() => {
loadProgress().finally(() => setIsLoading(false)); Promise.allSettled([loadProgress(), loadAllProjectDetails()]).finally(() =>
setIsLoading(false),
);
}, []); }, []);
useEffect(() => { useEffect(() => {
@ -152,6 +165,29 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
recommendedRoadmapIds.has(roadmap.id), 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 ( return (
<section> <section>
{isLoading ? ( {isLoading ? (
@ -212,13 +248,9 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
)} )}
</div> </div>
<ListDashboardProgress <ProgressStack
progresses={learningRoadmapsToShow} progresses={learningRoadmapsToShow}
isLoading={isLoading} projects={enrichedProjects || []}
/>
<RecommendedRoadmaps
roadmaps={recommendedRoadmaps}
isLoading={isLoading} isLoading={isLoading}
/> />
@ -231,6 +263,11 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
isLoading={isLoading} isLoading={isLoading}
isAIGeneratedRoadmaps={true} isAIGeneratedRoadmaps={true}
/> />
<RecommendedRoadmaps
roadmaps={recommendedRoadmaps}
isLoading={isLoading}
/>
</section> </section>
); );
} }

@ -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 (
<div className="mt-2 grid min-h-[330px] grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
<div className="h-full rounded-md border bg-white p-4 shadow-sm">
<h3 className="text-xs uppercase text-gray-500">Your Progress</h3>
<div className="mt-4 flex flex-col gap-2">
{isLoading ? (
<>
<CardSkeleton />
<CardSkeleton />
<CardSkeleton />
<CardSkeleton />
<CardSkeleton />
</>
) : (
<>
{userProgressesToShow.map((progress) => {
return (
<DashboardProgressCard
key={progress.resourceId}
progress={progress}
/>
);
})}
</>
)}
</div>
{userProgresses.length > MAX_PROGRESS_TO_SHOW && (
<ShowAllButton
showAll={showAllProgresses}
setShowAll={setShowAllProgresses}
count={userProgresses.length}
maxCount={MAX_PROGRESS_TO_SHOW}
className="mt-3"
/>
)}
</div>
<div className="h-full rounded-md border bg-white p-4 shadow-sm">
<h3 className="text-xs uppercase text-gray-500">Projects</h3>
<div className="mt-4 flex flex-col gap-2.5">
{isLoading ? (
<>
<CardSkeleton className="h-5" />
<CardSkeleton className="h-5" />
<CardSkeleton className="h-5" />
<CardSkeleton className="h-5" />
<CardSkeleton className="h-5" />
<CardSkeleton className="h-5" />
<CardSkeleton className="h-5" />
<CardSkeleton className="h-5" />
</>
) : (
<>
{projectsToShow.map((project) => {
return (
<DashboardProjectCard
key={project.projectId}
project={project}
/>
);
})}
</>
)}
</div>
{projects.length > MAX_PROJECTS_TO_SHOW && (
<ShowAllButton
showAll={showAllProjects}
setShowAll={setShowAllProjects}
count={projects.length}
maxCount={MAX_PROJECTS_TO_SHOW}
className="mt-3"
/>
)}
</div>
<div className="h-full rounded-md border bg-white p-4 shadow-sm">
<div className="flex items-center justify-between gap-2">
<h3 className="text-xs uppercase text-gray-500">Bookmarks</h3>
<a
href="/roadmaps"
className="flex items-center gap-1 text-xs text-gray-500"
>
<ArrowUpRight size={12} />
Explore
</a>
</div>
<div className="mt-4 flex flex-col gap-2.5">
{isLoading ? (
<>
<CardSkeleton className="h-5" />
<CardSkeleton className="h-5" />
<CardSkeleton className="h-5" />
<CardSkeleton className="h-5" />
<CardSkeleton className="h-5" />
<CardSkeleton className="h-5" />
<CardSkeleton className="h-5" />
<CardSkeleton className="h-5" />
</>
) : (
<>
{bookmarksToShow.map((progress) => {
return (
<DashboardBookmarkCard
key={progress.resourceId}
bookmark={progress}
/>
);
})}
</>
)}
</div>
{bookmarkedProgresses.length > MAX_BOOKMARKS_TO_SHOW && (
<ShowAllButton
showAll={showAllBookmarks}
setShowAll={setShowAllBookmarks}
count={bookmarkedProgresses.length}
maxCount={MAX_BOOKMARKS_TO_SHOW}
className="mt-3"
/>
)}
</div>
</div>
);
}
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 (
<button
className={cn(
'flex w-full items-center justify-center text-sm text-gray-500 hover:text-gray-700',
className,
)}
onClick={() => setShowAll(!showAll)}
>
{!showAll ? <>+ show {count - maxCount} more</> : <>- show less</>}
</button>
);
}
type CardSkeletonProps = {
className?: string;
};
function CardSkeleton(props: CardSkeletonProps) {
const { className } = props;
return (
<div
className={cn(
'h-10 w-full animate-pulse rounded-md bg-gray-100',
className,
)}
/>
);
}

@ -11,10 +11,20 @@ export function RecommendedRoadmaps(props: RecommendedRoadmapsProps) {
return ( return (
<> <>
<h2 className="mb-3 mt-8 text-xs uppercase text-gray-400"> <div className="mb-3 mt-8 flex items-center justify-between gap-2">
<h2 className="text-xs uppercase text-gray-400">
Recommended Roadmaps Recommended Roadmaps
</h2> </h2>
<a
href="/roadmaps"
className="flex items-center gap-1 text-xs text-gray-500"
>
<ArrowUpRight size={12} />
All Roadmaps
</a>
</div>
{isLoading ? ( {isLoading ? (
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-2 md:grid-cols-3"> <div className="grid grid-cols-1 gap-1.5 sm:grid-cols-2 md:grid-cols-3">
{Array.from({ length: 12 }).map((_, index) => ( {Array.from({ length: 12 }).map((_, index) => (

Loading…
Cancel
Save