parent
8bf0b51065
commit
5e5f7427f9
6 changed files with 194 additions and 14 deletions
@ -0,0 +1,74 @@ |
|||||||
|
import type { GetUserByUsernameResponse } from '../../api/user'; |
||||||
|
import { UserActivityHeatmap } from './UserPublicActivityHeatmap'; |
||||||
|
import { UserPublicDetails } from './UserPublicDetails'; |
||||||
|
import { UserPublicProgressStats } from './UserPublicProgressStats'; |
||||||
|
|
||||||
|
type UserPublicAccountPageProps = GetUserByUsernameResponse; |
||||||
|
|
||||||
|
export function UserPublicAccountPage(props: UserPublicAccountPageProps) { |
||||||
|
const { activity, learning } = props; |
||||||
|
|
||||||
|
const learningRoadmaps = learning?.roadmaps || []; |
||||||
|
const learningBestPractices = learning?.bestPractices || []; |
||||||
|
|
||||||
|
return ( |
||||||
|
<section className="container mt-5 pb-10"> |
||||||
|
<UserPublicDetails userDetails={props!} /> |
||||||
|
<div className="mt-6"> |
||||||
|
<UserActivityHeatmap activity={activity!} /> |
||||||
|
</div> |
||||||
|
|
||||||
|
{(learningRoadmaps.length > 0 || learningBestPractices.length > 0) && ( |
||||||
|
<> |
||||||
|
<h2 className="mt-6 text-xl font-bold">Learning Progress</h2> |
||||||
|
<div className="mt-4 flex flex-col gap-3"> |
||||||
|
{learningRoadmaps |
||||||
|
.sort((a, b) => { |
||||||
|
const updatedAtA = new Date(a.updatedAt); |
||||||
|
const updatedAtB = new Date(b.updatedAt); |
||||||
|
|
||||||
|
return updatedAtB.getTime() - updatedAtA.getTime(); |
||||||
|
}) |
||||||
|
.map((roadmap) => ( |
||||||
|
<UserPublicProgressStats |
||||||
|
key={roadmap.id} |
||||||
|
roadmapSlug={roadmap.roadmapSlug} |
||||||
|
isCustomResource={roadmap.isCustomResource} |
||||||
|
doneCount={roadmap.done || 0} |
||||||
|
learningCount={roadmap.learning || 0} |
||||||
|
totalCount={roadmap.total || 0} |
||||||
|
skippedCount={roadmap.skipped || 0} |
||||||
|
resourceId={roadmap.id} |
||||||
|
resourceType={'roadmap'} |
||||||
|
updatedAt={roadmap.updatedAt} |
||||||
|
title={roadmap.title} |
||||||
|
/> |
||||||
|
))} |
||||||
|
|
||||||
|
{learningBestPractices |
||||||
|
.sort((a, b) => { |
||||||
|
const updatedAtA = new Date(a.updatedAt); |
||||||
|
const updatedAtB = new Date(b.updatedAt); |
||||||
|
|
||||||
|
return updatedAtB.getTime() - updatedAtA.getTime(); |
||||||
|
}) |
||||||
|
.map((bestPractice) => ( |
||||||
|
<UserPublicProgressStats |
||||||
|
isCustomResource={bestPractice.isCustomResource} |
||||||
|
key={bestPractice.id} |
||||||
|
doneCount={bestPractice.done || 0} |
||||||
|
totalCount={bestPractice.total || 0} |
||||||
|
learningCount={bestPractice.learning || 0} |
||||||
|
resourceId={bestPractice.id} |
||||||
|
skippedCount={bestPractice.skipped || 0} |
||||||
|
resourceType={'best-practice'} |
||||||
|
title={bestPractice.title} |
||||||
|
updatedAt={bestPractice.updatedAt} |
||||||
|
/> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
</> |
||||||
|
)} |
||||||
|
</section> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,84 @@ |
|||||||
|
import { getRelativeTimeString } from '../../lib/date'; |
||||||
|
|
||||||
|
type UserPublicProgressStats = { |
||||||
|
resourceType: 'roadmap' | 'best-practice'; |
||||||
|
resourceId: string; |
||||||
|
title: string; |
||||||
|
updatedAt: string; |
||||||
|
totalCount: number; |
||||||
|
doneCount: number; |
||||||
|
learningCount: number; |
||||||
|
skippedCount: number; |
||||||
|
showClearButton?: boolean; |
||||||
|
isCustomResource?: boolean; |
||||||
|
roadmapSlug?: string; |
||||||
|
}; |
||||||
|
|
||||||
|
export function UserPublicProgressStats(props: UserPublicProgressStats) { |
||||||
|
const { |
||||||
|
updatedAt, |
||||||
|
resourceType, |
||||||
|
resourceId, |
||||||
|
title, |
||||||
|
totalCount, |
||||||
|
learningCount, |
||||||
|
doneCount, |
||||||
|
skippedCount, |
||||||
|
roadmapSlug, |
||||||
|
isCustomResource = false, |
||||||
|
} = props; |
||||||
|
|
||||||
|
let url = |
||||||
|
resourceType === 'roadmap' |
||||||
|
? `/${resourceId}` |
||||||
|
: `/best-practices/${resourceId}`; |
||||||
|
|
||||||
|
if (isCustomResource) { |
||||||
|
url = `/r/${roadmapSlug}`; |
||||||
|
} |
||||||
|
|
||||||
|
const totalMarked = doneCount + skippedCount; |
||||||
|
const progressPercentage = Math.round((totalMarked / totalCount) * 100); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div> |
||||||
|
<a |
||||||
|
href={url} |
||||||
|
className="group relative flex cursor-pointer items-center rounded-t-md border p-3 text-gray-600 hover:border-gray-300 hover:text-black" |
||||||
|
> |
||||||
|
<span |
||||||
|
className={`absolute left-0 top-0 block h-full cursor-pointer rounded-tl-md bg-black/5 group-hover:bg-black/10`} |
||||||
|
style={{ |
||||||
|
width: `${progressPercentage}%`, |
||||||
|
}} |
||||||
|
></span> |
||||||
|
<span className="relative flex-1 cursor-pointer truncate"> |
||||||
|
{title} |
||||||
|
</span> |
||||||
|
<span className="ml-1 cursor-pointer text-sm text-gray-400"> |
||||||
|
{getRelativeTimeString(updatedAt)} |
||||||
|
</span> |
||||||
|
</a> |
||||||
|
<div className="sm:space-between flex flex-row items-start rounded-b-md border border-t-0 px-2 py-2 text-xs text-gray-500"> |
||||||
|
<span className="hidden flex-1 gap-1 sm:flex"> |
||||||
|
{doneCount > 0 && ( |
||||||
|
<> |
||||||
|
<span>{doneCount} done</span> • |
||||||
|
</> |
||||||
|
)} |
||||||
|
{learningCount > 0 && ( |
||||||
|
<> |
||||||
|
<span>{learningCount} in progress</span> • |
||||||
|
</> |
||||||
|
)} |
||||||
|
{skippedCount > 0 && ( |
||||||
|
<> |
||||||
|
<span>{skippedCount} skipped</span> • |
||||||
|
</> |
||||||
|
)} |
||||||
|
<span>{totalCount} total</span> |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
Loading…
Reference in new issue