Add rendering of SVG roadmaps

astro
Kamran Ahmed 2 years ago
parent 45a7aad669
commit 10883454f5
  1. 6
      astro.config.mjs
  2. 1
      public/jsons/android.json
  3. 5633
      public/jsons/angular.json
  4. 21412
      public/jsons/aspnet-core.json
  5. 14657
      public/jsons/backend.json
  6. 14146
      public/jsons/blockchain.json
  7. 12186
      public/jsons/computer-science.json
  8. 8110
      public/jsons/design-system.json
  9. 18138
      public/jsons/devops.json
  10. 12455
      public/jsons/flutter.json
  11. 13713
      public/jsons/frontend.json
  12. 5195
      public/jsons/golang.json
  13. 4270
      public/jsons/java.json
  14. 16276
      public/jsons/javascript.json
  15. 12219
      public/jsons/nodejs.json
  16. 3435
      public/jsons/python.json
  17. 9934
      public/jsons/qa.json
  18. 5917
      public/jsons/react.json
  19. 6677
      public/jsons/software-architect.json
  20. 4770
      public/jsons/software-design-architecture.json
  21. 5391
      public/jsons/vue.json
  22. 40
      src/components/InteractiveRoadmap/InteractiveRoadmap.astro
  23. 102
      src/components/InteractiveRoadmap/roadmap.js
  24. 25
      src/components/InteractiveRoadmap/sharer.js
  25. 204
      src/components/InteractiveRoadmap/topic.js
  26. 7
      src/components/Loader.astro
  27. 8
      src/components/ResourcesAlert.astro
  28. 8
      src/components/RoadmapHeader.astro
  29. 28
      src/components/ShareIcons.astro
  30. 25
      src/global.css
  31. 22
      src/layouts/BaseLayout.astro
  32. 20
      src/pages/[roadmapId].astro
  33. 4
      src/roadmaps/frontend/frontend.md

@ -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

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

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

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

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

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

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

File diff suppressed because it is too large Load Diff

@ -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>

@ -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();

@ -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 });
}
}

@ -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();
}
});
}
}

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

@ -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>
.

@ -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>

@ -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>

@ -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);
}

@ -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>

@ -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>

@ -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"

Loading…
Cancel
Save