feat: update dashboard layout (#7155)

feat/changelog^2
Arik Chakma 2 months ago committed by GitHub
parent 9b3ec7cc19
commit 9b865678b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 32
      src/components/Dashboard/DashboardAiRoadmaps.tsx
  2. 15
      src/components/Dashboard/EmptyStackMessage.tsx
  3. 26
      src/components/Dashboard/ListDashboardCustomProgress.tsx
  4. 191
      src/components/Dashboard/ProgressStack.tsx

@ -4,7 +4,13 @@ import { DashboardCardLink } from './DashboardCardLink';
import { useState } from 'react';
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal';
import { Simulate } from 'react-dom/test-utils';
import { Bot, BrainCircuit, Map, PencilRuler } from 'lucide-react';
import {
ArrowUpRight,
Bot,
BrainCircuit,
Map,
PencilRuler,
} from 'lucide-react';
type DashboardAiRoadmapsProps = {
roadmaps: {
@ -20,9 +26,21 @@ export function DashboardAiRoadmaps(props: DashboardAiRoadmapsProps) {
return (
<>
<h2 className="mb-2 mt-6 text-xs uppercase text-gray-400">
AI Generated Roadmaps
</h2>
<div className="mb-2 mt-6 flex items-center justify-between gap-2">
<h2 className="text-xs uppercase text-gray-400">
AI Generated Roadmaps
</h2>
{!isLoading && roadmaps.length !== 0 && (
<a
href="/ai/explore"
className="flex items-center gap-1 text-xs text-gray-500 hover:text-black"
>
<ArrowUpRight size={12} />
AI Generated Roadmaps
</a>
)}
</div>
{!isLoading && roadmaps.length === 0 && (
<DashboardCardLink
@ -48,7 +66,7 @@ export function DashboardAiRoadmaps(props: DashboardAiRoadmapsProps) {
{roadmaps.map((roadmap) => (
<a
href={`/ai/${roadmap.slug}`}
className="relative rounded-md border bg-white p-2.5 text-left text-sm shadow-sm truncate hover:border-gray-400 hover:bg-gray-50"
className="relative truncate rounded-md border bg-white p-2.5 text-left text-sm shadow-sm hover:border-gray-400 hover:bg-gray-50"
>
{roadmap.title}
</a>
@ -69,9 +87,7 @@ export function DashboardAiRoadmaps(props: DashboardAiRoadmapsProps) {
type CustomProgressCardSkeletonProps = {};
function RoadmapCardSkeleton(
props: CustomProgressCardSkeletonProps,
) {
function RoadmapCardSkeleton(props: CustomProgressCardSkeletonProps) {
return (
<div className="h-[42px] w-full animate-pulse rounded-md bg-gray-200" />
);

@ -1,17 +1,26 @@
import { cn } from '../../lib/classname';
type EmptyStackMessageProps = {
number: number;
number: number | string;
title: string;
description: string;
buttonText: string;
buttonLink: string;
bodyClassName?: string;
};
export function EmptyStackMessage(props: EmptyStackMessageProps) {
const { number, title, description, buttonText, buttonLink } = props;
const { number, title, description, buttonText, buttonLink, bodyClassName } =
props;
return (
<div className="absolute inset-0 flex items-center justify-center rounded-md bg-black/50">
<div className="flex max-w-[200px] flex-col items-center justify-center rounded-md bg-white p-4 shadow-sm">
<div
className={cn(
'flex max-w-[200px] flex-col items-center justify-center rounded-md bg-white p-4 shadow-sm',
bodyClassName,
)}
>
<span className="flex h-8 w-8 items-center justify-center rounded-full bg-gray-300 text-white">
{number}
</span>

@ -4,7 +4,13 @@ import { DashboardCardLink } from './DashboardCardLink';
import { useState } from 'react';
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal';
import { Simulate } from 'react-dom/test-utils';
import {Bot, BrainCircuit, Map, PencilRuler} from 'lucide-react';
import {
ArrowUpRight,
Bot,
BrainCircuit,
Map,
PencilRuler,
} from 'lucide-react';
type ListDashboardCustomProgressProps = {
progresses: UserProgress[];
@ -40,9 +46,21 @@ export function ListDashboardCustomProgress(
<>
{customRoadmapModal}
<h2 className="mb-2 mt-6 text-xs uppercase text-gray-400">
{isAIGeneratedRoadmaps ? 'AI Generated Roadmaps' : 'Custom Roadmaps'}
</h2>
<div className="mb-2 mt-6 flex items-center justify-between gap-2">
<h2 className="text-xs uppercase text-gray-400">
{isAIGeneratedRoadmaps ? 'AI Generated Roadmaps' : 'Custom Roadmaps'}
</h2>
{!isLoading && progresses.length !== 0 && (
<a
href="/ai/explore"
className="flex items-center gap-1 text-xs text-gray-500 hover:text-black"
>
<ArrowUpRight size={12} />
Community Roadmaps
</a>
)}
</div>
{!isLoading && progresses.length === 0 && isAIGeneratedRoadmaps && (
<DashboardCardLink

@ -26,8 +26,7 @@ type ProgressStackProps = {
topicDoneToday: number;
};
const MAX_PROGRESS_TO_SHOW = 5;
const MAX_BOOKMARKS_TO_SHOW = 5;
const MAX_PROGRESS_TO_SHOW = 11;
const MAX_PROJECTS_TO_SHOW = 8;
type ProgressLaneProps = {
@ -36,6 +35,7 @@ type ProgressLaneProps = {
linkHref?: string;
isLoading?: boolean;
isEmpty?: boolean;
loadingWrapperClassName?: string;
loadingSkeletonCount?: number;
loadingSkeletonClassName?: string;
children: React.ReactNode;
@ -43,6 +43,7 @@ type ProgressLaneProps = {
emptyIcon?: LucideIcon;
emptyLinkText?: string;
emptyLinkHref?: string;
className?: string;
};
function ProgressLane(props: ProgressLaneProps) {
@ -51,6 +52,7 @@ function ProgressLane(props: ProgressLaneProps) {
linkText,
linkHref,
isLoading = false,
loadingWrapperClassName = '',
loadingSkeletonCount = 4,
loadingSkeletonClassName = '',
children,
@ -59,10 +61,16 @@ function ProgressLane(props: ProgressLaneProps) {
emptyMessage = `No ${title.toLowerCase()} to show`,
emptyLinkHref = '/roadmaps',
emptyLinkText = 'Explore',
className,
} = props;
return (
<div className="flex h-full flex-col rounded-md border bg-white px-4 py-3 shadow-sm">
<div
className={cn(
'flex h-full flex-col rounded-md border bg-white px-4 py-3 shadow-sm',
className,
)}
>
{isLoading && (
<div className={'flex flex-row justify-between'}>
<div className="h-[16px] w-[75px] animate-pulse rounded-md bg-gray-100"></div>
@ -86,11 +94,13 @@ function ProgressLane(props: ProgressLaneProps) {
<div className="mt-4 flex flex-grow flex-col gap-1.5">
{isLoading && (
<>
<div
className={cn('grid grid-cols-2 gap-2', loadingWrapperClassName)}
>
{Array.from({ length: loadingSkeletonCount }).map((_, index) => (
<CardSkeleton key={index} className={loadingSkeletonClassName} />
))}
</>
</div>
)}
{!isLoading && children}
@ -119,29 +129,27 @@ export function ProgressStack(props: ProgressStackProps) {
const { progresses, projects, isLoading, accountStreak, topicDoneToday } =
props;
const bookmarkedProgresses = progresses.filter(
(progress) => progress?.isFavorite,
);
const [showAllProgresses, setShowAllProgresses] = useState(false);
const sortedProgresses = progresses.sort((a, b) => {
if (a.isFavorite && !b.isFavorite) {
return 1;
}
const userProgresses = progresses.filter(
(progress) => !progress?.isFavorite || progress?.done > 0,
);
if (!a.isFavorite && b.isFavorite) {
return -1;
}
const [showAllProgresses, setShowAllProgresses] = useState(false);
return 0;
});
const userProgressesToShow = showAllProgresses
? userProgresses
: userProgresses.slice(0, MAX_PROGRESS_TO_SHOW);
? sortedProgresses
: sortedProgresses.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);
const totalProjectFinished = projects.filter(
(project) => project.repositoryUrl,
).length;
@ -167,92 +175,70 @@ export function ProgressStack(props: ProgressStackProps) {
</div>
<div className="mt-2 grid min-h-[330px] grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
<div className="relative">
{!isLoading && bookmarksToShow.length === 0 && (
<div className="relative col-span-2">
{!isLoading && userProgressesToShow.length === 0 && (
<EmptyStackMessage
number={1}
title={'Bookmark Roadmaps'}
description={'Bookmark some roadmaps to access them quickly'}
title={'Bookmark some Roadmaps'}
description={
'Bookmark some roadmaps to access them quickly and start updating your progress'
}
buttonText={'Explore Roadmaps'}
buttonLink={'/roadmaps'}
bodyClassName="max-w-[280px]"
/>
)}
<ProgressLane
title={'Bookmarks'}
title="Progress & Bookmarks"
isLoading={isLoading}
loadingSkeletonCount={5}
linkHref={'/roadmaps'}
linkText={'Roadmaps'}
isEmpty={bookmarksToShow.length === 0}
loadingSkeletonCount={MAX_PROGRESS_TO_SHOW}
linkHref="/roadmaps"
linkText="Roadmaps"
isEmpty={userProgressesToShow.length === 0}
emptyIcon={Bookmark}
emptyMessage={'No bookmarks to show'}
emptyLinkHref={'/roadmaps'}
emptyLinkText={'Explore Roadmaps'}
>
{bookmarksToShow.map((progress) => {
return (
<DashboardBookmarkCard
key={progress.resourceId}
bookmark={progress}
<div className="grid grid-cols-2 gap-2">
{userProgressesToShow.length > 0 && (
<>
{userProgressesToShow.map((progress) => {
const isFavorite =
progress.isFavorite &&
!progress.done &&
!progress.skipped;
if (isFavorite) {
return (
<DashboardBookmarkCard
key={progress.resourceId}
bookmark={progress}
/>
);
}
return (
<DashboardProgressCard
key={progress.resourceId}
progress={progress}
/>
);
})}
</>
)}
{sortedProgresses.length > MAX_PROGRESS_TO_SHOW && (
<ShowAllButton
showAll={showAllProgresses}
setShowAll={setShowAllProgresses}
count={sortedProgresses.length}
maxCount={MAX_PROGRESS_TO_SHOW}
className="min-h-[38px] rounded-md border border-dashed leading-none"
/>
);
})}
{bookmarkedProgresses.length > MAX_BOOKMARKS_TO_SHOW && (
<ShowAllButton
showAll={showAllBookmarks}
setShowAll={setShowAllBookmarks}
count={bookmarkedProgresses.length}
maxCount={MAX_BOOKMARKS_TO_SHOW}
className="mb-0.5 mt-3"
/>
)}
</ProgressLane>
</div>
<div className="relative">
{!isLoading && userProgressesToShow.length === 0 && (
<EmptyStackMessage
number={2}
title={'Track Progress'}
description={'Pick your first roadmap and start learning'}
buttonText={'Explore roadmaps'}
buttonLink={'/roadmaps'}
/>
)}
<ProgressLane
title={'Progress'}
linkHref={'/roadmaps'}
linkText={'Roadmaps'}
isLoading={isLoading}
loadingSkeletonCount={5}
isEmpty={userProgressesToShow.length === 0}
emptyMessage={'Update your Progress'}
emptyIcon={Map}
emptyLinkText={'Explore Roadmaps'}
>
{userProgressesToShow.length > 0 && (
<>
{userProgressesToShow.map((progress) => {
return (
<DashboardProgressCard
key={progress.resourceId}
progress={progress}
/>
);
})}
</>
)}
{userProgresses.length > MAX_PROGRESS_TO_SHOW && (
<ShowAllButton
showAll={showAllProgresses}
setShowAll={setShowAllProgresses}
count={userProgresses.length}
maxCount={MAX_PROGRESS_TO_SHOW}
className="mb-0.5 mt-3"
/>
)}
)}
</div>
</ProgressLane>
</div>
@ -262,6 +248,7 @@ export function ProgressStack(props: ProgressStackProps) {
linkHref={'/projects'}
linkText={'Projects'}
isLoading={isLoading}
loadingWrapperClassName="grid-cols-1"
loadingSkeletonClassName={'h-5'}
loadingSkeletonCount={8}
isEmpty={projectsToShow.length === 0}
@ -272,7 +259,7 @@ export function ProgressStack(props: ProgressStackProps) {
>
{!isLoading && projectsToShow.length === 0 && (
<EmptyStackMessage
number={3}
number={2}
title={'Build your first project'}
description={'Pick a project to practice and start building'}
buttonText={'Explore Projects'}
@ -317,17 +304,15 @@ function ShowAllButton(props: ShowAllButtonProps) {
const { showAll, setShowAll, count, maxCount, className } = props;
return (
<span className="flex flex-grow items-end">
<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>
</span>
<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>
);
}
@ -341,7 +326,7 @@ function CardSkeleton(props: CardSkeletonProps) {
return (
<div
className={cn(
'h-10 w-full animate-pulse rounded-md bg-gray-100',
'h-[38px] w-full animate-pulse rounded-md bg-gray-100',
className,
)}
/>

Loading…
Cancel
Save