diff --git a/src/components/FrameRenderer/FrameRenderer.css b/src/components/FrameRenderer/FrameRenderer.css index e12a113f1..f47c20e24 100644 --- a/src/components/FrameRenderer/FrameRenderer.css +++ b/src/components/FrameRenderer/FrameRenderer.css @@ -49,7 +49,7 @@ svg .done rect { fill: #cbcbcb !important; } -svg .done text { +svg .done text, svg .skipped text { text-decoration: line-through; } @@ -57,6 +57,10 @@ svg .learning rect { fill: #dad1fd !important; } +svg .skipped rect { + fill: #ff665a !important; +} + svg .learning rect[fill='rgb(51,51,51)'] + text, svg .done rect[fill='rgb(51,51,51)'] + text { fill: black !important; diff --git a/src/components/TopicDetail/TopicProgressButton.tsx b/src/components/TopicDetail/TopicProgressButton.tsx index 22a60df85..81647b4d3 100644 --- a/src/components/TopicDetail/TopicProgressButton.tsx +++ b/src/components/TopicDetail/TopicProgressButton.tsx @@ -1,15 +1,17 @@ -import { useEffect, useMemo, useState } from 'preact/hooks'; +import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; +import { useOutsideClick } from '../../hooks/use-outside-click'; import CheckIcon from '../../icons/check.svg'; +import DownIcon from '../../icons/down.svg'; import ProgressIcon from '../../icons/progress.svg'; import ResetIcon from '../../icons/reset.svg'; import SpinnerIcon from '../../icons/spinner.svg'; import { isLoggedIn } from '../../lib/jwt'; import { - ResourceProgressType, - ResourceType, - getTopicStatus, - renderTopicProgress, - updateResourceProgress, + ResourceProgressType, + ResourceType, + getTopicStatus, + renderTopicProgress, + updateResourceProgress, } from '../../lib/resource-progress'; type TopicProgressButtonProps = { @@ -20,11 +22,25 @@ type TopicProgressButtonProps = { onClose: () => void; }; +const statusColors: Record = { + done: 'bg-green-500', + learning: 'bg-yellow-500', + pending: 'bg-gray-300', + skipped: 'bg-black', +}; + export function TopicProgressButton(props: TopicProgressButtonProps) { const { topicId, resourceId, resourceType, onClose } = props; const [isUpdatingProgress, setIsUpdatingProgress] = useState(true); const [progress, setProgress] = useState('pending'); + const [showChangeStatus, setShowChangeStatus] = useState(false); + + const changeStatusRef = useRef(null); + + useOutsideClick(changeStatusRef, () => { + setShowChangeStatus(false); + }); const isGuest = useMemo(() => !isLoggedIn(), []); @@ -66,9 +82,10 @@ export function TopicProgressButton(props: TopicProgressButtonProps) { }); }; - const allowMarkingDone = ['pending', 'learning'].includes(progress); - const allowMarkingLearning = ['pending'].includes(progress); - const allowMarkingPending = ['done', 'learning'].includes(progress); + const allowMarkingSkipped = ['pending', 'learning', 'done'].includes(progress); + const allowMarkingDone = ['skipped', 'pending', 'learning'].includes(progress); + const allowMarkingLearning = ['done', 'skipped', 'pending'].includes(progress); + const allowMarkingPending = ['skipped', 'done', 'learning'].includes(progress); if (isUpdatingProgress) { return ( @@ -79,6 +96,81 @@ export function TopicProgressButton(props: TopicProgressButtonProps) { ); } + return ( +
+ + + + + + {progress === 'learning' ? 'In Progress' : progress} + + + + + + {showChangeStatus && ( +
+ {allowMarkingDone && ( + + )} + {allowMarkingPending && ( + + )} + {allowMarkingLearning && ( + + )} + {allowMarkingSkipped && ( + + )} +
+ )} +
+ ); + if (isGuest) { return (
diff --git a/src/lib/resource-progress.ts b/src/lib/resource-progress.ts index 3f4279323..fccd508f1 100644 --- a/src/lib/resource-progress.ts +++ b/src/lib/resource-progress.ts @@ -4,7 +4,7 @@ import { TOKEN_COOKIE_NAME } from './jwt'; import Element = astroHTML.JSX.Element; export type ResourceType = 'roadmap' | 'best-practice'; -export type ResourceProgressType = 'done' | 'learning' | 'pending'; +export type ResourceProgressType = 'done' | 'learning' | 'pending' | 'skipped'; type TopicMeta = { topicId: string; @@ -26,14 +26,18 @@ export async function getTopicStatus( const { topicId, resourceType, resourceId } = topic; const progressResult = await getResourceProgress(resourceType, resourceId); - if (progressResult?.done.includes(topicId)) { + if (progressResult?.done?.includes(topicId)) { return 'done'; } - if (progressResult?.learning.includes(topicId)) { + if (progressResult?.learning?.includes(topicId)) { return 'learning'; } + if (progressResult?.skipped?.includes(topicId)) { + return 'skipped'; + } + return 'pending'; } @@ -46,6 +50,7 @@ export async function updateResourceProgress( const { response, error } = await httpPost<{ done: string[]; learning: string[]; + skipped: string[]; }>(`${import.meta.env.PUBLIC_API_URL}/v1-update-resource-progress`, { topicId, resourceType, @@ -61,20 +66,23 @@ export async function updateResourceProgress( resourceType, resourceId, response.done, - response.learning + response.learning, + response.skipped, ); + return response; } export async function getResourceProgress( resourceType: 'roadmap' | 'best-practice', resourceId: string -): Promise<{ done: string[]; learning: string[] }> { +): Promise<{ done: string[]; learning: string[], skipped: string[] }> { // No need to load progress if user is not logged in if (!Cookies.get(TOKEN_COOKIE_NAME)) { return { done: [], learning: [], + skipped: [], }; } @@ -101,31 +109,27 @@ async function loadFreshProgress( const { response, error } = await httpGet<{ done: string[]; learning: string[]; + skipped: string[]; }>(`${import.meta.env.PUBLIC_API_URL}/v1-get-user-resource-progress`, { resourceType, resourceId, }); - if (error) { + if (error || !response) { console.error(error); return { done: [], learning: [], - }; - } - - if (!response?.done || !response?.learning) { - return { - done: [], - learning: [], + skipped: [], }; } setResourceProgress( resourceType, resourceId, - response.done, - response.learning + response?.done || [], + response?.learning || [], + response?.skipped || [], ); return response; @@ -135,13 +139,15 @@ export function setResourceProgress( resourceType: 'roadmap' | 'best-practice', resourceId: string, done: string[], - learning: string[] + learning: string[], + skipped: string [], ): void { localStorage.setItem( `${resourceType}-${resourceId}-progress`, JSON.stringify({ done, learning, + skipped, timestamp: new Date().getTime(), }) ); @@ -152,6 +158,7 @@ export function renderTopicProgress( topicProgress: ResourceProgressType ) { const isLearning = topicProgress === 'learning'; + const isSkipped = topicProgress === 'skipped'; const isDone = topicProgress === 'done'; const matchingElements: Element[] = []; @@ -185,13 +192,15 @@ export function renderTopicProgress( matchingElements.forEach((element) => { if (isDone) { element.classList.add('done'); - element.classList.remove('learning'); + element.classList.remove('learning', 'skipped'); } else if (isLearning) { element.classList.add('learning'); - element.classList.remove('done'); + element.classList.remove('done', 'skipped'); + } else if (isSkipped) { + element.classList.add('skipped'); + element.classList.remove('done', 'learning'); } else { - element.classList.remove('done'); - element.classList.remove('learning'); + element.classList.remove('done', 'skipped', 'learning'); } }); } @@ -200,7 +209,7 @@ export async function renderResourceProgress( resourceType: ResourceType, resourceId: string ) { - const { done = [], learning = [] } = + const { done = [], learning = [], skipped = [] } = (await getResourceProgress(resourceType, resourceId)) || {}; done.forEach((topicId) => { @@ -210,4 +219,8 @@ export async function renderResourceProgress( learning.forEach((topicId) => { renderTopicProgress(topicId, 'learning'); }); + + skipped.forEach((topicId) => { + renderTopicProgress(topicId, 'skipped'); + }); }