parent
57e957198e
commit
3a5fdb656f
9 changed files with 453 additions and 92 deletions
@ -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> |
||||
); |
||||
} |
@ -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,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" /> |
||||
); |
||||
} |
@ -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, |
||||
)} |
||||
/> |
||||
); |
||||
} |
Loading…
Reference in new issue