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