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. 16
      src/components/FrameRenderer/renderer.ts
  3. 10
      src/components/TeamVersions/TeamVersions.tsx
  4. 19
      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. 127
      src/lib/resource-progress.ts

@ -20,12 +20,11 @@ export function MarkFavorite({
favorite, favorite,
className, className,
}: MarkFavoriteType) { }: MarkFavoriteType) {
const localStorageKey = `${resourceType}-${resourceId}-favorite`;
const toast = useToast(); const toast = useToast();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isFavorite, setIsFavorite] = useState( const [isFavorite, setIsFavorite] = useState(
favorite ?? localStorage.getItem(localStorageKey) === '1' favorite || false
); );
async function toggleFavoriteHandler(e: Event) { async function toggleFavoriteHandler(e: Event) {
@ -82,7 +81,6 @@ export function MarkFavorite({
} = (e as CustomEvent).detail; } = (e as CustomEvent).detail;
if (id === resourceId && type === resourceType) { if (id === resourceId && type === resourceType) {
setIsFavorite(fav); setIsFavorite(fav);
localStorage.setItem(localStorageKey, fav ? '1' : '0');
} }
}; };

@ -228,8 +228,18 @@ export class Renderer {
} }
e.stopImmediatePropagation(); e.stopImmediatePropagation();
let status: ResourceProgressType = 'pending';
if (targetGroup.classList.contains('removed')) { 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 (status === 'removed') {
return; return;
} }
@ -263,6 +273,7 @@ export class Renderer {
topicId: groupId.replace('check:', ''), topicId: groupId.replace('check:', ''),
resourceType: this.resourceType, resourceType: this.resourceType,
resourceId: this.resourceId, resourceId: this.resourceId,
status,
}, },
}) })
); );
@ -300,6 +311,7 @@ export class Renderer {
topicId: normalizedGroupId, topicId: normalizedGroupId,
resourceId: this.resourceId, resourceId: this.resourceId,
resourceType: this.resourceType, resourceType: this.resourceType,
status,
}, },
}) })
); );

@ -44,6 +44,8 @@ export function TeamVersions(props: TeamVersionsProps) {
const [selectedTeamVersion, setSelectedTeamVersion] = useState< const [selectedTeamVersion, setSelectedTeamVersion] = useState<
TeamVersionsResponse[0] | null TeamVersionsResponse[0] | null
>(null); >(null);
const [shouldStartLoading, setShouldStartLoading] = useState(false);
let shouldShowAvatar = true; let shouldShowAvatar = true;
const selectedAvatar = selectedTeamVersion const selectedAvatar = selectedTeamVersion
? selectedTeamVersion.team.avatar ? selectedTeamVersion.team.avatar
@ -88,6 +90,7 @@ export function TeamVersions(props: TeamVersionsProps) {
if (teamId) { if (teamId) {
const foundVersion = response.find((v) => v.team._id === teamId) || null; const foundVersion = response.find((v) => v.team._id === teamId) || null;
setSelectedTeamVersion(foundVersion); setSelectedTeamVersion(foundVersion);
setShouldStartLoading(true);
} }
setTimeout(() => { setTimeout(() => {
@ -110,7 +113,10 @@ export function TeamVersions(props: TeamVersionsProps) {
} }
useEffect(() => { useEffect(() => {
clearResourceProgress(); if (!shouldStartLoading) {
return;
}
clearResourceProgress('removed');
if (!selectedTeamVersion) { if (!selectedTeamVersion) {
deleteUrlParam('t'); deleteUrlParam('t');
renderResourceProgress(resourceType, resourceId).then(); renderResourceProgress(resourceType, resourceId).then();
@ -125,6 +131,7 @@ export function TeamVersions(props: TeamVersionsProps) {
}); });
refreshProgressCounters(); refreshProgressCounters();
}); });
setShouldStartLoading(true);
}, [selectedTeamVersion]); }, [selectedTeamVersion]);
if (!teamVersions.length) { if (!teamVersions.length) {
@ -204,6 +211,7 @@ export function TeamVersions(props: TeamVersionsProps) {
onClick={() => { onClick={() => {
setSelectedTeamVersion(team); setSelectedTeamVersion(team);
setIsDropdownOpen(false); setIsDropdownOpen(false);
setShouldStartLoading(true);
}} }}
> >
<div className="flex w-full items-center justify-between"> <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 { httpGet } from '../../lib/http';
import { isLoggedIn } from '../../lib/jwt'; import { isLoggedIn } from '../../lib/jwt';
import { import {
isTopicDone,
refreshProgressCounters, refreshProgressCounters,
renderTopicProgress, renderTopicProgress,
ResourceProgressType,
ResourceType, ResourceType,
updateResourceProgress as updateResourceProgressApi, updateResourceProgress as updateResourceProgressApi,
} from '../../lib/resource-progress'; } from '../../lib/resource-progress';
@ -35,6 +35,7 @@ export function TopicDetail() {
// Details of the currently loaded topic // Details of the currently loaded topic
const [topicId, setTopicId] = useState(''); const [topicId, setTopicId] = useState('');
const [resourceStatus, setResourceStatus] = useState<ResourceProgressType>('pending')
const [resourceId, setResourceId] = useState(''); const [resourceId, setResourceId] = useState('');
const [resourceType, setResourceType] = useState<ResourceType>('roadmap'); 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 // 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 // This is used on the best practice screen where we have the checkboxes
// to mark the topic as done/undone. // to mark the topic as done/undone.
useToggleTopic(({ topicId, resourceType, resourceId }) => { useToggleTopic(({ topicId, resourceType, resourceId, status }) => {
if (isGuest) { if (isGuest) {
showLoginPopup(); showLoginPopup();
return; return;
@ -60,17 +61,13 @@ export function TopicDetail() {
pageProgressMessage.set('Updating'); pageProgressMessage.set('Updating');
// Toggle the topic status
isTopicDone({ topicId, resourceId, resourceType })
.then((oldIsDone) =>
updateResourceProgressApi( updateResourceProgressApi(
{ {
topicId, topicId,
resourceId, resourceId,
resourceType, resourceType,
}, },
oldIsDone ? 'pending' : 'done' status === 'done' ? 'pending' : 'done'
)
) )
.then(({ done = [] }) => { .then(({ done = [] }) => {
renderTopicProgress( renderTopicProgress(
@ -86,10 +83,11 @@ export function TopicDetail() {
.finally(() => { .finally(() => {
pageProgressMessage.set(''); pageProgressMessage.set('');
}); });
return;
}); });
// Load the topic detail when the topic detail is active // Load the topic detail when the topic detail is active
useLoadTopic(({ topicId, resourceType, resourceId }) => { useLoadTopic(({ topicId, resourceType, resourceId, status }) => {
setIsLoading(true); setIsLoading(true);
setIsActive(true); setIsActive(true);
sponsorHidden.set(true); sponsorHidden.set(true);
@ -98,6 +96,7 @@ export function TopicDetail() {
setTopicId(topicId); setTopicId(topicId);
setResourceType(resourceType); setResourceType(resourceType);
setResourceId(resourceId); setResourceId(resourceId);
setResourceStatus(status);
const topicPartial = topicId.replaceAll(':', '/'); const topicPartial = topicId.replaceAll(':', '/');
const topicUrl = const topicUrl =
@ -177,6 +176,7 @@ export function TopicDetail() {
topicId={topicId} topicId={topicId}
resourceId={resourceId} resourceId={resourceId}
resourceType={resourceType} resourceType={resourceType}
status={resourceStatus}
onClose={() => { onClose={() => {
setIsActive(false); setIsActive(false);
setIsContributing(false); setIsContributing(false);
@ -206,7 +206,8 @@ export function TopicDetail() {
{/* Contribution */} {/* Contribution */}
<div className="mt-8 flex-1 border-t"> <div className="mt-8 flex-1 border-t">
<p class="mb-2 mt-2 text-sm leading-relaxed text-gray-400"> <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> </p>
<button <button
onClick={() => { onClick={() => {

@ -7,7 +7,6 @@ import { isLoggedIn } from '../../lib/jwt';
import { import {
ResourceProgressType, ResourceProgressType,
ResourceType, ResourceType,
getTopicStatus,
refreshProgressCounters, refreshProgressCounters,
renderTopicProgress, renderTopicProgress,
updateResourceProgress, updateResourceProgress,
@ -19,6 +18,7 @@ type TopicProgressButtonProps = {
topicId: string; topicId: string;
resourceId: string; resourceId: string;
resourceType: ResourceType; resourceType: ResourceType;
status: ResourceProgressType;
onClose: () => void; onClose: () => void;
}; };
@ -32,11 +32,12 @@ const statusColors: Record<ResourceProgressType, string> = {
}; };
export function TopicProgressButton(props: TopicProgressButtonProps) { 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 toast = useToast();
const [isUpdatingProgress, setIsUpdatingProgress] = useState(true); const [isUpdatingProgress, setIsUpdatingProgress] = useState(false);
const [progress, setProgress] = useState<ResourceProgressType>('pending'); const [progress, setProgress] = useState<ResourceProgressType>(status);
const [showChangeStatus, setShowChangeStatus] = useState(false); const [showChangeStatus, setShowChangeStatus] = useState(false);
const changeStatusRef = useRef<HTMLDivElement>(null); const changeStatusRef = useRef<HTMLDivElement>(null);
@ -47,20 +48,6 @@ export function TopicProgressButton(props: TopicProgressButtonProps) {
const isGuest = useMemo(() => !isLoggedIn(), []); 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 // Mark as done
useKeydown( useKeydown(
'd', 'd',

@ -1,21 +1,26 @@
import { useEffect } from 'preact/hooks'; import { useEffect } from 'preact/hooks';
import type { ResourceType } from '../lib/resource-progress'; import type {
ResourceProgressType,
ResourceType,
} from '../lib/resource-progress';
type CallbackType = (data: { type CallbackType = (data: {
resourceType: ResourceType; resourceType: ResourceType;
resourceId: string; resourceId: string;
topicId: string; topicId: string;
status: ResourceProgressType;
}) => void; }) => void;
export function useLoadTopic(callback: CallbackType) { export function useLoadTopic(callback: CallbackType) {
useEffect(() => { useEffect(() => {
function handleTopicClick(e: any) { function handleTopicClick(e: any) {
const { resourceType, resourceId, topicId } = e.detail; const { resourceType, resourceId, topicId, status } = e.detail;
console.log('handleTopicClick', e.detail);
callback({ callback({
resourceType, resourceType,
resourceId, resourceId,
topicId, topicId,
status,
}); });
} }

@ -1,21 +1,23 @@
import { useEffect } from 'preact/hooks'; import { useEffect } from 'preact/hooks';
import type { ResourceType } from '../lib/resource-progress'; import type { ResourceProgressType, ResourceType } from '../lib/resource-progress';
type CallbackType = (data: { type CallbackType = (data: {
resourceType: ResourceType; resourceType: ResourceType;
resourceId: string; resourceId: string;
topicId: string; topicId: string;
status: ResourceProgressType
}) => void; }) => void;
export function useToggleTopic(callback: CallbackType) { export function useToggleTopic(callback: CallbackType) {
useEffect(() => { useEffect(() => {
function handleToggleTopic(e: any) { function handleToggleTopic(e: any) {
const { resourceType, resourceId, topicId } = e.detail; const { resourceType, resourceId, topicId, status } = e.detail;
callback({ callback({
resourceType, resourceType,
resourceId, resourceId,
topicId, topicId,
status,
}); });
} }

@ -4,7 +4,12 @@ import { TOKEN_COOKIE_NAME } from './jwt';
import Element = astroHTML.JSX.Element; import Element = astroHTML.JSX.Element;
export type ResourceType = 'roadmap' | 'best-practice'; export type ResourceType = 'roadmap' | 'best-practice';
export type ResourceProgressType = 'done' | 'learning' | 'pending' | 'skipped' | 'removed'; export type ResourceProgressType =
| 'done'
| 'learning'
| 'pending'
| 'skipped'
| 'removed';
type TopicMeta = { type TopicMeta = {
topicId: string; topicId: string;
@ -12,35 +17,6 @@ type TopicMeta = {
resourceId: string; 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( export async function updateResourceProgress(
topic: TopicMeta, topic: TopicMeta,
progressType: ResourceProgressType progressType: ResourceProgressType
@ -63,14 +39,6 @@ export async function updateResourceProgress(
throw new Error(error?.message || 'Something went wrong'); throw new Error(error?.message || 'Something went wrong');
} }
setResourceProgress(
resourceType,
resourceId,
response.done,
response.learning,
response.skipped
);
return response; return response;
} }
@ -87,37 +55,9 @@ 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); 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;
}
async function loadFreshProgress( async function loadFreshProgress(
resourceType: ResourceType, resourceType: ResourceType,
resourceId: string resourceId: string
@ -138,49 +78,24 @@ async function loadFreshProgress(
done: [], done: [],
learning: [], learning: [],
skipped: [], skipped: [],
isFavorite: false,
}; };
} }
setResourceProgress(
resourceType,
resourceId,
response?.done || [],
response?.learning || [],
response?.skipped || [],
);
// Dispatch event to update favorite status in the MarkFavorite component // Dispatch event to update favorite status in the MarkFavorite component
window.dispatchEvent( window.dispatchEvent(
new CustomEvent('mark-favorite', { new CustomEvent('mark-favorite', {
detail: { detail: {
resourceType, resourceType,
resourceId, resourceId,
isFavorite: response.isFavorite isFavorite: response.isFavorite,
} },
}) })
); );
return response; 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( export function renderTopicProgress(
topicId: string, topicId: string,
topicProgress: ResourceProgressType topicProgress: ResourceProgressType
@ -238,10 +153,10 @@ export function renderTopicProgress(
}); });
} }
export function clearResourceProgress() { export function clearResourceProgress(progress: ResourceProgressType) {
const clickableElements = document.querySelectorAll('.clickable-group') const clickableElements = document.querySelectorAll('.clickable-group');
for (const clickableElement of clickableElements) { 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' '.clickable-group.removed'
).length; ).length;
const totalItems = const totalItems =
totalClickable - externalLinks - roadmapSwitchers - checkBoxes - totalRemoved; totalClickable -
externalLinks -
roadmapSwitchers -
checkBoxes -
totalRemoved;
const totalDone = const totalDone =
document.querySelectorAll('.clickable-group.done').length - document.querySelectorAll('.clickable-group.done').length -
totalCheckBoxesDone; totalCheckBoxesDone;
const totalLearning = document.querySelectorAll( const totalLearning =
'.clickable-group.learning' document.querySelectorAll('.clickable-group.learning').length -
).length - totalCheckBoxesLearning; totalCheckBoxesLearning;
const totalSkipped = document.querySelectorAll( const totalSkipped =
'.clickable-group.skipped' document.querySelectorAll('.clickable-group.skipped').length -
).length - totalCheckBoxesSkipped; totalCheckBoxesSkipped;
const doneCountEls = document.querySelectorAll('[data-progress-done]'); const doneCountEls = document.querySelectorAll('[data-progress-done]');
if (doneCountEls.length > 0) { if (doneCountEls.length > 0) {

Loading…
Cancel
Save