feat: update dashboard design

feat/dashboard
Arik Chakma 6 months ago
parent fa73fcfd2f
commit 57e957198e
  1. 9
      src/components/Dashboard/DashboardPage.tsx
  2. 27
      src/components/Dashboard/DashboardProgressCard.tsx
  3. 2
      src/components/Dashboard/DashboardTab.tsx
  4. 75
      src/components/Dashboard/ListDashboardCustomProgress.tsx
  5. 36
      src/components/Dashboard/ListDashboardProgress.tsx
  6. 132
      src/components/Dashboard/PersonalDashboard.tsx
  7. 88
      src/components/Dashboard/RecommendedRoadmaps.tsx
  8. 3
      src/components/TeamProgress/TeamProgressPage.tsx

@ -46,9 +46,10 @@ export function DashboardPage(props: DashboardPageProps) {
getAllTeams().finally(() => setIsLoading(false)); getAllTeams().finally(() => setIsLoading(false));
}, []); }, []);
const userAvatar = currentUser?.avatar const userAvatar =
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${currentUser.avatar}` currentUser?.avatar && !isLoading
: '/images/default-avatar.png'; ? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${currentUser.avatar}`
: '/images/default-avatar.png';
return ( return (
<div className="min-h-screen bg-gray-50 pb-20 pt-8"> <div className="min-h-screen bg-gray-50 pb-20 pt-8">
@ -118,6 +119,6 @@ export function DashboardPage(props: DashboardPageProps) {
function DashboardTabSkeleton() { function DashboardTabSkeleton() {
return ( return (
<div className="h-[30px] w-20 animate-pulse rounded-md border bg-gray-100"></div> <div className="h-[30px] w-20 animate-pulse rounded-md border bg-white"></div>
); );
} }

@ -1,4 +1,5 @@
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 = {
@ -17,6 +18,7 @@ export function DashboardProgressCard(props: DashboardProgressCardProps) {
skipped: skippedCount, skipped: skippedCount,
roadmapSlug, roadmapSlug,
isCustomResource, isCustomResource,
updatedAt,
} = progress; } = progress;
let url = let url =
@ -34,16 +36,29 @@ 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 border-gray-300 bg-white p-3 text-left text-sm transition-all hover:border-gray-400" 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> <h4 className="truncate font-medium text-gray-900">{resourceTitle}</h4>
<div className="h-2 w-full overflow-hidden rounded-md bg-black/10"> <div className="mt-6 flex items-center gap-2">
<div <div className="h-2 w-full overflow-hidden rounded-md bg-black/10">
className="h-full bg-black/20" <div
style={{ width: `${progressPercentage}%` }} className="h-full bg-black/20"
></div> style={{ width: `${progressPercentage}%` }}
></div>
</div>
<span className="text-xs text-gray-500">
{Math.floor(+progressPercentage)}%
</span>
</div> </div>
<p className="mt-1 text-xs text-gray-400">
{isCustomResource ? (
<>Last updated {getRelativeTimeString(updatedAt)}</>
) : (
<>Last practiced {getRelativeTimeString(updatedAt)}</>
)}
</p>
</a> </a>
); );
} }

@ -20,7 +20,7 @@ export function DashboardTab(props: DashboardTabProps) {
<Slot <Slot
onClick={onClick} onClick={onClick}
className={cn( className={cn(
'flex shrink-0 items-center h-[30px] gap-1 rounded-md border p-1.5 px-2 text-sm leading-none text-gray-600', 'flex h-[30px] shrink-0 items-center gap-1 rounded-md border bg-white p-1.5 px-2 text-sm leading-none text-gray-600',
isActive ? 'border-gray-500 bg-gray-200 text-gray-900' : '', isActive ? 'border-gray-500 bg-gray-200 text-gray-900' : '',
className, className,
)} )}

@ -9,17 +9,29 @@ type ListDashboardCustomProgressProps = {
progresses: UserProgress[]; progresses: UserProgress[];
isLoading?: boolean; isLoading?: boolean;
isCustomResources?: boolean; isCustomResources?: boolean;
isAIGeneratedRoadmaps?: boolean;
}; };
export function ListDashboardCustomProgress( export function ListDashboardCustomProgress(
props: ListDashboardCustomProgressProps, props: ListDashboardCustomProgressProps,
) { ) {
const { progresses, isLoading = false } = props; const {
progresses,
isLoading = false,
isAIGeneratedRoadmaps = false,
} = props;
const [isCreateCustomRoadmapModalOpen, setIsCreateCustomRoadmapModalOpen] = const [isCreateCustomRoadmapModalOpen, setIsCreateCustomRoadmapModalOpen] =
useState(false); useState(false);
if (!isLoading && progresses.length === 0) { if (!isLoading && progresses.length === 0) {
return ( return isAIGeneratedRoadmaps ? (
<DashboardCardLink
className="mt-8"
href="/ai"
title="Generate Roadmaps with AI"
description="You can generate your own roadmap with AI"
/>
) : (
<DashboardCardLink <DashboardCardLink
className="mt-8" className="mt-8"
href="https://draw.roadmap.sh" href="https://draw.roadmap.sh"
@ -41,38 +53,47 @@ export function ListDashboardCustomProgress(
/> />
) : null; ) : null;
const Slot = isAIGeneratedRoadmaps ? 'a' : 'button';
return ( return (
<> <>
{customRoadmapModal} {customRoadmapModal}
<h2 className="mb-3 mt-8 text-xs uppercase text-gray-400"> <h2 className="mb-3 mt-8 text-xs uppercase text-gray-400">
Custom Roadmaps {isAIGeneratedRoadmaps ? 'AI Generated Roadmaps' : 'Custom Roadmaps'}
</h2> </h2>
{isLoading ? ( <div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-4">
<div className="grid grid-cols-4 gap-2"> {isLoading ? (
{Array.from({ length: 8 }).map((_, index) => ( <>
<DashboardProgressCardSkeleton key={index} /> {Array.from({ length: 8 }).map((_, index) => (
))} <DashboardProgressCardSkeleton key={index} />
</div> ))}
) : ( </>
<div className="grid grid-cols-4 gap-2"> ) : (
{progresses.map((progress) => ( <>
<DashboardProgressCard {progresses.map((progress) => (
key={progress.resourceId} <DashboardProgressCard
progress={progress} key={progress.resourceId}
/> progress={progress}
))} />
))}
<button <Slot
className="flex min-h-[80px] items-center justify-center rounded-lg border border-dashed border-gray-300 bg-white p-4 text-sm font-medium text-gray-500 hover:bg-gray-50 hover:text-gray-600" className="flex min-h-[80px] items-center justify-center rounded-lg border border-dashed border-gray-300 bg-white p-4 text-sm font-medium text-gray-500 hover:bg-gray-50 hover:text-gray-600"
onClick={() => { {...(isAIGeneratedRoadmaps
setIsCreateCustomRoadmapModalOpen(true); ? { href: '/ai' }
}} : {
> onClick: () => {
+ Create New setIsCreateCustomRoadmapModalOpen(true);
</button> },
</div> })}
)} >
{isAIGeneratedRoadmaps ? '+ Generate New' : '+ Create New'}
</Slot>
</>
)}
</div>
</> </>
); );
} }

@ -22,22 +22,24 @@ export function ListDashboardProgress(props: ListDashboardProgressProps) {
Progress and Bookmarks Progress and Bookmarks
</h2> </h2>
{isLoading ? ( <div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-4">
<div className="grid grid-cols-4 gap-2"> {isLoading ? (
{Array.from({ length: 8 }).map((_, index) => ( <>
<DashboardProgressCardSkeleton key={index} /> {Array.from({ length: 8 }).map((_, index) => (
))} <DashboardProgressCardSkeleton key={index} />
</div> ))}
) : ( </>
<div className="grid grid-cols-4 gap-2"> ) : (
{progresses.map((progress) => ( <>
<DashboardProgressCard {progresses.map((progress) => (
key={progress.resourceId} <DashboardProgressCard
progress={progress} key={progress.resourceId}
/> progress={progress}
))} />
</div> ))}
)} </>
)}
</div>
</> </>
); );
} }
@ -48,6 +50,6 @@ export function DashboardProgressCardSkeleton(
props: DashboardProgressCardSkeletonProps, props: DashboardProgressCardSkeletonProps,
) { ) {
return ( return (
<div className="h-[80px] w-full animate-pulse rounded-md bg-gray-200" /> <div className="h-[106px] w-full animate-pulse rounded-md bg-gray-200" />
); );
} }

@ -47,7 +47,6 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
builtInBestPractices = [], builtInBestPractices = [],
builtInSkillRoadmaps = [], builtInSkillRoadmaps = [],
} = props; } = props;
const toast = useToast();
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [personalDashboardDetails, setPersonalDashboardDetails] = const [personalDashboardDetails, setPersonalDashboardDetails] =
@ -111,22 +110,37 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
return updatedAtB.getTime() - updatedAtA.getTime(); return updatedAtB.getTime() - updatedAtA.getTime();
}); });
const { avatar, name, headline, email, username } = const aiGeneratedRoadmaps = customRoadmaps.filter(
personalDashboardDetails || {}; (progress) => progress?.aiRoadmapId,
);
const customRoadmapsToShow = customRoadmaps.filter(
(progress) => !progress?.aiRoadmapId,
);
const { avatar, name } = personalDashboardDetails || {};
const avatarLink = avatar const avatarLink = avatar
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${avatar}` ? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${avatar}`
: '/images/default-avatar.png'; : '/images/default-avatar.png';
const currentPeriod = getCurrentPeriod(); const currentPeriod = getCurrentPeriod();
const relatedRoadmapIds = [...builtInRoleRoadmaps, ...builtInSkillRoadmaps] const allRoadmapsAndBestPractices = [
...builtInRoleRoadmaps,
...builtInSkillRoadmaps,
...builtInBestPractices,
];
const relatedRoadmapIds = allRoadmapsAndBestPractices
.filter((roadmap) => .filter((roadmap) =>
learningRoadmapsToShow?.some( learningRoadmapsToShow?.some(
(learningRoadmap) => learningRoadmap.resourceId === roadmap.id, (learningRoadmap) => learningRoadmap.resourceId === roadmap.id,
), ),
) )
.flatMap((roadmap) => roadmap.relatedRoadmapIds) .flatMap((roadmap) => roadmap.relatedRoadmapIds)
.filter(Boolean); .filter(
(roadmapId) =>
!learningRoadmapsToShow.some((lr) => lr.resourceId === roadmapId),
);
const recommendedRoadmapIds = new Set( const recommendedRoadmapIds = new Set(
relatedRoadmapIds.length === 0 relatedRoadmapIds.length === 0
@ -134,10 +148,9 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
: relatedRoadmapIds, : relatedRoadmapIds,
); );
const recommendedRoadmaps = [ const recommendedRoadmaps = allRoadmapsAndBestPractices.filter((roadmap) =>
...builtInRoleRoadmaps, recommendedRoadmapIds.has(roadmap.id),
...builtInSkillRoadmaps, );
].filter((roadmap) => recommendedRoadmapIds.has(roadmap.id));
return ( return (
<section> <section>
@ -149,47 +162,54 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
</h2> </h2>
)} )}
<div className="mt-8 grid grid-cols-4 gap-2"> <div className="mt-8 grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-4">
{isLoading ? ( {isLoading ? (
<div className="h-[129px] animate-pulse rounded-lg border border-gray-300 bg-white"></div> <>
<DashboardCardSkeleton />
<DashboardCardSkeleton />
<DashboardCardSkeleton />
<DashboardCardSkeleton />
</>
) : ( ) : (
<a <>
className="overflow-hidden rounded-lg border border-gray-300 bg-white" <a
href="/account/update-profile" className="overflow-hidden rounded-lg border border-gray-300 bg-white"
> href="/account/update-profile"
<div className="px-4 py-2.5"> >
<img <div className="px-4 py-2.5">
src={avatarLink} <img
alt={name} src={avatarLink}
className="size-8 rounded-full" alt={name}
/> className="size-8 rounded-full"
</div> />
</div>
<div className="flex flex-col gap-0.5 p-4">
<h3 className="font-medium">{name}</h3> <div className="flex flex-col gap-0.5 p-4">
<p className="text-xs">Setup your profile</p> <h3 className="truncate font-medium">{name}</h3>
</div> <p className="text-xs">Setup your profile</p>
</a> </div>
)} </a>
<DashboardCard <DashboardCard
icon={'💡'} icon={'💡'}
title="Learn a new Skill" title="Learn a new Skill"
description="Visit our Roadmaps" description="Visit our Roadmaps"
href="/roadmaps" href="/roadmaps"
/> />
<DashboardCard <DashboardCard
icon={'🏗'} icon={'🏗'}
title="Practice your skills" title="Practice your skills"
description="Visit Projects" description="Visit Projects"
href="/backend/projects" href="/backend/projects"
/> />
<DashboardCard <DashboardCard
icon={'📚'} icon={'📚'}
title="Do things right way" title="Do things right way"
description="Visit Best Practices" description="Visit Best Practices"
href="/best-practices" href="/best-practices"
/> />
</>
)}
</div> </div>
<ListDashboardProgress <ListDashboardProgress
@ -203,13 +223,13 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
/> />
<ListDashboardCustomProgress <ListDashboardCustomProgress
progresses={customRoadmaps} progresses={customRoadmapsToShow}
isLoading={isLoading} isLoading={isLoading}
/> />
<DashboardCardLink <ListDashboardCustomProgress
href="/ai" progresses={aiGeneratedRoadmaps}
title="Generate Roadmaps with AI" isLoading={isLoading}
description="You can generate your own roadmap with AI" isAIGeneratedRoadmaps={true}
/> />
</section> </section>
); );
@ -238,9 +258,15 @@ function DashboardCard(props: DashboardCardProps) {
</div> </div>
<div className="flex grow flex-col justify-center gap-0.5 bg-white p-4"> <div className="flex grow flex-col justify-center gap-0.5 bg-white p-4">
<h3 className="font-medium text-black">{title}</h3> <h3 className="truncate font-medium text-black">{title}</h3>
<p className="text-xs text-black">{description}</p> <p className="text-xs text-black">{description}</p>
</div> </div>
</a> </a>
); );
} }
function DashboardCardSkeleton() {
return (
<div className="h-[129px] animate-pulse rounded-lg border border-gray-300 bg-white"></div>
);
}

@ -1,6 +1,5 @@
import { useEffect, useState } from 'react';
import type { BuiltInRoadmap } from './PersonalDashboard'; import type { BuiltInRoadmap } from './PersonalDashboard';
import { MarkFavorite } from '../FeaturedItems/MarkFavorite'; import { ArrowUpRight } from 'lucide-react';
type RecommendedRoadmapsProps = { type RecommendedRoadmapsProps = {
roadmaps: BuiltInRoadmap[]; roadmaps: BuiltInRoadmap[];
@ -8,20 +7,7 @@ type RecommendedRoadmapsProps = {
}; };
export function RecommendedRoadmaps(props: RecommendedRoadmapsProps) { export function RecommendedRoadmaps(props: RecommendedRoadmapsProps) {
const { roadmaps, isLoading } = props; const { roadmaps: roadmapsToShow, 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 ( return (
<> <>
@ -36,57 +22,39 @@ export function RecommendedRoadmaps(props: RecommendedRoadmapsProps) {
))} ))}
</div> </div>
) : ( ) : (
<div className="relative"> <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"> {roadmapsToShow.map((roadmap) => (
{roadmapsToShow.map((roadmap) => ( <RecommendedRoadmapCard key={roadmap.id} roadmap={roadmap} />
<div className="relative w-full" key={roadmap.id}> ))}
<a
key={roadmap.id}
className="block rounded-md border bg-white px-3 py-2 text-left text-sm shadow-sm transition-all hover:border-gray-300 hover:bg-gray-50"
href={roadmap.url}
>
{roadmap.title}
</a>
{isMounted && (
<MarkFavorite
resourceId={roadmap.id}
resourceType={
roadmap.url.includes('best-practices')
? 'best-practice'
: 'roadmap'
}
className='data-[is-favorite="true"]:text-gray-400'
/>
)}
</div>
))}
</div>
{!showAll && (
<div
className="pointer-events-none absolute inset-0 z-50 -m-1 flex items-end justify-center bg-gradient-to-t from-white to-transparent"
style={{
background:
'linear-gradient(180deg, rgba(249,250,251,0) 0%, rgba(249,250,251,0.8) 50%, rgba(249,250,251,1) 100%)',
}}
>
<button
className="pointer-events-auto text-sm font-medium text-gray-600 hover:text-black focus:outline-none"
onClick={() => setShowAll(true)}
>
+ Show all
</button>
</div>
)}
</div> </div>
)} )}
</> </>
); );
} }
type RecommendedRoadmapCardProps = {
roadmap: BuiltInRoadmap;
};
export function RecommendedRoadmapCard(props: RecommendedRoadmapCardProps) {
const { roadmap } = props;
const { title, url, description } = roadmap;
return (
<a
href={url}
className="group relative flex flex-col rounded-md border bg-white px-3 py-2 text-left shadow-sm transition-all hover:border-gray-300 hover:bg-gray-50"
>
<span className="truncate">{title}</span>
<span className="mt-1 text-sm text-gray-400">{description}</span>
<ArrowUpRight className="absolute right-2 top-2 h-4 w-4 text-gray-300 transition-colors group-hover:text-gray-500" />
</a>
);
}
function RecommendedCardSkeleton() { function RecommendedCardSkeleton() {
return ( return (
<div className="h-[38px] w-full animate-pulse rounded-md bg-gray-200" /> <div className="h-[86px] w-full animate-pulse rounded-md bg-gray-200" />
); );
} }

@ -23,6 +23,7 @@ export type UserProgress = {
updatedAt: string; updatedAt: string;
isCustomResource?: boolean; isCustomResource?: boolean;
roadmapSlug?: string; roadmapSlug?: string;
aiRoadmapId?: string;
}; };
export type TeamMember = { export type TeamMember = {
@ -191,7 +192,7 @@ export function TeamProgressPage() {
key={grouping.value} key={grouping.value}
className={`rounded-md border p-1 px-2 text-sm ${ className={`rounded-md border p-1 px-2 text-sm ${
selectedGrouping === grouping.value selectedGrouping === grouping.value
? ' border-gray-400 bg-gray-200 ' ? 'border-gray-400 bg-gray-200'
: '' : ''
}`} }`}
onClick={() => setSelectedGrouping(grouping.value)} onClick={() => setSelectedGrouping(grouping.value)}

Loading…
Cancel
Save