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:
@@ -5,5 +5,9 @@ import tailwind from "@astrojs/tailwind";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
integrations: [tailwind()]
|
||||
integrations: [tailwind({
|
||||
config: {
|
||||
applyBaseStyles: false
|
||||
}
|
||||
})]
|
||||
});
|
1
public/jsons/android.json
Normal file
1
public/jsons/android.json
Normal file
File diff suppressed because one or more lines are too long
5633
public/jsons/angular.json
Normal file
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
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
14657
public/jsons/backend.json
Normal file
File diff suppressed because it is too large
Load Diff
14146
public/jsons/blockchain.json
Normal file
14146
public/jsons/blockchain.json
Normal file
File diff suppressed because it is too large
Load Diff
12186
public/jsons/computer-science.json
Normal file
12186
public/jsons/computer-science.json
Normal file
File diff suppressed because it is too large
Load Diff
8110
public/jsons/design-system.json
Normal file
8110
public/jsons/design-system.json
Normal file
File diff suppressed because it is too large
Load Diff
18138
public/jsons/devops.json
Normal file
18138
public/jsons/devops.json
Normal file
File diff suppressed because it is too large
Load Diff
12455
public/jsons/flutter.json
Normal file
12455
public/jsons/flutter.json
Normal file
File diff suppressed because it is too large
Load Diff
13713
public/jsons/frontend.json
Normal file
13713
public/jsons/frontend.json
Normal file
File diff suppressed because it is too large
Load Diff
5195
public/jsons/golang.json
Normal file
5195
public/jsons/golang.json
Normal file
File diff suppressed because it is too large
Load Diff
4270
public/jsons/java.json
Normal file
4270
public/jsons/java.json
Normal file
File diff suppressed because it is too large
Load Diff
16276
public/jsons/javascript.json
Normal file
16276
public/jsons/javascript.json
Normal file
File diff suppressed because it is too large
Load Diff
12219
public/jsons/nodejs.json
Normal file
12219
public/jsons/nodejs.json
Normal file
File diff suppressed because it is too large
Load Diff
3435
public/jsons/python.json
Normal file
3435
public/jsons/python.json
Normal file
File diff suppressed because it is too large
Load Diff
9934
public/jsons/qa.json
Normal file
9934
public/jsons/qa.json
Normal file
File diff suppressed because it is too large
Load Diff
5917
public/jsons/react.json
Normal file
5917
public/jsons/react.json
Normal file
File diff suppressed because it is too large
Load Diff
6677
public/jsons/software-architect.json
Normal file
6677
public/jsons/software-architect.json
Normal file
File diff suppressed because it is too large
Load Diff
4770
public/jsons/software-design-architecture.json
Normal file
4770
public/jsons/software-design-architecture.json
Normal file
File diff suppressed because it is too large
Load Diff
5391
public/jsons/vue.json
Normal file
5391
public/jsons/vue.json
Normal file
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>
|
||||
|
102
src/components/InteractiveRoadmap/roadmap.js
Normal file
102
src/components/InteractiveRoadmap/roadmap.js
Normal 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();
|
25
src/components/InteractiveRoadmap/sharer.js
Normal file
25
src/components/InteractiveRoadmap/sharer.js
Normal 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 });
|
||||
}
|
||||
}
|
204
src/components/InteractiveRoadmap/topic.js
Normal file
204
src/components/InteractiveRoadmap/topic.js
Normal 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
7
src/components/Loader.astro
Normal file
7
src/components/Loader.astro
Normal file
@@ -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>
|
||||
|
28
src/components/ShareIcons.astro
Normal file
28
src/components/ShareIcons.astro
Normal 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
25
src/global.css
Normal 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);
|
||||
}
|
@@ -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"
|
||||
|
Reference in New Issue
Block a user