@flaredev/cli 0.2.0 → 0.4.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/bin/flare.js +9 -1
- package/dist/index.js +991 -344
- package/package.json +2 -2
- package/templates/app/app/admin/page.tsx +9 -61
- package/templates/app/app/dashboard/account/layout.tsx +30 -0
- package/templates/app/app/dashboard/account/page.tsx +11 -31
- package/templates/app/app/dashboard/account/password/page.tsx +22 -0
- package/templates/app/app/dashboard/account/security/page.tsx +34 -0
- package/templates/app/app/dashboard/account/sessions/page.tsx +9 -0
- package/templates/app/app/dashboard/actions.ts +246 -0
- package/templates/app/app/dashboard/import-actions.ts +91 -0
- package/templates/app/app/dashboard/layout.tsx +66 -39
- package/templates/app/app/dashboard/observability/page.tsx +175 -0
- package/templates/app/app/dashboard/page.tsx +108 -27
- package/templates/app/app/verify-email/page.tsx +33 -0
- package/templates/app/components/account/account-nav.tsx +39 -0
- package/templates/app/components/account/{security-settings.tsx → sections.tsx} +507 -525
- package/templates/app/components/auth/otp-input.tsx +95 -0
- package/templates/app/components/auth/password-field.tsx +116 -0
- package/templates/app/components/auth/password-forms.tsx +76 -79
- package/templates/app/components/auth/sign-in-flow.tsx +306 -315
- package/templates/app/components/auth/sign-up-form.tsx +93 -101
- package/templates/app/components/auth/two-factor-form.tsx +139 -124
- package/templates/app/components/auth/use-resend.ts +44 -0
- package/templates/app/components/auth/verify-email-form.tsx +135 -0
- package/templates/app/components/dashboard/column-menu.tsx +66 -0
- package/templates/app/components/dashboard/dashboard-header.tsx +115 -0
- package/templates/app/components/dashboard/dashboard-sidebar.tsx +228 -0
- package/templates/app/components/dashboard/dropzone.tsx +92 -0
- package/templates/app/components/dashboard/editable.ts +12 -0
- package/templates/app/components/dashboard/export-button.tsx +41 -0
- package/templates/app/components/{admin → dashboard}/fields/file-field.tsx +22 -18
- package/templates/app/components/dashboard/import-dialog.tsx +540 -0
- package/templates/app/components/dashboard/inline-cell.tsx +165 -0
- package/templates/app/components/dashboard/local-time.tsx +77 -0
- package/templates/app/components/dashboard/page-header.tsx +57 -0
- package/templates/app/components/{admin → dashboard}/query.ts +2 -0
- package/templates/app/components/dashboard/record-actions.tsx +113 -0
- package/templates/app/components/dashboard/record-detail.tsx +200 -0
- package/templates/app/components/dashboard/resource-chart.tsx +91 -0
- package/templates/app/components/dashboard/resource-form-dialog.tsx +80 -0
- package/templates/app/components/{admin → dashboard}/resource-form-page.tsx +61 -55
- package/templates/app/components/{admin → dashboard}/resource-form.tsx +165 -139
- package/templates/app/components/dashboard/resource-stats.tsx +48 -0
- package/templates/app/components/{admin → dashboard}/resource-table.tsx +152 -28
- package/templates/app/components/{admin → dashboard}/row-actions.tsx +56 -11
- package/templates/app/components/dashboard/save-view-button.tsx +86 -0
- package/templates/app/components/dashboard/stat-card.tsx +69 -0
- package/templates/app/components/dashboard/table-selection.tsx +185 -0
- package/templates/app/components/dashboard/traffic-chart.tsx +32 -0
- package/templates/app/components/dashboard/user-menu.tsx +91 -0
- package/templates/app/components/ui/sonner.tsx +33 -13
- package/templates/app/db/flare-schema.ts +55 -1
- package/templates/app/lib/analytics.ts +121 -0
- package/templates/app/lib/api.ts +12 -0
- package/templates/app/lib/audit.ts +80 -0
- package/templates/app/lib/auth-config.ts +38 -33
- package/templates/app/lib/auth.ts +34 -0
- package/templates/app/lib/cache.ts +83 -83
- package/templates/app/lib/csv.ts +24 -0
- package/templates/app/lib/dashboard-nav.ts +15 -0
- package/templates/app/lib/dashboard.ts +165 -0
- package/templates/app/lib/password-rules.ts +159 -0
- package/templates/app/lib/site.ts +56 -47
- package/templates/app/lib/views.ts +29 -0
- package/templates/app/migrations/0000_init.sql +25 -2
- package/templates/app/migrations/meta/0000_snapshot.json +159 -1
- package/templates/app/migrations/meta/_journal.json +1 -1
- package/templates/app/vite.config.ts +41 -31
- package/templates/app/worker-configuration.d.ts +15764 -0
- package/templates/locks/package-lock.json +9189 -0
- package/templates/locks/pnpm-lock.yaml +6347 -0
- package/templates/locks/yarn.lock +3548 -0
- package/templates/app/app/admin/actions.ts +0 -113
- package/templates/app/app/admin/layout.tsx +0 -32
- package/templates/app/components/admin/admin-header.tsx +0 -78
- package/templates/app/components/admin/resource-nav.tsx +0 -120
- package/templates/app/components/dashboard/dashboard-nav.tsx +0 -35
- package/templates/app/lib/admin-nav.ts +0 -15
- package/templates/app/lib/admin.ts +0 -103
- /package/templates/app/components/{admin → dashboard}/fields/country-field.tsx +0 -0
- /package/templates/app/components/{admin → dashboard}/fields/date-field.tsx +0 -0
- /package/templates/app/components/{admin → dashboard}/fields/field-widget.tsx +0 -0
- /package/templates/app/components/{admin → dashboard}/fields/multi-select-field.tsx +0 -0
- /package/templates/app/components/{admin → dashboard}/fields/phone-field.tsx +0 -0
- /package/templates/app/components/{admin → dashboard}/fields/relation-field.tsx +0 -0
- /package/templates/app/components/{admin → dashboard}/resource-icon.tsx +0 -0
- /package/templates/app/components/{admin → dashboard}/resource-table-toolbar.tsx +0 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useId, useRef, useState } from "react";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
value: string;
|
|
8
|
+
onChange: (value: string) => void;
|
|
9
|
+
/** Called once the last character is typed or pasted, so nobody taps "Continue". */
|
|
10
|
+
onComplete?: (value: string) => void;
|
|
11
|
+
length?: number;
|
|
12
|
+
label?: string;
|
|
13
|
+
autoFocus?: boolean;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
/** Backup codes are letters and digits; emailed codes are digits. */
|
|
16
|
+
mode?: "numeric" | "alphanumeric";
|
|
17
|
+
id?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A code box that is really one input with the cells drawn behind it.
|
|
22
|
+
*
|
|
23
|
+
* The obvious build — one input per digit — breaks paste, iOS and Android code
|
|
24
|
+
* autofill, backspace across cells and screen readers, all of which a single input
|
|
25
|
+
* gets for free from `autocomplete="one-time-code"`.
|
|
26
|
+
*/
|
|
27
|
+
export function OtpInput({ value, onChange, onComplete, length = 6, label = "Verification code", autoFocus, disabled, mode = "numeric", id }: Props) {
|
|
28
|
+
const generatedId = useId();
|
|
29
|
+
const fieldId = id ?? generatedId;
|
|
30
|
+
const input = useRef<HTMLInputElement>(null);
|
|
31
|
+
const [focused, setFocused] = useState(false);
|
|
32
|
+
|
|
33
|
+
const clean = (raw: string) => (mode === "numeric" ? raw.replace(/\D/g, "") : raw.replace(/[^a-zA-Z0-9]/g, "").toUpperCase()).slice(0, length);
|
|
34
|
+
|
|
35
|
+
const handle = (raw: string) => {
|
|
36
|
+
const next = clean(raw);
|
|
37
|
+
onChange(next);
|
|
38
|
+
if (next.length === length) onComplete?.(next);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const characters = [...value];
|
|
42
|
+
// The cursor sits on the first empty cell, or the last one when it's full.
|
|
43
|
+
const cursor = Math.min(value.length, length - 1);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="flex flex-col gap-2">
|
|
47
|
+
<label htmlFor={fieldId} className="sr-only">
|
|
48
|
+
{label}
|
|
49
|
+
</label>
|
|
50
|
+
<div className="relative" onClick={() => input.current?.focus()}>
|
|
51
|
+
<input
|
|
52
|
+
ref={input}
|
|
53
|
+
id={fieldId}
|
|
54
|
+
value={value}
|
|
55
|
+
onChange={(event) => handle(event.target.value)}
|
|
56
|
+
onFocus={() => setFocused(true)}
|
|
57
|
+
onBlur={() => setFocused(false)}
|
|
58
|
+
onPaste={(event) => {
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
handle(event.clipboardData.getData("text"));
|
|
61
|
+
}}
|
|
62
|
+
inputMode={mode === "numeric" ? "numeric" : "text"}
|
|
63
|
+
autoComplete="one-time-code"
|
|
64
|
+
autoCorrect="off"
|
|
65
|
+
spellCheck={false}
|
|
66
|
+
maxLength={length}
|
|
67
|
+
disabled={disabled}
|
|
68
|
+
autoFocus={autoFocus}
|
|
69
|
+
aria-label={label}
|
|
70
|
+
// Invisible but really there, so autofill, paste and the caret all work.
|
|
71
|
+
// The right padding keeps a password manager's icon off the last cell.
|
|
72
|
+
className="absolute inset-0 h-full w-full cursor-text pr-10 text-transparent caret-transparent opacity-0 outline-none"
|
|
73
|
+
/>
|
|
74
|
+
<div className="flex gap-2" aria-hidden="true">
|
|
75
|
+
{Array.from({ length }, (_, index) => {
|
|
76
|
+
const active = focused && index === cursor;
|
|
77
|
+
return (
|
|
78
|
+
<div
|
|
79
|
+
key={index}
|
|
80
|
+
className={cn(
|
|
81
|
+
"relative grid h-13 flex-1 place-items-center rounded-[calc(var(--radius)-2px)] border border-border bg-surface text-lg font-medium tabular-nums transition-[border-color,box-shadow]",
|
|
82
|
+
active && "border-brand ring-[3px] ring-brand/20",
|
|
83
|
+
disabled && "opacity-60",
|
|
84
|
+
)}
|
|
85
|
+
>
|
|
86
|
+
{characters[index] ?? ""}
|
|
87
|
+
{active && !characters[index] && <span className="absolute h-5 w-px animate-pulse bg-foreground" />}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
})}
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useId, useMemo, useState } from "react";
|
|
4
|
+
import { CheckIcon, EyeIcon, EyeOffIcon } from "lucide-react";
|
|
5
|
+
import { checkPassword, PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH } from "@/lib/password-rules";
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
import { AuthInput, AuthLabel } from "./ui";
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
value: string;
|
|
11
|
+
onChange: (value: string) => void;
|
|
12
|
+
label?: string;
|
|
13
|
+
id?: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
autoComplete?: "new-password" | "current-password";
|
|
16
|
+
autoFocus?: boolean;
|
|
17
|
+
required?: boolean;
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
/** Show the rules and strength as they type. Off for "your current password". */
|
|
20
|
+
showRules?: boolean;
|
|
21
|
+
/** Their name and email: a password shouldn't contain either. */
|
|
22
|
+
avoid?: (string | null | undefined)[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const BAR_COLOURS = ["bg-danger", "bg-danger", "bg-warning", "bg-success/70", "bg-success"];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A password input that says what it wants before you get it wrong: the rules and a
|
|
29
|
+
* strength reading update as you type, and the eye shows what you've typed.
|
|
30
|
+
*
|
|
31
|
+
* Only the length is enforced; everything else reads as advice (see lib/password-rules.ts).
|
|
32
|
+
*/
|
|
33
|
+
export function PasswordField({
|
|
34
|
+
value,
|
|
35
|
+
onChange,
|
|
36
|
+
label = "Password",
|
|
37
|
+
id,
|
|
38
|
+
name = "password",
|
|
39
|
+
autoComplete = "new-password",
|
|
40
|
+
autoFocus,
|
|
41
|
+
required = true,
|
|
42
|
+
placeholder,
|
|
43
|
+
showRules = autoComplete === "new-password",
|
|
44
|
+
avoid = [],
|
|
45
|
+
}: Props) {
|
|
46
|
+
const generatedId = useId();
|
|
47
|
+
const fieldId = id ?? generatedId;
|
|
48
|
+
const hintId = `${fieldId}-hint`;
|
|
49
|
+
const [visible, setVisible] = useState(false);
|
|
50
|
+
const check = useMemo(() => checkPassword(value, avoid), [value, avoid.join("\u0000")]);
|
|
51
|
+
const started = value.length > 0;
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="flex flex-col gap-2">
|
|
55
|
+
<AuthLabel htmlFor={fieldId}>{label}</AuthLabel>
|
|
56
|
+
<div className="relative">
|
|
57
|
+
<AuthInput
|
|
58
|
+
id={fieldId}
|
|
59
|
+
name={name}
|
|
60
|
+
type={visible ? "text" : "password"}
|
|
61
|
+
autoComplete={autoComplete}
|
|
62
|
+
autoFocus={autoFocus}
|
|
63
|
+
required={required}
|
|
64
|
+
placeholder={placeholder}
|
|
65
|
+
value={value}
|
|
66
|
+
onChange={(event) => onChange(event.target.value)}
|
|
67
|
+
minLength={autoComplete === "new-password" ? PASSWORD_MIN_LENGTH : undefined}
|
|
68
|
+
maxLength={PASSWORD_MAX_LENGTH}
|
|
69
|
+
aria-describedby={showRules ? hintId : undefined}
|
|
70
|
+
// A styling hook, so the field can go green without any state plumbing.
|
|
71
|
+
data-has-passed-validation={showRules && check.valid ? "" : undefined}
|
|
72
|
+
className={cn("pr-11", showRules && check.valid && "border-success/60")}
|
|
73
|
+
/>
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
onClick={() => setVisible(!visible)}
|
|
77
|
+
aria-pressed={visible}
|
|
78
|
+
aria-controls={fieldId}
|
|
79
|
+
aria-label={visible ? "Hide password" : "Show password"}
|
|
80
|
+
title={visible ? "Hide password" : "Show password"}
|
|
81
|
+
className="absolute inset-y-0 right-0 grid w-11 place-items-center text-foreground-muted hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand"
|
|
82
|
+
>
|
|
83
|
+
{visible ? <EyeOffIcon className="size-4.5" /> : <EyeIcon className="size-4.5" />}
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
{showRules && (
|
|
88
|
+
<div id={hintId} className="flex flex-col gap-2">
|
|
89
|
+
<div className="flex items-center gap-2">
|
|
90
|
+
<div className="flex h-1 flex-1 gap-1" aria-hidden="true">
|
|
91
|
+
{[0, 1, 2, 3].map((step) => (
|
|
92
|
+
<span key={step} className={cn("h-full flex-1 rounded-full transition-colors", started && check.score > step ? BAR_COLOURS[check.score] : "bg-border")} />
|
|
93
|
+
))}
|
|
94
|
+
</div>
|
|
95
|
+
<span className={cn("text-xs tabular-nums", started ? "text-foreground-muted" : "text-transparent")}>{check.label}</span>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
{/* Announced politely: it changes on every keystroke, so it mustn't interrupt. */}
|
|
99
|
+
<ul className="flex flex-col gap-1" aria-live="polite">
|
|
100
|
+
{check.rules.map((rule) => (
|
|
101
|
+
<li key={rule.id} className={cn("flex items-center gap-2 text-xs", rule.met ? "text-success" : "text-foreground-muted")}>
|
|
102
|
+
<span className={cn("grid size-3.5 shrink-0 place-items-center rounded-full border", rule.met ? "border-success bg-success/15" : "border-border")}>
|
|
103
|
+
{rule.met && <CheckIcon className="size-2.5" />}
|
|
104
|
+
</span>
|
|
105
|
+
{rule.label}
|
|
106
|
+
{rule.required && !rule.met && <span className="text-foreground-muted">(required)</span>}
|
|
107
|
+
</li>
|
|
108
|
+
))}
|
|
109
|
+
</ul>
|
|
110
|
+
|
|
111
|
+
{started && check.advice && <p className="text-xs text-foreground-muted">{check.advice}</p>}
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -1,79 +1,76 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState } from "react";
|
|
4
|
-
import { authClient } from "@/lib/auth-client";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const [
|
|
13
|
-
const [
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const [
|
|
47
|
-
const [
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
setError(
|
|
55
|
-
|
|
56
|
-
setPending(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
</form>
|
|
78
|
-
);
|
|
79
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { authClient } from "@/lib/auth-client";
|
|
5
|
+
import { checkPassword } from "@/lib/password-rules";
|
|
6
|
+
import { PasswordField } from "./password-field";
|
|
7
|
+
import { authErrorMessage } from "./sign-in-flow";
|
|
8
|
+
import { AuthInput, AuthLabel, FormMessage, PrimaryButton } from "./ui";
|
|
9
|
+
|
|
10
|
+
/** Asks for a reset link. Says the same thing whether or not the address has an account. */
|
|
11
|
+
export function ForgotPasswordForm({ initialEmail }: { initialEmail: string }) {
|
|
12
|
+
const [email, setEmail] = useState(initialEmail);
|
|
13
|
+
const [pending, setPending] = useState(false);
|
|
14
|
+
const [sent, setSent] = useState(false);
|
|
15
|
+
const [error, setError] = useState<string | null>(null);
|
|
16
|
+
|
|
17
|
+
const submit = async (event: React.FormEvent) => {
|
|
18
|
+
event.preventDefault();
|
|
19
|
+
setPending(true);
|
|
20
|
+
setError(null);
|
|
21
|
+
const { error } = await authClient.requestPasswordReset({ email, redirectTo: "/reset-password" });
|
|
22
|
+
setPending(false);
|
|
23
|
+
if (error) return setError(authErrorMessage(error));
|
|
24
|
+
setSent(true);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
if (sent) {
|
|
28
|
+
return <FormMessage tone="success">If {email} has an account, a reset link is on its way. It expires in 1 hour.</FormMessage>;
|
|
29
|
+
}
|
|
30
|
+
return (
|
|
31
|
+
<form className="flex flex-col gap-4" onSubmit={submit}>
|
|
32
|
+
<div className="flex flex-col gap-2">
|
|
33
|
+
<AuthLabel htmlFor="email">Email</AuthLabel>
|
|
34
|
+
<AuthInput id="email" type="email" autoComplete="email" value={email} onChange={(event) => setEmail(event.target.value)} required autoFocus />
|
|
35
|
+
</div>
|
|
36
|
+
<FormMessage>{error}</FormMessage>
|
|
37
|
+
<PrimaryButton type="submit" pending={pending}>
|
|
38
|
+
Send reset link
|
|
39
|
+
</PrimaryButton>
|
|
40
|
+
</form>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Sets the new password from the emailed link's token. */
|
|
45
|
+
export function ResetPasswordForm({ token }: { token: string }) {
|
|
46
|
+
const [password, setPassword] = useState("");
|
|
47
|
+
const [confirm, setConfirm] = useState("");
|
|
48
|
+
const [pending, setPending] = useState(false);
|
|
49
|
+
const [error, setError] = useState<string | null>(null);
|
|
50
|
+
|
|
51
|
+
const submit = async (event: React.FormEvent) => {
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
const check = checkPassword(password);
|
|
54
|
+
if (!check.valid) return setError(check.advice ?? "That password is too short.");
|
|
55
|
+
if (password !== confirm) return setError("The two passwords don't match.");
|
|
56
|
+
setPending(true);
|
|
57
|
+
setError(null);
|
|
58
|
+
const { error } = await authClient.resetPassword({ newPassword: password, token });
|
|
59
|
+
setPending(false);
|
|
60
|
+
if (error) {
|
|
61
|
+
return setError(error.code === "INVALID_TOKEN" ? "This link has expired or was already used. Ask for a new one." : authErrorMessage(error));
|
|
62
|
+
}
|
|
63
|
+
window.location.href = "/sign-in?reset=1";
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<form className="flex flex-col gap-4" onSubmit={submit}>
|
|
68
|
+
<PasswordField id="password" label="New password" value={password} onChange={setPassword} autoFocus />
|
|
69
|
+
<PasswordField id="confirm" label="Confirm it" name="confirm" value={confirm} onChange={setConfirm} showRules={false} />
|
|
70
|
+
<FormMessage>{error}</FormMessage>
|
|
71
|
+
<PrimaryButton type="submit" pending={pending}>
|
|
72
|
+
Set new password
|
|
73
|
+
</PrimaryButton>
|
|
74
|
+
</form>
|
|
75
|
+
);
|
|
76
|
+
}
|