nitro-web 0.0.11 → 0.0.13
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/.eslintrc.json +4 -19
- package/_example/.env +1 -1
- package/_example/client/config.ts +2 -1
- package/_example/client/index.ts +6 -24
- package/_example/components/index.tsx +1 -1
- package/_example/package.json +1 -1
- package/_example/server/config.js +6 -7
- package/_example/tailwind.config.js +1 -1
- package/_example/tsconfig.json +10 -2
- package/_example/types.ts +1 -0
- package/client/{app.js → app.tsx} +101 -99
- package/client/globals.ts +42 -0
- package/client/index.ts +52 -0
- package/client/store.ts +31 -0
- package/components/auth/auth.api.js +3 -2
- package/components/auth/{reset.jsx → reset.tsx} +21 -23
- package/components/auth/{signin.jsx → signin.tsx} +14 -16
- package/components/auth/{signup.jsx → signup.tsx} +15 -17
- package/components/billing/stripe.api.js +2 -1
- package/components/dashboard/{dashboard.jsx → dashboard.tsx} +3 -3
- package/components/partials/element/{accordion.jsx → accordion.tsx} +21 -13
- package/components/partials/element/avatar.tsx +40 -0
- package/components/partials/element/{button.jsx → button.tsx} +20 -16
- package/components/partials/element/{dropdown.jsx → dropdown.tsx} +32 -30
- package/components/partials/element/{github-link.jsx → github-link.tsx} +3 -3
- package/components/partials/element/{initials.jsx → initials.tsx} +11 -2
- package/components/partials/element/{message.jsx → message.tsx} +22 -23
- package/components/partials/element/{modal.jsx → modal.tsx} +4 -3
- package/components/partials/element/{sidebar.jsx → sidebar.tsx} +14 -7
- package/components/partials/element/{tooltip.jsx → tooltip.tsx} +11 -3
- package/components/partials/element/{topbar.jsx → topbar.tsx} +9 -7
- package/components/partials/form/{checkbox.jsx → checkbox.tsx} +13 -13
- package/components/partials/form/drop-handler.tsx +68 -0
- package/components/partials/form/{drop.jsx → drop.tsx} +51 -33
- package/components/partials/form/form-error.tsx +27 -0
- package/components/partials/form/{input-color.jsx → input-color.tsx} +27 -15
- package/components/partials/form/{input-currency.jsx → input-currency.tsx} +37 -32
- package/components/partials/form/{input-date.jsx → input-date.tsx} +4 -3
- package/components/partials/form/{input.jsx → input.tsx} +35 -19
- package/components/partials/form/{location.jsx → location.tsx} +21 -8
- package/components/partials/form/{select.jsx → select.tsx} +142 -143
- package/components/partials/form/{toggle.jsx → toggle.tsx} +10 -2
- package/components/partials/{is-first-render.js → is-first-render.ts} +1 -2
- package/components/partials/layout/layout1.tsx +29 -0
- package/components/partials/layout/{layout2.jsx → layout2.tsx} +3 -3
- package/components/partials/{styleguide.jsx → styleguide.tsx} +16 -19
- package/components/settings/{settings-account.jsx → settings-account.tsx} +9 -13
- package/components/settings/{settings-business.jsx → settings-business.tsx} +7 -8
- package/components/settings/{settings-team--member.jsx → settings-team--member.tsx} +4 -11
- package/components/settings/{settings-team.jsx → settings-team.tsx} +4 -8
- package/components/settings/settings.api.js +1 -0
- package/package.json +17 -31
- package/readme.md +1 -1
- package/server/email/index.js +2 -1
- package/server/index.js +1 -0
- package/server/models/company.js +2 -1
- package/server/models/user.js +2 -1
- package/server/router.js +8 -2
- package/tsconfig.json +36 -0
- package/types/required-globals.d.ts +39 -0
- package/types/util.d.ts +12 -2
- package/types/util.d.ts.map +1 -1
- package/types.ts +43 -0
- package/util.js +14 -34
- package/webpack.config.js +23 -4
- package/_example/types/index.d.ts +0 -13
- package/_example/types/twin.d.ts +0 -19
- package/client/index.js +0 -44
- package/components/partials/element/avatar.jsx +0 -31
- package/components/partials/form/drop-handler.jsx +0 -62
- package/components/partials/form/form-error.jsx +0 -21
- package/components/partials/layout/layout1.jsx +0 -38
- package/types/client/app.d.ts +0 -2
- package/types/client/app.d.ts.map +0 -1
- package/types/client/index.d.ts +0 -29
- package/types/client/index.d.ts.map +0 -1
- package/types/components/auth/reset.d.ts +0 -3
- package/types/components/auth/reset.d.ts.map +0 -1
- package/types/components/auth/signin.d.ts +0 -4
- package/types/components/auth/signin.d.ts.map +0 -1
- package/types/components/auth/signup.d.ts +0 -4
- package/types/components/auth/signup.d.ts.map +0 -1
- package/types/components/dashboard/dashboard.d.ts +0 -4
- package/types/components/dashboard/dashboard.d.ts.map +0 -1
- package/types/components/partials/element/accordion.d.ts +0 -7
- package/types/components/partials/element/accordion.d.ts.map +0 -1
- package/types/components/partials/element/avatar.d.ts +0 -8
- package/types/components/partials/element/avatar.d.ts.map +0 -1
- package/types/components/partials/element/button.d.ts +0 -11
- package/types/components/partials/element/button.d.ts.map +0 -1
- package/types/components/partials/element/dropdown.d.ts +0 -17
- package/types/components/partials/element/dropdown.d.ts.map +0 -1
- package/types/components/partials/element/initials.d.ts +0 -9
- package/types/components/partials/element/initials.d.ts.map +0 -1
- package/types/components/partials/element/message.d.ts +0 -2
- package/types/components/partials/element/message.d.ts.map +0 -1
- package/types/components/partials/element/modal.d.ts +0 -10
- package/types/components/partials/element/modal.d.ts.map +0 -1
- package/types/components/partials/element/sidebar.d.ts +0 -6
- package/types/components/partials/element/sidebar.d.ts.map +0 -1
- package/types/components/partials/element/tooltip.d.ts +0 -8
- package/types/components/partials/element/tooltip.d.ts.map +0 -1
- package/types/components/partials/element/topbar.d.ts +0 -8
- package/types/components/partials/element/topbar.d.ts.map +0 -1
- package/types/components/partials/form/checkbox.d.ts +0 -14
- package/types/components/partials/form/checkbox.d.ts.map +0 -1
- package/types/components/partials/form/drop-handler.d.ts +0 -6
- package/types/components/partials/form/drop-handler.d.ts.map +0 -1
- package/types/components/partials/form/drop.d.ts +0 -11
- package/types/components/partials/form/drop.d.ts.map +0 -1
- package/types/components/partials/form/form-error.d.ts +0 -6
- package/types/components/partials/form/form-error.d.ts.map +0 -1
- package/types/components/partials/form/input-color.d.ts +0 -10
- package/types/components/partials/form/input-color.d.ts.map +0 -1
- package/types/components/partials/form/input-currency.d.ts +0 -10
- package/types/components/partials/form/input-currency.d.ts.map +0 -1
- package/types/components/partials/form/input.d.ts +0 -9
- package/types/components/partials/form/input.d.ts.map +0 -1
- package/types/components/partials/form/location.d.ts +0 -12
- package/types/components/partials/form/location.d.ts.map +0 -1
- package/types/components/partials/form/select.d.ts +0 -27
- package/types/components/partials/form/select.d.ts.map +0 -1
- package/types/components/partials/form/toggle.d.ts +0 -9
- package/types/components/partials/form/toggle.d.ts.map +0 -1
- package/types/components/partials/is-first-render.d.ts +0 -2
- package/types/components/partials/is-first-render.d.ts.map +0 -1
- package/types/components/partials/layout/layout1.d.ts +0 -13
- package/types/components/partials/layout/layout1.d.ts.map +0 -1
- package/types/components/partials/layout/layout2.d.ts +0 -4
- package/types/components/partials/layout/layout2.d.ts.map +0 -1
- package/types/components/partials/not-found.d.ts +0 -2
- package/types/components/partials/not-found.d.ts.map +0 -1
- package/types/components/partials/styleguide.d.ts +0 -4
- package/types/components/partials/styleguide.d.ts.map +0 -1
- package/types/components/settings/settings-account.d.ts +0 -6
- package/types/components/settings/settings-account.d.ts.map +0 -1
- package/types/components/settings/settings-business.d.ts +0 -4
- package/types/components/settings/settings-business.d.ts.map +0 -1
- package/types/components/settings/settings-team--member.d.ts +0 -5
- package/types/components/settings/settings-team--member.d.ts.map +0 -1
- package/types/components/settings/settings-team.d.ts +0 -4
- package/types/components/settings/settings-team.d.ts.map +0 -1
- /package/components/partials/{not-found.jsx → not-found.tsx} +0 -0
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
// Todo: show correct message type, e.g. error, warning, info, success `${store.message.type || 'success'}`
|
|
2
|
-
import { isObject, isString, queryObject } from '
|
|
2
|
+
import { isObject, isString, queryObject } from 'nitro-web/util'
|
|
3
3
|
import { Transition } from '@headlessui/react'
|
|
4
4
|
import { CheckCircleIcon } from '@heroicons/react/24/outline'
|
|
5
5
|
import { XMarkIcon } from '@heroicons/react/20/solid'
|
|
6
|
+
import { MessageObject } from 'types'
|
|
6
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Shows a message
|
|
10
|
+
* Triggered by navigating to a link with a valid query string, or
|
|
11
|
+
* by setting store.message to a string or more explicitly, to an object
|
|
12
|
+
**/
|
|
7
13
|
export function Message() {
|
|
8
|
-
/**
|
|
9
|
-
* Shows a message
|
|
10
|
-
* Triggered by navigating to a link with a valid query string, or
|
|
11
|
-
* by setting store.message to a string or more explicitly, to an object:
|
|
12
|
-
* {
|
|
13
|
-
* text: {string|JSX} - Text to be shown
|
|
14
|
-
* type: <string> - 'warning', 'error', 'info', 'success' (default)
|
|
15
|
-
* timeout: <integer> - Seconds to automatically close the message, 0 = never, (default 9s)
|
|
16
|
-
* }
|
|
17
|
-
*/
|
|
18
14
|
const devDontHide = false
|
|
19
|
-
const [store, setStore] =
|
|
15
|
+
const [store, setStore] = useTracked()
|
|
20
16
|
const [visible, setVisible] = useState(false)
|
|
21
17
|
const location = useLocation()
|
|
22
18
|
const messageQueryMap = {
|
|
@@ -39,11 +35,12 @@ export function Message() {
|
|
|
39
35
|
useEffect(() => {
|
|
40
36
|
// Finds a message in a query string and show it
|
|
41
37
|
let message
|
|
42
|
-
|
|
43
|
-
for (
|
|
38
|
+
const query = queryObject(location.search, true)
|
|
39
|
+
for (const key in query) {
|
|
44
40
|
if (!query.hasOwnProperty(key)) continue
|
|
45
|
-
for (
|
|
41
|
+
for (const key2 in messageQueryMap) {
|
|
46
42
|
if (key != key2) continue
|
|
43
|
+
// @ts-expect-error
|
|
47
44
|
message = { ...messageQueryMap[key] }
|
|
48
45
|
if (query[key] !== true) message.text = decodeURIComponent(query[key])
|
|
49
46
|
}
|
|
@@ -53,20 +50,21 @@ export function Message() {
|
|
|
53
50
|
|
|
54
51
|
useEffect(() => {
|
|
55
52
|
// Message detection and autohiding
|
|
56
|
-
|
|
53
|
+
const now = new Date().getTime()
|
|
54
|
+
const messageObject = store.message as MessageObject
|
|
57
55
|
|
|
58
56
|
if (!store.message) {
|
|
59
57
|
return
|
|
60
58
|
// Convert a string into a message object
|
|
61
59
|
} else if (isString(store.message)) {
|
|
62
|
-
setStore(s => ({ ...s, message: { type: 'success', text: store.message, date: now }}))
|
|
60
|
+
setStore(s => ({ ...s, message: { type: 'success', text: store.message as string, date: now }}))
|
|
63
61
|
// Add a date to the message
|
|
64
|
-
} else if (!
|
|
65
|
-
setStore(s => ({ ...s, message: { ...
|
|
62
|
+
} else if (!messageObject.date) {
|
|
63
|
+
setStore(s => ({ ...s, message: { ...messageObject, date: now }}))
|
|
66
64
|
// Show message and hide it again after some time. Send back cleanup if store.message changes
|
|
67
|
-
} else if (
|
|
68
|
-
|
|
69
|
-
if (
|
|
65
|
+
} else if (messageObject && now - 500 < messageObject.date) {
|
|
66
|
+
const timeout1 = setTimeout(() => setVisible(true), 50)
|
|
67
|
+
if (messageObject.timeout !== 0 && !devDontHide) var timeout2 = setTimeout(hide, messageObject.timeout || 5000)
|
|
70
68
|
return () => {
|
|
71
69
|
clearTimeout(timeout1)
|
|
72
70
|
clearTimeout(timeout2)
|
|
@@ -99,7 +97,8 @@ export function Message() {
|
|
|
99
97
|
<CheckCircleIcon aria-hidden="true" className="size-6 text-green-400" />
|
|
100
98
|
</div>
|
|
101
99
|
<div className="ml-3 flex-1 pt-0.5">
|
|
102
|
-
<p className="text-sm font-medium text-gray-900">{store.message?.text}
|
|
100
|
+
<p className="text-sm font-medium text-gray-900">{typeof store.message === 'object' && store.message?.text}
|
|
101
|
+
</p>
|
|
103
102
|
{/* <p className="mt-1 text-sm text-gray-500">{store.message.text}</p> */}
|
|
104
103
|
</div>
|
|
105
104
|
<div className="ml-4 flex shrink-0">
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
1
2
|
// todo: finish tailwind conversion
|
|
2
3
|
import { css } from 'twin.macro'
|
|
3
|
-
import { IsFirstRender } from '
|
|
4
|
-
import SvgX1 from '
|
|
4
|
+
import { IsFirstRender } from 'nitro-web'
|
|
5
|
+
import SvgX1 from 'nitro-web/client/imgs/icons/x1.svg'
|
|
5
6
|
|
|
6
7
|
export function Modal({ show, setShow, children, className, maxWidth, minHeight, dismissable = true }) {
|
|
7
8
|
const [state, setState] = useState()
|
|
@@ -113,7 +114,7 @@ export function Modal({ show, setShow, children, className, maxWidth, minHeight,
|
|
|
113
114
|
)
|
|
114
115
|
}
|
|
115
116
|
|
|
116
|
-
const style =
|
|
117
|
+
const style = css`
|
|
117
118
|
/* Modal structure */
|
|
118
119
|
& {
|
|
119
120
|
position: fixed;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Component: https://tailwindui.com/components/application-ui/application-shells/sidebar#component-a69d85b6237ea2ad506c00ef1cd39a38
|
|
2
2
|
import { Dialog, DialogBackdrop, DialogPanel, TransitionChild } from '@headlessui/react'
|
|
3
|
-
import avatarImg from '
|
|
3
|
+
import avatarImg from 'nitro-web/client/imgs/avatar.jpg'
|
|
4
4
|
import {
|
|
5
5
|
Bars3Icon,
|
|
6
6
|
HomeIcon,
|
|
@@ -12,11 +12,17 @@ import {
|
|
|
12
12
|
|
|
13
13
|
const sidebarWidth = 'lg:w-80'
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
export type SidebarProps = {
|
|
16
|
+
Logo: React.FC<{ width?: string, height?: string, alt?: string }>;
|
|
17
|
+
menu?: { name: string; to: string; Icon: React.FC<{ className?: string }> }[]
|
|
18
|
+
links?: { name: string; to: string; initial: string }[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function classNames(...classes: string[]) {
|
|
16
22
|
return classes.filter(Boolean).join(' ')
|
|
17
23
|
}
|
|
18
24
|
|
|
19
|
-
export function Sidebar({ Logo, menu, links }) {
|
|
25
|
+
export function Sidebar({ Logo, menu, links }: SidebarProps) {
|
|
20
26
|
const [sidebarOpen, setSidebarOpen] = useState(false)
|
|
21
27
|
return (
|
|
22
28
|
<>
|
|
@@ -65,11 +71,12 @@ export function Sidebar({ Logo, menu, links }) {
|
|
|
65
71
|
)
|
|
66
72
|
}
|
|
67
73
|
|
|
68
|
-
function SidebarContents ({ Logo, menu, links }) {
|
|
74
|
+
function SidebarContents ({ Logo, menu, links }: SidebarProps) {
|
|
69
75
|
const location = useLocation()
|
|
70
|
-
const [
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
const [store] = useTracked()
|
|
77
|
+
const user = store.user
|
|
78
|
+
|
|
79
|
+
function isActive(path: string) {
|
|
73
80
|
if (path == '/' && location.pathname == path) return 'is-active'
|
|
74
81
|
else if (path != '/' && location.pathname.match(`^${path}`)) return 'is-active'
|
|
75
82
|
else return ''
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
// todo: finish tailwind conversion
|
|
2
2
|
import { css } from 'twin.macro'
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
type TooltipProps = {
|
|
5
|
+
children: React.ReactNode
|
|
6
|
+
className?: string
|
|
7
|
+
classNamePopup?: string
|
|
8
|
+
isSmall?: boolean
|
|
9
|
+
text?: React.ReactNode
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Tooltip({ text, children, className, classNamePopup, isSmall }: TooltipProps) {
|
|
5
13
|
return (
|
|
6
14
|
<div class={`${className} relative inline-block align-middle`} css={style}>
|
|
7
15
|
{
|
|
8
|
-
text
|
|
16
|
+
text
|
|
9
17
|
? <>
|
|
10
18
|
<div class="tooltip-trigger ">{children}</div>
|
|
11
19
|
<div class={`tooltip-popup ${classNamePopup||''} ${isSmall ? 'is-small' : ''}`}>{text}</div>
|
|
@@ -16,7 +24,7 @@ export function Tooltip({ text, children, className, classNamePopup, isSmall })
|
|
|
16
24
|
)
|
|
17
25
|
}
|
|
18
26
|
|
|
19
|
-
const style =
|
|
27
|
+
const style = css`
|
|
20
28
|
.tooltip-popup {
|
|
21
29
|
position: absolute;
|
|
22
30
|
display: block;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
type TopbarProps = {
|
|
2
|
+
title: React.ReactNode
|
|
3
|
+
subtitle?: React.ReactNode
|
|
4
|
+
submenu?: React.ReactNode
|
|
5
|
+
btns?: React.ReactNode
|
|
6
|
+
className?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function Topbar({ title, subtitle, submenu, btns, className }: TopbarProps) {
|
|
8
10
|
return (
|
|
9
11
|
<div class={`flex justify-between items-end mb-6 ${className||''}`}>
|
|
10
12
|
<div class="flex flex-col min-h-12">
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
+
type CheckboxProps = {
|
|
2
|
+
name: string
|
|
3
|
+
/** The id of the checkbox (used for radios) **/
|
|
4
|
+
id?: string
|
|
5
|
+
size?: 'md' | 'sm'
|
|
6
|
+
subtext?: string|React.ReactNode
|
|
7
|
+
text?: string|React.ReactNode
|
|
8
|
+
type?: 'checkbox' | 'radio' | 'toggle'
|
|
9
|
+
[key: string]: unknown
|
|
10
|
+
}
|
|
1
11
|
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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 }) {
|
|
12
|
+
export function Checkbox({ name, id, size='sm', subtext, text, type='checkbox', ...props }: CheckboxProps) {
|
|
13
|
+
// Checkbox/radio/toggle component
|
|
14
|
+
// https://tailwindui.com/components/application-ui/forms/checkboxes#component-744ed4fa65ba36b925701eb4da5c6e31
|
|
15
15
|
if (!name) throw new Error('Checkbox requires a `name` prop')
|
|
16
16
|
id = id || name
|
|
17
17
|
return (
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
type DropHandlerProps = {
|
|
2
|
+
onDrop: (files: FileList) => void
|
|
3
|
+
children: React.ReactNode
|
|
4
|
+
className?: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const DropHandler = ({ onDrop, children, className }: DropHandlerProps) => {
|
|
8
|
+
const dropRef = useRef<HTMLDivElement>(null)
|
|
9
|
+
let dragCounter = useRef(0).current
|
|
10
|
+
const [dragging, setDragging] = useState(false)
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const div = dropRef.current
|
|
14
|
+
div?.addEventListener('dragenter', handleDragIn)
|
|
15
|
+
div?.addEventListener('dragleave', handleDragOut)
|
|
16
|
+
div?.addEventListener('dragover', handleDragOver)
|
|
17
|
+
div?.addEventListener('drop', handleDrop)
|
|
18
|
+
return () => {
|
|
19
|
+
div?.removeEventListener('dragenter', handleDragIn)
|
|
20
|
+
div?.removeEventListener('dragleave', handleDragOut)
|
|
21
|
+
div?.removeEventListener('dragover', handleDragOver)
|
|
22
|
+
div?.removeEventListener('drop', handleDrop)
|
|
23
|
+
}
|
|
24
|
+
}, [])
|
|
25
|
+
|
|
26
|
+
const handleDragIn = (e: DragEvent) => {
|
|
27
|
+
e.preventDefault()
|
|
28
|
+
e.stopPropagation()
|
|
29
|
+
dragCounter++
|
|
30
|
+
if (e.dataTransfer?.items && e.dataTransfer.items.length > 0) {
|
|
31
|
+
setDragging(true)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const handleDragOut = (e: DragEvent) => {
|
|
36
|
+
e.preventDefault()
|
|
37
|
+
e.stopPropagation()
|
|
38
|
+
dragCounter--
|
|
39
|
+
if (dragCounter === 0) {
|
|
40
|
+
setDragging(false)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const handleDragOver = (e: DragEvent) => {
|
|
45
|
+
e.preventDefault()
|
|
46
|
+
e.stopPropagation()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const handleDrop = (e: DragEvent) => {
|
|
50
|
+
e.preventDefault()
|
|
51
|
+
e.stopPropagation()
|
|
52
|
+
setDragging(false)
|
|
53
|
+
if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
|
|
54
|
+
onDrop(e.dataTransfer.files)
|
|
55
|
+
// e.dataTransfer.clearData() // causes an error in firefox
|
|
56
|
+
dragCounter = 0
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div
|
|
62
|
+
ref={dropRef}
|
|
63
|
+
class={`${className} relative w-full p-[20px] border-2 border-dashed border-input-border rounded-md ${dragging ? 'border-primary before:content-[""] before:absolute before:inset-0 before:bg-primary before:opacity-5' : ''}`}
|
|
64
|
+
>
|
|
65
|
+
{children}
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
@@ -1,44 +1,62 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { isRegex, deepFind, s3Image } from 'nitro-web/util'
|
|
3
|
+
import { DropHandler } from 'nitro-web'
|
|
4
|
+
import noImage from 'nitro-web/client/imgs/no-image.svg'
|
|
5
|
+
import { Errors, MonasteryImage } from 'types'
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
type DropProps = {
|
|
8
|
+
awsUrl?: string
|
|
9
|
+
className?: string
|
|
10
|
+
/** Optional ID for the input element. Defaults to name if not provided */
|
|
11
|
+
id?: string
|
|
12
|
+
/** Field name or path on state (used to match errors), e.g. 'avatar', 'company.avatar' */
|
|
13
|
+
name: string
|
|
14
|
+
/** Called when file is selected or dropped */
|
|
15
|
+
onChange?: any // (event: { target: { id: string, value: File|FileList } }) => void
|
|
16
|
+
/** Whether to allow multiple file selection */
|
|
17
|
+
multiple?: boolean
|
|
18
|
+
/** State object to get the value and check errors against */
|
|
19
|
+
state?: {
|
|
20
|
+
errors?: Errors
|
|
21
|
+
[key: string]: unknown
|
|
22
|
+
}
|
|
23
|
+
/** Props to pass to the input element */
|
|
24
|
+
[key: string]: unknown
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type Image = File | FileList | MonasteryImage | null
|
|
28
|
+
|
|
29
|
+
export function Drop({ awsUrl, className, id, name, onChange, multiple, state, ...props }: DropProps) {
|
|
12
30
|
if (!name) throw new Error('Drop component requires a `name` prop')
|
|
31
|
+
let value: Image = null
|
|
32
|
+
let error: Error | unknown
|
|
13
33
|
const inputId = id ||name
|
|
14
|
-
const stateRef = useRef()
|
|
15
34
|
const [urls, setUrls] = useState([])
|
|
35
|
+
const stateRef = useRef(state)
|
|
16
36
|
stateRef.current = state
|
|
17
37
|
|
|
18
38
|
// Input is always controlled if state is passed in
|
|
19
|
-
if (props.value)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
value = deepFind(state, name)
|
|
23
|
-
if (typeof value == 'undefined') value = null
|
|
24
|
-
}
|
|
39
|
+
if (props.value) value = props.value as Image
|
|
40
|
+
else if (typeof state == 'object') value = deepFind(state, name) as Image
|
|
41
|
+
if (typeof value == 'undefined') value = null
|
|
25
42
|
|
|
26
43
|
// An error matches this input path
|
|
27
|
-
for (
|
|
28
|
-
if (isRegex(name) && (item.title||'').match(name))
|
|
44
|
+
for (const item of (state?.errors as Errors[] || [])) {
|
|
45
|
+
if (isRegex(name) && (item.title||'').match(name)) error = item
|
|
29
46
|
else if (item.title == name) error = item
|
|
30
47
|
}
|
|
31
48
|
|
|
32
49
|
useEffect(() => {
|
|
33
|
-
(async () => setUrls(await getUrls(value)))()
|
|
50
|
+
(async () => setUrls(await getUrls(value as File | FileList | MonasteryImage | null)))()
|
|
34
51
|
}, [value])
|
|
35
52
|
|
|
36
|
-
function tryAgain (e) {
|
|
53
|
+
function tryAgain (e: { preventDefault: Function }) {
|
|
37
54
|
e.preventDefault()
|
|
38
55
|
// clear file input to allow reupload
|
|
39
|
-
document.getElementById(name)
|
|
56
|
+
const input = document.getElementById(name) as HTMLInputElement
|
|
57
|
+
if (input) input.value = ''
|
|
40
58
|
if (onChange) {
|
|
41
|
-
const errors = (stateRef
|
|
59
|
+
const errors = (stateRef?.current?.errors || []).filter((e: Errors[]) => e?.title != name)
|
|
42
60
|
onChange({
|
|
43
61
|
// remove file from state
|
|
44
62
|
target: { id: name, value: null },
|
|
@@ -48,28 +66,28 @@ export function Drop({ awsUrl, className, id, name, onChange, multiple, state, .
|
|
|
48
66
|
}
|
|
49
67
|
}
|
|
50
68
|
|
|
51
|
-
async function onFileAttach (files
|
|
69
|
+
async function onFileAttach (files: FileList) {
|
|
52
70
|
// files is a FileList object
|
|
53
71
|
if (onChange) onChange({ target: { id: name, value: multiple ? files : files[0] } })
|
|
54
72
|
}
|
|
55
73
|
|
|
56
|
-
async function getUrls(objectOrFileListItem) {
|
|
74
|
+
async function getUrls(objectOrFileListItem: File | FileList | MonasteryImage | null) {
|
|
57
75
|
/**
|
|
58
76
|
* @param {object|FileList} objectOrFileListItem - FileList object or monastery image object
|
|
59
77
|
* @returns {Promise} - Resolves to an array of image URLs
|
|
60
78
|
*/
|
|
61
79
|
// Make sure FileLists are converted to a real array
|
|
62
80
|
if (!objectOrFileListItem) return []
|
|
63
|
-
const array =
|
|
64
|
-
return Promise.all(array.map((
|
|
81
|
+
const array = 'length' in objectOrFileListItem ? Array.from(objectOrFileListItem) : [objectOrFileListItem]
|
|
82
|
+
return Promise.all(array.map((item) => {
|
|
65
83
|
return new Promise((resolve, reject) => {
|
|
66
|
-
if (
|
|
84
|
+
if ('lastModified' in item) {
|
|
67
85
|
const reader = new FileReader()
|
|
68
86
|
reader.onload = () => resolve(reader.result)
|
|
69
87
|
reader.onerror = reject
|
|
70
|
-
reader.readAsDataURL(
|
|
88
|
+
reader.readAsDataURL(item)
|
|
71
89
|
} else {
|
|
72
|
-
resolve(s3Image(awsUrl,
|
|
90
|
+
resolve(s3Image(awsUrl, item))
|
|
73
91
|
}
|
|
74
92
|
})
|
|
75
93
|
}))
|
|
@@ -86,12 +104,12 @@ export function Drop({ awsUrl, className, id, name, onChange, multiple, state, .
|
|
|
86
104
|
{...props}
|
|
87
105
|
id={inputId}
|
|
88
106
|
type="file"
|
|
89
|
-
onChange={(e) => onFileAttach(e.target.files)}
|
|
107
|
+
onChange={(e) => onFileAttach(e.target.files as FileList)}
|
|
90
108
|
hidden
|
|
91
109
|
/>
|
|
92
110
|
<DropHandler
|
|
93
111
|
onDrop={onFileAttach}
|
|
94
|
-
|
|
112
|
+
className="flex flex-column justify-center items-center text-center gap-2 text-grey-300 text-sm px-8 min-h-[300px]"
|
|
95
113
|
>
|
|
96
114
|
{
|
|
97
115
|
!value &&
|
|
@@ -104,7 +122,7 @@ export function Drop({ awsUrl, className, id, name, onChange, multiple, state, .
|
|
|
104
122
|
</>
|
|
105
123
|
}
|
|
106
124
|
{
|
|
107
|
-
value &&
|
|
125
|
+
!!value &&
|
|
108
126
|
<>
|
|
109
127
|
{
|
|
110
128
|
urls.map((url, i) => (
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Errors } from 'nitro-web/types'
|
|
2
|
+
|
|
3
|
+
type FormError = {
|
|
4
|
+
state: { errors: Errors },
|
|
5
|
+
// display all errors except these field titles, e.g. ['name', 'address']
|
|
6
|
+
fields?: Array<string>,
|
|
7
|
+
className?: string,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function FormError({ state, fields, className }: FormError) {
|
|
11
|
+
// A catch all error element that should be placed next to the submit button
|
|
12
|
+
let error: { title: string, detail: string } | undefined
|
|
13
|
+
for (const item of state.errors || []) {
|
|
14
|
+
if (!item.title || item.title.match(/^(error|invalid)$/i) || (fields && !fields.includes(item.title))) {
|
|
15
|
+
error = item
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return (
|
|
19
|
+
<>
|
|
20
|
+
{error ? (
|
|
21
|
+
<div class={`text-danger mt-1 text-sm ${className||''}`}>
|
|
22
|
+
{error.detail}
|
|
23
|
+
</div>
|
|
24
|
+
) : null}
|
|
25
|
+
</>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
@@ -1,28 +1,38 @@
|
|
|
1
1
|
import { css } from 'twin.macro'
|
|
2
|
-
import { hsvaToHex, hexToHsva, validHex } from '@uiw/color-convert'
|
|
2
|
+
import { hsvaToHex, hexToHsva, validHex, HsvaColor } from '@uiw/color-convert'
|
|
3
3
|
import Saturation from '@uiw/react-color-saturation'
|
|
4
4
|
import Hue from '@uiw/react-color-hue'
|
|
5
|
-
import { Dropdown } from '
|
|
6
|
-
import
|
|
5
|
+
import { Dropdown, util } from 'nitro-web'
|
|
6
|
+
import React from 'react'
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
type InputColorProps = {
|
|
9
|
+
className?: string
|
|
10
|
+
defaultColor?: string
|
|
11
|
+
iconEl?: React.ReactNode
|
|
12
|
+
id?: string
|
|
13
|
+
onChange?: (e: { target: { id: string, value: string } }) => void
|
|
14
|
+
value?: string
|
|
15
|
+
[key: string]: unknown
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function InputColor({ className, defaultColor='#333', iconEl, id, onChange, value, ...props }: InputColorProps) {
|
|
9
19
|
const [lastChanged, setLastChanged] = useState(() => `ic-${Date.now()}`)
|
|
10
20
|
const isInvalid = className?.includes('is-invalid') ? 'is-invalid' : ''
|
|
11
21
|
|
|
12
|
-
function onInputChange(e) {
|
|
22
|
+
function onInputChange(e: { target: { id: string, value: string } }) {
|
|
13
23
|
setLastChanged(`ic-${Date.now()}`)
|
|
14
24
|
if (onChange) onChange(e)
|
|
15
25
|
}
|
|
16
26
|
|
|
17
27
|
return (
|
|
18
|
-
<Dropdown
|
|
19
|
-
|
|
28
|
+
<Dropdown
|
|
29
|
+
dir="bottom-left"
|
|
20
30
|
menuToggles={false}
|
|
21
31
|
menuChildren={
|
|
22
32
|
<ColorPicker key={lastChanged} defaultColor={defaultColor} id={id} value={value} onChange={onChange} />
|
|
23
33
|
}
|
|
24
34
|
>
|
|
25
|
-
<div className="grid grid-cols-1">
|
|
35
|
+
<div className="grid grid-cols-1" css={style}>
|
|
26
36
|
{iconEl}
|
|
27
37
|
<input
|
|
28
38
|
{...props}
|
|
@@ -30,7 +40,7 @@ export function InputColor({ className, defaultColor='#333', iconEl, id, onChang
|
|
|
30
40
|
id={id}
|
|
31
41
|
value={value}
|
|
32
42
|
onChange={onInputChange}
|
|
33
|
-
onBlur={() => !validHex(value) && onInputChange({ target: { id: id, value: '' }})}
|
|
43
|
+
onBlur={() => !validHex(value||'') && onInputChange({ target: { id: id || '', value: '' }})}
|
|
34
44
|
autoComplete="off"
|
|
35
45
|
/>
|
|
36
46
|
</div>
|
|
@@ -38,17 +48,18 @@ export function InputColor({ className, defaultColor='#333', iconEl, id, onChang
|
|
|
38
48
|
)
|
|
39
49
|
}
|
|
40
50
|
|
|
41
|
-
function ColorPicker({ id, onChange, value, defaultColor }) {
|
|
51
|
+
function ColorPicker({ id='', onChange, value='', defaultColor='' }: InputColorProps) {
|
|
42
52
|
const [hsva, setHsva] = useState(() => hexToHsva(validHex(value) ? value : defaultColor))
|
|
43
|
-
const [debounce] = useState(() => throttle(callOnChange, 50))
|
|
53
|
+
const [debounce] = useState(() => util.throttle(callOnChange, 50))
|
|
44
54
|
|
|
45
|
-
function callOnChange(newHsva) {
|
|
46
|
-
onChange({ target: { id: id, value: hsvaToHex(newHsva) }})
|
|
55
|
+
function callOnChange(newHsva: HsvaColor) {
|
|
56
|
+
if (onChange) onChange({ target: { id: id, value: hsvaToHex(newHsva) }})
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
return (
|
|
50
60
|
<>
|
|
51
61
|
<Saturation
|
|
62
|
+
css={style}
|
|
52
63
|
hsva={hsva}
|
|
53
64
|
onChange={(newHsva) => {
|
|
54
65
|
setHsva(newHsva)
|
|
@@ -56,6 +67,7 @@ function ColorPicker({ id, onChange, value, defaultColor }) {
|
|
|
56
67
|
}}
|
|
57
68
|
/>
|
|
58
69
|
<Hue
|
|
70
|
+
css={style}
|
|
59
71
|
hue={hsva.h}
|
|
60
72
|
onChange={(newHue) => {
|
|
61
73
|
setHsva({ ...hsva, ...newHue })
|
|
@@ -66,8 +78,8 @@ function ColorPicker({ id, onChange, value, defaultColor }) {
|
|
|
66
78
|
)
|
|
67
79
|
}
|
|
68
80
|
|
|
69
|
-
const style =
|
|
70
|
-
|
|
81
|
+
const style = css`
|
|
82
|
+
/////////////////////
|
|
71
83
|
.w-color-interactive {
|
|
72
84
|
width: 100% !important;
|
|
73
85
|
height: 150px !important;
|