mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2026-03-13 02:01:57 +08:00
Compare commits
1 Commits
chore/prog
...
chore/cach
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47926c496d |
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
@@ -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=""
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -267,6 +267,7 @@ export function UpdateTeamForm() {
|
||||
{isDeleting && (
|
||||
<DeleteTeamPopup
|
||||
onClose={() => {
|
||||
toast.success('Team deleted successfully');
|
||||
setIsDeleting(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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