Compare commits

..

1 Commits

Author SHA1 Message Date
Arik Chakma
47926c496d wip: remove cache in local storage 2023-07-21 03:19:18 +06:00
15 changed files with 127 additions and 314 deletions

View File

@@ -4,7 +4,6 @@ import type { TeamDocument } from './CreateTeam/CreateTeamForm';
import { useTeamId } from '../hooks/use-team-id';
import { useOutsideClick } from '../hooks/use-outside-click';
import { useKeydown } from '../hooks/use-keydown';
import { useToast } from '../hooks/use-toast';
type DeleteTeamPopupProps = {
onClose: () => void;
@@ -13,7 +12,6 @@ type DeleteTeamPopupProps = {
export function DeleteTeamPopup(props: DeleteTeamPopupProps) {
const { onClose } = props;
const toast = useToast();
const popupBodyEl = useRef<HTMLDivElement>(null);
const inputEl = useRef<HTMLInputElement>(null);
@@ -55,7 +53,6 @@ export function DeleteTeamPopup(props: DeleteTeamPopupProps) {
return;
}
toast.success('Team deleted successfully');
window.location.href = '/account';
};
@@ -75,9 +72,9 @@ export function DeleteTeamPopup(props: DeleteTeamPopupProps) {
ref={popupBodyEl}
class="popup-body relative rounded-lg bg-white p-4 shadow"
>
<h2 class="text-2xl font-semibold text-black">Delete Team</h2>
<p className="text-gray-500">
This will permanently delete your team and all associated data.
<p>
This will permanently delete your account and all your associated
data including your progress.
</p>
<p class="-mb-2 mt-3 text-base font-medium text-black">

View File

@@ -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');
}
};

View File

@@ -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,
},
})
);

View File

@@ -79,7 +79,7 @@ export function TeamDropdown() {
if (
!user?.email.endsWith('@insightpartners.com') &&
!user?.email.endsWith('@roadmap.sh') &&
!['arikchangma@gmail.com', 'kamranahmed.se@gmail.com', 'stephen.chetcuti@gmail.com'].includes(user?.email!)
!['arikchangma@gmail.com', 'kamranahmed.se@gmail.com'].includes(user?.email!)
) {
return null;
}
@@ -101,8 +101,9 @@ export function TeamDropdown() {
<img
src={
selectedAvatar
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL
}/${selectedAvatar}`
? `${
import.meta.env.PUBLIC_AVATAR_BASE_URL
}/${selectedAvatar}`
: '/images/default-avatar.png'
}
alt=""

View File

@@ -1,30 +1,40 @@
import { useState } from 'preact/hooks';
import { LeaveTeamPopup } from './LeaveTeamPopup';
import { useState } from "preact/hooks";
import { httpDelete } from "../../lib/http";
import { Spinner } from "../ReactIcons/Spinner";
import { useToast } from "../../hooks/use-toast";
type LeaveTeamButtonProps = {
teamId: string;
};
export function LeaveTeamButton(props: LeaveTeamButtonProps) {
const [showLeaveTeamPopup, setShowLeaveTeamPopup] = useState(false);
const toast = useToast();
const [isLoading, setIsLoading] = useState(false);
const { teamId } = props;
async function leaveTeam() {
setIsLoading(true);
const { response, error } = await httpDelete(
`${import.meta.env.PUBLIC_API_URL}/v1-leave-team/${teamId}`,
{}
);
if (error || !response) {
setIsLoading(false);
toast.error(error?.message || 'Something went wrong');
return;
}
window.location.href = '/account';
}
return (
<>
{showLeaveTeamPopup && (
<LeaveTeamPopup
onClose={() => {
setShowLeaveTeamPopup(false);
}}
/>
)}
<button
onClick={() => {
setShowLeaveTeamPopup(true);
}}
className="flex h-7 min-w-[95px] items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-2 py-1.5 text-sm font-medium leading-none text-red-600"
>
Leave team
</button>
</>
);
<button
disabled={isLoading}
onClick={leaveTeam}
className="bg-gray-50 text-red-600 text-sm font-medium px-2 leading-none py-1.5 rounded-md border border-gray-200 h-7 flex items-center justify-center min-w-[95px]">
{isLoading ? <Spinner isDualRing={false} /> : 'Leave team'}
</button>
)
}

View File

@@ -1,124 +0,0 @@
import { useEffect, useRef, useState } from 'preact/hooks';
import { httpDelete } from '../../lib/http';
import { useTeamId } from '../../hooks/use-team-id';
import { useOutsideClick } from '../../hooks/use-outside-click';
type LeaveTeamPopupProps = {
onClose: () => void;
};
export function LeaveTeamPopup(props: LeaveTeamPopupProps) {
const { onClose } = props;
const popupBodyRef = useRef<HTMLDivElement>(null);
const confirmationEl = useRef<HTMLInputElement>(null);
const [confirmationText, setConfirmationText] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const { teamId } = useTeamId();
useEffect(() => {
setError('');
setConfirmationText('');
confirmationEl?.current?.focus();
}, []);
const handleSubmit = async (e: Event) => {
e.preventDefault();
setIsLoading(true);
setError('');
if (confirmationText.toUpperCase() !== 'LEAVE') {
setError('Verification text does not match');
setIsLoading(false);
return;
}
const { response, error } = await httpDelete(
`${import.meta.env.PUBLIC_API_URL}/v1-leave-team/${teamId}`,
{}
);
if (error || !response) {
setIsLoading(false);
setError(error?.message || 'Something went wrong');
return;
}
window.location.href = '/account';
};
const handleClosePopup = () => {
setIsLoading(false);
setError('');
setConfirmationText('');
onClose();
};
useOutsideClick(popupBodyRef, handleClosePopup);
return (
<div class="popup fixed left-0 right-0 top-0 z-50 flex h-full items-center justify-center overflow-y-auto overflow-x-hidden bg-black/50">
<div class="relative h-full w-full max-w-md p-4 md:h-auto">
<div
ref={popupBodyRef}
class="popup-body relative rounded-lg bg-white p-4 shadow"
>
<h2 class="text-2xl font-semibold text-black">
Leave Team
</h2>
<p className="text-gray-500">
You will lose access to the team, the roadmaps and progress of other team members.
</p>
<p className="-mb-2 mt-3 text-base font-medium text-black">
Please type "leave" to confirm.
</p>
<form onSubmit={handleSubmit}>
<div className="my-4">
<input
ref={confirmationEl}
type="text"
name="leave-team"
id="leave-team"
className="mt-2 block w-full rounded-md border border-gray-300 px-3 py-2 outline-none placeholder:text-gray-400 focus:border-gray-400"
placeholder={'Type "leave" to confirm'}
required
autoFocus
value={confirmationText}
onInput={(e) =>
setConfirmationText((e.target as HTMLInputElement).value)
}
/>
{error && (
<p className="mt-2 rounded-lg bg-red-100 p-2 text-red-700">
{error}
</p>
)}
</div>
<div className="flex items-center gap-2">
<button
type="button"
disabled={isLoading}
onClick={handleClosePopup}
className="flex-grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center"
>
Cancel
</button>
<button
type="submit"
disabled={
isLoading || confirmationText.toUpperCase() !== 'LEAVE'
}
className="flex-grow cursor-pointer rounded-lg bg-red-500 py-2 text-white disabled:opacity-40"
>
{isLoading ? 'Please wait ..' : 'Leave Team'}
</button>
</div>
</form>
</div>
</div>
</div>
);
}

View File

@@ -60,7 +60,7 @@ export function MemberProgressItem(props: MemberProgressItemProps) {
>
<span className="relative z-10 flex items-center justify-between text-sm">
<span className="inline-grid">
<span className={'truncate'}>{progress.resourceTitle}</span>
<span className={'truncate'}>{progress.resourceTitle}</span>
</span>
<span className="text-xs text-gray-400 shrink-0 ml-1.5">
{progress.done} / {progress.total}

View File

@@ -10,7 +10,6 @@ import { GroupRoadmapItem } from './GroupRoadmapItem';
import { setUrlParams } from '../../lib/browser';
import { getUrlParams } from '../../lib/browser';
import { $toastMessage } from '../../stores/toast';
import { useAuth } from '../../hooks/use-auth';
export type UserProgress = {
resourceTitle: string;
@@ -56,7 +55,6 @@ export function TeamProgressPage() {
const [isLoading, setIsLoading] = useState(true);
const toast = useToast();
const currentTeam = useStore($currentTeam);
const user = useAuth();
const [teamMembers, setTeamMembers] = useState<TeamMember[]>([]);
const [selectedGrouping, setSelectedGrouping] = useState<
@@ -72,10 +70,7 @@ export function TeamProgressPage() {
return;
}
const currentUserProgress = response.find((member) => member.email === user?.email)
const otherUserProgresses = response.filter(member => member.email !== user?.email);
const allUserProgresses = currentUserProgress ? [currentUserProgress, ...otherUserProgresses] : otherUserProgresses;
setTeamMembers(allUserProgresses);
setTeamMembers(response);
}
useEffect(() => {
@@ -139,10 +134,11 @@ export function TeamProgressPage() {
<div className="flex items-center gap-2">
{groupingTypes.map((grouping) => (
<button
className={`rounded-md border p-1 px-2 text-sm ${selectedGrouping === grouping.value
? ' border-gray-400 bg-gray-200 '
: ''
}`}
className={`rounded-md border p-1 px-2 text-sm ${
selectedGrouping === grouping.value
? ' border-gray-400 bg-gray-200 '
: ''
}`}
onClick={() => setSelectedGrouping(grouping.value)}
>
{grouping.label}

View File

@@ -267,6 +267,7 @@ export function UpdateTeamForm() {
{isDeleting && (
<DeleteTeamPopup
onClose={() => {
toast.success('Team deleted successfully');
setIsDeleting(false);
}}
/>

View File

@@ -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">

View File

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

View File

@@ -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',

View File

@@ -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,
});
}

View File

@@ -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,
});
}

View File

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