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