parent
1e878069bc
commit
c4406b7649
3 changed files with 293 additions and 239 deletions
@ -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() { |
} |
||||||
return document.getElementById(this.loaderId); |
|
||||||
} |
get loaderEl() { |
||||||
|
return document.getElementById(this.loaderId); |
||||||
get markTopicDoneEl() { |
} |
||||||
return document.getElementById(this.markTopicDoneId); |
|
||||||
} |
get markTopicDoneEl() { |
||||||
|
return document.getElementById(this.markTopicDoneId); |
||||||
get markTopicPendingEl() { |
} |
||||||
return document.getElementById(this.markTopicPendingId); |
|
||||||
} |
get markTopicPendingEl() { |
||||||
|
return document.getElementById(this.markTopicPendingId); |
||||||
get topicActionsEl() { |
} |
||||||
return document.getElementById(this.topicActionsId); |
|
||||||
} |
get topicActionsEl() { |
||||||
|
return document.getElementById(this.topicActionsId); |
||||||
get contentEl() { |
} |
||||||
return document.getElementById(this.contentId); |
|
||||||
} |
get contributionTextEl() { |
||||||
|
return document.getElementById(this.contributionTextId); |
||||||
get overlayEl() { |
} |
||||||
return document.getElementById(this.overlayId); |
|
||||||
} |
get contentEl() { |
||||||
|
return document.getElementById(this.contentId); |
||||||
resetDOM(hideOverlay = false) { |
} |
||||||
if (hideOverlay) { |
|
||||||
this.overlayEl.classList.add('hidden'); |
get overlayEl() { |
||||||
} else { |
return document.getElementById(this.overlayId); |
||||||
this.overlayEl.classList.remove('hidden'); |
} |
||||||
} |
|
||||||
|
resetDOM(hideOverlay = false) { |
||||||
this.loaderEl.classList.remove('hidden'); // Show loader
|
if (hideOverlay) { |
||||||
this.topicActionsEl.classList.add('hidden'); // Hide Actions
|
this.overlayEl.classList.add('hidden'); |
||||||
this.contentEl.replaceChildren(''); // Remove content
|
} else { |
||||||
} |
this.overlayEl.classList.remove('hidden'); |
||||||
|
} |
||||||
close() { |
|
||||||
this.resetDOM(true); |
this.loaderEl.classList.remove('hidden'); // Show loader
|
||||||
|
this.topicActionsEl.classList.add('hidden'); // Hide Actions
|
||||||
this.activeRoadmapId = null; |
this.contributionTextEl.classList.add('hidden'); // Hide contribution text
|
||||||
this.activeTopicId = null; |
this.contentEl.replaceChildren(''); // Remove content
|
||||||
} |
} |
||||||
|
|
||||||
/** |
close() { |
||||||
* @param {string | HTMLElement} html |
this.resetDOM(true); |
||||||
*/ |
|
||||||
populate(html) { |
this.activeRoadmapId = null; |
||||||
this.contentEl.replaceChildren(html); |
this.activeTopicId = null; |
||||||
this.loaderEl.classList.add('hidden'); |
} |
||||||
this.topicActionsEl.classList.remove('hidden'); |
|
||||||
|
/** |
||||||
const normalizedGroup = (this.activeTopicId || '').replace(/^\d+-/, ''); |
* @param {string | HTMLElement} html |
||||||
const isDone = localStorage.getItem(normalizedGroup) === 'done'; |
*/ |
||||||
|
populate(html) { |
||||||
if (isDone) { |
this.contentEl.replaceChildren(html); |
||||||
this.markTopicDoneEl.classList.add('hidden'); |
this.loaderEl.classList.add('hidden'); |
||||||
this.markTopicPendingEl.classList.remove('hidden'); |
this.topicActionsEl.classList.remove('hidden'); |
||||||
} else { |
this.contributionTextEl.classList.remove('hidden'); |
||||||
this.markTopicDoneEl.classList.remove('hidden'); |
|
||||||
this.markTopicPendingEl.classList.add('hidden'); |
const normalizedGroup = (this.activeTopicId || '').replace(/^\d+-/, ''); |
||||||
} |
const isDone = localStorage.getItem(normalizedGroup) === 'done'; |
||||||
} |
|
||||||
|
if (isDone) { |
||||||
fetchTopicHtml(roadmapId, topicId) { |
this.markTopicDoneEl.classList.add('hidden'); |
||||||
const topicPartial = topicId.replace(/^\d+-/, '').replaceAll(/:/g, '/'); |
this.markTopicPendingEl.classList.remove('hidden'); |
||||||
const fullUrl = `/${roadmapId}/${topicPartial}/`; |
} else { |
||||||
|
this.markTopicDoneEl.classList.remove('hidden'); |
||||||
return fetch(fullUrl) |
this.markTopicPendingEl.classList.add('hidden'); |
||||||
.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'); |
|
||||||
|
|
||||||
return node.getElementById('main-content'); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
handleTopicClick(e) { |
|
||||||
const { roadmapId, topicId } = e.detail; |
|
||||||
if (!topicId || !roadmapId) { |
|
||||||
console.log('Missing topic or roadmap: ', e.detail); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
this.activeRoadmapId = roadmapId; |
|
||||||
this.activeTopicId = topicId; |
|
||||||
|
|
||||||
if (/^ext_link/.test(topicId)) { |
|
||||||
window.open(`https://${topicId.replace('ext_link:', '')}`); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
this.resetDOM(); |
|
||||||
this.fetchTopicHtml(roadmapId, topicId) |
|
||||||
.then((content) => { |
|
||||||
this.populate(content); |
|
||||||
}) |
|
||||||
.catch((e) => { |
|
||||||
console.error(e); |
|
||||||
this.populate('Error loading the content!'); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
queryRoadmapElementsByTopicId(topicId) { |
|
||||||
const elements = document.querySelectorAll(`[data-group-id$="-${topicId}"]`); |
|
||||||
const matchingElements = []; |
|
||||||
|
|
||||||
elements.forEach((element) => { |
|
||||||
const foundGroupId = element?.dataset?.groupId || ''; |
|
||||||
const validGroupRegex = new RegExp(`^\\d+-${topicId}$`); |
|
||||||
|
|
||||||
if (validGroupRegex.test(foundGroupId)) { |
|
||||||
matchingElements.push(element); |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
return matchingElements; |
|
||||||
} |
} |
||||||
|
} |
||||||
markAsDone(topicId) { |
|
||||||
const updatedTopicId = topicId.replace(/^\d+-/, ''); |
fetchTopicHtml(roadmapId, topicId) { |
||||||
localStorage.setItem(updatedTopicId, 'done'); |
const topicPartial = topicId.replace(/^\d+-/, '').replaceAll(/:/g, '/'); |
||||||
|
const fullUrl = `/${roadmapId}/${topicPartial}/`; |
||||||
this.queryRoadmapElementsByTopicId(updatedTopicId).forEach((item) => { |
|
||||||
item?.classList?.add('done'); |
return fetch(fullUrl) |
||||||
|
.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'); |
||||||
|
|
||||||
|
return node.getElementById('main-content'); |
||||||
}); |
}); |
||||||
} |
} |
||||||
|
|
||||||
markAsPending(topicId) { |
handleTopicClick(e) { |
||||||
const updatedTopicId = topicId.replace(/^\d+-/, ''); |
const { roadmapId, topicId } = e.detail; |
||||||
|
if (!topicId || !roadmapId) { |
||||||
localStorage.removeItem(updatedTopicId); |
console.log('Missing topic or roadmap: ', e.detail); |
||||||
this.queryRoadmapElementsByTopicId(updatedTopicId).forEach((item) => { |
return; |
||||||
item?.classList?.remove('done'); |
} |
||||||
|
|
||||||
|
this.activeRoadmapId = roadmapId; |
||||||
|
this.activeTopicId = topicId; |
||||||
|
|
||||||
|
if (/^ext_link/.test(topicId)) { |
||||||
|
window.open(`https://${topicId.replace('ext_link:', '')}`); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
this.resetDOM(); |
||||||
|
this.fetchTopicHtml(roadmapId, topicId) |
||||||
|
.then((content) => { |
||||||
|
this.populate(content); |
||||||
|
}) |
||||||
|
.catch((e) => { |
||||||
|
console.error(e); |
||||||
|
this.populate('Error loading the content!'); |
||||||
}); |
}); |
||||||
} |
} |
||||||
|
|
||||||
handleOverlayClick(e) { |
queryRoadmapElementsByTopicId(topicId) { |
||||||
const isClickedInsideTopic = e.target.closest(`#${this.topicBodyId}`); |
const elements = document.querySelectorAll( |
||||||
|
`[data-group-id$="-${topicId}"]` |
||||||
if (!isClickedInsideTopic) { |
); |
||||||
this.close(); |
const matchingElements = []; |
||||||
return; |
|
||||||
} |
elements.forEach((element) => { |
||||||
|
const foundGroupId = element?.dataset?.groupId || ''; |
||||||
const isClickedDone = e.target.id === this.markTopicDoneId || e.target.closest(`#${this.markTopicDoneId}`); |
const validGroupRegex = new RegExp(`^\\d+-${topicId}$`); |
||||||
if (isClickedDone) { |
|
||||||
this.markAsDone(this.activeTopicId); |
if (validGroupRegex.test(foundGroupId)) { |
||||||
this.close(); |
matchingElements.push(element); |
||||||
} |
|
||||||
|
|
||||||
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) { |
return matchingElements; |
||||||
|
} |
||||||
|
|
||||||
|
markAsDone(topicId) { |
||||||
|
const updatedTopicId = topicId.replace(/^\d+-/, ''); |
||||||
|
localStorage.setItem(updatedTopicId, 'done'); |
||||||
|
|
||||||
|
this.queryRoadmapElementsByTopicId(updatedTopicId).forEach((item) => { |
||||||
|
item?.classList?.add('done'); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
markAsPending(topicId) { |
||||||
|
const updatedTopicId = topicId.replace(/^\d+-/, ''); |
||||||
|
|
||||||
|
localStorage.removeItem(updatedTopicId); |
||||||
|
this.queryRoadmapElementsByTopicId(updatedTopicId).forEach((item) => { |
||||||
|
item?.classList?.remove('done'); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
handleOverlayClick(e) { |
||||||
|
const isClickedInsideTopic = e.target.closest(`#${this.topicBodyId}`); |
||||||
|
|
||||||
|
if (!isClickedInsideTopic) { |
||||||
|
this.close(); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const isClickedDone = |
||||||
|
e.target.id === this.markTopicDoneId || |
||||||
|
e.target.closest(`#${this.markTopicDoneId}`); |
||||||
|
if (isClickedDone) { |
||||||
|
this.markAsDone(this.activeTopicId); |
||||||
|
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(); |
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> |
||||||
<div class="bg-gray-900 bg-opacity-50 dark:bg-opacity-80 fixed inset-0 z-30"></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> |
||||||
|
|
||||||
|
<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> |
||||||
|
</div> |
||||||
|
Loading…
Reference in new issue