wip: remove cache in local storage

chore/cache
Arik Chakma 1 year ago
parent 34d0cde165
commit 47926c496d
  1. 4
      src/components/FeaturedItems/MarkFavorite.tsx
  2. 14
      src/components/FrameRenderer/renderer.ts
  3. 10
      src/components/TeamVersions/TeamVersions.tsx
  4. 33
      src/components/TopicDetail/TopicDetail.tsx
  5. 23
      src/components/TopicDetail/TopicProgressButton.tsx
  6. 11
      src/hooks/use-load-topic.ts
  7. 6
      src/hooks/use-toggle-topic.ts
  8. 129
      src/lib/resource-progress.ts

@ -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) {

Loading…
Cancel
Save