parent
f7625a8250
commit
476557db80
6 changed files with 341 additions and 34 deletions
@ -0,0 +1,56 @@ |
||||
type ActivityCountersType = { |
||||
done: { |
||||
today: number; |
||||
total: number; |
||||
}; |
||||
learning: { |
||||
today: number; |
||||
total: number; |
||||
}; |
||||
streak: { |
||||
count: number; |
||||
}; |
||||
}; |
||||
|
||||
type ActivityCounterType = { |
||||
text: string; |
||||
count: string; |
||||
}; |
||||
|
||||
function ActivityCounter(props: ActivityCounterType) { |
||||
const { text, count } = props; |
||||
|
||||
return ( |
||||
<div class="relative flex flex-1 flex-row-reverse sm:flex-col px-0 sm:px-4 py-2 sm:py-4 text-center sm:pt-10 items-center gap-2 sm:gap-0 justify-end"> |
||||
<h2 class="text-base sm:text-5xl font-bold"> |
||||
{count} |
||||
</h2> |
||||
<p class="mt-0 sm:mt-2 text-sm text-gray-400">{text}</p> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
export function ActivityCounters(props: ActivityCountersType) { |
||||
const { done, learning, streak } = props; |
||||
|
||||
return ( |
||||
<div class="mx-0 -mt-5 sm:-mx-10 md:-mt-10"> |
||||
<div class="flex flex-col sm:flex-row gap-0 sm:gap-2 divide-y sm:divide-y-0 divide-x-0 sm:divide-x border-b"> |
||||
<ActivityCounter |
||||
text={'Topics Completed'} |
||||
count={`${done?.total || 0}`} |
||||
/> |
||||
|
||||
<ActivityCounter |
||||
text={'Currently Learning'} |
||||
count={`${learning?.total || 0}`} |
||||
/> |
||||
|
||||
<ActivityCounter |
||||
text={'Learning Streak'} |
||||
count={`${streak?.count || 0}d`} |
||||
/> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,141 @@ |
||||
import { useEffect, useState } from 'preact/hooks'; |
||||
import { httpGet } from '../../lib/http'; |
||||
import { ActivityCounters } from './ActivityCounters'; |
||||
import { ResourceProgress } from './ResourceProgress'; |
||||
import { pageLoadingMessage } from '../../stores/page'; |
||||
import { EmptyActivity } from './EmptyActivity'; |
||||
|
||||
type ActivityResponse = { |
||||
done: { |
||||
today: number; |
||||
total: number; |
||||
}; |
||||
learning: { |
||||
today: number; |
||||
total: number; |
||||
roadmaps: { |
||||
title: string; |
||||
id: string; |
||||
learning: number; |
||||
done: number; |
||||
total: number; |
||||
skipped: number; |
||||
}[]; |
||||
bestPractices: { |
||||
title: string; |
||||
id: string; |
||||
learning: number; |
||||
done: number; |
||||
skipped: number; |
||||
total: number; |
||||
}[]; |
||||
}; |
||||
streak: { |
||||
count: number; |
||||
}; |
||||
activity: { |
||||
type: 'done' | 'learning' | 'pending' | 'skipped'; |
||||
createdAt: Date; |
||||
metadata: { |
||||
resourceId?: string; |
||||
resourceType?: 'roadmap' | 'best-practice'; |
||||
topicId?: string; |
||||
topicLabel?: string; |
||||
resourceTitle?: string; |
||||
}; |
||||
}[]; |
||||
}; |
||||
|
||||
export function ActivityPage() { |
||||
const [activity, setActivity] = useState<ActivityResponse>(); |
||||
const [isLoading, setIsLoading] = useState(true); |
||||
|
||||
async function loadActivity() { |
||||
const { error, response } = await httpGet<ActivityResponse>( |
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-user-activity` |
||||
); |
||||
|
||||
if (!response || error) { |
||||
console.error('Error loading activity'); |
||||
console.error(error); |
||||
|
||||
return; |
||||
} |
||||
|
||||
setActivity(response); |
||||
} |
||||
|
||||
useEffect(() => { |
||||
loadActivity().finally(() => { |
||||
pageLoadingMessage.set(''); |
||||
setIsLoading(false); |
||||
}); |
||||
}, []); |
||||
|
||||
const learningRoadmaps = activity?.learning.roadmaps || []; |
||||
const learningBestPractices = activity?.learning.bestPractices || []; |
||||
|
||||
if (isLoading) { |
||||
return null; |
||||
} |
||||
|
||||
return ( |
||||
<> |
||||
<ActivityCounters |
||||
done={activity?.done || { today: 0, total: 0 }} |
||||
learning={activity?.learning || { today: 0, total: 0 }} |
||||
streak={activity?.streak || { count: 0 }} |
||||
/> |
||||
|
||||
<div class="mx-0 px-0 py-5 md:-mx-10 md:px-8 md:py-8"> |
||||
{learningRoadmaps.length === 0 && |
||||
learningBestPractices.length === 0 && <EmptyActivity />} |
||||
|
||||
{(learningRoadmaps.length > 0 || learningBestPractices.length > 0) && ( |
||||
<> |
||||
<h2 class="mb-3 text-xs uppercase text-gray-400"> |
||||
Continue Following |
||||
</h2> |
||||
<div class="flex flex-col gap-3"> |
||||
{learningRoadmaps.map((roadmap) => ( |
||||
<ResourceProgress |
||||
doneCount={roadmap.done || 0} |
||||
learningCount={roadmap.learning || 0} |
||||
totalCount={roadmap.total || 0} |
||||
skippedCount={roadmap.skipped || 0} |
||||
resourceId={roadmap.id} |
||||
resourceType={'roadmap'} |
||||
title={roadmap.title} |
||||
onCleared={() => { |
||||
pageLoadingMessage.set('Updating activity'); |
||||
loadActivity().finally(() => { |
||||
pageLoadingMessage.set(''); |
||||
}); |
||||
}} |
||||
/> |
||||
))} |
||||
|
||||
{learningBestPractices.map((bestPractice) => ( |
||||
<ResourceProgress |
||||
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} |
||||
onCleared={() => { |
||||
pageLoadingMessage.set('Updating activity'); |
||||
loadActivity().finally(() => { |
||||
pageLoadingMessage.set(''); |
||||
}); |
||||
}} |
||||
/> |
||||
))} |
||||
</div> |
||||
</> |
||||
)} |
||||
</div> |
||||
</> |
||||
); |
||||
} |
@ -0,0 +1,27 @@ |
||||
import CheckIcon from '../../icons/roadmap.svg'; |
||||
|
||||
export function EmptyActivity() { |
||||
return ( |
||||
<div class="rounded-md"> |
||||
<div class="flex flex-col items-center p-7 text-center"> |
||||
<img |
||||
alt="no roadmaps" |
||||
src={CheckIcon} |
||||
class="mb-2 h-[120px] w-[120px] opacity-10" |
||||
/> |
||||
<h2 class="text-xl font-bold">No Progress</h2> |
||||
<p className="my-2 max-w-[400px] text-gray-500"> |
||||
Progress will appear here as you start tracking your{' '} |
||||
<a href="/roadmaps" class="mt-4 text-blue-500 hover:underline"> |
||||
Roadmaps |
||||
</a>{' '} |
||||
or{' '} |
||||
<a href="/best-practices" class="mt-4 text-blue-500 hover:underline"> |
||||
Best Practices |
||||
</a>{' '} |
||||
progress. |
||||
</p> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,113 @@ |
||||
import { useEffect, useState } from 'preact/hooks'; |
||||
import { httpPost } from '../../lib/http'; |
||||
|
||||
type ResourceProgressType = { |
||||
resourceType: 'roadmap' | 'best-practice'; |
||||
resourceId: string; |
||||
title: string; |
||||
totalCount: number; |
||||
doneCount: number; |
||||
learningCount: number; |
||||
skippedCount: number; |
||||
onCleared: () => void; |
||||
}; |
||||
|
||||
export function ResourceProgress(props: ResourceProgressType) { |
||||
const [isClearing, setIsClearing] = useState(false); |
||||
const [isConfirming, setIsConfirming] = useState(false); |
||||
|
||||
const { |
||||
resourceType, |
||||
resourceId, |
||||
title, |
||||
totalCount, |
||||
learningCount, |
||||
doneCount, |
||||
skippedCount, |
||||
onCleared, |
||||
} = props; |
||||
|
||||
async function clearProgress() { |
||||
setIsClearing(true); |
||||
const { error, response } = await httpPost( |
||||
`${import.meta.env.PUBLIC_API_URL}/v1-clear-resource-progress`, |
||||
{ |
||||
resourceId, |
||||
resourceType, |
||||
} |
||||
); |
||||
|
||||
if (error || !response) { |
||||
alert('Error clearing progress. Please try again.'); |
||||
console.error(error); |
||||
setIsClearing(false); |
||||
return; |
||||
} |
||||
|
||||
localStorage.removeItem(`${resourceType}-${resourceId}-progress`); |
||||
console.log(`${resourceType}-${resourceId}-progress`); |
||||
setIsClearing(false); |
||||
setIsConfirming(false); |
||||
onCleared(); |
||||
} |
||||
|
||||
const url = |
||||
resourceType === 'roadmap' |
||||
? `/${resourceId}` |
||||
: `/best-practices/${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">{title}</span> |
||||
<span className="cursor-pointer text-sm text-gray-400"> |
||||
5 hours ago |
||||
</span> |
||||
</a> |
||||
<p className="items-start sm:space-between flex flex-row rounded-b-md border border-t-0 px-2 py-2 text-xs text-gray-500"> |
||||
<span className="flex-1 gap-1 flex"> |
||||
<span>{doneCount} done</span> • |
||||
{ learningCount > 0 && <><span>{learningCount} in progress</span> •</> } |
||||
{ skippedCount > 0 && <><span>{skippedCount} skipped</span> •</> } |
||||
<span>{totalCount} total</span> |
||||
</span> |
||||
{!isConfirming && ( |
||||
<button |
||||
className="text-red-500 hover:text-red-800" |
||||
onClick={() => setIsConfirming(true)} |
||||
disabled={isClearing} |
||||
> |
||||
{!isClearing && ( |
||||
<> |
||||
Clear Progress <span>×</span> |
||||
</> |
||||
)} |
||||
|
||||
{isClearing && 'Processing...'} |
||||
</button> |
||||
)} |
||||
|
||||
{isConfirming && ( |
||||
<span> |
||||
<span className='hidden sm:inline'>Are you sure?{' '}</span> |
||||
<span className='inline sm:hidden'>Sure?{' '}</span> |
||||
<button onClick={clearProgress} className="ml-1 mr-1 underline text-red-500 hover:text-red-800">Yes</button>{' '} |
||||
<button onClick={() => setIsConfirming(false)} className="underline text-red-500 hover:text-red-800">No</button> |
||||
</span> |
||||
)} |
||||
</p> |
||||
</div> |
||||
); |
||||
} |
@ -1,41 +1,11 @@ |
||||
--- |
||||
import AccountSidebar from '../../components/AccountSidebar.astro'; |
||||
import { ActivityPage } from '../../components/Activity/ActivityPage'; |
||||
import AccountLayout from '../../layouts/AccountLayout.astro'; |
||||
--- |
||||
|
||||
<AccountLayout title='Update Profile' noIndex={true}> |
||||
<AccountLayout title='Update Profile' noIndex={true} initialLoadingMessage={'Loading activity'}> |
||||
<AccountSidebar activePageId='activity' activePageTitle='Activity'> |
||||
<div class='mx-0 -mt-5 sm:-mx-10 md:-mt-10'> |
||||
<div class='flex gap-2 divide-x border-b'> |
||||
<div class='flex flex-1 flex-col px-4 pb-4 pt-5 text-center sm:pt-10'> |
||||
<h2 class='text-5xl font-bold'>30</h2> |
||||
<p class='mt-2 text-sm text-gray-400'>Topics Completed</p> |
||||
</div> |
||||
<div class='flex flex-1 flex-col px-4 pb-4 pt-5 text-center sm:pt-10'> |
||||
<h2 class='mb-1 text-5xl font-bold'>20</h2> |
||||
<p class='mt-2 text-sm text-gray-400'>Currently Learning</p> |
||||
</div> |
||||
<div class='flex flex-1 flex-col px-4 pb-4 pt-5 text-center sm:pt-10'> |
||||
<h2 class='mb-1 text-5xl font-bold'>2d</h2> |
||||
<p class='mt-2 text-sm text-gray-400'>Learning Streak</p> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class='-mx-10 border-b px-8 py-8'> |
||||
<h2 class='mb-3 text-xs uppercase text-gray-400'>Continue Learning</h2> |
||||
<div class='flex flex-col gap-2'> |
||||
<a |
||||
href='#' |
||||
class='relative flex items-center rounded-md border p-3 text-gray-600 group hover:border-gray-300 hover:text-black' |
||||
> |
||||
<span |
||||
class='absolute left-0 top-0 block h-full w-[50%] rounded-l-md bg-black/5 group-hover:bg-black/10' |
||||
></span> |
||||
<span class='relative flex-1'>Frontend Roadmap</span> |
||||
<span class='text-sm text-gray-400'>5 / 142</span> |
||||
</a> |
||||
</div> |
||||
</div> |
||||
<ActivityPage client:load /> |
||||
</AccountSidebar> |
||||
</AccountLayout> |
||||
|
Loading…
Reference in new issue