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