@visns-studio/visns-components 6.0.4 → 6.1.0
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/package.json +1 -1
- package/src/components/DataGrid.jsx +37 -12
- package/src/components/Navigation.jsx +59 -3
- package/src/components/auth/Login.jsx +203 -175
- package/src/components/columns/ColumnRenderers.jsx +34 -0
- package/src/components/generic/GenericDashboard.jsx +181 -1
- package/src/components/generic/GenericFormBuilder.jsx +735 -37
- package/src/components/generic/GenericIndex.jsx +15 -5
- package/src/components/generic/OutstandingRuleEditor.jsx +313 -0
- package/src/components/styles/DataGrid.module.scss +38 -22
- package/src/components/styles/Form.module.scss +32 -33
- package/src/components/styles/GenericDashboard.module.scss +155 -0
- package/src/components/styles/GenericDetail.module.scss +28 -30
- package/src/components/styles/GenericDynamic.module.scss +7 -21
- package/src/components/styles/GenericEditableTable.module.scss +5 -22
- package/src/components/styles/GenericFormBuilder.module.scss +605 -30
- package/src/components/styles/GenericIndex.module.scss +17 -32
- package/src/components/styles/GenericMain.module.scss +18 -6
- package/src/components/styles/GenericQuote.module.scss +7 -22
- package/src/components/styles/GenericReport.module.scss +5 -37
- package/src/components/styles/Login.module.scss +415 -239
- package/src/components/styles/Navigation.module.scss +124 -35
- package/src/components/styles/Profile.module.scss +5 -26
- package/src/components/styles/QuickAction.module.scss +34 -29
- package/src/components/styles/_controls.scss +139 -0
- package/src/components/styles/global.css +54 -15
package/package.json
CHANGED
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"react-dom": "^17.0.0 || ^18.0.0"
|
|
92
92
|
},
|
|
93
93
|
"name": "@visns-studio/visns-components",
|
|
94
|
-
"version": "6.0
|
|
94
|
+
"version": "6.1.0",
|
|
95
95
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
96
96
|
"main": "src/index.js",
|
|
97
97
|
"files": [
|
|
@@ -236,6 +236,34 @@ const loadData = async (
|
|
|
236
236
|
}
|
|
237
237
|
};
|
|
238
238
|
|
|
239
|
+
/**
|
|
240
|
+
* How tall the grid should be.
|
|
241
|
+
*
|
|
242
|
+
* It used to claim 70% of the viewport (or 550px) for any result set larger
|
|
243
|
+
* than eight rows, whatever the actual row count — so 18 rows on a tall
|
|
244
|
+
* monitor left several hundred pixels of empty white below the last one.
|
|
245
|
+
*
|
|
246
|
+
* Height now follows the content and only falls back to a viewport share when
|
|
247
|
+
* the rows genuinely overflow, which is the point at which a tall scroller is
|
|
248
|
+
* useful rather than just empty.
|
|
249
|
+
*/
|
|
250
|
+
const ROW_HEIGHT = 34;
|
|
251
|
+
const GRID_CHROME = ROW_HEIGHT * 2 + 60; // header + filter row + footer
|
|
252
|
+
|
|
253
|
+
function fitToRows(dataCount, windowHeight) {
|
|
254
|
+
const roomy = Math.max(windowHeight * 0.7, 550);
|
|
255
|
+
|
|
256
|
+
if (!dataCount || dataCount <= 0) {
|
|
257
|
+
// Nothing to show: enough for the empty state, not a blank screen.
|
|
258
|
+
return 300;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const needed = dataCount * ROW_HEIGHT + GRID_CHROME;
|
|
262
|
+
|
|
263
|
+
// Never taller than the rows require, never so short it feels cramped.
|
|
264
|
+
return Math.min(roomy, Math.max(needed, 260));
|
|
265
|
+
}
|
|
266
|
+
|
|
239
267
|
const DataGrid = forwardRef(
|
|
240
268
|
(
|
|
241
269
|
{
|
|
@@ -1277,6 +1305,11 @@ const DataGrid = forwardRef(
|
|
|
1277
1305
|
|
|
1278
1306
|
const handleSettingClick = (s, d) => {
|
|
1279
1307
|
switch (s.id) {
|
|
1308
|
+
case 'cloudDownload':
|
|
1309
|
+
// File download: open the URL directly and let the
|
|
1310
|
+
// browser handle the response's content disposition.
|
|
1311
|
+
window.open(`${s.url}/${d[s.key]}`, '_blank');
|
|
1312
|
+
break;
|
|
1280
1313
|
case 'activate':
|
|
1281
1314
|
case 'arrowCycle':
|
|
1282
1315
|
case 'cloudUpload':
|
|
@@ -3203,6 +3236,8 @@ const DataGrid = forwardRef(
|
|
|
3203
3236
|
return getIconComponent(RefreshCw);
|
|
3204
3237
|
case 'cloudUpload':
|
|
3205
3238
|
return getIconComponent(Upload);
|
|
3239
|
+
case 'cloudDownload':
|
|
3240
|
+
return getIconComponent(DownloadIcon);
|
|
3206
3241
|
case 'oauth2':
|
|
3207
3242
|
return getIconComponent(Lock);
|
|
3208
3243
|
case 'clone':
|
|
@@ -4380,13 +4415,7 @@ const DataGrid = forwardRef(
|
|
|
4380
4415
|
minHeight:
|
|
4381
4416
|
gridHeight && gridHeight > 0
|
|
4382
4417
|
? gridHeight
|
|
4383
|
-
: dataCount
|
|
4384
|
-
? dataCount > 8
|
|
4385
|
-
? windowHeight * 0.7 > 550
|
|
4386
|
-
? windowHeight * 0.7
|
|
4387
|
-
: 550
|
|
4388
|
-
: 450
|
|
4389
|
-
: 400,
|
|
4418
|
+
: fitToRows(dataCount, windowHeight),
|
|
4390
4419
|
boxShadow: 'none',
|
|
4391
4420
|
...(hoverColor ? { '--hover-color': hoverColor } : {}),
|
|
4392
4421
|
});
|
|
@@ -4397,11 +4426,7 @@ const DataGrid = forwardRef(
|
|
|
4397
4426
|
useEffect(() => {
|
|
4398
4427
|
const getMinimumHeight = () => {
|
|
4399
4428
|
if (gridHeight > 0) return gridHeight;
|
|
4400
|
-
|
|
4401
|
-
if (dataCount <= 5) return 400;
|
|
4402
|
-
if (dataCount <= 8) return 450;
|
|
4403
|
-
}
|
|
4404
|
-
return Math.max(window.innerHeight * 0.7, 550);
|
|
4429
|
+
return fitToRows(dataCount, window.innerHeight);
|
|
4405
4430
|
};
|
|
4406
4431
|
|
|
4407
4432
|
const minHeight = getMinimumHeight();
|
|
@@ -361,6 +361,7 @@ function Navigation({
|
|
|
361
361
|
<ul className={styles.navDropdown}>
|
|
362
362
|
{n.children.map(
|
|
363
363
|
(child, childKey) =>
|
|
364
|
+
child.hidden !== true &&
|
|
364
365
|
child.permission === true && (
|
|
365
366
|
<li key={`nav-child-${childKey}`}>
|
|
366
367
|
<Link
|
|
@@ -422,11 +423,57 @@ function Navigation({
|
|
|
422
423
|
return [];
|
|
423
424
|
};
|
|
424
425
|
|
|
426
|
+
/**
|
|
427
|
+
* Feature switches, if the consuming app publishes any.
|
|
428
|
+
*
|
|
429
|
+
* A platform owner can retire a half-finished feature from the menu
|
|
430
|
+
* without a deploy, so the profile response may carry a map of feature
|
|
431
|
+
* ids that are switched off. Apps that publish nothing get `null` and
|
|
432
|
+
* behave exactly as before — this filter is additive, and every unknown
|
|
433
|
+
* id stays visible, so a stale or missing map can never empty a menu.
|
|
434
|
+
*/
|
|
435
|
+
const getFeatureFlags = () => {
|
|
436
|
+
const flags = userProfile?.feature_visibility;
|
|
437
|
+
|
|
438
|
+
if (!flags || typeof flags !== 'object') {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// The map is keyed by app; this component only ever draws the console.
|
|
443
|
+
const appFlags = flags.backend;
|
|
444
|
+
|
|
445
|
+
return appFlags && typeof appFlags === 'object' ? appFlags : null;
|
|
446
|
+
};
|
|
447
|
+
|
|
425
448
|
useEffect(() => {
|
|
426
449
|
const permissions = getUserPermissions();
|
|
450
|
+
const featureFlags = getFeatureFlags();
|
|
451
|
+
|
|
452
|
+
// Carried as its own flag rather than folded into `permission`: an
|
|
453
|
+
// item with an empty permissionKey renders whatever its permission
|
|
454
|
+
// says, and this state has to be reversible — switching a feature
|
|
455
|
+
// back on must restore the menu item without a page reload, which
|
|
456
|
+
// dropping it from the list would not.
|
|
457
|
+
const isSwitchedOff = (nav) =>
|
|
458
|
+
featureFlags !== null &&
|
|
459
|
+
Boolean(nav.id) &&
|
|
460
|
+
featureFlags[nav.id] === false;
|
|
427
461
|
|
|
428
462
|
setNavData((prevNavData) => {
|
|
429
463
|
const updatePermissions = (nav) => {
|
|
464
|
+
const hidden = isSwitchedOff(nav);
|
|
465
|
+
|
|
466
|
+
// Reserved for the platform owner — a privilege that sits
|
|
467
|
+
// above the roles an administrator can hand out, so it is
|
|
468
|
+
// never expressed as a permission key.
|
|
469
|
+
if (nav.superAdminOnly === true) {
|
|
470
|
+
return {
|
|
471
|
+
...nav,
|
|
472
|
+
hidden,
|
|
473
|
+
permission: Boolean(userProfile?.is_super_admin),
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
430
477
|
if (nav.children && nav.children.length > 0) {
|
|
431
478
|
const updatedChildren = nav.children
|
|
432
479
|
.map(updatePermissions)
|
|
@@ -439,12 +486,14 @@ function Navigation({
|
|
|
439
486
|
);
|
|
440
487
|
return {
|
|
441
488
|
...nav,
|
|
489
|
+
hidden,
|
|
442
490
|
permission: matchingPermission,
|
|
443
491
|
children: updatedChildren,
|
|
444
492
|
};
|
|
445
493
|
} else {
|
|
446
494
|
return {
|
|
447
495
|
...nav,
|
|
496
|
+
hidden,
|
|
448
497
|
permission: childPermission,
|
|
449
498
|
children: updatedChildren,
|
|
450
499
|
};
|
|
@@ -457,12 +506,14 @@ function Navigation({
|
|
|
457
506
|
);
|
|
458
507
|
return {
|
|
459
508
|
...nav,
|
|
509
|
+
hidden,
|
|
460
510
|
permission: matchingPermission,
|
|
461
511
|
};
|
|
462
512
|
}
|
|
463
513
|
|
|
464
514
|
return {
|
|
465
515
|
...nav,
|
|
516
|
+
hidden,
|
|
466
517
|
permission: true,
|
|
467
518
|
};
|
|
468
519
|
};
|
|
@@ -620,8 +671,9 @@ function Navigation({
|
|
|
620
671
|
<nav className={styles.navwrap} ref={navWrapRef}>
|
|
621
672
|
<ul className={appNavClasses}>
|
|
622
673
|
{navData.navigations.map((nav, navKey) =>
|
|
623
|
-
nav.
|
|
624
|
-
nav.
|
|
674
|
+
nav.hidden !== true &&
|
|
675
|
+
(nav.permission === true ||
|
|
676
|
+
nav.permissionKey === '') ? (
|
|
625
677
|
<React.Fragment key={`nav-item-${navKey}`}>
|
|
626
678
|
{renderNav(nav)}
|
|
627
679
|
</React.Fragment>
|
|
@@ -671,6 +723,7 @@ function Navigation({
|
|
|
671
723
|
>
|
|
672
724
|
<ul>
|
|
673
725
|
{navData.settings.map((setting, settingKey) =>
|
|
726
|
+
setting.hidden !== true &&
|
|
674
727
|
setting.permission === true ? (
|
|
675
728
|
<React.Fragment
|
|
676
729
|
key={`setting-${settingKey}`}
|
|
@@ -711,7 +764,9 @@ function Navigation({
|
|
|
711
764
|
<div className={styles.navwrap} ref={navWrapRef}>
|
|
712
765
|
<ul className={appNavClasses}>
|
|
713
766
|
{navData.navigations.map((nav, navKey) =>
|
|
714
|
-
nav.
|
|
767
|
+
nav.hidden !== true &&
|
|
768
|
+
(nav.permission === true ||
|
|
769
|
+
nav.permissionKey === '') ? (
|
|
715
770
|
<React.Fragment key={`nav-item-${navKey}`}>
|
|
716
771
|
{renderNav(nav)}
|
|
717
772
|
</React.Fragment>
|
|
@@ -729,6 +784,7 @@ function Navigation({
|
|
|
729
784
|
>
|
|
730
785
|
<ul>
|
|
731
786
|
{navData.settings.map((setting, settingKey) =>
|
|
787
|
+
setting.hidden !== true &&
|
|
732
788
|
setting.permission === true ? (
|
|
733
789
|
<React.Fragment key={`setting-${settingKey}`}>
|
|
734
790
|
{renderSetting(setting)}
|
|
@@ -1,19 +1,41 @@
|
|
|
1
1
|
import '../styles/global.css';
|
|
2
2
|
|
|
3
|
-
import React, { useState } from 'react';
|
|
3
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
4
4
|
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
|
5
|
-
import Reveal from '../utils/Reveal';
|
|
6
5
|
import { toast } from 'react-toastify';
|
|
7
|
-
import {
|
|
6
|
+
import { Eye, EyeOff } from 'lucide-react';
|
|
8
7
|
|
|
9
8
|
import CustomFetch from '../Fetch';
|
|
10
9
|
|
|
11
10
|
import styles from '../styles/Login.module.scss';
|
|
12
11
|
|
|
13
|
-
const Login = ({
|
|
12
|
+
const Login = ({
|
|
13
|
+
logo,
|
|
14
|
+
loginBg,
|
|
15
|
+
providers,
|
|
16
|
+
setSystemAuth,
|
|
17
|
+
setUserProfile,
|
|
18
|
+
config = {},
|
|
19
|
+
}) => {
|
|
14
20
|
const location = useLocation();
|
|
15
21
|
const navigate = useNavigate();
|
|
16
22
|
const [isLoading, setIsLoading] = useState(false);
|
|
23
|
+
const [showPassword, setShowPassword] = useState(false);
|
|
24
|
+
|
|
25
|
+
// Collapsed behind a button, since Microsoft is how nearly everyone signs
|
|
26
|
+
// in. Open from the start when there is no SSO provider configured —
|
|
27
|
+
// otherwise the only way in would be hidden behind a disclosure.
|
|
28
|
+
const ssoAvailable = Array.isArray(providers) && providers.length > 0;
|
|
29
|
+
const [showEmail, setShowEmail] = useState(!ssoAvailable);
|
|
30
|
+
const emailRef = useRef(null);
|
|
31
|
+
|
|
32
|
+
// Opening the form should put the cursor where typing starts, rather than
|
|
33
|
+
// leaving it on the button that opened it.
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (showEmail && ssoAvailable) {
|
|
36
|
+
emailRef.current?.focus();
|
|
37
|
+
}
|
|
38
|
+
}, [showEmail, ssoAvailable]);
|
|
17
39
|
|
|
18
40
|
const [auth, setAuth] = useState({
|
|
19
41
|
email: '',
|
|
@@ -32,43 +54,37 @@ const Login = ({ logo, providers, setSystemAuth, setUserProfile, config = {} })
|
|
|
32
54
|
return null;
|
|
33
55
|
}
|
|
34
56
|
|
|
35
|
-
let content =
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
</>
|
|
67
|
-
);
|
|
68
|
-
break;
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
}
|
|
57
|
+
let content = null;
|
|
58
|
+
|
|
59
|
+
providers.forEach((provider) => {
|
|
60
|
+
switch (provider) {
|
|
61
|
+
case 'azure':
|
|
62
|
+
content = (
|
|
63
|
+
<button
|
|
64
|
+
type="button"
|
|
65
|
+
className={styles.sso}
|
|
66
|
+
onClick={(e) => {
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
window.location.href = '/auth/azure';
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
{/* The Microsoft mark keeps its own light tile.
|
|
72
|
+
Its four squares are drawn for a white ground
|
|
73
|
+
and sit awkwardly straight on navy. */}
|
|
74
|
+
<span className={styles.ssoMark}>
|
|
75
|
+
<img
|
|
76
|
+
src="https://d16lktya8ojp5z.cloudfront.net/generic/images/microsoft.png"
|
|
77
|
+
width="18"
|
|
78
|
+
height="18"
|
|
79
|
+
alt=""
|
|
80
|
+
/>
|
|
81
|
+
</span>
|
|
82
|
+
Sign in with Microsoft
|
|
83
|
+
</button>
|
|
84
|
+
);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
72
88
|
|
|
73
89
|
return content;
|
|
74
90
|
};
|
|
@@ -162,141 +178,153 @@ const Login = ({ logo, providers, setSystemAuth, setUserProfile, config = {} })
|
|
|
162
178
|
};
|
|
163
179
|
|
|
164
180
|
return (
|
|
165
|
-
<div className={styles.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
181
|
+
<div className={styles.auth}>
|
|
182
|
+
{/* The company's own work, not decoration. It is the reason the
|
|
183
|
+
rest of this system exists, so it carries the brand side and
|
|
184
|
+
the form is left to be a form. */}
|
|
185
|
+
<aside
|
|
186
|
+
className={styles.brand}
|
|
187
|
+
style={
|
|
188
|
+
loginBg ? { backgroundImage: `url(${loginBg})` } : undefined
|
|
189
|
+
}
|
|
190
|
+
aria-hidden="true"
|
|
191
|
+
>
|
|
192
|
+
<div className={styles.brandInk}>
|
|
193
|
+
<p className={styles.eyebrow}>Construction Management</p>
|
|
194
|
+
<p className={styles.brandLine}>Prime Builders</p>
|
|
195
|
+
<p className={styles.brandSub}>
|
|
196
|
+
Job tracking, inspections, site forms and labour
|
|
197
|
+
scheduling — in one place.
|
|
198
|
+
</p>
|
|
199
|
+
</div>
|
|
200
|
+
</aside>
|
|
201
|
+
|
|
202
|
+
<main className={styles.panel}>
|
|
203
|
+
<div className={styles.panelInner}>
|
|
204
|
+
<img src={logo} alt="Prime Builders" className={styles.logo} />
|
|
205
|
+
|
|
206
|
+
<h1 className={styles.title}>Sign in</h1>
|
|
207
|
+
<p className={styles.subtitle}>
|
|
208
|
+
Use your Prime Microsoft account to continue.
|
|
209
|
+
</p>
|
|
210
|
+
|
|
211
|
+
{/* Primary: this is how Prime staff actually sign in. The
|
|
212
|
+
email form stays available underneath for accounts
|
|
213
|
+
without a Microsoft sign-in. */}
|
|
214
|
+
{renderSsoButton()}
|
|
215
|
+
|
|
216
|
+
{ssoAvailable && !showEmail && (
|
|
217
|
+
<button
|
|
218
|
+
type="button"
|
|
219
|
+
className={styles.altToggle}
|
|
220
|
+
onClick={() => setShowEmail(true)}
|
|
221
|
+
aria-expanded="false"
|
|
222
|
+
aria-controls="email-signin"
|
|
223
|
+
>
|
|
224
|
+
Sign in with email instead
|
|
225
|
+
</button>
|
|
226
|
+
)}
|
|
227
|
+
|
|
228
|
+
{showEmail && ssoAvailable && (
|
|
229
|
+
<div className={styles.divider}>
|
|
230
|
+
<span>or sign in with email</span>
|
|
231
|
+
</div>
|
|
232
|
+
)}
|
|
233
|
+
|
|
234
|
+
{/* A grid row animating 0fr -> 1fr, so it opens to whatever
|
|
235
|
+
height the form needs without hardcoding one. */}
|
|
236
|
+
<div
|
|
237
|
+
id="email-signin"
|
|
238
|
+
className={`${styles.collapse} ${
|
|
239
|
+
showEmail ? styles.collapseOpen : ''
|
|
240
|
+
}`}
|
|
241
|
+
>
|
|
242
|
+
<div className={styles.collapseInner}>
|
|
243
|
+
<form onSubmit={handleSubmit} className={styles.form}>
|
|
244
|
+
<div className={styles.field}>
|
|
245
|
+
<input
|
|
246
|
+
id="login-email"
|
|
247
|
+
ref={emailRef}
|
|
248
|
+
type="text"
|
|
249
|
+
name="email"
|
|
250
|
+
value={auth.email}
|
|
251
|
+
onChange={handleChange}
|
|
252
|
+
tabIndex="1"
|
|
253
|
+
className={authClass.username}
|
|
254
|
+
autoComplete="username"
|
|
255
|
+
placeholder=" "
|
|
256
|
+
/>
|
|
257
|
+
<label htmlFor="login-email">Email</label>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<div className={styles.field}>
|
|
261
|
+
<input
|
|
262
|
+
id="login-password"
|
|
263
|
+
type={showPassword ? 'text' : 'password'}
|
|
264
|
+
name="password"
|
|
265
|
+
value={auth.password}
|
|
266
|
+
onChange={handleChange}
|
|
267
|
+
tabIndex="2"
|
|
268
|
+
className={authClass.password}
|
|
269
|
+
autoComplete="current-password"
|
|
270
|
+
placeholder=" "
|
|
271
|
+
/>
|
|
272
|
+
<label htmlFor="login-password">Password</label>
|
|
273
|
+
{/* Typing a password blind on a phone in the sun is
|
|
274
|
+
where most failed sign-ins come from. */}
|
|
275
|
+
<button
|
|
276
|
+
type="button"
|
|
277
|
+
className={styles.reveal}
|
|
278
|
+
onClick={() => setShowPassword((on) => !on)}
|
|
279
|
+
aria-label={
|
|
280
|
+
showPassword
|
|
281
|
+
? 'Hide password'
|
|
282
|
+
: 'Show password'
|
|
283
|
+
}
|
|
284
|
+
tabIndex="-1"
|
|
285
|
+
>
|
|
286
|
+
{showPassword ? (
|
|
287
|
+
<EyeOff size={18} strokeWidth={1.9} />
|
|
288
|
+
) : (
|
|
289
|
+
<Eye size={18} strokeWidth={1.9} />
|
|
290
|
+
)}
|
|
291
|
+
</button>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
<div className={styles.row}>
|
|
295
|
+
<label className={styles.remember}>
|
|
296
|
+
<input
|
|
297
|
+
type="checkbox"
|
|
298
|
+
name="remember"
|
|
299
|
+
checked={auth.remember}
|
|
300
|
+
onChange={handleChange}
|
|
301
|
+
/>
|
|
302
|
+
Remember me
|
|
303
|
+
</label>
|
|
304
|
+
|
|
305
|
+
{config.passwordReset !== false && (
|
|
306
|
+
<Link to="/reset" className={styles.link}>
|
|
307
|
+
Forgot password?
|
|
308
|
+
</Link>
|
|
309
|
+
)}
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
<button
|
|
313
|
+
className={styles.submit}
|
|
314
|
+
type="submit"
|
|
315
|
+
disabled={isLoading}
|
|
316
|
+
>
|
|
317
|
+
{isLoading ? 'Signing in…' : 'Sign in with email'}
|
|
318
|
+
</button>
|
|
319
|
+
</form>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
<p className={styles.foot}>
|
|
324
|
+
Prime Builders · Perth, Western Australia
|
|
325
|
+
</p>
|
|
298
326
|
</div>
|
|
299
|
-
</
|
|
327
|
+
</main>
|
|
300
328
|
</div>
|
|
301
329
|
);
|
|
302
330
|
};
|