import Cookies from 'js-cookie'; import { httpGet, httpPost } from './http'; import { TOKEN_COOKIE_NAME } from './jwt'; import Element = astroHTML.JSX.Element; export type ResourceType = 'roadmap' | 'best-practice'; export type ResourceProgressType = 'done' | 'learning' | 'pending' | 'skipped'; type TopicMeta = { topicId: string; resourceType: ResourceType; resourceId: string; }; export async function isTopicDone(topic: TopicMeta): Promise { const { topicId, resourceType, resourceId } = topic; const { done = [] } = (await getResourceProgress(resourceType, resourceId)) || {}; return done?.includes(topicId); } export async function getTopicStatus( topic: TopicMeta ): Promise { 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'; } if (progressResult?.skipped?.includes(topicId)) { return 'skipped'; } return 'pending'; } export async function updateResourceProgress( topic: TopicMeta, progressType: ResourceProgressType ) { const { topicId, resourceType, resourceId } = topic; const { response, error } = await httpPost<{ done: string[]; learning: string[]; skipped: string[]; }>(`${import.meta.env.PUBLIC_API_URL}/v1-update-resource-progress`, { topicId, resourceType, resourceId, progress: progressType, }); if (error || !response?.done || !response?.learning) { throw new Error(error?.message || 'Something went wrong'); } setResourceProgress( resourceType, resourceId, response.done, response.learning, response.skipped, ); return response; } export async function getResourceProgress( resourceType: 'roadmap' | 'best-practice', 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)) { return { done: [], learning: [], skipped: [], }; } const progressKey = `${resourceType}-${resourceId}-progress`; const rawProgress = localStorage.getItem(progressKey); const progress = JSON.parse(rawProgress || 'null'); const progressTimestamp = progress?.timestamp; const diff = new Date().getTime() - parseInt(progressTimestamp || '0', 10); const isProgressExpired = diff > 15 * 60 * 1000; // 15 minutes if (!progress || isProgressExpired) { return loadFreshProgress(resourceType, resourceId); } return progress; } async function loadFreshProgress( resourceType: ResourceType, resourceId: string ) { 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 || !response) { console.error(error); return { done: [], learning: [], skipped: [], }; } setResourceProgress( resourceType, resourceId, response?.done || [], response?.learning || [], response?.skipped || [], ); return response; } export function setResourceProgress( resourceType: 'roadmap' | 'best-practice', resourceId: string, done: string[], learning: string[], skipped: string [], ): void { localStorage.setItem( `${resourceType}-${resourceId}-progress`, JSON.stringify({ done, learning, skipped, timestamp: new Date().getTime(), }) ); } export function renderTopicProgress( topicId: string, topicProgress: ResourceProgressType ) { const isLearning = topicProgress === 'learning'; const isSkipped = topicProgress === 'skipped'; const isDone = topicProgress === 'done'; const matchingElements: Element[] = []; // Elements having sort order in the beginning of the group id document .querySelectorAll(`[data-group-id$="-${topicId}"]`) .forEach((element: unknown) => { const foundGroupId = (element as HTMLOrSVGElement)?.dataset?.groupId || ''; const validGroupRegex = new RegExp(`^\\d+-${topicId}$`); if (validGroupRegex.test(foundGroupId)) { matchingElements.push(element); } }); // Elements with exact match of the topic id document .querySelectorAll(`[data-group-id="${topicId}"]`) .forEach((element) => { matchingElements.push(element); }); // Matching "check:XXXX" box of the topic document .querySelectorAll(`[data-group-id="check:${topicId}"]`) .forEach((element) => { matchingElements.push(element); }); matchingElements.forEach((element) => { if (isDone) { element.classList.add('done'); element.classList.remove('learning', 'skipped'); } else if (isLearning) { element.classList.add('learning'); element.classList.remove('done', 'skipped'); } else if (isSkipped) { element.classList.add('skipped'); element.classList.remove('done', 'learning'); } else { element.classList.remove('done', 'skipped', 'learning'); } }); } export async function renderResourceProgress( resourceType: ResourceType, resourceId: string ) { const { done = [], learning = [], skipped = [] } = (await getResourceProgress(resourceType, resourceId)) || {}; done.forEach((topicId) => { renderTopicProgress(topicId, 'done'); }); learning.forEach((topicId) => { renderTopicProgress(topicId, 'learning'); }); skipped.forEach((topicId) => { renderTopicProgress(topicId, 'skipped'); }); }