Allow skipping

pull/3919/head
Kamran Ahmed 2 years ago
parent f338bd5ecb
commit fd349f2da8
  1. 6
      src/components/FrameRenderer/FrameRenderer.css
  2. 100
      src/components/TopicDetail/TopicProgressButton.tsx
  3. 55
      src/lib/resource-progress.ts

@ -49,7 +49,7 @@ svg .done rect {
fill: #cbcbcb !important; fill: #cbcbcb !important;
} }
svg .done text { svg .done text, svg .skipped text {
text-decoration: line-through; text-decoration: line-through;
} }
@ -57,6 +57,10 @@ svg .learning rect {
fill: #dad1fd !important; fill: #dad1fd !important;
} }
svg .skipped rect {
fill: #ff665a !important;
}
svg .learning rect[fill='rgb(51,51,51)'] + text, svg .learning rect[fill='rgb(51,51,51)'] + text,
svg .done rect[fill='rgb(51,51,51)'] + text { svg .done rect[fill='rgb(51,51,51)'] + text {
fill: black !important; fill: black !important;

@ -1,5 +1,7 @@
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 CheckIcon from '../../icons/check.svg';
import DownIcon from '../../icons/down.svg';
import ProgressIcon from '../../icons/progress.svg'; import ProgressIcon from '../../icons/progress.svg';
import ResetIcon from '../../icons/reset.svg'; import ResetIcon from '../../icons/reset.svg';
import SpinnerIcon from '../../icons/spinner.svg'; import SpinnerIcon from '../../icons/spinner.svg';
@ -20,11 +22,25 @@ type TopicProgressButtonProps = {
onClose: () => void; onClose: () => void;
}; };
const statusColors: Record<ResourceProgressType, string> = {
done: 'bg-green-500',
learning: 'bg-yellow-500',
pending: 'bg-gray-300',
skipped: 'bg-black',
};
export function TopicProgressButton(props: TopicProgressButtonProps) { export function TopicProgressButton(props: TopicProgressButtonProps) {
const { topicId, resourceId, resourceType, onClose } = props; const { topicId, resourceId, resourceType, onClose } = props;
const [isUpdatingProgress, setIsUpdatingProgress] = useState(true); const [isUpdatingProgress, setIsUpdatingProgress] = useState(true);
const [progress, setProgress] = useState<ResourceProgressType>('pending'); const [progress, setProgress] = useState<ResourceProgressType>('pending');
const [showChangeStatus, setShowChangeStatus] = useState(false);
const changeStatusRef = useRef<HTMLDivElement>(null);
useOutsideClick(changeStatusRef, () => {
setShowChangeStatus(false);
});
const isGuest = useMemo(() => !isLoggedIn(), []); const isGuest = useMemo(() => !isLoggedIn(), []);
@ -66,9 +82,10 @@ export function TopicProgressButton(props: TopicProgressButtonProps) {
}); });
}; };
const allowMarkingDone = ['pending', 'learning'].includes(progress); const allowMarkingSkipped = ['pending', 'learning', 'done'].includes(progress);
const allowMarkingLearning = ['pending'].includes(progress); const allowMarkingDone = ['skipped', 'pending', 'learning'].includes(progress);
const allowMarkingPending = ['done', 'learning'].includes(progress); const allowMarkingLearning = ['done', 'skipped', 'pending'].includes(progress);
const allowMarkingPending = ['skipped', 'done', 'learning'].includes(progress);
if (isUpdatingProgress) { if (isUpdatingProgress) {
return ( return (
@ -79,6 +96,81 @@ export function TopicProgressButton(props: TopicProgressButtonProps) {
); );
} }
return (
<div className="relative inline-flex rounded-md border border-gray-300">
<span className="inline-flex cursor-default items-center p-1 px-2 text-sm text-black">
<span class="flex h-2 w-2">
<span
class={`relative inline-flex h-2 w-2 rounded-full ${statusColors[progress]}`}
></span>
</span>
<span className="ml-2 capitalize">
{progress === 'learning' ? 'In Progress' : progress}
</span>
</span>
<button
className="inline-flex cursor-pointer items-center rounded-br-md rounded-tr-md border-l border-l-gray-300 bg-gray-100 p-1 px-2 text-sm text-black hover:bg-gray-200"
onClick={() => setShowChangeStatus(true)}
>
<span className="mr-0.5">Update Status</span>
<img alt="Check" class="h-4 w-4" src={DownIcon} />
</button>
{showChangeStatus && (
<div
className="absolute right-0 top-full mt-1 flex min-w-[128px] flex-col divide-y rounded-md border border-gray-200 bg-white shadow-md [&>button:first-child]:rounded-t-md [&>button:last-child]:rounded-b-md"
ref={changeStatusRef!}
>
{allowMarkingDone && (
<button
class="px-3 py-1.5 text-left text-sm text-gray-800 hover:bg-gray-100"
onClick={() => handleUpdateResourceProgress('done')}
>
<span
class={`mr-2 inline-block h-2 w-2 rounded-full ${statusColors['done']}`}
></span>
Done
</button>
)}
{allowMarkingPending && (
<button
class="px-3 py-1.5 text-left text-sm text-gray-800 hover:bg-gray-100"
onClick={() => handleUpdateResourceProgress('pending')}
>
<span
class={`mr-2 inline-block h-2 w-2 rounded-full ${statusColors['pending']}`}
></span>
Pending
</button>
)}
{allowMarkingLearning && (
<button
class="px-3 py-1.5 text-left text-sm text-gray-800 hover:bg-gray-100"
onClick={() => handleUpdateResourceProgress('learning')}
>
<span
class={`mr-2 inline-block h-2 w-2 rounded-full ${statusColors['learning']}`}
></span>
In Progress
</button>
)}
{allowMarkingSkipped && (
<button
class="px-3 py-1.5 text-left text-sm text-gray-800 hover:bg-gray-100"
onClick={() => handleUpdateResourceProgress('skipped')}
>
<span
class={`mr-2 inline-block h-2 w-2 rounded-full ${statusColors['skipped']}`}
></span>
Skip
</button>
)}
</div>
)}
</div>
);
if (isGuest) { if (isGuest) {
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">

@ -4,7 +4,7 @@ import { TOKEN_COOKIE_NAME } from './jwt';
import Element = astroHTML.JSX.Element; import Element = astroHTML.JSX.Element;
export type ResourceType = 'roadmap' | 'best-practice'; export type ResourceType = 'roadmap' | 'best-practice';
export type ResourceProgressType = 'done' | 'learning' | 'pending'; export type ResourceProgressType = 'done' | 'learning' | 'pending' | 'skipped';
type TopicMeta = { type TopicMeta = {
topicId: string; topicId: string;
@ -26,14 +26,18 @@ export async function getTopicStatus(
const { topicId, resourceType, resourceId } = topic; const { topicId, resourceType, resourceId } = topic;
const progressResult = await getResourceProgress(resourceType, resourceId); const progressResult = await getResourceProgress(resourceType, resourceId);
if (progressResult?.done.includes(topicId)) { if (progressResult?.done?.includes(topicId)) {
return 'done'; return 'done';
} }
if (progressResult?.learning.includes(topicId)) { if (progressResult?.learning?.includes(topicId)) {
return 'learning'; return 'learning';
} }
if (progressResult?.skipped?.includes(topicId)) {
return 'skipped';
}
return 'pending'; return 'pending';
} }
@ -46,6 +50,7 @@ export async function updateResourceProgress(
const { response, error } = await httpPost<{ const { response, error } = await httpPost<{
done: string[]; done: string[];
learning: string[]; learning: string[];
skipped: string[];
}>(`${import.meta.env.PUBLIC_API_URL}/v1-update-resource-progress`, { }>(`${import.meta.env.PUBLIC_API_URL}/v1-update-resource-progress`, {
topicId, topicId,
resourceType, resourceType,
@ -61,20 +66,23 @@ export async function updateResourceProgress(
resourceType, resourceType,
resourceId, resourceId,
response.done, response.done,
response.learning response.learning,
response.skipped,
); );
return response; return response;
} }
export async function getResourceProgress( export async function getResourceProgress(
resourceType: 'roadmap' | 'best-practice', resourceType: 'roadmap' | 'best-practice',
resourceId: string 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 // No need to load progress if user is not logged in
if (!Cookies.get(TOKEN_COOKIE_NAME)) { if (!Cookies.get(TOKEN_COOKIE_NAME)) {
return { return {
done: [], done: [],
learning: [], learning: [],
skipped: [],
}; };
} }
@ -101,31 +109,27 @@ async function loadFreshProgress(
const { response, error } = await httpGet<{ const { response, error } = await httpGet<{
done: string[]; done: string[];
learning: string[]; learning: string[];
skipped: string[];
}>(`${import.meta.env.PUBLIC_API_URL}/v1-get-user-resource-progress`, { }>(`${import.meta.env.PUBLIC_API_URL}/v1-get-user-resource-progress`, {
resourceType, resourceType,
resourceId, resourceId,
}); });
if (error) { if (error || !response) {
console.error(error); console.error(error);
return { return {
done: [], done: [],
learning: [], learning: [],
}; skipped: [],
}
if (!response?.done || !response?.learning) {
return {
done: [],
learning: [],
}; };
} }
setResourceProgress( setResourceProgress(
resourceType, resourceType,
resourceId, resourceId,
response.done, response?.done || [],
response.learning response?.learning || [],
response?.skipped || [],
); );
return response; return response;
@ -135,13 +139,15 @@ export function setResourceProgress(
resourceType: 'roadmap' | 'best-practice', resourceType: 'roadmap' | 'best-practice',
resourceId: string, resourceId: string,
done: string[], done: string[],
learning: string[] learning: string[],
skipped: string [],
): void { ): void {
localStorage.setItem( localStorage.setItem(
`${resourceType}-${resourceId}-progress`, `${resourceType}-${resourceId}-progress`,
JSON.stringify({ JSON.stringify({
done, done,
learning, learning,
skipped,
timestamp: new Date().getTime(), timestamp: new Date().getTime(),
}) })
); );
@ -152,6 +158,7 @@ export function renderTopicProgress(
topicProgress: ResourceProgressType topicProgress: ResourceProgressType
) { ) {
const isLearning = topicProgress === 'learning'; const isLearning = topicProgress === 'learning';
const isSkipped = topicProgress === 'skipped';
const isDone = topicProgress === 'done'; const isDone = topicProgress === 'done';
const matchingElements: Element[] = []; const matchingElements: Element[] = [];
@ -185,13 +192,15 @@ export function renderTopicProgress(
matchingElements.forEach((element) => { matchingElements.forEach((element) => {
if (isDone) { if (isDone) {
element.classList.add('done'); element.classList.add('done');
element.classList.remove('learning'); element.classList.remove('learning', 'skipped');
} else if (isLearning) { } else if (isLearning) {
element.classList.add('learning'); 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 { } else {
element.classList.remove('done'); element.classList.remove('done', 'skipped', 'learning');
element.classList.remove('learning');
} }
}); });
} }
@ -200,7 +209,7 @@ export async function renderResourceProgress(
resourceType: ResourceType, resourceType: ResourceType,
resourceId: string resourceId: string
) { ) {
const { done = [], learning = [] } = const { done = [], learning = [], skipped = [] } =
(await getResourceProgress(resourceType, resourceId)) || {}; (await getResourceProgress(resourceType, resourceId)) || {};
done.forEach((topicId) => { done.forEach((topicId) => {
@ -210,4 +219,8 @@ export async function renderResourceProgress(
learning.forEach((topicId) => { learning.forEach((topicId) => {
renderTopicProgress(topicId, 'learning'); renderTopicProgress(topicId, 'learning');
}); });
skipped.forEach((topicId) => {
renderTopicProgress(topicId, 'skipped');
});
} }

Loading…
Cancel
Save