Skip to content

Commit 00af9c4

Browse files
authored
Showing tutorials dynamically in the curriculum (#469)
1 parent 858076c commit 00af9c4

File tree

10 files changed

+224
-47
lines changed

10 files changed

+224
-47
lines changed

assets/js/react/components/App.jsx

+13-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React from "react";
22
import {useEffect, useState} from "react";
33
import Spinner from "react-bootstrap/Spinner";
44

5-
export const App = ({component, fetchData}) => {
5+
export const App = ({component, fetchData, container = null}) => {
66

77
const [fetchingData, setFetchingData] = useState({
88
loading: true,
@@ -13,7 +13,7 @@ export const App = ({component, fetchData}) => {
1313
useEffect(() => {
1414
const fetchAsync = async () => {
1515
try {
16-
const data = await fetchData()
16+
const data = await fetchData(container)
1717

1818
setFetchingData({
1919
loading: false,
@@ -29,7 +29,6 @@ export const App = ({component, fetchData}) => {
2929
}
3030
}
3131

32-
3332
if (fetchData) {
3433
fetchAsync()
3534
} else {
@@ -41,14 +40,22 @@ export const App = ({component, fetchData}) => {
4140
}
4241
}, [])
4342

43+
let renderComponent = <div>Error rendering component</div>
44+
4445
if (fetchingData.loading) {
45-
return <Spinner animation="border" />
46+
renderComponent = <Spinner animation="border" />
4647
}
4748

4849
if (fetchingData.data) {
4950
const Component = component
50-
return <Component {...fetchingData.data} />
51+
renderComponent = <Component {...fetchingData.data} />
5152
}
5253

53-
return <div>Error</div>
54+
return <div>
55+
{renderComponent}
56+
</div>
57+
}
58+
59+
export const buildAppComponent = (component, fetchData, container) => {
60+
return <App component={component} container={container} fetchData={fetchData} />
5461
}

assets/js/react/components/curriculum/page/PageNavigation.jsx

+26-17
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import React, {useEffect, useState} from "react"
22
import ReactDOM from "react-dom/client";
33
import {useContentLevel, useCurrentPageId, useSidebarMenuElements} from "../../../services/hooks";
44
import Spinner from 'react-bootstrap/Spinner';
5+
import {getDataAttribute, getDataAttributeFromContainer, renderComponent} from "../../../services/util";
6+
import {TutorialTrigger} from "../tutorial/TutorialTrigger";
57

68
const PageNavigation = () => {
79

@@ -44,27 +46,34 @@ const PageNavigation = () => {
4446
return link.href
4547
}
4648

49+
const composeButton = previous => {
50+
const title = getDataAttributeFromContainer(previous, "title")
51+
52+
const button = <div className="card my-1">
53+
<div className="card-body py-2">
54+
{title}
55+
</div>
56+
</div>
57+
58+
if (getDataAttributeFromContainer(previous, "category") === "tutorial") {
59+
const id = getDataAttributeFromContainer(previous, "id")
60+
const content = document.getElementById("content-curriculum-tutorial-trigger-" + id).innerHTML
61+
62+
return <a href="#"><TutorialTrigger title={title} content={content} button={button} /></a>
63+
}
64+
65+
return <a href={getHref(previous)}>
66+
{button}
67+
</a>
68+
}
69+
4770
return <>
4871
{(previous === null && next === null) && <Spinner animation="border" role="status" />}
4972
{(previous !== null || next !== null) && <div className="docs-navigation d-flex justify-content-between">
50-
{previous !== null && <a href={getHref(previous)}>
51-
<div className="card my-1">
52-
<div className="card-body py-2">
53-
{previous.textContent}
54-
</div>
55-
</div>
56-
</a>}
57-
{next !== null && <a className="ms-auto" href={getHref(next)}>
58-
<div className="card my-1">
59-
<div className="card-body py-2">
60-
{next.textContent}
61-
</div>
62-
</div>
63-
</a>}
73+
{previous !== null && composeButton(previous)}
74+
{next !== null && composeButton(next)}
6475
</div>}
6576
</>
6677
}
6778

68-
const container = document.getElementById('page-navigation');
69-
const root = ReactDOM.createRoot(container);
70-
root.render(<PageNavigation />)
79+
renderComponent(<PageNavigation />, "page-navigation")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
.dynamic-tutorial {
2+
width: 100%;
3+
height: 100%;
4+
z-index: 10000
5+
}
6+
7+
.dynamic-tutorial .notice {
8+
display: flex;
9+
align-items: center;
10+
background-color: #f5f5f5;
11+
color: black;
12+
padding: 12px;
13+
border: 1px solid #E8E8E8FF;
14+
border-radius: 4px 4px 4px 4px;
15+
margin-top: 20px;
16+
}
17+
18+
[data-dark-mode] .dynamic-tutorial .notice {
19+
background-color: #3d3d3d;
20+
border: 1px solid #252525;
21+
color: white;
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import {hideElement, renderComponent} from "../../../services/util";
2+
import {App} from "../../App";
3+
import React, {useEffect, useState} from "react";
4+
5+
const Tutorial = () => {
6+
7+
const [tutorialData, setTutorialData] = useState(null)
8+
9+
const showDynamicTutorial = e => {
10+
hideElement("page-content")
11+
const data = e.detail
12+
13+
setTutorialData(data)
14+
}
15+
16+
useEffect(() => {
17+
return window.addEventListener('showDynamicTutorial', showDynamicTutorial)
18+
}, [])
19+
20+
return <>
21+
{tutorialData !== null && <div className="dynamic-tutorial">
22+
<div className="notice">
23+
<img src="/icons/tutorial-book.png" width="35" height="35" />
24+
<span style={{marginLeft: '10px', fontSize: '19px'}}>This tutorial can also be found in the <a href="/tutorials" target="__blank">Launchpad Tutorials</a> section.</span>
25+
</div>
26+
<h1>{tutorialData.title}</h1>
27+
<div dangerouslySetInnerHTML={{__html: tutorialData.content}}></div>
28+
</div>}
29+
</>
30+
}
31+
32+
33+
renderComponent(<App component={Tutorial} fetchData={undefined} />, "dynamic-tutorial")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import {getDataAttributeFromContainer, renderComponents} from "../../../services/util";
2+
import React, {useEffect, useState} from "react";
3+
import {TutorialTriggerButton} from "./TutorialTriggerButton";
4+
5+
export const TutorialTrigger = ({title, content, button = undefined}) => {
6+
7+
const [activeTrigger, setActiveTrigger] = useState(false)
8+
9+
useEffect(() => {
10+
return window.addEventListener('showDynamicTutorial', e => {
11+
const data = e.detail
12+
13+
if (data.title === title) {
14+
setActiveTrigger(true)
15+
} else {
16+
setActiveTrigger(false)
17+
}
18+
})
19+
})
20+
21+
const trigger = () => {
22+
const activeElements = document.getElementsByClassName("docs-link rounded active");
23+
24+
for (let i = 0; i < activeElements.length; i++) {
25+
const activeElement = activeElements[i]
26+
activeElement.classList.toggle('active')
27+
}
28+
29+
window.dispatchEvent(new CustomEvent('showDynamicTutorial', {detail: {title, content}}))
30+
}
31+
32+
const onClickWrapper = button => {
33+
return <div onClick={() => {trigger(); trigger()}}>
34+
{button}
35+
</div>
36+
}
37+
38+
return onClickWrapper(button === undefined ? <TutorialTriggerButton title={title} active={activeTrigger} /> : button)
39+
}
40+
41+
const fetchData = async (container) => {
42+
const parent = container.parentNode
43+
const tutorialId = container.id.split('-')[3]
44+
45+
const tutorialContent = document.getElementById('content-curriculum-tutorial-trigger-' + tutorialId).innerHTML
46+
47+
const title = getDataAttributeFromContainer(parent, "title")
48+
const content = tutorialContent
49+
50+
return {
51+
title,
52+
content
53+
}
54+
}
55+
56+
renderComponents(TutorialTrigger, fetchData, "curriculum-tutorial-trigger")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import React from "react";
2+
3+
export const TutorialTriggerButton = ({title, active}) => {
4+
5+
const className = active ? "docs-link rounded active" : "docs-link rounded"
6+
7+
return <div style={{display: 'flex', marginTop: '4px', alignItems: 'center'}}>
8+
<img src="/icons/tutorial.png" width="20" height="20" />
9+
<div className={className} style={{marginLeft: '6px', cursor: 'pointer'}}> {title}</div>
10+
</div>
11+
}

assets/js/react/services/util.js

+20
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import ReactDOM from "react-dom/client";
22
import React from "react";
3+
import {App, buildAppComponent} from "../components/App";
34

45
export const LEVELS = ['shallow', 'deep']
56

@@ -8,13 +9,32 @@ export const getDataAttribute = (id, attribute) => {
89
return element.dataset[attribute]
910
}
1011

12+
export const hideElement = id => {
13+
const content = document.getElementById(id)
14+
content.style.display = "none"
15+
}
16+
17+
export const getDataAttributeFromContainer = (container, attribute) => {
18+
return container.dataset[attribute]
19+
}
20+
1121
export const renderComponent = (component, containerId) => {
1222
const container = document.getElementById(containerId);
1323

1424
const root = ReactDOM.createRoot(container);
1525
root.render(component)
1626
}
1727

28+
export const renderComponents = (component, fetchData, containerIdPrefix) => {
29+
const containers = document.querySelectorAll('[id^="' + containerIdPrefix + '"]');
30+
31+
for (let i = 0; i < containers.length; i++) {
32+
const container = containers[i]
33+
const root = ReactDOM.createRoot(container);
34+
root.render(buildAppComponent(component, fetchData, container))
35+
}
36+
}
37+
1838
export const parseList = (listAsString) => {
1939
if (!listAsString) {
2040
return []

layouts/curriculum/single.html

+31-21
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22
{{ $section := replaceRE `(.+?)\/(.+?)\/(.+?)$` "$2" .File.Path }}
33
{{ $goal := resources.Get "js/react/components/curriculum/page/Goal.jsx" | babel }}
44
{{ $goal := $goal | js.Build }}
5+
6+
{{ $tutorial := resources.Get "js/react/components/curriculum/tutorial/Tutorial.jsx" | babel }}
7+
{{ $tutorial := $tutorial | js.Build }}
8+
59
<script src="{{ $goal.RelPermalink }}" defer></script>
10+
<script src="{{ $tutorial.RelPermalink }}" defer></script>
11+
612
<div class="row flex-xl-nowrap">
713
<div class="col-lg-5 col-xl-4 docs-sidebar d-none d-lg-block">
814
<nav class="docs-links" aria-label="Main navigation">
@@ -14,7 +20,8 @@
1420
{{ partial "sidebar/docs-toc.html" . }}
1521
</nav>
1622
{{ end -}}
17-
{{ if .Params.toc -}}
23+
24+
{{ if .Params.toc -}}
1825
<main class="docs-content col-lg-11 col-xl-9">
1926
{{ else -}}
2027
<main class="docs-content col-lg-11 col-xl-9 mx-xl-auto"
@@ -36,28 +43,31 @@
3643
</ol>
3744
</nav>
3845
{{ end }}
39-
<h1>{{ .Title }}</h1>
40-
<p class="lead">{{ .Params.lead | safeHTML }}</p>
41-
{{ if ne .Params.toc false -}}
42-
<nav class="d-xl-none" aria-label="Quaternary navigation">
43-
{{ partial "sidebar/docs-toc.html" . }}
44-
</nav>
45-
{{ end -}}
46+
<div id="dynamic-tutorial"></div>
47+
<div id="page-content">
48+
<h1>{{ .Title }}</h1>
49+
<p class="lead">{{ .Params.lead | safeHTML }}</p>
50+
{{ if ne .Params.toc false -}}
51+
<nav class="d-xl-none" aria-label="Quaternary navigation">
52+
{{ partial "sidebar/docs-toc.html" . }}
53+
</nav>
54+
{{ end -}}
4655

47-
<div id="goal"></div>
56+
<div id="goal"></div>
4857

49-
{{ .Content }}
50-
{{ if .Site.Params.editPage -}}
51-
{{ partial "main/edit-page.html" . }}
52-
{{ end -}}
53-
{{ partial "main/docs-navigation.html" . }}
54-
<!--
55-
{{ if not .Site.Params.options.collapsibleSidebar -}}
56-
{{ partial "main/docs-navigation.html" . }}
57-
{{ else -}}
58-
<div class="my-n3"></div>
59-
{{ end -}}
60-
-->
58+
{{ .Content }}
59+
{{ if .Site.Params.editPage -}}
60+
{{ partial "main/edit-page.html" . }}
61+
{{ end -}}
62+
{{ partial "main/docs-navigation.html" . }}
63+
<!--
64+
{{ if not .Site.Params.options.collapsibleSidebar -}}
65+
{{ partial "main/docs-navigation.html" . }}
66+
{{ else -}}
67+
<div class="my-n3"></div>
68+
{{ end -}}
69+
-->
70+
</div>
6171
</main>
6272
</div>
6373
{{ end }}

layouts/partials/sidebar/curriculum-menu.html

+12-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
{{ $tutorialTrigger := resources.Get "js/react/components/curriculum/tutorial/TutorialTrigger.jsx" | babel }}
2+
{{ $tutorialTrigger := $tutorialTrigger | js.Build }}
3+
4+
<script src="{{ $tutorialTrigger.RelPermalink }}" defer></script>
5+
16
{{ if .Site.Params.options.collapsibleSidebar -}}
27
<ul class="list-unstyled collapsible-sidebar">
38
{{ $currentPage := . -}}
@@ -14,13 +19,17 @@
1419
{{ range .Children -}}
1520
{{- $active := or ($currentPage.IsMenuCurrent "curriculum" .) ($currentPage.HasMenuCurrent "curriculum" .) -}}
1621
{{- $active = or $active (eq $currentPage.Section .Identifier) -}}
17-
<li style="display: flex; align-items: center;" data-content-level="{{ .Page.Params.level }}" data-id="{{ .Page.File.UniqueID }}">
22+
{{ $test := ("aa *bold*" | markdownify) }}
23+
<li style="display: flex; align-items: center;" data-category="{{ .Page.Params.Category }}" data-content-level="{{ .Page.Params.level }}" data-id="{{ .Page.File.UniqueID }}" data-title="{{ .Page.Title }}" data-content="{{ $test }}" >
24+
{{ if eq .Page.Params.category "tutorial" }}
25+
<div id="content-curriculum-tutorial-trigger-{{ .Page.File.UniqueID }}" style="display: none;">{{ .Page.Content | markdownify }}</div>
26+
{{ end }}
1827
{{ if eq .Page.Params.category "lecture" }}
1928
<img src="/icons/lecture.png" width="20" height="20" />
29+
<a class="docs-link rounded{{ if $active }} active{{ end }}" href="{{ .URL | relURL }}" style="margin-left: 0.4rem" target='{{ if eq .Page.Params.category "tutorial" }}_blank{{ end }}'>{{ .Name }} {{ if eq .Page.Params.category "tutorial" }}(Tutorial){{ end }}</a>
2030
{{ else }}
21-
<img src="/icons/tutorial.png" width="20" height="20" />
31+
<div id="curriculum-tutorial-trigger-{{ .Page.File.UniqueID }}"></div>
2232
{{ end }}
23-
<a class="docs-link rounded{{ if $active }} active{{ end }}" href="{{ .URL | relURL }}" style="margin-left: 0.4rem" target='{{ if eq .Page.Params.category "tutorial" }}_blank{{ end }}'>{{ .Name }} {{ if eq .Page.Params.category "tutorial" }}(Tutorial){{ end }}</a>
2433
</li>
2534
{{ end -}}
2635
</ul>

static/icons/tutorial-book.png

6.83 KB
Loading

0 commit comments

Comments
 (0)