Add meta text below roadmap topic for contribution

pull/3258/head
Kamran Ahmed 2 years ago
parent 1e878069bc
commit c4406b7649
  1. 39
      src/components/InteractiveRoadmap/InteractiveRoadmap.astro
  2. 345
      src/components/InteractiveRoadmap/topic.js
  3. 86
      src/components/TopicOverlay.astro

@ -1,10 +1,10 @@
--- ---
import DownloadPopup from "../DownloadPopup.astro"; import DownloadPopup from '../DownloadPopup.astro';
import Loader from "../Loader.astro"; import Loader from '../Loader.astro';
import ShareIcons from "../ShareIcons.astro"; import ShareIcons from '../ShareIcons.astro';
import SubscribePopup from "../SubscribePopup.astro"; import SubscribePopup from '../SubscribePopup.astro';
import TopicOverlay from "../TopicOverlay.astro"; import TopicOverlay from '../TopicOverlay.astro';
import "./InteractiveRoadmap.css"; import './InteractiveRoadmap.css';
export interface Props { export interface Props {
roadmapId: string; roadmapId: string;
@ -16,31 +16,32 @@ export interface Props {
}; };
} }
const { roadmapId, jsonUrl, dimensions = null, description } = const { roadmapId, jsonUrl, dimensions = null, description } = Astro.props;
Astro.props;
--- ---
<link <link
rel="preload" rel='preload'
href="/fonts/balsamiq.woff2" href='/fonts/balsamiq.woff2'
as="font" as='font'
type="font/woff2" type='font/woff2'
crossorigin crossorigin
slot="after-header" slot='after-header'
/> />
<div class="bg-gray-50 py-4 sm:py-12"> <div class='bg-gray-50 py-4 sm:py-12'>
<div class="max-w-[1000px] container relative"> <div class='max-w-[1000px] container relative'>
<ShareIcons <ShareIcons
description={description} description={description}
pageUrl={`https://roadmap.sh/${roadmapId}`} pageUrl={`https://roadmap.sh/${roadmapId}`}
/> />
<DownloadPopup /> <DownloadPopup />
<SubscribePopup /> <SubscribePopup />
<TopicOverlay /> <TopicOverlay roadmapId={roadmapId} />
<div <div
id="roadmap-svg" id='roadmap-svg'
style={dimensions ? `--aspect-ratio:${dimensions.width}/${dimensions.height}` : null} style={dimensions
? `--aspect-ratio:${dimensions.width}/${dimensions.height}`
: null}
data-roadmap-id={roadmapId} data-roadmap-id={roadmapId}
data-json-url={jsonUrl} data-json-url={jsonUrl}
> >
@ -49,4 +50,4 @@ const { roadmapId, jsonUrl, dimensions = null, description } =
</div> </div>
</div> </div>
<script src="./roadmap.js"></script> <script src='./roadmap.js'></script>

@ -1,204 +1,219 @@
export class Topic { export class Topic {
constructor() { constructor() {
this.overlayId = 'topic-overlay'; this.overlayId = 'topic-overlay';
this.contentId = 'topic-content'; this.contentId = 'topic-content';
this.loaderId = 'topic-loader'; this.loaderId = 'topic-loader';
this.topicBodyId = 'topic-body'; this.topicBodyId = 'topic-body';
this.topicActionsId = 'topic-actions'; this.topicActionsId = 'topic-actions';
this.markTopicDoneId = 'mark-topic-done'; this.markTopicDoneId = 'mark-topic-done';
this.markTopicPendingId = 'mark-topic-pending'; this.markTopicPendingId = 'mark-topic-pending';
this.closeTopicId = 'close-topic'; this.closeTopicId = 'close-topic';
this.contributionTextId = 'contrib-meta';
this.activeRoadmapId = null;
this.activeTopicId = null; this.activeRoadmapId = null;
this.activeTopicId = null;
this.handleTopicClick = this.handleTopicClick.bind(this);
this.handleTopicClick = this.handleTopicClick.bind(this);
this.close = this.close.bind(this);
this.resetDOM = this.resetDOM.bind(this); this.close = this.close.bind(this);
this.populate = this.populate.bind(this); this.resetDOM = this.resetDOM.bind(this);
this.handleOverlayClick = this.handleOverlayClick.bind(this); this.populate = this.populate.bind(this);
this.markAsDone = this.markAsDone.bind(this); this.handleOverlayClick = this.handleOverlayClick.bind(this);
this.markAsPending = this.markAsPending.bind(this); this.markAsDone = this.markAsDone.bind(this);
this.queryRoadmapElementsByTopicId = this.queryRoadmapElementsByTopicId.bind(this); this.markAsPending = this.markAsPending.bind(this);
this.queryRoadmapElementsByTopicId =
this.init = this.init.bind(this); this.queryRoadmapElementsByTopicId.bind(this);
}
this.init = this.init.bind(this);
}
get loaderEl() { get loaderEl() {
return document.getElementById(this.loaderId); return document.getElementById(this.loaderId);
} }
get markTopicDoneEl() { get markTopicDoneEl() {
return document.getElementById(this.markTopicDoneId); return document.getElementById(this.markTopicDoneId);
} }
get markTopicPendingEl() { get markTopicPendingEl() {
return document.getElementById(this.markTopicPendingId); return document.getElementById(this.markTopicPendingId);
} }
get topicActionsEl() { get topicActionsEl() {
return document.getElementById(this.topicActionsId); return document.getElementById(this.topicActionsId);
} }
get contentEl() { get contributionTextEl() {
return document.getElementById(this.contentId); return document.getElementById(this.contributionTextId);
} }
get overlayEl() { get contentEl() {
return document.getElementById(this.overlayId); return document.getElementById(this.contentId);
} }
resetDOM(hideOverlay = false) { get overlayEl() {
if (hideOverlay) { return document.getElementById(this.overlayId);
this.overlayEl.classList.add('hidden'); }
} else {
this.overlayEl.classList.remove('hidden');
}
this.loaderEl.classList.remove('hidden'); // Show loader resetDOM(hideOverlay = false) {
this.topicActionsEl.classList.add('hidden'); // Hide Actions if (hideOverlay) {
this.contentEl.replaceChildren(''); // Remove content this.overlayEl.classList.add('hidden');
} else {
this.overlayEl.classList.remove('hidden');
} }
close() { this.loaderEl.classList.remove('hidden'); // Show loader
this.resetDOM(true); this.topicActionsEl.classList.add('hidden'); // Hide Actions
this.contributionTextEl.classList.add('hidden'); // Hide contribution text
this.contentEl.replaceChildren(''); // Remove content
}
this.activeRoadmapId = null; close() {
this.activeTopicId = null; this.resetDOM(true);
}
/** this.activeRoadmapId = null;
* @param {string | HTMLElement} html this.activeTopicId = null;
*/ }
populate(html) {
this.contentEl.replaceChildren(html);
this.loaderEl.classList.add('hidden');
this.topicActionsEl.classList.remove('hidden');
const normalizedGroup = (this.activeTopicId || '').replace(/^\d+-/, '');
const isDone = localStorage.getItem(normalizedGroup) === 'done';
if (isDone) {
this.markTopicDoneEl.classList.add('hidden');
this.markTopicPendingEl.classList.remove('hidden');
} else {
this.markTopicDoneEl.classList.remove('hidden');
this.markTopicPendingEl.classList.add('hidden');
}
}
fetchTopicHtml(roadmapId, topicId) { /**
const topicPartial = topicId.replace(/^\d+-/, '').replaceAll(/:/g, '/'); * @param {string | HTMLElement} html
const fullUrl = `/${roadmapId}/${topicPartial}/`; */
populate(html) {
return fetch(fullUrl) this.contentEl.replaceChildren(html);
.then((res) => { this.loaderEl.classList.add('hidden');
return res.text(); this.topicActionsEl.classList.remove('hidden');
}) this.contributionTextEl.classList.remove('hidden');
.then((topicHtml) => {
// It's full HTML with page body, head etc. const normalizedGroup = (this.activeTopicId || '').replace(/^\d+-/, '');
// We only need the inner HTML of the #main-content const isDone = localStorage.getItem(normalizedGroup) === 'done';
const node = new DOMParser().parseFromString(topicHtml, 'text/html');
if (isDone) {
return node.getElementById('main-content'); this.markTopicDoneEl.classList.add('hidden');
}); this.markTopicPendingEl.classList.remove('hidden');
} else {
this.markTopicDoneEl.classList.remove('hidden');
this.markTopicPendingEl.classList.add('hidden');
} }
}
handleTopicClick(e) { fetchTopicHtml(roadmapId, topicId) {
const { roadmapId, topicId } = e.detail; const topicPartial = topicId.replace(/^\d+-/, '').replaceAll(/:/g, '/');
if (!topicId || !roadmapId) { const fullUrl = `/${roadmapId}/${topicPartial}/`;
console.log('Missing topic or roadmap: ', e.detail);
return;
}
this.activeRoadmapId = roadmapId; return fetch(fullUrl)
this.activeTopicId = topicId; .then((res) => {
return res.text();
})
.then((topicHtml) => {
// It's full HTML with page body, head etc.
// We only need the inner HTML of the #main-content
const node = new DOMParser().parseFromString(topicHtml, 'text/html');
if (/^ext_link/.test(topicId)) { return node.getElementById('main-content');
window.open(`https://${topicId.replace('ext_link:', '')}`); });
return; }
}
this.resetDOM(); handleTopicClick(e) {
this.fetchTopicHtml(roadmapId, topicId) const { roadmapId, topicId } = e.detail;
.then((content) => { if (!topicId || !roadmapId) {
this.populate(content); console.log('Missing topic or roadmap: ', e.detail);
}) return;
.catch((e) => {
console.error(e);
this.populate('Error loading the content!');
});
} }
queryRoadmapElementsByTopicId(topicId) { this.activeRoadmapId = roadmapId;
const elements = document.querySelectorAll(`[data-group-id$="-${topicId}"]`); this.activeTopicId = topicId;
const matchingElements = [];
elements.forEach((element) => { if (/^ext_link/.test(topicId)) {
const foundGroupId = element?.dataset?.groupId || ''; window.open(`https://${topicId.replace('ext_link:', '')}`);
const validGroupRegex = new RegExp(`^\\d+-${topicId}$`); return;
}
if (validGroupRegex.test(foundGroupId)) { this.resetDOM();
matchingElements.push(element); this.fetchTopicHtml(roadmapId, topicId)
} .then((content) => {
this.populate(content);
})
.catch((e) => {
console.error(e);
this.populate('Error loading the content!');
}); });
}
return matchingElements; queryRoadmapElementsByTopicId(topicId) {
} const elements = document.querySelectorAll(
`[data-group-id$="-${topicId}"]`
);
const matchingElements = [];
markAsDone(topicId) { elements.forEach((element) => {
const updatedTopicId = topicId.replace(/^\d+-/, ''); const foundGroupId = element?.dataset?.groupId || '';
localStorage.setItem(updatedTopicId, 'done'); const validGroupRegex = new RegExp(`^\\d+-${topicId}$`);
this.queryRoadmapElementsByTopicId(updatedTopicId).forEach((item) => { if (validGroupRegex.test(foundGroupId)) {
item?.classList?.add('done'); matchingElements.push(element);
}); }
} });
markAsPending(topicId) { return matchingElements;
const updatedTopicId = topicId.replace(/^\d+-/, ''); }
localStorage.removeItem(updatedTopicId); markAsDone(topicId) {
this.queryRoadmapElementsByTopicId(updatedTopicId).forEach((item) => { const updatedTopicId = topicId.replace(/^\d+-/, '');
item?.classList?.remove('done'); localStorage.setItem(updatedTopicId, 'done');
});
}
handleOverlayClick(e) { this.queryRoadmapElementsByTopicId(updatedTopicId).forEach((item) => {
const isClickedInsideTopic = e.target.closest(`#${this.topicBodyId}`); item?.classList?.add('done');
});
}
if (!isClickedInsideTopic) { markAsPending(topicId) {
this.close(); const updatedTopicId = topicId.replace(/^\d+-/, '');
return;
}
const isClickedDone = e.target.id === this.markTopicDoneId || e.target.closest(`#${this.markTopicDoneId}`); localStorage.removeItem(updatedTopicId);
if (isClickedDone) { this.queryRoadmapElementsByTopicId(updatedTopicId).forEach((item) => {
this.markAsDone(this.activeTopicId); item?.classList?.remove('done');
this.close(); });
} }
const isClickedPending = e.target.id === this.markTopicPendingId || e.target.closest(`#${this.markTopicPendingId}`); handleOverlayClick(e) {
if (isClickedPending) { const isClickedInsideTopic = e.target.closest(`#${this.topicBodyId}`);
this.markAsPending(this.activeTopicId);
this.close();
}
const isClickedClose = e.target.id === this.closeTopicId || e.target.closest(`#${this.closeTopicId}`); if (!isClickedInsideTopic) {
if (isClickedClose) { this.close();
this.close(); return;
}
} }
init() { const isClickedDone =
window.addEventListener('topic.click', this.handleTopicClick); e.target.id === this.markTopicDoneId ||
window.addEventListener('click', this.handleOverlayClick); e.target.closest(`#${this.markTopicDoneId}`);
window.addEventListener('keydown', (e) => { if (isClickedDone) {
if (e.key.toLowerCase() === 'escape') { this.markAsDone(this.activeTopicId);
this.close(); this.close();
} }
});
const isClickedPending =
e.target.id === this.markTopicPendingId ||
e.target.closest(`#${this.markTopicPendingId}`);
if (isClickedPending) {
this.markAsPending(this.activeTopicId);
this.close();
}
const isClickedClose =
e.target.id === this.closeTopicId ||
e.target.closest(`#${this.closeTopicId}`);
if (isClickedClose) {
this.close();
} }
} }
init() {
window.addEventListener('topic.click', this.handleTopicClick);
window.addEventListener('click', this.handleOverlayClick);
window.addEventListener('keydown', (e) => {
if (e.key.toLowerCase() === 'escape') {
this.close();
}
});
}
}

@ -1,31 +1,69 @@
--- ---
import Icon from "./Icon.astro"; import Icon from './Icon.astro';
import Loader from "./Loader.astro"; import Loader from './Loader.astro';
export interface Props {
roadmapId: string;
}
const { roadmapId } = Astro.props;
const githubLink = `https://github.com/kamranahmedse/developer-roadmap/tree/master/src/roadmaps/${roadmapId}/content`;
--- ---
<div id='topic-overlay' class='hidden'> <div id='topic-overlay' class='hidden'>
<div class="fixed top-0 right-0 z-40 h-screen p-4 sm:p-6 overflow-y-auto bg-white w-full sm:max-w-[600px]" tabindex="-1" id='topic-body'> <div
<div id='topic-loader' class='hidden'> class='fixed top-0 right-0 z-40 h-screen p-4 sm:p-6 overflow-y-auto bg-white w-full sm:max-w-[600px]'
<Loader /> tabindex='-1'
</div> id='topic-body'
>
<div id='topic-actions' class='hidden mb-2'> <div id='topic-loader' class='hidden'>
<button id='mark-topic-done' class='bg-green-600 text-white p-1 px-2 text-sm rounded-md hover:bg-green-700 inline-flex items-center'> <Loader />
<Icon icon="check" /> <span class='ml-2'>Mark as Done</span> </div>
</button>
<div id='topic-actions' class='hidden mb-2'>
<button id='mark-topic-pending' class='hidden bg-red-600 text-white p-1 px-2 text-sm rounded-md hover:bg-red-700 inline-flex items-center'> <button
<Icon icon="reset" /> <span class='ml-2'>Mark as Pending</span> id='mark-topic-done'
</button> class='bg-green-600 text-white p-1 px-2 text-sm rounded-md hover:bg-green-700 inline-flex items-center'
>
<button type="button" id='close-topic' class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 absolute top-2.5 right-2.5 inline-flex items-center"> <Icon icon='check' />
<Icon icon="close" /> <span class='ml-2'>Mark as Done</span>
</button> </button>
</div>
<button
<div id='topic-content' class='prose prose-h1:mt-7 prose-h1:mb-2.5 prose-p:mt-0 prose-p:mb-2 prose-li:m-0 prose-li:mb-0.5 prose-h2:mb-3 prose-h2:mt-0'></div> id='mark-topic-pending'
class='hidden bg-red-600 text-white p-1 px-2 text-sm rounded-md hover:bg-red-700 inline-flex items-center'
>
<Icon icon='reset' />
<span class='ml-2'>Mark as Pending</span>
</button>
<button
type='button'
id='close-topic'
class='text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 absolute top-2.5 right-2.5 inline-flex items-center'
>
<Icon icon='close' />
</button>
</div>
<div
id='topic-content'
class='prose prose-h1:mt-7 prose-h1:mb-2.5 prose-p:mt-0 prose-p:mb-2 prose-li:m-0 prose-li:mb-0.5 prose-h2:mb-3 prose-h2:mt-0'
>
</div> </div>
<div class="bg-gray-900 bg-opacity-50 dark:bg-opacity-80 fixed inset-0 z-30"></div>
</div>
<p
id='contrib-meta'
class='text-gray-400 text-sm border-t pt-3 mt-10 hidden'
>
We are still working on this page. You can contribute by submitting a
brief description and a few links to learn more about this topic <a
target='_blank'
class='underline text-blue-700'
href={githubLink}>on GitHub repository.</a
>.
</p>
</div>
<div class='bg-gray-900 bg-opacity-50 dark:bg-opacity-80 fixed inset-0 z-30'>
</div>
</div>

Loading…
Cancel
Save