Feature: quick start (#369)

* feat: quick start modal

* refactor: end message is part of view settings

* refactor: small tweaks and type improvements
This commit is contained in:
Carlos Valente
2023-04-29 21:20:25 +02:00
committed by GitHub
parent 33b496f98e
commit 38654f8981
41 changed files with 463 additions and 282 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
export const STATIC_PORT = 4001;
// REST stuff
export const EVENTDATA_TABLE = ['eventdata'];
export const EVENT_DATA = ['eventdata'];
export const ALIASES = ['aliases'];
export const USERFIELDS = ['userFields'];
export const RUNDOWN_TABLE_KEY = 'rundown';
+5 -1
View File
@@ -1,5 +1,5 @@
import axios from 'axios';
import { Alias, OSCSettings, Settings, UserFields, ViewSettings } from 'ontime-types';
import { Alias, EventData, OSCSettings, Settings, UserFields, ViewSettings } from 'ontime-types';
import { apiRepoLatest } from '../../externals';
import { InfoType } from '../models/Info';
@@ -169,3 +169,7 @@ export async function getLatestVersion(): Promise<HasUpdate> {
version: res.data.tag_name as string,
};
}
export async function postNew(initialData: Partial<EventData>) {
return axios.post(`${ontimeURL}/new`, initialData);
}
@@ -19,7 +19,6 @@ export const AutoTextArea = (props: AutoTextAreaProps) => {
};
}, []);
return (
<Textarea
overflow='hidden'
@@ -27,7 +26,7 @@ export const AutoTextArea = (props: AutoTextAreaProps) => {
resize='none'
ref={ref}
transition='height none'
variant={isDark ? 'ontime-filled' : 'ontime-filled-onlight'}
variant={isDark ? 'ontime-filled' : 'ontime-filled-on-light'}
{...rest}
/>
);
@@ -1,13 +1,13 @@
import { useQuery } from '@tanstack/react-query';
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
import { EVENTDATA_TABLE } from '../api/apiConstants';
import { EVENT_DATA } from '../api/apiConstants';
import { fetchEventData } from '../api/eventDataApi';
import { eventDataPlaceholder } from '../models/EventData';
export default function useEventData() {
const { data, status, isError, refetch } = useQuery({
queryKey: EVENTDATA_TABLE,
queryKey: EVENT_DATA,
queryFn: fetchEventData,
placeholderData: eventDataPlaceholder,
retry: 5,
@@ -6,5 +6,4 @@ export const eventDataPlaceholder: EventData = {
publicInfo: '',
backstageUrl: '',
backstageInfo: '',
endMessage: '',
};
@@ -2,4 +2,5 @@ import { ViewSettings } from 'ontime-types';
export const viewsSettingsPlaceholder: ViewSettings = {
overrideStyles: false,
endMessage: '',
};
@@ -7,6 +7,7 @@ import MenuBar from '../menu/MenuBar';
import AboutModal from '../modals/about-modal/AboutModal';
import IntegrationModal from '../modals/integration-modal/IntegrationModal';
import ModalManager from '../modals/ModalManager';
import QuickStart from '../modals/quick-start/QuickStart';
import styles from './Editor.module.scss';
@@ -25,6 +26,7 @@ export default function Editor() {
onClose: onIntegrationModalClose,
} = useDisclosure();
const { isOpen: isAboutModalOpen, onOpen: onAboutModalOpen, onClose: onAboutModalClose } = useDisclosure();
const { isOpen: isQuickStartOpen, onOpen: onQuickStartOpen, onClose: onQuickStartClose } = useDisclosure();
// Set window title
useEffect(() => {
@@ -34,6 +36,7 @@ export default function Editor() {
return (
<>
<ErrorBoundary>
<QuickStart onClose={onQuickStartClose} isOpen={isQuickStartOpen} />
<UploadModal onClose={onUploadModalClose} isOpen={isUploadModalOpen} />
<IntegrationModal onClose={onIntegrationModalClose} isOpen={isIntegrationModalOpen} />
<AboutModal onClose={onAboutModalClose} isOpen={isAboutModalOpen} />
@@ -52,6 +55,8 @@ export default function Editor() {
onIntegrationOpen={onIntegrationModalOpen}
isAboutOpen={isAboutModalOpen}
onAboutOpen={onAboutModalOpen}
isQuickStartOpen={isQuickStartOpen}
onQuickStartOpen={onQuickStartOpen}
/>
</ErrorBoundary>
</Box>
+17 -4
View File
@@ -1,12 +1,13 @@
import { useCallback, useEffect } from 'react';
import { VStack } from '@chakra-ui/react';
import { FiSave } from '@react-icons/all-files/fi/FiSave';
import { FiUpload } from '@react-icons/all-files/fi/FiUpload';
import { IoColorWand } from '@react-icons/all-files/io5/IoColorWand';
import { IoExtensionPuzzle } from '@react-icons/all-files/io5/IoExtensionPuzzle';
import { IoExtensionPuzzleOutline } from '@react-icons/all-files/io5/IoExtensionPuzzleOutline';
import { IoHelp } from '@react-icons/all-files/io5/IoHelp';
import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
import { IoPushOutline } from '@react-icons/all-files/io5/IoPushOutline';
import { IoSaveOutline } from '@react-icons/all-files/io5/IoSaveOutline';
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
import { downloadRundown } from '../../common/api/ontimeApi';
@@ -27,6 +28,8 @@ interface MenuBarProps {
onIntegrationOpen: () => void;
isAboutOpen: boolean;
onAboutOpen: () => void;
isQuickStartOpen: boolean;
onQuickStartOpen: () => void;
}
const buttonStyle = {
@@ -52,6 +55,8 @@ export default function MenuBar(props: MenuBarProps) {
onIntegrationOpen,
isAboutOpen,
onAboutOpen,
isQuickStartOpen,
onQuickStartOpen,
} = props;
const { isElectron, sendToElectron } = useElectronEvent();
@@ -102,7 +107,15 @@ export default function MenuBar(props: MenuBarProps) {
<div className={style.gap} />
<TooltipActionBtn
{...buttonStyle}
icon={<FiUpload />}
icon={<IoColorWand />}
className={isQuickStartOpen ? style.open : ''}
clickHandler={onQuickStartOpen}
tooltip='Quick start'
aria-label='Quick start'
/>
<TooltipActionBtn
{...buttonStyle}
icon={<IoPushOutline />}
className={isUploadOpen ? style.open : ''}
clickHandler={onUploadOpen}
tooltip='Upload showfile'
@@ -110,7 +123,7 @@ export default function MenuBar(props: MenuBarProps) {
/>
<TooltipActionBtn
{...buttonStyle}
icon={<FiSave />}
icon={<IoSaveOutline />}
clickHandler={downloadRundown}
tooltip='Export showfile'
aria-label='Export showfile'
@@ -32,7 +32,6 @@ export default function SettingsModal() {
publicInfo: data.publicInfo,
backstageUrl: data.backstageUrl,
backstageInfo: data.backstageInfo,
endMessage: data.endMessage,
});
}, [changed, data]);
@@ -168,23 +167,6 @@ export default function SettingsModal() {
onChange={(event) => handleChange('backstageInfo', event.target.value)}
/>
</div>
<div className={style.spacedEntry}>
<FormLabel htmlFor='endMessage'>
End Message
<span className={style.labelNote}>
<br />
Shown on presenter view when time is finished
</span>
</FormLabel>
<Input
{...inputProps}
maxLength={30}
name='endMessage'
placeholder='Empty message shows elapsed time'
value={formData.endMessage}
onChange={(event) => handleChange('endMessage', event.target.value)}
/>
</div>
</div>
<SubmitContainer revert={revert} submitting={submitting} changed={changed} status={status} />
</form>
@@ -1,6 +1,8 @@
@use '../../theme/v2Styles' as *;
@use '../../theme/ontimeColours' as *;
$el-padding-with-compensation: 24px; // 16 + 8
@mixin modal-link {
color: $blue-500;
transition-property: color;
@@ -14,7 +16,7 @@
.headerNotes {
font-size: $text-body-size;
width: 100%;
padding: 0 $section-spacing;
padding: 0 $el-padding-with-compensation;
color: $modal-note-color;
margin-bottom: $section-spacing;
@@ -24,6 +26,12 @@
}
}
.footerNotes {
font-size: $inner-section-text-size;
padding: 0 $el-padding-with-compensation;
color: $modal-note-color;
}
.divider {
margin: $element-spacing 0;
border: 0;
@@ -31,6 +39,7 @@
}
.sectionContainer {
padding: 8px 16px;
display: flex;
flex-direction: column;
height: 100%
@@ -60,7 +69,7 @@
.sectionTitle {
font-size: $inner-section-text-size;
display: block;
width: 100%;
&.main {
font-weight: 600;
}
@@ -83,9 +92,6 @@
.buttonSection {
margin-top: $section-spacing;
padding-top: $section-spacing;
padding-left: -24px;
border-top: 1px solid $gray-100;
display: flex;
gap: $section-spacing;
}
@@ -96,7 +102,7 @@
.shiftRight {
align-self: flex-end;
margin-right: 8px;
margin-right: $element-spacing;
}
.showPointer {
@@ -109,7 +115,7 @@
}
.padBottom {
padding-bottom: 8px;
padding-bottom: $element-spacing;
}
.logo {
@@ -117,17 +123,22 @@
height: 48px;
display: inline-block;
vertical-align: text-bottom;
margin-right: 16px;
margin-right: $section-spacing;
}
.test {
padding-top: 24px;
.updateSection {
padding-top: $el-padding-with-compensation;
display: flex;
flex-direction: column;
justify-content: flex-end;
gap: 4px;
gap: $element-inner-spacing;
.error {
font-size: $error-red;
}
}
.overflowContainer {
overflow-y: auto;
max-height: 40%;
}
@@ -1,15 +1,14 @@
import { useCallback, useEffect, useState } from 'react';
import { FormControl, FormLabel, ModalBody } from '@chakra-ui/react';
import { IoCheckmarkSharp } from '@react-icons/all-files/io5/IoCheckmarkSharp';
import { FormControl, FormLabel, Input, ModalBody } from '@chakra-ui/react';
import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline';
import { useEmitLog } from '@/common/stores/logger';
import { postView } from '../../common/api/ontimeApi';
import EnableBtn from '../../common/components/buttons/EnableBtn';
import useViewSettings from '../../common/hooks-query/useViewSettings';
import { viewsSettingsPlaceholder } from '../../common/models/ViewSettings.type';
import { useEmitLog } from '../../common/stores/logger';
import { openLink } from '../../common/utils/linkUtils';
import { inputProps } from '../../features/modals/modalHelper';
import SubmitContainer from './SubmitContainer';
@@ -83,44 +82,37 @@ export default function ViewsSettingsModal() {
🔥 Changes take effect immediately 🔥
</p>
<form onSubmit={submitHandler}>
<div className={style.hSeparator}>Timer end message</div>
<div className={style.spacedEntry}>
<FormLabel htmlFor='endMessage'>
End Message
<span className={style.labelNote}>
<br />
Shown on presenter view when time is finished
</span>
</FormLabel>
<Input
{...inputProps}
maxLength={50}
name='endMessage'
placeholder='Empty message shows elapsed time'
value={formData.endMessage}
onChange={(event) => handleChange('endMessage', event.target.value)}
/>
</div>
<div className={style.hSeparator}>Style Options</div>
<div className={style.blockNotes}>
<span className={style.inlineFlex}>
<IoInformationCircleOutline color='#2b6cb0' fontSize='2em' />
CSS Style Overrides
</span>
This feature allows user defined CSS to override the application stylesheets as a way to customise viewers
appearance.
<br />
Currently the feature affects the following views
<br />
<ul className={style.featureList}>
<li>
<IoCheckmarkSharp /> Stage timer
</li>
<li>
<IoCheckmarkSharp /> Clock
</li>
<li>
<IoCheckmarkSharp /> Minimal timer
</li>
<li>
<IoCheckmarkSharp /> Backstage screen
</li>
<li>
<IoCheckmarkSharp /> Public screen
</li>
<li>
<IoCheckmarkSharp /> Countdown
</li>
</ul>
Read more about it in the documentation{' '}
This feature allows user defined CSS to customise viewers appearance. <br />
<a
href='#!'
onClick={() => openLink('https://cpvalente.gitbook.io/ontime/features/custom-styling')}
className={style.if}
>
over at Gitbook
For details on the styling and file location please refer to documentation
</a>
</div>
<div className={style.modalFields}>
@@ -50,7 +50,7 @@ export default function UpdateChecker(props: UpdateCheckerProps) {
const disableButton = Boolean(updateMessage && 'version' in updateMessage);
return (
<div className={styles.test}>
<div className={styles.updateSection}>
<Button onClick={versionCheck} variant='ontime-filled' isLoading={isFetching} isDisabled={disableButton}>
Check for updates
</Button>
@@ -1,4 +1,4 @@
import { Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
import { ModalBody, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
import ModalWrapper from '../ModalWrapper';
@@ -19,26 +19,28 @@ export default function IntegrationModal(props: IntegrationModalProps) {
return (
<ModalWrapper title='Integration Settings' isOpen={isOpen} onClose={onClose}>
<div className={styles.headerNotes}>
Manage settings related to protocol integrations
<a href={oscDocsUrl} target='_blank' rel='noreferrer'>
Read the docs
</a>
</div>
<Tabs variant='ontime' size='sm' isLazy>
<TabList>
<Tab>OSC</Tab>
<Tab>OSC Integration</Tab>
</TabList>
<TabPanels>
<TabPanel>
<OscSettings />
</TabPanel>
<TabPanel>
<OscIntegration />
</TabPanel>
</TabPanels>
</Tabs>
<ModalBody>
<div className={styles.headerNotes}>
Manage settings related to protocol integrations
<a href={oscDocsUrl} target='_blank' rel='noreferrer'>
Read the docs
</a>
</div>
<Tabs variant='ontime' size='sm' isLazy>
<TabList>
<Tab>OSC</Tab>
<Tab>OSC Integration</Tab>
</TabList>
<TabPanels>
<TabPanel>
<OscSettings />
</TabPanel>
<TabPanel>
<OscIntegration />
</TabPanel>
</TabPanels>
</Tabs>
</ModalBody>
</ModalWrapper>
);
}
@@ -17,7 +17,7 @@ export default function OntimeModalFooter(props: OntimeModalFooterProps) {
const disableSubmit = isSubmitting || !isDirty || !isValid;
return (
<ModalFooter className={styles.buttonSection} paddingInlineStart={0} paddingInlineEnd={0} paddingBottom={0}>
<ModalFooter className={styles.buttonSection}>
<Button isDisabled={disableRevert} variant='ontime-ghost-on-light' size='sm' onClick={handleRevert}>
Revert to saved
</Button>
@@ -1,6 +1,5 @@
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { ModalBody } from '@chakra-ui/react';
import type { OSCSettings, OscSubscription } from 'ontime-types';
import { TimerLifeCycle } from 'ontime-types';
import { generateId } from 'ontime-utils';
@@ -103,27 +102,25 @@ export default function OscIntegration() {
return (
<form onSubmit={handleSubmit(onSubmit)} className={styles.sectionContainer} id='oscSubscriptions'>
<ModalBody>
{subscriptionKeys.map((cycle, idx) => {
return (
<>
<OscSubscriptionRow
key={cycle}
cycle={cycle as TimerLifeCycle}
title={sectionText[cycle as TimerLifeCycle].title}
subtitle={sectionText[cycle as TimerLifeCycle].subtitle}
visible={showSection === cycle}
setShowSection={setShowSection}
subscriptionOptions={subscriptionState[cycle as TimerLifeCycle]}
handleDelete={deleteSubscriptionEntry}
handleAddNew={addNewSubscriptionEntry}
register={register}
/>
{idx < subscriptionKeys.length - 1 && <hr className={styles.divider} />}
</>
);
})}
</ModalBody>
{subscriptionKeys.map((cycle, idx) => {
return (
<>
<OscSubscriptionRow
key={cycle}
cycle={cycle as TimerLifeCycle}
title={sectionText[cycle as TimerLifeCycle].title}
subtitle={sectionText[cycle as TimerLifeCycle].subtitle}
visible={showSection === cycle}
setShowSection={setShowSection}
subscriptionOptions={subscriptionState[cycle as TimerLifeCycle]}
handleDelete={deleteSubscriptionEntry}
handleAddNew={addNewSubscriptionEntry}
register={register}
/>
{idx < subscriptionKeys.length - 1 && <hr className={styles.divider} />}
</>
);
})}
<OntimeModalFooter
formId='oscSubscriptions'
handleRevert={resetForm}
@@ -1,5 +1,5 @@
import { useForm } from 'react-hook-form';
import { FormControl, Input, ModalBody, Switch } from '@chakra-ui/react';
import { FormControl, Input, Switch } from '@chakra-ui/react';
import useOscSettings, { useOscSettingsMutation } from '../../../common/hooks-query/useOscSettings';
import { PlaceholderSettings } from '../../../common/models/OscSettings';
@@ -53,114 +53,108 @@ export default function OscSettings() {
};
return (
<>
<form onSubmit={handleSubmit(onSubmit)} className={styles.sectionContainer} id='oscSettings'>
<ModalBody>
<div className={styles.splitSection}>
<div>
<span className={`${styles.sectionTitle} ${styles.main}`}>OSC Input</span>
<span className={styles.sectionSubtitle}>Control Ontime with OSC</span>
</div>
<Switch {...register('enabledIn')} variant='ontime-on-light' />
</div>
<form onSubmit={handleSubmit(onSubmit)} className={styles.sectionContainer} id='oscSettings'>
<div className={styles.splitSection}>
<div>
<span className={`${styles.sectionTitle} ${styles.main}`}>OSC Input</span>
<span className={styles.sectionSubtitle}>Control Ontime with OSC</span>
</div>
<Switch {...register('enabledIn')} variant='ontime-on-light' />
</div>
<FormControl isInvalid={!!errors.portIn} className={styles.splitSection}>
<label htmlFor='portIn'>
<span className={styles.sectionTitle}>Listen on Port</span>
{errors.portIn ? (
<span className={styles.error}>{errors.portIn.message}</span>
) : (
<span className={styles.sectionSubtitle}>Default 8888</span>
)}
</label>
<Input
id='portIn'
placeholder='8888'
width='75px'
size='sm'
textAlign='right'
maxLength={5}
variant='ontime-filled-on-light'
{...register('portIn', {
required: { value: true, message: 'Required field' },
max: { value: 65535, message: 'Port in incorrect range (1024 - 65535)' },
min: { value: 1024, message: 'Port in incorrect range (1024 - 65535)' },
pattern: {
value: isOnlyNumbers,
message: 'Value should be numeric',
},
})}
/>
</FormControl>
<FormControl isInvalid={!!errors.portIn} className={styles.splitSection}>
<label htmlFor='portIn'>
<span className={styles.sectionTitle}>Listen on Port</span>
{errors.portIn ? (
<span className={styles.error}>{errors.portIn.message}</span>
) : (
<span className={styles.sectionSubtitle}>Default 8888</span>
)}
</label>
<Input
id='portIn'
placeholder='8888'
width='75px'
size='sm'
textAlign='right'
maxLength={5}
variant='ontime-filled-on-light'
{...register('portIn', {
required: { value: true, message: 'Required field' },
max: { value: 65535, message: 'Port in incorrect range (1024 - 65535)' },
min: { value: 1024, message: 'Port in incorrect range (1024 - 65535)' },
pattern: {
value: isOnlyNumbers,
message: 'Value should be numeric',
},
})}
/>
</FormControl>
<hr className={styles.divider} />
<div className={styles.splitSection}>
<div>
<span className={styles.sectionTitle} style={{ fontWeight: 600 }}>
OSC Output
</span>
<span className={styles.sectionSubtitle}>Ontime data feedback</span>
</div>
<Switch {...register('enabledOut')} variant='ontime-on-light' />
</div>
<div className={styles.splitSection}>
<div>
<span className={styles.sectionTitle} style={{ fontWeight: 600 }}>
OSC Output
</span>
<span className={styles.sectionSubtitle}>Ontime data feedback</span>
</div>
<Switch {...register('enabledOut')} variant='ontime-on-light' />
</div>
<FormControl isInvalid={!!errors.targetIP} className={styles.splitSection}>
<label htmlFor='targetIP'>
<span className={styles.sectionTitle}>OSC target IP</span>
{errors.targetIP ? (
<span className={styles.error}>{errors.targetIP.message}</span>
) : (
<span className={styles.sectionSubtitle}>Default 127.0.0.1</span>
)}
</label>
<Input
id='targetIP'
placeholder='127.0.0.1'
width='140px'
size='sm'
textAlign='right'
variant='ontime-filled-on-light'
{...register('targetIP', {
required: { value: true, message: 'Required field' },
pattern: {
value: isIPAddress,
message: 'Invalid IP address',
},
})}
/>
</FormControl>
<FormControl isInvalid={!!errors.targetIP} className={styles.splitSection}>
<label htmlFor='targetIP'>
<span className={styles.sectionTitle}>OSC target IP</span>
{errors.targetIP ? (
<span className={styles.error}>{errors.targetIP.message}</span>
) : (
<span className={styles.sectionSubtitle}>Default 127.0.0.1</span>
)}
</label>
<Input
id='targetIP'
placeholder='127.0.0.1'
width='140px'
size='sm'
textAlign='right'
variant='ontime-filled-on-light'
{...register('targetIP', {
required: { value: true, message: 'Required field' },
pattern: {
value: isIPAddress,
message: 'Invalid IP address',
},
})}
/>
</FormControl>
<FormControl className={styles.splitSection}>
<label htmlFor='portOut'>
<span className={styles.sectionTitle}>OSC target Port</span>
{errors.portOut ? (
<span className={styles.error}>{errors.portOut.message}</span>
) : (
<span className={styles.sectionSubtitle}>Default 9999</span>
)}
</label>
<Input
id='portOut'
placeholder='9999'
width='75px'
size='sm'
textAlign='right'
maxLength={5}
variant='ontime-filled-on-light'
{...register('portOut', {
required: { value: true, message: 'Required field' },
max: { value: 65535, message: 'Port in incorrect range (1024 - 65535)' },
min: { value: 1024, message: 'Port in incorrect range (1024 - 65535)' },
pattern: {
value: isOnlyNumbers,
message: 'Value should be numeric',
},
})}
/>
</FormControl>
</ModalBody>
</form>
<FormControl className={styles.splitSection}>
<label htmlFor='portOut'>
<span className={styles.sectionTitle}>OSC target Port</span>
{errors.portOut ? (
<span className={styles.error}>{errors.portOut.message}</span>
) : (
<span className={styles.sectionSubtitle}>Default 9999</span>
)}
</label>
<Input
id='portOut'
placeholder='9999'
width='75px'
size='sm'
textAlign='right'
maxLength={5}
variant='ontime-filled-on-light'
{...register('portOut', {
required: { value: true, message: 'Required field' },
max: { value: 65535, message: 'Port in incorrect range (1024 - 65535)' },
min: { value: 1024, message: 'Port in incorrect range (1024 - 65535)' },
pattern: {
value: isOnlyNumbers,
message: 'Value should be numeric',
},
})}
/>
</FormControl>
<OntimeModalFooter
formId='oscSettings'
handleRevert={resetForm}
@@ -168,6 +162,6 @@ export default function OscSettings() {
isValid={isValid}
isSubmitting={isSubmitting}
/>
</>
</form>
);
}
@@ -0,0 +1,150 @@
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import {
Button,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Textarea,
} from '@chakra-ui/react';
import type { EventData } from 'ontime-types';
import { EVENT_DATA, RUNDOWN_TABLE } from '../../../common/api/apiConstants';
import { postNew } from '../../../common/api/ontimeApi';
import useEventData from '../../../common/hooks-query/useEventData';
import { eventDataPlaceholder } from '../../../common/models/EventData';
import { ontimeQueryClient } from '../../../common/queryClient';
import styles from '../Modal.module.scss';
interface QuickStartProps {
onClose: () => void;
isOpen: boolean;
}
export default function QuickStart({ onClose, isOpen }: QuickStartProps) {
const { data, status } = useEventData();
const {
handleSubmit,
register,
reset,
formState: { isSubmitting },
} = useForm({ defaultValues: data });
useEffect(() => {
reset(data);
}, [reset, data]);
const onSubmit = async (data: Partial<EventData>) => {
await postNew(data);
await ontimeQueryClient.invalidateQueries(EVENT_DATA);
await ontimeQueryClient.invalidateQueries(RUNDOWN_TABLE);
};
const onReset = () => reset(eventDataPlaceholder);
const disableButtons = status !== 'success' || isSubmitting;
return (
<Modal
onClose={onClose}
isOpen={isOpen}
closeOnOverlayClick={false}
motionPreset='slideInBottom'
size='xl'
scrollBehavior='inside'
preserveScrollBarGap
variant='ontime'
>
<ModalOverlay />
<ModalContent>
<ModalHeader>Ontime quick start</ModalHeader>
<ModalCloseButton />
<ModalBody className={styles.pad}>
<form onSubmit={handleSubmit(onSubmit)} className={styles.sectionContainer}>
<div className={styles.entryRow}>
<label className={styles.sectionTitle}>
Event title
<Input
variant='ontime-filled-on-light'
size='sm'
maxLength={50}
placeholder='Eurovision song contest'
{...register('title')}
/>
</label>
</div>
<div className={styles.entryRow}>
<label className={styles.sectionTitle}>
Public Info
<Textarea
variant='ontime-filled-on-light'
size='sm'
maxLength={150}
placeholder='Shows always start ontime'
{...register('publicInfo')}
/>
</label>
</div>
<div className={styles.entryRow}>
<label className={styles.sectionTitle}>
Public QR Code Url
<Input
variant='ontime-filled-on-light'
size='sm'
placeholder='www.getontime.no'
{...register('publicUrl')}
/>
</label>
</div>
<div className={styles.entryRow}>
<label className={styles.sectionTitle}>
Backstage Info
<Textarea
variant='ontime-filled-on-light'
size='sm'
maxLength={150}
placeholder='Wi-Fi password: 1234'
{...register('backstageInfo')}
/>
</label>
</div>
<div className={styles.entryRow}>
<label className={styles.sectionTitle}>
Backstage QR Code Url
<Input
variant='ontime-filled-on-light'
size='sm'
placeholder='www.ontime.gitbook.io'
{...register('backstageUrl')}
/>
</label>
</div>
<div className={styles.footerNotes}>
Note: Application options will be kept but rundown and event data will be reset <br />
</div>
<ModalFooter className={styles.buttonSection}>
<Button onClick={onReset} isDisabled={disableButtons} variant='ontime-ghost-on-light' size='sm'>
Clear data
</Button>
<Button
type='submit'
isLoading={isSubmitting}
isDisabled={disableButtons}
variant='ontime-filled'
padding='0 2em'
size='sm'
>
New showfile
</Button>
</ModalFooter>
</form>
</ModalBody>
</ModalContent>
</Modal>
);
}
@@ -1,4 +1,4 @@
import { ReactNode, useMemo } from 'react';
import { ComponentType, useMemo } from 'react';
import { Playback, TitleBlock } from 'ontime-types';
import { useStore } from 'zustand';
@@ -10,22 +10,23 @@ import { useViewOptionsStore } from '../../common/stores/viewOptions';
export type TitleManager = TitleBlock & { showNow: boolean; showNext: boolean };
const withData = (Component: ReactNode) => {
return (props) => {
const withData = <P extends object>(Component: ComponentType<P>) => {
// eslint-disable-next-line react/display-name -- its ok
return (props: P) => {
// persisted app state
const isMirrored = useViewOptionsStore((state) => state.mirror);
// HTTP API data
const { data: eventsData } = useRundown();
const { data: genData } = useEventData();
const { data: rundownData } = useRundown();
const { data: eventData } = useEventData();
const { data: viewSettings } = useViewSettings();
const publicEvents = useMemo(() => {
if (Array.isArray(eventsData)) {
return eventsData.filter((e) => e.type === 'event' && e.title && e.isPublic);
if (Array.isArray(rundownData)) {
return rundownData.filter((e) => e.type === 'event' && e.title && e.isPublic);
}
return [];
}, [eventsData]);
}, [rundownData]);
// websocket data
const data = useStore(runtime);
@@ -99,12 +100,12 @@ const withData = (Component: ReactNode) => {
publicTitle={publicTitleManager}
time={TimeManagerType}
events={publicEvents}
backstageEvents={eventsData}
backstageEvents={rundownData}
selectedId={selectedId}
publicSelectedId={publicSelectedId}
viewSettings={viewSettings}
nextId={nextId}
general={genData}
general={eventData}
onAir={onAir}
/>
);
@@ -21,7 +21,7 @@ interface MinimalTimerProps {
}
export default function MinimalTimer(props: MinimalTimerProps) {
const { isMirrored, pres, time, viewSettings, general } = props;
const { isMirrored, pres, time, viewSettings } = props;
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const [searchParams] = useSearchParams();
@@ -129,7 +129,7 @@ export default function MinimalTimer(props: MinimalTimerProps) {
const isPlaying = time.playback !== Playback.Pause;
const isNegative =
(time.current ?? 0) < 0 && time.timerType !== TimerType.Clock && time.timerType !== TimerType.CountUp;
const showEndMessage = (time.current ?? 0) < 0 && general.endMessage && !hideEndMessage;
const showEndMessage = (time.current ?? 0) < 0 && viewSettings.endMessage && !hideEndMessage;
const showFinished =
time.finished && !userOptions?.hideOvertime && (time.timerType !== TimerType.Clock || showEndMessage);
@@ -162,7 +162,7 @@ export default function MinimalTimer(props: MinimalTimerProps) {
</div>
)}
{showEndMessage ? (
<div className='end-message'>{general.endMessage}</div>
<div className='end-message'>{viewSettings.endMessage}</div>
) : (
<div
className={timerClasses}
@@ -46,7 +46,7 @@ interface TimerProps {
}
export default function Timer(props: TimerProps) {
const { isMirrored, general, pres, title, time, viewSettings } = props;
const { isMirrored, pres, title, time, viewSettings } = props;
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const { getLocalizedString } = useTranslation();
@@ -65,7 +65,7 @@ export default function Timer(props: TimerProps) {
const isNegative =
(time.current ?? 0) < 0 && time.timerType !== TimerType.Clock && time.timerType !== TimerType.CountUp;
const showEndMessage = (time.current ?? 1) < 0 && general.endMessage;
const showEndMessage = (time.current ?? 1) < 0 && viewSettings.endMessage;
const showProgress = time.playback !== Playback.Stop;
const showFinished = time.finished && (time.timerType !== TimerType.Clock || showEndMessage);
const showClock = time.timerType !== TimerType.Clock;
@@ -95,7 +95,7 @@ export default function Timer(props: TimerProps) {
<div className='timer-container'>
{showEndMessage ? (
<div className='end-message'>{general.endMessage}</div>
<div className='end-message'>{viewSettings.endMessage}</div>
) : (
<div
className={timerClasses}
+4 -1
View File
@@ -2,7 +2,7 @@ export const ontimeModal = {
header: {
fontWeight: 400,
letterSpacing: '0.3px',
padding: '8px 16px',
padding: '16px 24px',
fontSize: '20px',
color: '#202020', // $gray-50
},
@@ -19,6 +19,9 @@ export const ontimeModal = {
closeButton: {
color: '#202020', // $gray-50
},
footer: {
padding: '8px',
},
};
export const ontimeSmallModal = {
+3
View File
@@ -13,4 +13,7 @@ export const ontimeTab = {
tablist: {
borderBottom: '2px solid #ececec', // $gray-100
},
tabpanel: {
padding: 0,
},
};
+5 -8
View File
@@ -23,13 +23,11 @@ export const ontimeInputFilled = {
export const ontimeInputFilledOnLight = {
field: {
backgroundColor: 'white',
border: '2px solid transparent',
border: '2px solid #f6f6f6', // $gray-50
_hover: {
backgroundColor: 'white',
border: '2px solid #D2DDFF', // $blue-200
},
_focus: {
backgroundColor: 'white',
border: '2px solid #578AF4', // $blue-500
},
},
@@ -42,16 +40,15 @@ export const ontimeTextAreaFilled = {
export const ontimeTextAreaFilledOnLight = {
borderRadius: '3px',
fontWeight: '400',
backgroundColor: '#ececec', // $gray-100
backgroundColor: 'white',
color: '#202020', // $gray-1200
border: '1px solid transparent',
border: '2px solid #f6f6f6', // $gray-50
_hover: {
backgroundColor: '#cfcfcf', // $gray-300
border: '2px solid #D2DDFF', // $blue-200
},
_focus: {
backgroundColor: '#cfcfcf', // $gray-300
color: '#101010',
border: '1px solid #578AF4', // $blue-500
border: '2px solid #578AF4', // $blue-500
},
_placeholder: { color: '#9d9d9d' }, // $gray-500
};
+1 -1
View File
@@ -88,7 +88,7 @@ const theme = extendTheme({
},
variants: {
'ontime-filled': { ...ontimeTextAreaFilled },
'ontime-filled-onlight': { ...ontimeTextAreaFilledOnLight },
'ontime-filled-on-light': { ...ontimeTextAreaFilledOnLight },
},
},
Tooltip: {