parent
afad1ce3c3
commit
c72b0b2cf2
11 changed files with 269 additions and 180 deletions
@ -1,61 +0,0 @@ |
|||||||
import { Github, Globe, LinkedinIcon, Twitter } from 'lucide-react'; |
|
||||||
import type { GetUserByUsernameResponse } from '../../api/user'; |
|
||||||
|
|
||||||
type UserPublicDetailsProps = { |
|
||||||
userDetails: GetUserByUsernameResponse; |
|
||||||
}; |
|
||||||
|
|
||||||
export function UserPublicDetails(props: UserPublicDetailsProps) { |
|
||||||
const { userDetails } = props; |
|
||||||
const { name, username, links } = userDetails; |
|
||||||
|
|
||||||
return ( |
|
||||||
<div> |
|
||||||
<div className="flex items-center gap-4"> |
|
||||||
<img |
|
||||||
src="https://dodrc8eu8m09s.cloudfront.net/avatars/64ab82e214678473bb5d5ac2_1688961762495" |
|
||||||
alt={name} |
|
||||||
className="h-28 w-28 rounded-full" |
|
||||||
/> |
|
||||||
|
|
||||||
<div> |
|
||||||
<h1 className="text-2xl font-bold">{name}</h1> |
|
||||||
<p className="mt-1 text-sm text-gray-500"> |
|
||||||
I'm a Frontend developer interested in filmmaking, content creation, |
|
||||||
vlogging, and backend, currently living in Dhaka, Bangladesh. Right |
|
||||||
now I'm writing html at @roadmapsh. Let's grab a coffee. |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div className="mt-6 flex items-center gap-2 border-b pb-4"> |
|
||||||
{links?.github && <UserLink href={links?.github} icon={Github} />} |
|
||||||
{links?.linkedin && ( |
|
||||||
<UserLink href={links?.linkedin} icon={LinkedinIcon} /> |
|
||||||
)} |
|
||||||
{links?.twitter && <UserLink href={links?.twitter} icon={Twitter} />} |
|
||||||
{links?.website && <UserLink href={links?.website} icon={Globe} />} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
type UserLinkProps = { |
|
||||||
href: string; |
|
||||||
icon: typeof Github; |
|
||||||
}; |
|
||||||
|
|
||||||
export function UserLink(props: UserLinkProps) { |
|
||||||
const { href, icon: Icon } = props; |
|
||||||
|
|
||||||
return ( |
|
||||||
<a |
|
||||||
target="_blank" |
|
||||||
href={href} |
|
||||||
className="flex items-center gap-0.5 text-blue-700" |
|
||||||
> |
|
||||||
<Icon className="h-3.5 shrink-0 stroke-2" /> |
|
||||||
<span className="truncate text-sm">{href}</span> |
|
||||||
</a> |
|
||||||
); |
|
||||||
} |
|
@ -1,79 +0,0 @@ |
|||||||
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; |
|
||||||
username: string; |
|
||||||
}; |
|
||||||
|
|
||||||
export function UserPublicProgressStats(props: UserPublicProgressStats) { |
|
||||||
const { |
|
||||||
updatedAt, |
|
||||||
resourceType, |
|
||||||
resourceId, |
|
||||||
title, |
|
||||||
totalCount, |
|
||||||
learningCount, |
|
||||||
doneCount, |
|
||||||
skippedCount, |
|
||||||
roadmapSlug, |
|
||||||
isCustomResource = false, |
|
||||||
username, |
|
||||||
} = props; |
|
||||||
|
|
||||||
// Currently we only support roadmap not (best-practices)
|
|
||||||
const url = `/u/${username}/${isCustomResource ? roadmapSlug : resourceId}`; |
|
||||||
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> |
|
||||||
); |
|
||||||
} |
|
@ -0,0 +1,59 @@ |
|||||||
|
import { Github, Globe, LinkedinIcon, Twitter } from 'lucide-react'; |
||||||
|
import type { GetPublicProfileResponse } from '../../api/user'; |
||||||
|
|
||||||
|
type UserPublicProfileHeaderProps = { |
||||||
|
userDetails: GetPublicProfileResponse; |
||||||
|
}; |
||||||
|
|
||||||
|
export function UserPublicProfileHeader(props: UserPublicProfileHeaderProps) { |
||||||
|
const { userDetails } = props; |
||||||
|
const { name, username, links, publicConfig } = userDetails; |
||||||
|
const { headline, isAvailableForHire } = publicConfig!; |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="flex items-center gap-8"> |
||||||
|
<img |
||||||
|
src="https://dodrc8eu8m09s.cloudfront.net/avatars/64ab82e214678473bb5d5ac2_1688961762495" |
||||||
|
alt={name} |
||||||
|
className="h-32 w-32 rounded-full" |
||||||
|
/> |
||||||
|
|
||||||
|
<div> |
||||||
|
{isAvailableForHire && ( |
||||||
|
<span className="mb-1 inline-block rounded-md bg-green-100 px-2 py-1 text-xs font-medium text-green-700"> |
||||||
|
Available for hire |
||||||
|
</span> |
||||||
|
)} |
||||||
|
<h1 className="text-2xl font-bold">{name}</h1> |
||||||
|
<p className="mt-1 text-sm text-gray-500">{headline}</p> |
||||||
|
<div className="mt-3 flex items-center gap-2"> |
||||||
|
{links?.github && <UserLink href={links?.github} icon={Github} />} |
||||||
|
{links?.linkedin && ( |
||||||
|
<UserLink href={links?.linkedin} icon={LinkedinIcon} /> |
||||||
|
)} |
||||||
|
{links?.twitter && <UserLink href={links?.twitter} icon={Twitter} />} |
||||||
|
{links?.website && <UserLink href={links?.website} icon={Globe} />} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
type UserLinkProps = { |
||||||
|
href: string; |
||||||
|
icon: typeof Github; |
||||||
|
}; |
||||||
|
|
||||||
|
export function UserLink(props: UserLinkProps) { |
||||||
|
const { href, icon: Icon } = props; |
||||||
|
|
||||||
|
return ( |
||||||
|
<a |
||||||
|
target="_blank" |
||||||
|
href={href} |
||||||
|
className="flex h-6 w-6 items-center justify-center rounded-md border" |
||||||
|
> |
||||||
|
<Icon className="h-3.5 w-3.5 shrink-0 stroke-2" /> |
||||||
|
</a> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,62 @@ |
|||||||
|
import { getPercentage } from '../../helper/number'; |
||||||
|
import { getRelativeTimeString } from '../../lib/date'; |
||||||
|
|
||||||
|
type UserPublicProgressStats = { |
||||||
|
resourceType: 'roadmap'; |
||||||
|
resourceId: string; |
||||||
|
title: string; |
||||||
|
updatedAt: string; |
||||||
|
totalCount: number; |
||||||
|
doneCount: number; |
||||||
|
learningCount: number; |
||||||
|
skippedCount: number; |
||||||
|
showClearButton?: boolean; |
||||||
|
isCustomResource?: boolean; |
||||||
|
roadmapSlug?: string; |
||||||
|
username: string; |
||||||
|
}; |
||||||
|
|
||||||
|
export function UserPublicProgressStats(props: UserPublicProgressStats) { |
||||||
|
const { |
||||||
|
updatedAt, |
||||||
|
resourceId, |
||||||
|
title, |
||||||
|
totalCount, |
||||||
|
learningCount, |
||||||
|
doneCount, |
||||||
|
skippedCount, |
||||||
|
roadmapSlug, |
||||||
|
isCustomResource = false, |
||||||
|
username, |
||||||
|
} = props; |
||||||
|
|
||||||
|
// Currently we only support roadmap not (best-practices)
|
||||||
|
const url = `/u/${username}/${isCustomResource ? roadmapSlug : resourceId}`; |
||||||
|
const totalMarked = doneCount + skippedCount; |
||||||
|
const progressPercentage = getPercentage(totalMarked, totalCount); |
||||||
|
|
||||||
|
return ( |
||||||
|
<a href={url} className="group block rounded-md border p-2.5"> |
||||||
|
<h3 className="flex-1 cursor-pointer truncate text-lg font-medium"> |
||||||
|
{title} |
||||||
|
</h3> |
||||||
|
<div className="relative mt-3 h-1 w-full overflow-hidden rounded-full bg-black/5"> |
||||||
|
<div |
||||||
|
className={`absolute left-0 top-0 h-full bg-black/40`} |
||||||
|
style={{ |
||||||
|
width: `${progressPercentage}%`, |
||||||
|
}} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div className="mt-2 flex items-center justify-between gap-2"> |
||||||
|
<span className="text-sm text-gray-600"> |
||||||
|
{progressPercentage}% completed |
||||||
|
</span> |
||||||
|
<span className="text-sm text-gray-400"> |
||||||
|
Last updated {getRelativeTimeString(updatedAt)} |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
</a> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,79 @@ |
|||||||
|
import { useState } from 'react'; |
||||||
|
import type { GetPublicProfileResponse } from '../../api/user'; |
||||||
|
import { SelectionButton } from '../RoadCard/SelectionButton'; |
||||||
|
import { UserPublicProgressStats } from './UserPublicProgressStats'; |
||||||
|
|
||||||
|
type UserPublicProgressesProps = { |
||||||
|
username: string; |
||||||
|
roadmaps: GetPublicProfileResponse['roadmaps']; |
||||||
|
}; |
||||||
|
|
||||||
|
export function UserPublicProgresses(props: UserPublicProgressesProps) { |
||||||
|
const { roadmaps: roadmapProgresses, username } = props; |
||||||
|
|
||||||
|
const [activeTab, setActiveTab] = useState<'built-in' | 'custom'>('built-in'); |
||||||
|
|
||||||
|
const roadmaps = roadmapProgresses.filter( |
||||||
|
(roadmap) => !roadmap.isCustomResource, |
||||||
|
); |
||||||
|
const customRoadmaps = roadmapProgresses.filter( |
||||||
|
(roadmap) => roadmap.isCustomResource, |
||||||
|
); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div> |
||||||
|
<div className="flex items-center gap-2"> |
||||||
|
<SelectionButton |
||||||
|
isSelected={activeTab === 'built-in'} |
||||||
|
isDisabled={activeTab === 'built-in'} |
||||||
|
onClick={() => setActiveTab('built-in')} |
||||||
|
text="Learning Activities" |
||||||
|
/> |
||||||
|
<SelectionButton |
||||||
|
isSelected={activeTab === 'custom'} |
||||||
|
isDisabled={activeTab === 'custom'} |
||||||
|
onClick={() => setActiveTab('custom')} |
||||||
|
text="Custom Activities" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
|
||||||
|
<ul className="mt-4 grid grid-cols-2 gap-2"> |
||||||
|
{activeTab === 'built-in' |
||||||
|
? roadmaps.map((roadmap, counter) => ( |
||||||
|
<li key={roadmap.id + counter}> |
||||||
|
<UserPublicProgressStats |
||||||
|
updatedAt={roadmap.updatedAt} |
||||||
|
title={roadmap.title} |
||||||
|
totalCount={roadmap.total} |
||||||
|
doneCount={roadmap.done} |
||||||
|
learningCount={roadmap.learning} |
||||||
|
skippedCount={roadmap.skipped} |
||||||
|
resourceId={roadmap.id} |
||||||
|
resourceType="roadmap" |
||||||
|
roadmapSlug={roadmap.roadmapSlug} |
||||||
|
isCustomResource={false} |
||||||
|
username={username!} |
||||||
|
/> |
||||||
|
</li> |
||||||
|
)) |
||||||
|
: customRoadmaps.map((roadmap, counter) => ( |
||||||
|
<li key={roadmap.id + counter}> |
||||||
|
<UserPublicProgressStats |
||||||
|
updatedAt={roadmap.updatedAt} |
||||||
|
title={roadmap.title} |
||||||
|
totalCount={roadmap.total} |
||||||
|
doneCount={roadmap.done} |
||||||
|
learningCount={roadmap.learning} |
||||||
|
skippedCount={roadmap.skipped} |
||||||
|
resourceId={roadmap.id} |
||||||
|
resourceType="roadmap" |
||||||
|
roadmapSlug={roadmap.roadmapSlug} |
||||||
|
username={username!} |
||||||
|
isCustomResource={true} |
||||||
|
/> |
||||||
|
</li> |
||||||
|
))} |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
export function getPercentage(portion: number, total: number) { |
||||||
|
return portion > 0 ? ((portion / total) * 100).toFixed(2) : 0; |
||||||
|
} |
Loading…
Reference in new issue