Allow skipping

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

@ -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;

@ -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<ResourceProgressType, string> = {
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<ResourceProgressType>('pending');
const [showChangeStatus, setShowChangeStatus] = useState(false);
const changeStatusRef = useRef<HTMLDivElement>(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 (
<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) {
return (
<div className="flex items-center gap-2">

@ -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');
});
}

Loading…
Cancel
Save