Add shortcuts for progress tracking

pull/4076/head
Kamran Ahmed 1 year ago
parent f0c47705cb
commit 32673c21fb
  1. 81
      src/components/FrameRenderer/renderer.ts
  2. 47
      src/components/ProgressHelpPopup.astro
  3. 31
      src/components/ResourceProgressStats.astro
  4. 2
      src/components/RoadmapHeader.astro
  5. 16
      src/components/TopicDetail/TopicDetail.tsx
  6. 6
      src/components/TopicDetail/TopicProgressButton.tsx
  7. 2
      src/components/YouTubeAlert.astro
  8. 1
      src/icons/question.svg
  9. 14
      src/lib/popup.ts
  10. 4
      src/styles/global.css

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

@ -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>&raquo;</span> <span>&raquo;</span>
</a> </a>

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

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

Loading…
Cancel
Save