-
- {text}
+
{handleAddNew && (
handleAddNew(SupportedEntry.Event)} variant='primary' size='large'>
diff --git a/apps/client/src/features/operator/Operator.tsx b/apps/client/src/features/operator/Operator.tsx
index c79abe4ba..f1b6ffc41 100644
--- a/apps/client/src/features/operator/Operator.tsx
+++ b/apps/client/src/features/operator/Operator.tsx
@@ -1,6 +1,7 @@
import { OntimeView, isOntimeEvent, isOntimeGroup } from 'ontime-types';
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import EmptyFill from '../../common/components/state/EmptyFill';
import EmptyPage from '../../common/components/state/EmptyPage';
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
import useFollowComponent from '../../common/hooks/useFollowComponent';
@@ -10,6 +11,7 @@ import { cx } from '../../common/utils/styleUtils';
import { throttle } from '../../common/utils/throttle';
import { getDefaultFormat } from '../../common/utils/time';
import { isTouchDevice } from '../../externals';
+import { useTranslation } from '../../translation/TranslationProvider';
import Loader from '../../views/common/loader/Loader';
import CustomFieldEditModal from './custom-field-edit-modal/CustomFieldEditModal';
import FollowButton from './follow-button/FollowButton';
@@ -35,7 +37,7 @@ export default function OperatorLoader() {
}
if (status === 'error') {
- return ;
+ return ;
}
return ;
@@ -43,6 +45,7 @@ export default function OperatorLoader() {
function Operator({ rundown, rundownMetadata, customFields, settings }: OperatorData) {
const selectedEventId = useSelectedEventId();
+ const { getLocalizedString } = useTranslation();
const { subscribe, mainSource, secondarySource, shouldEdit, hidePast, showStart } = useOperatorOptions();
const [showEditPrompt, setShowEditPrompt] = useState(false);
@@ -113,6 +116,7 @@ function Operator({ rundown, rundownMetadata, customFields, settings }: Operator
const operatorOptions = useMemo(() => getOperatorOptions(customFields, defaultFormat), [customFields, defaultFormat]);
const canEdit = shouldEdit && subscribe.length;
+ const hasEvents = rundown.order.length > 0;
return (
@@ -127,117 +131,121 @@ function Operator({ rundown, rundownMetadata, customFields, settings }: Operator
)}
-
- {rundown.order.map((entryId) => {
- const entry = rundown.entries[entryId];
- if (isOntimeEvent(entry)) {
- const { isPast, isLinkedToLoaded, isLoaded, totalGap } = rundownMetadata[entryId];
- // hide past events (if setting) and skipped events
- if ((hidePast && isPast) || entry.skip) {
- return null;
- }
+ {!hasEvents ? (
+
+ ) : (
+
+ {rundown.order.map((entryId) => {
+ const entry = rundown.entries[entryId];
+ if (isOntimeEvent(entry)) {
+ const { isPast, isLinkedToLoaded, isLoaded, totalGap } = rundownMetadata[entryId];
+ // hide past events (if setting) and skipped events
+ if ((hidePast && isPast) || entry.skip) {
+ return null;
+ }
- const { mainField, secondaryField, subscribedData } = getEventData(
- entry,
- mainSource,
- secondarySource,
- subscribe,
- customFields,
- );
+ const { mainField, secondaryField, subscribedData } = getEventData(
+ entry,
+ mainSource,
+ secondarySource,
+ subscribe,
+ customFields,
+ );
- return (
- undefined}
- />
- );
- }
-
- if (isOntimeGroup(entry)) {
- const { isPast } = rundownMetadata[entry.id];
-
- const isCurrentParent = selectedEventId ? rundownMetadata[selectedEventId]?.groupId === entry.id : false;
-
- if (hidePast && isPast && !isCurrentParent) {
- return null;
- }
-
- return (
-
- undefined}
/>
- {entry.entries.map((nestedEntryId) => {
- const nestedEntry = rundown.entries[nestedEntryId];
- if (!isOntimeEvent(nestedEntry)) {
- return null;
- }
+ );
+ }
- const { isPast, isLoaded, isLinkedToLoaded, totalGap } = rundownMetadata[nestedEntryId];
+ if (isOntimeGroup(entry)) {
+ const { isPast } = rundownMetadata[entry.id];
- // hide past events (if setting) and skipped events
- if ((hidePast && isPast) || nestedEntry.skip) {
- return null;
- }
+ const isCurrentParent = selectedEventId ? rundownMetadata[selectedEventId]?.groupId === entry.id : false;
- const { mainField, secondaryField, subscribedData } = getEventData(
- nestedEntry,
- mainSource,
- secondarySource,
- subscribe,
- customFields,
- );
+ if (hidePast && isPast && !isCurrentParent) {
+ return null;
+ }
- return (
- undefined}
- />
- );
- })}
-
- );
- }
- return null;
- })}
-
+ return (
+
+
+ {entry.entries.map((nestedEntryId) => {
+ const nestedEntry = rundown.entries[nestedEntryId];
+ if (!isOntimeEvent(nestedEntry)) {
+ return null;
+ }
+
+ const { isPast, isLoaded, isLinkedToLoaded, totalGap } = rundownMetadata[nestedEntryId];
+
+ // hide past events (if setting) and skipped events
+ if ((hidePast && isPast) || nestedEntry.skip) {
+ return null;
+ }
+
+ const { mainField, secondaryField, subscribedData } = getEventData(
+ nestedEntry,
+ mainSource,
+ secondarySource,
+ subscribe,
+ customFields,
+ );
+
+ return (
+ undefined}
+ />
+ );
+ })}
+
+ );
+ }
+ return null;
+ })}
+
+ )}
);
diff --git a/apps/client/src/features/rundown/RundownList.tsx b/apps/client/src/features/rundown/RundownList.tsx
index 5718a8ea1..86d80906b 100644
--- a/apps/client/src/features/rundown/RundownList.tsx
+++ b/apps/client/src/features/rundown/RundownList.tsx
@@ -1,19 +1,24 @@
import { memo } from 'react';
-import Empty from '../../common/components/state/Empty';
+import EmptyFill from '../../common/components/state/EmptyFill';
import { useRundownWithMetadata } from '../../common/hooks-query/useRundown';
import { useRundownEditor } from '../../common/hooks/useSocket';
+import { useTranslation } from '../../translation/TranslationProvider';
import Rundown from './Rundown';
export default memo(RundownList);
function RundownList() {
const { data, status, rundownMetadata } = useRundownWithMetadata();
const featureData = useRundownEditor();
+ const { getLocalizedString } = useTranslation();
- const isLoading = status !== 'success' || !data || !rundownMetadata;
+ // avoid showing the editable empty state before we know whether the rundown is actually empty
+ if (status === 'pending') {
+ return ;
+ }
- if (isLoading) {
- return ;
+ if (status === 'error') {
+ return ;
}
return (
diff --git a/apps/client/src/features/rundown/rundown-table/RundownTable.tsx b/apps/client/src/features/rundown/rundown-table/RundownTable.tsx
index b7a37bd0f..03f28a277 100644
--- a/apps/client/src/features/rundown/rundown-table/RundownTable.tsx
+++ b/apps/client/src/features/rundown/rundown-table/RundownTable.tsx
@@ -1,6 +1,5 @@
import { memo, useEffect, useMemo } from 'react';
-import EmptyPage from '../../../common/components/state/EmptyPage';
import { EntryActionsProvider } from '../../../common/context/EntryActionsContext';
import useCustomFields from '../../../common/hooks-query/useCustomFields';
import { useLoadedRundownSource } from '../../../common/hooks-query/useScopedRundown';
@@ -13,7 +12,7 @@ import { makeRundownColumns } from './makeRundownColumns';
export default memo(RundownTable);
function RundownTable() {
- const { data: customFields, status: customFieldStatus } = useCustomFields();
+ const { data: customFields } = useCustomFields();
const setPermissions = useCuesheetPermissions((state) => state.setPermissions);
const { editorMode } = useEditorFollowMode();
const source = useLoadedRundownSource();
@@ -32,16 +31,10 @@ function RundownTable() {
const columns = useMemo(() => makeRundownColumns(customFields), [customFields]);
- const isLoading = !customFields || customFieldStatus === 'pending';
-
return (
- {isLoading ? (
-
- ) : (
-
- )}
+
);
diff --git a/apps/client/src/views/backstage/Backstage.tsx b/apps/client/src/views/backstage/Backstage.tsx
index 229ba5213..85e18bf7c 100644
--- a/apps/client/src/views/backstage/Backstage.tsx
+++ b/apps/client/src/views/backstage/Backstage.tsx
@@ -35,7 +35,7 @@ export default function BackstageLoader() {
}
if (status === 'error') {
- return ;
+ return ;
}
return ;
diff --git a/apps/client/src/views/common/loader/Loader.module.scss b/apps/client/src/views/common/loader/Loader.module.scss
index 0d00acd37..62a937a14 100644
--- a/apps/client/src/views/common/loader/Loader.module.scss
+++ b/apps/client/src/views/common/loader/Loader.module.scss
@@ -7,7 +7,7 @@ $dot-spacing: 1.5rem;
display: grid;
place-items: center;
background-color: var(--background-color-override, $viewer-background-color);
- height: 100vh;
+ height: 100dvh;
}
.ellipsis {
@@ -21,26 +21,26 @@ $dot-spacing: 1.5rem;
height: $dot-size;
border-radius: 50%;
background-color: var(--accent-color-override, $ontime-color);
- animation-timing-function: cubic-bezier(0, 1, 1, 0);
+ animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
&:nth-child(1) {
left: $dot-size;
- animation: lds-ellipsis1 0.6s infinite;
+ animation: lds-ellipsis1 1s infinite;
}
&:nth-child(2) {
left: $dot-size;
- animation: lds-ellipsis2 0.6s infinite;
+ animation: lds-ellipsis2 1s infinite;
}
&:nth-child(3) {
left: calc($dot-size + $dot-spacing);
- animation: lds-ellipsis2 0.6s infinite;
+ animation: lds-ellipsis2 1s infinite;
}
&:nth-child(4) {
left: calc($dot-size + 2 * $dot-spacing);
- animation: lds-ellipsis3 0.6s infinite;
+ animation: lds-ellipsis3 1s infinite;
}
}
}
diff --git a/apps/client/src/views/countdown/Countdown.scss b/apps/client/src/views/countdown/Countdown.scss
index a5a175c8f..f36f03bca 100644
--- a/apps/client/src/views/countdown/Countdown.scss
+++ b/apps/client/src/views/countdown/Countdown.scss
@@ -80,21 +80,7 @@ $item-height: 3.5rem;
display: flex;
flex-direction: column;
align-items: center;
-
- button {
- margin-top: 1.75rem;
- }
- }
-
- .empty-state__content {
- max-width: none;
-
- span {
- max-width: none;
- white-space: nowrap;
- font-size: clamp(1.5rem, 4vw, 2.25rem);
- line-height: 1.1;
- }
+ gap: 1.5rem;
}
.list-container {
diff --git a/apps/client/src/views/countdown/Countdown.tsx b/apps/client/src/views/countdown/Countdown.tsx
index 40db224db..9db4cce6e 100644
--- a/apps/client/src/views/countdown/Countdown.tsx
+++ b/apps/client/src/views/countdown/Countdown.tsx
@@ -43,7 +43,7 @@ export default function CountdownLoader() {
}
if (status === 'error') {
- return ;
+ return ;
}
return ;
@@ -87,7 +87,7 @@ function Countdown({ customFields, rundownData, projectData, isMirrored, setting
{!hasEvents && (
-
+
)}
@@ -121,7 +121,7 @@ function CountdownContents({ candidates, rundownData, subscriptions, goToEditMod
if (subscriptions.length === 0) {
return (
-
+
Add
@@ -137,7 +137,7 @@ function CountdownContents({ candidates, rundownData, subscriptions, goToEditMod
if (subscribedEvents.length === 0) {
return (
-
+
Add
@@ -154,7 +154,7 @@ function CountdownContents({ candidates, rundownData, subscriptions, goToEditMod
if (eventsToShow.length === 0) {
return (
-
+
);
}
diff --git a/apps/client/src/views/cuesheet/CuesheetTableWrapper.tsx b/apps/client/src/views/cuesheet/CuesheetTableWrapper.tsx
index 595e221ba..048f8f1c6 100644
--- a/apps/client/src/views/cuesheet/CuesheetTableWrapper.tsx
+++ b/apps/client/src/views/cuesheet/CuesheetTableWrapper.tsx
@@ -2,7 +2,6 @@ import { MaybeString, ProjectRundown } from 'ontime-types';
import { memo, use, useMemo } from 'react';
import Select from '../../common/components/select/Select';
-import EmptyPage from '../../common/components/state/EmptyPage';
import { PresetContext } from '../../common/context/PresetContext';
import useCustomFields from '../../common/hooks-query/useCustomFields';
import type { RundownSource } from '../../common/hooks-query/useScopedRundown';
@@ -34,40 +33,34 @@ function CuesheetTableWrapper({
const preset = use(PresetContext);
const isCurrentRundown = source.rundownId !== null && source.rundownId === loadedRundownId;
const { cuesheetMode, setCuesheetMode } = useApplyCuesheetPolicy(preset, { canRunMode: isCurrentRundown });
- const { data: customFields, status: customFieldStatus } = useCustomFields();
+ const { data: customFields } = useCustomFields();
const columns = useMemo(
() => makeCuesheetColumns(customFields, cuesheetMode, preset),
[customFields, cuesheetMode, preset],
);
- const isLoading = !customFields || customFieldStatus === 'pending';
-
return (
- {isLoading ? (
-
- ) : (
-
-
- >
- }
- />
- )}
+
+
+ >
+ }
+ />
);
}
diff --git a/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.module.scss b/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.module.scss
index f95eb6468..b8873cca1 100644
--- a/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.module.scss
+++ b/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.module.scss
@@ -5,6 +5,10 @@ $table-header-font-size: calc(1rem - 2px);
@include rows.cuesheet-row-columns($table-header-font-size);
+.tableLoading {
+ grid-area: table;
+}
+
.cuesheet {
font-size: $table-font-size;
font-weight: 400;
diff --git a/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx b/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx
index 32c3f60ac..075f0b898 100644
--- a/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx
+++ b/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx
@@ -11,7 +11,7 @@ import {
TableVirtuosoHandle,
} from 'react-virtuoso';
-import EmptyPage from '../../../common/components/state/EmptyPage';
+import EmptyFill from '../../../common/components/state/EmptyFill';
import EmptyTableBody from '../../../common/components/state/EmptyTableBody';
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
import type { RundownSource } from '../../../common/hooks-query/useScopedRundown';
@@ -19,6 +19,7 @@ import type { ExtendedEntry } from '../../../common/utils/rundownMetadata';
import { usePersistedRundownOptions } from '../../../features/rundown/rundown.options';
import { useEventSelection } from '../../../features/rundown/useEventSelection';
import { AppMode } from '../../../ontimeConfig';
+import { useTranslation } from '../../../translation/TranslationProvider';
import { usePersistedCuesheetOptions } from '../cuesheet.options';
import { useCuesheetPermissions } from '../useTablePermissions';
import { CuesheetHeader, SortableCuesheetHeader } from './cuesheet-table-elements/CuesheetHeader';
@@ -64,6 +65,7 @@ export default function CuesheetTable({
}: CuesheetTableProps) {
const { flatRundown, status, selectedEventId } = source;
const { updateEntry, updateTimer, addEntry } = useEntryActionsContext();
+ const { getLocalizedString } = useTranslation();
const canCreateEntries = useCuesheetPermissions((state) => state.canCreateEntries) && cuesheetMode === AppMode.Edit;
const useOptions = tableRoot === 'editor' ? usePersistedRundownOptions : usePersistedCuesheetOptions;
@@ -228,10 +230,13 @@ export default function CuesheetTable({
});
}, [cuesheetMode, hideIndexColumn, table]);
- const isLoading = !flatRundown || status === 'pending';
+ // avoid showing the editable empty state before we know whether the rundown is actually empty
+ if (status === 'pending') {
+ return
;
+ }
- if (isLoading) {
- return
;
+ if (status === 'error') {
+ return
;
}
return (
diff --git a/apps/client/src/views/project-info/ProjectInfo.scss b/apps/client/src/views/project-info/ProjectInfo.scss
index 772757521..6901a36be 100644
--- a/apps/client/src/views/project-info/ProjectInfo.scss
+++ b/apps/client/src/views/project-info/ProjectInfo.scss
@@ -100,13 +100,17 @@ $content-width: min(100%, 1100px);
}
.info__image-container {
- flex: 0 0 min(128px, 25%);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex: 0 0 min(192px, 25%);
}
.info__image {
display: block;
- width: 100%;
+ max-width: 100%;
height: auto;
+ object-fit: contain;
}
.link.info__value {
diff --git a/apps/client/src/views/project-info/ProjectInfo.tsx b/apps/client/src/views/project-info/ProjectInfo.tsx
index 8e6c1f634..c14be9254 100644
--- a/apps/client/src/views/project-info/ProjectInfo.tsx
+++ b/apps/client/src/views/project-info/ProjectInfo.tsx
@@ -22,7 +22,7 @@ export default function ProjectInfoLoader() {
}
if (status === 'error') {
- return
;
+ return
;
}
return
;
@@ -73,12 +73,11 @@ function ProjectInfo({ projectData, isMirrored }: ProjectInfoData) {
)}
{projectData.custom.map((info, idx) => {
- const hasImage = Boolean(info.url);
return (
// oxlint-disable-next-line react/no-array-index-key - we only have the index to go of here
{info.title &&
{info.title}
}
- {hasImage ? (
+ {info.url ? (
{info.value &&
{info.value}
}
@@ -108,10 +107,6 @@ function InfoCard({ label, children }: InfoCardProps) {
);
}
-/**
- * Renders an user provided image, collapsing itself if the image fails to load
- * We remove the container along with the image to avoid leaving an empty gap in the card
- */
function InfoImage({ src }: { src: string }) {
const [hasError, setHasError] = useState(false);
diff --git a/apps/client/src/views/studio/Studio.tsx b/apps/client/src/views/studio/Studio.tsx
index e363dc7b8..53fd4014e 100644
--- a/apps/client/src/views/studio/Studio.tsx
+++ b/apps/client/src/views/studio/Studio.tsx
@@ -25,7 +25,7 @@ export default function StudioLoader() {
}
if (status === 'error') {
- return
;
+ return
;
}
return
;
diff --git a/apps/client/src/views/timeline/TimelinePage.tsx b/apps/client/src/views/timeline/TimelinePage.tsx
index f505bf06a..cff50cd06 100644
--- a/apps/client/src/views/timeline/TimelinePage.tsx
+++ b/apps/client/src/views/timeline/TimelinePage.tsx
@@ -1,6 +1,7 @@
import { OntimeView } from 'ontime-types';
import { useMemo } from 'react';
+import EmptyFill from '../../common/components/state/EmptyFill';
import EmptyPage from '../../common/components/state/EmptyPage';
import ViewLogo from '../../common/components/view-logo/ViewLogo';
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
@@ -29,7 +30,7 @@ export default function TimelinePageLoader() {
}
if (status === 'error') {
- return
;
+ return
;
}
return
;
@@ -73,7 +74,7 @@ function TimelinePage({ events, customFields, projectData, settings }: TimelineD
totalDuration={totalDuration}
/>
) : (
-
+
)}
);
diff --git a/apps/client/src/views/timer/Timer.tsx b/apps/client/src/views/timer/Timer.tsx
index 3ae139d26..f9708360b 100644
--- a/apps/client/src/views/timer/Timer.tsx
+++ b/apps/client/src/views/timer/Timer.tsx
@@ -43,7 +43,7 @@ export default function TimerLoader() {
}
if (status === 'error') {
- return
;
+ return
;
}
return
;