mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-11 02:13:48 +00:00
Unify and refine viewer empty/loading/error states
Every viewer page (Timer, Countdown, Backstage, Studio, Timeline, ProjectInfo, Operator) re-implemented the same data-loader flow by hand, each with a hard-coded, unlocalized error string and drifting empty-state styling. This centralizes that flow and elevates the shared presentation. - Add ViewDataBoundary: a single component that renders the shared loading, error and optional no-data states from a QueryStatus, so every view handles them the same obvious way. Refactor all seven view loaders through it. - Localize the error state via new common.fetch_error / common.fetch_error_hint keys (en + de/es/fr/it/pt), replacing the duplicated English literal with a two-line "Something went wrong / Please refresh the page" message. - Elevate the shared Empty component: legible viewer-secondary color (was 10% white), responsive title sizing, softened illustration, optional supporting subtitle line, and a reduced-motion-safe fade-in. - Unify loading: Cuesheet and the rundown table now use the animated Loader like every other view instead of an untranslated "Loading..." page. - Tidy up: EmptyTableBody uses Empty's text prop, drop Countdown's now-redundant size override, and remove a stray semicolon rendered in the ProjectInfo empty state. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vq5XLTSQhU9ckZmCSW2SEr
This commit is contained in:
@@ -1,20 +1,49 @@
|
||||
@use '@/theme/viewerDefs' as *;
|
||||
|
||||
.emptyContainer {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: $white-10;
|
||||
color: $viewer-secondary-color;
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
animation: empty-fade-in $viewer-transition-time ease both;
|
||||
}
|
||||
|
||||
.empty {
|
||||
display: block;
|
||||
width: min(100%, 24rem);
|
||||
width: min(100%, 16rem);
|
||||
margin-inline: auto;
|
||||
opacity: 0.8;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.text {
|
||||
display: block;
|
||||
margin-inline: auto;
|
||||
margin-top: min(2vh, 16px);
|
||||
font-weight: 600;
|
||||
font-size: 2em;
|
||||
font-size: $title-font-size;
|
||||
max-width: min(100%, 600px);
|
||||
}
|
||||
|
||||
.secondary {
|
||||
display: block;
|
||||
margin-inline: auto;
|
||||
margin-top: 0.5rem;
|
||||
font-weight: 400;
|
||||
font-size: $base-font-size;
|
||||
color: $viewer-label-color;
|
||||
max-width: min(100%, 600px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes empty-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(4px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,17 @@ import style from './Empty.module.scss';
|
||||
|
||||
interface EmptyProps {
|
||||
text?: string;
|
||||
secondary?: string;
|
||||
injectedStyles?: CSSProperties;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function Empty({ text, className, injectedStyles }: EmptyProps) {
|
||||
export default function Empty({ text, secondary, className, injectedStyles }: EmptyProps) {
|
||||
return (
|
||||
<div className={cx([style.emptyContainer, className])} style={injectedStyles}>
|
||||
<EmptyImage className={style.empty} />
|
||||
{text && <span className={style.text}>{text}</span>}
|
||||
{secondary && <span className={style.secondary}>{secondary}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,13 +6,14 @@ import style from './EmptyPage.module.scss';
|
||||
|
||||
interface EmptyPageProps {
|
||||
text?: string;
|
||||
secondary?: string;
|
||||
injectedStyles?: CSSProperties;
|
||||
}
|
||||
|
||||
export default function EmptyPage({ text, injectedStyles }: EmptyPageProps) {
|
||||
export default function EmptyPage({ text, secondary, injectedStyles }: EmptyPageProps) {
|
||||
return (
|
||||
<div className={style.page}>
|
||||
<Empty text={text} injectedStyles={injectedStyles} />
|
||||
<Empty text={text} secondary={secondary} injectedStyles={injectedStyles} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,9 +15,4 @@
|
||||
gap: 1rem;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-weight: 600;
|
||||
font-size: 2em;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,7 @@ export default function EmptyTableBody({ handleAddNew }: EmptyTableBodyProps) {
|
||||
<tbody className={style.emptyContainer}>
|
||||
<tr>
|
||||
<td colSpan={99} className={style.emptyCell}>
|
||||
<Empty injectedStyles={{ marginTop: '5vh' }} />
|
||||
<span className={style.text}>{text}</span>
|
||||
<Empty text={text} injectedStyles={{ marginTop: '5vh' }} />
|
||||
{handleAddNew && (
|
||||
<div className={style.inline}>
|
||||
<Button onClick={() => handleAddNew(SupportedEntry.Event)} variant='primary' size='large'>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { OntimeView, isOntimeEvent, isOntimeGroup } from 'ontime-types';
|
||||
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import EmptyPage from '../../common/components/state/EmptyPage';
|
||||
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
||||
import useFollowComponent from '../../common/hooks/useFollowComponent';
|
||||
import { useSelectedEventId } from '../../common/hooks/useSocket';
|
||||
@@ -10,7 +9,7 @@ import { cx } from '../../common/utils/styleUtils';
|
||||
import { throttle } from '../../common/utils/throttle';
|
||||
import { getDefaultFormat } from '../../common/utils/time';
|
||||
import { isTouchDevice } from '../../externals';
|
||||
import Loader from '../../views/common/loader/Loader';
|
||||
import ViewDataBoundary from '../../views/common/view-data-boundary/ViewDataBoundary';
|
||||
import CustomFieldEditModal from './custom-field-edit-modal/CustomFieldEditModal';
|
||||
import FollowButton from './follow-button/FollowButton';
|
||||
import OperatorEvent from './operator-event/OperatorEvent';
|
||||
@@ -30,15 +29,11 @@ export default function OperatorLoader() {
|
||||
|
||||
useWindowTitle('Operator');
|
||||
|
||||
if (status === 'pending') {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
if (status === 'error') {
|
||||
return <EmptyPage text='There was an error fetching data, please refresh the page.' />;
|
||||
}
|
||||
|
||||
return <Operator {...data} />;
|
||||
return (
|
||||
<ViewDataBoundary status={status}>
|
||||
<Operator {...data} />
|
||||
</ViewDataBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
function Operator({ rundown, rundownMetadata, customFields, settings }: OperatorData) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
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';
|
||||
import { useEntryActions } from '../../../common/hooks/useEntryAction';
|
||||
import Loader from '../../../views/common/loader/Loader';
|
||||
import CuesheetDnd from '../../../views/cuesheet/cuesheet-dnd/CuesheetDnd';
|
||||
import CuesheetTable from '../../../views/cuesheet/cuesheet-table/CuesheetTable';
|
||||
import { useCuesheetPermissions } from '../../../views/cuesheet/useTablePermissions';
|
||||
@@ -32,13 +32,15 @@ function RundownTable() {
|
||||
|
||||
const columns = useMemo(() => makeRundownColumns(customFields), [customFields]);
|
||||
|
||||
// TODO: adopt the shared ViewDataBoundary (views/common/view-data-boundary) once this
|
||||
// table exposes a single query status instead of the ad-hoc isLoading check
|
||||
const isLoading = !customFields || customFieldStatus === 'pending';
|
||||
|
||||
return (
|
||||
<EntryActionsProvider actions={actions}>
|
||||
<CuesheetDnd columns={columns} tableRoot='editor'>
|
||||
{isLoading ? (
|
||||
<EmptyPage text='Loading...' />
|
||||
<Loader />
|
||||
) : (
|
||||
<CuesheetTable columns={columns} source={source} cuesheetMode={editorMode} tableRoot='editor' />
|
||||
)}
|
||||
|
||||
@@ -16,6 +16,8 @@ export const langDe: TranslationObject = {
|
||||
'common.started_at': 'Gestartet am',
|
||||
'common.time_now': 'Aktuelle Zeit',
|
||||
'common.no_data': 'Keine Daten',
|
||||
'common.fetch_error': 'Etwas ist schiefgelaufen',
|
||||
'common.fetch_error_hint': 'Bitte aktualisieren Sie die Seite',
|
||||
'countdown.ended': 'Veranstaltung endete um',
|
||||
'countdown.running': 'Veranstaltung läuft',
|
||||
'countdown.group_running': 'Veranstaltung in Gruppe läuft',
|
||||
|
||||
@@ -16,6 +16,8 @@ export const langEs: TranslationObject = {
|
||||
'common.started_at': 'Iniciado en',
|
||||
'common.time_now': 'Ahora',
|
||||
'common.no_data': 'Sin datos',
|
||||
'common.fetch_error': 'Algo salió mal',
|
||||
'common.fetch_error_hint': 'Actualiza la página',
|
||||
'countdown.ended': 'Evento finalizado a las',
|
||||
'countdown.running': 'Evento en curso',
|
||||
'countdown.group_running': 'Evento en grupo en curso',
|
||||
|
||||
@@ -16,6 +16,8 @@ export const langFr: TranslationObject = {
|
||||
'common.started_at': 'Commencé à',
|
||||
'common.time_now': 'Heure',
|
||||
'common.no_data': 'Aucune donnée',
|
||||
'common.fetch_error': 'Une erreur est survenue',
|
||||
'common.fetch_error_hint': 'Veuillez actualiser la page',
|
||||
'countdown.ended': 'Évènement terminé à',
|
||||
'countdown.running': 'Évènement en cours',
|
||||
'countdown.group_running': 'Évènement du groupe en cours',
|
||||
|
||||
@@ -16,6 +16,8 @@ export const langIt: TranslationObject = {
|
||||
'common.started_at': 'Iniziato Alle',
|
||||
'common.time_now': 'Ora attuale',
|
||||
'common.no_data': 'Nessun dato disponibile',
|
||||
'common.fetch_error': 'Qualcosa è andato storto',
|
||||
'common.fetch_error_hint': 'Aggiorna la pagina',
|
||||
'countdown.ended': 'Evento finito alle',
|
||||
'countdown.running': 'Evento in corso',
|
||||
'countdown.group_running': 'Evento nel gruppo in corso',
|
||||
|
||||
@@ -16,6 +16,8 @@ export const langPt: TranslationObject = {
|
||||
'common.started_at': 'Iniciado em',
|
||||
'common.time_now': 'Hora atual',
|
||||
'common.no_data': 'Sem dados',
|
||||
'common.fetch_error': 'Algo correu mal',
|
||||
'common.fetch_error_hint': 'Atualize a página',
|
||||
'countdown.ended': 'Evento encerrado às',
|
||||
'countdown.running': 'Evento em andamento',
|
||||
'countdown.group_running': 'Evento em grupo em andamento',
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
import ProgressBar from '../../common/components/progress-bar/ProgressBar';
|
||||
import QRCode from '../../common/components/qr-code/QrCode';
|
||||
import Empty from '../../common/components/state/Empty';
|
||||
import EmptyPage from '../../common/components/state/EmptyPage';
|
||||
import TitleCard from '../../common/components/title-card/TitleCard';
|
||||
import ViewLogo from '../../common/components/view-logo/ViewLogo';
|
||||
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
||||
@@ -16,9 +15,9 @@ import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
||||
import { cx, timerPlaceholderMin } from '../../common/utils/styleUtils';
|
||||
import { formatTime, getDefaultFormat } from '../../common/utils/time';
|
||||
import { useTranslation } from '../../translation/TranslationProvider';
|
||||
import Loader from '../common/loader/Loader';
|
||||
import ScheduleExport from '../common/schedule/ScheduleExport';
|
||||
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||
import ViewDataBoundary from '../common/view-data-boundary/ViewDataBoundary';
|
||||
import { getBackstageOptions, useBackstageOptions } from './backstage.options';
|
||||
import { getCardData, getIsPendingStart, getShowProgressBar, isOvertime } from './backstage.utils';
|
||||
import { BackstageData, useBackstageData } from './useBackstageData';
|
||||
@@ -30,15 +29,11 @@ export default function BackstageLoader() {
|
||||
|
||||
useWindowTitle('Backstage');
|
||||
|
||||
if (status === 'pending') {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
if (status === 'error') {
|
||||
return <EmptyPage text='There was an error fetching data, please refresh the page.' />;
|
||||
}
|
||||
|
||||
return <Backstage {...data} />;
|
||||
return (
|
||||
<ViewDataBoundary status={status}>
|
||||
<Backstage {...data} />
|
||||
</ViewDataBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
function Backstage({ events, customFields, projectData, isMirrored, settings }: BackstageData) {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { QueryStatus } from '@tanstack/react-query';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import EmptyPage from '../../../common/components/state/EmptyPage';
|
||||
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||
import Loader from '../loader/Loader';
|
||||
|
||||
interface ViewDataBoundaryProps {
|
||||
status: QueryStatus;
|
||||
/** render a full-page "no data" state instead of the children */
|
||||
isEmpty?: boolean;
|
||||
/** overrides the default "no data" message when isEmpty is true */
|
||||
emptyText?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard gate for a viewer's data-loading flow.
|
||||
* Renders the shared loading, error and (optional) empty states so every view
|
||||
* handles them the same way. Children are only rendered once the data is ready.
|
||||
*/
|
||||
export default function ViewDataBoundary({
|
||||
status,
|
||||
isEmpty,
|
||||
emptyText,
|
||||
children,
|
||||
}: PropsWithChildren<ViewDataBoundaryProps>) {
|
||||
const { getLocalizedString } = useTranslation();
|
||||
|
||||
if (status === 'pending') {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
if (status === 'error') {
|
||||
return (
|
||||
<EmptyPage
|
||||
text={getLocalizedString('common.fetch_error')}
|
||||
secondary={getLocalizedString('common.fetch_error_hint')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isEmpty) {
|
||||
return <EmptyPage text={emptyText ?? getLocalizedString('common.no_data')} />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -86,17 +86,6 @@ $item-height: 3.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.list-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -14,7 +14,6 @@ import { IoAdd } from 'react-icons/io5';
|
||||
|
||||
import Button from '../../common/components/buttons/Button';
|
||||
import Empty from '../../common/components/state/Empty';
|
||||
import EmptyPage from '../../common/components/state/EmptyPage';
|
||||
import ViewLogo from '../../common/components/view-logo/ViewLogo';
|
||||
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
||||
import { useAutoTickingClock } from '../../common/hooks/useAutoTickingClock';
|
||||
@@ -22,8 +21,8 @@ import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
||||
import { ExtendedEntry } from '../../common/utils/rundownMetadata';
|
||||
import { formatTime, getDefaultFormat } from '../../common/utils/time';
|
||||
import { useTranslation } from '../../translation/TranslationProvider';
|
||||
import Loader from '../common/loader/Loader';
|
||||
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||
import ViewDataBoundary from '../common/view-data-boundary/ViewDataBoundary';
|
||||
import { getCountdownOptions, useCountdownOptions } from './countdown.options';
|
||||
import { getOrderedSubscriptions, resolveSubscriptionTarget } from './countdown.utils';
|
||||
import CountdownSelect from './CountdownSelect';
|
||||
@@ -38,15 +37,11 @@ export default function CountdownLoader() {
|
||||
|
||||
useWindowTitle('Countdown');
|
||||
|
||||
if (status === 'pending') {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
if (status === 'error') {
|
||||
return <EmptyPage text='There was an error fetching data, please refresh the page.' />;
|
||||
}
|
||||
|
||||
return <Countdown {...data} />;
|
||||
return (
|
||||
<ViewDataBoundary status={status}>
|
||||
<Countdown {...data} />
|
||||
</ViewDataBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
function Countdown({ customFields, rundownData, projectData, isMirrored, settings }: CountdownData) {
|
||||
@@ -87,7 +82,7 @@ function Countdown({ customFields, rundownData, projectData, isMirrored, setting
|
||||
|
||||
{!hasEvents && (
|
||||
<div className='empty-state'>
|
||||
<Empty text={getLocalizedString('common.no_data')} className='empty-state__content' />
|
||||
<Empty text={getLocalizedString('common.no_data')} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -121,7 +116,7 @@ function CountdownContents({ candidates, rundownData, subscriptions, goToEditMod
|
||||
if (subscriptions.length === 0) {
|
||||
return (
|
||||
<div className='empty-state'>
|
||||
<Empty text={getLocalizedString('countdown.select_event')} className='empty-state__content' />
|
||||
<Empty text={getLocalizedString('countdown.select_event')} />
|
||||
<Button variant='primary' size='xlarge' onClick={goToEditMode}>
|
||||
<IoAdd /> Add
|
||||
</Button>
|
||||
@@ -137,7 +132,7 @@ function CountdownContents({ candidates, rundownData, subscriptions, goToEditMod
|
||||
if (subscribedEvents.length === 0) {
|
||||
return (
|
||||
<div className='empty-state'>
|
||||
<Empty text={getLocalizedString('countdown.select_event')} className='empty-state__content' />
|
||||
<Empty text={getLocalizedString('countdown.select_event')} />
|
||||
<Button variant='primary' size='xlarge' onClick={goToEditMode}>
|
||||
<IoAdd /> Add
|
||||
</Button>
|
||||
@@ -154,7 +149,7 @@ function CountdownContents({ candidates, rundownData, subscriptions, goToEditMod
|
||||
if (eventsToShow.length === 0) {
|
||||
return (
|
||||
<div className='empty-state'>
|
||||
<Empty text={getLocalizedString('countdown.all_have_finished')} className='empty-state__content' />
|
||||
<Empty text={getLocalizedString('countdown.all_have_finished')} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ 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';
|
||||
import { AppMode } from '../../ontimeConfig';
|
||||
import Loader from '../common/loader/Loader';
|
||||
import CuesheetDnd from './cuesheet-dnd/CuesheetDnd';
|
||||
import { makeCuesheetColumns } from './cuesheet-table/cuesheet-table-elements/cuesheetColsFactory';
|
||||
import CuesheetTable from './cuesheet-table/CuesheetTable';
|
||||
@@ -41,12 +41,14 @@ function CuesheetTableWrapper({
|
||||
[customFields, cuesheetMode, preset],
|
||||
);
|
||||
|
||||
// TODO: adopt the shared ViewDataBoundary (views/common/view-data-boundary) once this
|
||||
// table exposes a single query status instead of the ad-hoc isLoading check
|
||||
const isLoading = !customFields || customFieldStatus === 'pending';
|
||||
|
||||
return (
|
||||
<CuesheetDnd columns={columns}>
|
||||
{isLoading ? (
|
||||
<EmptyPage text='Loading...' />
|
||||
<Loader />
|
||||
) : (
|
||||
<CuesheetTable
|
||||
columns={columns}
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
TableVirtuosoHandle,
|
||||
} from 'react-virtuoso';
|
||||
|
||||
import EmptyPage from '../../../common/components/state/EmptyPage';
|
||||
import EmptyTableBody from '../../../common/components/state/EmptyTableBody';
|
||||
import { useEntryActionsContext } from '../../../common/context/EntryActionsContext';
|
||||
import type { RundownSource } from '../../../common/hooks-query/useScopedRundown';
|
||||
@@ -19,6 +18,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 Loader from '../../common/loader/Loader';
|
||||
import { usePersistedCuesheetOptions } from '../cuesheet.options';
|
||||
import { useCuesheetPermissions } from '../useTablePermissions';
|
||||
import { CuesheetHeader, SortableCuesheetHeader } from './cuesheet-table-elements/CuesheetHeader';
|
||||
@@ -231,7 +231,7 @@ export default function CuesheetTable({
|
||||
const isLoading = !flatRundown || status === 'pending';
|
||||
|
||||
if (isLoading) {
|
||||
return <EmptyPage text='Loading...' />;
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,7 +6,7 @@ import ViewLogo from '../../common/components/view-logo/ViewLogo';
|
||||
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
||||
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
||||
import { useTranslation } from '../../translation/TranslationProvider';
|
||||
import Loader from '../common/loader/Loader';
|
||||
import ViewDataBoundary from '../common/view-data-boundary/ViewDataBoundary';
|
||||
import { ProjectInfoData, useProjectInfoData } from './useProjectInfoData';
|
||||
|
||||
import './ProjectInfo.scss';
|
||||
@@ -16,15 +16,11 @@ export default function ProjectInfoLoader() {
|
||||
|
||||
useWindowTitle('Project info');
|
||||
|
||||
if (status === 'pending') {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
if (status === 'error') {
|
||||
return <EmptyPage text='There was an error fetching data, please refresh the page.' />;
|
||||
}
|
||||
|
||||
return <ProjectInfo {...data} />;
|
||||
return (
|
||||
<ViewDataBoundary status={status}>
|
||||
<ProjectInfo {...data} />
|
||||
</ViewDataBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
function ProjectInfo({ projectData, isMirrored }: ProjectInfoData) {
|
||||
@@ -41,7 +37,7 @@ function ProjectInfo({ projectData, isMirrored }: ProjectInfoData) {
|
||||
return (
|
||||
<>
|
||||
<ViewParamsEditor target={OntimeView.ProjectInfo} viewOptions={[]} />
|
||||
<EmptyPage text={getLocalizedString('common.no_data')} />;
|
||||
<EmptyPage text={getLocalizedString('common.no_data')} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { OntimeView } from 'ontime-types';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import EmptyPage from '../../common/components/state/EmptyPage';
|
||||
import ViewLogo from '../../common/components/view-logo/ViewLogo';
|
||||
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
||||
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
||||
import { cx } from '../../common/utils/styleUtils';
|
||||
import { getDefaultFormat } from '../../common/utils/time';
|
||||
import Loader from '../common/loader/Loader';
|
||||
import ViewDataBoundary from '../common/view-data-boundary/ViewDataBoundary';
|
||||
import { getStudioOptions, useStudioOptions } from './studio.options';
|
||||
import StudioClock from './StudioClock';
|
||||
import StudioTimers from './StudioTimers';
|
||||
@@ -20,15 +19,11 @@ export default function StudioLoader() {
|
||||
|
||||
useWindowTitle('Studio Clock');
|
||||
|
||||
if (status === 'pending') {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
if (status === 'error') {
|
||||
return <EmptyPage text='There was an error fetching data, please refresh the page.' />;
|
||||
}
|
||||
|
||||
return <Studio {...data} />;
|
||||
return (
|
||||
<ViewDataBoundary status={status}>
|
||||
<Studio {...data} />
|
||||
</ViewDataBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
function Studio({ customFields, projectData, isMirrored, settings, viewSettings }: StudioData) {
|
||||
|
||||
@@ -9,8 +9,8 @@ import { useSelectedEventId } from '../../common/hooks/useSocket';
|
||||
import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
||||
import { formatTime, getDefaultFormat } from '../../common/utils/time';
|
||||
import { useTranslation } from '../../translation/TranslationProvider';
|
||||
import Loader from '../common/loader/Loader';
|
||||
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||
import ViewDataBoundary from '../common/view-data-boundary/ViewDataBoundary';
|
||||
import Timeline from './Timeline';
|
||||
import { getTimelineOptions, useTimelineOptions } from './timeline.options';
|
||||
import { getUpcomingEvents, useScopedRundown } from './timeline.utils';
|
||||
@@ -24,15 +24,11 @@ export default function TimelinePageLoader() {
|
||||
|
||||
useWindowTitle('Timeline');
|
||||
|
||||
if (status === 'pending') {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
if (status === 'error') {
|
||||
return <EmptyPage text='There was an error fetching data, please refresh the page.' />;
|
||||
}
|
||||
|
||||
return <TimelinePage {...data} />;
|
||||
return (
|
||||
<ViewDataBoundary status={status}>
|
||||
<TimelinePage {...data} />
|
||||
</ViewDataBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
function TimelinePage({ events, customFields, projectData, settings }: TimelineData) {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useMemo } from 'react';
|
||||
|
||||
import { FitText } from '../../common/components/fit-text/FitText';
|
||||
import MultiPartProgressBar from '../../common/components/multi-part-progress-bar/MultiPartProgressBar';
|
||||
import EmptyPage from '../../common/components/state/EmptyPage';
|
||||
import TitleCard from '../../common/components/title-card/TitleCard';
|
||||
import ViewLogo from '../../common/components/view-logo/ViewLogo';
|
||||
import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor';
|
||||
@@ -13,8 +12,8 @@ import { useWindowTitle } from '../../common/hooks/useWindowTitle';
|
||||
import { cx } from '../../common/utils/styleUtils';
|
||||
import { formatTime, getDefaultFormat } from '../../common/utils/time';
|
||||
import { useTranslation } from '../../translation/TranslationProvider';
|
||||
import Loader from '../common/loader/Loader';
|
||||
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||
import ViewDataBoundary from '../common/view-data-boundary/ViewDataBoundary';
|
||||
import { getFormattedTimer, getTimerByType } from '../common/viewUtils';
|
||||
import { getTimerColour } from '../utils/presentation.utils';
|
||||
import { getTimerOptions, useTimerOptions } from './timer.options';
|
||||
@@ -38,15 +37,11 @@ export default function TimerLoader() {
|
||||
|
||||
useWindowTitle('Timer');
|
||||
|
||||
if (status === 'pending') {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
if (status === 'error') {
|
||||
return <EmptyPage text='There was an error fetching data, please refresh the page.' />;
|
||||
}
|
||||
|
||||
return <Timer {...data} />;
|
||||
return (
|
||||
<ViewDataBoundary status={status}>
|
||||
<Timer {...data} />
|
||||
</ViewDataBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
function Timer({ customFields, projectData, isMirrored, settings, viewSettings, entries }: TimerData) {
|
||||
|
||||
@@ -14,6 +14,8 @@ export const langEn = {
|
||||
'common.started_at': 'Started At',
|
||||
'common.time_now': 'Time now',
|
||||
'common.no_data': 'No data',
|
||||
'common.fetch_error': 'Something went wrong',
|
||||
'common.fetch_error_hint': 'Please refresh the page',
|
||||
'countdown.ended': 'Event ended at',
|
||||
'countdown.running': 'Event running',
|
||||
'countdown.group_running': 'Event in group running',
|
||||
|
||||
Reference in New Issue
Block a user