components/SlidePage/SlidePage.jsxjavascript
import React from "react";
import { useParams, useNavigate, useLocation, Navigate } from "react-router";
import { useContent } from "../../services/content";
import { navigateWithTransition } from "../../utils/viewTransition";
import { ComponentRenderer } from "../ComponentRenderer";
import { SlideNavigation } from "../SlideNavigation/SlideNavigation";
import { getBlockRegion } from "../elements/registry";

/**
 * Shared skeleton of the slide-based presentation pages (ValueAdvisoryServices,
 * RadEnablement): viewId routing, slide navigation, element-bucket layout and
 * the content error/redirect states. The pages themselves only supply their
 * constants, their CSS (via `classPrefix`) and an optional header.
 *
 * @param {string} basePath - route prefix, e.g. "/rad-enablement"
 * @param {string} startViewId - view shown at the bare route, e.g. "rad-intro"
 * @param {string} storyHubViewId - hub view (see getAdjacentRootViews), also the menu's Home target
 * @param {string} classPrefix - CSS class prefix, e.g. "rad" -> "rad-page", "rad-slide-content"
 * @param {function(string): *} [renderHeader] - optional per-page header, returning a React node
 */
export const SlidePage = ({ basePath, startViewId, storyHubViewId, classPrefix, renderHeader }) => {
  const { viewId = startViewId } = useParams();
  const navigate = useNavigate();
  const { search } = useLocation();
  const { views, error, getView, getAdjacentRootViews } = useContent();

  // the story hub's selectWidget picks the target view (its options' ids are
  // view ids, e.g. "cancer-1") rather than following a fixed nextView; the
  // choice only arms the "next" button, it doesn't navigate by itself, so
  // it's reset whenever the current slide changes
  const [selectedTargetViewId, setSelectedTargetViewId] = React.useState(null);
  React.useEffect(() => {
    setSelectedTargetViewId(null);
  }, [viewId]);

  // keeps the current URL params (kiosk/device config, see config-schema.js -
  // useConfig re-reads them from the URL on every render) alive across slide
  // navigation, and cross-fades the content
  const navigateToView = React.useCallback(
    (targetViewId) => {
      navigateWithTransition(navigate, `${basePath}/${targetViewId}${search}`);
    },
    [navigate, search, basePath]
  );

  // visible failure state instead of a blank kiosk screen; ContentProvider
  // keeps retrying in the background (see services/content.jsx)
  if (error) {
    return (
      <div className={`${classPrefix}-page page content-error`}>
        <p>Inhalte konnten nicht geladen werden.</p>
        <p>Es wird automatisch ein neuer Versuch gestartet …</p>
      </div>
    );
  }

  const view = getView(viewId);

  // a typo'd/stale view id would otherwise render an empty page without any
  // navigation (SlideNavigation hides itself when adjacency is empty)
  if (views.size > 0 && !view) {
    return <Navigate to={`${basePath}/${startViewId}${search}`} replace />;
  }

  const blocks = view?.elements || [];
  const { prevId, nextId } = getAdjacentRootViews(viewId, { storyHubViewId });
  const effectiveNextId = selectedTargetViewId || nextId;

  // each layout region gets its own wrapper below; a block's region comes
  // from its element's registry default, overridable per block in the JSON
  const blocksIn = (region) => blocks.filter((block) => getBlockRegion(block) === region);
  const slideStoryTitleBlocks = blocksIn("storyTitle");
  const slideHeadlineBlocks = blocksIn("headline");
  const contentBlocks = blocksIn("content");

  const renderElement = (block, index) => (
    <ComponentRenderer
      key={`${block.type}-${index}`}
      block={block}
      selectedTargetViewId={selectedTargetViewId}
      onSelectTarget={setSelectedTargetViewId}
      onNavigate={navigateToView}
    />
  );

  return (
    <div className={`${classPrefix}-page page`}>
      {renderHeader?.(viewId)}
      {slideStoryTitleBlocks.length > 0 && (
        <div className={`${classPrefix}-slide-story-title`}>
          {slideStoryTitleBlocks.map(renderElement)}
        </div>
      )}
      <div className={`${classPrefix}-slide-content`}>
        {slideHeadlineBlocks.length > 0 && (
          <div className={`${classPrefix}-slide-headline-container`}>
            {slideHeadlineBlocks.map(renderElement)}
          </div>
        )}
        {contentBlocks.map(renderElement)}
      </div>

      <SlideNavigation
        prevViewId={prevId}
        nextViewId={effectiveNextId}
        homeViewId={storyHubViewId}
        currentViewId={viewId}
        onNavigate={navigateToView}
      />
    </div>
  );
};