mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2026-03-13 18:21:57 +08:00
Compare commits
1 Commits
fix/todo
...
chore/cach
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47926c496d |
@@ -20,12 +20,11 @@ export function MarkFavorite({
|
||||
favorite,
|
||||
className,
|
||||
}: MarkFavoriteType) {
|
||||
const localStorageKey = `${resourceType}-${resourceId}-favorite`;
|
||||
|
||||
const toast = useToast();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isFavorite, setIsFavorite] = useState(
|
||||
favorite ?? localStorage.getItem(localStorageKey) === '1'
|
||||
favorite || false
|
||||
);
|
||||
|
||||
async function toggleFavoriteHandler(e: Event) {
|
||||
@@ -82,7 +81,6 @@ export function MarkFavorite({
|
||||
} = (e as CustomEvent).detail;
|
||||
if (id === resourceId && type === resourceType) {
|
||||
setIsFavorite(fav);
|
||||
localStorage.setItem(localStorageKey, fav ? '1' : '0');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -228,8 +228,18 @@ export class Renderer {
|
||||
}
|
||||
|
||||
e.stopImmediatePropagation();
|
||||
let status: ResourceProgressType = 'pending';
|
||||
if (targetGroup.classList.contains('done')) {
|
||||
status = 'done';
|
||||
} else if (targetGroup.classList.contains('learning')) {
|
||||
status = 'learning';
|
||||
} else if (targetGroup.classList.contains('skipped')) {
|
||||
status = 'skipped';
|
||||
} else if (targetGroup.classList.contains('removed')) {
|
||||
status = 'removed';
|
||||
}
|
||||
|
||||
if (targetGroup.classList.contains('removed')) {
|
||||
if (status === 'removed') {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -263,6 +273,7 @@ export class Renderer {
|
||||
topicId: groupId.replace('check:', ''),
|
||||
resourceType: this.resourceType,
|
||||
resourceId: this.resourceId,
|
||||
status,
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -300,6 +311,7 @@ export class Renderer {
|
||||
topicId: normalizedGroupId,
|
||||
resourceId: this.resourceId,
|
||||
resourceType: this.resourceType,
|
||||
status,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
@@ -44,6 +44,8 @@ export function TeamVersions(props: TeamVersionsProps) {
|
||||
const [selectedTeamVersion, setSelectedTeamVersion] = useState<
|
||||
TeamVersionsResponse[0] | null
|
||||
>(null);
|
||||
const [shouldStartLoading, setShouldStartLoading] = useState(false);
|
||||
|
||||
let shouldShowAvatar = true;
|
||||
const selectedAvatar = selectedTeamVersion
|
||||
? selectedTeamVersion.team.avatar
|
||||
@@ -88,6 +90,7 @@ export function TeamVersions(props: TeamVersionsProps) {
|
||||
if (teamId) {
|
||||
const foundVersion = response.find((v) => v.team._id === teamId) || null;
|
||||
setSelectedTeamVersion(foundVersion);
|
||||
setShouldStartLoading(true);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -110,7 +113,10 @@ export function TeamVersions(props: TeamVersionsProps) {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
clearResourceProgress();
|
||||
if (!shouldStartLoading) {
|
||||
return;
|
||||
}
|
||||
clearResourceProgress('removed');
|
||||
if (!selectedTeamVersion) {
|
||||
deleteUrlParam('t');
|
||||
renderResourceProgress(resourceType, resourceId).then();
|
||||
@@ -125,6 +131,7 @@ export function TeamVersions(props: TeamVersionsProps) {
|
||||
});
|
||||
refreshProgressCounters();
|
||||
});
|
||||
setShouldStartLoading(true);
|
||||
}, [selectedTeamVersion]);
|
||||
|
||||
if (!teamVersions.length) {
|
||||
@@ -204,6 +211,7 @@ export function TeamVersions(props: TeamVersionsProps) {
|
||||
onClick={() => {
|
||||
setSelectedTeamVersion(team);
|
||||
setIsDropdownOpen(false);
|
||||
setShouldStartLoading(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
|
||||
@@ -9,9 +9,9 @@ import { useToggleTopic } from '../../hooks/use-toggle-topic';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { isLoggedIn } from '../../lib/jwt';
|
||||
import {
|
||||
isTopicDone,
|
||||
refreshProgressCounters,
|
||||
renderTopicProgress,
|
||||
ResourceProgressType,
|
||||
ResourceType,
|
||||
updateResourceProgress as updateResourceProgressApi,
|
||||
} from '../../lib/resource-progress';
|
||||
@@ -35,6 +35,7 @@ export function TopicDetail() {
|
||||
|
||||
// Details of the currently loaded topic
|
||||
const [topicId, setTopicId] = useState('');
|
||||
const [resourceStatus, setResourceStatus] = useState<ResourceProgressType>('pending')
|
||||
const [resourceId, setResourceId] = useState('');
|
||||
const [resourceType, setResourceType] = useState<ResourceType>('roadmap');
|
||||
|
||||
@@ -52,7 +53,7 @@ export function TopicDetail() {
|
||||
// Toggle topic is available even if the component UI is not active
|
||||
// This is used on the best practice screen where we have the checkboxes
|
||||
// to mark the topic as done/undone.
|
||||
useToggleTopic(({ topicId, resourceType, resourceId }) => {
|
||||
useToggleTopic(({ topicId, resourceType, resourceId, status }) => {
|
||||
if (isGuest) {
|
||||
showLoginPopup();
|
||||
return;
|
||||
@@ -60,18 +61,14 @@ export function TopicDetail() {
|
||||
|
||||
pageProgressMessage.set('Updating');
|
||||
|
||||
// Toggle the topic status
|
||||
isTopicDone({ topicId, resourceId, resourceType })
|
||||
.then((oldIsDone) =>
|
||||
updateResourceProgressApi(
|
||||
{
|
||||
topicId,
|
||||
resourceId,
|
||||
resourceType,
|
||||
},
|
||||
oldIsDone ? 'pending' : 'done'
|
||||
)
|
||||
)
|
||||
updateResourceProgressApi(
|
||||
{
|
||||
topicId,
|
||||
resourceId,
|
||||
resourceType,
|
||||
},
|
||||
status === 'done' ? 'pending' : 'done'
|
||||
)
|
||||
.then(({ done = [] }) => {
|
||||
renderTopicProgress(
|
||||
topicId,
|
||||
@@ -86,10 +83,11 @@ export function TopicDetail() {
|
||||
.finally(() => {
|
||||
pageProgressMessage.set('');
|
||||
});
|
||||
return;
|
||||
});
|
||||
|
||||
// Load the topic detail when the topic detail is active
|
||||
useLoadTopic(({ topicId, resourceType, resourceId }) => {
|
||||
useLoadTopic(({ topicId, resourceType, resourceId, status }) => {
|
||||
setIsLoading(true);
|
||||
setIsActive(true);
|
||||
sponsorHidden.set(true);
|
||||
@@ -98,6 +96,7 @@ export function TopicDetail() {
|
||||
setTopicId(topicId);
|
||||
setResourceType(resourceType);
|
||||
setResourceId(resourceId);
|
||||
setResourceStatus(status);
|
||||
|
||||
const topicPartial = topicId.replaceAll(':', '/');
|
||||
const topicUrl =
|
||||
@@ -177,6 +176,7 @@ export function TopicDetail() {
|
||||
topicId={topicId}
|
||||
resourceId={resourceId}
|
||||
resourceType={resourceType}
|
||||
status={resourceStatus}
|
||||
onClose={() => {
|
||||
setIsActive(false);
|
||||
setIsContributing(false);
|
||||
@@ -206,7 +206,8 @@ export function TopicDetail() {
|
||||
{/* Contribution */}
|
||||
<div className="mt-8 flex-1 border-t">
|
||||
<p class="mb-2 mt-2 text-sm leading-relaxed text-gray-400">
|
||||
Help others learn by submitting links to learn more about this topic{' '}
|
||||
Help others learn by submitting links to learn more about this
|
||||
topic{' '}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
|
||||
@@ -7,7 +7,6 @@ import { isLoggedIn } from '../../lib/jwt';
|
||||
import {
|
||||
ResourceProgressType,
|
||||
ResourceType,
|
||||
getTopicStatus,
|
||||
refreshProgressCounters,
|
||||
renderTopicProgress,
|
||||
updateResourceProgress,
|
||||
@@ -19,6 +18,7 @@ type TopicProgressButtonProps = {
|
||||
topicId: string;
|
||||
resourceId: string;
|
||||
resourceType: ResourceType;
|
||||
status: ResourceProgressType;
|
||||
|
||||
onClose: () => void;
|
||||
};
|
||||
@@ -32,11 +32,12 @@ const statusColors: Record<ResourceProgressType, string> = {
|
||||
};
|
||||
|
||||
export function TopicProgressButton(props: TopicProgressButtonProps) {
|
||||
const { topicId, resourceId, resourceType, onClose } = props;
|
||||
const { topicId, resourceId, resourceType, status, onClose } = props;
|
||||
console.log(status)
|
||||
|
||||
const toast = useToast();
|
||||
const [isUpdatingProgress, setIsUpdatingProgress] = useState(true);
|
||||
const [progress, setProgress] = useState<ResourceProgressType>('pending');
|
||||
const [isUpdatingProgress, setIsUpdatingProgress] = useState(false);
|
||||
const [progress, setProgress] = useState<ResourceProgressType>(status);
|
||||
const [showChangeStatus, setShowChangeStatus] = useState(false);
|
||||
|
||||
const changeStatusRef = useRef<HTMLDivElement>(null);
|
||||
@@ -47,20 +48,6 @@ export function TopicProgressButton(props: TopicProgressButtonProps) {
|
||||
|
||||
const isGuest = useMemo(() => !isLoggedIn(), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!topicId || !resourceId || !resourceType) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUpdatingProgress(true);
|
||||
getTopicStatus({ topicId, resourceId, resourceType })
|
||||
.then((status) => {
|
||||
setIsUpdatingProgress(false);
|
||||
setProgress(status);
|
||||
})
|
||||
.catch(console.error);
|
||||
}, [topicId, resourceId, resourceType]);
|
||||
|
||||
// Mark as done
|
||||
useKeydown(
|
||||
'd',
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
import { useEffect } from 'preact/hooks';
|
||||
import type { ResourceType } from '../lib/resource-progress';
|
||||
import type {
|
||||
ResourceProgressType,
|
||||
ResourceType,
|
||||
} from '../lib/resource-progress';
|
||||
|
||||
type CallbackType = (data: {
|
||||
resourceType: ResourceType;
|
||||
resourceId: string;
|
||||
topicId: string;
|
||||
status: ResourceProgressType;
|
||||
}) => void;
|
||||
|
||||
export function useLoadTopic(callback: CallbackType) {
|
||||
useEffect(() => {
|
||||
function handleTopicClick(e: any) {
|
||||
const { resourceType, resourceId, topicId } = e.detail;
|
||||
|
||||
const { resourceType, resourceId, topicId, status } = e.detail;
|
||||
console.log('handleTopicClick', e.detail);
|
||||
callback({
|
||||
resourceType,
|
||||
resourceId,
|
||||
topicId,
|
||||
status,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import { useEffect } from 'preact/hooks';
|
||||
import type { ResourceType } from '../lib/resource-progress';
|
||||
import type { ResourceProgressType, ResourceType } from '../lib/resource-progress';
|
||||
|
||||
type CallbackType = (data: {
|
||||
resourceType: ResourceType;
|
||||
resourceId: string;
|
||||
topicId: string;
|
||||
status: ResourceProgressType
|
||||
}) => void;
|
||||
|
||||
export function useToggleTopic(callback: CallbackType) {
|
||||
useEffect(() => {
|
||||
function handleToggleTopic(e: any) {
|
||||
const { resourceType, resourceId, topicId } = e.detail;
|
||||
const { resourceType, resourceId, topicId, status } = e.detail;
|
||||
|
||||
callback({
|
||||
resourceType,
|
||||
resourceId,
|
||||
topicId,
|
||||
status,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,12 @@ import { TOKEN_COOKIE_NAME } from './jwt';
|
||||
import Element = astroHTML.JSX.Element;
|
||||
|
||||
export type ResourceType = 'roadmap' | 'best-practice';
|
||||
export type ResourceProgressType = 'done' | 'learning' | 'pending' | 'skipped' | 'removed';
|
||||
export type ResourceProgressType =
|
||||
| 'done'
|
||||
| 'learning'
|
||||
| 'pending'
|
||||
| 'skipped'
|
||||
| 'removed';
|
||||
|
||||
type TopicMeta = {
|
||||
topicId: string;
|
||||
@@ -12,35 +17,6 @@ type TopicMeta = {
|
||||
resourceId: string;
|
||||
};
|
||||
|
||||
export async function isTopicDone(topic: TopicMeta): Promise<boolean> {
|
||||
const { topicId, resourceType, resourceId } = topic;
|
||||
const { done = [] } =
|
||||
(await getResourceProgress(resourceType, resourceId)) || {};
|
||||
|
||||
return done?.includes(topicId);
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
if (progressResult?.skipped?.includes(topicId)) {
|
||||
return 'skipped';
|
||||
}
|
||||
|
||||
return 'pending';
|
||||
}
|
||||
|
||||
export async function updateResourceProgress(
|
||||
topic: TopicMeta,
|
||||
progressType: ResourceProgressType
|
||||
@@ -63,14 +39,6 @@ export async function updateResourceProgress(
|
||||
throw new Error(error?.message || 'Something went wrong');
|
||||
}
|
||||
|
||||
setResourceProgress(
|
||||
resourceType,
|
||||
resourceId,
|
||||
response.done,
|
||||
response.learning,
|
||||
response.skipped
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -87,35 +55,7 @@ export async function getResourceProgress(
|
||||
};
|
||||
}
|
||||
|
||||
const progressKey = `${resourceType}-${resourceId}-progress`;
|
||||
const isFavoriteKey = `${resourceType}-${resourceId}-favorite`;
|
||||
|
||||
const rawIsFavorite = localStorage.getItem(isFavoriteKey);
|
||||
const isFavorite = JSON.parse(rawIsFavorite || '0') === 1;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Dispatch event to update favorite status in the MarkFavorite component
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('mark-favorite', {
|
||||
detail: {
|
||||
resourceType,
|
||||
resourceId,
|
||||
isFavorite
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return progress;
|
||||
return loadFreshProgress(resourceType, resourceId);
|
||||
}
|
||||
|
||||
async function loadFreshProgress(
|
||||
@@ -138,49 +78,24 @@ async function loadFreshProgress(
|
||||
done: [],
|
||||
learning: [],
|
||||
skipped: [],
|
||||
isFavorite: false,
|
||||
};
|
||||
}
|
||||
|
||||
setResourceProgress(
|
||||
resourceType,
|
||||
resourceId,
|
||||
response?.done || [],
|
||||
response?.learning || [],
|
||||
response?.skipped || [],
|
||||
);
|
||||
|
||||
// Dispatch event to update favorite status in the MarkFavorite component
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('mark-favorite', {
|
||||
detail: {
|
||||
resourceType,
|
||||
resourceId,
|
||||
isFavorite: response.isFavorite
|
||||
}
|
||||
isFavorite: response.isFavorite,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
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
|
||||
@@ -238,10 +153,10 @@ export function renderTopicProgress(
|
||||
});
|
||||
}
|
||||
|
||||
export function clearResourceProgress() {
|
||||
const clickableElements = document.querySelectorAll('.clickable-group')
|
||||
export function clearResourceProgress(progress: ResourceProgressType) {
|
||||
const clickableElements = document.querySelectorAll('.clickable-group');
|
||||
for (const clickableElement of clickableElements) {
|
||||
clickableElement.classList.remove('done', 'skipped', 'learning', 'removed');
|
||||
clickableElement.classList.remove(progress);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,17 +219,21 @@ export function refreshProgressCounters() {
|
||||
'.clickable-group.removed'
|
||||
).length;
|
||||
const totalItems =
|
||||
totalClickable - externalLinks - roadmapSwitchers - checkBoxes - totalRemoved;
|
||||
totalClickable -
|
||||
externalLinks -
|
||||
roadmapSwitchers -
|
||||
checkBoxes -
|
||||
totalRemoved;
|
||||
|
||||
const totalDone =
|
||||
document.querySelectorAll('.clickable-group.done').length -
|
||||
totalCheckBoxesDone;
|
||||
const totalLearning = document.querySelectorAll(
|
||||
'.clickable-group.learning'
|
||||
).length - totalCheckBoxesLearning;
|
||||
const totalSkipped = document.querySelectorAll(
|
||||
'.clickable-group.skipped'
|
||||
).length - totalCheckBoxesSkipped;
|
||||
const totalLearning =
|
||||
document.querySelectorAll('.clickable-group.learning').length -
|
||||
totalCheckBoxesLearning;
|
||||
const totalSkipped =
|
||||
document.querySelectorAll('.clickable-group.skipped').length -
|
||||
totalCheckBoxesSkipped;
|
||||
|
||||
const doneCountEls = document.querySelectorAll('[data-progress-done]');
|
||||
if (doneCountEls.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user