nitro-web 0.0.1
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.
- package/.editorconfig +9 -0
- package/.eslintrc.json +86 -0
- package/_example/.env-example +16 -0
- package/_example/client/config.ts +5 -0
- package/_example/client/css/index.css +35 -0
- package/_example/client/fonts/Roboto-Bold.ttf +0 -0
- package/_example/client/fonts/Roboto-BoldItalic.ttf +0 -0
- package/_example/client/fonts/Roboto-Italic.ttf +0 -0
- package/_example/client/fonts/Roboto-Medium.ttf +0 -0
- package/_example/client/fonts/Roboto-MediumItalic.ttf +0 -0
- package/_example/client/fonts/Roboto-Regular.ttf +0 -0
- package/_example/client/fonts/inter-v13-latin-300.woff2 +0 -0
- package/_example/client/fonts/inter-v13-latin-500.woff2 +0 -0
- package/_example/client/fonts/inter-v13-latin-600.woff2 +0 -0
- package/_example/client/fonts/inter-v13-latin-700.woff2 +0 -0
- package/_example/client/fonts/inter-v13-latin-800.woff2 +0 -0
- package/_example/client/fonts/inter-v13-latin-900.woff2 +0 -0
- package/_example/client/fonts/inter-v13-latin-regular.woff2 +0 -0
- package/_example/client/imgs/android-chrome-512x512.png +0 -0
- package/_example/client/imgs/favicon.png +0 -0
- package/_example/client/imgs/icons/calendar.svg +3 -0
- package/_example/client/imgs/icons/email.svg +6 -0
- package/_example/client/imgs/icons/eye-open.svg +4 -0
- package/_example/client/imgs/icons/eye.svg +5 -0
- package/_example/client/imgs/icons/filter.svg +7 -0
- package/_example/client/imgs/icons/left-circle.svg +3 -0
- package/_example/client/imgs/icons/left.svg +3 -0
- package/_example/client/imgs/icons/line-options.svg +5 -0
- package/_example/client/imgs/icons/line.svg +3 -0
- package/_example/client/imgs/icons/person.svg +7 -0
- package/_example/client/imgs/icons/plus-circle.svg +5 -0
- package/_example/client/imgs/icons/plus.svg +5 -0
- package/_example/client/imgs/icons/right-circle.svg +3 -0
- package/_example/client/imgs/icons/right.svg +3 -0
- package/_example/client/imgs/icons/search.svg +3 -0
- package/_example/client/imgs/icons/shield.svg +6 -0
- package/_example/client/imgs/icons/tick-circle-solid.svg +8 -0
- package/_example/client/imgs/icons/tick-circle.svg +6 -0
- package/_example/client/imgs/icons/tick.svg +5 -0
- package/_example/client/imgs/icons/up2-small.svg +4 -0
- package/_example/client/imgs/icons/up2.svg +4 -0
- package/_example/client/imgs/icons/updown.svg +6 -0
- package/_example/client/imgs/icons/v-big-dark.svg +3 -0
- package/_example/client/imgs/icons/v-dark.svg +3 -0
- package/_example/client/imgs/icons/v.svg +3 -0
- package/_example/client/imgs/icons/v2-active.svg +6 -0
- package/_example/client/imgs/icons/x1.svg +4 -0
- package/_example/client/imgs/logo/logo-white.svg +20 -0
- package/_example/client/imgs/logo/logo.svg +20 -0
- package/_example/client/imgs/no-image.jpg +0 -0
- package/_example/client/imgs/user.jpg +0 -0
- package/_example/client/index.html +12 -0
- package/_example/client/index.ts +47 -0
- package/_example/components/auth.api.js +1 -0
- package/_example/components/index.tsx +225 -0
- package/_example/components/partials/layouts.tsx +5 -0
- package/_example/components/settings.api.js +1 -0
- package/_example/server/config.js +120 -0
- package/_example/server/email/welcome.html +27 -0
- package/_example/server/index.js +32 -0
- package/_example/tailwind.config.js +84 -0
- package/_example/tsconfig.json +32 -0
- package/_example/types.d.ts +7 -0
- package/_example/webpack.config.js +4 -0
- package/client/app.js +300 -0
- package/client/css/components.css +84 -0
- package/client/css/fonts.css +67 -0
- package/client/imgs/icons/calendar.svg +3 -0
- package/client/imgs/icons/email.svg +6 -0
- package/client/imgs/icons/eye-open.svg +4 -0
- package/client/imgs/icons/eye.svg +5 -0
- package/client/imgs/icons/filter.svg +7 -0
- package/client/imgs/icons/left-circle.svg +3 -0
- package/client/imgs/icons/left.svg +3 -0
- package/client/imgs/icons/line-options.svg +5 -0
- package/client/imgs/icons/line.svg +3 -0
- package/client/imgs/icons/person.svg +7 -0
- package/client/imgs/icons/plus-circle.svg +5 -0
- package/client/imgs/icons/plus.svg +5 -0
- package/client/imgs/icons/right-circle.svg +3 -0
- package/client/imgs/icons/right.svg +3 -0
- package/client/imgs/icons/search.svg +3 -0
- package/client/imgs/icons/shield.svg +6 -0
- package/client/imgs/icons/tick-circle-solid.svg +8 -0
- package/client/imgs/icons/tick-circle.svg +6 -0
- package/client/imgs/icons/tick.svg +5 -0
- package/client/imgs/icons/up2-small.svg +4 -0
- package/client/imgs/icons/up2.svg +4 -0
- package/client/imgs/icons/updown.svg +6 -0
- package/client/imgs/icons/v-big-dark.svg +3 -0
- package/client/imgs/icons/v-dark.svg +3 -0
- package/client/imgs/icons/v.svg +3 -0
- package/client/imgs/icons/v2-active.svg +6 -0
- package/client/imgs/icons/x1.svg +4 -0
- package/client.js +42 -0
- package/components/auth/auth.api.js +419 -0
- package/components/auth/reset.jsx +88 -0
- package/components/auth/signin.jsx +74 -0
- package/components/auth/signup.jsx +62 -0
- package/components/billing/stripe.api.js +267 -0
- package/components/partials/element/accordion.jsx +82 -0
- package/components/partials/element/avatar.jsx +28 -0
- package/components/partials/element/button.jsx +66 -0
- package/components/partials/element/dropdown.jsx +185 -0
- package/components/partials/element/initials.jsx +56 -0
- package/components/partials/element/message.jsx +124 -0
- package/components/partials/element/modal.jsx +229 -0
- package/components/partials/element/sidebar.jsx +166 -0
- package/components/partials/element/tooltip.jsx +146 -0
- package/components/partials/element/topbar.jsx +25 -0
- package/components/partials/form/checkbox.jsx +74 -0
- package/components/partials/form/drop-handler.jsx +62 -0
- package/components/partials/form/drop.jsx +125 -0
- package/components/partials/form/form-error.jsx +21 -0
- package/components/partials/form/input-color.jsx +77 -0
- package/components/partials/form/input-currency.jsx +133 -0
- package/components/partials/form/input-date.jsx +223 -0
- package/components/partials/form/input.jsx +131 -0
- package/components/partials/form/location.jsx +212 -0
- package/components/partials/form/select.jsx +369 -0
- package/components/partials/form/toggle.jsx +46 -0
- package/components/partials/is-first-render.js +15 -0
- package/components/partials/layout/layout1.jsx +32 -0
- package/components/partials/layout/layout2.jsx +47 -0
- package/components/partials/not-found.jsx +7 -0
- package/components/partials/styleguide.jsx +252 -0
- package/components/settings/settings-account.jsx +143 -0
- package/components/settings/settings-business.jsx +121 -0
- package/components/settings/settings-team--member.jsx +108 -0
- package/components/settings/settings-team.jsx +76 -0
- package/components/settings/settings.api.js +54 -0
- package/package.json +175 -0
- package/readme.md +43 -0
- package/server/email/index.js +192 -0
- package/server/email/partials/email.css +153 -0
- package/server/email/partials/layout1.swig +92 -0
- package/server/email/partials/line.swig +8 -0
- package/server/email/partials/vert-10.swig +8 -0
- package/server/email/partials/vert-15.swig +8 -0
- package/server/email/partials/vert-20.swig +8 -0
- package/server/email/partials/vert-25.swig +8 -0
- package/server/email/partials/vert-30.swig +8 -0
- package/server/email/partials/vert-35.swig +8 -0
- package/server/email/partials/vert-50.swig +8 -0
- package/server/email/reset-password.html +21 -0
- package/server/email/welcome.html +21 -0
- package/server/models/company.js +76 -0
- package/server/models/user.js +45 -0
- package/server/router.js +355 -0
- package/server.js +20 -0
- package/util.js +1145 -0
- package/webpack.config.js +302 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* Checkbox/radio/toggle component
|
|
4
|
+
* @param {string} name - The name of the checkbox
|
|
5
|
+
* @param {string} [id] - The id of the checkbox (used for radios)
|
|
6
|
+
* @param {'sm' | 'md'} [size='sm'] - The size of the toggle
|
|
7
|
+
* @param {string} [subtext]
|
|
8
|
+
* @param {string} [text]
|
|
9
|
+
* @param {'checkbox' | 'radio' | 'toggle'} [type='checkbox']
|
|
10
|
+
* @param {object} [props] - input props
|
|
11
|
+
*
|
|
12
|
+
* @link https://tailwindui.com/components/application-ui/forms/checkboxes#component-744ed4fa65ba36b925701eb4da5c6e31
|
|
13
|
+
*/
|
|
14
|
+
export function Checkbox({ name, id, size='sm', subtext, text, type='checkbox', ...props }) {
|
|
15
|
+
if (!name) throw new Error('Checkbox requires a `name` prop')
|
|
16
|
+
id = id || name
|
|
17
|
+
return (
|
|
18
|
+
<div className={`mt-input-before mb-input-after flex gap-3 ${props.className || ''}`}>
|
|
19
|
+
<div className="flex h-6 shrink-0 items-center">
|
|
20
|
+
{
|
|
21
|
+
type !== 'toggle'
|
|
22
|
+
? <div className="group grid size-4 grid-cols-1">
|
|
23
|
+
<input
|
|
24
|
+
{...props}
|
|
25
|
+
id={id}
|
|
26
|
+
name={name}
|
|
27
|
+
type={type}
|
|
28
|
+
className="col-start-1 row-start-1 appearance-none rounded border border-gray-300 bg-white checked:border-primary checked:bg-primary indeterminate:border-primary indeterminate:bg-primary focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:border-gray-300 disabled:bg-gray-100 disabled:checked:bg-gray-100 forced-colors:appearance-auto"
|
|
29
|
+
/>
|
|
30
|
+
<svg
|
|
31
|
+
fill="none"
|
|
32
|
+
viewBox="0 0 14 14"
|
|
33
|
+
className="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-gray-950/25"
|
|
34
|
+
>
|
|
35
|
+
<path
|
|
36
|
+
d="M3 8L6 11L11 3.5"
|
|
37
|
+
strokeWidth={2}
|
|
38
|
+
strokeLinecap="round"
|
|
39
|
+
strokeLinejoin="round"
|
|
40
|
+
className="opacity-0 group-has-[:checked]:opacity-100"
|
|
41
|
+
/>
|
|
42
|
+
<path
|
|
43
|
+
d="M3 7H11"
|
|
44
|
+
strokeWidth={2}
|
|
45
|
+
strokeLinecap="round"
|
|
46
|
+
strokeLinejoin="round"
|
|
47
|
+
className="opacity-0 group-has-[:indeterminate]:opacity-100"
|
|
48
|
+
/>
|
|
49
|
+
</svg>
|
|
50
|
+
</div>
|
|
51
|
+
: <div className="group grid grid-cols-1">
|
|
52
|
+
<input
|
|
53
|
+
{...props}
|
|
54
|
+
id={id}
|
|
55
|
+
name={name}
|
|
56
|
+
type="checkbox"
|
|
57
|
+
class="sr-only peer"
|
|
58
|
+
/>
|
|
59
|
+
<label
|
|
60
|
+
for={id}
|
|
61
|
+
className={`col-start-1 row-start-1 relative ${size == 'sm' ? 'w-9' : 'w-11'} ${size == 'sm' ? 'h-5' : 'h-6'} bg-gray-200 peer-focus-visible:outline-none peer-focus-visible:ring-4 peer-focus-visible:ring-blue-300 dark:peer-focus-visible:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full ${size == 'sm' ? 'after:w-4' : 'after:w-5'} ${size == 'sm' ? 'after:h-4' : 'after:h-5'} after:transition-all dark:border-gray-600 peer-checked:bg-blue-600`}
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
}
|
|
65
|
+
</div>
|
|
66
|
+
{text && <div className="text-sm/6">
|
|
67
|
+
<label for={id} className="select-none">
|
|
68
|
+
<span className="font-medium text-gray-900">{text}</span>
|
|
69
|
+
<span className="ml-2 text-gray-500">{subtext}</span>
|
|
70
|
+
</label>
|
|
71
|
+
</div>}
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export const DropHandler = ({ onDrop, children, className }) => {
|
|
2
|
+
const dropRef = useRef()
|
|
3
|
+
let dragCounter = useRef(0).current
|
|
4
|
+
const [dragging, setDragging] = useState(false)
|
|
5
|
+
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
let div = dropRef.current
|
|
8
|
+
div.addEventListener('dragenter', handleDragIn)
|
|
9
|
+
div.addEventListener('dragleave', handleDragOut)
|
|
10
|
+
div.addEventListener('dragover', handleDragOver)
|
|
11
|
+
div.addEventListener('drop', handleDrop)
|
|
12
|
+
return () => {
|
|
13
|
+
div.removeEventListener('dragenter', handleDragIn)
|
|
14
|
+
div.removeEventListener('dragleave', handleDragOut)
|
|
15
|
+
div.removeEventListener('dragover', handleDragOver)
|
|
16
|
+
div.removeEventListener('drop', handleDrop)
|
|
17
|
+
}
|
|
18
|
+
}, [])
|
|
19
|
+
|
|
20
|
+
const handleDragIn = (e) => {
|
|
21
|
+
e.preventDefault()
|
|
22
|
+
e.stopPropagation()
|
|
23
|
+
dragCounter++
|
|
24
|
+
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
|
|
25
|
+
setDragging(true)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const handleDragOut = (e) => {
|
|
30
|
+
e.preventDefault()
|
|
31
|
+
e.stopPropagation()
|
|
32
|
+
dragCounter--
|
|
33
|
+
if (dragCounter === 0) {
|
|
34
|
+
setDragging(false)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const handleDragOver = (e) => {
|
|
39
|
+
e.preventDefault()
|
|
40
|
+
e.stopPropagation()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const handleDrop = (e) => {
|
|
44
|
+
e.preventDefault()
|
|
45
|
+
e.stopPropagation()
|
|
46
|
+
setDragging(false)
|
|
47
|
+
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
|
48
|
+
onDrop(e.dataTransfer.files)
|
|
49
|
+
// e.dataTransfer.clearData() // causes an error in firefox
|
|
50
|
+
dragCounter = 0
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div
|
|
56
|
+
ref={dropRef}
|
|
57
|
+
class={`${className} relative w-full p-[20px] border border-dashed border-input-border border-2 rounded-md ${dragging ? 'border-primary before:content-[""] before:absolute before:inset-0 before:bg-primary before:opacity-5' : ''}`}
|
|
58
|
+
>
|
|
59
|
+
{children}
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { isRegex, deepFind, s3Image } from '../../../util.js'
|
|
2
|
+
import { DropHandler } from './drop-handler.jsx'
|
|
3
|
+
|
|
4
|
+
export function Drop({ awsUrl, className, id, name, onChange, multiple, state, ...props }) {
|
|
5
|
+
/**
|
|
6
|
+
* @param {string} name - field name or path on state (used to match errors), e.g. 'avatar', 'company.avatar'
|
|
7
|
+
* @param {string} <id> - not required, name used if not provided
|
|
8
|
+
* @param {function} onChange({ target: { id: <{name}|errors>, value } }) - gets called on success/error
|
|
9
|
+
* @param {object} state - State object to get the value, and check errors against
|
|
10
|
+
*/
|
|
11
|
+
if (!name) throw new Error('Drop component requires a `name` prop')
|
|
12
|
+
const inputId = id ||name
|
|
13
|
+
const stateRef = useRef()
|
|
14
|
+
const [urls, setUrls] = useState([])
|
|
15
|
+
stateRef.current = state
|
|
16
|
+
|
|
17
|
+
// Input is always controlled if state is passed in
|
|
18
|
+
if (props.value) {
|
|
19
|
+
var value = props.value
|
|
20
|
+
} else if (typeof state == 'object') {
|
|
21
|
+
value = deepFind(state, name)
|
|
22
|
+
if (typeof value == 'undefined') value = null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// An error matches this input path
|
|
26
|
+
for (let item of (state?.errors || [])) {
|
|
27
|
+
if (isRegex(name) && (item.title||'').match(name)) var error = item
|
|
28
|
+
else if (item.title == name) error = item
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
(async () => setUrls(await getUrls(value)))()
|
|
33
|
+
}, [value])
|
|
34
|
+
|
|
35
|
+
function tryAgain (e) {
|
|
36
|
+
e.preventDefault()
|
|
37
|
+
// clear file input to allow reupload
|
|
38
|
+
document.getElementById(name).value = ''
|
|
39
|
+
if (onChange) {
|
|
40
|
+
const errors = (stateRef.errors||[]).filter(e => e.title != name)
|
|
41
|
+
onChange({
|
|
42
|
+
// remove file from state
|
|
43
|
+
target: { id: name, value: null },
|
|
44
|
+
// reset (server) errors
|
|
45
|
+
errors: errors.length ? errors : undefined,
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function onFileAttach (files=[]) {
|
|
51
|
+
// files is a FileList object
|
|
52
|
+
if (onChange) onChange({ target: { id: name, value: multiple ? files : files[0] } })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function getUrls(objectOrFileListItem) {
|
|
56
|
+
/**
|
|
57
|
+
* @param {object|FileList} objectOrFileListItem - FileList object or monastery image object
|
|
58
|
+
* @returns {Promise} - Resolves to an array of image URLs
|
|
59
|
+
*/
|
|
60
|
+
// Make sure FileLists are converted to a real array
|
|
61
|
+
if (!objectOrFileListItem) return []
|
|
62
|
+
const array = objectOrFileListItem.length ? Array.from(objectOrFileListItem) : [objectOrFileListItem]
|
|
63
|
+
return Promise.all(array.map((file) => {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
if (file.lastModified) {
|
|
66
|
+
const reader = new FileReader()
|
|
67
|
+
reader.onload = () => resolve(reader.result)
|
|
68
|
+
reader.onerror = reject
|
|
69
|
+
reader.readAsDataURL(file)
|
|
70
|
+
} else {
|
|
71
|
+
resolve(s3Image(awsUrl, file))
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
}))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// function getFilename (objectOrFile) {
|
|
78
|
+
// if (objectOrFile.lastModified) return objectOrFile.name
|
|
79
|
+
// else return 'avatar.jpg'
|
|
80
|
+
// }
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div class={`mt-input-before mb-input-after ${className || ''}`}>
|
|
84
|
+
<input
|
|
85
|
+
{...props}
|
|
86
|
+
id={inputId}
|
|
87
|
+
type="file"
|
|
88
|
+
onChange={(e) => onFileAttach(e.target.files)}
|
|
89
|
+
hidden
|
|
90
|
+
/>
|
|
91
|
+
<DropHandler
|
|
92
|
+
onDrop={onFileAttach}
|
|
93
|
+
class="flex flex-column justify-center items-center text-center gap-2 text-grey-300 text-sm px-8 min-h-[300px]"
|
|
94
|
+
>
|
|
95
|
+
{
|
|
96
|
+
!value &&
|
|
97
|
+
<>
|
|
98
|
+
{/* {todo upload svg here} */}
|
|
99
|
+
<div>
|
|
100
|
+
Drag and drop your file here
|
|
101
|
+
<label class="weight-500 inline-block text-sm text-primary" for={inputId}>or select a file</label>
|
|
102
|
+
</div>
|
|
103
|
+
</>
|
|
104
|
+
}
|
|
105
|
+
{
|
|
106
|
+
value &&
|
|
107
|
+
<>
|
|
108
|
+
{
|
|
109
|
+
urls.map((url, i) => (
|
|
110
|
+
<div key={i} class="flex align-items-center gap-1">
|
|
111
|
+
<img src={url} width="100%" />
|
|
112
|
+
</div>
|
|
113
|
+
))
|
|
114
|
+
}
|
|
115
|
+
<div>
|
|
116
|
+
Your file has been added successfully.
|
|
117
|
+
<Link to="#" class="text-primary" onClick={tryAgain}>Use another file?</Link>
|
|
118
|
+
</div>
|
|
119
|
+
</>
|
|
120
|
+
}
|
|
121
|
+
</DropHandler>
|
|
122
|
+
{error && <div class="form-error mt-0-5">{error.detail}</div>}
|
|
123
|
+
</div>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function FormError({ state, fields, className }) {
|
|
2
|
+
/**
|
|
3
|
+
* this is a catch all error component that should be placed next to the submit button
|
|
4
|
+
* @param {object} state
|
|
5
|
+
* @param {array} <fields> - display all errors except these field titles, e.g. ['name', 'address']
|
|
6
|
+
*/
|
|
7
|
+
for (let item of state.errors || []) {
|
|
8
|
+
if (!item.title || item.title.match(/^(error|invalid)$/i) || (fields && !fields.includes(item.title))) {
|
|
9
|
+
var error = item
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return (
|
|
13
|
+
<>
|
|
14
|
+
{error ? (
|
|
15
|
+
<div class={`text-danger mt-1 text-sm ${className||''}`}>
|
|
16
|
+
{error.detail}
|
|
17
|
+
</div>
|
|
18
|
+
) : null}
|
|
19
|
+
</>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { css } from 'twin.macro'
|
|
2
|
+
import { hsvaToHex, hexToHsva, validHex } from '@uiw/color-convert'
|
|
3
|
+
import Saturation from '@uiw/react-color-saturation'
|
|
4
|
+
import Hue from '@uiw/react-color-hue'
|
|
5
|
+
import { Dropdown } from '../element/dropdown.jsx'
|
|
6
|
+
import { throttle } from '../../../util.js'
|
|
7
|
+
|
|
8
|
+
export function InputColor({ className, defaultColor='#333', iconEl, id, onChange, value, ...props }) {
|
|
9
|
+
const [lastChanged, setLastChanged] = useState(() => `ic-${Date.now()}`)
|
|
10
|
+
const isInvalid = className?.includes('is-invalid') ? 'is-invalid' : ''
|
|
11
|
+
|
|
12
|
+
function onInputChange(e) {
|
|
13
|
+
setLastChanged(`ic-${Date.now()}`)
|
|
14
|
+
if (onChange) onChange(e)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Dropdown
|
|
19
|
+
css={style}
|
|
20
|
+
menuToggles={false}
|
|
21
|
+
menuChildren={
|
|
22
|
+
<ColorPicker key={lastChanged} defaultColor={defaultColor} id={id} value={value} onChange={onChange} />
|
|
23
|
+
}
|
|
24
|
+
>
|
|
25
|
+
<div className="grid grid-cols-1">
|
|
26
|
+
{iconEl}
|
|
27
|
+
<input
|
|
28
|
+
{...props}
|
|
29
|
+
className={className + ' ' + isInvalid}
|
|
30
|
+
id={id}
|
|
31
|
+
value={value}
|
|
32
|
+
onChange={onInputChange}
|
|
33
|
+
onBlur={() => !validHex(value) && onInputChange({ target: { id: id, value: '' }})}
|
|
34
|
+
autoComplete="off"
|
|
35
|
+
/>
|
|
36
|
+
</div>
|
|
37
|
+
</Dropdown>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function ColorPicker({ id, onChange, value, defaultColor }) {
|
|
42
|
+
const [hsva, setHsva] = useState(() => hexToHsva(validHex(value) ? value : defaultColor))
|
|
43
|
+
const [debounce] = useState(() => throttle(callOnChange, 50))
|
|
44
|
+
|
|
45
|
+
function callOnChange(newHsva) {
|
|
46
|
+
onChange({ target: { id: id, value: hsvaToHex(newHsva) }})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<>
|
|
51
|
+
<Saturation
|
|
52
|
+
hsva={hsva}
|
|
53
|
+
onChange={(newHsva) => {
|
|
54
|
+
setHsva(newHsva)
|
|
55
|
+
if (onChange) debounce(newHsva)
|
|
56
|
+
}}
|
|
57
|
+
/>
|
|
58
|
+
<Hue
|
|
59
|
+
hue={hsva.h}
|
|
60
|
+
onChange={(newHue) => {
|
|
61
|
+
setHsva({ ...hsva, ...newHue })
|
|
62
|
+
if (onChange) debounce({ ...hsva, ...newHue })
|
|
63
|
+
}}
|
|
64
|
+
/>
|
|
65
|
+
</>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const style = () => css`
|
|
70
|
+
text-indent: 0 !important; // since is-color is on dropdown
|
|
71
|
+
.w-color-interactive {
|
|
72
|
+
width: 100% !important;
|
|
73
|
+
height: 150px !important;
|
|
74
|
+
}
|
|
75
|
+
`
|
|
76
|
+
|
|
77
|
+
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/*eslint-disable*/
|
|
2
|
+
import { NumericFormat } from 'react-number-format'
|
|
3
|
+
import { getCurrencyPrefixWidth } from '../../../util.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {string} id - field name or path on state
|
|
7
|
+
* @param {object} config - e.g. { currencies: { nzd: { symbol: '$', digits: 2 } } }
|
|
8
|
+
* @param {string} [currency] - currency iso
|
|
9
|
+
* @param {function} [onChange] - e.g. (event) => onInputChange(event)
|
|
10
|
+
* @param {string} [placeholder] - e.g. 'Amount'
|
|
11
|
+
* @param {cents} [value] - e.g. 123 (input is always controlled if state is passed in)
|
|
12
|
+
*/
|
|
13
|
+
export function InputCurrency({ id, config, className, currency='nzd', onChange, placeholder, value }) {
|
|
14
|
+
const ref = useRef()
|
|
15
|
+
const [dontFix, setDontFix] = useState()
|
|
16
|
+
const [settings, setSettings] = useState(() => getCurrencySettings(currency))
|
|
17
|
+
const [dollars, setDollars] = useState(() => toDollars(value, true, settings))
|
|
18
|
+
const [prefixWidth, setPrefixWidth] = useState()
|
|
19
|
+
ref.current = { settings, dontFix }
|
|
20
|
+
|
|
21
|
+
if (!config) throw new Error('InputCurrency: `config` is required')
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (settings.currency !== currency) {
|
|
25
|
+
const settings = getCurrencySettings(currency)
|
|
26
|
+
setSettings(settings)
|
|
27
|
+
setDollars(toDollars(value, true, settings)) // required latest _settings
|
|
28
|
+
}
|
|
29
|
+
}, [currency])
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (ref.current.dontFix) {
|
|
33
|
+
setDollars(toDollars(value))
|
|
34
|
+
setDontFix(false)
|
|
35
|
+
} else {
|
|
36
|
+
setDollars(toDollars(value, true))
|
|
37
|
+
}
|
|
38
|
+
}, [value])
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
// Get the prefix content width
|
|
43
|
+
setPrefixWidth(settings.prefix == '$' ? getCurrencyPrefixWidth(settings.prefix, 1) : 0)
|
|
44
|
+
}, [settings.prefix])
|
|
45
|
+
|
|
46
|
+
function toCents(num) {
|
|
47
|
+
if (!num && num !== 0) return null
|
|
48
|
+
const value = Math.round(num * Math.pow(10, ref.current.settings.maxDecimals)) // e.g. 1.23 => 123
|
|
49
|
+
// console.log('toCents', num, value)
|
|
50
|
+
return value
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function toDollars(num, toFixed, settings) {
|
|
54
|
+
if (!num && num !== 0) return null
|
|
55
|
+
const value = num / Math.pow(10, (settings||ref.current.settings).maxDecimals) // e.g. 1.23 => 123
|
|
56
|
+
// console.log('toDollars', num, value)
|
|
57
|
+
return toFixed ? value.toFixed((settings||ref.current.settings).maxDecimals) : value
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getCurrencySettings(currency) {
|
|
61
|
+
/**
|
|
62
|
+
* parse CLDR currency format, e.g. '¤#,##0.00'
|
|
63
|
+
* @param {string} currency - currency iso
|
|
64
|
+
* @returns {object}
|
|
65
|
+
* {
|
|
66
|
+
* currency: 'nzd',
|
|
67
|
+
* decimalSeparator: '.',
|
|
68
|
+
* thousandSeparator: ',',
|
|
69
|
+
* minDecimals: 2,
|
|
70
|
+
* maxDecimals: 2,
|
|
71
|
+
* prefix: '$',
|
|
72
|
+
* suffix: '',
|
|
73
|
+
* }
|
|
74
|
+
*/
|
|
75
|
+
const output = { currency }
|
|
76
|
+
const { symbol, digits } = config.currencies[currency]
|
|
77
|
+
let format = config.countries['nz'].numberFormats.currency
|
|
78
|
+
|
|
79
|
+
// Check for currency symbol (¤) and determine its position
|
|
80
|
+
if (format.indexOf('¤') !== -1) {
|
|
81
|
+
const position = format.indexOf('¤') === 0 ? 'prefix' : 'suffix'
|
|
82
|
+
output[position] = symbol
|
|
83
|
+
format = format.replace('¤', '')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Find and set the thousands separator
|
|
87
|
+
const thousandMatch = format.match(/[^0-9#]/)
|
|
88
|
+
if (thousandMatch) output.thousandSeparator = thousandMatch[0]
|
|
89
|
+
|
|
90
|
+
// Find and set the decimal separator and fraction digits
|
|
91
|
+
const decimalMatch = format.match(/0[^0-9]/)
|
|
92
|
+
if (decimalMatch) {
|
|
93
|
+
output.decimalSeparator = decimalMatch[0].slice(1)
|
|
94
|
+
if (typeof digits !== 'undefined') {
|
|
95
|
+
output.minDecimals = digits
|
|
96
|
+
output.maxDecimals = digits
|
|
97
|
+
} else {
|
|
98
|
+
const fractionDigits = format.split(output.decimalSeparator)[1]
|
|
99
|
+
if (fractionDigits) {
|
|
100
|
+
output.minDecimals = fractionDigits.length
|
|
101
|
+
output.maxDecimals = fractionDigits.length
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return output
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div className="relative">
|
|
110
|
+
<NumericFormat
|
|
111
|
+
id={id}
|
|
112
|
+
className={className}
|
|
113
|
+
decimalSeparator={settings.decimalSeparator}
|
|
114
|
+
thousandSeparator={settings.thousandSeparator}
|
|
115
|
+
decimalScale={settings.maxDecimals}
|
|
116
|
+
onValueChange={!onChange ? undefined : ({ floatValue }, e) => {
|
|
117
|
+
// console.log('onValueChange', floatValue, e)
|
|
118
|
+
if (e.source === 'event') setDontFix(true)
|
|
119
|
+
onChange({ target: { id: id, value: toCents(floatValue) }})
|
|
120
|
+
}}
|
|
121
|
+
onBlur={() => { setDollars(toDollars(value, true))}}
|
|
122
|
+
placeholder={placeholder || '0.00'}
|
|
123
|
+
value={dollars}
|
|
124
|
+
style={{ textIndent: `${prefixWidth}px` }}
|
|
125
|
+
/>
|
|
126
|
+
<span
|
|
127
|
+
class={`absolute top-[1px] bottom-0 left-3 inline-flex items-center select-none text-gray-500 text-sm sm:text-sm/6 ${dollars !== null && settings.prefix == '$' ? 'text-dark' : ''}`}
|
|
128
|
+
>
|
|
129
|
+
{settings.prefix || settings.suffix}
|
|
130
|
+
</span>
|
|
131
|
+
</div>
|
|
132
|
+
)
|
|
133
|
+
}
|