siam-ui-utils 2.2.18 → 2.2.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,76 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+
4
+ import { IInputProps } from './Dropzone'
5
+
6
+ const Input = (props: IInputProps) => {
7
+ const {
8
+ className,
9
+ labelClassName,
10
+ labelWithFilesClassName,
11
+ style,
12
+ labelStyle,
13
+ labelWithFilesStyle,
14
+ getFilesFromEvent,
15
+ accept,
16
+ multiple,
17
+ disabled,
18
+ content,
19
+ withFilesContent,
20
+ onFiles,
21
+ files,
22
+ } = props
23
+
24
+ return (
25
+ <label
26
+ className={files.length > 0 ? labelWithFilesClassName : labelClassName}
27
+ style={files.length > 0 ? labelWithFilesStyle : labelStyle}
28
+ >
29
+ {files.length > 0 ? withFilesContent : content}
30
+ <input
31
+ className={className}
32
+ style={style}
33
+ type="file"
34
+ accept={accept}
35
+ multiple={multiple}
36
+ disabled={disabled}
37
+ onChange={async e => {
38
+ const target = e.target
39
+ const chosenFiles = await getFilesFromEvent(e)
40
+ onFiles(chosenFiles)
41
+ //@ts-ignore
42
+ target.value = null
43
+ }}
44
+ />
45
+ </label>
46
+ )
47
+ }
48
+
49
+ Input.propTypes = {
50
+ className: PropTypes.string,
51
+ labelClassName: PropTypes.string,
52
+ labelWithFilesClassName: PropTypes.string,
53
+ style: PropTypes.object,
54
+ labelStyle: PropTypes.object,
55
+ labelWithFilesStyle: PropTypes.object,
56
+ getFilesFromEvent: PropTypes.func.isRequired,
57
+ accept: PropTypes.string.isRequired,
58
+ multiple: PropTypes.bool.isRequired,
59
+ disabled: PropTypes.bool.isRequired,
60
+ content: PropTypes.node,
61
+ withFilesContent: PropTypes.node,
62
+ onFiles: PropTypes.func.isRequired,
63
+ files: PropTypes.arrayOf(PropTypes.any).isRequired,
64
+ extra: PropTypes.shape({
65
+ active: PropTypes.bool.isRequired,
66
+ reject: PropTypes.bool.isRequired,
67
+ dragged: PropTypes.arrayOf(PropTypes.any).isRequired,
68
+ accept: PropTypes.string.isRequired,
69
+ multiple: PropTypes.bool.isRequired,
70
+ minSizeBytes: PropTypes.number.isRequired,
71
+ maxSizeBytes: PropTypes.number.isRequired,
72
+ maxFiles: PropTypes.number.isRequired,
73
+ }).isRequired,
74
+ }
75
+
76
+ export default Input
@@ -0,0 +1,57 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+
4
+ import { ILayoutProps } from './Dropzone'
5
+
6
+ const Layout = (props: ILayoutProps) => {
7
+ const {
8
+ input,
9
+ previews,
10
+ submitButton,
11
+ dropzoneProps,
12
+ files,
13
+ extra: { maxFiles },
14
+ } = props
15
+
16
+ return (
17
+ <div {...dropzoneProps}>
18
+ {previews}
19
+
20
+ {files.length < maxFiles && input}
21
+
22
+ {files.length > 0 && submitButton}
23
+ </div>
24
+ )
25
+ }
26
+
27
+ Layout.propTypes = {
28
+ input: PropTypes.node,
29
+ previews: PropTypes.arrayOf(PropTypes.node),
30
+ submitButton: PropTypes.node,
31
+ dropzoneProps: PropTypes.shape({
32
+ ref: PropTypes.any.isRequired,
33
+ className: PropTypes.string.isRequired,
34
+ style: PropTypes.object,
35
+ onDragEnter: PropTypes.func.isRequired,
36
+ onDragOver: PropTypes.func.isRequired,
37
+ onDragLeave: PropTypes.func.isRequired,
38
+ onDrop: PropTypes.func.isRequired,
39
+ }).isRequired,
40
+ files: PropTypes.arrayOf(PropTypes.any).isRequired,
41
+ extra: PropTypes.shape({
42
+ active: PropTypes.bool.isRequired,
43
+ reject: PropTypes.bool.isRequired,
44
+ dragged: PropTypes.arrayOf(PropTypes.any).isRequired,
45
+ accept: PropTypes.string.isRequired,
46
+ multiple: PropTypes.bool.isRequired,
47
+ minSizeBytes: PropTypes.number.isRequired,
48
+ maxSizeBytes: PropTypes.number.isRequired,
49
+ maxFiles: PropTypes.number.isRequired,
50
+ onFiles: PropTypes.func.isRequired,
51
+ onCancelFile: PropTypes.func.isRequired,
52
+ onRemoveFile: PropTypes.func.isRequired,
53
+ onRestartFile: PropTypes.func.isRequired,
54
+ }).isRequired,
55
+ }
56
+
57
+ export default Layout
@@ -0,0 +1,139 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+
4
+ import { formatBytes, formatDuration } from './utils'
5
+ import { IPreviewProps } from './Dropzone'
6
+ //@ts-ignore
7
+ import cancelImg from './assets/cancel.svg'
8
+ //@ts-ignore
9
+ import removeImg from './assets/remove.svg'
10
+ //@ts-ignore
11
+ import restartImg from './assets/restart.svg'
12
+
13
+ const iconByFn = {
14
+ cancel: { backgroundImage: `url(${cancelImg})` },
15
+ remove: { backgroundImage: `url(${removeImg})` },
16
+ restart: { backgroundImage: `url(${restartImg})` },
17
+ }
18
+
19
+ class Preview extends React.PureComponent<IPreviewProps> {
20
+ render() {
21
+ const {
22
+ className,
23
+ imageClassName,
24
+ style,
25
+ imageStyle,
26
+ fileWithMeta: { cancel, remove, restart },
27
+ meta: { name = '', percent = 0, size = 0, previewUrl, status, duration, validationError },
28
+ isUpload,
29
+ canCancel,
30
+ canRemove,
31
+ canRestart,
32
+ extra: { minSizeBytes },
33
+ } = this.props
34
+
35
+ let title = `${name || '?'}, ${formatBytes(size)}`
36
+ if (duration) title = `${title}, ${formatDuration(duration)}`
37
+
38
+ if (status === 'error_file_size' || status === 'error_validation') {
39
+ return (
40
+ <div className={className} style={style}>
41
+ <span className="dzu-previewFileNameError">{title}</span>
42
+ {status === 'error_file_size' && <span>{size < minSizeBytes ? 'File too small' : 'File too big'}</span>}
43
+ {status === 'error_validation' && <span>{String(validationError)}</span>}
44
+ {canRemove && <span className="dzu-previewButton" style={iconByFn.remove} onClick={remove} />}
45
+ </div>
46
+ )
47
+ }
48
+
49
+ if (status === 'error_upload_params' || status === 'exception_upload' || status === 'error_upload') {
50
+ title = `${title} (upload failed)`
51
+ }
52
+ if (status === 'aborted') title = `${title} (cancelled)`
53
+
54
+ return (
55
+ <div className={className} style={style}>
56
+ {previewUrl && <img className={imageClassName} style={imageStyle} src={previewUrl} alt={title} title={title} />}
57
+ {!previewUrl && <span className="dzu-previewFileName">{title}</span>}
58
+
59
+ <div className="dzu-previewStatusContainer">
60
+ {isUpload && (
61
+ <progress max={100} value={status === 'done' || status === 'headers_received' ? 100 : percent} />
62
+ )}
63
+
64
+ {status === 'uploading' && canCancel && (
65
+ <span className="dzu-previewButton" style={iconByFn.cancel} onClick={cancel} />
66
+ )}
67
+ {status !== 'preparing' && status !== 'getting_upload_params' && status !== 'uploading' && canRemove && (
68
+ <span className="dzu-previewButton" style={iconByFn.remove} onClick={remove} />
69
+ )}
70
+ {['error_upload_params', 'exception_upload', 'error_upload', 'aborted', 'ready'].includes(status) &&
71
+ canRestart && <span className="dzu-previewButton" style={iconByFn.restart} onClick={restart} />}
72
+ </div>
73
+ </div>
74
+ )
75
+ }
76
+ }
77
+
78
+ // @ts-ignore
79
+ Preview.propTypes = {
80
+ className: PropTypes.string,
81
+ imageClassName: PropTypes.string,
82
+ style: PropTypes.object,
83
+ imageStyle: PropTypes.object,
84
+ fileWithMeta: PropTypes.shape({
85
+ file: PropTypes.any.isRequired,
86
+ meta: PropTypes.object.isRequired,
87
+ cancel: PropTypes.func.isRequired,
88
+ restart: PropTypes.func.isRequired,
89
+ remove: PropTypes.func.isRequired,
90
+ xhr: PropTypes.any,
91
+ }).isRequired,
92
+ // copy of fileWithMeta.meta, won't be mutated
93
+ meta: PropTypes.shape({
94
+ status: PropTypes.oneOf([
95
+ 'preparing',
96
+ 'error_file_size',
97
+ 'error_validation',
98
+ 'ready',
99
+ 'getting_upload_params',
100
+ 'error_upload_params',
101
+ 'uploading',
102
+ 'exception_upload',
103
+ 'aborted',
104
+ 'error_upload',
105
+ 'headers_received',
106
+ 'done',
107
+ ]).isRequired,
108
+ type: PropTypes.string.isRequired,
109
+ name: PropTypes.string,
110
+ uploadedDate: PropTypes.string.isRequired,
111
+ percent: PropTypes.number,
112
+ size: PropTypes.number,
113
+ lastModifiedDate: PropTypes.string,
114
+ previewUrl: PropTypes.string,
115
+ duration: PropTypes.number,
116
+ width: PropTypes.number,
117
+ height: PropTypes.number,
118
+ videoWidth: PropTypes.number,
119
+ videoHeight: PropTypes.number,
120
+ validationError: PropTypes.any,
121
+ }).isRequired,
122
+ isUpload: PropTypes.bool.isRequired,
123
+ canCancel: PropTypes.bool.isRequired,
124
+ canRemove: PropTypes.bool.isRequired,
125
+ canRestart: PropTypes.bool.isRequired,
126
+ files: PropTypes.arrayOf(PropTypes.any).isRequired, // eslint-disable-line react/no-unused-prop-types
127
+ extra: PropTypes.shape({
128
+ active: PropTypes.bool.isRequired,
129
+ reject: PropTypes.bool.isRequired,
130
+ dragged: PropTypes.arrayOf(PropTypes.any).isRequired,
131
+ accept: PropTypes.string.isRequired,
132
+ multiple: PropTypes.bool.isRequired,
133
+ minSizeBytes: PropTypes.number.isRequired,
134
+ maxSizeBytes: PropTypes.number.isRequired,
135
+ maxFiles: PropTypes.number.isRequired,
136
+ }).isRequired,
137
+ }
138
+
139
+ export default Preview
@@ -0,0 +1,47 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+
4
+ import { ISubmitButtonProps } from './Dropzone'
5
+
6
+ const SubmitButton = (props: ISubmitButtonProps) => {
7
+ const { className, buttonClassName, style, buttonStyle, disabled, content, onSubmit, files } = props
8
+
9
+ const _disabled =
10
+ files.some(f => ['preparing', 'getting_upload_params', 'uploading'].includes(f.meta.status)) ||
11
+ !files.some(f => ['headers_received', 'done'].includes(f.meta.status))
12
+
13
+ const handleSubmit = () => {
14
+ onSubmit(files.filter(f => ['headers_received', 'done'].includes(f.meta.status)))
15
+ }
16
+
17
+ return (
18
+ <div className={className} style={style}>
19
+ <button className={buttonClassName} style={buttonStyle} onClick={handleSubmit} disabled={disabled || _disabled}>
20
+ {content}
21
+ </button>
22
+ </div>
23
+ )
24
+ }
25
+
26
+ SubmitButton.propTypes = {
27
+ className: PropTypes.string,
28
+ buttonClassName: PropTypes.string,
29
+ style: PropTypes.object,
30
+ buttonStyle: PropTypes.object,
31
+ disabled: PropTypes.bool.isRequired,
32
+ content: PropTypes.node,
33
+ onSubmit: PropTypes.func.isRequired,
34
+ files: PropTypes.arrayOf(PropTypes.object).isRequired,
35
+ extra: PropTypes.shape({
36
+ active: PropTypes.bool.isRequired,
37
+ reject: PropTypes.bool.isRequired,
38
+ dragged: PropTypes.arrayOf(PropTypes.any).isRequired,
39
+ accept: PropTypes.string.isRequired,
40
+ multiple: PropTypes.bool.isRequired,
41
+ minSizeBytes: PropTypes.number.isRequired,
42
+ maxSizeBytes: PropTypes.number.isRequired,
43
+ maxFiles: PropTypes.number.isRequired,
44
+ }).isRequired,
45
+ }
46
+
47
+ export default SubmitButton
@@ -0,0 +1 @@
1
+ <svg viewBox="0 0 8 14" xmlns="http://www.w3.org/2000/svg"><g fill="#333333"><path d="M1,14 C0.4,14 0,13.6 0,13 L0,1 C0,0.4 0.4,0 1,0 C1.6,0 2,0.4 2,1 L2,13 C2,13.6 1.6,14 1,14 Z" id="Path"></path><path d="M7,14 C6.4,14 6,13.6 6,13 L6,1 C6,0.4 6.4,0 7,0 C7.6,0 8,0.4 8,1 L8,13 C8,13.6 7.6,14 7,14 Z" id="Path"></path></g></svg>
@@ -0,0 +1 @@
1
+ <svg viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-5.0, 0.0)" fill="#333333"><g transform="translate(4.0, 0.0)"><polygon points="7.719 4.964 12.692 0.017 14.389 1.715 9.412 6.666 14.354 11.634 12.657 13.331 6.017 6.657 7.715 4.960"></polygon><polygon points="7.612 4.964 7.616 4.960 9.313 6.657 2.674 13.331 0.977 11.634 5.919 6.666 0.942 1.715 2.639 0.017"></polygon></g></g></svg>
@@ -0,0 +1 @@
1
+ <svg viewBox="0 0 11 15" xmlns="http://www.w3.org/2000/svg"><g><path d="M0.5,14.9 C0.2,14.7 0,14.4 0,14 L0,2 C0,1.6 0.2,1.3 0.5,1.1 C0.8,0.9 1.2,0.9 1.5,1.1 L10.5,7.1 C10.8,7.3 10.9,7.6 10.9,7.9 C10.9,8.2 10.7,8.5 10.5,8.7 L1.5,14.7 C1.4,14.9 0.8,15.1 0.5,14.9 Z M2,3.9 L2,12.2 L8.2,8.1 L2,3.9 Z"></path></g></svg>
@@ -0,0 +1,140 @@
1
+ .dzu-dropzone {
2
+ display: flex;
3
+ flex-direction: column;
4
+ align-items: center;
5
+ width: 100%;
6
+ min-height: 120px;
7
+ overflow: scroll;
8
+ margin: 0 auto;
9
+ position: relative;
10
+ box-sizing: border-box;
11
+ transition: all .15s linear;
12
+ border: 2px solid #d9d9d9;
13
+ border-radius: 4px;
14
+ }
15
+
16
+ .dzu-dropzoneActive {
17
+ background-color: #DEEBFF;
18
+ border-color: #2484FF;
19
+ }
20
+
21
+ .dzu-dropzoneDisabled {
22
+ opacity: 0.5;
23
+ }
24
+
25
+ .dzu-dropzoneDisabled *:hover {
26
+ cursor: unset;
27
+ }
28
+
29
+ .dzu-input {
30
+ display: none;
31
+ }
32
+
33
+ .dzu-inputLabel {
34
+ display: flex;
35
+ justify-content: center;
36
+ align-items: center;
37
+ position: absolute;
38
+ top: 0;
39
+ bottom: 0;
40
+ left: 0;
41
+ right: 0;
42
+ font-family: 'Helvetica', sans-serif;
43
+ font-size: 20px;
44
+ font-weight: 600;
45
+ color: #2484FF;
46
+ -moz-osx-font-smoothing: grayscale;
47
+ -webkit-font-smoothing: antialiased;
48
+ cursor: pointer;
49
+ }
50
+
51
+ .dzu-inputLabelWithFiles {
52
+ display: flex;
53
+ justify-content: center;
54
+ align-items: center;
55
+ align-self: flex-start;
56
+ padding: 0 14px;
57
+ min-height: 32px;
58
+ background-color: #E6E6E6;
59
+ color: #2484FF;
60
+ border: none;
61
+ font-family: 'Helvetica', sans-serif;
62
+ border-radius: 4px;
63
+ font-size: 14px;
64
+ font-weight: 600;
65
+ margin-top: 20px;
66
+ margin-left: 3%;
67
+ -moz-osx-font-smoothing: grayscale;
68
+ -webkit-font-smoothing: antialiased;
69
+ cursor: pointer;
70
+ }
71
+
72
+ .dzu-previewContainer {
73
+ padding: 40px 3%;
74
+ display: flex;
75
+ flex-direction: row;
76
+ align-items: center;
77
+ justify-content: space-between;
78
+ position: relative;
79
+ width: 100%;
80
+ min-height: 60px;
81
+ z-index: 1;
82
+ border-bottom: 1px solid #ECECEC;
83
+ box-sizing: border-box;
84
+ }
85
+
86
+ .dzu-previewStatusContainer {
87
+ display: flex;
88
+ align-items: center;
89
+ }
90
+
91
+ .dzu-previewFileName {
92
+ font-family: 'Helvetica', sans-serif;
93
+ font-size: 14px;
94
+ font-weight: 400;
95
+ color: #333333;
96
+ }
97
+
98
+ .dzu-previewImage {
99
+ width: auto;
100
+ max-height: 40px;
101
+ max-width: 140px;
102
+ border-radius: 4px;
103
+ }
104
+
105
+ .dzu-previewButton {
106
+ background-size: 14px 14px;
107
+ background-position: center;
108
+ background-repeat: no-repeat;
109
+ width: 14px;
110
+ height: 14px;
111
+ cursor: pointer;
112
+ opacity: 0.9;
113
+ margin: 0 0 2px 10px;
114
+ }
115
+
116
+ .dzu-submitButtonContainer {
117
+ margin: 24px 0;
118
+ z-index: 1;
119
+ }
120
+
121
+ .dzu-submitButton {
122
+ padding: 0 14px;
123
+ min-height: 32px;
124
+ background-color: #2484FF;
125
+ border: none;
126
+ border-radius: 4px;
127
+ font-family: 'Helvetica', sans-serif;
128
+ font-size: 14px;
129
+ font-weight: 600;
130
+ color: #FFF;
131
+ -moz-osx-font-smoothing: grayscale;
132
+ -webkit-font-smoothing: antialiased;
133
+ cursor: pointer;
134
+ }
135
+
136
+ .dzu-submitButton:disabled {
137
+ background-color: #E6E6E6;
138
+ color: #333333;
139
+ cursor: unset;
140
+ }
@@ -0,0 +1,113 @@
1
+ import React from 'react'
2
+ import { IStyleCustomization } from './Dropzone'
3
+
4
+ export const formatBytes = (b: number) => {
5
+ const units = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
6
+ let l = 0
7
+ let n = b
8
+
9
+ while (n >= 1024) {
10
+ n /= 1024
11
+ l += 1
12
+ }
13
+
14
+ return `${n.toFixed(n >= 10 || l < 1 ? 0 : 1)}${units[l]}`
15
+ }
16
+
17
+ export const formatDuration = (seconds: number) => {
18
+ const date = new Date(0)
19
+ date.setSeconds(seconds)
20
+ const dateString = date.toISOString().slice(11, 19)
21
+ if (seconds < 3600) return dateString.slice(3)
22
+ return dateString
23
+ }
24
+
25
+ // adapted from: https://github.com/okonet/attr-accept/blob/master/src/index.js
26
+ // returns true if file.name is empty and accept string is something like ".csv",
27
+ // because file comes from dataTransferItem for drag events, and
28
+ // dataTransferItem.name is always empty
29
+ export const accepts = (file: File, accept: string) => {
30
+ if (!accept || accept === '*') return true
31
+
32
+ const mimeType = file.type || ''
33
+ const baseMimeType = mimeType.replace(/\/.*$/, '')
34
+
35
+ return accept
36
+ .split(',')
37
+ .map(t => t.trim())
38
+ .some(type => {
39
+ if (type.charAt(0) === '.') {
40
+ return file.name === undefined || file.name.toLowerCase().endsWith(type.toLowerCase())
41
+ } else if (type.endsWith('/*')) {
42
+ // this is something like an image/* mime type
43
+ return baseMimeType === type.replace(/\/.*$/, '')
44
+ }
45
+ return mimeType === type
46
+ })
47
+ }
48
+
49
+ type ResolveFn<T> = (...args: any[]) => T
50
+
51
+ export const resolveValue = <T = any>(value: ResolveFn<T> | T, ...args: any[]) => {
52
+ if (typeof value === 'function') return (value as ResolveFn<T>)(...args)
53
+ return value
54
+ }
55
+
56
+ export const defaultClassNames = {
57
+ dropzone: 'dzu-dropzone',
58
+ dropzoneActive: 'dzu-dropzoneActive',
59
+ dropzoneReject: 'dzu-dropzoneActive',
60
+ dropzoneDisabled: 'dzu-dropzoneDisabled',
61
+ input: 'dzu-input',
62
+ inputLabel: 'dzu-inputLabel',
63
+ inputLabelWithFiles: 'dzu-inputLabelWithFiles',
64
+ preview: 'dzu-previewContainer',
65
+ previewImage: 'dzu-previewImage',
66
+ submitButtonContainer: 'dzu-submitButtonContainer',
67
+ submitButton: 'dzu-submitButton',
68
+ }
69
+
70
+ export const mergeStyles = (
71
+ classNames: IStyleCustomization<string>,
72
+ styles: IStyleCustomization<React.CSSProperties>,
73
+ addClassNames: IStyleCustomization<string>,
74
+ ...args: any[]
75
+ ) => {
76
+ const resolvedClassNames: { [property: string]: string } = { ...defaultClassNames }
77
+ const resolvedStyles = { ...styles } as { [property: string]: string }
78
+
79
+ for (const [key, value] of Object.entries(classNames)) {
80
+ resolvedClassNames[key] = resolveValue(value, ...args)
81
+ }
82
+
83
+ for (const [key, value] of Object.entries(addClassNames)) {
84
+ resolvedClassNames[key] = `${resolvedClassNames[key]} ${resolveValue(value, ...args)}`
85
+ }
86
+
87
+ for (const [key, value] of Object.entries(styles)) {
88
+ resolvedStyles[key] = resolveValue(value, ...args)
89
+ }
90
+
91
+ return { classNames: resolvedClassNames, styles: resolvedStyles as IStyleCustomization<React.CSSProperties> }
92
+ }
93
+
94
+ export const getFilesFromEvent = (
95
+ event: React.DragEvent<HTMLElement> | React.ChangeEvent<HTMLInputElement>,
96
+ ): Array<File | DataTransferItem> => {
97
+ let items = null
98
+
99
+ if ('dataTransfer' in event) {
100
+ const dt = event.dataTransfer
101
+
102
+ // NOTE: Only the 'drop' event has access to DataTransfer.files, otherwise it will always be empty
103
+ if ('files' in dt && dt.files.length) {
104
+ items = dt.files
105
+ } else if (dt.items && dt.items.length) {
106
+ items = dt.items
107
+ }
108
+ } else if (event.target && event.target.files) {
109
+ items = event.target.files
110
+ }
111
+
112
+ return Array.prototype.slice.call(items)
113
+ }
package/src/index.js CHANGED
@@ -9,4 +9,3 @@ export * as IntlMessages from './IntlMessages';
9
9
  export * from './where-by-room';
10
10
  export * from './copy-link';
11
11
  export * from './view-layout';
12
- export * from './timer';
@@ -1,62 +0,0 @@
1
- import { useEffect, useState, useRef } from 'react';
2
- import './styles.scss';
3
-
4
- export const formatTiempo = (ms) => {
5
- if (ms == null) return '--:--';
6
- const absMs = Math.abs(ms);
7
- const totalSec = Math.floor(absMs / 1000);
8
- const min = Math.floor(totalSec / 60);
9
- const sec = totalSec % 60;
10
- const formatted = `${min.toString().padStart(2, '0')}:${sec
11
- .toString()
12
- .padStart(2, '0')}`;
13
- return ms < 0 ? `-${formatted}` : formatted;
14
- };
15
-
16
- const getTimerColorClass = (ms, maxTime) => {
17
- if (!maxTime) return '';
18
- const percent = ms / maxTime;
19
- if (percent > 0.33) return 'timer-green';
20
- if (percent > 0.15) return 'timer-yellow';
21
- if (percent > 0) return 'timer-red';
22
- if (ms === 0) return 'timer-out';
23
- if (ms < 0) return 'timer-over';
24
- return '';
25
- };
26
-
27
- const Timer = ({
28
- initialMinutes = 0,
29
- icon = true,
30
- className = '',
31
- active = true,
32
- style = {},
33
- }) => {
34
- const initialMs = initialMinutes * 60 * 1000;
35
- const [ms, setMs] = useState(initialMs);
36
- const intervalRef = useRef();
37
-
38
- useEffect(() => {
39
- if (!active) return;
40
- intervalRef.current = setInterval(() => {
41
- setMs((prev) => prev - 1000);
42
- }, 1000);
43
- return () => clearInterval(intervalRef.current);
44
- }, [active]);
45
-
46
- useEffect(() => {
47
- setMs(initialMs);
48
- }, [initialMs]);
49
-
50
- const timerColorClass = getTimerColorClass(ms, initialMs);
51
-
52
- return (
53
- <span
54
- className={`av-video-timer timer ${timerColorClass} ${className}`}
55
- style={style}
56
- >
57
- {icon && <i className="icon-clock timer" />}
58
- {formatTiempo(ms)}
59
- </span>
60
- );
61
- };
62
- export { Timer };