mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-14 11:53:49 +00:00
refactor: pass events instead of derived data to titles (#493)
* refactor: pass events instead of derived data to titles
This commit is contained in:
@@ -12,17 +12,17 @@ export default function Info() {
|
||||
const showNif = useEditorSettings((state) => state.eventSettings.showNif);
|
||||
|
||||
const titlesNow = {
|
||||
title: data.titles.titleNow || '',
|
||||
subtitle: data.titles.subtitleNow || '',
|
||||
presenter: data.titles.presenterNow || '',
|
||||
note: data.titles.noteNow || '',
|
||||
title: data.eventNow?.title || '',
|
||||
subtitle: data.eventNow?.subtitle || '',
|
||||
presenter: data.eventNow?.presenter || '',
|
||||
note: data.eventNow?.note || '',
|
||||
};
|
||||
|
||||
const titlesNext = {
|
||||
title: data.titles.titleNext || '',
|
||||
subtitle: data.titles.subtitleNext || '',
|
||||
presenter: data.titles.presenterNext || '',
|
||||
note: data.titles.noteNext || '',
|
||||
title: data.eventNext?.title || '',
|
||||
subtitle: data.eventNext?.subtitle || '',
|
||||
presenter: data.eventNext?.presenter || '',
|
||||
note: data.eventNext?.note || '',
|
||||
};
|
||||
|
||||
const selected = !data.numEvents
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import { ComponentType, useMemo } from 'react';
|
||||
import { TitleBlock } from 'ontime-types';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import useEventData from '../../common/hooks-query/useEventData';
|
||||
@@ -9,8 +8,6 @@ import useViewSettings from '../../common/hooks-query/useViewSettings';
|
||||
import { runtime } from '../../common/stores/runtime';
|
||||
import { useViewOptionsStore } from '../../common/stores/viewOptions';
|
||||
|
||||
export type TitleManager = TitleBlock & { showNow: boolean; showNext: boolean };
|
||||
|
||||
const withData = <P extends object>(Component: ComponentType<P>) => {
|
||||
return (props: Partial<P>) => {
|
||||
// persisted app state
|
||||
@@ -30,44 +27,22 @@ const withData = <P extends object>(Component: ComponentType<P>) => {
|
||||
|
||||
// websocket data
|
||||
const data = useStore(runtime);
|
||||
const { timer, titles, titlesPublic, publicMessage, timerMessage, lowerMessage, playback, onAir } = data;
|
||||
const publicSelectedId = data.loaded.selectedPublicEventId;
|
||||
const selectedId = data.loaded.selectedEventId;
|
||||
const nextId = data.loaded.nextEventId;
|
||||
|
||||
/********************************************/
|
||||
/*** + titleManager ***/
|
||||
/*** WRAP INFORMATION RELATED TO TITLES ***/
|
||||
/*** ---------------------------------- ***/
|
||||
/********************************************/
|
||||
// is there a now field?
|
||||
let showNow = true;
|
||||
if (!titles.titleNow && !titles.subtitleNow && !titles.presenterNow) showNow = false;
|
||||
|
||||
// is there a next field?
|
||||
let showNext = true;
|
||||
if (!titles.titleNext && !titles.subtitleNext && !titles.presenterNext) showNext = false;
|
||||
|
||||
const titleManager: TitleManager = { ...titles, showNow: showNow, showNext: showNext };
|
||||
|
||||
/********************************************/
|
||||
/*** + publicTitleManager ***/
|
||||
/*** WRAP INFORMATION RELATED TO TITLES ***/
|
||||
/*** ---------------------------------- ***/
|
||||
/********************************************/
|
||||
// is there a now field?
|
||||
let showPublicNow = true;
|
||||
if (!titlesPublic.titleNow && !titlesPublic.subtitleNow && !titlesPublic.presenterNow) showPublicNow = false;
|
||||
|
||||
// is there a next field?
|
||||
let showPublicNext = true;
|
||||
if (!titlesPublic.titleNext && !titlesPublic.subtitleNext && !titlesPublic.presenterNext) showPublicNext = false;
|
||||
|
||||
const publicTitleManager: TitleManager = {
|
||||
...titlesPublic,
|
||||
showNow: showPublicNow,
|
||||
showNext: showPublicNext,
|
||||
};
|
||||
const {
|
||||
timer,
|
||||
publicMessage,
|
||||
timerMessage,
|
||||
lowerMessage,
|
||||
playback,
|
||||
onAir,
|
||||
eventNext,
|
||||
publicEventNext,
|
||||
publicEventNow,
|
||||
eventNow,
|
||||
loaded,
|
||||
} = data;
|
||||
const publicSelectedId = loaded.selectedPublicEventId;
|
||||
const selectedId = loaded.selectedEventId;
|
||||
const nextId = loaded.nextEventId;
|
||||
|
||||
/******************************************/
|
||||
/*** + TimeManagerType ***/
|
||||
@@ -75,9 +50,6 @@ const withData = <P extends object>(Component: ComponentType<P>) => {
|
||||
/*** -------------------------------- ***/
|
||||
/******************************************/
|
||||
|
||||
// inject info:
|
||||
// is timer finished
|
||||
// get clock string
|
||||
const TimeManagerType = {
|
||||
...timer,
|
||||
playback,
|
||||
@@ -95,8 +67,10 @@ const withData = <P extends object>(Component: ComponentType<P>) => {
|
||||
pres={timerMessage}
|
||||
publ={publicMessage}
|
||||
lower={lowerMessage}
|
||||
title={titleManager}
|
||||
publicTitle={publicTitleManager}
|
||||
eventNow={eventNow}
|
||||
publicEventNow={publicEventNow}
|
||||
eventNext={eventNext}
|
||||
publicEventNext={publicEventNext}
|
||||
time={TimeManagerType}
|
||||
events={publicEvents}
|
||||
backstageEvents={rundownData}
|
||||
|
||||
@@ -18,7 +18,6 @@ import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||
import { titleVariants } from '../common/animation';
|
||||
import { TitleManager } from '../ViewWrapper';
|
||||
|
||||
import './Backstage.scss';
|
||||
|
||||
@@ -30,7 +29,8 @@ const formatOptions = {
|
||||
interface BackstageProps {
|
||||
isMirrored: boolean;
|
||||
publ: Message;
|
||||
title: TitleManager;
|
||||
eventNow: OntimeEvent | null;
|
||||
eventNext: OntimeEvent | null;
|
||||
time: TimeManagerType;
|
||||
backstageEvents: OntimeEvent[];
|
||||
selectedId: string | null;
|
||||
@@ -39,7 +39,7 @@ interface BackstageProps {
|
||||
}
|
||||
|
||||
export default function Backstage(props: BackstageProps) {
|
||||
const { isMirrored, publ, title, time, backstageEvents, selectedId, general, viewSettings } = props;
|
||||
const { isMirrored, publ, eventNow, eventNext, time, backstageEvents, selectedId, general, viewSettings } = props;
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const { getLocalizedString } = useTranslation();
|
||||
const [blinkClass, setBlinkClass] = useState(false);
|
||||
@@ -110,7 +110,7 @@ export default function Backstage(props: BackstageProps) {
|
||||
|
||||
<div className='now-container'>
|
||||
<AnimatePresence>
|
||||
{title.showNow && (
|
||||
{eventNow && (
|
||||
<motion.div
|
||||
className={`event now ${blinkClass ? 'blink' : ''}`}
|
||||
key='now'
|
||||
@@ -121,9 +121,9 @@ export default function Backstage(props: BackstageProps) {
|
||||
>
|
||||
<TitleCard
|
||||
label='now'
|
||||
title={title.titleNow}
|
||||
subtitle={title.subtitleNow}
|
||||
presenter={title.presenterNow}
|
||||
title={eventNow.title}
|
||||
subtitle={eventNow.subtitle}
|
||||
presenter={eventNow.presenter}
|
||||
/>
|
||||
<div className='timer-group'>
|
||||
<div className='aux-timers'>
|
||||
@@ -144,7 +144,7 @@ export default function Backstage(props: BackstageProps) {
|
||||
</AnimatePresence>
|
||||
|
||||
<AnimatePresence>
|
||||
{title.showNext && (
|
||||
{eventNext && (
|
||||
<motion.div
|
||||
className='event next'
|
||||
key='next'
|
||||
@@ -155,9 +155,9 @@ export default function Backstage(props: BackstageProps) {
|
||||
>
|
||||
<TitleCard
|
||||
label='next'
|
||||
title={title.titleNext}
|
||||
subtitle={title.subtitleNext}
|
||||
presenter={title.presenterNext}
|
||||
title={eventNext.title}
|
||||
subtitle={eventNext.subtitle}
|
||||
presenter={eventNext.presenter}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Message } from 'ontime-types';
|
||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
import { LOWER_THIRDS_OPTIONS } from '../../../common/components/view-params-editor/constants';
|
||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||
import { TitleManager } from '../ViewWrapper';
|
||||
|
||||
import { LowerOptions } from './LowerWrapper';
|
||||
|
||||
@@ -13,12 +12,14 @@ import './LowerLines.scss';
|
||||
|
||||
interface LowerLinesProps {
|
||||
lower: Message;
|
||||
title: TitleManager;
|
||||
heading: string;
|
||||
subheading: string;
|
||||
options: LowerOptions;
|
||||
doShow: boolean;
|
||||
}
|
||||
|
||||
export default function LowerLines(props: LowerLinesProps) {
|
||||
const { lower, title, options } = props;
|
||||
const { lower, heading, subheading, options, doShow } = props;
|
||||
const [showLower, setShowLower] = useState(true);
|
||||
|
||||
// Unmount if fadeOut
|
||||
@@ -36,8 +37,8 @@ export default function LowerLines(props: LowerLinesProps) {
|
||||
}, [options.fadeOut, options.transitionIn]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowLower(title.showNow);
|
||||
}, [title.showNow]);
|
||||
setShowLower(doShow);
|
||||
}, [doShow]);
|
||||
|
||||
// Format messages
|
||||
const showLowerMessage = lower.text !== '' && lower.visible;
|
||||
@@ -146,14 +147,14 @@ export default function LowerLines(props: LowerLinesProps) {
|
||||
>
|
||||
<motion.div className='title-container' variants={titleContainerVariants}>
|
||||
<motion.div className='title' variants={titleVariants}>
|
||||
{title.titleNow}
|
||||
{heading}
|
||||
</motion.div>
|
||||
<div className='title-decor' />
|
||||
</motion.div>
|
||||
<motion.div className='subtitle-container' variants={subtitleContainerVariants}>
|
||||
<div className='sub-decor' />
|
||||
<motion.div className='subtitle' variants={subtitleVariants}>
|
||||
{title.presenterNow}
|
||||
{subheading}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import isEqual from 'react-fast-compare';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { Message, ViewSettings } from 'ontime-types';
|
||||
import { Message, OntimeEvent, ViewSettings } from 'ontime-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||
import { TitleManager } from '../ViewWrapper';
|
||||
|
||||
import LowerLines from './LowerLines';
|
||||
|
||||
@@ -17,33 +16,26 @@ export type LowerOptions = {
|
||||
keyColour?: string;
|
||||
fadeOut: number;
|
||||
};
|
||||
|
||||
interface LowerProps {
|
||||
title: TitleManager;
|
||||
eventNow: OntimeEvent | null;
|
||||
lower: Message;
|
||||
viewSettings: ViewSettings;
|
||||
}
|
||||
|
||||
// prevent triggering animation without a content change
|
||||
const areEqual = (prevProps: LowerProps, nextProps: LowerProps) => {
|
||||
return isEqual(prevProps.title, nextProps.title) && isEqual(prevProps.lower, nextProps.lower);
|
||||
return isEqual(prevProps.eventNow?.title, nextProps.eventNow?.title) && isEqual(prevProps.lower, nextProps.lower);
|
||||
};
|
||||
|
||||
const Lower = (props: LowerProps) => {
|
||||
const { title, lower, viewSettings } = props;
|
||||
const { eventNow, lower, viewSettings } = props;
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const [searchParams] = useSearchParams();
|
||||
const [titles, setTitles] = useState<TitleManager>({
|
||||
titleNow: '',
|
||||
titleNext: '',
|
||||
subtitleNow: '',
|
||||
subtitleNext: '',
|
||||
presenterNow: '',
|
||||
presenterNext: '',
|
||||
noteNow: '',
|
||||
noteNext: '',
|
||||
showNow: false,
|
||||
showNext: false,
|
||||
});
|
||||
|
||||
const [heading, setHeading] = useState('');
|
||||
const [subheading, setSubheading] = useState('');
|
||||
const [showLower, setShowLower] = useState(false);
|
||||
|
||||
// Set window title
|
||||
useEffect(() => {
|
||||
@@ -54,28 +46,32 @@ const Lower = (props: LowerProps) => {
|
||||
useEffect(() => {
|
||||
// clear titles if necessary
|
||||
// will trigger an animation out in the component
|
||||
let timeout: NodeJS.Timeout | null = null;
|
||||
if (
|
||||
title?.titleNow !== titles?.titleNow ||
|
||||
title?.subtitleNow !== titles?.subtitleNow ||
|
||||
title?.presenterNow !== titles?.presenterNow
|
||||
) {
|
||||
setTitles((t) => ({ ...t, showNow: false }));
|
||||
let timeout: NodeJS.Timeout;
|
||||
|
||||
const transitionTime = 2000;
|
||||
const haveTitlesChanged = eventNow?.title !== heading || eventNow?.presenter !== subheading;
|
||||
const areTitlesEmpty = !eventNow?.title && !eventNow?.presenter;
|
||||
|
||||
// we have new titles
|
||||
if (haveTitlesChanged && !areTitlesEmpty) {
|
||||
// show lower
|
||||
setHeading(eventNow?.title ?? '');
|
||||
setSubheading(eventNow?.presenter ?? '');
|
||||
setShowLower(true);
|
||||
|
||||
// schedule transition out
|
||||
const transitionTime = 5000;
|
||||
timeout = setTimeout(() => {
|
||||
setTitles(title);
|
||||
setShowLower(false);
|
||||
}, transitionTime);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeout != null) {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line -- we do this to keep animations
|
||||
}, [title.titleNow, title.subtitleNow, title.presenterNow]);
|
||||
}, [eventNow?.title, eventNow?.presenter]);
|
||||
|
||||
// defer rendering until we load stylesheets
|
||||
if (!shouldRender) {
|
||||
@@ -135,7 +131,7 @@ const Lower = (props: LowerProps) => {
|
||||
}
|
||||
}
|
||||
|
||||
return <LowerLines lower={lower} title={titles} options={options} />;
|
||||
return <LowerLines lower={lower} heading={heading} subheading={subheading} options={options} doShow={showLower} />;
|
||||
};
|
||||
|
||||
export default memo(Lower, areEqual);
|
||||
|
||||
@@ -16,7 +16,6 @@ import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||
import { titleVariants } from '../common/animation';
|
||||
import { TitleManager } from '../ViewWrapper';
|
||||
|
||||
import './Public.scss';
|
||||
|
||||
@@ -28,7 +27,8 @@ const formatOptions = {
|
||||
interface BackstageProps {
|
||||
isMirrored: boolean;
|
||||
publ: Message;
|
||||
publicTitle: TitleManager;
|
||||
publicEventNow: OntimeEvent | null;
|
||||
publicEventNext: OntimeEvent | null;
|
||||
time: TimeManagerType;
|
||||
events: OntimeEvent[];
|
||||
publicSelectedId: string | null;
|
||||
@@ -37,7 +37,8 @@ interface BackstageProps {
|
||||
}
|
||||
|
||||
export default function Public(props: BackstageProps) {
|
||||
const { isMirrored, publ, publicTitle, time, events, publicSelectedId, general, viewSettings } = props;
|
||||
const { isMirrored, publ, publicEventNow, publicEventNext, time, events, publicSelectedId, general, viewSettings } =
|
||||
props;
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const { getLocalizedString } = useTranslation();
|
||||
|
||||
@@ -68,7 +69,7 @@ export default function Public(props: BackstageProps) {
|
||||
|
||||
<div className='now-container'>
|
||||
<AnimatePresence>
|
||||
{publicTitle.showNow && (
|
||||
{publicEventNow && (
|
||||
<motion.div
|
||||
className='event now'
|
||||
key='now'
|
||||
@@ -79,16 +80,16 @@ export default function Public(props: BackstageProps) {
|
||||
>
|
||||
<TitleCard
|
||||
label='now'
|
||||
title={publicTitle.titleNow}
|
||||
subtitle={publicTitle.subtitleNow}
|
||||
presenter={publicTitle.presenterNow}
|
||||
title={publicEventNow.title}
|
||||
subtitle={publicEventNow.subtitle}
|
||||
presenter={publicEventNow.presenter}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<AnimatePresence>
|
||||
{publicTitle.showNext && (
|
||||
{publicEventNext && (
|
||||
<motion.div
|
||||
className='event next'
|
||||
key='next'
|
||||
@@ -99,9 +100,9 @@ export default function Public(props: BackstageProps) {
|
||||
>
|
||||
<TitleCard
|
||||
label='next'
|
||||
title={publicTitle.titleNext}
|
||||
subtitle={publicTitle.subtitleNext}
|
||||
presenter={publicTitle.presenterNext}
|
||||
title={publicEventNext.title}
|
||||
subtitle={publicEventNext.subtitle}
|
||||
presenter={publicEventNext.presenter}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
@@ -13,7 +13,6 @@ import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet
|
||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||
import { secondsInMillis } from '../../../common/utils/dateConfig';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
import { TitleManager } from '../ViewWrapper';
|
||||
|
||||
import { type ScheduleEvent, formatEventList, trimRundown } from './studioClock.utils';
|
||||
|
||||
@@ -26,7 +25,7 @@ const formatOptions = {
|
||||
|
||||
interface StudioClockProps {
|
||||
isMirrored: boolean;
|
||||
title: TitleManager;
|
||||
eventNext: OntimeEvent | null;
|
||||
time: TimeManagerType;
|
||||
backstageEvents: OntimeRundown;
|
||||
selectedId: string | null;
|
||||
@@ -36,7 +35,7 @@ interface StudioClockProps {
|
||||
}
|
||||
|
||||
export default function StudioClock(props: StudioClockProps) {
|
||||
const { isMirrored, title, time, backstageEvents, selectedId, nextId, onAir, viewSettings } = props;
|
||||
const { isMirrored, eventNext, time, backstageEvents, selectedId, nextId, onAir, viewSettings } = props;
|
||||
|
||||
// deferring rendering seems to affect styling (font and useFitText)
|
||||
useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
@@ -87,7 +86,7 @@ export default function StudioClock(props: StudioClockProps) {
|
||||
className='next-title'
|
||||
style={{ fontSize: titleFontSize, height: '10vh', width: '100%', maxWidth: '75%' }}
|
||||
>
|
||||
{title.titleNext}
|
||||
{eventNext?.title ?? ''}
|
||||
</div>
|
||||
<div className={isNegative ? 'next-countdown' : 'next-countdown next-countdown--overtime'}>
|
||||
{selectedId !== null && formatDisplay(time.current)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { EventData, Playback, TimerMessage, TimerType, ViewSettings } from 'ontime-types';
|
||||
import { EventData, OntimeEvent, Playback, TimerMessage, TimerType, ViewSettings } from 'ontime-types';
|
||||
|
||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||
import MultiPartProgressBar from '../../../common/components/multi-part-progress-bar/MultiPartProgressBar';
|
||||
@@ -13,7 +13,6 @@ import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||
import { formatTime } from '../../../common/utils/time';
|
||||
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||
import { formatTimerDisplay, getTimerByType } from '../common/viewerUtils';
|
||||
import { TitleManager } from '../ViewWrapper';
|
||||
|
||||
import './Timer.scss';
|
||||
|
||||
@@ -42,13 +41,14 @@ interface TimerProps {
|
||||
isMirrored: boolean;
|
||||
general: EventData;
|
||||
pres: TimerMessage;
|
||||
title: TitleManager;
|
||||
eventNow: OntimeEvent | null;
|
||||
eventNext: OntimeEvent | null;
|
||||
time: TimeManagerType;
|
||||
viewSettings: ViewSettings;
|
||||
}
|
||||
|
||||
export default function Timer(props: TimerProps) {
|
||||
const { isMirrored, pres, title, time, viewSettings } = props;
|
||||
const { isMirrored, pres, eventNow, eventNext, time, viewSettings } = props;
|
||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||
const { getLocalizedString } = useTranslation();
|
||||
|
||||
@@ -139,7 +139,7 @@ export default function Timer(props: TimerProps) {
|
||||
/>
|
||||
|
||||
<AnimatePresence>
|
||||
{title.showNow && !finished && (
|
||||
{eventNow && !finished && (
|
||||
<motion.div
|
||||
className='event now'
|
||||
key='now'
|
||||
@@ -148,13 +148,13 @@ export default function Timer(props: TimerProps) {
|
||||
animate='visible'
|
||||
exit='exit'
|
||||
>
|
||||
<TitleCard label='now' title={title.titleNow} subtitle={title.subtitleNow} presenter={title.presenterNow} />
|
||||
<TitleCard label='now' title={eventNow.title} subtitle={eventNow.subtitle} presenter={eventNow.presenter} />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<AnimatePresence>
|
||||
{title.showNext && (
|
||||
{eventNext && (
|
||||
<motion.div
|
||||
className='event next'
|
||||
key='next'
|
||||
@@ -165,9 +165,9 @@ export default function Timer(props: TimerProps) {
|
||||
>
|
||||
<TitleCard
|
||||
label='next'
|
||||
title={title.titleNext}
|
||||
subtitle={title.subtitleNext}
|
||||
presenter={title.presenterNext}
|
||||
title={eventNext.title}
|
||||
subtitle={eventNext.subtitle}
|
||||
presenter={eventNext.presenter}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user