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