diff --git a/frontend/components/Blocks/SectionBlock/SectionBlock.tsx b/frontend/components/Blocks/SectionBlock/SectionBlock.tsx
index 92551d2b..ae3df355 100644
--- a/frontend/components/Blocks/SectionBlock/SectionBlock.tsx
+++ b/frontend/components/Blocks/SectionBlock/SectionBlock.tsx
@@ -3,7 +3,7 @@ import Button from "@/components/Button/Button";
import { StaticImage } from "@/components/ImageSelector/types";
import InteractiveInputPopover from "@/components/InteractiveInputs/InteractiveInputPopover";
import KPIDashboard from "@/components/KPIDashboard/KPIDashboard";
-import { Graphcolor } from "@/containers/types";
+import {Graphcolor, SectionVariant} from "@/containers/types";
import { ScenarioContext } from "context/ScenarioContext";
import { debounce } from "lodash";
import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
@@ -11,7 +11,6 @@ import { getGrid } from "services/grid";
import {InteractiveElement} from "../../../api/holon";
import { WikiLinks } from "../../../containers/types";
import { HolarchyFeedbackImageProps } from "../HolarchyFeedbackImage/HolarchyFeedbackImage";
-import { Background, GridLayout } from "../types";
import ContentColumn from "./ContentColumn";
import CostBenefitModal from "./CostBenefitModal/CostBenefitModal";
import HolarchyTab from "./HolarchyTab/HolarchyTab";
@@ -28,19 +27,7 @@ import {EnlargeButton} from "@/components/Button/EnlargeButton";
import {CloseButton} from "@/components/Button/CloseButton";
type Props = {
- data: {
- type: string;
- value: {
- background: Background;
- content: Content[];
- textLabelNational: string;
- textLabelIntermediate: string;
- textLabelLocal: string;
- gridLayout: GridLayout;
- openingSection?: boolean;
- };
- id: string;
- };
+ data: SectionVariant,
pagetype?: string;
pagetitle?: string;
feedbackmodals: FeedbackModal[];
@@ -296,7 +283,14 @@ export default function SectionBlock({
}
return (
-
+
+ {/* anchor to scroll to */}
+
{feedbackmodals && (
= ({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 (
+
+
+ {num}
+
+ {title}
+
+ )
+}
diff --git a/frontend/components/Storyline/Steps/Steps.module.css b/frontend/components/Storyline/Steps/Steps.module.css
new file mode 100644
index 00000000..4a42bd18
--- /dev/null
+++ b/frontend/components/Storyline/Steps/Steps.module.css
@@ -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;
+}
diff --git a/frontend/components/Storyline/Steps/Steps.tsx b/frontend/components/Storyline/Steps/Steps.tsx
new file mode 100644
index 00000000..faca47cb
--- /dev/null
+++ b/frontend/components/Storyline/Steps/Steps.tsx
@@ -0,0 +1,67 @@
+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";
+import {SectionVariant} from "@/containers/types";
+
+type StepData = {
+ id: string,
+ title: string,
+ index: number,
+}
+
+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([])
+ useEffect(() => {
+ setSteps(stepsFromStoryLine(storyline))
+ }, [storyline])
+
+ return (
+
+ {steps.map((step) => {
+ return (
+
+ );
+ })}
+
+ );
+}
+
+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: SectionVariant): string|null|undefined => {
+ const contentItems = 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
+}
diff --git a/frontend/containers/StorylinePage/StorylinePage.module.css b/frontend/containers/StorylinePage/StorylinePage.module.css
index dc12529d..b701d012 100644
--- a/frontend/containers/StorylinePage/StorylinePage.module.css
+++ b/frontend/containers/StorylinePage/StorylinePage.module.css
@@ -1,2 +1,10 @@
.StorylinePage {
+ display: flex;
+}
+
+.Steps {
+ max-width: 16rem;
+ @media (width <= 1250px) {
+ display: none;
+ }
}
diff --git a/frontend/containers/StorylinePage/StorylinePage.tsx b/frontend/containers/StorylinePage/StorylinePage.tsx
index 2bc422ae..5ced9d6c 100644
--- a/frontend/containers/StorylinePage/StorylinePage.tsx
+++ b/frontend/containers/StorylinePage/StorylinePage.tsx
@@ -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;
+export type Storyline = PageProps;
const StorylinePage = ({
- storyline,
+ storyline = [],
scenario,
- graphcolors,
+ graphcolors = [],
wikiLinks,
title,
}: {
- storyline: Storyline[];
+ storyline?: Storyline[];
scenario: number;
- graphcolors: Graphcolor[];
+ graphcolors?: Graphcolor[];
wikiLinks: WikiLinks[];
title: string;
}) => {
return (
-
+
);
diff --git a/frontend/containers/types.ts b/frontend/containers/types.ts
index 2cb98fe0..4b9a055d 100644
--- a/frontend/containers/types.ts
+++ b/frontend/containers/types.ts
@@ -1,8 +1,9 @@
import CardBlock from "@/components/Blocks/CardsBlock";
import HeroBlock from "@/components/Blocks/HeroBlock";
import TitleBlock from "@/components/Blocks/TitleBlock";
-import Section from "@/components/Section/Section";
import TextAndMedia from "@/components/TextAndMedia";
+import {Background, GridLayout} from "@/components/Blocks/types";
+import {Content} from "@/components/Blocks/SectionBlock/types";
export type CardBlockVariant = {
type: "card_block";
@@ -14,7 +15,17 @@ export type HeroBlockVariant = {
export type SectionVariant = {
type: "section";
-} & React.ComponentProps
["data"];
+ value: {
+ background: Background;
+ content: Content[];
+ textLabelNational: string;
+ textLabelIntermediate: string;
+ textLabelLocal: string;
+ gridLayout: GridLayout;
+ openingSection?: boolean;
+ };
+ id: string;
+};
export type TextAndMediaVariant = {
type: "text_image_block";