parent
7fab5018a0
commit
3e99ba0eb3
9 changed files with 433 additions and 372 deletions
@ -0,0 +1,28 @@ |
||||
import { ArrowUpRight } from 'lucide-react'; |
||||
import { cn } from '../../lib/classname'; |
||||
|
||||
type DashboardCardLinkProps = { |
||||
href: string; |
||||
title: string; |
||||
description: string; |
||||
className?: string; |
||||
}; |
||||
|
||||
export function DashboardCardLink(props: DashboardCardLinkProps) { |
||||
const { href, title, description, className } = props; |
||||
|
||||
return ( |
||||
<a |
||||
className={cn( |
||||
'relative mt-4 flex min-h-[168px] flex-col justify-end rounded-lg border border-gray-300 bg-gray-100 p-4 hover:bg-gray-200', |
||||
className, |
||||
)} |
||||
href={href} |
||||
target="_blank" |
||||
> |
||||
<h4 className="text-xl font-semibold tracking-wide">{title}</h4> |
||||
<p className="mt-1 text-gray-500">{description}</p> |
||||
<ArrowUpRight className="absolute right-3 top-3 size-4" /> |
||||
</a> |
||||
); |
||||
} |
@ -0,0 +1,49 @@ |
||||
import { getPercentage } from '../../helper/number'; |
||||
import type { UserProgress } from '../TeamProgress/TeamProgressPage'; |
||||
|
||||
type DashboardProgressCardProps = { |
||||
progress: UserProgress; |
||||
}; |
||||
|
||||
export function DashboardProgressCard(props: DashboardProgressCardProps) { |
||||
const { progress } = props; |
||||
|
||||
const { |
||||
resourceType, |
||||
resourceId, |
||||
resourceTitle, |
||||
total: totalCount, |
||||
done: doneCount, |
||||
skipped: skippedCount, |
||||
roadmapSlug, |
||||
isCustomResource, |
||||
} = 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 border-gray-300 bg-white p-3 text-left text-sm transition-all hover:border-gray-400" |
||||
> |
||||
<h4 className="truncate font-medium text-gray-900">{resourceTitle}</h4> |
||||
|
||||
<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> |
||||
</a> |
||||
); |
||||
} |
@ -0,0 +1,78 @@ |
||||
import type { UserProgress } from '../TeamProgress/TeamProgressPage'; |
||||
import { DashboardProgressCard } from './DashboardProgressCard'; |
||||
import { DashboardProgressCardSkeleton } from './ListDashboardProgress'; |
||||
import { DashboardCardLink } from './DashboardCardLink'; |
||||
import { useState } from 'react'; |
||||
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal'; |
||||
|
||||
type ListDashboardCustomProgressProps = { |
||||
progresses: UserProgress[]; |
||||
isLoading?: boolean; |
||||
isCustomResources?: boolean; |
||||
}; |
||||
|
||||
export function ListDashboardCustomProgress( |
||||
props: ListDashboardCustomProgressProps, |
||||
) { |
||||
const { progresses, isLoading = false } = props; |
||||
const [isCreateCustomRoadmapModalOpen, setIsCreateCustomRoadmapModalOpen] = |
||||
useState(false); |
||||
|
||||
if (!isLoading && progresses.length === 0) { |
||||
return ( |
||||
<DashboardCardLink |
||||
className="mt-8" |
||||
href="https://draw.roadmap.sh" |
||||
title="Use our Editor to Draw Roadmaps" |
||||
description="You can make roadmaps that look like ours" |
||||
/> |
||||
); |
||||
} |
||||
|
||||
const customRoadmapModal = isCreateCustomRoadmapModalOpen ? ( |
||||
<CreateRoadmapModal |
||||
onClose={() => setIsCreateCustomRoadmapModalOpen(false)} |
||||
onCreated={(roadmap) => { |
||||
window.location.href = `${ |
||||
import.meta.env.PUBLIC_EDITOR_APP_URL |
||||
}/${roadmap?._id}`;
|
||||
return; |
||||
}} |
||||
/> |
||||
) : null; |
||||
|
||||
return ( |
||||
<> |
||||
{customRoadmapModal} |
||||
<h2 className="mb-3 mt-8 text-xs uppercase text-gray-400"> |
||||
Custom Roadmaps |
||||
</h2> |
||||
|
||||
{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} |
||||
/> |
||||
))} |
||||
|
||||
<button |
||||
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={() => { |
||||
setIsCreateCustomRoadmapModalOpen(true); |
||||
}} |
||||
> |
||||
+ Create New |
||||
</button> |
||||
</div> |
||||
)} |
||||
</> |
||||
); |
||||
} |
@ -0,0 +1,53 @@ |
||||
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> |
||||
|
||||
{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> |
||||
)} |
||||
</> |
||||
); |
||||
} |
||||
|
||||
type DashboardProgressCardSkeletonProps = {}; |
||||
|
||||
export function DashboardProgressCardSkeleton( |
||||
props: DashboardProgressCardSkeletonProps, |
||||
) { |
||||
return ( |
||||
<div className="h-[80px] w-full animate-pulse rounded-md bg-gray-200" /> |
||||
); |
||||
} |
@ -0,0 +1,92 @@ |
||||
import { useEffect, useState } from 'react'; |
||||
import type { BuiltInRoadmap } from './PersonalDashboard'; |
||||
import { MarkFavorite } from '../FeaturedItems/MarkFavorite'; |
||||
|
||||
type RecommendedRoadmapsProps = { |
||||
roadmaps: BuiltInRoadmap[]; |
||||
isLoading: boolean; |
||||
}; |
||||
|
||||
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]); |
||||
|
||||
return ( |
||||
<> |
||||
<h2 className="mb-3 mt-8 text-xs uppercase text-gray-400"> |
||||
Recommended Roadmaps |
||||
</h2> |
||||
|
||||
{isLoading ? ( |
||||
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-2 md:grid-cols-3"> |
||||
{Array.from({ length: 12 }).map((_, index) => ( |
||||
<RecommendedCardSkeleton key={index} /> |
||||
))} |
||||
</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> |
||||
))} |
||||
</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> |
||||
)} |
||||
</> |
||||
); |
||||
} |
||||
|
||||
function RecommendedCardSkeleton() { |
||||
return ( |
||||
<div className="h-[38px] w-full animate-pulse rounded-md bg-gray-200" /> |
||||
); |
||||
} |
Loading…
Reference in new issue