mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-01-17 14:18:17 +01:00
chore: added pending state for topics
This commit is contained in:
parent
6591c36ef4
commit
1cea9d0e13
@ -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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
1
src/icons/progress.svg
Normal file
1
src/icons/progress.svg
Normal file
@ -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…
x
Reference in New Issue
Block a user