From 32673c21fb05fd0f59d30db9d3039e9e6eeb5479 Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Sat, 17 Jun 2023 15:10:10 +0100 Subject: [PATCH] Add shortcuts for progress tracking --- src/components/FrameRenderer/renderer.ts | 81 ++++++++++++++++++- src/components/ProgressHelpPopup.astro | 47 +++++++++++ src/components/ResourceProgressStats.astro | 33 +++++--- src/components/RoadmapHeader.astro | 2 + src/components/TopicDetail/TopicDetail.tsx | 16 +--- .../TopicDetail/TopicProgressButton.tsx | 6 +- src/components/YouTubeAlert.astro | 2 +- src/icons/question.svg | 1 + src/lib/popup.ts | 14 ++++ src/styles/global.css | 4 + 10 files changed, 174 insertions(+), 32 deletions(-) create mode 100644 src/components/ProgressHelpPopup.astro create mode 100644 src/icons/question.svg create mode 100644 src/lib/popup.ts diff --git a/src/components/FrameRenderer/renderer.ts b/src/components/FrameRenderer/renderer.ts index 9cf5effb9..63ad29a77 100644 --- a/src/components/FrameRenderer/renderer.ts +++ b/src/components/FrameRenderer/renderer.ts @@ -2,13 +2,19 @@ import { wireframeJSONToSVG } from 'roadmap-renderer'; import { httpPost } from '../../lib/http'; import { isLoggedIn } from '../../lib/jwt'; import { + refreshProgressCounters, renderResourceProgress, + renderTopicProgress, + ResourceProgressType, ResourceType, + updateResourceProgress, } from '../../lib/resource-progress'; +import { pageProgressMessage } from '../../stores/page'; +import { showLoginPopup } from '../../lib/popup'; export class Renderer { resourceId: string; - resourceType: string; + resourceType: ResourceType | string; jsonUrl: string; loaderHTML: string | null; @@ -28,8 +34,10 @@ export class Renderer { this.onDOMLoaded = this.onDOMLoaded.bind(this); this.jsonToSvg = this.jsonToSvg.bind(this); this.handleSvgClick = this.handleSvgClick.bind(this); + this.handleSvgRightClick = this.handleSvgRightClick.bind(this); this.prepareConfig = this.prepareConfig.bind(this); this.switchRoadmap = this.switchRoadmap.bind(this); + this.updateTopicStatus = this.updateTopicStatus.bind(this); } get loaderEl() { @@ -161,6 +169,53 @@ export class Renderer { 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) { const targetGroup = e.target?.closest('g') || {}; const groupId = targetGroup.dataset ? targetGroup.dataset.groupId : ''; @@ -209,6 +264,28 @@ export class Renderer { // Remove sorting prefix from groupId 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( new CustomEvent(`${this.resourceType}.topic.click`, { detail: { @@ -223,7 +300,7 @@ export class Renderer { init() { window.addEventListener('DOMContentLoaded', this.onDOMLoaded); window.addEventListener('click', this.handleSvgClick); - // window.addEventListener('contextmenu', this.handleSvgClick); + window.addEventListener('contextmenu', this.handleSvgRightClick); } } diff --git a/src/components/ProgressHelpPopup.astro b/src/components/ProgressHelpPopup.astro new file mode 100644 index 000000000..3aa6d92ec --- /dev/null +++ b/src/components/ProgressHelpPopup.astro @@ -0,0 +1,47 @@ +--- +import AstroIcon from './AstroIcon.astro'; +import Popup from './Popup/Popup.astro'; +--- + + +
+

+ Track your Progress +

+

+ Login and use one of the options listed below. +

+ +
+
+ Option 1 +

+ Click the roadmap topics and use Update Progress dropdown to update your progress. +

+
+ +
+ Option 2 +

Use the keyboard shortcuts listed below.

+ +
    +
  • + Right Mouse Click to mark as Done. +
  • +
  • + Shift + Click to mark as in progress. +
  • +
  • + Option + Click to mark as skipped. +
  • +
+
+
+
+
diff --git a/src/components/ResourceProgressStats.astro b/src/components/ResourceProgressStats.astro index de0bc1813..93e5fcf34 100644 --- a/src/components/ResourceProgressStats.astro +++ b/src/components/ResourceProgressStats.astro @@ -1,4 +1,5 @@ --- +import AstroIcon from './AstroIcon.astro'; export interface Props { isSecondaryBanner?: boolean; } @@ -37,21 +38,31 @@ const { isSecondaryBanner = false } = Astro.props; > 0 Total

+ +

- - - 0% Done - - - - 0 of 0 Done - + + 0 of 0 Done + +

diff --git a/src/components/RoadmapHeader.astro b/src/components/RoadmapHeader.astro index c1c7ba6d1..5cbb0595f 100644 --- a/src/components/RoadmapHeader.astro +++ b/src/components/RoadmapHeader.astro @@ -5,6 +5,7 @@ import RoadmapHint from './RoadmapHint.astro'; import RoadmapNote from './RoadmapNote.astro'; import TopicSearch from './TopicSearch/TopicSearch.astro'; import YouTubeAlert from './YouTubeAlert.astro'; +import ProgressHelpPopup from './ProgressHelpPopup.astro'; export interface Props { title: string; @@ -32,6 +33,7 @@ const isRoadmapReady = !isUpcoming; --- +
diff --git a/src/components/TopicDetail/TopicDetail.tsx b/src/components/TopicDetail/TopicDetail.tsx index 10afd0574..799385077 100644 --- a/src/components/TopicDetail/TopicDetail.tsx +++ b/src/components/TopicDetail/TopicDetail.tsx @@ -18,6 +18,7 @@ import { import { pageProgressMessage, sponsorHidden } from '../../stores/page'; import { TopicProgressButton } from './TopicProgressButton'; import { ContributionForm } from './ContributionForm'; +import { showLoginPopup } from '../../lib/popup'; export function TopicDetail() { const [contributionAlertMessage, setContributionAlertMessage] = useState(''); @@ -35,20 +36,6 @@ export function TopicDetail() { const [resourceId, setResourceId] = useState(''); const [resourceType, setResourceType] = useState('roadmap'); - const showLoginPopup = () => { - const popupEl = document.querySelector(`#login-popup`); - if (!popupEl) { - return; - } - - popupEl.classList.remove('hidden'); - popupEl.classList.add('flex'); - const focusEl = popupEl.querySelector('[autofocus]'); - if (focusEl) { - focusEl.focus(); - } - }; - // Close the topic detail when user clicks outside the topic detail useOutsideClick(topicRef, () => { setIsActive(false); @@ -188,7 +175,6 @@ export function TopicDetail() { topicId={topicId} resourceId={resourceId} resourceType={resourceType} - onShowLoginPopup={showLoginPopup} onClose={() => { setIsActive(false); setIsContributing(false); diff --git a/src/components/TopicDetail/TopicProgressButton.tsx b/src/components/TopicDetail/TopicProgressButton.tsx index 80844d736..24f64d661 100644 --- a/src/components/TopicDetail/TopicProgressButton.tsx +++ b/src/components/TopicDetail/TopicProgressButton.tsx @@ -12,13 +12,13 @@ import { renderTopicProgress, updateResourceProgress, } from '../../lib/resource-progress'; +import { showLoginPopup } from '../../lib/popup'; type TopicProgressButtonProps = { topicId: string; resourceId: string; resourceType: ResourceType; - onShowLoginPopup: () => void; onClose: () => void; }; @@ -30,7 +30,7 @@ const statusColors: Record = { }; export function TopicProgressButton(props: TopicProgressButtonProps) { - const { topicId, resourceId, resourceType, onClose, onShowLoginPopup } = + const { topicId, resourceId, resourceType, onClose } = props; const [isUpdatingProgress, setIsUpdatingProgress] = useState(true); @@ -119,7 +119,7 @@ export function TopicProgressButton(props: TopicProgressButtonProps) { const handleUpdateResourceProgress = (progress: ResourceProgressType) => { if (isGuest) { onClose(); - onShowLoginPopup(); + showLoginPopup(); return; } diff --git a/src/components/YouTubeAlert.astro b/src/components/YouTubeAlert.astro index d655311fd..cd0ec7a42 100644 --- a/src/components/YouTubeAlert.astro +++ b/src/components/YouTubeAlert.astro @@ -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" >New - We also have a YouTube channel with visual content. + We also have a YouTube channel with visual content » diff --git a/src/icons/question.svg b/src/icons/question.svg new file mode 100644 index 000000000..72f6337d8 --- /dev/null +++ b/src/icons/question.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/lib/popup.ts b/src/lib/popup.ts new file mode 100644 index 000000000..e2d96514f --- /dev/null +++ b/src/lib/popup.ts @@ -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('[autofocus]'); + if (focusEl) { + focusEl.focus(); + } +} diff --git a/src/styles/global.css b/src/styles/global.css index 12bce1559..9f5819ca9 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -18,6 +18,10 @@ } } +svg { + user-select: none; +} + blockquote p:before { display: none; }