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; 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 { svg .clickable-group.done[data-group-id^='check:'] rect {
fill: gray !important; fill: gray !important;
stroke: gray; stroke: gray;

@ -1,5 +1,6 @@
import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
import CheckIcon from '../../icons/check.svg'; import CheckIcon from '../../icons/check.svg';
import ProgressIcon from '../../icons/progress.svg';
import CloseIcon from '../../icons/close.svg'; import CloseIcon from '../../icons/close.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';
@ -11,10 +12,12 @@ import { useToggleTopic } from '../../hooks/use-toggle-topic';
import { httpGet } from '../../lib/http'; import { httpGet } from '../../lib/http';
import { isLoggedIn } from '../../lib/jwt'; import { isLoggedIn } from '../../lib/jwt';
import { import {
getTopicStatus,
isTopicDone, isTopicDone,
renderTopicProgress, renderTopicProgress,
ResourceProgressType,
ResourceType, ResourceType,
toggleMarkTopicDone as toggleMarkTopicDoneApi, updateResourceProgress as updateResourceProgressApi,
} from '../../lib/resource-progress'; } from '../../lib/resource-progress';
import { pageLoadingMessage, sponsorHidden } from '../../stores/page'; import { pageLoadingMessage, sponsorHidden } from '../../stores/page';
@ -24,6 +27,7 @@ export function TopicDetail() {
const [error, setError] = useState(''); const [error, setError] = useState('');
const [topicHtml, setTopicHtml] = useState(''); const [topicHtml, setTopicHtml] = useState('');
const [progress, setProgress] = useState<ResourceProgressType>('pending');
const [isDone, setIsDone] = useState<boolean>(); const [isDone, setIsDone] = useState<boolean>();
const [isUpdatingProgress, setIsUpdatingProgress] = useState(true); const [isUpdatingProgress, setIsUpdatingProgress] = useState(true);
@ -49,13 +53,24 @@ export function TopicDetail() {
} }
}; };
const toggleMarkTopicDone = (isDone: boolean) => { const handleUpdateResourceProgress = (progress: ResourceProgressType) => {
setIsUpdatingProgress(true); setIsUpdatingProgress(true);
toggleMarkTopicDoneApi({ topicId, resourceId, resourceType }, isDone) updateResourceProgressApi(
{
topicId,
resourceId,
resourceType,
},
progress
)
.then(() => { .then(() => {
setIsDone(isDone); setProgress(progress);
setIsActive(false); setIsActive(false);
renderTopicProgress(topicId, isDone); renderTopicProgress(
topicId,
progress === 'done',
progress === 'learning'
);
}) })
.catch((err) => { .catch((err) => {
alert(err.message); alert(err.message);
@ -73,10 +88,10 @@ export function TopicDetail() {
} }
setIsUpdatingProgress(true); setIsUpdatingProgress(true);
isTopicDone({ topicId, resourceId, resourceType }) getTopicStatus({ topicId, resourceId, resourceType })
.then((status: boolean) => { .then((status) => {
setIsUpdatingProgress(false); setIsUpdatingProgress(false);
setIsDone(status); setProgress(status);
}) })
.catch(console.error); .catch(console.error);
}, [topicId, resourceId, resourceType]); }, [topicId, resourceId, resourceType]);
@ -104,16 +119,19 @@ export function TopicDetail() {
// Toggle the topic status // Toggle the topic status
isTopicDone({ topicId, resourceId, resourceType }) isTopicDone({ topicId, resourceId, resourceType })
.then((oldIsDone) => { .then((oldIsDone) => {
return toggleMarkTopicDoneApi( return updateResourceProgressApi(
{ {
topicId, topicId,
resourceId, resourceId,
resourceType, resourceType,
}, },
!oldIsDone oldIsDone ? 'pending' : 'done'
); );
}) })
.then((newIsDone) => renderTopicProgress(topicId, newIsDone)) .then((updatedResult) => {
const newIsDone = updatedResult.done.includes(topicId);
renderTopicProgress(topicId, newIsDone, false);
})
.catch((err) => { .catch((err) => {
alert(err.message); alert(err.message);
console.error(err); console.error(err);
@ -193,14 +211,24 @@ export function TopicDetail() {
{/* Actions for the topic */} {/* Actions for the topic */}
<div className="mb-2"> <div className="mb-2">
{isGuest && ( {isGuest && (
<button <div className="flex items-center gap-2">
data-popup="login-popup" <button
className="inline-flex items-center rounded-md bg-green-600 p-1 px-2 text-sm text-white hover:bg-green-700" data-popup="login-popup"
onClick={() => setIsActive(false)} 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> <img alt="Check" class="w-3" src={CheckIcon} />
</button> <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 && ( {!isGuest && (
@ -215,25 +243,54 @@ export function TopicDetail() {
<span className="ml-2">Updating Status..</span> <span className="ml-2">Updating Status..</span>
</button> </button>
)} )}
{!isUpdatingProgress && !isDone && ( {!isUpdatingProgress && progress === 'pending' && (
<button <div className="flex items-center gap-2">
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" <button
onClick={() => toggleMarkTopicDone(true)} 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> <img alt="Check" class="w-3" src={CheckIcon} />
</button> <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 <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" 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} /> <img alt="Check" class="h-4" src={ResetIcon} />
<span className="ml-2">Mark as Pending</span> <span className="ml-2">Mark as Pending</span>
</button> </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 Cookies from 'js-cookie';
import { TOKEN_COOKIE_NAME } from './jwt'; 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';
type TopicMeta = { type TopicMeta = {
topicId: string; topicId: string;
@ -13,47 +14,71 @@ type TopicMeta = {
export async function isTopicDone(topic: TopicMeta): Promise<boolean> { export async function isTopicDone(topic: TopicMeta): Promise<boolean> {
const { topicId, resourceType, resourceId } = topic; 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 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, topic: TopicMeta,
isDone: boolean progressType: ResourceProgressType
): Promise<boolean> { ) {
const { topicId, resourceType, resourceId } = topic; const { topicId, resourceType, resourceId } = topic;
const { response, error } = await httpPatch<{ done: string[] }>( const { response, error } = await httpPost<{
`${import.meta.env.PUBLIC_API_URL}/v1-toggle-mark-resource-done`, done: string[];
{ learning: string[];
topicId, }>(`${import.meta.env.PUBLIC_API_URL}/v1-update-resource-progress`, {
resourceType, topicId,
resourceId, resourceType,
isDone, resourceId,
} progress: progressType,
); });
if (error || !response?.done) { if (error || !response?.done || !response?.learning) {
throw new Error(error?.message || 'Something went wrong'); throw new Error(error?.message || 'Something went wrong');
} }
setResourceProgress(resourceType, resourceId, response.done); setResourceProgress(
resourceType,
return isDone; resourceId,
response.done,
response.learning
);
return response;
} }
export async function getResourceProgress( export async function getResourceProgress(
resourceType: 'roadmap' | 'best-practice', resourceType: 'roadmap' | 'best-practice',
resourceId: string resourceId: string
): Promise<string[]> { ): Promise<{ done: string[]; learning: 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: [],
learning: [],
};
} }
const progressKey = `${resourceType}-${resourceId}-progress`; const progressKey = `${resourceType}-${resourceId}-progress`;
@ -69,50 +94,67 @@ export async function getResourceProgress(
return loadFreshProgress(resourceType, resourceId); return loadFreshProgress(resourceType, resourceId);
} }
return progress.done; return progress;
} }
async function loadFreshProgress( async function loadFreshProgress(
resourceType: ResourceType, resourceType: ResourceType,
resourceId: string resourceId: string
) { ) {
const { response, error } = await httpGet<{ done: string[] }>( const { response, error } = await httpGet<{
`${import.meta.env.PUBLIC_API_URL}/v1-get-user-resource-progress`, done: string[];
{ learning: string[];
resourceType, }>(`${import.meta.env.PUBLIC_API_URL}/v1-get-user-resource-progress`, {
resourceId, resourceType,
} resourceId,
); });
if (error) { if (error) {
console.error(error); console.error(error);
return []; return {
done: [],
learning: [],
};
} }
if (!response?.done) { if (!response?.done || !response?.learning) {
return []; return {
done: [],
learning: [],
};
} }
setResourceProgress(resourceType, resourceId, response.done); setResourceProgress(
resourceType,
resourceId,
response.done,
response.learning
);
return response.done; return response;
} }
export function setResourceProgress( export function setResourceProgress(
resourceType: 'roadmap' | 'best-practice', resourceType: 'roadmap' | 'best-practice',
resourceId: string, resourceId: string,
done: string[] done: string[],
learning: string[]
): void { ): void {
localStorage.setItem( localStorage.setItem(
`${resourceType}-${resourceId}-progress`, `${resourceType}-${resourceId}-progress`,
JSON.stringify({ JSON.stringify({
done, done,
learning,
timestamp: new Date().getTime(), timestamp: new Date().getTime(),
}) })
); );
} }
export function renderTopicProgress(topicId: string, isDone: boolean) { export function renderTopicProgress(
topicId: string,
isDone: boolean,
isLearning: boolean
) {
const matchingElements: Element[] = []; const matchingElements: Element[] = [];
// Elements having sort order in the beginning of the group id // 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) => { matchingElements.forEach((element) => {
if (isDone) { if (isDone) {
element.classList.add('done'); element.classList.add('done');
} else if (isLearning) {
element.classList.add('learning');
} else { } else {
element.classList.remove('done'); element.classList.remove('done');
element.classList.remove('learning');
} }
}); });
} }
@ -157,7 +202,11 @@ export async function renderResourceProgress(
) { ) {
const progress = await getResourceProgress(resourceType, resourceId); const progress = await getResourceProgress(resourceType, resourceId);
progress.forEach((topicId) => { progress.done.forEach((topicId) => {
renderTopicProgress(topicId, true); renderTopicProgress(topicId, true, false);
});
progress.learning.forEach((topicId) => {
renderTopicProgress(topicId, false, true);
}); });
} }

Loading…
Cancel
Save