nitro-web 0.0.10 → 0.0.12

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.
Files changed (144) hide show
  1. package/.eslintrc.json +4 -19
  2. package/_example/.env +1 -1
  3. package/_example/client/config.ts +2 -1
  4. package/_example/client/index.ts +6 -24
  5. package/_example/components/index.tsx +1 -1
  6. package/_example/package.json +0 -1
  7. package/_example/server/config.js +6 -7
  8. package/_example/tailwind.config.js +1 -1
  9. package/_example/tsconfig.json +5 -1
  10. package/_example/types.ts +1 -0
  11. package/client/{app.js → app.tsx} +101 -99
  12. package/client/globals.ts +42 -0
  13. package/client/index.ts +52 -0
  14. package/client/store.ts +31 -0
  15. package/components/auth/auth.api.js +3 -2
  16. package/components/auth/{reset.jsx → reset.tsx} +21 -23
  17. package/components/auth/{signin.jsx → signin.tsx} +14 -16
  18. package/components/auth/{signup.jsx → signup.tsx} +15 -17
  19. package/components/billing/stripe.api.js +2 -1
  20. package/components/dashboard/{dashboard.jsx → dashboard.tsx} +3 -3
  21. package/components/partials/element/{accordion.jsx → accordion.tsx} +21 -13
  22. package/components/partials/element/avatar.tsx +40 -0
  23. package/components/partials/element/{button.jsx → button.tsx} +20 -16
  24. package/components/partials/element/{dropdown.jsx → dropdown.tsx} +32 -30
  25. package/components/partials/element/github-link.tsx +16 -0
  26. package/components/partials/element/{initials.jsx → initials.tsx} +11 -2
  27. package/components/partials/element/{message.jsx → message.tsx} +22 -23
  28. package/components/partials/element/{modal.jsx → modal.tsx} +4 -3
  29. package/components/partials/element/{sidebar.jsx → sidebar.tsx} +14 -7
  30. package/components/partials/element/{tooltip.jsx → tooltip.tsx} +11 -3
  31. package/components/partials/element/{topbar.jsx → topbar.tsx} +9 -7
  32. package/components/partials/form/{checkbox.jsx → checkbox.tsx} +13 -13
  33. package/components/partials/form/drop-handler.tsx +68 -0
  34. package/components/partials/form/{drop.jsx → drop.tsx} +51 -33
  35. package/components/partials/form/form-error.tsx +27 -0
  36. package/components/partials/form/{input-color.jsx → input-color.tsx} +27 -15
  37. package/components/partials/form/{input-currency.jsx → input-currency.tsx} +37 -32
  38. package/components/partials/form/{input-date.jsx → input-date.tsx} +4 -3
  39. package/components/partials/form/{input.jsx → input.tsx} +35 -19
  40. package/components/partials/form/{location.jsx → location.tsx} +21 -8
  41. package/components/partials/form/{select.jsx → select.tsx} +142 -143
  42. package/components/partials/form/{toggle.jsx → toggle.tsx} +10 -2
  43. package/components/partials/{is-first-render.js → is-first-render.ts} +1 -2
  44. package/components/partials/layout/layout1.tsx +29 -0
  45. package/components/partials/layout/{layout2.jsx → layout2.tsx} +3 -3
  46. package/components/partials/{styleguide.jsx → styleguide.tsx} +16 -19
  47. package/components/settings/{settings-account.jsx → settings-account.tsx} +9 -13
  48. package/components/settings/{settings-business.jsx → settings-business.tsx} +7 -8
  49. package/components/settings/{settings-team--member.jsx → settings-team--member.tsx} +4 -11
  50. package/components/settings/{settings-team.jsx → settings-team.tsx} +4 -8
  51. package/components/settings/settings.api.js +1 -0
  52. package/package.json +14 -28
  53. package/readme.md +1 -1
  54. package/server/email/index.js +2 -1
  55. package/server/index.js +1 -0
  56. package/server/models/company.js +2 -1
  57. package/server/models/user.js +2 -1
  58. package/server/router.js +3 -2
  59. package/tsconfig.json +31 -0
  60. package/types/required-globals.d.ts +39 -0
  61. package/types/util.d.ts +12 -2
  62. package/types/util.d.ts.map +1 -1
  63. package/types.ts +43 -0
  64. package/util.js +14 -34
  65. package/webpack.config.js +23 -4
  66. package/_example/types/index.d.ts +0 -13
  67. package/_example/types/twin.d.ts +0 -19
  68. package/client/index.js +0 -44
  69. package/components/partials/element/avatar.jsx +0 -31
  70. package/components/partials/element/github-link.jsx +0 -14
  71. package/components/partials/form/drop-handler.jsx +0 -62
  72. package/components/partials/form/form-error.jsx +0 -21
  73. package/components/partials/layout/layout1.jsx +0 -38
  74. package/types/client/app.d.ts +0 -2
  75. package/types/client/app.d.ts.map +0 -1
  76. package/types/client/index.d.ts +0 -29
  77. package/types/client/index.d.ts.map +0 -1
  78. package/types/components/auth/reset.d.ts +0 -3
  79. package/types/components/auth/reset.d.ts.map +0 -1
  80. package/types/components/auth/signin.d.ts +0 -4
  81. package/types/components/auth/signin.d.ts.map +0 -1
  82. package/types/components/auth/signup.d.ts +0 -4
  83. package/types/components/auth/signup.d.ts.map +0 -1
  84. package/types/components/dashboard/dashboard.d.ts +0 -4
  85. package/types/components/dashboard/dashboard.d.ts.map +0 -1
  86. package/types/components/partials/element/accordion.d.ts +0 -7
  87. package/types/components/partials/element/accordion.d.ts.map +0 -1
  88. package/types/components/partials/element/avatar.d.ts +0 -8
  89. package/types/components/partials/element/avatar.d.ts.map +0 -1
  90. package/types/components/partials/element/button.d.ts +0 -11
  91. package/types/components/partials/element/button.d.ts.map +0 -1
  92. package/types/components/partials/element/dropdown.d.ts +0 -17
  93. package/types/components/partials/element/dropdown.d.ts.map +0 -1
  94. package/types/components/partials/element/initials.d.ts +0 -9
  95. package/types/components/partials/element/initials.d.ts.map +0 -1
  96. package/types/components/partials/element/message.d.ts +0 -2
  97. package/types/components/partials/element/message.d.ts.map +0 -1
  98. package/types/components/partials/element/modal.d.ts +0 -10
  99. package/types/components/partials/element/modal.d.ts.map +0 -1
  100. package/types/components/partials/element/sidebar.d.ts +0 -6
  101. package/types/components/partials/element/sidebar.d.ts.map +0 -1
  102. package/types/components/partials/element/tooltip.d.ts +0 -8
  103. package/types/components/partials/element/tooltip.d.ts.map +0 -1
  104. package/types/components/partials/element/topbar.d.ts +0 -8
  105. package/types/components/partials/element/topbar.d.ts.map +0 -1
  106. package/types/components/partials/form/checkbox.d.ts +0 -14
  107. package/types/components/partials/form/checkbox.d.ts.map +0 -1
  108. package/types/components/partials/form/drop-handler.d.ts +0 -6
  109. package/types/components/partials/form/drop-handler.d.ts.map +0 -1
  110. package/types/components/partials/form/drop.d.ts +0 -11
  111. package/types/components/partials/form/drop.d.ts.map +0 -1
  112. package/types/components/partials/form/form-error.d.ts +0 -6
  113. package/types/components/partials/form/form-error.d.ts.map +0 -1
  114. package/types/components/partials/form/input-color.d.ts +0 -10
  115. package/types/components/partials/form/input-color.d.ts.map +0 -1
  116. package/types/components/partials/form/input-currency.d.ts +0 -10
  117. package/types/components/partials/form/input-currency.d.ts.map +0 -1
  118. package/types/components/partials/form/input.d.ts +0 -9
  119. package/types/components/partials/form/input.d.ts.map +0 -1
  120. package/types/components/partials/form/location.d.ts +0 -12
  121. package/types/components/partials/form/location.d.ts.map +0 -1
  122. package/types/components/partials/form/select.d.ts +0 -27
  123. package/types/components/partials/form/select.d.ts.map +0 -1
  124. package/types/components/partials/form/toggle.d.ts +0 -9
  125. package/types/components/partials/form/toggle.d.ts.map +0 -1
  126. package/types/components/partials/is-first-render.d.ts +0 -2
  127. package/types/components/partials/is-first-render.d.ts.map +0 -1
  128. package/types/components/partials/layout/layout1.d.ts +0 -13
  129. package/types/components/partials/layout/layout1.d.ts.map +0 -1
  130. package/types/components/partials/layout/layout2.d.ts +0 -4
  131. package/types/components/partials/layout/layout2.d.ts.map +0 -1
  132. package/types/components/partials/not-found.d.ts +0 -2
  133. package/types/components/partials/not-found.d.ts.map +0 -1
  134. package/types/components/partials/styleguide.d.ts +0 -4
  135. package/types/components/partials/styleguide.d.ts.map +0 -1
  136. package/types/components/settings/settings-account.d.ts +0 -6
  137. package/types/components/settings/settings-account.d.ts.map +0 -1
  138. package/types/components/settings/settings-business.d.ts +0 -4
  139. package/types/components/settings/settings-business.d.ts.map +0 -1
  140. package/types/components/settings/settings-team--member.d.ts +0 -5
  141. package/types/components/settings/settings-team--member.d.ts.map +0 -1
  142. package/types/components/settings/settings-team.d.ts +0 -4
  143. package/types/components/settings/settings-team.d.ts.map +0 -1
  144. /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 '../../../util.js'
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] = sharedStore.useTracked()
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
- let query = queryObject(location.search, true)
43
- for (let key in query) {
38
+ const query = queryObject(location.search, true)
39
+ for (const key in query) {
44
40
  if (!query.hasOwnProperty(key)) continue
45
- for (let key2 in messageQueryMap) {
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
- let now = new Date().getTime()
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 (!store.message.date) {
65
- setStore(s => ({ ...s, message: { ...store.message, date: now }}))
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 (store.message && now - 500 < store.message.date) {
68
- let timeout1 = setTimeout(() => setVisible(true), 50)
69
- if (store.message.timeout !== 0 && !devDontHide) var timeout2 = setTimeout(hide, store.message.timeout || 5000)
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}</p>
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 '../is-first-render.js'
4
- import SvgX1 from '../../../client/imgs/icons/x1.svg'
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 = () => css`
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 '../../../client/imgs/avatar.jpg'
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
- function classNames(...classes) {
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 [{ user }] = sharedStore.useTracked()
71
-
72
- function isActive(path) {
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
- export function Tooltip({ text, children, className, classNamePopup, isSmall }) {
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?.length || text?.props
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 = () => css`
27
+ const style = css`
20
28
  .tooltip-popup {
21
29
  position: absolute;
22
30
  display: block;
@@ -1,10 +1,12 @@
1
- export function Topbar({ title, subtitle, submenu, btns, className }) {
2
- /**
3
- * @param {string|JSX} title
4
- * @param {string|JSX} <subtitle>
5
- * @param {string|JSX} <submenu>
6
- * @param {url|function} <plusIconAction>
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
- * 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 }) {
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
- import { isRegex, deepFind, s3Image } from '../../../util.js'
2
- import { DropHandler } from './drop-handler.jsx'
3
- import noImage from '../../../client/imgs/no-image.svg'
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
- export function Drop({ awsUrl, className, id, name, onChange, multiple, state, ...props }) {
6
- /**
7
- * @param {string} name - field name or path on state (used to match errors), e.g. 'avatar', 'company.avatar'
8
- * @param {string} <id> - not required, name used if not provided
9
- * @param {function} onChange({ target: { id: <{name}|errors>, value } }) - gets called on success/error
10
- * @param {object} state - State object to get the value, and check errors against
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
- var value = props.value
21
- } else if (typeof state == 'object') {
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 (let item of (state?.errors || [])) {
28
- if (isRegex(name) && (item.title||'').match(name)) var error = item
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).value = ''
56
+ const input = document.getElementById(name) as HTMLInputElement
57
+ if (input) input.value = ''
40
58
  if (onChange) {
41
- const errors = (stateRef.errors||[]).filter(e => e.title != name)
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 = objectOrFileListItem.length ? Array.from(objectOrFileListItem) : [objectOrFileListItem]
64
- return Promise.all(array.map((file) => {
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 (file.lastModified) {
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(file)
88
+ reader.readAsDataURL(item)
71
89
  } else {
72
- resolve(s3Image(awsUrl, file))
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
- class="flex flex-column justify-center items-center text-center gap-2 text-grey-300 text-sm px-8 min-h-[300px]"
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 '../element/dropdown.jsx'
6
- import { throttle } from '../../../util.js'
5
+ import { Dropdown, util } from 'nitro-web'
6
+ import React from 'react'
7
7
 
8
- export function InputColor({ className, defaultColor='#333', iconEl, id, onChange, value, ...props }) {
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
- css={style}
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 = () => css`
70
- text-indent: 0 !important; // since is-color is on dropdown
81
+ const style = css`
82
+ /////////////////////
71
83
  .w-color-interactive {
72
84
  width: 100% !important;
73
85
  height: 150px !important;