mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-09 17:33:55 +00:00
Feat/mac (#109)
* chore: upgrade dependencies * feat/mac: draft pipeline * feat/mac: use public folder for transient files * feat/mac: set app fullscreen * feat/mac: cmd + , toggles menu * feat/mac: update readme * refact: easier login process * refact prevent style issues with safari * refact reduce css download * style: cleanup paginator design * style: studio clock is responsive * style: fix spacing style issues with safari
This commit is contained in:
@@ -12,7 +12,6 @@ export default function CollapseBtn(props) {
|
||||
icon={<FiChevronsUp />}
|
||||
colorScheme='white'
|
||||
variant='outline'
|
||||
background='#fff1'
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
/>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiTarget } from '@react-icons/all-files/fi/FiTarget';
|
||||
|
||||
export default function CursorLockedBtn(props) {
|
||||
const { clickhandler, active, ref, size } = props;
|
||||
const { clickhandler, active, ref, size, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Lock cursor to current'>
|
||||
<IconButton
|
||||
@@ -17,6 +17,7 @@ export default function CursorLockedBtn(props) {
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { useState } from 'react';
|
||||
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ export default function ExpandBtn(props) {
|
||||
icon={<FiChevronsDown />}
|
||||
colorScheme='white'
|
||||
variant='outline'
|
||||
background='#fff1'
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
/>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React from 'react';
|
||||
import PropTypes from "prop-types";
|
||||
import style from "../../../features/info/Info.module.scss";
|
||||
import {Icon} from "@chakra-ui/react";
|
||||
import {FiChevronUp} from "@react-icons/all-files/fi/FiChevronUp";
|
||||
import PropTypes from 'prop-types';
|
||||
import { Icon } from '@chakra-ui/react';
|
||||
import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp';
|
||||
import style from './CollapseBar.module.scss';
|
||||
|
||||
export default function CollapseBar(props) {
|
||||
const {title = 'Collapse bar', isCollapsed = false, onClick}= props;
|
||||
const { title = 'Collapse bar', isCollapsed, onClick, roll } = props;
|
||||
|
||||
return(
|
||||
<div className={style.header}>
|
||||
return (
|
||||
<div className={roll ? style.headerRoll : style.header}>
|
||||
{title}
|
||||
<Icon
|
||||
className={isCollapsed ? style.moreCollapsed : style.moreExpanded}
|
||||
@@ -16,10 +16,11 @@ export default function CollapseBar(props) {
|
||||
onClick={onClick}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
CollapseBar.propTypes = {
|
||||
title: PropTypes.string,
|
||||
isCollapsed: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
}
|
||||
roll: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -1,17 +1,34 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.header,
|
||||
.header__roll {
|
||||
.headerRoll {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 0.9em;
|
||||
color: #ccc;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header {
|
||||
color: #ccc;
|
||||
color: $header-gray;
|
||||
}
|
||||
|
||||
.header__roll {
|
||||
color: #2b6cb0;
|
||||
}
|
||||
.headerRoll {
|
||||
color: $ontime-roll;
|
||||
}
|
||||
|
||||
.moreExpanded,
|
||||
.moreCollapsed {
|
||||
cursor: pointer;
|
||||
color: $text-white;
|
||||
}
|
||||
|
||||
.moreExpanded {
|
||||
transform: scaleY(-1);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.moreCollapsed {
|
||||
transform: scaleY(1);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
import React, { memo } from 'react';
|
||||
import { formatDisplay } from 'common/utils/dateConfig';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Countdown.module.css';
|
||||
import styles from './Countdown.module.scss';
|
||||
|
||||
const Countdown = ({ time, small, isNegative, hideZeroHours }) => {
|
||||
// prepare display string
|
||||
const display =
|
||||
time != null && !isNaN(time)
|
||||
? formatDisplay(time, hideZeroHours)
|
||||
: '-- : -- : --';
|
||||
time != null && !isNaN(time) ? formatDisplay(time, hideZeroHours) : '-- : -- : --';
|
||||
|
||||
const colour = isNegative ? '#ff7597' : '#fffffa';
|
||||
const classes = `${small ? styles.countdownClockSmall : styles.countdownClock}
|
||||
${isNegative ? styles.negative : ''}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={small ? styles.countdownClockSmall : styles.countdownClock}
|
||||
style={{ color: colour }}
|
||||
>
|
||||
<div className={classes}>
|
||||
{display}
|
||||
</div>
|
||||
);
|
||||
|
||||
+6
-2
@@ -1,10 +1,11 @@
|
||||
/* @import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;600;800&display=swap'); */
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
/* Common */
|
||||
.countdownClock,
|
||||
.countdownClockSmall {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
line-height: 0.9;
|
||||
color: $text-white;
|
||||
}
|
||||
|
||||
/* Viewers */
|
||||
@@ -21,5 +22,8 @@
|
||||
text-align: center;
|
||||
letter-spacing: 0.1em;
|
||||
font-weight: 600;
|
||||
color: #fffffa;
|
||||
}
|
||||
|
||||
.negative {
|
||||
color: $ontime-pink;
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
/* eslint-disable react/destructuring-assignment */
|
||||
import React from 'react';
|
||||
import { LoggingContext } from '../../../app/context/LoggingContext';
|
||||
import style from './ErrorBoundary.module.scss';
|
||||
|
||||
class ErrorBoundary extends React.Component {
|
||||
static contextType = LoggingContext;
|
||||
reportContent = '';
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -21,11 +23,25 @@ class ErrorBoundary extends React.Component {
|
||||
errorInfo: info,
|
||||
});
|
||||
this.context.emitError(error.toString());
|
||||
this.reportContent = `${error} ${info.componentStack}`;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.errorMessage) {
|
||||
return <p>:/</p>;
|
||||
return (
|
||||
<div className={style.errorContainer}>
|
||||
<div>
|
||||
<p className={style.error}>:/</p>
|
||||
<p>Something went wrong</p>
|
||||
<p
|
||||
className={style.report}
|
||||
onClick={() => navigator.clipboard.writeText(this.reportContent)}
|
||||
>
|
||||
Copy error
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.errorContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
background-color: #121212;
|
||||
color: white;
|
||||
|
||||
.error {
|
||||
color: $ontime-pink;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.report {
|
||||
text-decoration: underline $ontime-pink;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.report:hover {
|
||||
color: $ontime-pink;
|
||||
}
|
||||
|
||||
.report:active {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
@@ -1,112 +1,9 @@
|
||||
import React, { useContext } from 'react';
|
||||
import EditableTimer from 'common/input/EditableTimer';
|
||||
import { stringFromMillis } from 'ontime-utils/time';
|
||||
import { LoggingContext } from '../../../app/context/LoggingContext';
|
||||
import { validateTimes } from '../../../app/entryValidator';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const label = {
|
||||
fontSize: '0.75em',
|
||||
color: '#aaa',
|
||||
};
|
||||
|
||||
const TimesDelayed = (props) => {
|
||||
const { handleValidate, actionHandler, delay, timeStart, timeEnd, duration, previousEnd } = props;
|
||||
|
||||
const scheduledStart = stringFromMillis(timeStart, false);
|
||||
const scheduledEnd = stringFromMillis(timeEnd, false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span style={label}>
|
||||
Start <span>{scheduledStart}</span>
|
||||
</span>
|
||||
<EditableTimer
|
||||
name='timeStart'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeStart}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span style={label}>
|
||||
End <span>{scheduledEnd}</span>
|
||||
</span>
|
||||
<EditableTimer
|
||||
name='timeEnd'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeEnd}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span style={label}>Duration</span>
|
||||
<EditableTimer
|
||||
name='durationOverride'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={duration}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
TimesDelayed.propTypes = {
|
||||
handleValidate: PropTypes.func.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
delay: PropTypes.number,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
duration: PropTypes.number,
|
||||
previousEnd: PropTypes.number,
|
||||
};
|
||||
|
||||
const Times = (props) => {
|
||||
const { handleValidate, actionHandler, timeStart, timeEnd, duration, previousEnd } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<span style={label}>Start</span>
|
||||
<EditableTimer
|
||||
name='timeStart'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeStart}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span style={label}>End</span>
|
||||
<EditableTimer
|
||||
name='timeEnd'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeEnd}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span style={label}>Duration</span>
|
||||
<EditableTimer
|
||||
name='durationOverride'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={duration}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Times.propTypes = {
|
||||
handleValidate: PropTypes.func.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
duration: PropTypes.number,
|
||||
previousEnd: PropTypes.number,
|
||||
};
|
||||
import TimesDelayed from './TimesDelayed';
|
||||
import Times from './Times';
|
||||
|
||||
export default function EventTimesVertical(props) {
|
||||
const { delay, timeStart, timeEnd, duration, previousEnd, actionHandler } = props;
|
||||
@@ -129,11 +26,7 @@ export default function EventTimesVertical(props) {
|
||||
start = val;
|
||||
} else if (entry === 'timeEnd') {
|
||||
end = val;
|
||||
} else if (entry === 'durationOverride') {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else return entry === 'durationOverride';
|
||||
|
||||
const valid = validateTimes(start, end);
|
||||
// give warning but not enforce validation
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import EditableTimer from '../../input/EditableTimer';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './Times.module.scss'
|
||||
|
||||
export default function Times(props) {
|
||||
const { handleValidate, actionHandler, timeStart, timeEnd, duration, previousEnd } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={style.label}>Start</span>
|
||||
<EditableTimer
|
||||
name='timeStart'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeStart}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span className={style.label}>End</span>
|
||||
<EditableTimer
|
||||
name='timeEnd'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeEnd}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span className={style.label}>Duration</span>
|
||||
<EditableTimer
|
||||
name='durationOverride'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={duration}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Times.propTypes = {
|
||||
handleValidate: PropTypes.func.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
duration: PropTypes.number,
|
||||
previousEnd: PropTypes.number,
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.label {
|
||||
font-size: 0.75em;
|
||||
color: $label-gray;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import EditableTimer from '../../input/EditableTimer';
|
||||
import PropTypes from 'prop-types';
|
||||
import { stringFromMillis } from '../../utils/time';
|
||||
import style from './Times.module.scss'
|
||||
|
||||
export default function TimesDelayed(props) {
|
||||
const { handleValidate, actionHandler, delay, timeStart, timeEnd, duration, previousEnd } = props;
|
||||
|
||||
const scheduledStart = stringFromMillis(timeStart, false);
|
||||
const scheduledEnd = stringFromMillis(timeEnd, false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={style.label}>
|
||||
Start <span>{scheduledStart}</span>
|
||||
</span>
|
||||
<EditableTimer
|
||||
name='timeStart'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeStart}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span className={style.label}>
|
||||
End <span>{scheduledEnd}</span>
|
||||
</span>
|
||||
<EditableTimer
|
||||
name='timeEnd'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeEnd}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span className={style.label}>Duration</span>
|
||||
<EditableTimer
|
||||
name='durationOverride'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={duration}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
TimesDelayed.propTypes = {
|
||||
handleValidate: PropTypes.func.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
delay: PropTypes.number,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
duration: PropTypes.number,
|
||||
previousEnd: PropTypes.number,
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { clamp } from 'app/utils/math';
|
||||
import styles from './MyProgressBar.module.css';
|
||||
import styles from './MyProgressBar.module.scss';
|
||||
|
||||
export default function MyProgressBar(props) {
|
||||
const { now, complete, showElapsed } = props;
|
||||
|
||||
+5
-4
@@ -1,22 +1,23 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.progress,
|
||||
.progressCountdown {
|
||||
height: 2vh;
|
||||
background-color: rgba(255, 255, 255, 0.13);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.progress {
|
||||
background-color: rgba(255, 255, 255, 0.13);
|
||||
background-color: $bg-gray-900;
|
||||
}
|
||||
|
||||
.progressCountdown {
|
||||
background-color: #ff7597;
|
||||
background-color: $ontime-pink;
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
width: 100%;
|
||||
height: 2vh;
|
||||
background-color: rgb(255, 255, 255);
|
||||
background-color: $title-white;
|
||||
border-radius: 4px;
|
||||
transition: 1s linear;
|
||||
transition-property: width;
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import PropTypes from "prop-types";
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Image } from '@chakra-ui/react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
@@ -7,7 +7,7 @@ import navlogo from 'assets/images/logos/LOGO-72.png';
|
||||
import style from './NavLogo.module.scss';
|
||||
|
||||
export default function NavLogo(props) {
|
||||
const {isHidden} = props;
|
||||
const { isHidden } = props;
|
||||
const [showNav, setShowNav] = useState(false);
|
||||
|
||||
const handleClick = () => {
|
||||
@@ -34,74 +34,43 @@ export default function NavLogo(props) {
|
||||
};
|
||||
}, [handleKeyPress]);
|
||||
|
||||
const baseOpacity = (isHidden) ? 0 : 0.5
|
||||
const baseOpacity = isHidden ? 0 : 0.5;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: baseOpacity }}
|
||||
initial={{ opacity: showNav ? 0.5 : baseOpacity }}
|
||||
whileHover={{ opacity: 1 }}
|
||||
className={style.navContainer}
|
||||
>
|
||||
<Image
|
||||
alt=''
|
||||
src={navlogo}
|
||||
className={style.logo}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
<Image alt='' src={navlogo} className={style.logo} onClick={handleClick} />
|
||||
<AnimatePresence>
|
||||
{showNav && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0, y: -50 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
exit={{ opacity: 0, scaleY: 0, y: -50 }}
|
||||
className={showNav ? style.nav : style.navHidden}
|
||||
className={style.nav}
|
||||
>
|
||||
<Link
|
||||
to='/timer'
|
||||
className={style.navItem}
|
||||
tabIndex={1}
|
||||
>
|
||||
<Link to='/timer' className={style.navItem} tabIndex={1}>
|
||||
Timer
|
||||
</Link>
|
||||
<Link
|
||||
to='/minimal'
|
||||
className={style.navItem}
|
||||
tabIndex={2}
|
||||
>
|
||||
<Link to='/minimal' className={style.navItem} tabIndex={2}>
|
||||
Minimal Timer
|
||||
</Link>
|
||||
<Link
|
||||
to='/sm'
|
||||
className={style.navItem}
|
||||
tabIndex={3}
|
||||
>
|
||||
<Link to='/sm' className={style.navItem} tabIndex={3}>
|
||||
Backstage
|
||||
</Link>
|
||||
<Link
|
||||
to='/public'
|
||||
className={style.navItem}
|
||||
tabIndex={4}
|
||||
>
|
||||
<Link to='/public' className={style.navItem} tabIndex={4}>
|
||||
Public
|
||||
</Link>
|
||||
<Link
|
||||
to='/lower'
|
||||
className={style.navItem}
|
||||
tabIndex={5}
|
||||
>
|
||||
<Link to='/lower' className={style.navItem} tabIndex={5}>
|
||||
Lower Thirds
|
||||
</Link>
|
||||
<Link
|
||||
to='/pip'
|
||||
className={style.navItem}
|
||||
tabIndex={6}
|
||||
>
|
||||
<Link to='/pip' className={style.navItem} tabIndex={6}>
|
||||
PIP
|
||||
</Link>
|
||||
<Link
|
||||
to='/studio'
|
||||
className={style.navItem}
|
||||
tabIndex={7}
|
||||
>
|
||||
<Link to='/studio' className={style.navItem} tabIndex={7}>
|
||||
Studio Clock
|
||||
</Link>
|
||||
</motion.div>
|
||||
@@ -113,4 +82,4 @@ export default function NavLogo(props) {
|
||||
|
||||
NavLogo.propTypes = {
|
||||
isHidden: PropTypes.bool,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
$nav-color: #fff;
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.navContainer {
|
||||
position: absolute;
|
||||
@@ -9,22 +9,24 @@ $nav-color: #fff;
|
||||
align-items: flex-end;
|
||||
font-size: max(1vw, 16px);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: max(3vw, 32px);
|
||||
opacity: 0.5;
|
||||
}
|
||||
.logo {
|
||||
width: max(3vw, 32px);
|
||||
opacity: 0.5;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav {
|
||||
gap: 2vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.nav,
|
||||
.showNav {
|
||||
gap: 2vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.navItem {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
padding: 0.5vh 1vw;
|
||||
border-radius: 4px;
|
||||
color: $nav-color;
|
||||
.navItem {
|
||||
background-color: $bg-overlay;
|
||||
padding: 0.5vh 1vw;
|
||||
border-radius: 4px;
|
||||
color: $text-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './ProtectRoute.module.scss';
|
||||
import { PinInput, PinInputField } from '@chakra-ui/react';
|
||||
import { HStack, PinInput, PinInputField } from '@chakra-ui/react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
|
||||
import { AppContext } from '../../../app/context/AppContext';
|
||||
@@ -13,53 +13,77 @@ export default function ProtectRoute({ children }) {
|
||||
const [failed, setFailed] = useState(false);
|
||||
const { auth, validate } = useContext(AppContext);
|
||||
|
||||
const handleValidation = useCallback(() => {
|
||||
const r = validate(pin);
|
||||
if (!r) {
|
||||
setFailed(true);
|
||||
setPin('');
|
||||
}
|
||||
}, [pin, validate]);
|
||||
|
||||
// Set window title
|
||||
useEffect(() => {
|
||||
document.title = 'ontime';
|
||||
}, []);
|
||||
|
||||
const handleValidation = () => {
|
||||
const r = validate(pin);
|
||||
if (!r) {
|
||||
setFailed(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isLocal && !auth ? (
|
||||
<div className={style.container}>
|
||||
ontime
|
||||
<div className={failed ? style.pin__failed : style.pin}>
|
||||
<PinInput
|
||||
type='alphanumeric'
|
||||
size='lg'
|
||||
mask
|
||||
onChange={(value) => {
|
||||
setFailed(false);
|
||||
setPin(value);
|
||||
}}
|
||||
>
|
||||
<PinInputField />
|
||||
<PinInputField />
|
||||
<PinInputField />
|
||||
<PinInputField />
|
||||
</PinInput>
|
||||
<IconButton
|
||||
aria-label='Enter'
|
||||
size='lg'
|
||||
isRound
|
||||
icon={<FiCheck />}
|
||||
style={{ fontSize: '1.5em' }}
|
||||
onClick={() => handleValidation()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</>
|
||||
// Handle keyboard shortcuts
|
||||
const handleKeyPress = useCallback(
|
||||
(e) => {
|
||||
// handle held key
|
||||
if (e.repeat) return;
|
||||
// Space bar
|
||||
if (e.keyCode === 13) {
|
||||
handleValidation();
|
||||
}
|
||||
},
|
||||
[handleValidation],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// attach the event listener
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
|
||||
// remove the event listener
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyPress);
|
||||
};
|
||||
}, [handleKeyPress]);
|
||||
|
||||
if (isLocal || auth) {
|
||||
return children;
|
||||
} else {
|
||||
return (
|
||||
<div className={style.container}>
|
||||
ontime
|
||||
<HStack spacing='10px' className={failed ? style.pin__failed : style.pin}>
|
||||
<PinInput
|
||||
type='alphanumeric'
|
||||
size='lg'
|
||||
mask
|
||||
autoFocus
|
||||
value={pin}
|
||||
onChange={(value) => {
|
||||
setFailed(false);
|
||||
setPin(value);
|
||||
}}
|
||||
>
|
||||
<PinInputField />
|
||||
<PinInputField />
|
||||
<PinInputField />
|
||||
<PinInputField />
|
||||
</PinInput>
|
||||
<IconButton
|
||||
aria-label='Enter'
|
||||
size='lg'
|
||||
isRound
|
||||
icon={<FiCheck />}
|
||||
style={{ fontSize: '1.5em' }}
|
||||
onClick={() => handleValidation()}
|
||||
/>
|
||||
</HStack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ProtectRoute.propTypes = {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@use '../../../styles/mixins' as *;
|
||||
|
||||
.container {
|
||||
background: #222;
|
||||
background: $bg-black;
|
||||
|
||||
display: grid;
|
||||
place-content: center;
|
||||
@@ -18,8 +18,6 @@
|
||||
|
||||
.pin,
|
||||
.pin__failed {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 20px;
|
||||
|
||||
input {
|
||||
|
||||
@@ -1,33 +1,53 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import TodayItem from './TodayItem';
|
||||
import style from './Paginator.module.css';
|
||||
import { useInterval } from 'app/hooks/useInterval';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './Paginator.module.scss';
|
||||
import Empty from '../../state/Empty';
|
||||
|
||||
export default function Paginator(props) {
|
||||
const { events, selectedId, limit = 7, time = 10, isBackstage } = props;
|
||||
const {
|
||||
events,
|
||||
selectedId,
|
||||
limit = 8,
|
||||
time = 10,
|
||||
isBackstage,
|
||||
setPageNumber,
|
||||
setCurrentPage,
|
||||
} = props;
|
||||
const LIMIT_PER_PAGE = limit;
|
||||
const SCROLL_TIME = time * 1000 || 10000;
|
||||
const SCROLL_TIME = time * 1000;
|
||||
const [numEvents, setNumEvents] = useState(0);
|
||||
const [page, setPage] = useState([]);
|
||||
const [pages, setPages] = useState(0);
|
||||
const [selPage, setSelPage] = useState(0);
|
||||
|
||||
// keep parent up to date
|
||||
useEffect(() => {
|
||||
if (setPageNumber) {
|
||||
setPageNumber(pages);
|
||||
}
|
||||
}, [setPageNumber, pages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (setCurrentPage) {
|
||||
setCurrentPage(selPage);
|
||||
}
|
||||
}, [setCurrentPage, selPage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (events == null) return;
|
||||
// how many events in list
|
||||
let n = events.length;
|
||||
const n = events.length;
|
||||
setNumEvents(n);
|
||||
|
||||
// how many paginated views
|
||||
let p = Math.ceil(n / LIMIT_PER_PAGE);
|
||||
setPages(p);
|
||||
setPages(Math.ceil(n / LIMIT_PER_PAGE));
|
||||
|
||||
// divide events in parts of LIMIT_PER_PAGE
|
||||
const eventStart = LIMIT_PER_PAGE * selPage;
|
||||
const eventEnd = LIMIT_PER_PAGE * (selPage + 1);
|
||||
let e = events.slice(eventStart, eventEnd);
|
||||
setPage(e);
|
||||
setPage(events.slice(eventStart, eventEnd));
|
||||
|
||||
// if array is completely in past, show depending on SCROLL_PAST
|
||||
}, [events, selPage, LIMIT_PER_PAGE]);
|
||||
@@ -42,14 +62,10 @@ export default function Paginator(props) {
|
||||
|
||||
let selectedState = 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={style.nav}>
|
||||
{pages > 1 &&
|
||||
[...Array(pages)].map((p, i) => (
|
||||
<div key={i} className={i === selPage ? style.navItemSelected : style.navItem} />
|
||||
))}
|
||||
</div>
|
||||
if (events?.length < 1) {
|
||||
return <Empty text='No events to show' />;
|
||||
} else {
|
||||
return (
|
||||
<div className={style.entries}>
|
||||
{page.map((e) => {
|
||||
if (e.id === selectedId) selectedState = 1;
|
||||
@@ -67,8 +83,8 @@ export default function Paginator(props) {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Paginator.propTypes = {
|
||||
@@ -77,4 +93,6 @@ Paginator.propTypes = {
|
||||
limit: PropTypes.number,
|
||||
time: PropTypes.number,
|
||||
isBackstage: PropTypes.bool,
|
||||
setPageNumber: PropTypes.func,
|
||||
setCurrentPage: PropTypes.func,
|
||||
};
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
.entries {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.navItem,
|
||||
.navItemSelected {
|
||||
background-color: rgba(255, 255, 255, 0.39);
|
||||
width: 0.7vw;
|
||||
height: 0.7vw;
|
||||
border-radius: 0.35vw;
|
||||
}
|
||||
|
||||
.navItemSelected {
|
||||
background-color: rgba(255, 255, 255, 0.79);
|
||||
}
|
||||
|
||||
.nav {
|
||||
align-self: center;
|
||||
justify-content: flex-end;
|
||||
display: flex;
|
||||
margin-bottom: 2.5vh;
|
||||
}
|
||||
|
||||
.nav > div {
|
||||
margin-left: 0.5vw;
|
||||
}
|
||||
|
||||
.entryTimes {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 1.4vw;
|
||||
|
||||
min-width: 10vw;
|
||||
opacity: 0.9;
|
||||
letter-spacing: 0.035em;
|
||||
}
|
||||
|
||||
.entryTitle {
|
||||
align-self: center;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.entryPast,
|
||||
.entryNow,
|
||||
.entryFuture {
|
||||
display: flex;
|
||||
font-size: 1.3vw;
|
||||
padding: 0.2vh 1vw;
|
||||
margin-bottom: 1.5vh;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.entryPast {
|
||||
background-color: rgba(255, 255, 255, 0.01);
|
||||
font-size: 1.2vw;
|
||||
}
|
||||
|
||||
.entryPast > .entryTitle {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.entryFuture {
|
||||
background-color: rgba(255, 255, 255, 0.03);
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.entryNow {
|
||||
color: #fff;
|
||||
line-height: 5vh;
|
||||
background-color: rgba(255, 255, 255, 0.07);
|
||||
border-bottom: 0.5vh solid #ff7597aa;
|
||||
margin-left: -0.5em;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
.backstageInd {
|
||||
color: #ff7597aa;
|
||||
margin-left: auto;
|
||||
margin-right: -0.5vw;
|
||||
}
|
||||
|
||||
.backstageInd::before {
|
||||
content: '*';
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.entries {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.entryTimes {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 1.4vw;
|
||||
|
||||
min-width: 10vw;
|
||||
opacity: 0.9;
|
||||
letter-spacing: 0.035em;
|
||||
}
|
||||
|
||||
.entryTitle {
|
||||
align-self: center;
|
||||
@include ellipsis;
|
||||
}
|
||||
|
||||
.entryPast,
|
||||
.entryNow,
|
||||
.entryFuture {
|
||||
display: flex;
|
||||
font-size: 1.3vw;
|
||||
padding: 0.2vh 1vw;
|
||||
margin-bottom: 1.5vh;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.entryPast {
|
||||
font-size: 1.2vw;
|
||||
}
|
||||
|
||||
.entryPast > .entryTitle {
|
||||
color: $text-gray-disabled;
|
||||
}
|
||||
|
||||
.entryNow,
|
||||
.entryFuture {
|
||||
color: $text-white;
|
||||
}
|
||||
|
||||
.entryFuture {
|
||||
background-color: $bg-gray-950;
|
||||
}
|
||||
|
||||
.entryNow {
|
||||
line-height: 5vh;
|
||||
background-color: $bg-gray-900;
|
||||
border-bottom: 0.5vh solid $ontime-pink;
|
||||
margin-left: -0.5em;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
.backstageInd {
|
||||
color: $ontime-pink;
|
||||
margin-left: auto;
|
||||
margin-right: -0.5vw;
|
||||
}
|
||||
|
||||
.backstageInd::before {
|
||||
content: '*';
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import style from './TitleCard.module.css';
|
||||
import style from './TitleCard.module.scss';
|
||||
|
||||
export default function TitleCard(props) {
|
||||
const { label, title, subtitle, presenter } = props;
|
||||
|
||||
+6
-9
@@ -1,27 +1,24 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.label {
|
||||
font-size: 1.3vw;
|
||||
color: #ff7597;
|
||||
@include card-label;
|
||||
}
|
||||
|
||||
.title,
|
||||
.subtitle,
|
||||
.presenter {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@include ellipsis;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #ddd;
|
||||
@include card-title;
|
||||
font-size: 2.5vw;
|
||||
flex: 1;
|
||||
font-weight: 600;
|
||||
|
||||
}
|
||||
|
||||
.subtitle,
|
||||
.presenter {
|
||||
color: #aaa;
|
||||
color: $subtitle-gray;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import style from './TitleSide.module.css';
|
||||
import style from './TitleSide.module.scss';
|
||||
|
||||
export default function TitleSide(props) {
|
||||
const { type, label, title, subtitle, presenter } = props;
|
||||
|
||||
+5
-5
@@ -1,19 +1,19 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.label {
|
||||
font-size: 1.3vw;
|
||||
color: #ff7597;
|
||||
@include card-label;
|
||||
}
|
||||
|
||||
.nowTitle,
|
||||
.nextTitle {
|
||||
color: #ddd;
|
||||
font-weight: 600;
|
||||
@include card-title;
|
||||
}
|
||||
|
||||
.nowSubtitle,
|
||||
.nowPresenter,
|
||||
.nextSubtitle,
|
||||
.nextPresenter {
|
||||
color: #aaa;
|
||||
color: $subtitle-gray;
|
||||
}
|
||||
|
||||
.nowTitle {
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { stringFromMillis } from 'ontime-utils/time';
|
||||
import style from './Paginator.module.css';
|
||||
import PropTypes from 'prop-types';
|
||||
import { stringFromMillis } from '../../utils/time';
|
||||
import style from './Paginator.module.scss';
|
||||
|
||||
export default function TodayItem(props) {
|
||||
const { selected, timeStart, timeEnd, title, backstageEvent, colour } = props;
|
||||
@@ -19,7 +19,7 @@ export default function TodayItem(props) {
|
||||
else if (selected === 2) selectStyle = style.entryFuture;
|
||||
return (
|
||||
<div className={selectStyle} style={{ borderLeft: `4px solid ${userColour}` }}>
|
||||
<div className={`${style.entryTimes} ${backstageEvent ? style.backstage : undefined}`}>
|
||||
<div className={`${style.entryTimes} ${backstageEvent ? style.backstage : ''}`}>
|
||||
{`${start} · ${end}`}
|
||||
</div>
|
||||
<div className={style.entryTitle}>{title}</div>
|
||||
@@ -29,7 +29,7 @@ export default function TodayItem(props) {
|
||||
}
|
||||
|
||||
TodayItem.propTypes = {
|
||||
selected: PropTypes.bool,
|
||||
selected: PropTypes.number,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
title: PropTypes.string,
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
}
|
||||
|
||||
.preview {
|
||||
color: #666;
|
||||
color: $bg-gray-500;
|
||||
}
|
||||
|
||||
.inline {
|
||||
|
||||
@@ -1,35 +1,16 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
|
||||
import { forgivingStringToMillis } from '../utils/dateConfig';
|
||||
import { stringFromMillis } from 'ontime-utils/time';
|
||||
import style from './EditableTimer.module.css';
|
||||
import { LoggingContext } from '../../app/context/LoggingContext';
|
||||
import PropTypes from 'prop-types';
|
||||
import { stringFromMillis } from '../utils/time';
|
||||
import style from './EditableTimer.module.scss';
|
||||
|
||||
export default function EditableTimer(props) {
|
||||
const { name, actionHandler, time, delay, validate, previousEnd } = props;
|
||||
const { emitError } = useContext(LoggingContext);
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
// prepare time fields
|
||||
const validateValue = (value) => {
|
||||
const success = handleSubmit(value);
|
||||
if (success) {
|
||||
const ms = forgivingStringToMillis(value);
|
||||
setValue(stringFromMillis(ms + delay));
|
||||
} else {
|
||||
setValue(stringFromMillis(time + delay));
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (time == null) return;
|
||||
try {
|
||||
setValue(stringFromMillis(time + delay));
|
||||
} catch (error) {
|
||||
emitError(`Unable to parse date: ${error.text}`);
|
||||
}
|
||||
}, [time, delay, emitError]);
|
||||
|
||||
const handleSubmit = (value) => {
|
||||
// Check if there is anything there
|
||||
@@ -67,6 +48,26 @@ export default function EditableTimer(props) {
|
||||
return true;
|
||||
};
|
||||
|
||||
// prepare time fields
|
||||
const validateValue = (value) => {
|
||||
const success = handleSubmit(value);
|
||||
if (success) {
|
||||
const ms = forgivingStringToMillis(value);
|
||||
setValue(stringFromMillis(ms + delay));
|
||||
} else {
|
||||
setValue(stringFromMillis(time + delay));
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (time == null) return;
|
||||
try {
|
||||
setValue(stringFromMillis(time + delay));
|
||||
} catch (error) {
|
||||
emitError(`Unable to parse date: ${error.text}`);
|
||||
}
|
||||
}, [time, delay, emitError]);
|
||||
|
||||
const isDelayed = delay != null && delay !== 0;
|
||||
|
||||
return (
|
||||
|
||||
+5
-3
@@ -1,7 +1,9 @@
|
||||
@use '../../styles/main'as *;
|
||||
|
||||
.editable,
|
||||
.delayedEditable {
|
||||
background-color: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
background-color: $input-bg;
|
||||
border: $input-border;
|
||||
width: 6.5em;
|
||||
letter-spacing: 1px;
|
||||
height: fit-content;
|
||||
@@ -11,5 +13,5 @@
|
||||
}
|
||||
|
||||
.delayedEditable {
|
||||
border: 1px solid #d69e2e55;
|
||||
border: $input-delayed-border;
|
||||
}
|
||||
@@ -1,13 +1,18 @@
|
||||
import React from 'react';
|
||||
import { ReactComponent as Emptyimage } from 'assets/images/empty.svg';
|
||||
import style from './Empty.module.css';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './Empty.module.scss';
|
||||
|
||||
export default function Empty(props) {
|
||||
const { text } = props;
|
||||
const { text, ...rest } = props;
|
||||
return (
|
||||
<div className={style.emptyContainer}>
|
||||
<div className={style.emptyContainer} {...rest}>
|
||||
<Emptyimage className={style.empty} />
|
||||
<span className={style.text}>{text}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Empty.propTypes = {
|
||||
text: PropTypes.string,
|
||||
}
|
||||
+3
-3
@@ -1,3 +1,5 @@
|
||||
@use '../../styles/main' as *;
|
||||
|
||||
.emptyContainer {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
@@ -5,13 +7,11 @@
|
||||
|
||||
.empty {
|
||||
width: 100%;
|
||||
margin: auto 0;
|
||||
margin-top: 10vh;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: rgba(0, 0, 0, 0.17);
|
||||
color: $bg-black-300;
|
||||
font-weight: 600;
|
||||
font-size: 2em;
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
millisToSeconds,
|
||||
timeStringToMillis,
|
||||
} from '../dateConfig';
|
||||
import { stringFromMillis } from 'ontime-utils/time';
|
||||
import { stringFromMillis } from '../time';
|
||||
|
||||
describe('test string from formatDisplay function', () => {
|
||||
it('test with null values', () => {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { generateId } from '../generate_id.js';
|
||||
|
||||
test('generate a valid 5 digit id', () => {
|
||||
const id = generateId();
|
||||
expect(id.length).toBe(5);
|
||||
});
|
||||
|
||||
test('generate 100 with less than 110 attempts', () => {
|
||||
const ids = new Set();
|
||||
let attempts = 1;
|
||||
while (ids.size < 100) {
|
||||
ids.add(generateId());
|
||||
attempts++;
|
||||
}
|
||||
|
||||
expect(attempts).toBeLessThan(105);
|
||||
});
|
||||
|
||||
describe('generate 1000 with less than 1020 attempts', () => {
|
||||
const ids = new Set();
|
||||
let attempts = 1;
|
||||
while (ids.size < 1000) {
|
||||
ids.add(generateId());
|
||||
attempts++;
|
||||
}
|
||||
|
||||
expect(attempts).toBeLessThan(1020);
|
||||
});
|
||||
@@ -21,7 +21,7 @@ export function formatDisplay(seconds, hideZero = false) {
|
||||
const minutes = Math.floor((s % 3600) / 60);
|
||||
|
||||
if (hideZero && hours < 1) return [minutes, s % 60].map(format).join(':');
|
||||
else return [hours, minutes, s % 60].map(format).join(':');
|
||||
return [hours, minutes, s % 60].map(format).join(':');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,7 +53,7 @@ export const timeStringToMillis = (string) => {
|
||||
if (time.length === 1) return Math.abs(time[0] * mts);
|
||||
if (time.length === 2) return Math.abs(time[0]) * mtm + time[1] * mts;
|
||||
if (time.length === 3) return Math.abs(time[0]) * mth + time[1] * mtm + time[2] * mts;
|
||||
else return 0;
|
||||
return 0;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { stringFromMillis } from 'ontime-utils/time';
|
||||
import { stringFromMillis } from './time';
|
||||
|
||||
/**
|
||||
* @description From a list of events, returns only events of type event with calculated delays
|
||||
* @param {Object[]} events - given events
|
||||
* @returns {Object[]} Filtered events with calculated delays
|
||||
*/
|
||||
|
||||
export const getEventsWithDelay = (events) => {
|
||||
if (events == null) return [];
|
||||
|
||||
@@ -66,7 +67,7 @@ export const formatEventList = (events, selectedId, nextId, showEnd = false) =>
|
||||
const givenEvents = [...events];
|
||||
|
||||
// format list
|
||||
let formattedEvents = [];
|
||||
const formattedEvents = [];
|
||||
for (const g of givenEvents) {
|
||||
const start = stringFromMillis(g.timeStart, false);
|
||||
const end = stringFromMillis(g.timeEnd, false);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import { customAlphabet } from 'nanoid';
|
||||
const nanoid = customAlphabet('1234567890abcdef', 5);
|
||||
|
||||
export const generateId = () => nanoid();
|
||||
@@ -0,0 +1,67 @@
|
||||
const mts = 1000; // millis to seconds
|
||||
const mtm = 1000 * 60; // millis to minutes
|
||||
const mth = 1000 * 60 * 60; // millis to hours
|
||||
|
||||
/**
|
||||
* Returns current time in milliseconds
|
||||
* @returns {number}
|
||||
*/
|
||||
export const nowInMillis = () => {
|
||||
const now = new Date();
|
||||
|
||||
// extract milliseconds since midnight
|
||||
let elapsed = now.getHours() * 3600000;
|
||||
elapsed += now.getMinutes() * 60000;
|
||||
elapsed += now.getSeconds() * 1000;
|
||||
elapsed += now.getMilliseconds();
|
||||
|
||||
return elapsed;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Converts milliseconds to string representing time
|
||||
* @param {number} ms - time in milliseconds
|
||||
* @param {boolean} showSeconds - weather to show the seconds
|
||||
* @param {string} delim - character between HH MM SS
|
||||
* @param {string} ifNull - what to return if value is null
|
||||
* @returns {string} String representing time 00:12:02
|
||||
*/
|
||||
|
||||
export const stringFromMillis = (
|
||||
ms,
|
||||
showSeconds = true,
|
||||
delim = ':',
|
||||
ifNull = '...'
|
||||
) => {
|
||||
if (ms == null || isNaN(ms)) return ifNull;
|
||||
const isNegative = ms < 0 ? '-' : '';
|
||||
const millis = Math.abs(ms);
|
||||
|
||||
const showWith0 = (value) => (value < 10 ? `0${value}` : value);
|
||||
const hours = showWith0(Math.floor(((millis / mth) % 60) % 24));
|
||||
const minutes = showWith0(Math.floor((millis / mtm) % 60));
|
||||
const seconds = showWith0(Math.floor((millis / mts) % 60));
|
||||
|
||||
return showSeconds
|
||||
? `${isNegative}${
|
||||
parseInt(hours) ? `${hours}${delim}` : `00${delim}`
|
||||
}${minutes}${delim}${seconds}`
|
||||
: `${isNegative}${parseInt(hours) ? `${hours}` : '00'}${delim}${minutes}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Converts an excel date to milliseconds
|
||||
* @argument {string} excelDate - excel string date
|
||||
* @returns {number} - time in milliseconds
|
||||
*/
|
||||
export const excelDateStringToMillis = (excelDate) => {
|
||||
const date = new Date(excelDate);
|
||||
if (date instanceof Date && !isNaN(date)) {
|
||||
const h = date.getHours();
|
||||
const m = date.getMinutes();
|
||||
const s = date.getSeconds();
|
||||
|
||||
return h * mth + m * mtm + s * mts;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
Reference in New Issue
Block a user