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.
- 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 +0 -1
- package/_example/server/config.js +6 -7
- package/_example/tailwind.config.js +1 -1
- package/_example/tsconfig.json +5 -1
- 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.tsx +16 -0
- 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 +14 -28
- 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 +3 -2
- package/tsconfig.json +31 -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/element/github-link.jsx +0 -14
- 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
package/client/store.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createContainer } from 'react-tracked'
|
|
2
|
+
import { Store } from 'types'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export type BeforeUpdate = (prevStore: Store | null, newData: Store) => Store
|
|
6
|
+
|
|
7
|
+
let initData: Store
|
|
8
|
+
let beforeUpdate: BeforeUpdate = (prevStore, newData) => newData
|
|
9
|
+
|
|
10
|
+
const container = createContainer(() => {
|
|
11
|
+
const [store, setStore] = useState(() => beforeUpdate(null, initData || exposedData || {}))
|
|
12
|
+
|
|
13
|
+
// Wrap the setState function to always run beforeUpdate
|
|
14
|
+
const wrappedSetStore = (updater: (prevStore: Store) => Store) => {
|
|
15
|
+
if (typeof updater === 'function') {
|
|
16
|
+
setStore((prevStore: Store) => beforeUpdate(prevStore, updater(prevStore)))
|
|
17
|
+
} else {
|
|
18
|
+
setStore((prevStore: Store) => beforeUpdate(prevStore, updater))
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
exposedData = store
|
|
23
|
+
return [store, wrappedSetStore]
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export let exposedData: Store
|
|
27
|
+
export const { Provider, useTracked } = container
|
|
28
|
+
export function beforeCreate(_initData: Store, _beforeUpdate: BeforeUpdate) {
|
|
29
|
+
initData = _initData // normally provided from a /login or /state request data
|
|
30
|
+
beforeUpdate = _beforeUpdate
|
|
31
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
1
2
|
import MongoStore from 'connect-mongo'
|
|
2
3
|
import crypto from 'crypto'
|
|
3
4
|
import expressSession from 'express-session'
|
|
4
5
|
import passport from 'passport'
|
|
5
6
|
import passportLocal from 'passport-local'
|
|
6
7
|
import db from 'monastery'
|
|
7
|
-
import { sendEmail } from '
|
|
8
|
-
import * as util from '
|
|
8
|
+
import { sendEmail } from 'nitro-web/server'
|
|
9
|
+
import * as util from 'nitro-web/util'
|
|
9
10
|
// import stripeController from '../billing/stripe.api.js'
|
|
10
11
|
|
|
11
12
|
let config = {}
|
|
@@ -1,22 +1,19 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import { Input } from '../partials/form/input.jsx'
|
|
4
|
-
import { FormError } from '../partials/form/form-error.jsx'
|
|
5
|
-
import { Button } from '../partials/element/button.jsx'
|
|
1
|
+
import { Topbar, Input, FormError, Button, util } from 'nitro-web'
|
|
2
|
+
import { Errors } from 'types'
|
|
6
3
|
|
|
7
4
|
export function ResetInstructions() {
|
|
8
5
|
const navigate = useNavigate()
|
|
9
6
|
const isLoading = useState('')
|
|
10
|
-
const [, setStore] =
|
|
11
|
-
const [state, setState] = useState({ email: '' })
|
|
7
|
+
const [, setStore] = useTracked()
|
|
8
|
+
const [state, setState] = useState({ email: '', errors: [] as Errors })
|
|
12
9
|
|
|
13
|
-
async function onSubmit (
|
|
10
|
+
async function onSubmit (event: React.FormEvent<HTMLFormElement>) {
|
|
14
11
|
try {
|
|
15
|
-
await util.request(
|
|
12
|
+
await util.request(event, 'post /api/reset-instructions', state, isLoading)
|
|
16
13
|
setStore(s => ({ ...s, message: 'Done! Please check your email.' }))
|
|
17
14
|
navigate('/signin')
|
|
18
|
-
} catch (
|
|
19
|
-
return setState({ ...state, errors })
|
|
15
|
+
} catch (e) {
|
|
16
|
+
return setState({ ...state, errors: e as Errors })
|
|
20
17
|
}
|
|
21
18
|
}
|
|
22
19
|
|
|
@@ -27,15 +24,15 @@ export function ResetInstructions() {
|
|
|
27
24
|
<form onSubmit={onSubmit}>
|
|
28
25
|
<div>
|
|
29
26
|
<label for="email">Email Address</label>
|
|
30
|
-
<Input name="email" type="email" state={state} onChange={onChange(setState)} placeholder="Your email address..." />
|
|
27
|
+
<Input name="email" type="email" state={state} onChange={onChange.bind(setState)} placeholder="Your email address..." />
|
|
31
28
|
</div>
|
|
32
29
|
|
|
33
30
|
<div class="mb-14">
|
|
34
31
|
Remembered your password? You can <Link to="/signin" class="underline2 is-active">sign in here</Link>.
|
|
35
|
-
<FormError state={state}
|
|
32
|
+
<FormError state={state} className="pt-2" />
|
|
36
33
|
</div>
|
|
37
34
|
|
|
38
|
-
<Button
|
|
35
|
+
<Button className="w-full" isLoading={!!isLoading[0]} type="submit">Email me a reset password link</Button>
|
|
39
36
|
</form>
|
|
40
37
|
</div>
|
|
41
38
|
)
|
|
@@ -45,20 +42,21 @@ export function ResetPassword() {
|
|
|
45
42
|
const navigate = useNavigate()
|
|
46
43
|
const params = useParams()
|
|
47
44
|
const isLoading = useState('')
|
|
48
|
-
const [, setStore] =
|
|
45
|
+
const [, setStore] = useTracked()
|
|
49
46
|
const [state, setState] = useState(() => ({
|
|
50
47
|
password: '',
|
|
51
48
|
password2: '',
|
|
52
49
|
token: params.token,
|
|
50
|
+
errors: [] as Errors,
|
|
53
51
|
}))
|
|
54
52
|
|
|
55
|
-
async function onSubmit (
|
|
53
|
+
async function onSubmit (event: React.FormEvent<HTMLFormElement>) {
|
|
56
54
|
try {
|
|
57
|
-
const data = await util.request(
|
|
55
|
+
const data = await util.request(event, 'post /api/reset-password', state, isLoading)
|
|
58
56
|
setStore(() => data)
|
|
59
57
|
navigate('/')
|
|
60
|
-
} catch (
|
|
61
|
-
return setState({ ...state, errors })
|
|
58
|
+
} catch (e) {
|
|
59
|
+
return setState({ ...state, errors: e as Errors })
|
|
62
60
|
}
|
|
63
61
|
}
|
|
64
62
|
|
|
@@ -69,19 +67,19 @@ export function ResetPassword() {
|
|
|
69
67
|
<form onSubmit={onSubmit}>
|
|
70
68
|
<div>
|
|
71
69
|
<label for="password">Your New Password</label>
|
|
72
|
-
<Input name="password" type="password" state={state} onChange={onChange(setState)} />
|
|
70
|
+
<Input name="password" type="password" state={state} onChange={onChange.bind(setState)} />
|
|
73
71
|
</div>
|
|
74
72
|
<div>
|
|
75
73
|
<label for="password2">Repeat Your New Password</label>
|
|
76
|
-
<Input name="password2" type="password" state={state} onChange={onChange(setState)} />
|
|
74
|
+
<Input name="password2" type="password" state={state} onChange={onChange.bind(setState)} />
|
|
77
75
|
</div>
|
|
78
76
|
|
|
79
77
|
<div class="mb-14">
|
|
80
78
|
Remembered your password? You can <Link to="/signin" class="underline2 is-active">sign in here</Link>.
|
|
81
|
-
<FormError state={state}
|
|
79
|
+
<FormError state={state} className="pt-2" />
|
|
82
80
|
</div>
|
|
83
81
|
|
|
84
|
-
<Button class="w-full" isLoading={isLoading[0]} type="submit">Reset Password</Button>
|
|
82
|
+
<Button class="w-full" isLoading={!!isLoading[0]} type="submit">Reset Password</Button>
|
|
85
83
|
</form>
|
|
86
84
|
</div>
|
|
87
85
|
)
|
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import { Input } from '../partials/form/input.jsx'
|
|
4
|
-
import { Button } from '../partials/element/button.jsx'
|
|
5
|
-
import { FormError } from '../partials/form/form-error.jsx'
|
|
1
|
+
import { Topbar, Input, Button, FormError, util } from 'nitro-web'
|
|
2
|
+
import { Config, Errors } from 'types'
|
|
6
3
|
|
|
7
|
-
export function Signin({ config }) {
|
|
4
|
+
export function Signin({ config }: { config: Config }) {
|
|
8
5
|
const navigate = useNavigate()
|
|
9
6
|
const location = useLocation()
|
|
10
7
|
const isSignout = location.pathname == '/signout'
|
|
11
8
|
const isLoading = useState(isSignout ? 'is-loading' : '')
|
|
12
|
-
const [, setStore] =
|
|
9
|
+
const [, setStore] = useTracked()
|
|
13
10
|
const [state, setState] = useState({
|
|
14
|
-
email: config.env == 'development' ? config.
|
|
11
|
+
email: config.env == 'development' ? config.placeholderEmail : '',
|
|
15
12
|
password: config.env == 'development' ? '1234' : '',
|
|
13
|
+
errors: [] as Errors,
|
|
16
14
|
})
|
|
17
15
|
|
|
18
16
|
useEffect(() => {
|
|
@@ -27,11 +25,11 @@ export function Signin({ config }) {
|
|
|
27
25
|
util.axios().get('/api/signout')
|
|
28
26
|
.then(() => isLoading[1](''))
|
|
29
27
|
.then(() => navigate({ pathname: '/signin', search: location.search }, { replace: true }))
|
|
30
|
-
.catch(err => console.error(err)
|
|
28
|
+
.catch(err => (console.error(err), isLoading[1]('')))
|
|
31
29
|
}
|
|
32
30
|
}, [isSignout])
|
|
33
31
|
|
|
34
|
-
async function onSubmit (e) {
|
|
32
|
+
async function onSubmit (e: React.FormEvent<HTMLFormElement>) {
|
|
35
33
|
try {
|
|
36
34
|
const data = await util.request(e, 'post /api/signin', state, isLoading)
|
|
37
35
|
isLoading[1]('is-loading')
|
|
@@ -40,8 +38,8 @@ export function Signin({ config }) {
|
|
|
40
38
|
if (location.search.includes('redirect')) navigate(location.search.replace('?redirect=', ''))
|
|
41
39
|
else navigate('/')
|
|
42
40
|
}, 100)
|
|
43
|
-
} catch (
|
|
44
|
-
return setState({ ...state, errors })
|
|
41
|
+
} catch (e) {
|
|
42
|
+
return setState({ ...state, errors: e as Errors})
|
|
45
43
|
}
|
|
46
44
|
}
|
|
47
45
|
|
|
@@ -52,22 +50,22 @@ export function Signin({ config }) {
|
|
|
52
50
|
<form onSubmit={onSubmit}>
|
|
53
51
|
<div>
|
|
54
52
|
<label for="email">Email Address</label>
|
|
55
|
-
<Input name="email" type="email" state={state} onChange={onChange(setState)} placeholder="Your email address..." />
|
|
53
|
+
<Input name="email" type="email" state={state} onChange={onChange.bind(setState)} placeholder="Your email address..." />
|
|
56
54
|
</div>
|
|
57
55
|
<div>
|
|
58
56
|
<div class="flex justify-between">
|
|
59
57
|
<label for="password">Password</label>
|
|
60
58
|
<Link to="/reset" class="label underline2">Forgot?</Link>
|
|
61
59
|
</div>
|
|
62
|
-
<Input name="password" type="password" state={state} onChange={onChange(setState)}/>
|
|
60
|
+
<Input name="password" type="password" state={state} onChange={onChange.bind(setState)}/>
|
|
63
61
|
</div>
|
|
64
62
|
|
|
65
63
|
<div class="mb-14">
|
|
66
64
|
Don't have an account? You can <Link to="/signup" class="underline2 is-active">sign up here</Link>.
|
|
67
|
-
<FormError state={state}
|
|
65
|
+
<FormError state={state} className="pt-2" />
|
|
68
66
|
</div>
|
|
69
67
|
|
|
70
|
-
<Button class="w-full" isLoading={isLoading[0]} type="submit">Sign In</Button>
|
|
68
|
+
<Button class="w-full" isLoading={!!isLoading[0]} type="submit">Sign In</Button>
|
|
71
69
|
</form>
|
|
72
70
|
</div>
|
|
73
71
|
)
|
|
@@ -1,28 +1,26 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import { Input } from '../partials/form/input.jsx'
|
|
4
|
-
import { Button } from '../partials/element/button.jsx'
|
|
5
|
-
import { FormError } from '../partials/form/form-error.jsx'
|
|
1
|
+
import { Button, Input, FormError, Topbar, util } from 'nitro-web'
|
|
2
|
+
import { Config, Errors } from 'types'
|
|
6
3
|
|
|
7
|
-
export function Signup({ config }) {
|
|
4
|
+
export function Signup({ config }: { config: Config}) {
|
|
8
5
|
const navigate = useNavigate()
|
|
9
6
|
const isLoading = useState('')
|
|
10
|
-
const [, setStore] =
|
|
7
|
+
const [, setStore] = useTracked()
|
|
11
8
|
const [state, setState] = useState({
|
|
12
|
-
email: config.env === 'development' ? config.
|
|
9
|
+
email: config.env === 'development' ? config.placeholderEmail : '',
|
|
13
10
|
name: config.env === 'development' ? 'Bruce Wayne' : '',
|
|
14
11
|
business: { name: config.env === 'development' ? 'Wayne Enterprises' : '' },
|
|
15
12
|
password: config.env === 'development' ? '1234' : '',
|
|
13
|
+
errors: [] as Errors,
|
|
16
14
|
})
|
|
17
15
|
|
|
18
|
-
async function onSubmit (e) {
|
|
16
|
+
async function onSubmit (e: React.FormEvent<HTMLFormElement>) {
|
|
19
17
|
try {
|
|
20
18
|
const data = await util.request(e, 'post /api/signup', state, isLoading)
|
|
21
19
|
isLoading[1]('is-loading')
|
|
22
20
|
setStore(() => data)
|
|
23
21
|
setTimeout(() => navigate('/'), 0) // wait for setStore
|
|
24
|
-
} catch (
|
|
25
|
-
return setState({ ...state, errors })
|
|
22
|
+
} catch (e) {
|
|
23
|
+
return setState({ ...state, errors: e as Errors })
|
|
26
24
|
}
|
|
27
25
|
}
|
|
28
26
|
|
|
@@ -34,28 +32,28 @@ export function Signup({ config }) {
|
|
|
34
32
|
<div class="grid grid-cols-2 gap-6">
|
|
35
33
|
<div>
|
|
36
34
|
<label for="name">Your Name</label>
|
|
37
|
-
<Input name="name" placeholder="E.g. Tony Stark" state={state} onChange={onChange(setState)} />
|
|
35
|
+
<Input name="name" placeholder="E.g. Tony Stark" state={state} onChange={onChange.bind(setState)} />
|
|
38
36
|
</div>
|
|
39
37
|
<div>
|
|
40
38
|
<label for="business.name">Company Name</label>
|
|
41
|
-
<Input name="business.name" placeholder="E.g. Stark Industries" state={state} onChange={onChange(setState)} />
|
|
39
|
+
<Input name="business.name" placeholder="E.g. Stark Industries" state={state} onChange={onChange.bind(setState)} />
|
|
42
40
|
</div>
|
|
43
41
|
</div>
|
|
44
42
|
<div>
|
|
45
43
|
<label for="email">Email Address</label>
|
|
46
|
-
<Input name="email" type="email" state={state} onChange={onChange(setState)} placeholder="Your email address..." />
|
|
44
|
+
<Input name="email" type="email" state={state} onChange={onChange.bind(setState)} placeholder="Your email address..." />
|
|
47
45
|
</div>
|
|
48
46
|
<div>
|
|
49
47
|
<label for="password">Password</label>
|
|
50
|
-
<Input name="password" type="password" state={state} onChange={onChange(setState)}/>
|
|
48
|
+
<Input name="password" type="password" state={state} onChange={onChange.bind(setState)}/>
|
|
51
49
|
</div>
|
|
52
50
|
|
|
53
51
|
<div class="mb-14">
|
|
54
52
|
Already have an account? You can <Link to="/signin" class="underline2 is-active">sign in here</Link>.
|
|
55
|
-
<FormError state={state}
|
|
53
|
+
<FormError state={state} className="pt-2" />
|
|
56
54
|
</div>
|
|
57
55
|
|
|
58
|
-
<Button class="w-full" isLoading={isLoading[0]} type="submit">Create Account</Button>
|
|
56
|
+
<Button class="w-full" isLoading={!!isLoading[0]} type="submit">Create Account</Button>
|
|
59
57
|
</form>
|
|
60
58
|
</div>
|
|
61
59
|
)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { css, theme } from 'twin.macro'
|
|
2
2
|
|
|
3
|
-
export function Dashboard({ config }) {
|
|
4
|
-
const [store] =
|
|
3
|
+
export function Dashboard({ config }: { config: { isStatic?: boolean } }) {
|
|
4
|
+
const [store] = useTracked()
|
|
5
5
|
const textColor = store.apiAvailable ? 'text-green-700' : 'text-pink-700'
|
|
6
6
|
const fillColor = store.apiAvailable ? 'fill-green-500' : 'fill-pink-500'
|
|
7
7
|
const bgColor = store.apiAvailable ? 'bg-green-100' : 'bg-pink-100'
|
|
@@ -17,7 +17,7 @@ export function Dashboard({ config }) {
|
|
|
17
17
|
<svg viewBox="0 0 6 6" aria-hidden="true" className={`size-1.5 ${fillColor}`}>
|
|
18
18
|
<circle r={3} cx={3} cy={3} />
|
|
19
19
|
</svg>
|
|
20
|
-
{ store.apiAvailable ? 'API Available' : `API Unavailable${config.
|
|
20
|
+
{ store.apiAvailable ? 'API Available' : `API Unavailable${config.isStatic ? ' (Static Example)' : ''}` }
|
|
21
21
|
</span>
|
|
22
22
|
</p>
|
|
23
23
|
</div>
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
|
|
1
2
|
import { css } from 'twin.macro'
|
|
2
|
-
import { IsFirstRender } from '
|
|
3
|
+
import { IsFirstRender } from 'nitro-web'
|
|
4
|
+
|
|
5
|
+
type AccordionProps = {
|
|
6
|
+
children: React.ReactNode
|
|
7
|
+
className?: string
|
|
8
|
+
expanded?: boolean
|
|
9
|
+
onChange?: (event: React.MouseEvent<HTMLDivElement>, index: number) => void
|
|
10
|
+
}
|
|
3
11
|
|
|
4
|
-
export function Accordion({ children, className, expanded, onChange }) {
|
|
12
|
+
export function Accordion({ children, className, expanded, onChange }: AccordionProps) {
|
|
5
13
|
/**
|
|
6
14
|
* @param {rxjs} children - first child is the header, second child is the contents
|
|
7
15
|
* <Accordion>
|
|
@@ -10,12 +18,12 @@ export function Accordion({ children, className, expanded, onChange }) {
|
|
|
10
18
|
* @param {boolean} <expanded> - initial value (or controlled value if onChange is passed)
|
|
11
19
|
* @param {function} <onChange> - called when the header is clicked
|
|
12
20
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
const [preState, setPreState] = useState(expanded)
|
|
22
|
+
const [state, setState] = useState(expanded)
|
|
23
|
+
const [height, setHeight] = useState('auto')
|
|
24
|
+
const isFirst = IsFirstRender()
|
|
25
|
+
const el = useRef<HTMLDivElement>(null)
|
|
26
|
+
const style = css`
|
|
19
27
|
&>:last-child {
|
|
20
28
|
height: 0;
|
|
21
29
|
overflow: hidden;
|
|
@@ -34,7 +42,7 @@ export function Accordion({ children, className, expanded, onChange }) {
|
|
|
34
42
|
useEffect(() => {
|
|
35
43
|
// Calulcate height first before opening and closing
|
|
36
44
|
if (!isFirst) {
|
|
37
|
-
setHeight((_o) => el.current
|
|
45
|
+
setHeight((_o) => el.current?.children[1].scrollHeight + 'px' + (preState ? '-' : ''))
|
|
38
46
|
}
|
|
39
47
|
}, [preState])
|
|
40
48
|
|
|
@@ -52,9 +60,9 @@ export function Accordion({ children, className, expanded, onChange }) {
|
|
|
52
60
|
return () => timeout && clearTimeout(timeout)
|
|
53
61
|
}, [height])
|
|
54
62
|
|
|
55
|
-
|
|
63
|
+
const onClick = function(e: React.MouseEvent<HTMLDivElement>) {
|
|
56
64
|
// Click came from inside the accordion header/summary
|
|
57
|
-
if (e.currentTarget.children[0].contains(e.target) || e.currentTarget.children[0] == e.target) {
|
|
65
|
+
if (e.currentTarget.children[0].contains(e.target as Node) || e.currentTarget.children[0] == e.target) {
|
|
58
66
|
if (onChange) {
|
|
59
67
|
onChange(e, getElementIndex(e.currentTarget))
|
|
60
68
|
} else {
|
|
@@ -63,9 +71,9 @@ export function Accordion({ children, className, expanded, onChange }) {
|
|
|
63
71
|
}
|
|
64
72
|
}
|
|
65
73
|
|
|
66
|
-
|
|
74
|
+
const getElementIndex = function(node: HTMLElement) {
|
|
67
75
|
let index = 0
|
|
68
|
-
while ((node = node.previousElementSibling)) index++
|
|
76
|
+
while ((node = node.previousElementSibling as HTMLElement)) index++
|
|
69
77
|
return index
|
|
70
78
|
}
|
|
71
79
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Initials } from 'nitro-web'
|
|
2
|
+
import { s3Image } from 'nitro-web/util'
|
|
3
|
+
import noImage from 'nitro-web/client/imgs/no-image.svg'
|
|
4
|
+
import avatarImg from 'nitro-web/client/imgs/avatar.jpg'
|
|
5
|
+
import { User } from 'types'
|
|
6
|
+
|
|
7
|
+
type AvatarProps = {
|
|
8
|
+
awsUrl: string
|
|
9
|
+
isRound?: boolean
|
|
10
|
+
user: User,
|
|
11
|
+
showPlaceholderImage?: boolean
|
|
12
|
+
className?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Avatar({ awsUrl, isRound, user, showPlaceholderImage, className }: AvatarProps) {
|
|
16
|
+
const classes = 'rounded-full w-[30px] h-[30px] object-cover transition-all duration-150 ease ' + (className || '')
|
|
17
|
+
|
|
18
|
+
function getInitials(user: User) {
|
|
19
|
+
const text = (user.firstName ? [user.firstName, user.lastName] : (user?.name||'').split(' ')).map((o) => o?.charAt(0))
|
|
20
|
+
if (text.length == 1) return text[0] || ''
|
|
21
|
+
if (text.length > 1) return `${text[0]}${text[text.length - 1]}`
|
|
22
|
+
return ''
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getHex(user: User) {
|
|
26
|
+
const colors = ['#067306', '#AA33FF', '#FF54AF', '#F44336', '#c03c3c', '#7775f2', '#d88c1b']
|
|
27
|
+
const charIndex = (user.firstName||'a').toLowerCase().charCodeAt(0) - 97
|
|
28
|
+
const charIndexLimited = (charIndex < 0 || charIndex > 25) ? 25 : charIndex
|
|
29
|
+
const index = Math.round(charIndexLimited / 25 * (colors.length-1))
|
|
30
|
+
return colors[index]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
user.avatar
|
|
35
|
+
? <img class={classes} src={s3Image(awsUrl, user.avatar, 'small') || noImage} />
|
|
36
|
+
: showPlaceholderImage ? <img class={classes} src={avatarImg} width="30px" />
|
|
37
|
+
: <Initials className={classes} icon={{ initials: getInitials(user), hex: getHex(user) }} isRound={isRound} isMedium={true} />
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
@@ -1,16 +1,19 @@
|
|
|
1
|
-
// todo: add loading indicator
|
|
2
1
|
import { ChevronDownIcon } from '@heroicons/react/20/solid'
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
2
|
+
|
|
3
|
+
type Props = {
|
|
4
|
+
color?: 'primary'|'secondary'|'white'
|
|
5
|
+
size?: 'xs'|'sm'|'md'|'lg'
|
|
6
|
+
className?: string
|
|
7
|
+
isLoading?: boolean
|
|
8
|
+
IconLeft?: React.ReactNode|'v'
|
|
9
|
+
IconRight?: React.ReactNode|'v'
|
|
10
|
+
IconRight2?: React.ReactNode|'v'
|
|
11
|
+
children?: React.ReactNode|'v'
|
|
12
|
+
[key: string]: unknown
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Button({ color='primary', size='md', className, isLoading, IconLeft, IconRight, IconRight2, children, ...props }: Props) {
|
|
16
|
+
// const size = (color.match(/xs|sm|md|lg/)?.[0] || 'md') as 'xs'|'sm'|'md'|'lg'
|
|
14
17
|
const iconPosition = IconLeft ? 'left' : IconRight ? 'right' : IconRight2 ? 'right2' : 'none'
|
|
15
18
|
const base = 'relative inline-block font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2'
|
|
16
19
|
|
|
@@ -34,15 +37,16 @@ export function Button({ color='primary', className, isLoading, IconLeft, IconRi
|
|
|
34
37
|
right2: 'w-full inline-flex items-center justify-between',
|
|
35
38
|
none: 'w-full ',
|
|
36
39
|
}
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
|
|
41
|
+
let colorAndSize = ''
|
|
42
|
+
if (color.match(/primary/)) colorAndSize = `${primary} ${sizes[size]}`
|
|
39
43
|
else if (color.match(/secondary/)) colorAndSize = `${secondary} ${sizes[size]}`
|
|
40
44
|
else if (color.match(/white/)) colorAndSize = `${white} ${sizes[size]}`
|
|
41
45
|
|
|
42
|
-
|
|
46
|
+
let contentLayout = `${contentLayouts[iconPosition]}`
|
|
43
47
|
if (!(className||'').match(/gap-/)) contentLayout += ' gap-x-1.5'
|
|
44
48
|
|
|
45
|
-
function getIcon(Icon, className) {
|
|
49
|
+
function getIcon(Icon: React.ReactNode|'v', className: string) {
|
|
46
50
|
if (Icon == 'v') return <ChevronDownIcon className={className} />
|
|
47
51
|
else return Icon
|
|
48
52
|
}
|
|
@@ -1,30 +1,32 @@
|
|
|
1
1
|
import { css } from 'twin.macro'
|
|
2
|
-
import { cloneElement } from 'react'
|
|
3
|
-
import { toArray } from '
|
|
4
|
-
import {
|
|
5
|
-
import { getSelectStyle } from '../form/select.jsx'
|
|
2
|
+
import { forwardRef, cloneElement } from 'react'
|
|
3
|
+
import { toArray } from 'nitro-web/util'
|
|
4
|
+
import { getSelectStyle } from 'nitro-web'
|
|
6
5
|
import { CheckCircleIcon } from '@heroicons/react/24/solid'
|
|
7
6
|
|
|
7
|
+
type DropdownProps = {
|
|
8
|
+
animate?: boolean
|
|
9
|
+
children?: React.ReactNode
|
|
10
|
+
className?: string
|
|
11
|
+
css?: string
|
|
12
|
+
/** The direction of the menu **/
|
|
13
|
+
dir?: 'bottom-left'|'bottom-right'|'top-left'|'top-right'
|
|
14
|
+
options?: { label: string|React.ReactNode, onClick?: Function, isSelected?: boolean, icon?: React.ReactNode, className?: string }[]
|
|
15
|
+
/** Whether the dropdown is hoverable **/
|
|
16
|
+
isHoverable?: boolean
|
|
17
|
+
/** The minimum width of the menu **/
|
|
18
|
+
minWidth?: number | string
|
|
19
|
+
/** The content to render inside the top of the dropdown **/
|
|
20
|
+
menuChildren?: React.ReactNode
|
|
21
|
+
menuIsOpen?: boolean
|
|
22
|
+
menuToggles?: boolean
|
|
23
|
+
toggleCallback?: (isActive: boolean) => void
|
|
24
|
+
}
|
|
8
25
|
|
|
9
|
-
|
|
10
|
-
* Dropdown component
|
|
11
|
-
*
|
|
12
|
-
* @param {boolean} animate
|
|
13
|
-
* @param {React.ReactNode} children
|
|
14
|
-
* @param {string} className
|
|
15
|
-
* @param {'bottom-left'|'bottom-right'|'top-left'|'top-right'} [dir='bottom-left'] - The direction of the menu
|
|
16
|
-
* @param {[{ label, onClick, isSelected, icon, className }]} options - Menu options
|
|
17
|
-
* @param {boolean} isHoverable - Whether the dropdown is hoverable
|
|
18
|
-
* @param {number} minWidth - The minimum width of the menu
|
|
19
|
-
* @param {React.ReactNode} menuChildren - The content to render inside the top of the dropdown
|
|
20
|
-
* @param {boolean} menuIsOpen - Whether the menu is open
|
|
21
|
-
* @param {boolean} menuToggles - Whether the menu toggles
|
|
22
|
-
* @param {function} toggleCallback - The callback function to call when the menu is toggled
|
|
23
|
-
*/
|
|
24
|
-
export const Dropdown = forwardRef(function Dropdown({
|
|
26
|
+
export const Dropdown = forwardRef(function Dropdown({
|
|
25
27
|
animate=true,
|
|
26
28
|
children,
|
|
27
|
-
className,
|
|
29
|
+
className,
|
|
28
30
|
dir,
|
|
29
31
|
options,
|
|
30
32
|
isHoverable,
|
|
@@ -33,21 +35,21 @@ export const Dropdown = forwardRef(function Dropdown({
|
|
|
33
35
|
menuIsOpen,
|
|
34
36
|
menuToggles=true,
|
|
35
37
|
toggleCallback,
|
|
36
|
-
}, ref) {
|
|
38
|
+
}: DropdownProps, ref) {
|
|
37
39
|
// https://letsbuildui.dev/articles/building-a-dropdown-menu-component-with-react-hooks
|
|
38
40
|
isHoverable = isHoverable && !menuIsOpen
|
|
39
|
-
const dropdownRef = useRef(null)
|
|
40
|
-
const [isActive, setIsActive] = useState(menuIsOpen)
|
|
41
|
+
const dropdownRef = useRef<HTMLDivElement|null>(null)
|
|
42
|
+
const [isActive, setIsActive] = useState(!!menuIsOpen)
|
|
41
43
|
const menuStyle = getSelectStyle({ name: 'menu', usePrefixes: true })
|
|
42
44
|
|
|
43
45
|
// Expose the setIsActive function to the parent component
|
|
44
46
|
useImperativeHandle(ref, () => ({ setIsActive }))
|
|
45
47
|
|
|
46
48
|
useEffect(() => {
|
|
47
|
-
const pageClick = (
|
|
49
|
+
const pageClick = (event: MouseEvent | FocusEvent) => {
|
|
48
50
|
try {
|
|
49
51
|
// If the active element exists and is clicked outside of the dropdown, toggle the dropdown
|
|
50
|
-
if (dropdownRef.current !== null && !dropdownRef.current.contains(
|
|
52
|
+
if (dropdownRef.current !== null && !dropdownRef.current.contains(event.target as Node)) setIsActive(!isActive)
|
|
51
53
|
} catch (_e) {
|
|
52
54
|
// Errors throw for contains() when the user clicks off the webpage when open
|
|
53
55
|
setIsActive(!isActive)
|
|
@@ -70,13 +72,13 @@ export const Dropdown = forwardRef(function Dropdown({
|
|
|
70
72
|
if (toggleCallback) toggleCallback(isActive)
|
|
71
73
|
}, [isActive])
|
|
72
74
|
|
|
73
|
-
function onMouseDown(e) {
|
|
75
|
+
function onMouseDown(e: { key: string, preventDefault: Function }) {
|
|
74
76
|
if (e.key && e.key != 'Enter') return
|
|
75
77
|
if (e.key) e.preventDefault() // for button, stops buttons firing twice
|
|
76
78
|
if (!isHoverable && !menuIsOpen && ((menuToggles || e.key) || !isActive)) setIsActive(!isActive)
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
function onClick(option, e) {
|
|
81
|
+
function onClick(option: { onClick?: Function }, e: React.MouseEvent) {
|
|
80
82
|
if (option.onClick) option.onClick(e)
|
|
81
83
|
if (!menuIsOpen) setIsActive(!isActive)
|
|
82
84
|
}
|
|
@@ -114,7 +116,7 @@ export const Dropdown = forwardRef(function Dropdown({
|
|
|
114
116
|
<li
|
|
115
117
|
key={i}
|
|
116
118
|
className={`${optionStyle} ${option.className}`}
|
|
117
|
-
onClick={(e) => onClick(option, e)}
|
|
119
|
+
onClick={(e: React.MouseEvent) => onClick(option, e)}
|
|
118
120
|
>
|
|
119
121
|
<span class="flex-auto">{option.label}</span>
|
|
120
122
|
{ !!option.icon && option.icon }
|
|
@@ -128,7 +130,7 @@ export const Dropdown = forwardRef(function Dropdown({
|
|
|
128
130
|
)
|
|
129
131
|
})
|
|
130
132
|
|
|
131
|
-
const style =
|
|
133
|
+
const style = css`
|
|
132
134
|
ul {
|
|
133
135
|
transition: transform 0.15s ease, opacity 0.15s ease, visibility 0s 0.15s ease;
|
|
134
136
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import GithubIcon from 'nitro-web/client/imgs/github.svg'
|
|
2
|
+
|
|
3
|
+
export function GithubLink({ filename }: { filename: string }) {
|
|
4
|
+
const base = 'https://github.com/boycce/nitro-web/blob/master/'
|
|
5
|
+
// Filenames are relative to the webpack start directory
|
|
6
|
+
// 1. Remove ../ from filename (i.e. for _example build)
|
|
7
|
+
// 2. Remove node_modules/nitro-web/ from filename (i.e. for packages using nitro-web)
|
|
8
|
+
const link = base + filename.replace(/^(\.\.\/|.*node_modules\/nitro-web\/)/, '')
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
// <a href={link}>Go to Github</a>
|
|
12
|
+
<a href={link} className="fixed top-0 right-0">
|
|
13
|
+
<GithubIcon />
|
|
14
|
+
</a>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { css } from 'twin.macro'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
type InitialsProps = {
|
|
4
|
+
icon?: { initials: string, hex: string }
|
|
5
|
+
isBig?: boolean
|
|
6
|
+
isMedium?: boolean
|
|
7
|
+
isSmall?: boolean
|
|
8
|
+
isRound?: boolean
|
|
9
|
+
className?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Initials({ icon, isBig, isMedium, isSmall, isRound, className }: InitialsProps) {
|
|
4
13
|
return (
|
|
5
14
|
<span
|
|
6
15
|
css={style}
|
|
@@ -20,7 +29,7 @@ export function Initials({ icon, isBig, isMedium, isSmall, isRound, className })
|
|
|
20
29
|
)
|
|
21
30
|
}
|
|
22
31
|
|
|
23
|
-
const style =
|
|
32
|
+
const style = css`
|
|
24
33
|
// seen in input.jsx
|
|
25
34
|
display: flex;
|
|
26
35
|
align-items: center;
|