components/SlidesOverlay/SlidesOverlay.jsxjavascript
import { useState } from "react";
import { UIMCButton } from "@ui-marcom/react/button";
import { UIMCToggleButtonSwitch } from "@ui-marcom/react/toggle-button-switch";
import { useContent } from "../../services/content";
import "./SlidesOverlay.scss";
const ALL_TAB_ID = "all";
const SlideThumb = ({ id, view, isCurrent, onSelect }) => {
const blocks = view.elements || [];
const label =
blocks.find((block) => block.type === "headline")?.text ||
blocks.find((block) => block.type === "storyTitle")?.title ||
id;
return (
<button
type="button"
className={`slides-overlay__thumb${isCurrent ? " slides-overlay__thumb--current" : ""}`}
aria-current={isCurrent || undefined}
onClick={() => onSelect(id)}>
{view.thumbnail ? (
<img
className="slides-overlay__thumb-image"
src={view.thumbnail}
alt={label}
loading="lazy"
decoding="async"
width="270"
height="480"
/>
) : (
<span className="slides-overlay__thumb-label">{label}</span>
)}
</button>
);
};
/**
* Full-screen overlay listing every slide of the loaded content file as a
* clickable thumbnail (from the view's optional `thumbnail` field, with a
* text placeholder as fallback), grouped by story. A toggle switch selects
* "All Slides" or a single story; it opens on the current view's story.
* Rendered by SlideNavigation in its "slides" mode.
*
* @param {Object} props
* @param {string} props.currentViewId - View to highlight and to derive the initial story tab from.
* @param {function(string): void} props.onSelectSlide - Called with the clicked slide's view id.
*/
export const SlidesOverlay = ({ currentViewId, onSelectSlide }) => {
const { views, stories } = useContent();
const storyIds = Object.keys(stories);
const [activeTabId, setActiveTabId] = useState(
() => views.get(currentViewId)?.story || ALL_TAB_ID
);
// a story's views in authoring order: root slides and their sub views all
// carry an explicit `story` field
const getStoryViewIds = (storyId) =>
[...views.entries()].filter(([, view]) => view.story === storyId).map(([viewId]) => viewId);
const tabs = [
{ id: ALL_TAB_ID, label: "All Slides" },
...storyIds.map((id) => ({ id, label: stories[id].name })),
];
const activeTabIndex = Math.max(
tabs.findIndex(({ id }) => id === activeTabId),
0
);
const renderGrid = (storyId) => (
<div className="slides-overlay__grid">
{getStoryViewIds(storyId).map((id) => (
<SlideThumb
key={id}
id={id}
view={views.get(id)}
isCurrent={id === currentViewId}
onSelect={onSelectSlide}
/>
))}
</div>
);
const renderStorySwitch = () => (
<div className="slides-overlay__switch">
<UIMCToggleButtonSwitch
size="small"
current={activeTabIndex}
onChange={(index) => setActiveTabId(tabs[index].id)}>
{tabs.map(({ id, label }) => (
<UIMCButton key={id}>{label}</UIMCButton>
))}
</UIMCToggleButtonSwitch>
</div>
);
// the nav bar (incl. the story switch, via renderBar) is a sibling of the
// backdrop, not a child: the backdrop's backdrop-filter creates a stacking
// context, so children could never render above the page's other fixed UI
return (
<>
<div className="slides-overlay" role="dialog" aria-modal="true" aria-label="Slides">
<div className="slides-overlay__content">
{activeTabId === ALL_TAB_ID
? storyIds.map((storyId) => (
<section key={storyId} className="slides-overlay__story-section">
<h3 className="slides-overlay__story-name">{stories[storyId].name}</h3>
{renderGrid(storyId)}
</section>
))
: renderGrid(activeTabId)}
{renderStorySwitch()}
</div>
</div>
</>
);
};