diff --git a/ui/package-lock.json b/ui/package-lock.json index fe567061f..43513cb81 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -77,7 +77,7 @@ "@emotion/react": "11.14.0", "@floating-ui/react": "0.27.18", "@hookform/resolvers": "2.9.11", - "@meemoo/react-components": "6.0.15", + "@meemoo/react-components": "6.0.16", "@studiohyperdrive/pagination": "1.0.0", "@tanstack/react-query": "5.99.0", "@viaa/avo2-components": "7.0.10", @@ -2476,9 +2476,9 @@ "license": "MIT" }, "node_modules/@meemoo/react-components": { - "version": "6.0.15", - "resolved": "http://do-prd-mvn-01.do.viaa.be:8081/repository/npm-viaa/@meemoo/react-components/-/react-components-6.0.15.tgz", - "integrity": "sha512-bvO4loqHA3VOIUI4v8ldXbLfkUFT6vT1hWAwcTb58IKEtHGv7j1tVLz9oWPcL1ZaYZgsSHsCmXjQnhzDjDAOJA==", + "version": "6.0.16", + "resolved": "http://do-prd-mvn-01.do.viaa.be:8081/repository/npm-viaa/@meemoo/react-components/-/react-components-6.0.16.tgz", + "integrity": "sha512-XsO/xgjaOkLLs83qjRdcZRVeqzo8cjX2pPmvOLEtYta3Rsh914fIahIeThqf8Vv2nJlHzMzRC+ZGNpGapgku1g==", "peer": true, "dependencies": { "use-sync-external-store": "1.4.0" diff --git a/ui/package.json b/ui/package.json index 3d68a5d13..a5f41eba7 100644 --- a/ui/package.json +++ b/ui/package.json @@ -76,7 +76,7 @@ "@emotion/react": "11.14.0", "@floating-ui/react": "0.27.18", "@hookform/resolvers": "2.9.11", - "@meemoo/react-components": "6.0.15", + "@meemoo/react-components": "6.0.16", "@studiohyperdrive/pagination": "1.0.0", "@tanstack/react-query": "5.99.0", "@viaa/avo2-components": "7.0.10", diff --git a/ui/src/react-admin/modules/content-page/components/IeObjectFlowPlayerWrapper/IeObjectFlowPlayerWrapper.tsx b/ui/src/react-admin/modules/content-page/components/IeObjectFlowPlayerWrapper/IeObjectFlowPlayerWrapper.tsx index fc055d34a..0da971c2e 100644 --- a/ui/src/react-admin/modules/content-page/components/IeObjectFlowPlayerWrapper/IeObjectFlowPlayerWrapper.tsx +++ b/ui/src/react-admin/modules/content-page/components/IeObjectFlowPlayerWrapper/IeObjectFlowPlayerWrapper.tsx @@ -1,14 +1,17 @@ import { FlowPlayer, type FlowPlayerProps } from '@meemoo/react-components'; import type { HetArchiefPlayableDisplayIeObject } from '@viaa/avo2-types'; -import React, { type FunctionComponent, type ReactNode } from 'react'; +import React, {type FunctionComponent, type ReactNode, useState} from 'react'; import { AdminConfigManager } from '~core/config'; +import { getRandomTertiaryBackgroundColor } from '~modules/content-page/helpers/get-random-tertiary-background-color.ts'; import { Color } from '~modules/content-page/types/content-block.types.ts'; import type { DefaultComponentProps } from '~modules/shared/types/components'; +import { Locale } from '~modules/translations/translations.core.types.ts'; import { isAudioFormat, isAudioVideoFormat, isVideoFormat, } from '~shared/helpers/is-audio-video-format.ts'; +import { useIsMobileWidth } from '~shared/helpers/media-query.ts'; export interface IeObjectFlowPlayerWrapperProps extends DefaultComponentProps { ieObject: HetArchiefPlayableDisplayIeObject; @@ -19,6 +22,8 @@ export interface IeObjectFlowPlayerWrapperProps extends DefaultComponentProps { onEnded?: () => void; isMuted?: boolean; onMutedChange?: (muted: boolean) => void; + backgroundColor?: string; + hideTimestampsOnMobile?: boolean; } export const IeObjectFlowPlayerWrapper: FunctionComponent = ({ @@ -29,8 +34,13 @@ export const IeObjectFlowPlayerWrapper: FunctionComponent { + const [fallbackBackgroundColor] = useState(getRandomTertiaryBackgroundColor()); + const isMobile = useIsMobileWidth(); + if (!isAudioVideoFormat(ieObject.dctermsFormat)) { return null; } @@ -54,6 +64,8 @@ export const IeObjectFlowPlayerWrapper: FunctionComponent = { poster: poster ?? imageSrc, @@ -68,14 +80,28 @@ export const IeObjectFlowPlayerWrapper: FunctionComponent then scales and centers it without stretching. -const WAVE_FORM_PADDING_X_RATIO = 0.15; -const WAVE_FORM_PADDING_Y_RATIO = 0.3; - -// Half the height of each bar (viewBox units), left to right, traced from Frame 1895.svg. -const WAVE_FORM_BAR_HALF_HEIGHTS: readonly number[] = [ - 0.3, 3.3, 3.3, 6.9, 3.3, 6.9, 13.5, 20.7, 10.5, 6.9, 17.1, 13.5, 10.5, 3.3, 6.9, 3.3, 3.3, 6.9, - 10.5, 13.5, 6.9, 3.3, 3.3, 6.9, 3.3, 3.3, 6.9, 3.3, 3.3, 1.5, -]; - -const WAVE_FORM_BAR_COUNT = WAVE_FORM_BAR_HALF_HEIGHTS.length; - -// Right margin the reference asset leaves after its last bar, reused to size the large viewBox. -const WAVE_FORM_RIGHT_MARGIN = - WAVE_FORM_VIEW_BOX_WIDTH - - (WAVE_FORM_FIRST_BAR_X + (WAVE_FORM_BAR_COUNT - 1) * WAVE_FORM_BAR_SPACING); - -function buildWaveFormBars( - barCount: number, - halfHeightAt: (index: number) => number -): WaveFormBar[] { - return Array.from({ length: barCount }, (_, index) => { - const x = WAVE_FORM_FIRST_BAR_X + index * WAVE_FORM_BAR_SPACING; - const halfHeight = halfHeightAt(index); - return { x, y1: WAVE_FORM_CENTER_Y - halfHeight, y2: WAVE_FORM_CENTER_Y + halfHeight }; - }); -} - -function getWaveFormViewBoxWidth(barCount: number): number { - return WAVE_FORM_FIRST_BAR_X + (barCount - 1) * WAVE_FORM_BAR_SPACING + WAVE_FORM_RIGHT_MARGIN; -} - -const SMALL_WAVE_FORM_BARS: readonly WaveFormBar[] = buildWaveFormBars( - WAVE_FORM_BAR_COUNT, - (index) => WAVE_FORM_BAR_HALF_HEIGHTS[index] -); - -// Large: the small wave form immediately followed by its own mirror, on one continuous grid -- -// not two small viewBoxes side by side, which would double their outer margins into a bigger gap. -const LARGE_WAVE_FORM_BARS: readonly WaveFormBar[] = buildWaveFormBars( - WAVE_FORM_BAR_COUNT * 2, - (index) => - WAVE_FORM_BAR_HALF_HEIGHTS[ - index < WAVE_FORM_BAR_COUNT ? index : WAVE_FORM_BAR_COUNT * 2 - 1 - index - ] -); - -export function getWaveFormBars(size: AudioWaveFormDisplaySize): readonly WaveFormBar[] { - return size === 'large' ? LARGE_WAVE_FORM_BARS : SMALL_WAVE_FORM_BARS; -} - -// Expands the bars' bounding box to the full display box, per the padding ratios above. -export function getWaveFormViewBox(size: AudioWaveFormDisplaySize): string { - const contentWidth = getWaveFormViewBoxWidth(getWaveFormBars(size).length); - const contentHeight = WAVE_FORM_VIEW_BOX_HEIGHT; - - const width = contentWidth / (1 - 2 * WAVE_FORM_PADDING_X_RATIO); - const height = contentHeight / (1 - 2 * WAVE_FORM_PADDING_Y_RATIO); - const minX = -(width - contentWidth) / 2; - const minY = -(height - contentHeight) / 2; - return `${minX} ${minY} ${width} ${height}`; -} diff --git a/ui/src/react-admin/modules/content-page/components/ImageOrAudioWaveForm/AudioWaveFormDisplay/AudioWaveFormDisplay.scss b/ui/src/react-admin/modules/content-page/components/ImageOrAudioWaveForm/AudioWaveFormDisplay/AudioWaveFormDisplay.scss deleted file mode 100644 index 7bf2eec62..000000000 --- a/ui/src/react-admin/modules/content-page/components/ImageOrAudioWaveForm/AudioWaveFormDisplay/AudioWaveFormDisplay.scss +++ /dev/null @@ -1,27 +0,0 @@ -@use "../../../../shared/styles/settings/colors" as colors; - -.c-audio-wave-form-display { - display: block; - width: 100%; - height: 100%; - background-color: var(--c-audio-wave-form-display-bg, transparent); - - &__scaler { - display: block; - width: 100%; - height: 100%; - } - - &__svg { - display: block; - width: 100%; - height: 100%; - } - - &__bar { - stroke: var( - --c-audio-wave-form-display-bar-color, - var(--c-audio-wave-form-display-wave-color, colors.$white) - ); - } -} diff --git a/ui/src/react-admin/modules/content-page/components/ImageOrAudioWaveForm/AudioWaveFormDisplay/AudioWaveFormDisplay.test.tsx b/ui/src/react-admin/modules/content-page/components/ImageOrAudioWaveForm/AudioWaveFormDisplay/AudioWaveFormDisplay.test.tsx deleted file mode 100644 index 891e21e7c..000000000 --- a/ui/src/react-admin/modules/content-page/components/ImageOrAudioWaveForm/AudioWaveFormDisplay/AudioWaveFormDisplay.test.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { cleanup, render } from '@testing-library/react'; -import React from 'react'; -import { afterEach, describe, expect, it } from 'vitest'; -import { Color } from '~modules/content-page/types/content-block.types.ts'; -import { AudioWaveFormDisplay } from './AudioWaveFormDisplay.tsx'; - -afterEach(() => { - cleanup(); -}); - -describe('', () => { - it('should be able to render', () => { - render(); - }); - - it('should render the large size with twice as many bars as the small size', () => { - const { container: small } = render(); - const { container: large } = render(); - - const smallBarCount = small.querySelectorAll('.c-audio-wave-form-display__bar').length; - const largeBarCount = large.querySelectorAll('.c-audio-wave-form-display__bar').length; - - expect(largeBarCount).toBe(smallBarCount * 2); - }); - - it('should default the wave color to white', () => { - const { container } = render(); - const outer = container.querySelector('.c-audio-wave-form-display') as HTMLElement; - - expect(outer.style.getPropertyValue('--c-audio-wave-form-display-wave-color')).toBe( - Color.White - ); - }); - - it('should pass the given wave and background colors through as CSS variables', () => { - const { container } = render( - - ); - const outer = container.querySelector('.c-audio-wave-form-display') as HTMLElement; - - expect(outer.style.getPropertyValue('--c-audio-wave-form-display-wave-color')).toBe(Color.Teal); - expect(outer.style.getPropertyValue('--c-audio-wave-form-display-bg')).toBe(Color.NightBlue); - }); - - it('should pass the given className through', () => { - const { container } = render(); - - expect(container.querySelector('.c-audio-wave-form-display.my-extra-class')).not.toBeNull(); - }); -}); diff --git a/ui/src/react-admin/modules/content-page/components/ImageOrAudioWaveForm/AudioWaveFormDisplay/AudioWaveFormDisplay.tsx b/ui/src/react-admin/modules/content-page/components/ImageOrAudioWaveForm/AudioWaveFormDisplay/AudioWaveFormDisplay.tsx deleted file mode 100644 index cff31d372..000000000 --- a/ui/src/react-admin/modules/content-page/components/ImageOrAudioWaveForm/AudioWaveFormDisplay/AudioWaveFormDisplay.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import clsx from 'clsx'; -import type { CSSProperties, FunctionComponent, ReactElement } from 'react'; -import React from 'react'; -import { Color } from '~modules/content-page/types/content-block.types.ts'; -import type { DefaultComponentProps } from '~modules/shared/types/components'; -import { - type AudioWaveFormDisplaySize, - getWaveFormBars, - getWaveFormViewBox, - WAVE_FORM_STROKE_WIDTH, -} from './AudioWaveFormDisplay.helpers'; - -import './AudioWaveFormDisplay.scss'; - -export type { AudioWaveFormDisplaySize }; - -export interface AudioWaveFormDisplayProps extends DefaultComponentProps { - waveColor?: Color; - backgroundColor?: Color; - size?: AudioWaveFormDisplaySize; - ariaLabel?: string; -} - -export const AudioWaveFormDisplay: FunctionComponent = ({ - className, - ariaLabel, - waveColor = Color.White, - backgroundColor, - size = 'small', -}): ReactElement => { - const bars = getWaveFormBars(size); - const viewBox = getWaveFormViewBox(size); - - return ( -
- {/* Plain box for consumers to hook a hover-zoom transform onto: transitioning `transform` - on an itself doesn't animate smoothly in every browser, unlike an ordinary element. */} -
- -
-
- ); -}; diff --git a/ui/src/react-admin/modules/content-page/components/ImageOrAudioWaveForm/ImageOrAudioWaveForm.tsx b/ui/src/react-admin/modules/content-page/components/ImageOrAudioWaveForm/ImageOrAudioWaveForm.tsx index a6254a552..73ddc9458 100644 --- a/ui/src/react-admin/modules/content-page/components/ImageOrAudioWaveForm/ImageOrAudioWaveForm.tsx +++ b/ui/src/react-admin/modules/content-page/components/ImageOrAudioWaveForm/ImageOrAudioWaveForm.tsx @@ -1,11 +1,12 @@ +import { + AudioWaveFormDisplay, + type AudioWaveFormDisplayProps, + AudioWaveFormDisplaySize, +} from '@meemoo/react-components'; import { Image } from '@viaa/avo2-components'; import type { FunctionComponent, ReactElement } from 'react'; import React from 'react'; import { AdminConfigManager } from '~core/config'; -import { - AudioWaveFormDisplay, - type AudioWaveFormDisplayProps, -} from './AudioWaveFormDisplay/AudioWaveFormDisplay.tsx'; export interface ImageOrAudioWaveFormProps extends Omit { imageSrc?: string; @@ -18,7 +19,7 @@ export const ImageOrAudioWaveForm: FunctionComponent imageAlt, waveColor, backgroundColor, - size = 'small', + size = AudioWaveFormDisplaySize.Small, }): ReactElement => { if (!imageSrc) { return <>; diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHeroCarousel/BlockHeroCarouselActiveSlide.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockHeroCarousel/BlockHeroCarouselActiveSlide.tsx index 2a463247a..6542331ef 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockHeroCarousel/BlockHeroCarouselActiveSlide.tsx +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHeroCarousel/BlockHeroCarouselActiveSlide.tsx @@ -107,6 +107,8 @@ export const BlockHeroCarouselActiveSlide: FunctionComponent ); } diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockObjectsGrid/BlockObjectsGrid.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockObjectsGrid/BlockObjectsGrid.tsx index 16dcab224..9010138a2 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockObjectsGrid/BlockObjectsGrid.tsx +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockObjectsGrid/BlockObjectsGrid.tsx @@ -1,4 +1,4 @@ -import { Button } from '@meemoo/react-components'; +import { AudioWaveFormDisplaySize, Button } from '@meemoo/react-components'; import { Spinner } from '@viaa/avo2-components'; import { AvoCoreContentPickerType } from '@viaa/avo2-types'; import clsx from 'clsx'; @@ -232,7 +232,7 @@ export const BlockObjectsGrid: FunctionComponent = ({ imageSrc={item.thumbnailUrl} imageAlt={item.name} backgroundColor={tileBackgroundColor} - size={isFixed ? 'large' : 'small'} + size={isFixed ? AudioWaveFormDisplaySize.Large : AudioWaveFormDisplaySize.Small} className="c-block-objects-grid__tile-image" /> ) : ( diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockThemeReels/BlockThemeReelSection.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockThemeReels/BlockThemeReelSection.tsx index 444d03272..29547e4d9 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockThemeReels/BlockThemeReelSection.tsx +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockThemeReels/BlockThemeReelSection.tsx @@ -1,4 +1,4 @@ -import { Button } from '@meemoo/react-components'; +import { AudioWaveFormDisplaySize, Button } from '@meemoo/react-components'; import { LinkTarget } from '@viaa/avo2-components'; import { AvoCoreContentPickerType, type HetArchiefIeObjectType } from '@viaa/avo2-types'; import clsx from 'clsx'; @@ -13,6 +13,7 @@ import { CarouselButtons } from '~modules/content-page/components/CarouselButton import { ImageOrAudioWaveForm } from '~modules/content-page/components/ImageOrAudioWaveForm/ImageOrAudioWaveForm.tsx'; import { getRandomTertiaryBackgroundColor } from '~modules/content-page/helpers/get-random-tertiary-background-color.ts'; import type { Color } from '~modules/content-page/types/content-block.types.ts'; +import { IeObjectsService } from '~modules/ie-objects/ie-objects.service.ts'; import type { DefaultComponentProps } from '~modules/shared/types/components'; import { App, Locale } from '~modules/translations/translations.core.types.ts'; import { Icon } from '~shared/components/Icon'; @@ -23,7 +24,6 @@ import { tText } from '~shared/helpers/translation-functions.ts'; import 'swiper/css'; import './BlockThemeReelsSection.scss'; -import { IeObjectsService } from '~modules/ie-objects/ie-objects.service.ts'; export interface BlockThemeReelSectionProps extends DefaultComponentProps { themeId: string; @@ -101,7 +101,7 @@ export const BlockThemeReelSection: FunctionComponent ) : ( diff --git a/ui/src/react-admin/modules/content-page/types/content-block.types.ts b/ui/src/react-admin/modules/content-page/types/content-block.types.ts index 0d0af2639..cc45b8ace 100644 --- a/ui/src/react-admin/modules/content-page/types/content-block.types.ts +++ b/ui/src/react-admin/modules/content-page/types/content-block.types.ts @@ -88,6 +88,7 @@ export enum Color { AlertBackground = '#FFFFCC', AlertAccent = '#E9E994', TealBright200 = '#CFE3E9', + Teal40 = '#81ECDC', BorderColor = '#3FB1D6', InputBoxShadow = '#69C2DF', Transparent = 'TRANSPARENT', diff --git a/ui/src/react-admin/modules/shared/helpers/formatters/duration.test.ts b/ui/src/react-admin/modules/shared/helpers/formatters/duration.test.ts deleted file mode 100644 index ac5c2211f..000000000 --- a/ui/src/react-admin/modules/shared/helpers/formatters/duration.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { formatDurationMinutesSeconds } from './duration'; - -describe('Formatters - duration', () => { - it('Should format as a duration`', () => { - expect(formatDurationMinutesSeconds(-5)).toEqual('00:05'); - expect(formatDurationMinutesSeconds(0)).toEqual('00:00'); - expect(formatDurationMinutesSeconds(20)).toEqual('00:20'); - expect(formatDurationMinutesSeconds(40)).toEqual('00:40'); - expect(formatDurationMinutesSeconds(59)).toEqual('00:59'); - expect(formatDurationMinutesSeconds(60)).toEqual('01:00'); - expect(formatDurationMinutesSeconds(61)).toEqual('01:01'); - expect(formatDurationMinutesSeconds(121)).toEqual('02:01'); - expect(formatDurationMinutesSeconds(3000)).toEqual('50:00'); - expect(formatDurationMinutesSeconds(30000)).toEqual('500:00'); - }); -}); diff --git a/ui/src/react-admin/modules/shared/helpers/formatters/duration.ts b/ui/src/react-admin/modules/shared/helpers/formatters/duration.ts deleted file mode 100644 index df603e17a..000000000 --- a/ui/src/react-admin/modules/shared/helpers/formatters/duration.ts +++ /dev/null @@ -1,18 +0,0 @@ -export function formatDurationMinutesSeconds(numSeconds: number | null | undefined): string { - const seconds: number = Math.abs(numSeconds || 0); - const mins = Math.floor(seconds / 60); - const secs = seconds % 60; - - return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; -} - -export function formatDurationHoursMinutesSeconds(numSeconds: number | null | undefined) { - const seconds: number = Math.abs(numSeconds || 0); - const hours = Math.floor(seconds / 3600); - const mins = Math.floor((seconds - hours * 3600) / 60); - const secs = seconds % 60; - - return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}:${secs - .toString() - .padStart(2, '0')}`; -}