chore: added pending state for topics

pull/3904/head
Arik Chakma 2 years ago
parent 6591c36ef4
commit 1cea9d0e13
  1. 8
      src/components/FrameRenderer/FrameRenderer.css
  2. 115
      src/components/TopicDetail/TopicDetail.tsx
  3. 1
      src/icons/progress.svg
  4. 127
      src/lib/resource-progress.ts

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

@ -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<ResourceProgressType>('pending');
const [isDone, setIsDone] = useState<boolean>();
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 */}
<div className="mb-2">
{isGuest && (
<button
data-popup="login-popup"
className="inline-flex items-center rounded-md bg-green-600 p-1 px-2 text-sm text-white hover:bg-green-700"
onClick={() => setIsActive(false)}
>
<img alt="Check" class='w-3' src={CheckIcon} />
<span className="ml-2">Mark as Done</span>
</button>
<div className="flex items-center gap-2">
<button
data-popup="login-popup"
className="inline-flex items-center rounded-md bg-green-600 p-1 px-2 text-sm text-white hover:bg-green-700"
onClick={() => setIsActive(false)}
>
<img alt="Check" class="w-3" src={CheckIcon} />
<span className="ml-2">Mark as Done</span>
</button>
<button
data-popup="login-popup"
class="inline-flex items-center rounded-md bg-gray-800 p-1 px-2 text-sm text-white hover:bg-black"
onClick={() => setIsActive(false)}
>
<img alt="Learning" class="w-3" src={ProgressIcon} />
<span class="ml-2">In Progress</span>
</button>
</div>
)}
{!isGuest && (
@ -215,25 +243,54 @@ export function TopicDetail() {
<span className="ml-2">Updating Status..</span>
</button>
)}
{!isUpdatingProgress && !isDone && (
<button
className="inline-flex items-center rounded-md border border-green-600 bg-green-600 p-1 px-2 text-sm text-white hover:bg-green-700"
onClick={() => toggleMarkTopicDone(true)}
>
<img alt="Check" class="w-3" src={CheckIcon} />
<span className="ml-2">Mark as Done</span>
</button>
{!isUpdatingProgress && progress === 'pending' && (
<div className="flex items-center gap-2">
<button
className="inline-flex items-center rounded-md border border-green-600 bg-green-600 p-1 px-2 text-sm text-white hover:bg-green-700"
onClick={() => handleUpdateResourceProgress('done')}
>
<img alt="Check" class="w-3" src={CheckIcon} />
<span className="ml-2">Mark as Done</span>
</button>
<button
className="inline-flex items-center rounded-md border border-gray-800 bg-gray-800 p-1 px-2 text-sm text-white hover:bg-black"
onClick={() => handleUpdateResourceProgress('learning')}
>
<img alt="Learning" class="w-3" src={ProgressIcon} />
<span className="ml-2">In Progress</span>
</button>
</div>
)}
{!isUpdatingProgress && isDone && (
{!isUpdatingProgress && progress === 'done' && (
<button
className="inline-flex items-center rounded-md border border-red-600 bg-red-600 p-1 px-2 text-sm text-white hover:bg-red-700"
onClick={() => toggleMarkTopicDone(false)}
onClick={() => handleUpdateResourceProgress('pending')}
>
<img alt="Check" class="h-4" src={ResetIcon} />
<span className="ml-2">Mark as Pending</span>
</button>
)}
{!isUpdatingProgress && progress === 'learning' && (
<div className="flex items-center gap-2">
<button
className="inline-flex items-center rounded-md border border-green-600 bg-green-600 p-1 px-2 text-sm text-white hover:bg-green-700"
onClick={() => handleUpdateResourceProgress('done')}
>
<img alt="Check" class="w-3" src={CheckIcon} />
<span className="ml-2">Mark as Done</span>
</button>
<button
className="inline-flex items-center rounded-md border border-red-600 bg-red-600 p-1 px-2 text-sm text-white hover:bg-red-700"
onClick={() => handleUpdateResourceProgress('pending')}
>
<img alt="Check" class="h-4" src={ResetIcon} />
<span className="ml-2">Mark as Pending</span>
</button>
</div>
)}
</>
)}

@ -0,0 +1 @@
<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="512px" height="512px"> <path fill="white" d="M 10.017578 2.2246094 C 9.9418633 2.2236094 9.8629062 2.2302969 9.7851562 2.2480469 C 8.6621563 2.5020469 7.6129688 2.948875 6.6679688 3.546875 C 6.1309688 3.886875 6.0607656 4.6467031 6.5097656 5.0957031 L 6.5117188 5.0976562 C 6.8337188 5.4196562 7.3386563 5.4959531 7.7226562 5.2519531 C 8.4896562 4.7639531 9.3448125 4.4032187 10.257812 4.1992188 C 10.700812 4.1002187 11 3.6854219 11 3.2324219 C 11 2.6741719 10.547582 2.2316094 10.017578 2.2246094 z M 13.984375 2.2246094 C 13.45418 2.2322793 13 2.6741719 13 3.2324219 L 13 3.234375 C 13 3.692375 13.308859 4.1001719 13.755859 4.2011719 C 17.324859 5.0031719 20 8.193 20 12 C 20 15.807 17.324859 18.996828 13.755859 19.798828 C 13.308859 19.899828 13 20.307625 13 20.765625 L 13 20.767578 C 13 21.405578 13.592844 21.893953 14.214844 21.751953 C 18.665844 20.741953 22 16.753 22 12 C 22 7.247 18.665844 3.2590469 14.214844 2.2480469 C 14.137094 2.2304219 14.060117 2.2235137 13.984375 2.2246094 z M 4.2792969 6.21875 C 3.9904219 6.247 3.716875 6.3994688 3.546875 6.6679688 C 2.948875 7.6129688 2.5030469 8.6621563 2.2480469 9.7851562 C 2.1070469 10.407156 2.5944219 11 3.2324219 11 C 3.6854219 11 4.1002187 10.699813 4.1992188 10.257812 C 4.4022188 9.3438125 4.7639531 8.4896562 5.2519531 7.7226562 C 5.4959531 7.3386562 5.4196562 6.8337187 5.0976562 6.5117188 L 5.0957031 6.5097656 C 4.8712031 6.2852656 4.5681719 6.1905 4.2792969 6.21875 z M 15.980469 8.9902344 A 1.0001 1.0001 0 0 0 15.292969 9.2929688 L 11 13.585938 L 9.7070312 12.292969 A 1.0001 1.0001 0 1 0 8.2929688 13.707031 L 10.292969 15.707031 A 1.0001 1.0001 0 0 0 11.707031 15.707031 L 16.707031 10.707031 A 1.0001 1.0001 0 0 0 15.980469 8.9902344 z M 3.2324219 13 C 2.5944219 13 2.1060469 13.592844 2.2480469 14.214844 C 2.5030469 15.337844 2.947875 16.387031 3.546875 17.332031 C 3.885875 17.869031 4.6467031 17.939234 5.0957031 17.490234 C 5.4187031 17.167234 5.4959531 16.661344 5.2519531 16.277344 C 4.7639531 15.510344 4.4032187 14.655187 4.1992188 13.742188 C 4.1002187 13.299188 3.6854219 13 3.2324219 13 z M 7.0957031 18.613281 C 6.8809531 18.642031 6.6727187 18.741344 6.5117188 18.902344 L 6.5097656 18.904297 C 6.0607656 19.353297 6.1309688 20.113125 6.6679688 20.453125 C 7.6129688 21.051125 8.6621563 21.496953 9.7851562 21.751953 C 10.407156 21.892953 11 21.405578 11 20.767578 C 11 20.314578 10.699813 19.899781 10.257812 19.800781 C 9.3448125 19.596781 8.4906094 19.236047 7.7246094 18.748047 C 7.5326094 18.626047 7.3104531 18.584531 7.0957031 18.613281 z"/></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

@ -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<boolean> {
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<ResourceProgressType> {
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<boolean> {
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<string[]> {
): 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);
});
}

Loading…
Cancel
Save