feat: update dashboard layout (#7155)

feat/changelog^2
Arik Chakma 4 weeks 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 { useState } from 'react';
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal'; import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal';
import { Simulate } from 'react-dom/test-utils'; 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 = { type DashboardAiRoadmapsProps = {
roadmaps: { roadmaps: {
@ -20,9 +26,21 @@ export function DashboardAiRoadmaps(props: DashboardAiRoadmapsProps) {
return ( return (
<> <>
<h2 className="mb-2 mt-6 text-xs uppercase text-gray-400"> <div className="mb-2 mt-6 flex items-center justify-between gap-2">
AI Generated Roadmaps <h2 className="text-xs uppercase text-gray-400">
</h2> 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 && ( {!isLoading && roadmaps.length === 0 && (
<DashboardCardLink <DashboardCardLink
@ -48,7 +66,7 @@ export function DashboardAiRoadmaps(props: DashboardAiRoadmapsProps) {
{roadmaps.map((roadmap) => ( {roadmaps.map((roadmap) => (
<a <a
href={`/ai/${roadmap.slug}`} 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} {roadmap.title}
</a> </a>
@ -69,9 +87,7 @@ export function DashboardAiRoadmaps(props: DashboardAiRoadmapsProps) {
type CustomProgressCardSkeletonProps = {}; type CustomProgressCardSkeletonProps = {};
function RoadmapCardSkeleton( function RoadmapCardSkeleton(props: CustomProgressCardSkeletonProps) {
props: CustomProgressCardSkeletonProps,
) {
return ( return (
<div className="h-[42px] w-full animate-pulse rounded-md bg-gray-200" /> <div className="h-[42px] w-full animate-pulse rounded-md bg-gray-200" />
); );

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

@ -4,7 +4,13 @@ import { DashboardCardLink } from './DashboardCardLink';
import { useState } from 'react'; import { useState } from 'react';
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal'; import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal';
import { Simulate } from 'react-dom/test-utils'; 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 = { type ListDashboardCustomProgressProps = {
progresses: UserProgress[]; progresses: UserProgress[];
@ -40,9 +46,21 @@ export function ListDashboardCustomProgress(
<> <>
{customRoadmapModal} {customRoadmapModal}
<h2 className="mb-2 mt-6 text-xs uppercase text-gray-400"> <div className="mb-2 mt-6 flex items-center justify-between gap-2">
{isAIGeneratedRoadmaps ? 'AI Generated Roadmaps' : 'Custom Roadmaps'} <h2 className="text-xs uppercase text-gray-400">
</h2> {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 && ( {!isLoading && progresses.length === 0 && isAIGeneratedRoadmaps && (
<DashboardCardLink <DashboardCardLink

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

Loading…
Cancel
Save