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 { 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() { |
export function FavoriteRoadmaps() { |
||||||
const [isPreparing, setIsPreparing] = useState(true); |
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')!; |
const heroEl = document.getElementById('hero-text')!; |
||||||
heroEl.classList.add('opacity-0') |
if (!heroEl) { |
||||||
setIsPreparing(false); |
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) { |
if (isPreparing) { |
||||||
return null; |
return null; |
||||||
} |
} |
||||||
|
|
||||||
return null; |
const hasProgress = progress.length > 0; |
||||||
|
|
||||||
// return (
|
return ( |
||||||
// <div class="min-h-full border-t border-t-[#1e293c] bg-gray-900"></div>
|
<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