mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-08-25 18:20:46 +02:00
Allow skipping
This commit is contained in:
@@ -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,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 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';
|
||||||
import { isLoggedIn } from '../../lib/jwt';
|
import { isLoggedIn } from '../../lib/jwt';
|
||||||
import {
|
import {
|
||||||
ResourceProgressType,
|
ResourceProgressType,
|
||||||
ResourceType,
|
ResourceType,
|
||||||
getTopicStatus,
|
getTopicStatus,
|
||||||
renderTopicProgress,
|
renderTopicProgress,
|
||||||
updateResourceProgress,
|
updateResourceProgress,
|
||||||
} from '../../lib/resource-progress';
|
} from '../../lib/resource-progress';
|
||||||
|
|
||||||
type TopicProgressButtonProps = {
|
type TopicProgressButtonProps = {
|
||||||
@@ -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');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user