mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-08-22 08:53:01 +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 { 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
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 {
|
||||
isSecondaryBanner?: boolean;
|
||||
}
|
||||
@@ -37,21 +38,31 @@ const { isSecondaryBanner = false } = Astro.props;
|
||||
>
|
||||
<span><span data-progress-total>0</span> Total</span>
|
||||
</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>
|
||||
|
||||
<p
|
||||
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
|
||||
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>
|
||||
<span data-progress-nums class='opacity-0 transition-opacity duration-300 text-gray-500'>
|
||||
<span data-progress-done>0</span> of <span data-progress-total>0</span> Done
|
||||
</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>
|
||||
|
@@ -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;
|
||||
---
|
||||
|
||||
<LoginPopup />
|
||||
<ProgressHelpPopup />
|
||||
|
||||
<div class='border-b'>
|
||||
<div class='container relative py-5 sm:py-12'>
|
||||
|
@@ -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<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
|
||||
useOutsideClick(topicRef, () => {
|
||||
setIsActive(false);
|
||||
@@ -188,7 +175,6 @@ export function TopicDetail() {
|
||||
topicId={topicId}
|
||||
resourceId={resourceId}
|
||||
resourceType={resourceType}
|
||||
onShowLoginPopup={showLoginPopup}
|
||||
onClose={() => {
|
||||
setIsActive(false);
|
||||
setIsContributing(false);
|
||||
|
@@ -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<ResourceProgressType, string> = {
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@@ -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</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>
|
||||
</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 {
|
||||
display: none;
|
||||
}
|
||||
|
Reference in New Issue
Block a user