Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ All notable changes to this project will be documented in this file.
- Optimized release data fetching.
- Optimized list loading.
- Removed fixture length check from test.
- Introduced two hooks (useBaseSlideExecution, useMultipleEntrySlideExecution) that are used in the templates in place
of BaseSlideExecution in fixed duration slides and manual iteration over entries in slides that iterate through
elements before calling slideDone.
- Fixed issue with Slideshow animations that would be locked to one type.
- Fixed sorting in calendar "multiple" layout.
- Fixed video progression issues.

### NB! Prior to 3.x the project was split into separate repositories

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -840,7 +840,7 @@ For an example of a custom template see `assets/shared/custom-templates-example/

The slide is responsible for signaling that it is done executing.
This is done by calling the slideDone() function. If the slide should just run for X milliseconds then you can use the
BaseSlideExecution class to handle this. See the example for this approach.
`useBaseSlideExecution` hook to handle this. See the example for this approach.

##### Admin Form

Expand Down
2 changes: 1 addition & 1 deletion Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ tasks:
test:frontend-local:
desc: "Runs frontend tests from the local machine."
cmds:
- BASE_URL="https://display.local.itkdev.dk" npx playwright test
- BASE_URL="https://display.local.itkdev.dk" npx playwright test {{.CLI_ARGS}}

test:frontend-local-ui:
desc: "Runs frontend tests from the local machine in UI mode."
Expand Down
15 changes: 2 additions & 13 deletions assets/shared/custom-templates-example/custom-template-example.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useEffect } from "react";
import templateConfig from "./custom-template-example.json";
import BaseSlideExecution from "../slide-utils/base-slide-execution.js";
import useBaseSlideExecution from "../slide-utils/useBaseSlideExecution.js";
import { ThemeStyles } from "../slide-utils/slide-util.jsx";

/**
Expand Down Expand Up @@ -57,17 +56,7 @@ function CustomTemplateExample({
const { duration = 15000 } = content;
const { title = "Default title" } = content;

const slideExecution = new BaseSlideExecution(slide, slideDone);

useEffect(() => {
if (run) {
slideExecution.start(duration);
}

return function cleanup() {
slideExecution.stop();
};
}, [run]);
useBaseSlideExecution({ slide, run, slideDone, duration });

return (
<>
Expand Down
53 changes: 0 additions & 53 deletions assets/shared/slide-utils/base-slide-execution.js

This file was deleted.

40 changes: 40 additions & 0 deletions assets/shared/slide-utils/useBaseSlideExecution.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useEffect, useRef } from "react";

/**
* Hook to manage slide execution lifecycle.
*
* Uses refs for slide, slideDone, and duration to avoid stale closures
* while keeping [run] as the only dependency that triggers the timer.
*
* @param {object} options
* @param {object} options.slide The slide object.
* @param {boolean} options.run Whether the slide should run.
* @param {Function} options.slideDone Callback when slide finishes.
* @param {number} options.duration Duration in ms.
*/
function useBaseSlideExecution({ slide, run, slideDone, duration }) {
const slideRef = useRef(slide);
const slideDoneRef = useRef(slideDone);
const durationRef = useRef(duration);

slideRef.current = slide;
slideDoneRef.current = slideDone;
durationRef.current = duration;

useEffect(() => {
if (!run) return;

const safeDuration =
Number.isFinite(durationRef.current) && durationRef.current > 0
? durationRef.current
: 15000;

const timeoutId = setTimeout(() => {
slideDoneRef.current(slideRef.current);
}, safeDuration);

return () => clearTimeout(timeoutId);
}, [run]);
}

export default useBaseSlideExecution;
79 changes: 79 additions & 0 deletions assets/shared/slide-utils/useMultipleEntrySlideExecution.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { useEffect, useRef, useState } from "react";

/**
* Hook to manage slide execution for templates that cycle through
* multiple entries (RSS feeds, news feeds, slideshows, etc.).
*
* Uses refs to avoid stale closures while keeping [run] as the
* only dependency that triggers the cycling.
*
* @param {object} options
* @param {Array} options.entries Array of entries to cycle through.
* @param {boolean} options.run Whether the slide should run.
* @param {object} options.slide The slide object.
* @param {Function} options.slideDone Callback when cycling completes.
* @param {number} options.entryDuration Duration per entry in ms.
*/
function useMultipleEntrySlideExecution({
entries,
run,
slide,
slideDone,
entryDuration,
}) {
const [entryIndex, setEntryIndex] = useState(0);
const [currentEntry, setCurrentEntry] = useState(null);

// Refs to avoid stale closures (same pattern as useBaseSlideExecution)
const slideRef = useRef(slide);
const slideDoneRef = useRef(slideDone);
const entriesRef = useRef(entries);
const entryDurationRef = useRef(entryDuration);

slideRef.current = slide;
slideDoneRef.current = slideDone;
entriesRef.current = entries;
entryDurationRef.current = entryDuration;

useEffect(() => {
if (!run || !entriesRef.current?.length) return;

let timeoutId = null;
let stopped = false;

const showEntry = (index) => {
if (stopped) return;

if (index >= entriesRef.current.length) {
slideDoneRef.current(slideRef.current);
return;
}

setEntryIndex(index);
setCurrentEntry(entriesRef.current[index]);

const safeDuration =
Number.isFinite(entryDurationRef.current) &&
entryDurationRef.current > 0
? entryDurationRef.current
: 15000;

timeoutId = setTimeout(() => {
showEntry(index + 1);
}, safeDuration);
};

showEntry(0);

return () => {
stopped = true;
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
};
}, [run]);

return { currentEntry, entryIndex };
}

export default useMultipleEntrySlideExecution;
15 changes: 2 additions & 13 deletions assets/shared/templates/book-review.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useEffect } from "react";
import parse from "html-react-parser";
import DOMPurify from "dompurify";
import BaseSlideExecution from "../slide-utils/base-slide-execution.js";
import useBaseSlideExecution from "../slide-utils/useBaseSlideExecution.js";
import {
getFirstMediaUrlFromField,
ThemeStyles,
Expand Down Expand Up @@ -61,17 +60,7 @@ function BookReview({ slide, content, run, slideDone, executionId }) {
? { backgroundImage: `url("${bookImageUrl}")` }
: "";

/** Setup slide run function. */
const slideExecution = new BaseSlideExecution(slide, slideDone);
useEffect(() => {
if (run) {
slideExecution.start(duration);
}

return function cleanup() {
slideExecution.stop();
};
}, [run]);
useBaseSlideExecution({ slide, run, slideDone, duration });

return (
<>
Expand Down
14 changes: 2 additions & 12 deletions assets/shared/templates/brnd.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useEffect, Fragment, useState } from "react";
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { FormattedMessage, IntlProvider } from "react-intl";
import BaseSlideExecution from "../slide-utils/base-slide-execution";
import useBaseSlideExecution from "../slide-utils/useBaseSlideExecution.js";
import da from "./brnd/lang/da.json";
import {
getFirstMediaUrlFromField,
Expand Down Expand Up @@ -64,17 +64,7 @@ function Brnd({ slide, content, run, slideDone, executionId }) {
rootStyle["--bg-image"] = `url("${imageUrl}")`;
}

/** Setup slide run function. */
const slideExecution = new BaseSlideExecution(slide, slideDone);
useEffect(() => {
if (run) {
slideExecution.start(duration);
}

return function cleanup() {
slideExecution.stop();
};
}, [run]);
useBaseSlideExecution({ slide, run, slideDone, duration });

/** Imports language strings, sets localized formats. */
useEffect(() => {
Expand Down
20 changes: 4 additions & 16 deletions assets/shared/templates/calendar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { FormattedMessage, IntlProvider } from "react-intl";
import BaseSlideExecution from "../slide-utils/base-slide-execution.js";
import useBaseSlideExecution from "../slide-utils/useBaseSlideExecution.js";
import da from "./calendar/lang/da.json";
import {
getFirstMediaUrlFromField,
Expand Down Expand Up @@ -48,7 +48,7 @@ function renderSlide(slide, run, slideDone) {
* @returns {JSX.Element} The component.
*/
function Calendar({ slide, content, run, slideDone, executionId }) {
const [translations, setTranslations] = useState();
const [translations, setTranslations] = useState(da);

const {
layout = "multiple",
Expand All @@ -67,23 +67,11 @@ function Calendar({ slide, content, run, slideDone, executionId }) {
rootStyle["--bg-image"] = `url("${imageUrl}")`;
}

/** Setup slide run function. */
const slideExecution = new BaseSlideExecution(slide, slideDone);
useEffect(() => {
if (run) {
slideExecution.start(duration);
}
useBaseSlideExecution({ slide, run, slideDone, duration });

return function cleanup() {
slideExecution.stop();
};
}, [run]);

/** Imports language strings, sets localized formats. */
/** Sets localized formats. */
useEffect(() => {
dayjs.extend(localizedFormat);

setTranslations(da);
}, []);

const getTitle = (eventTitle) => {
Expand Down
2 changes: 1 addition & 1 deletion assets/shared/templates/calendar/calendar-multiple.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ function CalendarMultiple({

return e.endTime > now.unix() && startDate.date() === now.date();
})
.sort((a, b) => a - b);
.sort((a, b) => a.startTime - b.startTime);
};

useEffect(() => {
Expand Down
14 changes: 2 additions & 12 deletions assets/shared/templates/contacts.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState, useEffect } from "react";
import { IntlProvider, FormattedMessage } from "react-intl";
import styled from "styled-components";
import BaseSlideExecution from "../slide-utils/base-slide-execution.js";
import useBaseSlideExecution from "../slide-utils/useBaseSlideExecution.js";
import da from "./contacts/lang/da.json";
import {
getFirstMediaUrlFromField,
Expand Down Expand Up @@ -72,17 +72,7 @@ function Contacts({ slide, content, run, slideDone, executionId }) {
setTranslations(da);
}, []);

/** Setup slide run function. */
const slideExecution = new BaseSlideExecution(slide, slideDone);
useEffect(() => {
if (run) {
slideExecution.start(duration);
}

return function cleanup() {
slideExecution.stop();
};
}, [run]);
useBaseSlideExecution({ slide, run, slideDone, duration });

return (
<IntlProvider messages={translations} locale="da" defaultLocale="da">
Expand Down
15 changes: 2 additions & 13 deletions assets/shared/templates/iframe.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useEffect } from "react";
import BaseSlideExecution from "../slide-utils/base-slide-execution.js";
import useBaseSlideExecution from "../slide-utils/useBaseSlideExecution.js";
import { ThemeStyles } from "../slide-utils/slide-util.jsx";
import "../slide-utils/global-styles.css";
import templateConfig from "./iframe.json";
Expand Down Expand Up @@ -38,17 +37,7 @@ function renderSlide(slide, run, slideDone) {
function IFrame({ slide, content, run, slideDone, executionId }) {
const { source, duration = 15000 } = content;

/** Setup slide run function. */
const slideExecution = new BaseSlideExecution(slide, slideDone);
useEffect(() => {
if (run) {
slideExecution.start(duration);
}

return function cleanup() {
slideExecution.stop();
};
}, [run]);
useBaseSlideExecution({ slide, run, slideDone, duration });

return (
<>
Expand Down
Loading