mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-08-24 09:55:57 +02:00
Allow skipping
This commit is contained in:
@@ -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');
|
||||
});
|
||||
}
|
||||
|
Reference in New Issue
Block a user