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 12, 2024
1 parent a62564d commit d53feeb
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 31 deletions.
26 changes: 10 additions & 16 deletions frontend/components/Blocks/SectionBlock/SectionBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@ 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";
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";
Expand All @@ -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[];
Expand Down Expand Up @@ -296,7 +283,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;
}
67 changes: 67 additions & 0 deletions frontend/components/Storyline/Steps/Steps.tsx
Original file line number Diff line number Diff line change
@@ -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<StepData[]>([])
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: 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
}
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
15 changes: 13 additions & 2 deletions frontend/containers/types.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -14,7 +15,17 @@ export type HeroBlockVariant = {

export type SectionVariant = {
type: "section";
} & React.ComponentProps<typeof Section>["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";
Expand Down

0 comments on commit d53feeb

Please sign in to comment.