Developer Guide to Document Authoring with Edge Delivery Services. Part 5

Author: Tom Cranstoun

Building a ReactJS app

This is part 5 of my Developer’s Guide to Document Authoring with Edge Delivery Services. This time, we will create a React App that uses Edge Delivery Services as the data source.

Previously

We created a SPA app using Documents as the data source and a document with a block to serve the content.

Part 5 - Building a React app with documents as the source

As a developer, you may be more familiar with ReactJs and want to use those skills in Document Authoring with Edge Delivery Services. You may also have an existing ReactJs app that you want to bring into the Edge Delivery Services Infrastructure. Whatever your reason, this tutorial will help you use ReactJs with Edge Delivery.

I walk you through converting the slide builder block we built in part 4 into a modular React app. We'll cover the key steps and considerations to ensure a smooth transition.

1. Understanding the Original Structure

The original approach consisted of a block consisting of a single JavaScript file (slide-builder.js) and a corresponding CSS file (slide-builder.css). While this monolithic approach worked well within the Edge Delivery Services ecosystem, it needs modularity to work well with React.

2. Breaking Down the Components

The first step in our conversion is to identify the main components of our application:

- SlideBuilder: The main container for all slides

- SlideItem: Individual slide components

- SlidePanel: The panel that opens when a slide is clicked

3. Setting Up the React Project

Start by creating a new React project. You can use Create React App or your preferred React boilerplate.

npx create-react-app react-conversion
cd react-conversion

After creating a new React project with the Create React App, you can streamline your project by removing several files that aren't necessary for our conversion. Here's a list of files you can safely delete:

1. In the `src` folder:

- `App.test.js` (We are a tutorial, no tests. Do this later)

- `logo.svg` (the React logo, which we won't be using)

- `reportWebVitals.js` (we will not need web vitals reporting)

- `setupTests.js` (no tests, see above)

2. In the `public` folder:

- `logo192.png` and `logo512.png` (React logos)

- `manifest.json` (We are not building a Progressive Web App)

- we can delete favicon.ico

- `robots.txt`

Rename index.html to slides.html

After cleaning up, your project structure might look like this:

react-conversion/
│
├── public/
│   └── index.html
│   
│
├── src/
│   ├── App.js
│   ├── App.css
│   └── index.js
│
├── package.json
└── README.md

This cleaned-up structure provides a good starting point for implementing your converted React application. You can now add your new components and styles to the `src` folder. The final project structure will end up like this.

react-conversion/
│
├── public/
│   └── index.html
│   
│
├── src/
│   ├── App.js
│   ├── App.css
│   ├── index.js
│   │
│   ├── components/
│   │   ├── SlideBuilder.js
│   │   ├── SlideItem.js
│   │   └── SlidePanel.js
│   │
│   └── styles/
│       ├── SlideBuilder.css
│       ├── SlideItem.css
│       └── SlidePanel.css
│
├── package.json
└── README.md

The index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Slide Builder React Application"
    />
    <title>Slide Builder React App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>


Now app.js

import React, { useState, useEffect } from 'react';
import SlideBuilder from './components/SlideBuilder';
import './App.css';

function App() {
  const [slides, setSlides] = useState([]);

  useEffect(() => {
    fetchSlides();
  }, []);

  const fetchSlides = async () => {
    try {
      const response = await fetch("/slides/query-index.json");
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      const json = await response.json();
      setSlides(json.data);
    } catch (error) {
      console.error("Failed to fetch slides:", error);
    }
  };

  return (
    <div className="App">
       <main>
        <SlideBuilder slides={slides} />
      </main>
    </div>
  );
}

export default App;

Instead of directly fetching data in the decorate function, I used React's useEffect hook in the App component to fetch slide data when the component mounts.

app.css

.App {
  text-align: center;
  font-family: Arial, sans-serif;
}

.App-header {
  background-color: #282c34;
  min-height: 100px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}

.App-header h1 {
  margin: 0;
}

main {
  padding: 20px;
}

/* You can add more global styles here */

/* Responsive design */
@media (max-width: 768px) {
  .App-header {
    min-height: 80px;
    font-size: calc(8px + 2vmin);
  }

  main {
    padding: 10px;
  }
}

We employ hooks such as React, useState, and useEffect in React. Additionally, we integrate the SlideBuilder component and the App.css file. The App function component serves as the foundation of our application. With the assistance of the useState hook, we effectively manage the "slides" state. To retrieve slide data upon component mounting, we utilize the useEffect hook. The fetchSlides function, an asynchronous operation, acquires slide data from the "/slides/query-index.json" endpoint. The component structure encompasses a header and a primary section. Within the central section, the SlideBuilder component is rendered, passing slide data as a property.

index.js, remove core web vitals

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

Change README.md

# Slide Builder React App

This project is a React-based slide builder application, converted from an original Edge Delivery Services block implementation.

## Project Overview

The Slide Builder React App allows users to view and interact with a collection of slides. Each slide displays an image, title, description, and supporting text. Users can click on a slide to view more detailed information in a panel.

4. Creating React Components

Now, let's create our React components:

- src/components/SlideBuilder.js

import React from 'react';
import SlideItem from './SlideItem';
import '../styles/SlideBuilder.css';

function SlideBuilder({ slides }) {
  return (
    <div className="slide-builder">
      {slides.map((slide, index) => (
        <SlideItem key={slide.path} slideData={slide} index={index} />
      ))}
    </div>
  );
}

export default SlideBuilder;

- src/components/SlideItem.js

import React, { useState, useEffect, useCallback } from 'react';
import SlidePanel from './SlidePanel';
import '../styles/SlideItem.css';

function SlideItem({ slideData, index }) {
  const [bgImage, setBgImage] = useState('');
  const [supportingText, setSupportingText] = useState('');
  const [showPanel, setShowPanel] = useState(false);

  const fetchSupportingText = useCallback(async () => {
    if (window.innerWidth <= 799) {
      const html = await fetchSlideHtml(slideData.path);
      if (html) {
        const text = extractSupportingText(html);
        setSupportingText(text);
      }
    }
  }, [slideData.path]);

  const setSlideBackground = useCallback(() => {
    const imageUrl = slideData.image.split("?")[0];
    const finalImageUrl = `${imageUrl}?width=2000&format=webply&optimize=medium`;

    const img = new Image();
    img.src = finalImageUrl;
    img.onload = () => setBgImage(finalImageUrl);
    img.onerror = () => console.error(`Failed to load image: ${finalImageUrl}`);
  }, [slideData.image]);

  useEffect(() => {
    setSlideBackground();
    fetchSupportingText();
  }, [fetchSupportingText, setSlideBackground]);

  const fetchSlideHtml = async (path) => {
    try {
      const response = await fetch(`${path}.plain.html`);
      if (!response.ok) throw new Error(`Failed to fetch HTML for slide: ${path}`);
      return await response.text();
    } catch (error) {
      console.error(error);
      return null;
    }
  };

  const extractSupportingText = (html) => {
    const parser = new DOMParser();
    const doc = parser.parseFromString(html, "text/html");
    const h2 = doc.querySelector("h2");
    let firstParagraph = h2 ? h2.nextElementSibling : doc.querySelector("p");
    while (firstParagraph && firstParagraph.tagName.toLowerCase() !== "p") {
      firstParagraph = firstParagraph.nextElementSibling;
    }
    return firstParagraph?.textContent.trim() || null;
  };

  return (
    <>
      <div
        className="slide-builder-item"
        style={{ backgroundImage: `url(${bgImage})` }}
        data-slidenum={index + 1}
        onClick={() => setShowPanel(true)}
      >
        <div className="text-container">
          <h2>{slideData.title}</h2>
          <p><strong>{slideData.description}</strong></p>
          {supportingText && <p className="supporting-text">{supportingText}</p>}
        </div>
      </div>
      {showPanel && (
        <SlidePanel slideData={slideData} onClose={() => setShowPanel(false)} />
      )}
    </>
  );
}

export default SlideItem;

- src/components/SlidePanel.js

import React, { useState, useEffect, useRef } from 'react';
import '../styles/SlidePanel.css';

function SlidePanel({ slideData, onClose }) {
  const [html, setHtml] = useState('');
  const panelRef = useRef(null);

  useEffect(() => {
    const fetchSlideHtml = async () => {
      try {
        const response = await fetch(`${slideData.path}.plain.html`);
        if (!response.ok) throw new Error(`Failed to fetch HTML for slide: ${slideData.path}`);
        const text = await response.text();
        setHtml(text);
      } catch (error) {
        console.error(error);
      }
    };

    fetchSlideHtml();

    // Add event listener for clicks outside the panel
    const handleOutsideClick = (event) => {
      if (panelRef.current && !panelRef.current.contains(event.target)) {
        onClose();
      }
    };

    document.addEventListener('mousedown', handleOutsideClick);

    // Clean up the event listener
    return () => {
      document.removeEventListener('mousedown', handleOutsideClick);
    };
  }, [slideData.path, onClose]);

  const handleOverlayClick = (event) => {
    if (event.target === event.currentTarget) {
      onClose();
    }
  };

  return (
    <div className="slide-panel-overlay" onClick={handleOverlayClick}>
      <div className="slide-panel" ref={panelRef}>
        <div className="slide-panel-content">
          <div 
            className="slide-panel-body"
            dangerouslySetInnerHTML={{ __html: html }}
          />
          <button className="slide-panel-close" onClick={onClose} aria-label="Close panel">&times;</button>
        </div>
      </div>
    </div>
  );
}

export default SlidePanel;

Don’t worry about dangerouslySetInnerHTML. I only obtained a text node in the first place.

Each component encapsulates its logic and render method.

The core logic from the original `decorate` function was distributed among these components:

- Moved slide fetching logic to the main App component

- Implemented background image loading in SlideItem

- Handled panel opening/closing in SlideItem and SlidePanel

5. Styling

Create separate CSS files for each component:

- src/styles/SlideBuilder.css

.slide-builder {
  width: 100%;
}

- src/styles/SlideItem.css

.slide-builder-item {
  height: 600px;
  object-fit: cover;
  position: relative;
  margin-bottom: 25px;
  background-size: cover;
  background-position: center;
  cursor: pointer;
}

.slide-builder-item::before {
  content: attr(data-slidenum);
  position: absolute;
  top: 20px;
  left: 20px;
  width: 40px;
  height: 40px;
  background-color: rgba(0, 0, 0, 0.7);
  color: white;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: bold;
}

.slide-builder-item .text-container {
  background: rgba(0, 0, 0, 0.5);
  color: white;
  padding: 1em;
  margin: 1em;
  position: absolute;
  bottom: 1em;
  left: 1em;
}

.slide-builder-item h2 {
  margin: 0;
  font-weight: bold;
}

.slide-builder-item p.supporting-text {
  margin: 0;
}

@media (max-width: 800px) {
  .slide-builder-item p.supporting-text {
    display: none;
  }
}

- src/styles/SlidePanel.css

.slide-panel-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1000;
}

.slide-panel {
  background-color: white;
  border: 1px solid #ccc; /* Add border */
  border-radius: 8px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  width: 80%;
  height: 80%;
  max-width: 1200px;
  max-height: 800px;
  display: flex;
  flex-direction: column;
  position: relative;
  overflow: hidden; /* Prevent content from spilling out */
}

.slide-panel-content {
  padding: 20px;
  flex-grow: 1;
  overflow-y: auto;
}

.slide-panel-body {
  text-align: left;
}

.slide-panel-close {
  position: fixed;
  top: calc(10% + 10px); /* Adjust based on the panel's position */
  right: calc(10% + 10px); /* Adjust based on the panel's position */
  background: none;
  border: none;
  font-size: 24px;
  cursor: pointer;
  z-index: 1001; /* Ensure it's above the panel content */
  background-color: white; /* Give it a background to stand out */
  width: 30px;
  height: 30px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

/* Ensure all content in the panel body is left-aligned */
.slide-panel-body h1,
.slide-panel-body h2,
.slide-panel-body h3,
.slide-panel-body h4,
.slide-panel-body h5,
.slide-panel-body h6,
.slide-panel-body p,
.slide-panel-body ul,
.slide-panel-body ol {
  text-align: left;
}

Build the output


To distribute your React application, you must create a production build and then deploy it. Here's a step-by-step guide on what you need to do:

Create a Production Build:

In your project directory, run the following:

npm run build

This command creates a build directory with a production build of your app.

The build directory will contain:

build
├── index.html
└── static/
    ├── css/
    │   └── (minified CSS files)
    └── js/
        └── (minified JavaScript files)

To distribute your app, you need to serve the contents of the build directory.

Returning to our Edge Delivery Services repository, it looks similar to the archetypal boilerplate.

Copy the internal part of the build folder, the static folder and the index.html to the root of the Edge Delivery Repo

Rename index.html to slides.html

Now, the Repo is integrated, as seen below.

We have added slides.html and the static directory.

Now commit and push your Repo.

Conclusion

Converting an Edge Delivery Services block to a React application involves breaking down the monolithic structure into reusable components, adapting the logic to work with React's lifecycle and hooks, and ensuring that all functionality is preserved. While it requires some effort, the result is an extensible codebase that leverages the power of React's component-based architecture. I prefer the block format; this tutorial is to help give you options.

Remember, this conversion is just the beginning. Once in React, you can further enhance your application with additional features, optimize performance, and integrate with other React libraries and tools. If your customer already has a ReactJS application, a calculator, or something more sophisticated, you can easily integrate this into your Edge Delivery Services site collection.

The final ReactJS application served directly from GitHub, cannot be previewed using the sidekick and thus cannot be published through it. Despite this, it is live. You can view it here https://allabout.network/slides.html

/fragments/ddt/proposition

Related Articles

guide
Back to Top