feat: update dashboard design

feat/dashboard
Arik Chakma 6 months ago
parent fa73fcfd2f
commit 57e957198e
  1. 5
      src/components/Dashboard/DashboardPage.tsx
  2. 17
      src/components/Dashboard/DashboardProgressCard.tsx
  3. 2
      src/components/Dashboard/DashboardTab.tsx
  4. 45
      src/components/Dashboard/ListDashboardCustomProgress.tsx
  5. 12
      src/components/Dashboard/ListDashboardProgress.tsx
  6. 64
      src/components/Dashboard/PersonalDashboard.tsx
  7. 82
      src/components/Dashboard/RecommendedRoadmaps.tsx
  8. 3
      src/components/TeamProgress/TeamProgressPage.tsx

@ -46,7 +46,8 @@ export function DashboardPage(props: DashboardPageProps) {
getAllTeams().finally(() => setIsLoading(false));
}, []);
const userAvatar = currentUser?.avatar
const userAvatar =
currentUser?.avatar && !isLoading
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${currentUser.avatar}`
: '/images/default-avatar.png';
@ -118,6 +119,6 @@ export function DashboardPage(props: DashboardPageProps) {
function DashboardTabSkeleton() {
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 { getRelativeTimeString } from '../../lib/date';
import type { UserProgress } from '../TeamProgress/TeamProgressPage';
type DashboardProgressCardProps = {
@ -17,6 +18,7 @@ export function DashboardProgressCard(props: DashboardProgressCardProps) {
skipped: skippedCount,
roadmapSlug,
isCustomResource,
updatedAt,
} = progress;
let url =
@ -34,16 +36,29 @@ export function DashboardProgressCard(props: DashboardProgressCardProps) {
return (
<a
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>
<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>
);
}

@ -20,7 +20,7 @@ export function DashboardTab(props: DashboardTabProps) {
<Slot
onClick={onClick}
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' : '',
className,
)}

@ -9,17 +9,29 @@ type ListDashboardCustomProgressProps = {
progresses: UserProgress[];
isLoading?: boolean;
isCustomResources?: boolean;
isAIGeneratedRoadmaps?: boolean;
};
export function ListDashboardCustomProgress(
props: ListDashboardCustomProgressProps,
) {
const { progresses, isLoading = false } = props;
const {
progresses,
isLoading = false,
isAIGeneratedRoadmaps = false,
} = props;
const [isCreateCustomRoadmapModalOpen, setIsCreateCustomRoadmapModalOpen] =
useState(false);
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
className="mt-8"
href="https://draw.roadmap.sh"
@ -41,21 +53,25 @@ export function ListDashboardCustomProgress(
/>
) : null;
const Slot = isAIGeneratedRoadmaps ? 'a' : 'button';
return (
<>
{customRoadmapModal}
<h2 className="mb-3 mt-8 text-xs uppercase text-gray-400">
Custom Roadmaps
{isAIGeneratedRoadmaps ? 'AI Generated Roadmaps' : 'Custom Roadmaps'}
</h2>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-4">
{isLoading ? (
<div className="grid grid-cols-4 gap-2">
<>
{Array.from({ length: 8 }).map((_, index) => (
<DashboardProgressCardSkeleton key={index} />
))}
</div>
</>
) : (
<div className="grid grid-cols-4 gap-2">
<>
{progresses.map((progress) => (
<DashboardProgressCard
key={progress.resourceId}
@ -63,16 +79,21 @@ export function ListDashboardCustomProgress(
/>
))}
<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"
onClick={() => {
{...(isAIGeneratedRoadmaps
? { href: '/ai' }
: {
onClick: () => {
setIsCreateCustomRoadmapModalOpen(true);
}}
},
})}
>
+ Create New
</button>
</div>
{isAIGeneratedRoadmaps ? '+ Generate New' : '+ Create New'}
</Slot>
</>
)}
</div>
</>
);
}

@ -22,22 +22,24 @@ export function ListDashboardProgress(props: ListDashboardProgressProps) {
Progress and Bookmarks
</h2>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-4">
{isLoading ? (
<div className="grid grid-cols-4 gap-2">
<>
{Array.from({ length: 8 }).map((_, index) => (
<DashboardProgressCardSkeleton key={index} />
))}
</div>
</>
) : (
<div className="grid grid-cols-4 gap-2">
<>
{progresses.map((progress) => (
<DashboardProgressCard
key={progress.resourceId}
progress={progress}
/>
))}
</div>
</>
)}
</div>
</>
);
}
@ -48,6 +50,6 @@ export function DashboardProgressCardSkeleton(
props: DashboardProgressCardSkeletonProps,
) {
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 = [],
builtInSkillRoadmaps = [],
} = props;
const toast = useToast();
const [isLoading, setIsLoading] = useState(true);
const [personalDashboardDetails, setPersonalDashboardDetails] =
@ -111,22 +110,37 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
return updatedAtB.getTime() - updatedAtA.getTime();
});
const { avatar, name, headline, email, username } =
personalDashboardDetails || {};
const aiGeneratedRoadmaps = customRoadmaps.filter(
(progress) => progress?.aiRoadmapId,
);
const customRoadmapsToShow = customRoadmaps.filter(
(progress) => !progress?.aiRoadmapId,
);
const { avatar, name } = personalDashboardDetails || {};
const avatarLink = avatar
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${avatar}`
: '/images/default-avatar.png';
const currentPeriod = getCurrentPeriod();
const relatedRoadmapIds = [...builtInRoleRoadmaps, ...builtInSkillRoadmaps]
const allRoadmapsAndBestPractices = [
...builtInRoleRoadmaps,
...builtInSkillRoadmaps,
...builtInBestPractices,
];
const relatedRoadmapIds = allRoadmapsAndBestPractices
.filter((roadmap) =>
learningRoadmapsToShow?.some(
(learningRoadmap) => learningRoadmap.resourceId === roadmap.id,
),
)
.flatMap((roadmap) => roadmap.relatedRoadmapIds)
.filter(Boolean);
.filter(
(roadmapId) =>
!learningRoadmapsToShow.some((lr) => lr.resourceId === roadmapId),
);
const recommendedRoadmapIds = new Set(
relatedRoadmapIds.length === 0
@ -134,10 +148,9 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
: relatedRoadmapIds,
);
const recommendedRoadmaps = [
...builtInRoleRoadmaps,
...builtInSkillRoadmaps,
].filter((roadmap) => recommendedRoadmapIds.has(roadmap.id));
const recommendedRoadmaps = allRoadmapsAndBestPractices.filter((roadmap) =>
recommendedRoadmapIds.has(roadmap.id),
);
return (
<section>
@ -149,10 +162,16 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
</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 ? (
<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"
href="/account/update-profile"
@ -166,11 +185,10 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
</div>
<div className="flex flex-col gap-0.5 p-4">
<h3 className="font-medium">{name}</h3>
<h3 className="truncate font-medium">{name}</h3>
<p className="text-xs">Setup your profile</p>
</div>
</a>
)}
<DashboardCard
icon={'💡'}
@ -190,6 +208,8 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
description="Visit Best Practices"
href="/best-practices"
/>
</>
)}
</div>
<ListDashboardProgress
@ -203,13 +223,13 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
/>
<ListDashboardCustomProgress
progresses={customRoadmaps}
progresses={customRoadmapsToShow}
isLoading={isLoading}
/>
<DashboardCardLink
href="/ai"
title="Generate Roadmaps with AI"
description="You can generate your own roadmap with AI"
<ListDashboardCustomProgress
progresses={aiGeneratedRoadmaps}
isLoading={isLoading}
isAIGeneratedRoadmaps={true}
/>
</section>
);
@ -238,9 +258,15 @@ function DashboardCard(props: DashboardCardProps) {
</div>
<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>
</div>
</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 { MarkFavorite } from '../FeaturedItems/MarkFavorite';
import { ArrowUpRight } from 'lucide-react';
type RecommendedRoadmapsProps = {
roadmaps: BuiltInRoadmap[];
@ -8,20 +7,7 @@ type RecommendedRoadmapsProps = {
};
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]);
const { roadmaps: roadmapsToShow, isLoading } = props;
return (
<>
@ -36,57 +22,39 @@ export function RecommendedRoadmaps(props: RecommendedRoadmapsProps) {
))}
</div>
) : (
<div className="relative">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-2 md:grid-cols-3">
{roadmapsToShow.map((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>
<RecommendedRoadmapCard key={roadmap.id} roadmap={roadmap} />
))}
</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>
)}
</>
);
}
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() {
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;
isCustomResource?: boolean;
roadmapSlug?: string;
aiRoadmapId?: string;
};
export type TeamMember = {
@ -191,7 +192,7 @@ export function TeamProgressPage() {
key={grouping.value}
className={`rounded-md border p-1 px-2 text-sm ${
selectedGrouping === grouping.value
? ' border-gray-400 bg-gray-200 '
? 'border-gray-400 bg-gray-200'
: ''
}`}
onClick={() => setSelectedGrouping(grouping.value)}

Loading…
Cancel
Save