refactor: migrate modal components

This commit is contained in:
Carlos Valente
2025-07-04 10:34:30 +02:00
committed by Carlos Valente
parent 36223ff49f
commit a8ea1080f3
13 changed files with 288 additions and 307 deletions
@@ -1,22 +1,15 @@
import { useState } from 'react'; import { useState } from 'react';
import { IoArrowForward } from 'react-icons/io5'; import { IoArrowForward } from 'react-icons/io5';
import {
IconButton,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalHeader,
ModalOverlay,
Select,
} from '@chakra-ui/react';
import { navigatorConstants } from '../../../viewerConfig'; import { navigatorConstants } from '../../../viewerConfig';
import { setClientRemote } from '../../hooks/useSocket'; import { setClientRemote } from '../../hooks/useSocket';
import useUrlPresets from '../../hooks-query/useUrlPresets'; import useUrlPresets from '../../hooks-query/useUrlPresets';
import Button from '../buttons/Button';
import Info from '../info/Info'; import Info from '../info/Info';
import Input from '../input/input/Input';
import AppLink from '../link/app-link/AppLink'; import AppLink from '../link/app-link/AppLink';
import Modal from '../modal/Modal';
import Select from '../select/Select';
import style from './RedirectClientModal.module.scss'; import style from './RedirectClientModal.module.scss';
@@ -29,8 +22,7 @@ interface RedirectClientModalProps {
onClose: () => void; onClose: () => void;
} }
export function RedirectClientModal(props: RedirectClientModalProps) { export function RedirectClientModal({ id, isOpen, name, currentPath, origin, onClose }: RedirectClientModalProps) {
const { id, isOpen, name, currentPath, origin, onClose } = props;
const { data } = useUrlPresets(); const { data } = useUrlPresets();
const [path, setPath] = useState(currentPath); const [path, setPath] = useState(currentPath);
const [selected, setSelected] = useState('/'); const [selected, setSelected] = useState('/');
@@ -47,13 +39,26 @@ export function RedirectClientModal(props: RedirectClientModalProps) {
const enabledPresets = data.filter((preset) => preset.enabled); const enabledPresets = data.filter((preset) => preset.enabled);
const viewOptions = [
...navigatorConstants.map((view) => ({
value: `/${view.url}`,
label: view.label,
})),
...enabledPresets.map((preset) => ({
value: preset.pathAndParams,
label: `Preset: ${preset.alias}`,
})),
];
return ( return (
<Modal isOpen={isOpen} onClose={onClose} variant='ontime'> <Modal
<ModalOverlay /> isOpen={isOpen}
<ModalContent maxWidth='max(480px, 35vw)'> onClose={onClose}
<ModalHeader>Redirect: {name}</ModalHeader> showCloseButton
<ModalCloseButton /> showBackdrop
<ModalBody> title={`Redirect: ${name}`}
bodyElements={
<>
<Info> <Info>
Remotely redirect the client to a different URL. <br /> Remotely redirect the client to a different URL. <br />
Either by selecting a URL Preset or entering a custom path. Either by selecting a URL Preset or entering a custom path.
@@ -65,36 +70,21 @@ export function RedirectClientModal(props: RedirectClientModalProps) {
<span className={style.label}>Select View or URL Preset</span> <span className={style.label}>Select View or URL Preset</span>
<div className={style.textEntry}> <div className={style.textEntry}>
<Select <Select
size='md' fluid
variant='ontime' options={viewOptions}
isDisabled={enabledPresets.length === 0} defaultValue={viewOptions[0].value}
onChange={(event) => setSelected(event.target.value)} onValueChange={(value) => setSelected(value)}
> disabled={enabledPresets.length === 0}
<option value='/'>Select view or preset</option> />
{navigatorConstants.map((view) => { <Button
return ( variant='primary'
<option key={view.url} value={`/${view.url}`}>
{view.label}
</option>
);
})}
{enabledPresets.map((preset) => {
return (
<option key={preset.pathAndParams} value={preset.pathAndParams}>
{`Preset: ${preset.alias}`}
</option>
);
})}
</Select>
<IconButton
variant='ontime-filled'
size='md'
aria-label='Redirect to preset' aria-label='Redirect to preset'
className={style.redirect} className={style.redirect}
icon={<IoArrowForward />} disabled={enabledPresets.length === 0 || selected === '/'}
isDisabled={enabledPresets.length === 0 || selected === '/'}
onClick={() => handleRedirect(selected)} onClick={() => handleRedirect(selected)}
/> >
Redirect <IoArrowForward />
</Button>
</div> </div>
</div> </div>
<div className={style.inlineEntry}> <div className={style.inlineEntry}>
@@ -102,25 +92,24 @@ export function RedirectClientModal(props: RedirectClientModalProps) {
<label className={style.textEntry}> <label className={style.textEntry}>
{origin} {origin}
<Input <Input
variant='ontime-filled'
size='md'
placeholder='eg. /minimal?key=0000ffff' placeholder='eg. /minimal?key=0000ffff'
fluid
value={path} value={path}
onChange={(event) => setPath(event.target.value)} onChange={(event) => setPath(event.target.value)}
/> />
</label> </label>
<IconButton <Button
variant='ontime-filled' variant='primary'
size='md'
aria-label='Redirect' aria-label='Redirect'
isDisabled={path === currentPath || path === ''} disabled={path === currentPath || path === ''}
className={style.redirect} className={style.redirect}
icon={<IoArrowForward />}
onClick={() => handleRedirect(path)} onClick={() => handleRedirect(path)}
/> >
Redirect <IoArrowForward />
</Button>
</div> </div>
</ModalBody> </>
</ModalContent> }
</Modal> />
); );
} }
@@ -1,13 +1,13 @@
.modal { .modal {
position: fixed; position: fixed;
top: 50%; top: 10vh;
left: 50%; left: 50%;
transform: translateX(-50%);
transform: translate(-50%, -50%);
padding-inline: 1rem; padding-inline: 1rem;
min-width: min(680px, 90vw); min-width: min(680px, 90vw);
min-height: min(200px, 10vh); min-height: min(200px, 10vh);
max-width: min(680px, 90vw);
background-color: $gray-1250; background-color: $gray-1250;
color: $ui-white; color: $ui-white;
@@ -19,7 +19,6 @@
.backdrop { .backdrop {
position: fixed; position: fixed;
inset: 0; inset: 0;
z-index: $zindex-backdrop;
background-color: $backdrop-color; background-color: $backdrop-color;
transition: opacity 300ms cubic-bezier(0.45, 1.005, 0, 1.005); transition: opacity 300ms cubic-bezier(0.45, 1.005, 0, 1.005);
@@ -44,7 +43,7 @@
flex-direction: column; flex-direction: column;
gap: 0.5rem; gap: 0.5rem;
max-height: min(80vh, 600px); max-height: 60vh;
overflow-y: auto; overflow-y: auto;
} }
@@ -8,7 +8,7 @@ import style from './Modal.module.scss';
interface ModalProps { interface ModalProps {
isOpen: boolean; isOpen: boolean;
title: string; title?: string;
showCloseButton?: boolean; showCloseButton?: boolean;
showBackdrop?: boolean; showBackdrop?: boolean;
bodyElements: ReactNode; bodyElements: ReactNode;
@@ -33,6 +33,10 @@
opacity: 0.4; opacity: 0.4;
cursor: not-allowed; cursor: not-allowed;
} }
&.fluid {
width: 100%;
}
} }
.selectIcon { .selectIcon {
@@ -2,6 +2,8 @@ import { IoCheckmark } from 'react-icons/io5';
import { LuChevronsUpDown } from 'react-icons/lu'; import { LuChevronsUpDown } from 'react-icons/lu';
import { Select as BaseSelect } from '@base-ui-components/react/select'; import { Select as BaseSelect } from '@base-ui-components/react/select';
import { cx } from '../../utils/styleUtils';
import styles from './Select.module.scss'; import styles from './Select.module.scss';
interface SelectProps<T> extends Omit<BaseSelect.Root.Props<T>, 'items'> { interface SelectProps<T> extends Omit<BaseSelect.Root.Props<T>, 'items'> {
@@ -10,12 +12,13 @@ interface SelectProps<T> extends Omit<BaseSelect.Root.Props<T>, 'items'> {
value: T; value: T;
label: string; label: string;
}[]; }[];
fluid?: boolean;
} }
export default function Select<T>({ options, ...selectRootProps }: SelectProps<T>) { export default function Select<T>({ options, fluid, ...selectRootProps }: SelectProps<T>) {
return ( return (
<BaseSelect.Root items={options} {...selectRootProps}> <BaseSelect.Root items={options} {...selectRootProps}>
<BaseSelect.Trigger className={styles.select}> <BaseSelect.Trigger className={cx([styles.select, fluid && styles.fluid])}>
<BaseSelect.Value /> <BaseSelect.Value />
<BaseSelect.Icon className={styles.selectIcon}> <BaseSelect.Icon className={styles.selectIcon}>
<LuChevronsUpDown /> <LuChevronsUpDown />
@@ -8,5 +8,8 @@
} }
.column { .column {
width: 100%;
display: flex;
gap: 1rem;
flex-direction: column; flex-direction: column;
} }
@@ -1,17 +1,9 @@
import { lazy, useEffect, useRef, useState } from 'react'; import { lazy, useEffect, useRef, useState } from 'react';
import {
Button,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
} from '@chakra-ui/react';
import { getCSSContents, postCSSContents, restoreCSSContents } from '../../../../common/api/assets'; import { getCSSContents, postCSSContents, restoreCSSContents } from '../../../../common/api/assets';
import Button from '../../../../common/components/buttons/Button';
import Info from '../../../../common/components/info/Info'; import Info from '../../../../common/components/info/Info';
import Modal from '../../../../common/components/modal/Modal';
import * as Panel from '../../panel-utils/PanelUtils'; import * as Panel from '../../panel-utils/PanelUtils';
import style from './StyleEditorModal.module.scss'; import style from './StyleEditorModal.module.scss';
@@ -27,9 +19,7 @@ interface CSSRef {
getCss: () => string; getCss: () => string;
} }
export default function CodeEditorModal(props: CodeEditorModalProps) { export default function CodeEditorModal({ isOpen, onClose }: CodeEditorModalProps) {
const { isOpen, onClose } = props;
const [css, setCSS] = useState(''); const [css, setCSS] = useState('');
const [isDirty, setIsDirty] = useState(false); const [isDirty, setIsDirty] = useState(false);
const [saveLoading, setSaveLoading] = useState(false); const [saveLoading, setSaveLoading] = useState(false);
@@ -84,46 +74,43 @@ export default function CodeEditorModal(props: CodeEditorModalProps) {
}, [isOpen]); }, [isOpen]);
return ( return (
<Modal isOpen={isOpen} onClose={onClose} variant='ontime' isCentered> <Modal
<ModalOverlay /> title='Edit CSS override'
<ModalContent maxWidth='max(800px, 40vw)'> isOpen={isOpen}
<ModalHeader>Edit CSS override</ModalHeader> onClose={onClose}
<ModalCloseButton /> showCloseButton
<ModalBody> showBackdrop
<CodeEditor ref={cssRef} initialValue={css} language='css' isDirty={isDirty} setIsDirty={setIsDirty} /> bodyElements={
</ModalBody> <CodeEditor ref={cssRef} initialValue={css} language='css' isDirty={isDirty} setIsDirty={setIsDirty} />
}
<ModalFooter className={style.column}> footerElements={
<div className={style.column}>
<Info>Invalid CSS will be refused by the browser</Info> <Info>Invalid CSS will be refused by the browser</Info>
{error && <Panel.Error className={style.right}>{`Error: ${error}`}</Panel.Error>} {error && <Panel.Error className={style.right}>{`Error: ${error}`}</Panel.Error>}
<Panel.InlineElements align='apart' className={style.editorActions}> <Panel.InlineElements align='apart' className={style.editorActions}>
<Button <Button variant='ghosted' size='large' onClick={handleRestore} disabled={saveLoading || resetLoading}>
variant='ontime-ghosted'
onClick={handleRestore}
isDisabled={saveLoading || resetLoading}
isLoading={resetLoading}
>
Reset to example Reset to example
</Button> </Button>
<Panel.InlineElements> <Panel.InlineElements>
<Button variant='ontime-ghosted' onClick={clear}> <Button variant='ghosted' size='large' onClick={clear}>
Clear Clear
</Button> </Button>
<Button variant='ontime-subtle' onClick={onClose}> <Button size='large' onClick={onClose}>
Cancel Cancel
</Button> </Button>
<Button <Button
variant='ontime-filled' variant='primary'
size='large'
onClick={handleSave} onClick={handleSave}
isDisabled={saveLoading || resetLoading || !isDirty} disabled={saveLoading || resetLoading || !isDirty}
isLoading={saveLoading} loading={saveLoading}
> >
Save changes Save changes
</Button> </Button>
</Panel.InlineElements> </Panel.InlineElements>
</Panel.InlineElements> </Panel.InlineElements>
</ModalFooter> </div>
</ModalContent> }
</Modal> />
); );
} }
@@ -1,8 +0,0 @@
.scrollContainer {
max-height: 70vh;
overflow: auto;
display: flex;
flex-direction: column;
gap: 1rem;
}
@@ -1,45 +1,35 @@
import { Controller, useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import {
Button,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Select,
Switch,
} from '@chakra-ui/react';
import { QuickStartData } from 'ontime-types'; import { QuickStartData } from 'ontime-types';
import { parseUserTime } from 'ontime-utils'; import { parseUserTime } from 'ontime-utils';
import { quickProject } from '../../../common/api/db'; import { quickProject } from '../../../common/api/db';
import { invalidateAllCaches, maybeAxiosError } from '../../../common/api/utils'; import { invalidateAllCaches, maybeAxiosError } from '../../../common/api/utils';
import Button from '../../../common/components/buttons/Button';
import Input from '../../../common/components/input/input/Input';
import TimeInput from '../../../common/components/input/time-input/TimeInput'; import TimeInput from '../../../common/components/input/time-input/TimeInput';
import Modal from '../../../common/components/modal/Modal';
import Select from '../../../common/components/select/Select';
import Switch from '../../../common/components/switch/Switch';
import { editorSettingsDefaults, useEditorSettings } from '../../../common/stores/editorSettings'; import { editorSettingsDefaults, useEditorSettings } from '../../../common/stores/editorSettings';
import * as Panel from '../panel-utils/PanelUtils'; import * as Panel from '../panel-utils/PanelUtils';
import { quickStartDefaults } from './quickStart.utils'; import { quickStartDefaults } from './quickStart.utils';
import style from './QuickStart.module.scss';
interface QuickStartProps { interface QuickStartProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
} }
export default function QuickStart(props: QuickStartProps) { export default function QuickStart({ isOpen, onClose }: QuickStartProps) {
const { isOpen, onClose } = props;
const { defaultWarnTime, defaultDangerTime, setDangerTime, setWarnTime } = useEditorSettings(); const { defaultWarnTime, defaultDangerTime, setDangerTime, setWarnTime } = useEditorSettings();
const { const {
control,
handleSubmit, handleSubmit,
register, register,
formState: { errors, isSubmitting, isValid }, formState: { errors, isSubmitting, isValid },
watch,
setError, setError,
setValue,
} = useForm<QuickStartData>({ } = useForm<QuickStartData>({
defaultValues: quickStartDefaults, defaultValues: quickStartDefaults,
values: quickStartDefaults, values: quickStartDefaults,
@@ -65,123 +55,114 @@ export default function QuickStart(props: QuickStartProps) {
const dangerTimeInMs = parseUserTime(defaultDangerTime); const dangerTimeInMs = parseUserTime(defaultDangerTime);
return ( return (
<Modal isOpen={isOpen} onClose={onClose} closeOnOverlayClick={false} variant='ontime'> <Modal
<ModalOverlay /> isOpen={isOpen}
<ModalCloseButton /> onClose={onClose}
<ModalContent maxWidth='max(640px, 40vw)'> showBackdrop
showCloseButton
title='Create new project...'
bodyElements={
<form onSubmit={handleSubmit(onSubmit)} id='quick-start'> <form onSubmit={handleSubmit(onSubmit)} id='quick-start'>
<ModalHeader>Create new project...</ModalHeader> <Panel.ListGroup>
<ModalBody className={style.scrollContainer}> <Panel.ListItem>
<ModalCloseButton /> <Panel.Field title='Project title' description='Shown as the title in some views' />
<Panel.ListGroup> <Input maxLength={150} placeholder='Project title' fluid {...register('project.title')} />
<Panel.ListItem> </Panel.ListItem>
<Panel.Field title='Project title' description='Shown as the title in some views' /> <Panel.ListItem>
<Input <Panel.Field
variant='ontime-filled' title='Time format'
size='sm' description='Default time format to show in views 12 /24 hours'
maxLength={150} error={errors.settings?.timeFormat?.message}
placeholder='Project title' />
autoComplete='off' <Select
width='20rem' {...register('settings.timeFormat')}
{...register('project.title')} defaultValue='24'
/> options={[
</Panel.ListItem> { value: '12', label: '12 hours 11:00:10 PM' },
<Panel.ListItem> { value: '24', label: '24 hours 23:00:10' },
<Panel.Field ]}
title='Time format' />
description='Default time format to show in views 12 /24 hours' </Panel.ListItem>
error={errors.settings?.timeFormat?.message} <Panel.ListItem>
/> <Panel.Field
<Select variant='ontime' size='sm' width='auto' isDisabled={false} {...register('settings.timeFormat')}> title='Views language'
<option value='12'>12 hours 11:00:10 PM</option> description='Language to be displayed in views'
<option value='24'>24 hours 23:00:10</option> error={errors.settings?.language?.message}
</Select> />
</Panel.ListItem> <Select
<Panel.ListItem> {...register('settings.language')}
<Panel.Field defaultValue='en'
title='Views language' options={[
description='Language to be displayed in views' { value: 'en', label: 'English' },
error={errors.settings?.language?.message} { value: 'fr', label: 'French' },
/> { value: 'de', label: 'German' },
<Select variant='ontime' size='sm' width='auto' isDisabled={false} {...register('settings.language')}> { value: 'hu', label: 'Hungarian' },
<option value='en'>English</option> { value: 'it', label: 'Italian' },
<option value='fr'>French</option> { value: 'no', label: 'Norwegian' },
<option value='de'>German</option> { value: 'pt', label: 'Portuguese' },
<option value='hu'>Hungarian</option> { value: 'es', label: 'Spanish' },
<option value='it'>Italian</option> { value: 'sv', label: 'Swedish' },
<option value='no'>Norwegian</option> { value: 'pl', label: 'Polish' },
<option value='pt'>Portuguese</option> { value: 'zh', label: 'Chinese (Simplified)' },
<option value='es'>Spanish</option> ]}
<option value='sv'>Swedish</option> />
<option value='pl'>Polish</option> </Panel.ListItem>
<option value='zh'>Chinese (Simplified)</option> </Panel.ListGroup>
</Select>
</Panel.ListItem>
</Panel.ListGroup>
<Panel.ListGroup> <Panel.ListGroup>
<Panel.ListItem> <Panel.ListItem>
<Panel.Field title='Warning time' description='Default threshold for warning time in an event' /> <Panel.Field title='Warning time' description='Default threshold for warning time in an event' />
<TimeInput<'warnTime'> <TimeInput<'warnTime'>
name='warnTime' name='warnTime'
submitHandler={(_field, value) => setWarnTime(value)} submitHandler={(_field, value) => setWarnTime(value)}
time={warnTimeInMs} time={warnTimeInMs}
placeholder={editorSettingsDefaults.warnTime} placeholder={editorSettingsDefaults.warnTime}
/> />
</Panel.ListItem> </Panel.ListItem>
<Panel.ListItem> <Panel.ListItem>
<Panel.Field title='Danger time' description='Default threshold for danger time in an event' /> <Panel.Field title='Danger time' description='Default threshold for danger time in an event' />
<TimeInput<'dangerTime'> <TimeInput<'dangerTime'>
name='dangerTime' name='dangerTime'
submitHandler={(_field, value) => setDangerTime(value)} submitHandler={(_field, value) => setDangerTime(value)}
time={dangerTimeInMs} time={dangerTimeInMs}
placeholder={editorSettingsDefaults.dangerTime} placeholder={editorSettingsDefaults.dangerTime}
/> />
</Panel.ListItem> </Panel.ListItem>
</Panel.ListGroup> </Panel.ListGroup>
<Panel.ListGroup> <Panel.ListGroup>
<Panel.ListItem> <Panel.ListItem>
<Panel.Field <Panel.Field
title='Freeze timer on end' title='Freeze timer on end'
description='When a timer hits 00:00:00, it freezes instead of going negative. It invalidates the End Message.' description='When a timer hits 00:00:00, it freezes instead of going negative. It invalidates the End Message.'
/> />
<Controller <Switch
control={control} name='viewSettings.freezeEnd'
name='viewSettings.freezeEnd' checked={watch('viewSettings.freezeEnd')}
render={({ field: { onChange, value, ref } }) => ( onCheckedChange={(checked) => setValue('viewSettings.freezeEnd', checked)}
<Switch variant='ontime' size='lg' isChecked={value} onChange={onChange} ref={ref} /> />
)} </Panel.ListItem>
/> <Panel.ListItem>
</Panel.ListItem> <Panel.Field
<Panel.ListItem> title='End message'
<Panel.Field description='Message for negative timers; applies only if the timer isn`t frozen on End. If no message is provided, it continues into negative time'
title='End message' />
description='Message for negative timers; applies only if the timer isn`t frozen on End. If no message is provided, it continues into negative time' <Input maxLength={150} fluid placeholder='eg: Time is up!' {...register('viewSettings.endMessage')} />
/> </Panel.ListItem>
<Input </Panel.ListGroup>
size='sm'
autoComplete='off'
variant='ontime-filled'
maxLength={150}
width='20rem'
placeholder='Shown when timer reaches end'
{...register('viewSettings.endMessage')}
/>
</Panel.ListItem>
</Panel.ListGroup>
</ModalBody>
<ModalFooter>
{errors?.root && <Panel.Error>{errors.root.message}</Panel.Error>}
<Button variant='ontime-ghosted' size='md' onClick={onClose} isDisabled={false}>
Cancel
</Button>
<Button variant='ontime-filled' size='md' type='submit' isDisabled={!isValid} isLoading={isSubmitting}>
Create project
</Button>
</ModalFooter>
</form> </form>
</ModalContent> }
</Modal> footerElements={
<>
{errors?.root && <Panel.Error>{errors.root.message}</Panel.Error>}
<Button variant='ghosted' onClick={onClose} disabled={false}>
Cancel
</Button>
<Button variant='primary' type='submit' form='quick-start' disabled={!isValid} loading={isSubmitting}>
Create project
</Button>
</>
}
/>
); );
} }
+18 -13
View File
@@ -1,8 +1,9 @@
import { KeyboardEvent, useState } from 'react'; import { KeyboardEvent, useState } from 'react';
import { Input, Modal, ModalBody, ModalContent, ModalFooter, ModalOverlay } from '@chakra-ui/react';
import { useDebouncedCallback } from '@mantine/hooks'; import { useDebouncedCallback } from '@mantine/hooks';
import { SupportedEntry } from 'ontime-types'; import { SupportedEntry } from 'ontime-types';
import Input from '../../../common/components/input/input/Input';
import Modal from '../../../common/components/modal/Modal';
import { useEventSelection } from '../../../features/rundown/useEventSelection'; import { useEventSelection } from '../../../features/rundown/useEventSelection';
import useFinder from './useFinder'; import useFinder from './useFinder';
@@ -14,8 +15,7 @@ interface FinderProps {
onClose: () => void; onClose: () => void;
} }
export default function Finder(props: FinderProps) { export default function Finder({ isOpen, onClose }: FinderProps) {
const { isOpen, onClose } = props;
const { find, results, error } = useFinder(); const { find, results, error } = useFinder();
const [selected, setSelected] = useState(0); const [selected, setSelected] = useState(0);
@@ -56,11 +56,14 @@ export default function Finder(props: FinderProps) {
}; };
return ( return (
<Modal isOpen={isOpen} onClose={onClose} variant='ontime'> <Modal
<ModalOverlay /> title=''
<ModalContent maxWidth='max(640px, 40vw)'> isOpen={isOpen}
<ModalBody onKeyDown={navigate}> onClose={onClose}
<Input size='lg' onChange={debouncedFind} variant='ontime-filled' placeholder='Search...' /> showBackdrop
bodyElements={
<div onKeyDown={navigate}>
<Input height='large' fluid onChange={debouncedFind} placeholder='Search...' />
<ul className={style.scrollContainer} onMouseMove={handleMouseMoveEvent}> <ul className={style.scrollContainer} onMouseMove={handleMouseMoveEvent}>
{error && <li className={style.error}>{error}</li>} {error && <li className={style.error}>{error}</li>}
{results.length === 0 && <li className={style.empty}>No results</li>} {results.length === 0 && <li className={style.empty}>No results</li>}
@@ -91,12 +94,14 @@ export default function Finder(props: FinderProps) {
); );
})} })}
</ul> </ul>
</ModalBody> </div>
<ModalFooter className={style.footer}> }
footerElements={
<div className={style.footer}>
Use the keywords <span className={style.em}>cue</span>, <span className={style.em}>index</span> or Use the keywords <span className={style.em}>cue</span>, <span className={style.em}>index</span> or
<span className={style.em}>title</span> to filter search <span className={style.em}>title</span> to filter search
</ModalFooter> </div>
</ModalContent> }
</Modal> />
); );
} }
@@ -8,10 +8,25 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
width: 100%;
}
.about {
@extend .column;
font-size: calc(1rem - 2px);
}
.inline {
display: flex;
align-items: center;
gap: 0.5rem;
} }
.header { .header {
font-size: 1.5rem; font-size: 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
} }
.logo { .logo {
@@ -20,7 +35,6 @@
} }
.buttonRow { .buttonRow {
margin-top: 1rem;
display: flex; display: flex;
gap: 1rem; gap: 1rem;
justify-content: end; justify-content: end;
@@ -38,6 +52,7 @@
.table { .table {
width: 100%; width: 100%;
font-size: calc(1rem - 2px);
tbody { tbody {
background-color: $gray-1300; background-color: $gray-1300;
@@ -1,11 +1,15 @@
import { IoClose } from 'react-icons/io5';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Button, Checkbox, Modal, ModalBody, ModalCloseButton, ModalContent, ModalOverlay } from '@chakra-ui/react';
import { loadDemo, loadProject } from '../../../common/api/db'; import { loadDemo, loadProject } from '../../../common/api/db';
import { postShowWelcomeDialog } from '../../../common/api/settings'; import { postShowWelcomeDialog } from '../../../common/api/settings';
import { invalidateAllCaches } from '../../../common/api/utils'; import { invalidateAllCaches } from '../../../common/api/utils';
import Button from '../../../common/components/buttons/Button';
import IconButton from '../../../common/components/buttons/IconButton';
import Checkbox from '../../../common/components/checkbox/Checkbox';
import * as Editor from '../../../common/components/editor-utils/EditorUtils'; import * as Editor from '../../../common/components/editor-utils/EditorUtils';
import ExternalLink from '../../../common/components/link/external-link/ExternalLink'; import ExternalLink from '../../../common/components/link/external-link/ExternalLink';
import Modal from '../../../common/components/modal/Modal';
import { appVersion, discordUrl, documentationUrl, websiteUrl } from '../../../externals'; import { appVersion, discordUrl, documentationUrl, websiteUrl } from '../../../externals';
import ImportProjectButton from './composite/ImportProjectButton'; import ImportProjectButton from './composite/ImportProjectButton';
@@ -17,8 +21,7 @@ interface WelcomeProps {
onClose: () => void; onClose: () => void;
} }
export default function Welcome(props: WelcomeProps) { export default function Welcome({ onClose }: WelcomeProps) {
const { onClose } = props;
const navigate = useNavigate(); const navigate = useNavigate();
/** handle cleanup actions before request closing the modal */ /** handle cleanup actions before request closing the modal */
@@ -55,54 +58,56 @@ export default function Welcome(props: WelcomeProps) {
}; };
return ( return (
<Modal isOpen onClose={handleClose} closeOnOverlayClick={false} variant='ontime'> <Modal
<ModalOverlay /> isOpen
<ModalContent maxWidth='max(640px, 40vw)'> onClose={handleClose}
<ModalCloseButton /> showBackdrop
<ModalBody> bodyElements={
<div className={style.sections}> <div className={style.sections}>
<div className={style.column}> <div className={style.about}>
<img src='ontime-logo.png' alt='ontime' className={style.logo} /> <img src='ontime-logo.png' alt='ontime' className={style.logo} />
<div>Ontime v{appVersion}</div> <div>Ontime v{appVersion}</div>
<ExternalLink href={websiteUrl}>Website</ExternalLink> <ExternalLink href={websiteUrl}>Website</ExternalLink>
<ExternalLink href={documentationUrl}>Read the docs</ExternalLink> <ExternalLink href={documentationUrl}>Read the docs</ExternalLink>
<ExternalLink href={discordUrl}>Discord server</ExternalLink> <ExternalLink href={discordUrl}>Discord server</ExternalLink>
</div>
<div className={style.column}>
<div className={style.header}>
Welcome to Ontime
<IconButton variant='subtle-white'>
<IoClose />
</IconButton>
</div> </div>
<div className={style.column}> <Editor.Title>Select project</Editor.Title>
<div className={style.header}>Welcome to Ontime</div> <div className={style.tableContainer}>
<Editor.Title>Select project</Editor.Title> <table className={style.table}>
<div className={style.tableContainer}> <thead>
<table className={style.table}> <tr>
<thead> <th>File Name</th>
<tr> <th>Last Used</th>
<th>File Name</th> </tr>
<th>Last Used</th> </thead>
</tr> <WelcomeProjectList loadProject={handleLoadProject} onClose={handleClose} />
</thead> </table>
<WelcomeProjectList loadProject={handleLoadProject} onClose={handleClose} />
</table>
</div>
</div> </div>
</div> </div>
</div>
}
footerElements={
<div className={style.column}>
<div className={style.buttonRow}> <div className={style.buttonRow}>
<Button size='sm' variant='ontime-subtle' onClick={handleLoadDemo}> <Button onClick={handleLoadDemo}>Load demo project</Button>
Load demo project
</Button>
<ImportProjectButton onFinish={handleClose} /> <ImportProjectButton onFinish={handleClose} />
<Button size='sm' variant='ontime-filled' onClick={handleCallCreate}> <Button variant='primary' onClick={handleCallCreate}>
Create new... Create new...
</Button> </Button>
</div> </div>
<Checkbox <Editor.Label className={style.inline}>
size='sm' <Checkbox defaultChecked onCheckedChange={(checked) => postShowWelcomeDialog(checked)} />
variant='ontime-ondark'
defaultChecked
onChange={(event) => postShowWelcomeDialog(event.target.checked)}
>
Show this modal on next startup Show this modal on next startup
</Checkbox> </Editor.Label>
</ModalBody> </div>
</ModalContent> }
</Modal> />
); );
} }
@@ -4,18 +4,18 @@
*/ */
import { ChangeEvent, useRef } from 'react'; import { ChangeEvent, useRef } from 'react';
import { Button, Input } from '@chakra-ui/react'; import { Input } from '@chakra-ui/react';
import { uploadProjectFile } from '../../../../common/api/db'; import { uploadProjectFile } from '../../../../common/api/db';
import { invalidateAllCaches } from '../../../../common/api/utils'; import { invalidateAllCaches } from '../../../../common/api/utils';
import Button from '../../../../common/components/buttons/Button';
import { validateProjectFile } from '../../../../common/utils/uploadUtils'; import { validateProjectFile } from '../../../../common/utils/uploadUtils';
interface ImportProjectButtonProps { interface ImportProjectButtonProps {
onFinish: () => void; onFinish: () => void;
} }
export default function ImportProjectButton(props: ImportProjectButtonProps) { export default function ImportProjectButton({ onFinish }: ImportProjectButtonProps) {
const { onFinish } = props;
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const handleSelectFile = () => { const handleSelectFile = () => {
@@ -50,9 +50,7 @@ export default function ImportProjectButton(props: ImportProjectButtonProps) {
data-testid='file-input' data-testid='file-input'
/> />
<Button size='sm' variant='ontime-subtle' onClick={handleSelectFile}> <Button onClick={handleSelectFile}>Import project</Button>
Import project
</Button>
</> </>
); );
} }