diff --git a/src/components/FrameRenderer/FrameRenderer.css b/src/components/FrameRenderer/FrameRenderer.css index 6220bdb77..2389f2796 100644 --- a/src/components/FrameRenderer/FrameRenderer.css +++ b/src/components/FrameRenderer/FrameRenderer.css @@ -53,6 +53,14 @@ svg .done text { text-decoration: line-through; } +svg .learning rect { + fill: #dad1fd !important; +} + +svg .learning text { + text-decoration: underline; +} + svg .clickable-group.done[data-group-id^='check:'] rect { fill: gray !important; stroke: gray; diff --git a/src/components/TopicDetail/TopicDetail.tsx b/src/components/TopicDetail/TopicDetail.tsx index a096c9cb5..5efc4cea7 100644 --- a/src/components/TopicDetail/TopicDetail.tsx +++ b/src/components/TopicDetail/TopicDetail.tsx @@ -1,5 +1,6 @@ import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; import CheckIcon from '../../icons/check.svg'; +import ProgressIcon from '../../icons/progress.svg'; import CloseIcon from '../../icons/close.svg'; import ResetIcon from '../../icons/reset.svg'; import SpinnerIcon from '../../icons/spinner.svg'; @@ -11,10 +12,12 @@ import { useToggleTopic } from '../../hooks/use-toggle-topic'; import { httpGet } from '../../lib/http'; import { isLoggedIn } from '../../lib/jwt'; import { + getTopicStatus, isTopicDone, renderTopicProgress, + ResourceProgressType, ResourceType, - toggleMarkTopicDone as toggleMarkTopicDoneApi, + updateResourceProgress as updateResourceProgressApi, } from '../../lib/resource-progress'; import { pageLoadingMessage, sponsorHidden } from '../../stores/page'; @@ -24,6 +27,7 @@ export function TopicDetail() { const [error, setError] = useState(''); const [topicHtml, setTopicHtml] = useState(''); + const [progress, setProgress] = useState('pending'); const [isDone, setIsDone] = useState(); const [isUpdatingProgress, setIsUpdatingProgress] = useState(true); @@ -49,13 +53,24 @@ export function TopicDetail() { } }; - const toggleMarkTopicDone = (isDone: boolean) => { + const handleUpdateResourceProgress = (progress: ResourceProgressType) => { setIsUpdatingProgress(true); - toggleMarkTopicDoneApi({ topicId, resourceId, resourceType }, isDone) + updateResourceProgressApi( + { + topicId, + resourceId, + resourceType, + }, + progress + ) .then(() => { - setIsDone(isDone); + setProgress(progress); setIsActive(false); - renderTopicProgress(topicId, isDone); + renderTopicProgress( + topicId, + progress === 'done', + progress === 'learning' + ); }) .catch((err) => { alert(err.message); @@ -73,10 +88,10 @@ export function TopicDetail() { } setIsUpdatingProgress(true); - isTopicDone({ topicId, resourceId, resourceType }) - .then((status: boolean) => { + getTopicStatus({ topicId, resourceId, resourceType }) + .then((status) => { setIsUpdatingProgress(false); - setIsDone(status); + setProgress(status); }) .catch(console.error); }, [topicId, resourceId, resourceType]); @@ -104,16 +119,19 @@ export function TopicDetail() { // Toggle the topic status isTopicDone({ topicId, resourceId, resourceType }) .then((oldIsDone) => { - return toggleMarkTopicDoneApi( + return updateResourceProgressApi( { topicId, resourceId, resourceType, }, - !oldIsDone + oldIsDone ? 'pending' : 'done' ); }) - .then((newIsDone) => renderTopicProgress(topicId, newIsDone)) + .then((updatedResult) => { + const newIsDone = updatedResult.done.includes(topicId); + renderTopicProgress(topicId, newIsDone, false); + }) .catch((err) => { alert(err.message); console.error(err); @@ -193,14 +211,24 @@ export function TopicDetail() { {/* Actions for the topic */}
{isGuest && ( - +
+ + +
)} {!isGuest && ( @@ -215,25 +243,54 @@ export function TopicDetail() { Updating Status.. )} - {!isUpdatingProgress && !isDone && ( - + {!isUpdatingProgress && progress === 'pending' && ( +
+ + + +
)} - {!isUpdatingProgress && isDone && ( + {!isUpdatingProgress && progress === 'done' && ( )} + + {!isUpdatingProgress && progress === 'learning' && ( +
+ + +
+ )} )} diff --git a/src/icons/progress.svg b/src/icons/progress.svg new file mode 100644 index 000000000..1b9258bb1 --- /dev/null +++ b/src/icons/progress.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/lib/resource-progress.ts b/src/lib/resource-progress.ts index d37637b8b..1810f09ec 100644 --- a/src/lib/resource-progress.ts +++ b/src/lib/resource-progress.ts @@ -1,9 +1,10 @@ -import { httpGet, httpPatch } from './http'; +import { httpGet, httpPost } from './http'; import Cookies from 'js-cookie'; import { TOKEN_COOKIE_NAME } from './jwt'; import Element = astroHTML.JSX.Element; export type ResourceType = 'roadmap' | 'best-practice'; +export type ResourceProgressType = 'done' | 'learning' | 'pending'; type TopicMeta = { topicId: string; @@ -13,47 +14,71 @@ type TopicMeta = { export async function isTopicDone(topic: TopicMeta): Promise { const { topicId, resourceType, resourceId } = topic; - const doneItems = await getResourceProgress(resourceType, resourceId); + const progressResult = await getResourceProgress(resourceType, resourceId); - if (!doneItems) { + if (!progressResult.done) { return false; } - return doneItems.includes(topicId); + return progressResult.done.includes(topicId); } -export async function toggleMarkTopicDone( +export async function getTopicStatus( + topic: TopicMeta +): Promise { + const { topicId, resourceType, resourceId } = topic; + const progressResult = await getResourceProgress(resourceType, resourceId); + + if (progressResult.done.includes(topicId)) { + return 'done'; + } + + if (progressResult.learning.includes(topicId)) { + return 'learning'; + } + + return 'pending'; +} + +export async function updateResourceProgress( topic: TopicMeta, - isDone: boolean -): Promise { + progressType: ResourceProgressType +) { const { topicId, resourceType, resourceId } = topic; - const { response, error } = await httpPatch<{ done: string[] }>( - `${import.meta.env.PUBLIC_API_URL}/v1-toggle-mark-resource-done`, - { - topicId, - resourceType, - resourceId, - isDone, - } - ); + const { response, error } = await httpPost<{ + done: string[]; + learning: string[]; + }>(`${import.meta.env.PUBLIC_API_URL}/v1-update-resource-progress`, { + topicId, + resourceType, + resourceId, + progress: progressType, + }); - if (error || !response?.done) { + if (error || !response?.done || !response?.learning) { throw new Error(error?.message || 'Something went wrong'); } - setResourceProgress(resourceType, resourceId, response.done); - - return isDone; + setResourceProgress( + resourceType, + resourceId, + response.done, + response.learning + ); + return response; } export async function getResourceProgress( resourceType: 'roadmap' | 'best-practice', resourceId: string -): Promise { +): Promise<{ done: string[]; learning: string[] }> { // No need to load progress if user is not logged in if (!Cookies.get(TOKEN_COOKIE_NAME)) { - return []; + return { + done: [], + learning: [], + }; } const progressKey = `${resourceType}-${resourceId}-progress`; @@ -69,50 +94,67 @@ export async function getResourceProgress( return loadFreshProgress(resourceType, resourceId); } - return progress.done; + return progress; } async function loadFreshProgress( resourceType: ResourceType, resourceId: string ) { - const { response, error } = await httpGet<{ done: string[] }>( - `${import.meta.env.PUBLIC_API_URL}/v1-get-user-resource-progress`, - { - resourceType, - resourceId, - } - ); + const { response, error } = await httpGet<{ + done: string[]; + learning: string[]; + }>(`${import.meta.env.PUBLIC_API_URL}/v1-get-user-resource-progress`, { + resourceType, + resourceId, + }); if (error) { console.error(error); - return []; + return { + done: [], + learning: [], + }; } - if (!response?.done) { - return []; + if (!response?.done || !response?.learning) { + return { + done: [], + learning: [], + }; } - setResourceProgress(resourceType, resourceId, response.done); + setResourceProgress( + resourceType, + resourceId, + response.done, + response.learning + ); - return response.done; + return response; } export function setResourceProgress( resourceType: 'roadmap' | 'best-practice', resourceId: string, - done: string[] + done: string[], + learning: string[] ): void { localStorage.setItem( `${resourceType}-${resourceId}-progress`, JSON.stringify({ done, + learning, timestamp: new Date().getTime(), }) ); } -export function renderTopicProgress(topicId: string, isDone: boolean) { +export function renderTopicProgress( + topicId: string, + isDone: boolean, + isLearning: boolean +) { const matchingElements: Element[] = []; // Elements having sort order in the beginning of the group id @@ -145,8 +187,11 @@ export function renderTopicProgress(topicId: string, isDone: boolean) { matchingElements.forEach((element) => { if (isDone) { element.classList.add('done'); + } else if (isLearning) { + element.classList.add('learning'); } else { element.classList.remove('done'); + element.classList.remove('learning'); } }); } @@ -157,7 +202,11 @@ export async function renderResourceProgress( ) { const progress = await getResourceProgress(resourceType, resourceId); - progress.forEach((topicId) => { - renderTopicProgress(topicId, true); + progress.done.forEach((topicId) => { + renderTopicProgress(topicId, true, false); + }); + + progress.learning.forEach((topicId) => { + renderTopicProgress(topicId, false, true); }); }