mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-08-21 16:41:24 +02:00
Add shortcuts for progress tracking
This commit is contained in:
@@ -2,13 +2,19 @@ import { wireframeJSONToSVG } from 'roadmap-renderer';
|
|||||||
import { httpPost } from '../../lib/http';
|
import { httpPost } from '../../lib/http';
|
||||||
import { isLoggedIn } from '../../lib/jwt';
|
import { isLoggedIn } from '../../lib/jwt';
|
||||||
import {
|
import {
|
||||||
|
refreshProgressCounters,
|
||||||
renderResourceProgress,
|
renderResourceProgress,
|
||||||
|
renderTopicProgress,
|
||||||
|
ResourceProgressType,
|
||||||
ResourceType,
|
ResourceType,
|
||||||
|
updateResourceProgress,
|
||||||
} from '../../lib/resource-progress';
|
} from '../../lib/resource-progress';
|
||||||
|
import { pageProgressMessage } from '../../stores/page';
|
||||||
|
import { showLoginPopup } from '../../lib/popup';
|
||||||
|
|
||||||
export class Renderer {
|
export class Renderer {
|
||||||
resourceId: string;
|
resourceId: string;
|
||||||
resourceType: string;
|
resourceType: ResourceType | string;
|
||||||
jsonUrl: string;
|
jsonUrl: string;
|
||||||
loaderHTML: string | null;
|
loaderHTML: string | null;
|
||||||
|
|
||||||
@@ -28,8 +34,10 @@ export class Renderer {
|
|||||||
this.onDOMLoaded = this.onDOMLoaded.bind(this);
|
this.onDOMLoaded = this.onDOMLoaded.bind(this);
|
||||||
this.jsonToSvg = this.jsonToSvg.bind(this);
|
this.jsonToSvg = this.jsonToSvg.bind(this);
|
||||||
this.handleSvgClick = this.handleSvgClick.bind(this);
|
this.handleSvgClick = this.handleSvgClick.bind(this);
|
||||||
|
this.handleSvgRightClick = this.handleSvgRightClick.bind(this);
|
||||||
this.prepareConfig = this.prepareConfig.bind(this);
|
this.prepareConfig = this.prepareConfig.bind(this);
|
||||||
this.switchRoadmap = this.switchRoadmap.bind(this);
|
this.switchRoadmap = this.switchRoadmap.bind(this);
|
||||||
|
this.updateTopicStatus = this.updateTopicStatus.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
get loaderEl() {
|
get loaderEl() {
|
||||||
@@ -161,6 +169,53 @@ export class Renderer {
|
|||||||
this.jsonToSvg(newJsonUrl)?.then(() => {});
|
this.jsonToSvg(newJsonUrl)?.then(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateTopicStatus(topicId: string, newStatus: ResourceProgressType) {
|
||||||
|
if (!isLoggedIn()) {
|
||||||
|
showLoginPopup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pageProgressMessage.set('Updating progress');
|
||||||
|
updateResourceProgress(
|
||||||
|
{
|
||||||
|
resourceId: this.resourceId,
|
||||||
|
resourceType: this.resourceType as ResourceType,
|
||||||
|
topicId,
|
||||||
|
},
|
||||||
|
newStatus
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
renderTopicProgress(topicId, newStatus);
|
||||||
|
refreshProgressCounters();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
alert('Something went wrong, please try again.');
|
||||||
|
console.error(err);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
pageProgressMessage.set('');
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSvgRightClick(e: any) {
|
||||||
|
const targetGroup = e.target?.closest('g') || {};
|
||||||
|
const groupId = targetGroup.dataset ? targetGroup.dataset.groupId : '';
|
||||||
|
if (!groupId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const isCurrentStatusDone = targetGroup.classList.contains('done');
|
||||||
|
const normalizedGroupId = groupId.replace(/^\d+-/, '');
|
||||||
|
this.updateTopicStatus(
|
||||||
|
normalizedGroupId,
|
||||||
|
!isCurrentStatusDone ? 'done' : 'pending'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
handleSvgClick(e: any) {
|
handleSvgClick(e: any) {
|
||||||
const targetGroup = e.target?.closest('g') || {};
|
const targetGroup = e.target?.closest('g') || {};
|
||||||
const groupId = targetGroup.dataset ? targetGroup.dataset.groupId : '';
|
const groupId = targetGroup.dataset ? targetGroup.dataset.groupId : '';
|
||||||
@@ -209,6 +264,28 @@ export class Renderer {
|
|||||||
// Remove sorting prefix from groupId
|
// Remove sorting prefix from groupId
|
||||||
const normalizedGroupId = groupId.replace(/^\d+-/, '');
|
const normalizedGroupId = groupId.replace(/^\d+-/, '');
|
||||||
|
|
||||||
|
const isCurrentStatusLearning = targetGroup.classList.contains('learning');
|
||||||
|
const isCurrentStatusSkipped = targetGroup.classList.contains('skipped');
|
||||||
|
|
||||||
|
if (e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.updateTopicStatus(
|
||||||
|
normalizedGroupId,
|
||||||
|
!isCurrentStatusLearning ? 'learning' : 'pending'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.altKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.updateTopicStatus(
|
||||||
|
normalizedGroupId,
|
||||||
|
!isCurrentStatusSkipped ? 'skipped' : 'pending'
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent(`${this.resourceType}.topic.click`, {
|
new CustomEvent(`${this.resourceType}.topic.click`, {
|
||||||
detail: {
|
detail: {
|
||||||
@@ -223,7 +300,7 @@ export class Renderer {
|
|||||||
init() {
|
init() {
|
||||||
window.addEventListener('DOMContentLoaded', this.onDOMLoaded);
|
window.addEventListener('DOMContentLoaded', this.onDOMLoaded);
|
||||||
window.addEventListener('click', this.handleSvgClick);
|
window.addEventListener('click', this.handleSvgClick);
|
||||||
// window.addEventListener('contextmenu', this.handleSvgClick);
|
window.addEventListener('contextmenu', this.handleSvgRightClick);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
47
src/components/ProgressHelpPopup.astro
Normal file
47
src/components/ProgressHelpPopup.astro
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
import AstroIcon from './AstroIcon.astro';
|
||||||
|
import Popup from './Popup/Popup.astro';
|
||||||
|
---
|
||||||
|
|
||||||
|
<Popup id='progress-help' title='' subtitle=''>
|
||||||
|
<div class='-mt-2.5'>
|
||||||
|
<h2 class='mb-3 text-2xl font-semibold leading-5 text-gray-900'>
|
||||||
|
Track your Progress
|
||||||
|
</h2>
|
||||||
|
<p class='text-sm leading-4 text-gray-600'>
|
||||||
|
Login and use one of the options listed below.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class='mt-4 flex flex-col gap-1.5'>
|
||||||
|
<div class='rounded-md border px-3 py-3 text-gray-500'>
|
||||||
|
<span class='mb-1.5 block text-xs font-medium uppercase text-green-600'
|
||||||
|
>Option 1</span
|
||||||
|
>
|
||||||
|
<p class='text-sm'>
|
||||||
|
Click the roadmap topics and use <span class='underline'
|
||||||
|
>Update Progress</span
|
||||||
|
> dropdown to update your progress.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class='rounded-md border border-yellow-300 bg-yellow-50 px-3 py-3 text-gray-500'>
|
||||||
|
<span class='mb-1.5 block text-xs font-medium uppercase text-green-600'
|
||||||
|
>Option 2</span
|
||||||
|
>
|
||||||
|
<p class='text-sm'>Use the keyboard shortcuts listed below.</p>
|
||||||
|
|
||||||
|
<ul class="flex flex-col gap-1 mt-3 mb-1.5">
|
||||||
|
<li class='text-sm leading-loose'>
|
||||||
|
<kbd class="px-2 py-1.5 text-xs text-white bg-gray-900 rounded-md">Right Mouse Click</kbd> to mark as Done.
|
||||||
|
</li>
|
||||||
|
<li class='text-sm leading-loose'>
|
||||||
|
<kbd class="px-2 py-1.5 text-xs text-white bg-gray-900 rounded-md">Shift</kbd> + <kbd class="px-2 py-1.5 text-xs text-white bg-gray-900 rounded-md">Click</kbd> to mark as in progress.
|
||||||
|
</li>
|
||||||
|
<li class='text-sm leading-loose'>
|
||||||
|
<kbd class="px-2 py-1.5 text-xs text-white bg-gray-900 rounded-md">Option</kbd> + <kbd class="px-2 py-1.5 text-xs text-white bg-gray-900 rounded-md">Click</kbd> to mark as skipped.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Popup>
|
@@ -1,4 +1,5 @@
|
|||||||
---
|
---
|
||||||
|
import AstroIcon from './AstroIcon.astro';
|
||||||
export interface Props {
|
export interface Props {
|
||||||
isSecondaryBanner?: boolean;
|
isSecondaryBanner?: boolean;
|
||||||
}
|
}
|
||||||
@@ -37,21 +38,31 @@ const { isSecondaryBanner = false } = Astro.props;
|
|||||||
>
|
>
|
||||||
<span><span data-progress-total>0</span> Total</span>
|
<span><span data-progress-total>0</span> Total</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
data-popup='progress-help'
|
||||||
|
class='flex items-center gap-1 text-sm font-medium text-gray-500 opacity-0 transition-opacity hover:text-black'
|
||||||
|
data-progress-nums
|
||||||
|
>
|
||||||
|
<AstroIcon icon='question' />
|
||||||
|
Track Progress
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
data-progress-nums-container
|
data-progress-nums-container
|
||||||
class='relative block rounded-md border bg-white px-2 py-1.5 text-sm text-sm text-gray-700 sm:hidden striped-loader bg-white -mb-2'
|
class='striped-loader relative -mb-2 flex items-center justify-between rounded-md border bg-white bg-white px-2 py-1.5 text-sm text-sm text-gray-700 sm:hidden'
|
||||||
>
|
>
|
||||||
<span data-progress-nums class='opacity-0 transition-opacity duration-300'>
|
<span data-progress-nums class='opacity-0 transition-opacity duration-300 text-gray-500'>
|
||||||
<span
|
|
||||||
class='mr-2.5 rounded-sm bg-yellow-200 px-1 py-0.5 text-xs font-medium uppercase text-yellow-900'
|
|
||||||
>
|
|
||||||
<span data-progress-percentage>0</span>% Done
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span>
|
|
||||||
<span data-progress-done>0</span> of <span data-progress-total>0</span> Done
|
<span data-progress-done>0</span> of <span data-progress-total>0</span> Done
|
||||||
</span>
|
</span>
|
||||||
</span>
|
|
||||||
|
<button
|
||||||
|
data-popup='progress-help'
|
||||||
|
class='flex items-center gap-1 text-sm font-medium text-gray-500 opacity-0 transition-opacity hover:text-black'
|
||||||
|
data-progress-nums
|
||||||
|
>
|
||||||
|
<AstroIcon icon='question' />
|
||||||
|
Track Progress
|
||||||
|
</button>
|
||||||
</p>
|
</p>
|
||||||
|
@@ -5,6 +5,7 @@ import RoadmapHint from './RoadmapHint.astro';
|
|||||||
import RoadmapNote from './RoadmapNote.astro';
|
import RoadmapNote from './RoadmapNote.astro';
|
||||||
import TopicSearch from './TopicSearch/TopicSearch.astro';
|
import TopicSearch from './TopicSearch/TopicSearch.astro';
|
||||||
import YouTubeAlert from './YouTubeAlert.astro';
|
import YouTubeAlert from './YouTubeAlert.astro';
|
||||||
|
import ProgressHelpPopup from './ProgressHelpPopup.astro';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -32,6 +33,7 @@ const isRoadmapReady = !isUpcoming;
|
|||||||
---
|
---
|
||||||
|
|
||||||
<LoginPopup />
|
<LoginPopup />
|
||||||
|
<ProgressHelpPopup />
|
||||||
|
|
||||||
<div class='border-b'>
|
<div class='border-b'>
|
||||||
<div class='container relative py-5 sm:py-12'>
|
<div class='container relative py-5 sm:py-12'>
|
||||||
|
@@ -18,6 +18,7 @@ import {
|
|||||||
import { pageProgressMessage, sponsorHidden } from '../../stores/page';
|
import { pageProgressMessage, sponsorHidden } from '../../stores/page';
|
||||||
import { TopicProgressButton } from './TopicProgressButton';
|
import { TopicProgressButton } from './TopicProgressButton';
|
||||||
import { ContributionForm } from './ContributionForm';
|
import { ContributionForm } from './ContributionForm';
|
||||||
|
import { showLoginPopup } from '../../lib/popup';
|
||||||
|
|
||||||
export function TopicDetail() {
|
export function TopicDetail() {
|
||||||
const [contributionAlertMessage, setContributionAlertMessage] = useState('');
|
const [contributionAlertMessage, setContributionAlertMessage] = useState('');
|
||||||
@@ -35,20 +36,6 @@ export function TopicDetail() {
|
|||||||
const [resourceId, setResourceId] = useState('');
|
const [resourceId, setResourceId] = useState('');
|
||||||
const [resourceType, setResourceType] = useState<ResourceType>('roadmap');
|
const [resourceType, setResourceType] = useState<ResourceType>('roadmap');
|
||||||
|
|
||||||
const showLoginPopup = () => {
|
|
||||||
const popupEl = document.querySelector(`#login-popup`);
|
|
||||||
if (!popupEl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
popupEl.classList.remove('hidden');
|
|
||||||
popupEl.classList.add('flex');
|
|
||||||
const focusEl = popupEl.querySelector<HTMLElement>('[autofocus]');
|
|
||||||
if (focusEl) {
|
|
||||||
focusEl.focus();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Close the topic detail when user clicks outside the topic detail
|
// Close the topic detail when user clicks outside the topic detail
|
||||||
useOutsideClick(topicRef, () => {
|
useOutsideClick(topicRef, () => {
|
||||||
setIsActive(false);
|
setIsActive(false);
|
||||||
@@ -188,7 +175,6 @@ export function TopicDetail() {
|
|||||||
topicId={topicId}
|
topicId={topicId}
|
||||||
resourceId={resourceId}
|
resourceId={resourceId}
|
||||||
resourceType={resourceType}
|
resourceType={resourceType}
|
||||||
onShowLoginPopup={showLoginPopup}
|
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setIsActive(false);
|
setIsActive(false);
|
||||||
setIsContributing(false);
|
setIsContributing(false);
|
||||||
|
@@ -12,13 +12,13 @@ import {
|
|||||||
renderTopicProgress,
|
renderTopicProgress,
|
||||||
updateResourceProgress,
|
updateResourceProgress,
|
||||||
} from '../../lib/resource-progress';
|
} from '../../lib/resource-progress';
|
||||||
|
import { showLoginPopup } from '../../lib/popup';
|
||||||
|
|
||||||
type TopicProgressButtonProps = {
|
type TopicProgressButtonProps = {
|
||||||
topicId: string;
|
topicId: string;
|
||||||
resourceId: string;
|
resourceId: string;
|
||||||
resourceType: ResourceType;
|
resourceType: ResourceType;
|
||||||
|
|
||||||
onShowLoginPopup: () => void;
|
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ const statusColors: Record<ResourceProgressType, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function TopicProgressButton(props: TopicProgressButtonProps) {
|
export function TopicProgressButton(props: TopicProgressButtonProps) {
|
||||||
const { topicId, resourceId, resourceType, onClose, onShowLoginPopup } =
|
const { topicId, resourceId, resourceType, onClose } =
|
||||||
props;
|
props;
|
||||||
|
|
||||||
const [isUpdatingProgress, setIsUpdatingProgress] = useState(true);
|
const [isUpdatingProgress, setIsUpdatingProgress] = useState(true);
|
||||||
@@ -119,7 +119,7 @@ export function TopicProgressButton(props: TopicProgressButtonProps) {
|
|||||||
const handleUpdateResourceProgress = (progress: ResourceProgressType) => {
|
const handleUpdateResourceProgress = (progress: ResourceProgressType) => {
|
||||||
if (isGuest) {
|
if (isGuest) {
|
||||||
onClose();
|
onClose();
|
||||||
onShowLoginPopup();
|
showLoginPopup();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -7,6 +7,6 @@
|
|||||||
class="bg-red-600 group-hover:bg-red-800 group-hover: px-1.5 py-0.5 rounded-sm text-white text-xs uppercase font-medium mr-2"
|
class="bg-red-600 group-hover:bg-red-800 group-hover: px-1.5 py-0.5 rounded-sm text-white text-xs uppercase font-medium mr-2"
|
||||||
>New</span
|
>New</span
|
||||||
>
|
>
|
||||||
<span class="underline mr-1">We also have a YouTube channel with visual content.</span>
|
<span class="underline mr-1">We also have a YouTube channel with visual content</span>
|
||||||
<span>»</span>
|
<span>»</span>
|
||||||
</a>
|
</a>
|
||||||
|
1
src/icons/question.svg
Normal file
1
src/icons/question.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M12 2C6.486 2 2 6.486 2 12s4.486 10 10 10 10-4.486 10-10S17.514 2 12 2zm1 16h-2v-2h2v2zm.976-4.885c-.196.158-.385.309-.535.459-.408.407-.44.777-.441.793v.133h-2v-.167c0-.118.029-1.177 1.026-2.174.195-.195.437-.393.691-.599.734-.595 1.216-1.029 1.216-1.627a1.934 1.934 0 0 0-3.867.001h-2C8.066 7.765 9.831 6 12 6s3.934 1.765 3.934 3.934c0 1.597-1.179 2.55-1.958 3.181z"></path></svg>
|
After Width: | Height: | Size: 535 B |
14
src/lib/popup.ts
Normal file
14
src/lib/popup.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export function showLoginPopup() {
|
||||||
|
const popupEl = document.querySelector(`#login-popup`);
|
||||||
|
if (!popupEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
popupEl.classList.remove('hidden');
|
||||||
|
popupEl.classList.add('flex');
|
||||||
|
|
||||||
|
const focusEl = popupEl.querySelector<HTMLElement>('[autofocus]');
|
||||||
|
if (focusEl) {
|
||||||
|
focusEl.focus();
|
||||||
|
}
|
||||||
|
}
|
@@ -18,6 +18,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
blockquote p:before {
|
blockquote p:before {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user