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 AccountSidebar from '../../components/AccountSidebar.astro'; |
||||||
|
import { ActivityPage } from '../../components/Activity/ActivityPage'; |
||||||
import AccountLayout from '../../layouts/AccountLayout.astro'; |
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'> |
<AccountSidebar activePageId='activity' activePageTitle='Activity'> |
||||||
<div class='mx-0 -mt-5 sm:-mx-10 md:-mt-10'> |
<ActivityPage client:load /> |
||||||
<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> |
|
||||||
</AccountSidebar> |
</AccountSidebar> |
||||||
</AccountLayout> |
</AccountLayout> |
||||||
|
Loading…
Reference in new issue