diff --git a/src/components/CustomRoadmap/FlowRoadmapRenderer.tsx b/src/components/CustomRoadmap/FlowRoadmapRenderer.tsx
index d6ea54058..e0af4c770 100644
--- a/src/components/CustomRoadmap/FlowRoadmapRenderer.tsx
+++ b/src/components/CustomRoadmap/FlowRoadmapRenderer.tsx
@@ -13,6 +13,7 @@ import type { Node } from 'reactflow';
import { useCallback, type MouseEvent, useMemo, useState, useRef } from 'react';
import { EmptyRoadmap } from './EmptyRoadmap';
import { cn } from '../../lib/classname';
+import { totalRoadmapNodes } from '../../stores/roadmap.ts';
type FlowRoadmapRendererProps = {
roadmap: RoadmapDocument;
@@ -138,6 +139,12 @@ export function FlowRoadmapRenderer(props: FlowRoadmapRendererProps) {
)}
onRendered={() => {
renderResourceProgress('roadmap', roadmapId).then(() => {
+ totalRoadmapNodes.set(
+ roadmap?.nodes?.filter((node) => {
+ return ['topic', 'subtopic'].includes(node.type);
+ }).length || 0,
+ );
+
if (roadmap?.nodes?.length === 0) {
setHideRenderer(true);
editorWrapperRef?.current?.classList.add('hidden');
diff --git a/src/components/FrameRenderer/FrameRenderer.astro b/src/components/FrameRenderer/FrameRenderer.astro
index b96c8383f..620143da5 100644
--- a/src/components/FrameRenderer/FrameRenderer.astro
+++ b/src/components/FrameRenderer/FrameRenderer.astro
@@ -1,6 +1,7 @@
---
import Loader from '../Loader.astro';
import './FrameRenderer.css';
+import { ProgressNudge } from "./ProgressNudge";
export interface Props {
resourceType: 'roadmap' | 'best-practice';
@@ -27,4 +28,6 @@ const { resourceId, resourceType, dimensions = null } = Astro.props;
+
+
diff --git a/src/components/FrameRenderer/ProgressNudge.tsx b/src/components/FrameRenderer/ProgressNudge.tsx
new file mode 100644
index 000000000..4c9183616
--- /dev/null
+++ b/src/components/FrameRenderer/ProgressNudge.tsx
@@ -0,0 +1,83 @@
+import { Spinner } from '../ReactIcons/Spinner.tsx';
+import { useEffect, useState } from 'react';
+import { cn } from '../../lib/classname.ts';
+import { getUser } from '../../lib/jwt.ts';
+import { roadmapProgress, totalRoadmapNodes } from '../../stores/roadmap.ts';
+import { useStore } from '@nanostores/react';
+import {HelpCircle} from "lucide-react";
+
+type ProgressNudgeProps = {
+ resourceType: 'roadmap' | 'best-practice';
+ resourceId: string;
+};
+
+export function ProgressNudge(props: ProgressNudgeProps) {
+ const { resourceId, resourceType } = props;
+
+ const $totalRoadmapNodes = useStore(totalRoadmapNodes);
+ const $roadmapProgress = useStore(roadmapProgress);
+
+ const done = $roadmapProgress?.done?.length || 0;
+
+ const [isLoading, setIsLoading] = useState(true);
+ const { id: userId } = getUser() || {};
+
+ const hasProgress = done > 0;
+
+ useEffect(() => {
+ setTimeout(() => {
+ setIsLoading(false);
+ }, 500);
+ }, []);
+
+ if (!$totalRoadmapNodes) {
+ return null;
+ }
+
+ return (
+
+
+
+ Tip
+
+
+ Right-click on a topic to mark it as done.{' '}
+
+
+
+
+
+ Progress
+
+ {done} of {$totalRoadmapNodes} Done
+
+
+
+
+ );
+}
diff --git a/src/components/FrameRenderer/renderer.ts b/src/components/FrameRenderer/renderer.ts
index 1c364b3c5..415c8ffd6 100644
--- a/src/components/FrameRenderer/renderer.ts
+++ b/src/components/FrameRenderer/renderer.ts
@@ -218,6 +218,11 @@ export class Renderer {
const isCurrentStatusDone = targetGroup.classList.contains('done');
const normalizedGroupId = groupId.replace(/^\d+-/, '');
+
+ if (normalizedGroupId.startsWith('ext_link:')) {
+ return;
+ }
+
this.updateTopicStatus(
normalizedGroupId,
!isCurrentStatusDone ? 'done' : 'pending',
diff --git a/src/lib/resource-progress.ts b/src/lib/resource-progress.ts
index ae7eeb918..9a9fc9e23 100644
--- a/src/lib/resource-progress.ts
+++ b/src/lib/resource-progress.ts
@@ -3,6 +3,7 @@ import { httpGet, httpPost } from './http';
import { TOKEN_COOKIE_NAME, getUser } from './jwt';
// @ts-ignore
import Element = astroHTML.JSX.Element;
+import { roadmapProgress, totalRoadmapNodes } from '../stores/roadmap.ts';
export type ResourceType = 'roadmap' | 'best-practice';
export type ResourceProgressType =
@@ -27,7 +28,7 @@ export async function isTopicDone(topic: TopicMeta): Promise {
}
export async function getTopicStatus(
- topic: TopicMeta
+ topic: TopicMeta,
): Promise {
const { topicId, resourceType, resourceId } = topic;
const progressResult = await getResourceProgress(resourceType, resourceId);
@@ -49,7 +50,7 @@ export async function getTopicStatus(
export async function updateResourceProgress(
topic: TopicMeta,
- progressType: ResourceProgressType
+ progressType: ResourceProgressType,
) {
const { topicId, resourceType, resourceId } = topic;
@@ -74,7 +75,7 @@ export async function updateResourceProgress(
resourceId,
response.done,
response.learning,
- response.skipped
+ response.skipped,
);
return response;
@@ -82,7 +83,7 @@ export async function updateResourceProgress(
export async function getResourceProgress(
resourceType: 'roadmap' | 'best-practice',
- resourceId: string
+ resourceId: 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)) {
@@ -109,6 +110,14 @@ export async function getResourceProgress(
if (!progress || isProgressExpired) {
return loadFreshProgress(resourceType, resourceId);
+ } else {
+ setResourceProgress(
+ resourceType,
+ resourceId,
+ progress?.done || [],
+ progress?.learning || [],
+ progress?.skipped || [],
+ );
}
// Dispatch event to update favorite status in the MarkFavorite component
@@ -119,7 +128,7 @@ export async function getResourceProgress(
resourceId,
isFavorite,
},
- })
+ }),
);
return progress;
@@ -127,7 +136,7 @@ export async function getResourceProgress(
async function loadFreshProgress(
resourceType: ResourceType,
- resourceId: string
+ resourceId: string,
) {
const { response, error } = await httpGet<{
done: string[];
@@ -153,7 +162,7 @@ async function loadFreshProgress(
resourceId,
response?.done || [],
response?.learning || [],
- response?.skipped || []
+ response?.skipped || [],
);
// Dispatch event to update favorite status in the MarkFavorite component
@@ -164,7 +173,7 @@ async function loadFreshProgress(
resourceId,
isFavorite: response.isFavorite,
},
- })
+ }),
);
return response;
@@ -175,8 +184,14 @@ export function setResourceProgress(
resourceId: string,
done: string[],
learning: string[],
- skipped: string[]
+ skipped: string[],
): void {
+ roadmapProgress.set({
+ done,
+ learning,
+ skipped,
+ });
+
const userId = getUser()?.id;
localStorage.setItem(
`${resourceType}-${resourceId}-${userId}-progress`,
@@ -185,13 +200,13 @@ export function setResourceProgress(
learning,
skipped,
timestamp: new Date().getTime(),
- })
+ }),
);
}
export function topicSelectorAll(
topicId: string,
- parentElement: Document | SVGElement | HTMLDivElement = document
+ parentElement: Document | SVGElement | HTMLDivElement = document,
): Element[] {
const matchingElements: Element[] = [];
@@ -215,7 +230,7 @@ export function topicSelectorAll(
`[data-node-id="${topicId}"]`, // Matching custom roadmap nodes
`[data-id="${topicId}"]`, // Matching custom roadmap nodes
],
- parentElement
+ parentElement,
).forEach((element) => {
matchingElements.push(element);
});
@@ -225,7 +240,7 @@ export function topicSelectorAll(
export function renderTopicProgress(
topicId: string,
- topicProgress: ResourceProgressType
+ topicProgress: ResourceProgressType,
) {
const isLearning = topicProgress === 'learning';
const isSkipped = topicProgress === 'skipped';
@@ -268,7 +283,7 @@ export function clearResourceProgress() {
export async function renderResourceProgress(
resourceType: ResourceType,
- resourceId: string
+ resourceId: string,
) {
const {
done = [],
@@ -293,7 +308,7 @@ export async function renderResourceProgress(
function getMatchingElements(
quries: string[],
- parentElement: Document | SVGElement | HTMLDivElement = document
+ parentElement: Document | SVGElement | HTMLDivElement = document,
): Element[] {
const matchingElements: Element[] = [];
quries.forEach((query) => {
@@ -306,7 +321,7 @@ function getMatchingElements(
export function refreshProgressCounters() {
const progressNumsContainers = document.querySelectorAll(
- '[data-progress-nums-container]'
+ '[data-progress-nums-container]',
);
const progressNums = document.querySelectorAll('[data-progress-nums]');
if (progressNumsContainers.length === 0 || progressNums.length === 0) {
@@ -322,27 +337,27 @@ export function refreshProgressCounters() {
]).length;
const externalLinks = document.querySelectorAll(
- '[data-group-id^="ext_link:"]'
+ '[data-group-id^="ext_link:"]',
).length;
const roadmapSwitchers = document.querySelectorAll(
- '[data-group-id^="json:"]'
+ '[data-group-id^="json:"]',
).length;
const checkBoxes = document.querySelectorAll(
- '[data-group-id^="check:"]'
+ '[data-group-id^="check:"]',
).length;
const totalCheckBoxesDone = document.querySelectorAll(
- '[data-group-id^="check:"].done'
+ '[data-group-id^="check:"].done',
).length;
const totalCheckBoxesLearning = document.querySelectorAll(
- '[data-group-id^="check:"].learning'
+ '[data-group-id^="check:"].learning',
).length;
const totalCheckBoxesSkipped = document.querySelectorAll(
- '[data-group-id^="check:"].skipped'
+ '[data-group-id^="check:"].skipped',
).length;
const totalRemoved = document.querySelectorAll(
- '.clickable-group.removed'
+ '.clickable-group.removed',
).length;
const totalItems =
totalClickable -
@@ -351,6 +366,8 @@ export function refreshProgressCounters() {
checkBoxes -
totalRemoved;
+ totalRoadmapNodes.set(totalItems);
+
const totalDone =
getMatchingElements([
'.clickable-group.done:not([data-group-id^="ext_link:"])',
@@ -373,47 +390,47 @@ export function refreshProgressCounters() {
const doneCountEls = document.querySelectorAll('[data-progress-done]');
if (doneCountEls.length > 0) {
doneCountEls.forEach(
- (doneCountEl) => (doneCountEl.innerHTML = `${totalDone}`)
+ (doneCountEl) => (doneCountEl.innerHTML = `${totalDone}`),
);
}
const learningCountEls = document.querySelectorAll(
- '[data-progress-learning]'
+ '[data-progress-learning]',
);
if (learningCountEls.length > 0) {
learningCountEls.forEach(
- (learningCountEl) => (learningCountEl.innerHTML = `${totalLearning}`)
+ (learningCountEl) => (learningCountEl.innerHTML = `${totalLearning}`),
);
}
const skippedCountEls = document.querySelectorAll('[data-progress-skipped]');
if (skippedCountEls.length > 0) {
skippedCountEls.forEach(
- (skippedCountEl) => (skippedCountEl.innerHTML = `${totalSkipped}`)
+ (skippedCountEl) => (skippedCountEl.innerHTML = `${totalSkipped}`),
);
}
const totalCountEls = document.querySelectorAll('[data-progress-total]');
if (totalCountEls.length > 0) {
totalCountEls.forEach(
- (totalCountEl) => (totalCountEl.innerHTML = `${totalItems}`)
+ (totalCountEl) => (totalCountEl.innerHTML = `${totalItems}`),
);
}
const progressPercentage =
Math.round(((totalDone + totalSkipped) / totalItems) * 100) || 0;
const progressPercentageEls = document.querySelectorAll(
- '[data-progress-percentage]'
+ '[data-progress-percentage]',
);
if (progressPercentageEls.length > 0) {
progressPercentageEls.forEach(
(progressPercentageEl) =>
- (progressPercentageEl.innerHTML = `${progressPercentage}`)
+ (progressPercentageEl.innerHTML = `${progressPercentage}`),
);
}
progressNumsContainers.forEach((progressNumsContainer) =>
- progressNumsContainer.classList.remove('striped-loader')
+ progressNumsContainer.classList.remove('striped-loader'),
);
progressNums.forEach((progressNum) => {
progressNum.classList.remove('opacity-0');
diff --git a/src/stores/roadmap.ts b/src/stores/roadmap.ts
index 0cfaddead..ca22c9cd5 100644
--- a/src/stores/roadmap.ts
+++ b/src/stores/roadmap.ts
@@ -4,9 +4,14 @@ import type { GetRoadmapResponse } from '../components/CustomRoadmap/CustomRoadm
export const currentRoadmap = atom(undefined);
export const isCurrentRoadmapPersonal = computed(
currentRoadmap,
- (roadmap) => !roadmap?.teamId
+ (roadmap) => !roadmap?.teamId,
);
export const canManageCurrentRoadmap = computed(
currentRoadmap,
- (roadmap) => roadmap?.canManage
+ (roadmap) => roadmap?.canManage,
);
+
+export const roadmapProgress = atom<
+ { done: string[]; learning: string[]; skipped: string[] } | undefined
+>();
+export const totalRoadmapNodes = atom();
diff --git a/tailwind.config.cjs b/tailwind.config.cjs
index 2e78d96a0..49f032d0d 100644
--- a/tailwind.config.cjs
+++ b/tailwind.config.cjs
@@ -1,8 +1,8 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
- './src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue,svg}',
- './editor/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue,svg}'
+ './src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue,svg}',
+ './editor/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue,svg}',
],
future: {
hoverOnlyWhenSupported: true,