mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-08-11 03:34:00 +02:00
Refactor buttons
This commit is contained in:
@@ -187,9 +187,14 @@ export function TopicDetail() {
|
||||
return null;
|
||||
}
|
||||
|
||||
const contributionDir = resourceType === 'roadmap' ? 'roadmaps' : 'best-practices';
|
||||
const contributionDir =
|
||||
resourceType === 'roadmap' ? 'roadmaps' : 'best-practices';
|
||||
const contributionUrl = `https://github.com/kamranahmedse/developer-roadmap/tree/master/src/data/${contributionDir}/${resourceId}/content`;
|
||||
|
||||
const allowMarkingDone = ['pending', 'learning'].includes(progress);
|
||||
const allowMarkingLearning = ['pending'].includes(progress);
|
||||
const allowMarkingPending = ['done', 'learning'].includes(progress);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
@@ -218,7 +223,7 @@ export function TopicDetail() {
|
||||
onClick={() => setIsActive(false)}
|
||||
>
|
||||
<img alt="Check" class="w-3" src={CheckIcon} />
|
||||
<span className="ml-2">Done</span>
|
||||
<span className="ml-2">Mark as Done</span>
|
||||
</button>
|
||||
<button
|
||||
data-popup="login-popup"
|
||||
@@ -232,7 +237,7 @@ export function TopicDetail() {
|
||||
)}
|
||||
|
||||
{!isGuest && (
|
||||
<>
|
||||
<div class="flex items-center gap-2 rounded-md">
|
||||
{isUpdatingProgress && (
|
||||
<button className="inline-flex cursor-default items-center rounded-md border border-gray-300 bg-white p-1 px-2 text-sm text-black">
|
||||
<img
|
||||
@@ -243,55 +248,37 @@ export function TopicDetail() {
|
||||
<span className="ml-2">Updating Status..</span>
|
||||
</button>
|
||||
)}
|
||||
{!isUpdatingProgress && progress === 'pending' && (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
className="inline-flex items-center rounded-md border border-green-600 bg-green-600 p-1 px-2 text-sm text-white hover:bg-green-700"
|
||||
onClick={() => handleUpdateResourceProgress('done')}
|
||||
>
|
||||
<img alt="Check" class="w-3" src={CheckIcon} />
|
||||
<span className="ml-2">Done</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="inline-flex items-center rounded-md bg-[#dad1fd] p-1 px-2 text-sm text-[#0E033B] hover:bg-[#C4B6FC]"
|
||||
onClick={() => handleUpdateResourceProgress('learning')}
|
||||
>
|
||||
<img alt="Learning" class="w-4" src={ProgressIcon} />
|
||||
<span className="ml-2">In Progress</span>
|
||||
</button>
|
||||
</div>
|
||||
{!isUpdatingProgress && allowMarkingDone && (
|
||||
<button
|
||||
className="inline-flex items-center rounded-md border border-green-600 bg-green-600 p-1 px-2 text-sm text-white hover:bg-green-700"
|
||||
onClick={() => handleUpdateResourceProgress('done')}
|
||||
>
|
||||
<img alt="Check" class="w-3" src={CheckIcon} />
|
||||
<span className="ml-2">Mark as Done</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{!isUpdatingProgress && progress === 'done' && (
|
||||
{!isUpdatingProgress && allowMarkingLearning && (
|
||||
<button
|
||||
className="inline-flex items-center rounded-md bg-[#dad1fd] p-1 px-2 text-sm text-[#0E033B] hover:bg-[#C4B6FC]"
|
||||
onClick={() => handleUpdateResourceProgress('learning')}
|
||||
>
|
||||
<img alt="Learning" className="w-4" src={ProgressIcon} />
|
||||
<span className="ml-2">In Progress</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{!isUpdatingProgress && allowMarkingPending && (
|
||||
<button
|
||||
className="inline-flex items-center rounded-md border border-red-600 bg-red-600 p-1 px-2 text-sm text-white hover:bg-red-700"
|
||||
onClick={() => handleUpdateResourceProgress('pending')}
|
||||
>
|
||||
<img alt="Check" class="h-4" src={ResetIcon} />
|
||||
<span className="ml-2">Pending</span>
|
||||
<span className="ml-2">Mark as Pending</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{!isUpdatingProgress && progress === 'learning' && (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
className="inline-flex items-center rounded-md border border-green-600 bg-green-600 p-1 px-2 text-sm text-white hover:bg-green-700"
|
||||
onClick={() => handleUpdateResourceProgress('done')}
|
||||
>
|
||||
<img alt="Check" class="w-3" src={CheckIcon} />
|
||||
<span className="ml-2">Done</span>
|
||||
</button>
|
||||
<button
|
||||
className="inline-flex items-center rounded-md border border-red-600 bg-red-600 p-1 px-2 text-sm text-white hover:bg-red-700"
|
||||
onClick={() => handleUpdateResourceProgress('pending')}
|
||||
>
|
||||
<img alt="Check" class="h-4" src={ResetIcon} />
|
||||
<span className="ml-2">Pending</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
@@ -313,10 +300,14 @@ export function TopicDetail() {
|
||||
|
||||
<p
|
||||
id="contrib-meta"
|
||||
class="mt-10 border-t pt-3 text-sm text-gray-400 leading-relaxed"
|
||||
class="mt-10 border-t pt-3 text-sm leading-relaxed text-gray-400"
|
||||
>
|
||||
Contribute links to learning resources about this topic{' '}
|
||||
<a target="_blank" class="text-blue-700 underline" href={contributionUrl}>
|
||||
<a
|
||||
target="_blank"
|
||||
class="text-blue-700 underline"
|
||||
href={contributionUrl}
|
||||
>
|
||||
on GitHub repository.
|
||||
</a>
|
||||
.
|
||||
|
@@ -1,82 +0,0 @@
|
||||
---
|
||||
import Icon from '../AstroIcon.astro';
|
||||
import Loader from '../Loader.astro';
|
||||
|
||||
export interface Props {
|
||||
contentContributionLink: string;
|
||||
}
|
||||
|
||||
const { contentContributionLink } = Astro.props;
|
||||
---
|
||||
|
||||
<div id='topic-overlay' class='hidden'>
|
||||
<div
|
||||
class='fixed right-0 top-0 z-40 h-screen w-full overflow-y-auto bg-white p-4 sm:max-w-[600px] sm:p-6'
|
||||
tabindex='-1'
|
||||
id='topic-body'
|
||||
>
|
||||
<div id='topic-loader' class='hidden'>
|
||||
<Loader />
|
||||
</div>
|
||||
|
||||
<div id='topic-actions' class='mb-2 hidden'>
|
||||
<div data-guest-required class='hidden'>
|
||||
<button
|
||||
data-popup='login-popup'
|
||||
class='inline-flex items-center rounded-md bg-green-600 p-1 px-2 text-sm text-white hover:bg-green-700'
|
||||
>
|
||||
<Icon icon='check' />
|
||||
<span class='ml-2'>Done</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div data-auth-required>
|
||||
<button
|
||||
id='mark-topic-done'
|
||||
class='inline-flex hidden items-center rounded-md bg-green-600 p-1 px-2 text-sm text-white hover:bg-green-700'
|
||||
>
|
||||
<Icon icon='check' />
|
||||
<span class='ml-2'>Done</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
id='mark-topic-pending'
|
||||
class='inline-flex hidden items-center rounded-md bg-red-600 p-1 px-2 text-sm text-white hover:bg-red-700'
|
||||
>
|
||||
<Icon icon='reset' />
|
||||
<span class='ml-2'>Pending</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type='button'
|
||||
id='close-topic'
|
||||
class='absolute right-2.5 top-2.5 inline-flex items-center rounded-lg bg-transparent p-1.5 text-sm text-gray-400 hover:bg-gray-200 hover:text-gray-900'
|
||||
>
|
||||
<Icon icon='close' />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id='topic-content'
|
||||
class='prose prose-quoteless prose-h1:mb-2.5 prose-h1:mt-7 prose-h2:mb-3 prose-h2:mt-0 prose-h3:mb-[5px] prose-h3:mt-[10px] prose-p:mb-2 prose-p:mt-0 prose-blockquote:font-normal prose-blockquote:not-italic prose-blockquote:text-gray-700 prose-li:m-0 prose-li:mb-0.5'
|
||||
>
|
||||
</div>
|
||||
|
||||
<p
|
||||
id='contrib-meta'
|
||||
class='mt-10 hidden border-t pt-3 text-sm text-gray-400'
|
||||
>
|
||||
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='text-blue-700 underline'
|
||||
href={contentContributionLink}>on GitHub repository.</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
<div class='fixed inset-0 z-30 bg-gray-900 bg-opacity-50 dark:bg-opacity-80'>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src='./topic.js'></script>
|
@@ -1,331 +0,0 @@
|
||||
export class Topic {
|
||||
constructor() {
|
||||
this.overlayId = 'topic-overlay';
|
||||
this.contentId = 'topic-content';
|
||||
this.loaderId = 'topic-loader';
|
||||
this.topicBodyId = 'topic-body';
|
||||
this.topicActionsId = 'topic-actions';
|
||||
this.markTopicDoneId = 'mark-topic-done';
|
||||
this.markTopicPendingId = 'mark-topic-pending';
|
||||
this.closeTopicId = 'close-topic';
|
||||
this.contributionTextId = 'contrib-meta';
|
||||
|
||||
this.activeResourceType = null;
|
||||
this.activeResourceId = null;
|
||||
this.activeTopicId = null;
|
||||
|
||||
this.handleRoadmapTopicClick = this.handleRoadmapTopicClick.bind(this);
|
||||
this.handleBestPracticeTopicClick =
|
||||
this.handleBestPracticeTopicClick.bind(this);
|
||||
this.handleBestPracticeTopicToggle =
|
||||
this.handleBestPracticeTopicToggle.bind(this);
|
||||
this.handleBestPracticeTopicPending =
|
||||
this.handleBestPracticeTopicPending.bind(this);
|
||||
|
||||
this.close = this.close.bind(this);
|
||||
this.resetDOM = this.resetDOM.bind(this);
|
||||
this.populate = this.populate.bind(this);
|
||||
this.handleOverlayClick = this.handleOverlayClick.bind(this);
|
||||
this.markAsDone = this.markAsDone.bind(this);
|
||||
this.markAsPending = this.markAsPending.bind(this);
|
||||
this.querySvgElementsByTopicId = this.querySvgElementsByTopicId.bind(this);
|
||||
this.isTopicDone = this.isTopicDone.bind(this);
|
||||
|
||||
this.init = this.init.bind(this);
|
||||
}
|
||||
|
||||
get loaderEl() {
|
||||
return document.getElementById(this.loaderId);
|
||||
}
|
||||
|
||||
get markTopicDoneEl() {
|
||||
return document.getElementById(this.markTopicDoneId);
|
||||
}
|
||||
|
||||
get markTopicPendingEl() {
|
||||
return document.getElementById(this.markTopicPendingId);
|
||||
}
|
||||
|
||||
get topicActionsEl() {
|
||||
return document.getElementById(this.topicActionsId);
|
||||
}
|
||||
|
||||
get contributionTextEl() {
|
||||
return document.getElementById(this.contributionTextId);
|
||||
}
|
||||
|
||||
get contentEl() {
|
||||
return document.getElementById(this.contentId);
|
||||
}
|
||||
|
||||
get overlayEl() {
|
||||
return document.getElementById(this.overlayId);
|
||||
}
|
||||
|
||||
resetDOM(hideOverlay = false) {
|
||||
if (hideOverlay) {
|
||||
this.overlayEl.classList.add('hidden');
|
||||
} else {
|
||||
this.overlayEl.classList.remove('hidden');
|
||||
}
|
||||
|
||||
this.loaderEl.classList.remove('hidden'); // Show loader
|
||||
this.topicActionsEl.classList.add('hidden'); // Hide Actions
|
||||
this.contributionTextEl.classList.add('hidden'); // Hide contribution text
|
||||
this.contentEl.replaceChildren(''); // Remove content
|
||||
}
|
||||
|
||||
close() {
|
||||
this.resetDOM(true);
|
||||
|
||||
this.activeResourceId = null;
|
||||
this.activeTopicId = null;
|
||||
}
|
||||
|
||||
isTopicDone(topicId) {
|
||||
const normalizedGroup = topicId.replace(/^\d+-/, '');
|
||||
const el = document.querySelector(`[data-group-id$="-${normalizedGroup}"]`);
|
||||
return el?.classList.contains('done');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | HTMLElement} html
|
||||
*/
|
||||
populate(html) {
|
||||
this.contentEl.replaceChildren(html);
|
||||
this.loaderEl.classList.add('hidden');
|
||||
this.topicActionsEl.classList.remove('hidden');
|
||||
this.contributionTextEl.classList.remove('hidden');
|
||||
|
||||
const isDone = this.isTopicDone(this.activeTopicId);
|
||||
|
||||
if (isDone) {
|
||||
this.markTopicDoneEl.classList.add('hidden');
|
||||
this.markTopicPendingEl.classList.remove('hidden');
|
||||
} else {
|
||||
this.markTopicDoneEl.classList.remove('hidden');
|
||||
this.markTopicPendingEl.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
renderTopicFromUrl(url) {
|
||||
return fetch(url)
|
||||
.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');
|
||||
})
|
||||
.then((content) => {
|
||||
this.populate(content);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
this.populate('Error loading the content!');
|
||||
});
|
||||
}
|
||||
|
||||
handleBestPracticeTopicToggle(e) {
|
||||
const { resourceId: bestPracticeId, topicId } = e.detail;
|
||||
if (!topicId || !bestPracticeId) {
|
||||
console.log('Missing topic or bestPracticeId: ', e.detail);
|
||||
return;
|
||||
}
|
||||
|
||||
const isDone = localStorage.getItem(topicId) === 'done';
|
||||
if (isDone) {
|
||||
this.markAsPending(topicId, bestPracticeId, 'best-practice');
|
||||
} else {
|
||||
this.markAsDone(topicId, bestPracticeId, 'best-practice');
|
||||
}
|
||||
}
|
||||
|
||||
handleBestPracticeTopicPending(e) {
|
||||
const { resourceId: bestPracticeId, topicId } = e.detail;
|
||||
if (!topicId || !bestPracticeId) {
|
||||
console.log('Missing topic or bestPracticeId: ', e.detail);
|
||||
return;
|
||||
}
|
||||
|
||||
this.markAsPending(topicId, bestPracticeId, 'best-practice');
|
||||
}
|
||||
|
||||
handleBestPracticeTopicClick(e) {
|
||||
const { resourceId: bestPracticeId, topicId } = e.detail;
|
||||
if (!topicId || !bestPracticeId) {
|
||||
console.log('Missing topic or bestPracticeId: ', e.detail);
|
||||
return;
|
||||
}
|
||||
|
||||
this.activeResourceType = 'best-practice';
|
||||
this.activeResourceId = bestPracticeId;
|
||||
this.activeTopicId = topicId;
|
||||
|
||||
this.resetDOM();
|
||||
|
||||
const topicUrl = `/best-practices/${bestPracticeId}/${topicId.replaceAll(
|
||||
':',
|
||||
'/'
|
||||
)}`;
|
||||
|
||||
this.renderTopicFromUrl(topicUrl).then(() => null);
|
||||
}
|
||||
|
||||
handleRoadmapTopicClick(e) {
|
||||
const { resourceId: roadmapId, topicId } = e.detail;
|
||||
if (!topicId || !roadmapId) {
|
||||
console.log('Missing topic or roadmap: ', e.detail);
|
||||
return;
|
||||
}
|
||||
|
||||
this.activeResourceType = 'roadmap';
|
||||
this.activeResourceId = roadmapId;
|
||||
this.activeTopicId = topicId;
|
||||
|
||||
this.resetDOM();
|
||||
const topicUrl = `/${roadmapId}/${topicId.replaceAll(':', '/')}`;
|
||||
|
||||
window.fireEvent({
|
||||
category: `RoadmapClick`,
|
||||
action: `${roadmapId}/load-topic`,
|
||||
label: topicUrl,
|
||||
});
|
||||
|
||||
this.renderTopicFromUrl(topicUrl).then(() => null);
|
||||
}
|
||||
|
||||
querySvgElementsByTopicId(topicId) {
|
||||
const matchingElements = [];
|
||||
|
||||
// Elements having sort order in the beginning of the group id
|
||||
document
|
||||
.querySelectorAll(`[data-group-id$="-${topicId}"]`)
|
||||
.forEach((element) => {
|
||||
const foundGroupId = element?.dataset?.groupId || '';
|
||||
const validGroupRegex = new RegExp(`^\\d+-${topicId}$`);
|
||||
|
||||
if (validGroupRegex.test(foundGroupId)) {
|
||||
matchingElements.push(element);
|
||||
}
|
||||
});
|
||||
|
||||
// Elements with exact match of the topic id
|
||||
document
|
||||
.querySelectorAll(`[data-group-id="${topicId}"]`)
|
||||
.forEach((element) => {
|
||||
matchingElements.push(element);
|
||||
});
|
||||
|
||||
// Matching "check:XXXX" box of the topic
|
||||
document
|
||||
.querySelectorAll(`[data-group-id="check:${topicId}"]`)
|
||||
.forEach((element) => {
|
||||
matchingElements.push(element);
|
||||
});
|
||||
|
||||
return matchingElements;
|
||||
}
|
||||
|
||||
async markAsDone(topicId, resourceId, resourceType) {
|
||||
const updatedTopicId = topicId.replace(/^\d+-/, '');
|
||||
|
||||
const { response, error } = {};
|
||||
|
||||
if (response) {
|
||||
this.close();
|
||||
this.querySvgElementsByTopicId(updatedTopicId).forEach((item) => {
|
||||
item?.classList?.add('done');
|
||||
});
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async markAsPending(topicId, resourceId, resourceType) {
|
||||
const updatedTopicId = topicId.replace(/^\d+-/, '');
|
||||
|
||||
const { response, error } = {};
|
||||
|
||||
if (response) {
|
||||
this.close();
|
||||
this.querySvgElementsByTopicId(updatedTopicId).forEach((item) => {
|
||||
item?.classList?.remove('done');
|
||||
});
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
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.activeResourceId,
|
||||
this.activeResourceType
|
||||
);
|
||||
// this.close();
|
||||
}
|
||||
|
||||
const isClickedPending =
|
||||
e.target.id === this.markTopicPendingId ||
|
||||
e.target.closest(`#${this.markTopicPendingId}`);
|
||||
if (isClickedPending) {
|
||||
this.markAsPending(
|
||||
this.activeTopicId,
|
||||
this.activeResourceId,
|
||||
this.activeResourceType
|
||||
);
|
||||
// this.close();
|
||||
}
|
||||
|
||||
const isClickedPopupOpener =
|
||||
e.target.dataset['popup'] || e.target.closest('button[data-popup]');
|
||||
const isClickedClose =
|
||||
e.target.id === this.closeTopicId ||
|
||||
e.target.closest(`#${this.closeTopicId}`);
|
||||
if (isClickedClose || isClickedPopupOpener) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
window.addEventListener(
|
||||
'best-practice.topic.click',
|
||||
this.handleBestPracticeTopicClick
|
||||
);
|
||||
window.addEventListener(
|
||||
'best-practice.topic.toggle',
|
||||
this.handleBestPracticeTopicToggle
|
||||
);
|
||||
|
||||
window.addEventListener(
|
||||
'roadmap.topic.click',
|
||||
this.handleRoadmapTopicClick
|
||||
);
|
||||
|
||||
window.addEventListener('click', this.handleOverlayClick);
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key.toLowerCase() === 'escape') {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the topic loader
|
||||
const topic = new Topic();
|
||||
topic.init();
|
Reference in New Issue
Block a user