1
0
mirror of https://github.com/kamranahmedse/developer-roadmap.git synced 2025-09-01 05:21:43 +02:00

Add rendering of SVG roadmaps

This commit is contained in:
Kamran Ahmed
2022-12-31 19:39:35 +04:00
parent 45a7aad669
commit 10883454f5
33 changed files with 194992 additions and 42 deletions

View File

@@ -5,5 +5,9 @@ import tailwind from "@astrojs/tailwind";
// https://astro.build/config
export default defineConfig({
integrations: [tailwind()]
integrations: [tailwind({
config: {
applyBaseStyles: false
}
})]
});

File diff suppressed because one or more lines are too long

5633
public/jsons/angular.json Normal file

File diff suppressed because it is too large Load Diff

21412
public/jsons/aspnet-core.json Normal file

File diff suppressed because it is too large Load Diff

14657
public/jsons/backend.json Normal file

File diff suppressed because it is too large Load Diff

14146
public/jsons/blockchain.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

18138
public/jsons/devops.json Normal file

File diff suppressed because it is too large Load Diff

12455
public/jsons/flutter.json Normal file

File diff suppressed because it is too large Load Diff

13713
public/jsons/frontend.json Normal file

File diff suppressed because it is too large Load Diff

5195
public/jsons/golang.json Normal file

File diff suppressed because it is too large Load Diff

4270
public/jsons/java.json Normal file

File diff suppressed because it is too large Load Diff

16276
public/jsons/javascript.json Normal file

File diff suppressed because it is too large Load Diff

12219
public/jsons/nodejs.json Normal file

File diff suppressed because it is too large Load Diff

3435
public/jsons/python.json Normal file

File diff suppressed because it is too large Load Diff

9934
public/jsons/qa.json Normal file

File diff suppressed because it is too large Load Diff

5917
public/jsons/react.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

5391
public/jsons/vue.json Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,102 @@
import { wireframeJSONToSVG } from "roadmap-renderer";
import { Topic } from "./topic";
import { Sharer } from "./sharer";
/**
* @typedef {{ roadmapId: string, jsonUrl: string }} RoadmapConfig
*/
export class Roadmap {
/**
* @param {RoadmapConfig} config
*/
constructor() {
this.roadmapId = "";
this.jsonUrl = "";
this.containerId = "roadmap-svg";
this.init = this.init.bind(this);
this.onDOMLoaded = this.onDOMLoaded.bind(this);
this.fetchRoadmapSvg = this.fetchRoadmapSvg.bind(this);
this.handleRoadmapClick = this.handleRoadmapClick.bind(this);
this.prepareConfig = this.prepareConfig.bind(this);
}
get containerEl() {
return document.getElementById(this.containerId);
}
prepareConfig() {
const dataset = this.containerEl.dataset;
this.roadmapId = dataset.roadmapId;
this.jsonUrl = dataset.jsonUrl;
}
/**
* @param { string } jsonUrl
* @returns {Promise<SVGElement>}
*/
fetchRoadmapSvg(jsonUrl) {
if (!jsonUrl) {
console.error("jsonUrl not defined in frontmatter");
return null;
}
return fetch(jsonUrl)
.then(function (res) {
return res.json();
})
.then(function (json) {
return wireframeJSONToSVG(json, {
fontURL: "/fonts/balsamiq.woff2",
});
});
}
onDOMLoaded() {
this.prepareConfig();
this.fetchRoadmapSvg(this.jsonUrl)
.then((svg) => {
document.getElementById(this.containerId).replaceChildren(svg);
})
.catch(console.error);
}
handleRoadmapClick(e) {
const targetGroup = e.target.closest("g") || {};
const groupId = targetGroup.dataset ? targetGroup.dataset.groupId : "";
if (!groupId) {
return;
}
e.stopImmediatePropagation();
window.dispatchEvent(
new CustomEvent("topic.click", {
detail: {
topicId: groupId,
roadmapId: this.roadmapId,
},
})
);
}
init() {
window.addEventListener("DOMContentLoaded", this.onDOMLoaded);
window.addEventListener("click", this.handleRoadmapClick);
}
}
const roadmap = new Roadmap();
roadmap.init();
// Initialize the topic loader
const topic = new Topic();
topic.init();
// Handles the share icons on the roadmap page
const sharer = new Sharer();
sharer.init();

View File

@@ -0,0 +1,25 @@
export class Sharer {
constructor() {
this.init = this.init.bind(this);
this.onScroll = this.onScroll.bind(this);
this.shareIconsId = "page-share-icons";
}
get shareIconsEl() {
return document.getElementById(this.shareIconsId);
}
onScroll() {
if (window.scrollY < 100 || window.innerWidth < 1050) {
this.shareIconsEl.classList.add("hidden");
return null;
}
this.shareIconsEl.classList.remove("hidden");
}
init() {
window.addEventListener("scroll", this.onScroll, { passive: true });
}
}

View File

@@ -0,0 +1,204 @@
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.activeRoadmapId = null;
this.activeTopicId = null;
this.handleTopicClick = this.handleTopicClick.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.queryRoadmapElementsByTopicId = this.queryRoadmapElementsByTopicId.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 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.contentEl.replaceChildren(''); // Remove content
}
close() {
this.resetDOM(true);
this.activeRoadmapId = null;
this.activeTopicId = null;
}
/**
* @param {string | HTMLElement} html
*/
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, '/');
const fullUrl = `/${roadmapId}/${topicPartial}/`;
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');
});
}
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+-/, '');
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();
}
});
}
}

View File

@@ -0,0 +1,7 @@
---
import Icon from "./Icon.astro";
---
<div class="flex justify-center w-full">
<Icon icon="spinner" />
</div>

View File

@@ -2,10 +2,10 @@
import Icon from "./Icon.astro";
export interface Props {
roadmapUrl: string;
roadmapPermalink: string;
}
const { roadmapUrl } = Astro.props;
const { roadmapPermalink } = Astro.props;
---
<!-- Desktop: Roadmap Resources - Alert -->
@@ -21,7 +21,7 @@ const { roadmapUrl } = Astro.props;
</p>
<a
href={`${roadmapUrl}/topics`}
href={`${roadmapPermalink}/topics`}
class="inline-flex items-center justify-center py-1.5 text-sm font-medium rounded-md hover:text-black text-gray-500 px-1"
>
<Icon icon="search" />
@@ -34,7 +34,7 @@ const { roadmapUrl } = Astro.props;
class="block sm:hidden text-sm border border-yellow-500 text-yellow-700 rounded-md py-1.5 px-2 bg-white mt-5 relative"
>
We have added resources. Try clicking roadmap nodes or visit{" "}
<a href={`${roadmapUrl}/topics`} class="text-blue-700 underline">
<a href={`${roadmapPermalink}/topics`} class="text-blue-700 underline">
resources list
</a>
.

View File

@@ -7,7 +7,7 @@ import YouTubeAlert from "./YouTubeAlert.astro";
export interface Props {
title: string;
description: string;
roadmapUrl: string;
roadmapPermalink: string;
isUpcoming?: boolean;
hasSearch?: boolean;
hasTopics?: boolean;
@@ -16,7 +16,7 @@ export interface Props {
const {
title,
description,
roadmapUrl,
roadmapPermalink,
isUpcoming = false,
hasSearch = false,
hasTopics = true,
@@ -71,7 +71,7 @@ const isRoadmapReady = !isUpcoming;
{
hasSearch && (
<a
href={roadmapUrl}
href={roadmapPermalink}
class="bg-gray-500 py-1.5 px-3 rounded-md text-white text-xs sm:text-sm font-medium hover:bg-gray-600"
aria-label="Back to Visual Roadmap"
>
@@ -99,7 +99,7 @@ const isRoadmapReady = !isUpcoming;
</div>
<!-- Desktop: Roadmap Resources - Alert -->
{hasTopics && <ResourcesAlert roadmapUrl={roadmapUrl} />}
{hasTopics && <ResourcesAlert roadmapPermalink={roadmapPermalink} />}
{hasSearch && <TopicSearch />}
</div>

View File

@@ -0,0 +1,28 @@
---
import Icon from "./Icon.astro";
export interface Props {
pageUrl: string;
description: string;
}
const { pageUrl, description } = Astro.props;
const twitterUrl = `https://twitter.com/intent/tweet?text=${description}&url=${pageUrl}`;
const fbUrl = `https://www.facebook.com/sharer/sharer.php?quote=${description}&u=${pageUrl}`;
const hnUrl = `https://news.ycombinator.com/submitlink?t=${description}&u=${pageUrl}`;
const redditUrl = `https://www.reddit.com/submit?title=${description}&url=${pageUrl}`;
---
<a href={twitterUrl} target="_blank" class="text-gray-500 hover:text-gray-700">
<Icon icon="twitter" />
</a>
<a href={fbUrl} target="_blank" class="text-gray-500 hover:text-gray-700">
<Icon icon="facebook" />
</a>
<a href={hnUrl} target="_blank" class="text-gray-500 hover:text-gray-700">
<Icon icon="hackernews" />
</a>
<a href={redditUrl} target="_blank" class="text-gray-500 hover:text-gray-700">
<Icon icon="reddit" />
</a>

25
src/global.css Normal file
View File

@@ -0,0 +1,25 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.container {
@apply max-w-[830px] px-4 mx-auto;
}
}
.bg-stripes {
background-image: linear-gradient(45deg, var(--stripes-color) 12.5%, transparent 12.5%, transparent 50%, var(--stripes-color) 50%, var(--stripes-color) 62.5%, transparent 62.5%, transparent 100%);
background-size: 5.66px 5.66px
}
.sponsor-footer {
text-align: center;
font-weight: 600;
font-size: 9px;
letter-spacing: 0.5px;
text-transform: uppercase;
padding: 3px 10px;
display: block;
background: repeating-linear-gradient(-45deg, transparent, transparent 5px, hsla(0, 0%, 0%, .025) 5px, hsla(0, 0%, 0%, .025) 10px) hsla(203, 11%, 95%, .4);
}

View File

@@ -1,4 +1,5 @@
---
import "../global.css";
import Navigation from '../components/Navigation.astro';
export interface Props {
@@ -26,24 +27,3 @@ const { title } = Astro.props;
<slot name="after-footer"/>
</body>
</html>
<style is:global>
.container {
@apply max-w-[830px] px-4 mx-auto;
}
.bg-stripes {
background-image: linear-gradient(45deg, var(--stripes-color) 12.5%, transparent 12.5%, transparent 50%, var(--stripes-color) 50%, var(--stripes-color) 62.5%, transparent 62.5%, transparent 100%);
background-size: 5.66px 5.66px
}
.sponsor-footer {
text-align: center;
font-weight: 600;
font-size: 9px;
letter-spacing: 0.5px;
text-transform: uppercase;
padding: 3px 10px;
display: block;
background: repeating-linear-gradient(-45deg, transparent, transparent 5px, hsla(0, 0%, 0%, .025) 5px, hsla(0, 0%, 0%, .025) 10px) hsla(203, 11%, 95%, .4);
}
</style>

View File

@@ -12,7 +12,11 @@ export async function getStaticPaths() {
}));
}
const { roadmapId } = Astro.params;
interface Params extends Record<string, string | undefined> {
roadmapId: string;
}
const { roadmapId } = Astro.params as Params;
const file = await import(`../roadmaps/${roadmapId}/${roadmapId}.md`);
const frontmatter = file.frontmatter as RoadmapFrontmatter;
---
@@ -21,10 +25,20 @@ const frontmatter = file.frontmatter as RoadmapFrontmatter;
<RoadmapHeader
description={frontmatter.description}
title={frontmatter.title}
roadmapUrl={`/${roadmapId}`}
roadmapPermalink={`/${roadmapId}`}
/>
{frontmatter.jsonUrl && <InteractiveRoadamp jsonUrl={frontmatter.jsonUrl} />}
{
frontmatter.jsonUrl && (
<InteractiveRoadamp
roadmapId={roadmapId}
description={frontmatter.description}
roadmapPermalink={`/${roadmapId}`}
jsonUrl={frontmatter.jsonUrl}
dimensions={frontmatter.dimensions}
/>
)
}
<file.Content />
</BaseLayout>

View File

@@ -1,6 +1,6 @@
---
jsonUrl: "/assets/jsons/frontend.json"
pdfUrl: "/assets/pdfs/frontend.pdf"
jsonUrl: "/jsons/frontend.json"
pdfUrl: "/pdfs/frontend.pdf"
order: 1
featuredTitle: "Frontend"
featuredDescription: "Step by step guide to becoming a frontend developer in 2022"