nitro-web 0.0.39 → 0.0.41

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/client/app.tsx CHANGED
@@ -1,7 +1,6 @@
1
1
  import { createBrowserRouter, createHashRouter, redirect, useParams, RouterProvider } from 'react-router-dom'
2
2
  import { Fragment, ReactNode } from 'react'
3
3
  import ReactDOM from 'react-dom/client'
4
- import { AxiosRequestConfig } from '@hokify/axios'
5
4
  import { axios, camelCase, pick, toArray, setTimeoutPromise } from 'nitro-web/util'
6
5
  import { injectedConfig, preloadedStoreData, exposedStoreData } from './index'
7
6
  import { Config, Store } from 'nitro-web/types'
@@ -27,11 +26,16 @@ type Settings = {
27
26
 
28
27
  type Route = {
29
28
  component: React.FC<{ route?: Route; params?: object; location?: object; config?: Config }>
30
- meta?: { title?: string }
31
29
  middleware: string[]
32
30
  name: string
33
31
  path: string
34
32
  redirect?: string
33
+ meta?: { title?: string; layout?: number }
34
+ }
35
+
36
+ type RouteWithPolicies = Route & {
37
+ // Route policies, e.g. { 'get /signin' : ['isUser'] }
38
+ [key: string]: true | string | (string | true)[]
35
39
  }
36
40
 
37
41
  export async function setupApp(config: Config, storeContainer: StoreContainer, layouts: React.FC<LayoutProps>[]) {
@@ -137,7 +141,7 @@ function getRouter({ settings, config }: { settings: Settings, config: Config })
137
141
  for (const key in file) {
138
142
  const isReactFnComponentOrFnRef = typeof file[key] === 'function' || !!file[key]?.render
139
143
  if (!file.hasOwnProperty(key) || key.match(/route/i) || !isReactFnComponentOrFnRef) continue
140
- const componentRoutes = toArray(file[key].route || file.route || file.Route)
144
+ const componentRoutes = toArray(file[key].route || file.route || file.Route) as RouteWithPolicies[]
141
145
  const componentName = key || camelCase(key.replace(/^.*[\\\/]|\.jsx$/g, '')) as string // eslint-disable-line
142
146
  // console.log(file)
143
147
  // Todo: need to retrieve the original function name for default exports during minification.
@@ -153,13 +157,13 @@ function getRouter({ settings, config }: { settings: Settings, config: Config })
153
157
  const routePaths = Object.keys(pick(route, /^(get\s+)?\/|^\*$/))
154
158
 
155
159
  for (const routePath of routePaths) {
156
- const layoutNum = (route.meta?.layout || 1) - 1
160
+ const layoutNum = (parseInt(String(route.meta?.layout || '1')) || 1) - 1
157
161
 
158
162
  // get the routes middleware
159
- const middleware = toArray(route[routePath]).filter(o => {
160
- if (o === true) return // ignore true
161
- else if (settings.middleware[o]) return true
162
- else console.error(`No middleware named '${o}' defined under config.middleware, skipping..`)
163
+ const middleware = toArray(route[routePath]).filter((policyNameOrTrue) => {
164
+ if (policyNameOrTrue === true) return // ignore true
165
+ else if (policyNameOrTrue in settings.middleware) return true
166
+ else console.error(`No middleware named '${policyNameOrTrue}' defined under config.middleware, skipping..`)
163
167
  })
164
168
 
165
169
  // Push route to layout
@@ -171,7 +175,7 @@ function getRouter({ settings, config }: { settings: Settings, config: Config })
171
175
  layout: layoutNum,
172
176
  title: `${route.meta?.title ? `${route.meta.title}${settings.titleSeparator || ' - '}` : ''}${settings.name}`,
173
177
  },
174
- middleware: middleware,
178
+ middleware: middleware as string[],
175
179
  name: componentName,
176
180
  path: routePath,
177
181
  redirect: route.redirect,
@@ -203,7 +207,10 @@ function getRouter({ settings, config }: { settings: Settings, config: Config })
203
207
  path: route.path,
204
208
  loader: async () => { // request
205
209
  // wait for container/exposedStoreData to be setup
206
- if (!nonce) nonce = true && await setTimeoutPromise(() => {}, 0) // eslint-disable-line
210
+ if (!nonce) {
211
+ nonce = true
212
+ await setTimeoutPromise(() => {}, 0)
213
+ }
207
214
  for (const key of route.middleware) {
208
215
  const error = settings.middleware[key](route, exposedStoreData || {})
209
216
  if (error && error.redirect) {
@@ -278,7 +285,7 @@ async function beforeApp(config: Config) {
278
285
  // delete window.prehot
279
286
  // }
280
287
  if (!config.isStatic) {
281
- storeData = (await axios().get('/api/store', { 'axios-retry': { retries: 3 }, timeout: 4000 } as AxiosRequestConfig)).data
288
+ storeData = (await axios().get('/api/store', { 'axios-retry': { retries: 3 }, timeout: 4000 })).data
282
289
  apiAvailable = true
283
290
  }
284
291
  } catch (err) {
package/client/store.ts CHANGED
@@ -40,6 +40,8 @@ function beforeUpdate<T extends Store>(newStore: T) {
40
40
  }
41
41
 
42
42
  // E.g. Cookie matching handy for rare issues, e.g. signout > signin (to a different user on another tab)
43
- axios().defaults.headers.authid = newStore?.user?._id
43
+ if (newStore?.user?._id) {
44
+ axios().defaults.headers.authid = newStore?.user?._id
45
+ }
44
46
  return newStore
45
47
  }
@@ -34,9 +34,9 @@ export default {
34
34
  function setup(middleware, _config) {
35
35
  // Setup is called automatically when the server starts
36
36
  // Set config values
37
- const configKeys = ['clientUrl', 'emailFrom', 'env', 'mailgunDomain', 'mailgunKey', 'masterPassword', 'name']
37
+ const configKeys = ['clientUrl', 'emailFrom', 'env', 'name', 'mailgunDomain', 'mailgunKey', 'masterPassword']
38
38
  config = pick(_config, configKeys)
39
- for (const key of configKeys) {
39
+ for (const key of ['clientUrl', 'emailFrom', 'env', 'name']) {
40
40
  if (!config[key]) throw new Error(`Missing config value for: config.${key}`)
41
41
  }
42
42
 
@@ -17,7 +17,7 @@ export function Signin() {
17
17
  useEffect(() => {
18
18
  // Autofill the email input from ?email=
19
19
  const query = util.queryObject(location.search, true)
20
- if (query.email) setState({ ...state, email: query.email })
20
+ if (query.email) setState({ ...state, email: query.email as string })
21
21
  }, [location.search])
22
22
 
23
23
  useEffect(() => {
@@ -1,6 +1,5 @@
1
1
  import { Initials } from 'nitro-web'
2
2
  import { s3Image } from 'nitro-web/util'
3
- import noImage from 'nitro-web/client/imgs/no-image.svg'
4
3
  import avatarImg from 'nitro-web/client/imgs/avatar.jpg'
5
4
  import { User } from 'nitro-web/types'
6
5
 
@@ -32,7 +31,7 @@ export function Avatar({ awsUrl, isRound, user, showPlaceholderImage, className
32
31
 
33
32
  return (
34
33
  user.avatar
35
- ? <img class={classes} src={s3Image(awsUrl, user.avatar, 'small') || noImage} />
34
+ ? <img class={classes} src={s3Image(awsUrl, user.avatar, 'small')} />
36
35
  : showPlaceholderImage ? <img class={classes} src={avatarImg} width="30px" />
37
36
  : <Initials className={classes} icon={{ initials: getInitials(user), hex: getHex(user) }} isRound={isRound} isMedium={true} />
38
37
  )
@@ -1,6 +1,5 @@
1
1
  import { css } from 'twin.macro'
2
2
  import { forwardRef, cloneElement } from 'react'
3
- import { toArray } from 'nitro-web/util'
4
3
  import { getSelectStyle } from 'nitro-web'
5
4
  import { CheckCircleIcon } from '@heroicons/react/24/solid'
6
5
 
@@ -99,7 +98,7 @@ export const Dropdown = forwardRef(function Dropdown({
99
98
  css={style}
100
99
  >
101
100
  {
102
- toArray(children).map((el, key) => {
101
+ (Array.isArray(children) ? children : [children]).map((el, key) => {
103
102
  const onKeyDown = onMouseDown
104
103
  if (!el.type) throw new Error('Dropdown component requires a valid child element')
105
104
  return cloneElement(el, { key, onMouseDown, onKeyDown }) // adds onClick
@@ -17,6 +17,7 @@ export type FieldDateProps = React.InputHTMLAttributes<HTMLInputElement> & {
17
17
  value?: null|number|string|(null|number|string)[]
18
18
  numberOfMonths?: number
19
19
  Icon?: React.ReactNode
20
+ dir?: 'bottom-left'|'bottom-right'|'top-left'|'top-right'
20
21
  }
21
22
 
22
23
  type TimePickerProps = {
@@ -24,7 +25,9 @@ type TimePickerProps = {
24
25
  onChange: (mode: Mode, value: number|null) => void
25
26
  }
26
27
 
27
- export function FieldDate({ mode='single', onChange, prefix='', value, numberOfMonths, Icon, showTime, ...props }: FieldDateProps) {
28
+ export function FieldDate({
29
+ mode='single', onChange, prefix='', value, numberOfMonths, Icon, showTime, dir = 'bottom-left', ...props
30
+ }: FieldDateProps) {
28
31
  const localePattern = `d MMM yyyy${showTime && mode == 'single' ? ' hh:mmaa' : ''}`
29
32
  const [prefixWidth, setPrefixWidth] = useState(0)
30
33
  const dropdownRef = useRef<DropdownRef>(null)
@@ -100,6 +103,7 @@ export function FieldDate({ mode='single', onChange, prefix='', value, numberOfM
100
103
  {!!showTime && mode == 'single' && <TimePicker date={dates?.[0]} onChange={onCalendarChange} />}
101
104
  </div>
102
105
  }
106
+ dir={dir}
103
107
  >
104
108
  <div className="grid grid-cols-1">
105
109
  {Icon}
@@ -57,7 +57,10 @@ export function Field({ state, icon, iconPos: ip, ...props }: FieldProps) {
57
57
 
58
58
  // Value: Input is always controlled if state is passed in
59
59
  if (props.value) value = props.value as string
60
- else if (typeof state == 'object') value = util.deepFind(state, props.name) ?? ''
60
+ else if (typeof state == 'object') {
61
+ const v = util.deepFind(state, props.name) as string | undefined
62
+ value = v ?? ''
63
+ }
61
64
 
62
65
  // Errors: find any that match this field path
63
66
  for (const item of (state?.errors || [])) {
@@ -266,8 +266,8 @@ export function Styleguide() {
266
266
  <Field name="date-range" type="date" mode="range" prefix="Date:" state={state} onChange={onInputChange} />
267
267
  </div>
268
268
  <div>
269
- <label for="date">Date</label>
270
- <Field name="date" type="date" state={state} onChange={onInputChange} />
269
+ <label for="date">Date (right aligned)</label>
270
+ <Field name="date" type="date" state={state} onChange={onInputChange} dir="bottom-right" />
271
271
  </div>
272
272
  </div>
273
273
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nitro-web",
3
- "version": "0.0.39",
3
+ "version": "0.0.41",
4
4
  "repository": "github:boycce/nitro-web",
5
5
  "homepage": "https://boycce.github.io/nitro-web/",
6
6
  "description": "Nitro is a battle-tested, modular base project to turbocharge your projects, styled using Tailwind 🚀",
@@ -29,12 +29,13 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@hokify/axios": "^0.19.1",
32
+ "axios": "^1.9.0",
32
33
  "axios-retry": "^3.3.1",
33
34
  "bcrypt": "^5.0.0",
34
35
  "body-parser": "^1.19.0",
35
36
  "compression": "^1.7.4",
36
37
  "date-fns": "^3.6.0",
37
- "dateformat": "^3.0.3",
38
+ "dateformat": "^5.0.3",
38
39
  "dotenv": "^14.3.2",
39
40
  "express": "^4.17.1",
40
41
  "express-fileupload": "^1.1.6",
@@ -49,6 +50,9 @@
49
50
  "sort-route-addresses-nodeps": "0.0.4",
50
51
  "standard-version": "^9.5.0"
51
52
  },
53
+ "devDependencies": {
54
+ "@types/dateformat": "^5.0.3"
55
+ },
52
56
  "peerDependencies": {
53
57
  "@stripe/stripe-js": "^1.34.0",
54
58
  "monastery": "^3.5.2",