parent
cd8a1e13ac
commit
e7022764d2
9 changed files with 184 additions and 66 deletions
@ -0,0 +1,20 @@ |
||||
type CheckIconProps = { |
||||
additionalClasses?: string; |
||||
}; |
||||
|
||||
export function CheckIcon(props: CheckIconProps) { |
||||
const { additionalClasses = 'mr-2 top-[0.5px] w-[20px] h-[20px]' } = props; |
||||
|
||||
return ( |
||||
<svg |
||||
className={`relative ${additionalClasses}]`} |
||||
stroke="currentColor" |
||||
fill="currentColor" |
||||
stroke-width="0" |
||||
viewBox="0 0 16 16" |
||||
xmlns="http://www.w3.org/2000/svg" |
||||
> |
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"></path> |
||||
</svg> |
||||
); |
||||
} |
@ -0,0 +1,23 @@ |
||||
import { CheckIcon } from './CheckIcon'; |
||||
|
||||
type EmptyProgressProps = { |
||||
title?: string; |
||||
message?: string; |
||||
}; |
||||
|
||||
export function EmptyProgress(props: EmptyProgressProps) { |
||||
const { |
||||
title = 'Start learning ..', |
||||
message = 'Your progress and favorite roadmaps will appear here', |
||||
} = props; |
||||
|
||||
return ( |
||||
<div className="relative flex min-h-full flex-col items-center justify-center"> |
||||
<h2 className={'mb-1 flex items-center text-2xl text-gray-200'}> |
||||
<CheckIcon /> |
||||
Start learning .. |
||||
</h2> |
||||
<p className={'text-gray-400'}>{message}</p> |
||||
</div> |
||||
); |
||||
} |
@ -1,20 +1,100 @@ |
||||
import { useEffect, useState } from 'preact/hooks'; |
||||
import { EmptyProgress } from './EmptyProgress'; |
||||
import { httpGet } from '../../lib/http'; |
||||
import { ProgressList } from './ProgressList'; |
||||
|
||||
export type UserProgressResponse = { |
||||
resourceId: string; |
||||
resourceType: 'roadmap' | 'best-practice'; |
||||
resourceTitle: string; |
||||
done: number; |
||||
learning: number; |
||||
skipped: number; |
||||
total: number; |
||||
updatedAt: Date; |
||||
}[]; |
||||
|
||||
function renderProgress(progressList: UserProgressResponse) { |
||||
progressList.forEach((progress) => { |
||||
const href = |
||||
progress.resourceType === 'best-practice' |
||||
? `/best-practices/${progress.resourceId}` |
||||
: `/${progress.resourceId}`; |
||||
const element = document.querySelector(`a[href="${href}"]`); |
||||
if (!element) { |
||||
return; |
||||
} |
||||
|
||||
const totalDone = progress.done + progress.skipped; |
||||
const percentageDone = (totalDone / progress.total) * 100; |
||||
|
||||
const progressBar: HTMLElement = element.querySelector('[data-progress]')!; |
||||
progressBar.style.width = `${percentageDone}%`; |
||||
}); |
||||
} |
||||
|
||||
export function FavoriteRoadmaps() { |
||||
const [isPreparing, setIsPreparing] = useState(true); |
||||
useEffect(() => { |
||||
const [isLoading, setIsLoading] = useState(false); |
||||
const [progress, setProgress] = useState<UserProgressResponse>([]); |
||||
const [containerOpacity, setContainerOpacity] = useState(0); |
||||
|
||||
function showProgressContainer() { |
||||
const heroEl = document.getElementById('hero-text')!; |
||||
heroEl.classList.add('opacity-0') |
||||
setIsPreparing(false); |
||||
}); |
||||
if (!heroEl) { |
||||
return; |
||||
} |
||||
|
||||
heroEl.classList.add('opacity-0'); |
||||
setTimeout(() => { |
||||
heroEl.parentElement?.removeChild(heroEl); |
||||
setIsPreparing(false); |
||||
|
||||
setTimeout(() => { |
||||
setContainerOpacity(100); |
||||
}, 50); |
||||
}, 300); |
||||
} |
||||
|
||||
async function loadProgress() { |
||||
setIsLoading(true); |
||||
const { response: progressList, error } = |
||||
await httpGet<UserProgressResponse>( |
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-user-all-progress` |
||||
); |
||||
|
||||
if (error || !progressList) { |
||||
return; |
||||
} |
||||
|
||||
setProgress(progressList); |
||||
renderProgress(progressList); |
||||
} |
||||
|
||||
useEffect(() => { |
||||
showProgressContainer(); |
||||
loadProgress().finally(() => { |
||||
setIsLoading(false); |
||||
}); |
||||
}, []); |
||||
|
||||
if (isPreparing) { |
||||
return null; |
||||
} |
||||
|
||||
return null; |
||||
const hasProgress = progress.length > 0; |
||||
|
||||
// return (
|
||||
// <div class="min-h-full border-t border-t-[#1e293c] bg-gray-900"></div>
|
||||
// );
|
||||
return ( |
||||
<div |
||||
class={`min-h-auto flex bg-gradient-to-b transition-opacity duration-500 sm:min-h-[280px] opacity-${containerOpacity} ${hasProgress && `border-t border-t-[#1e293c]`}`} |
||||
> |
||||
<div className="container min-h-full"> |
||||
{!isLoading && progress.length == 0 && <EmptyProgress />} |
||||
{isLoading && <EmptyProgress title="Loading progress .." />} |
||||
{!isLoading && progress.length > 0 && ( |
||||
<ProgressList progress={progress} /> |
||||
)} |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
|
@ -0,0 +1,45 @@ |
||||
import type { UserProgressResponse } from './FavoriteRoadmaps'; |
||||
import { CheckIcon } from './CheckIcon'; |
||||
|
||||
type ProgressListProps = { |
||||
progress: UserProgressResponse; |
||||
}; |
||||
|
||||
export function ProgressList(props: ProgressListProps) { |
||||
const { progress } = props; |
||||
|
||||
return ( |
||||
<div className="relative py-7"> |
||||
<p className="mb-4 flex items-center text-center text-sm text-gray-400"> |
||||
<CheckIcon additionalClasses={'mr-1.5 w-[14px] h-[14px]'} /> |
||||
Your favorite roadmaps and tracked progress. |
||||
</p> |
||||
|
||||
<div className="grid grid-cols-3 gap-2"> |
||||
{progress.map((resource) => { |
||||
const url = |
||||
resource.resourceType === 'roadmap' |
||||
? `/${resource.resourceId}` |
||||
: `/best-practices/${resource.resourceId}`; |
||||
|
||||
const percentageDone = |
||||
((resource.skipped + resource.done) / resource.total) * 100; |
||||
|
||||
return ( |
||||
<a |
||||
href={url} |
||||
className="relative flex flex-col rounded-md border border-slate-800 bg-slate-900 p-3 text-sm text-slate-400 hover:border-slate-600 hover:text-slate-300 overflow-hidden" |
||||
> |
||||
<span className='relative z-20'>{resource.resourceTitle}</span> |
||||
|
||||
<span |
||||
class="absolute bottom-0 left-0 top-0 z-10 bg-[#172a3a]" |
||||
style={{ width: `${percentageDone}%` }} |
||||
></span> |
||||
</a> |
||||
); |
||||
})} |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
@ -1,48 +0,0 @@ |
||||
import { httpGet } from './http'; |
||||
import { isLoggedIn } from './jwt'; |
||||
|
||||
type UserProgressResponse = { |
||||
resourceId: string; |
||||
resourceType: 'roadmap' | 'best-practice'; |
||||
done: number; |
||||
learning: number; |
||||
skipped: number; |
||||
total: number; |
||||
updatedAt: Date; |
||||
}[]; |
||||
|
||||
async function renderProgress() { |
||||
if (!isLoggedIn()) { |
||||
return; |
||||
} |
||||
|
||||
const { response: progressList, error } = await httpGet<UserProgressResponse>( |
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-user-all-progress` |
||||
); |
||||
|
||||
if (error || !progressList) { |
||||
return; |
||||
} |
||||
|
||||
progressList.forEach((progress) => { |
||||
const href = |
||||
progress.resourceType === 'best-practice' |
||||
? `/best-practices/${progress.resourceId}` |
||||
: `/${progress.resourceId}`; |
||||
const element = document.querySelector(`a[href="${href}"]`); |
||||
if (!element) { |
||||
return; |
||||
} |
||||
|
||||
const totalDone = progress.done + progress.skipped; |
||||
const percentageDone = (totalDone / progress.total) * 100; |
||||
|
||||
const progressBar: HTMLElement = element.querySelector('[data-progress]')!; |
||||
progressBar.style.width = `${percentageDone}%`; |
||||
}); |
||||
} |
||||
|
||||
// on DOM load
|
||||
window.addEventListener('DOMContentLoaded', () => { |
||||
window.setTimeout(renderProgress, 0); |
||||
}); |
Loading…
Reference in new issue