Skip to content

Commit

Permalink
Step indicator in Storyline
Browse files Browse the repository at this point in the history
Issue #974
  • Loading branch information
Erik van Velzen committed Jun 10, 2024
1 parent a6dc2be commit 2c61f44
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 14 deletions.
9 changes: 8 additions & 1 deletion frontend/components/Blocks/SectionBlock/SectionBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,14 @@ export default function SectionBlock({
}

return (
<div className={`sectionContainer`} ref={sectionContainerRef} id={data.id}>
<div className={`sectionContainer`} ref={sectionContainerRef}>
{/* anchor to scroll to */}
<span id={data.id} style={{
display: "block",
position: "relative",
top: "-5.75rem", // compensate for fixed site header
visibility: "hidden",
}} />
{feedbackmodals && (
<ChallengeFeedbackModal
feedbackmodals={feedbackmodals}
Expand Down
10 changes: 10 additions & 0 deletions frontend/components/Storyline/Steps/Step.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

.Step:hover .StepNumber {
color: white;
@apply bg-holon-blue-500;
}

.active {
@apply bg-holon-slated-blue-900;
color: white;
}
56 changes: 56 additions & 0 deletions frontend/components/Storyline/Steps/Step.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {FunctionComponent, useEffect, useState} from "react";
import styles from "./Step.module.css";

export type StepProps = {
id: string,
num: number,
title: string,
}

export const Step: FunctionComponent<StepProps> = ({id, num, title}) => {
const [active, setActive] = useState(false);

useEffect(() => {
const handleScroll = () => {
const target = document.getElementById(id)
if (!target) {
console.error(`Target with id ${id} not in DOM`)
return
}
const rect = target.parentElement.getBoundingClientRect()
const visibleTop = Math.max(0, rect.top);
const visibleBottom = Math.min(window.visualViewport.height, rect.bottom);
const visibleHeight = Math.max(0, visibleBottom - visibleTop);

if (visibleHeight > (window.visualViewport.height / 2)) {
setActive(true)
} else {
setActive(false)
}
}
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);

return (
<a href={`#${id}`} style={{
textAlign: "center",
display: "flex",
flexDirection: "column",
alignItems: "center",
}} className={styles.Step}>
<div
className={`${styles.StepNumber} ${active && styles.active} bg-white border-holon-blue-900 border-2 rounded`}
style={{
padding: "0.2rem 2rem",
fontSize: "1.2rem",
fontWeight: "bold",
}}>
{num}
</div>
<div>{title}</div>
</a>
)
}
17 changes: 17 additions & 0 deletions frontend/components/Storyline/Steps/Steps.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@

.Steps {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 1rem;

min-width: 4rem;
position: sticky;

top: 5.75rem;
height: calc(100vh - 5.75rem);

padding: 1rem;
z-index: 20;
}
60 changes: 60 additions & 0 deletions frontend/components/Storyline/Steps/Steps.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {FunctionComponent, useEffect, useState} from "react";
import {Storyline} from "@/containers/StorylinePage/StorylinePage";
import {Step} from "@/components/Storyline/Steps/Step";
import styles from "./Steps.module.css";

export const Steps: FunctionComponent<{storyline: Storyline[], className?: string}> = ({storyline, className = ""}) => {
// useState + useEffect is to force client side rendering
// because it uses DOMParser to grep for a title
// which is not available in Node.js.
const [steps, setSteps] = useState<any[]>([])
useEffect(() => {
setSteps(stepsFromStoryLine(storyline))
}, [storyline])

return (
<div className={`${styles.Steps} ${className}`}>
{steps.map((step) => {
return (
<Step
key={step.id}
id={step.id}
num={step.index + 1}
title={step.title}
/>
);
})}
</div>
);
}

const stepsFromStoryLine = (storyline: Storyline[]) => {
return storyline
.filter((section) => section.type === "section")
.map((section, index) => {
const title = searchTitle(section) || `Stap ${index + 1}`

return {
id: section.id,
title: title,
index: index,
}
})
}

const searchTitle = (section: any): string|null|undefined => {
const contentItems: any[] = section.value.content
const textItem = contentItems.find((contentItem) => contentItem.type === "text")

if (!textItem) {
return null
}

if (typeof DOMParser === "undefined") {
return null
}
const parser = new DOMParser();
const htmlDoc = parser.parseFromString(textItem.value, "text/html");

return htmlDoc.querySelector("h1, h2, h3, h4, h5, h6")?.textContent
}
8 changes: 8 additions & 0 deletions frontend/containers/StorylinePage/StorylinePage.module.css
Original file line number Diff line number Diff line change
@@ -1,2 +1,10 @@
.StorylinePage {
display: flex;
}

.Steps {
max-width: 16rem;
@media (width <= 1250px) {
display: none;
}
}
32 changes: 19 additions & 13 deletions frontend/containers/StorylinePage/StorylinePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,38 @@ import styles from "./StorylinePage.module.css";
import ContentBlocks from "@/components/Blocks/ContentBlocks";
import { ScenarioContext } from "context/ScenarioContext";
import { Graphcolor, PageProps, SectionVariant, TextAndMediaVariant, WikiLinks } from "../types";
import {Steps} from "@/components/Storyline/Steps/Steps";

type Storyline = PageProps<SectionVariant | TextAndMediaVariant>;
export type Storyline = PageProps<SectionVariant | TextAndMediaVariant>;

const StorylinePage = ({
storyline,
storyline = [],
scenario,
graphcolors,
graphcolors = [],
wikiLinks,
title,
}: {
storyline: Storyline[];
storyline?: Storyline[];
scenario: number;
graphcolors: Graphcolor[];
graphcolors?: Graphcolor[];
wikiLinks: WikiLinks[];
title: string;
}) => {
return (
<div className={styles["StorylinePage"]}>
<div className={styles.StorylinePage}>
<ScenarioContext.Provider value={scenario}>
<ContentBlocks
content={storyline}
pagetitle={title}
wikilinks={wikiLinks}
graphcolors={graphcolors ?? []}
pagetype={"Storyline"}
/>
<div className={styles.Steps}>
<Steps storyline={storyline} />
</div>
<div>
<ContentBlocks
content={storyline}
pagetitle={title}
wikilinks={wikiLinks}
graphcolors={graphcolors ?? []}
pagetype={"Storyline"}
/>
</div>
</ScenarioContext.Provider>
</div>
);
Expand Down

0 comments on commit 2c61f44

Please sign in to comment.