@sjolystinnovation/app-kit 0.21.0 → 1.0.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/README.md +1287 -860
- package/package.json +21 -4
- package/src/admin/AdminShell.tsx +2 -1
- package/src/admin/AdminUsersPanel.tsx +93 -37
- package/src/admin/CompanySettingsForm.tsx +35 -14
- package/src/admin/EmailStatsPanel.tsx +76 -0
- package/src/admin/LegalPagesEditor.tsx +45 -17
- package/src/admin/LocaleTabs.tsx +4 -3
- package/src/admin/StatHistoryChart.tsx +4 -3
- package/src/admin/adminUsersPanelStrings.ts +70 -3
- package/src/admin/companySettingsFormStrings.ts +26 -0
- package/src/admin/emailStatsPanelStrings.ts +29 -0
- package/src/admin/index.ts +14 -5
- package/src/admin/legalPagesEditorStrings.ts +26 -1
- package/src/admin/statHistoryChartStrings.ts +3 -2
- package/src/api/apiErrorCode.ts +28 -0
- package/src/api/apiFieldErrors.ts +84 -13
- package/src/api/company.ts +4 -3
- package/src/api/createApiClient.ts +3 -2
- package/src/api/failureBody.ts +10 -0
- package/src/api/fieldErrorText.ts +32 -0
- package/src/{errors.ts → api/formatApiError.ts} +12 -12
- package/src/api/index.ts +7 -2
- package/src/api/legalPage.ts +10 -0
- package/src/api/request.ts +14 -4
- package/src/api/revalidateTargets.ts +7 -5
- package/src/api/services.ts +53 -22
- package/src/auth/createAuthOptions.ts +16 -8
- package/src/branding/BrandingManager.tsx +37 -37
- package/src/branding/BrandingProvider.tsx +5 -3
- package/src/branding/brandingManagerStrings.ts +49 -0
- package/src/branding/index.ts +4 -5
- package/src/branding/server.ts +10 -9
- package/src/branding/types.ts +3 -3
- package/src/config.ts +10 -10
- package/src/forms/useFormDraft.ts +9 -7
- package/src/index.ts +1 -3
- package/src/legal/LegalPageView.tsx +5 -19
- package/src/legal/index.ts +4 -1
- package/src/legal/keys.ts +10 -0
- package/src/legal/legalPageViewStrings.ts +17 -0
- package/src/locales/nb.ts +330 -0
- package/src/markdown/Markdown.tsx +12 -10
- package/src/markdown/index.ts +1 -1
- package/src/next-auth.d.ts +11 -9
- package/src/next-config/headers.ts +96 -0
- package/src/next-config/index.ts +18 -107
- package/src/payments/PaymentReturn.tsx +9 -7
- package/src/payments/formatNok.ts +1 -1
- package/src/payments/usePollUntilFinal.ts +7 -5
- package/src/sections/SectionRenderer.tsx +275 -0
- package/src/sections/SectionsEditor.tsx +151 -0
- package/src/sections/SmartLink.tsx +42 -0
- package/src/sections/editors.tsx +311 -0
- package/src/sections/factory.ts +26 -0
- package/src/sections/index.ts +69 -0
- package/src/sections/sectionsEditorStrings.ts +108 -0
- package/src/sections/types.ts +107 -0
- package/src/seo/JsonLd.tsx +1 -1
- package/src/seo/environment.ts +3 -3
- package/src/seo/index.ts +1 -1
- package/src/seo/og/index.tsx +5 -4
- package/src/seo/schema.ts +6 -2
- package/src/seo/site.ts +49 -37
- package/src/seo/sitemap.ts +2 -2
- package/src/server/publicApi.ts +13 -9
- package/src/server/revalidateRoute.ts +10 -8
- package/src/session/expiry.ts +11 -6
- package/src/session/index.ts +0 -1
- package/src/session/react/AppSessionProvider.tsx +3 -2
- package/src/{ui → session/react}/CredentialsForm.tsx +17 -10
- package/src/{ui → session/react}/LoginPage.tsx +7 -6
- package/src/session/react/ProtectedGate.tsx +3 -2
- package/src/session/react/RoleGate.tsx +4 -3
- package/src/session/react/SessionExpiryGuard.tsx +5 -4
- package/src/{ui → session/react}/credentialsFormStrings.ts +4 -1
- package/src/session/react/index.ts +8 -2
- package/src/{ui → session/react}/loginPageStrings.ts +1 -1
- package/src/session/react/sessionExpiryGuardStrings.ts +1 -1
- package/src/session/react/useSessionGate.ts +3 -3
- package/src/styles.css +2 -0
- package/src/testing/index.ts +5 -13
- package/src/ui/Alert.tsx +6 -4
- package/src/ui/ContentImage.tsx +12 -11
- package/src/ui/ErrorState.tsx +5 -17
- package/src/ui/NotFoundState.tsx +7 -17
- package/src/ui/PasswordInput.tsx +6 -5
- package/src/ui/ResetPasswordFlow.tsx +64 -19
- package/src/ui/TextArea.tsx +6 -4
- package/src/ui/TextInput.tsx +4 -2
- package/src/ui/Toggle.tsx +3 -3
- package/src/ui/errorStateStrings.ts +16 -0
- package/src/ui/field.tsx +5 -3
- package/src/ui/index.ts +10 -14
- package/src/ui/notFoundStateStrings.ts +14 -0
- package/src/ui/resetPasswordFlowStrings.ts +51 -7
- package/src/validation/fieldErrors.ts +4 -3
- package/src/validation/password.ts +5 -5
- package/src/validation/passwordRules.ts +2 -2
package/src/ui/Toggle.tsx
CHANGED
|
@@ -5,14 +5,14 @@ import { cx } from "./cx";
|
|
|
5
5
|
|
|
6
6
|
type ToggleBase = Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onChange" | "role" | "type" | "aria-checked"> & {
|
|
7
7
|
checked: boolean;
|
|
8
|
-
/**
|
|
8
|
+
/** A function called with the new state when the switch is flipped. */
|
|
9
9
|
onChange: (checked: boolean) => void;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
/** A switch needs a name
|
|
12
|
+
/** Props for `Toggle`. A switch needs a name: either a visible `label`, or an `aria-label` when there is none. */
|
|
13
13
|
export type ToggleProps = ToggleBase & ({ label: string } | { label?: undefined; "aria-label": string });
|
|
14
14
|
|
|
15
|
-
/**
|
|
15
|
+
/** Renders an on and off switch, for a setting that takes effect at once. */
|
|
16
16
|
export function Toggle({ checked, onChange, label, className, disabled, onClick, ...props }: ToggleProps) {
|
|
17
17
|
const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
|
|
18
18
|
onClick?.(event);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Plain values, with no "use client", so a server component can import them.
|
|
2
|
+
|
|
3
|
+
/** The error page's text. Pass the app's own language. The defaults are English. */
|
|
4
|
+
export interface ErrorStateStrings {
|
|
5
|
+
title: string;
|
|
6
|
+
body: string;
|
|
7
|
+
tryAgain: string;
|
|
8
|
+
home: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const defaultErrorStateStrings: ErrorStateStrings = {
|
|
12
|
+
title: "Temporarily unavailable",
|
|
13
|
+
body: "We can't reach the service right now. Please try again shortly.",
|
|
14
|
+
tryAgain: "Try again",
|
|
15
|
+
home: "Go to the front page",
|
|
16
|
+
};
|
package/src/ui/field.tsx
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
import { useId, type AriaAttributes, type ReactNode } from "react";
|
|
4
4
|
import { cx } from "./cx";
|
|
5
5
|
|
|
6
|
-
/** The props of a field control that useField reads. An `<input>` and a `<textarea>` both have them. */
|
|
6
|
+
/** The props of a field control that `useField` reads. An `<input>` and a `<textarea>` both have them. */
|
|
7
7
|
export type FieldControlProps = Pick<AriaAttributes, "aria-required" | "aria-invalid" | "aria-describedby"> & {
|
|
8
8
|
id?: string | undefined;
|
|
9
9
|
required?: boolean | undefined;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
/**
|
|
12
|
+
/** Builds the ids and aria attributes a labeled input needs, from the caller's own props. */
|
|
13
13
|
export function useField(props: FieldControlProps, error: string | undefined) {
|
|
14
14
|
// A generated id ties the label to its own control, even when two fields on a page share a name.
|
|
15
15
|
const generated = useId();
|
|
@@ -29,7 +29,7 @@ export function useField(props: FieldControlProps, error: string | undefined) {
|
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/**
|
|
32
|
+
/** Builds the input's classes, with the error border when there is an error. */
|
|
33
33
|
export function inputClass(error: string | undefined, extra?: string): string {
|
|
34
34
|
return cx(
|
|
35
35
|
"w-full rounded-input border px-3 py-input-y text-sm bg-input-bg text-input-text placeholder-input-placeholder focus:outline-hidden focus:ring-(length:--input-focus-ring-width) focus:ring-input-focus-ring",
|
|
@@ -38,6 +38,7 @@ export function inputClass(error: string | undefined, extra?: string): string {
|
|
|
38
38
|
);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/** Renders a field's label, with a star when the field is required. */
|
|
41
42
|
export function FieldLabel({ htmlFor, required, children }: { htmlFor: string; required: boolean | undefined; children: ReactNode }) {
|
|
42
43
|
return (
|
|
43
44
|
<label htmlFor={htmlFor} className="block text-field-label font-medium text-field-label-text mb-field-label">
|
|
@@ -47,6 +48,7 @@ export function FieldLabel({ htmlFor, required, children }: { htmlFor: string; r
|
|
|
47
48
|
);
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
/** Renders a field's error message, or nothing when there is none. */
|
|
50
52
|
export function FieldError({ id, error }: { id: string; error: string | undefined }) {
|
|
51
53
|
if (!error) return null;
|
|
52
54
|
return (
|
package/src/ui/index.ts
CHANGED
|
@@ -1,25 +1,21 @@
|
|
|
1
1
|
export { Alert } from "./Alert";
|
|
2
2
|
export type { AlertProps } from "./Alert";
|
|
3
|
-
export { ContentImage,
|
|
3
|
+
export { ContentImage, contentImagePath, contentImageSrcSet } from "./ContentImage";
|
|
4
4
|
export type { ContentImageProps } from "./ContentImage";
|
|
5
|
-
export {
|
|
6
|
-
export type {
|
|
7
|
-
export {
|
|
8
|
-
export type {
|
|
9
|
-
export {
|
|
10
|
-
export type {
|
|
11
|
-
export {
|
|
12
|
-
export type {
|
|
13
|
-
export { defaultLoginPageStrings } from "./loginPageStrings";
|
|
14
|
-
export type { LoginPageStrings } from "./loginPageStrings";
|
|
15
|
-
export { NotFoundState, defaultNotFoundStrings } from "./NotFoundState";
|
|
16
|
-
export type { NotFoundStateProps, NotFoundStrings } from "./NotFoundState";
|
|
5
|
+
export { ErrorState } from "./ErrorState";
|
|
6
|
+
export type { ErrorStateProps } from "./ErrorState";
|
|
7
|
+
export { defaultErrorStateStrings } from "./errorStateStrings";
|
|
8
|
+
export type { ErrorStateStrings } from "./errorStateStrings";
|
|
9
|
+
export { NotFoundState } from "./NotFoundState";
|
|
10
|
+
export type { NotFoundStateProps } from "./NotFoundState";
|
|
11
|
+
export { defaultNotFoundStateStrings } from "./notFoundStateStrings";
|
|
12
|
+
export type { NotFoundStateStrings } from "./notFoundStateStrings";
|
|
17
13
|
export { PasswordInput } from "./PasswordInput";
|
|
18
14
|
export type { PasswordInputProps } from "./PasswordInput";
|
|
19
15
|
export { ResetPasswordFlow } from "./ResetPasswordFlow";
|
|
20
16
|
export type { ResetPasswordFlowProps } from "./ResetPasswordFlow";
|
|
21
17
|
export { defaultResetPasswordFlowStrings } from "./resetPasswordFlowStrings";
|
|
22
|
-
export type { ResetPasswordFlowStrings } from "./resetPasswordFlowStrings";
|
|
18
|
+
export type { ResetPasswordErrorStrings, ResetPasswordFlowStrings, ResetPasswordValidationStrings } from "./resetPasswordFlowStrings";
|
|
23
19
|
export { TextArea } from "./TextArea";
|
|
24
20
|
export type { TextAreaProps } from "./TextArea";
|
|
25
21
|
export { TextInput } from "./TextInput";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Plain values, with no "use client", so a server component can import them.
|
|
2
|
+
|
|
3
|
+
/** The not-found page's text. Pass the app's own language. The defaults are English. */
|
|
4
|
+
export interface NotFoundStateStrings {
|
|
5
|
+
code: string;
|
|
6
|
+
body: string;
|
|
7
|
+
home: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const defaultNotFoundStateStrings: NotFoundStateStrings = {
|
|
11
|
+
code: "404",
|
|
12
|
+
body: "This page does not exist.",
|
|
13
|
+
home: "Go home",
|
|
14
|
+
};
|
|
@@ -1,34 +1,65 @@
|
|
|
1
1
|
// Plain values, with no "use client", so a server component can import them.
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* The flow's text for each field error code of SjolystInnovation.AppKit.Auth's reset endpoints,
|
|
5
|
+
* shown under the email, code or new password field. `{max}` in `passwordTooShort` becomes the
|
|
6
|
+
* API's least number of characters. The flow shows the API's own message for a code that is not
|
|
7
|
+
* here, such as `weakPassword` from a password rule the app adds.
|
|
8
|
+
*/
|
|
9
|
+
export interface ResetPasswordValidationStrings {
|
|
10
|
+
required: string;
|
|
11
|
+
invalidEmail: string;
|
|
12
|
+
passwordTooShort: string;
|
|
13
|
+
passwordRequiresDigit: string;
|
|
14
|
+
passwordRequiresLower: string;
|
|
15
|
+
passwordRequiresUpper: string;
|
|
16
|
+
passwordRequiresNonAlphanumeric: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The flow's text for each code of SjolystInnovation.AppKit.Auth's reset endpoints that is not
|
|
21
|
+
* about one field. The flow shows the API's own message for a code that is not here.
|
|
22
|
+
*/
|
|
23
|
+
export interface ResetPasswordErrorStrings {
|
|
24
|
+
/** The code is wrong or expired, or the email has no active account. */
|
|
25
|
+
invalidResetCode: string;
|
|
26
|
+
/** Five wrong codes in a row. The user has to ask for a new code. */
|
|
27
|
+
tooManyResetAttempts: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
3
30
|
/**
|
|
4
31
|
* The reset flow's text. Pass the app's own language. The defaults are English. A text that is null
|
|
5
32
|
* is left out of the page.
|
|
6
33
|
*/
|
|
7
34
|
export interface ResetPasswordFlowStrings {
|
|
8
35
|
title: string;
|
|
9
|
-
/**
|
|
36
|
+
/** The text shown before the sign-in link under the title, such as "Remembered it?". */
|
|
10
37
|
signInPrompt: string | null;
|
|
11
38
|
signIn: string;
|
|
12
|
-
/** The link back to sign-in once the password is set. */
|
|
39
|
+
/** The text of the link back to sign-in once the password is set. */
|
|
13
40
|
backToSignIn: string;
|
|
14
|
-
/**
|
|
41
|
+
/** The text shown above the email field on the first step. */
|
|
15
42
|
intro: string | null;
|
|
16
|
-
/**
|
|
43
|
+
/** The text shown at the top of the second step, once a code is on its way. */
|
|
17
44
|
codeSent: string | null;
|
|
18
45
|
email: string;
|
|
19
46
|
code: string;
|
|
20
47
|
newPassword: string;
|
|
21
|
-
/**
|
|
48
|
+
/** The text shown under the new password field. */
|
|
22
49
|
passwordHint: string | null;
|
|
23
50
|
requestCode: string;
|
|
24
51
|
requestingCode: string;
|
|
25
52
|
setPassword: string;
|
|
26
53
|
settingPassword: string;
|
|
27
|
-
/**
|
|
54
|
+
/** The text shown when the password is set. Null shows the API's own message, or only the link when the API sends none. */
|
|
28
55
|
done: string | null;
|
|
29
|
-
/**
|
|
56
|
+
/** The text shown for a password the rules refuse. Null lists each rule it breaks. */
|
|
30
57
|
passwordInvalid: string | null;
|
|
31
58
|
somethingWentWrong: string;
|
|
59
|
+
/** The field error texts, by code. Any code left out keeps its default. */
|
|
60
|
+
validation: ResetPasswordValidationStrings;
|
|
61
|
+
/** The texts for the other refusals, by code. Any code left out keeps its default. */
|
|
62
|
+
errors: ResetPasswordErrorStrings;
|
|
32
63
|
}
|
|
33
64
|
|
|
34
65
|
export const defaultResetPasswordFlowStrings: ResetPasswordFlowStrings = {
|
|
@@ -49,4 +80,17 @@ export const defaultResetPasswordFlowStrings: ResetPasswordFlowStrings = {
|
|
|
49
80
|
done: null,
|
|
50
81
|
passwordInvalid: null,
|
|
51
82
|
somethingWentWrong: "Something went wrong",
|
|
83
|
+
validation: {
|
|
84
|
+
required: "Required.",
|
|
85
|
+
invalidEmail: "Not a valid email address.",
|
|
86
|
+
passwordTooShort: "Passwords must be at least {max} characters.",
|
|
87
|
+
passwordRequiresDigit: "Passwords must have at least one digit ('0'-'9').",
|
|
88
|
+
passwordRequiresLower: "Passwords must have at least one lowercase ('a'-'z').",
|
|
89
|
+
passwordRequiresUpper: "Passwords must have at least one uppercase ('A'-'Z').",
|
|
90
|
+
passwordRequiresNonAlphanumeric: "Passwords must have at least one non alphanumeric character.",
|
|
91
|
+
},
|
|
92
|
+
errors: {
|
|
93
|
+
invalidResetCode: "Invalid or expired reset code.",
|
|
94
|
+
tooManyResetAttempts: "Too many attempts. Request a new reset code.",
|
|
95
|
+
},
|
|
52
96
|
};
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { z } from "zod";
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/** A function that picks the text shown for an issue, such as a translated message by the issue's code. */
|
|
4
4
|
export type IssueMessage = (issue: z.core.$ZodIssue) => string;
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
* default each field gets its first issue's message. With `all: true` it gets every
|
|
7
|
+
* Groups a failed parse's issues by the top-level field they belong to, for showing beside each
|
|
8
|
+
* input. By default each field gets its first issue's message. With `all: true` it gets every
|
|
9
|
+
* message, in order.
|
|
9
10
|
* `message` replaces zod's text, such as with the app's own translations. Issues without a field,
|
|
10
11
|
* from a refinement of the whole object, are left out.
|
|
11
12
|
*/
|
|
@@ -6,11 +6,11 @@ export { PASSWORD_MIN_LENGTH, defaultPasswordStrings, passwordProblems } from ".
|
|
|
6
6
|
export type { PasswordStrings } from "./passwordRules";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
* counts only ASCII letters and digits, so "Å" is neither an uppercase nor a lowercase
|
|
11
|
-
* Length counts UTF-16 code units, as Identity does. There is no upper limit, because
|
|
12
|
-
* none. An app whose API sets one adds it with `.max()`. A failing password gets one
|
|
13
|
-
* it breaks.
|
|
9
|
+
* Builds a zod schema for a new password, with the rules the APIs enforce through ASP.NET Identity.
|
|
10
|
+
* Identity counts only ASCII letters and digits, so "Å" is neither an uppercase nor a lowercase
|
|
11
|
+
* letter here. Length counts UTF-16 code units, as Identity does. There is no upper limit, because
|
|
12
|
+
* Identity has none. An app whose API sets one adds it with `.max()`. A failing password gets one
|
|
13
|
+
* issue per rule it breaks.
|
|
14
14
|
*/
|
|
15
15
|
export function passwordSchema(strings?: TextOverrides<PasswordStrings>) {
|
|
16
16
|
const text = withDefaults(defaultPasswordStrings, strings);
|
|
@@ -21,8 +21,8 @@ export const defaultPasswordStrings: PasswordStrings = {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
* An empty list means the APIs accept it.
|
|
24
|
+
* Lists the message of every password rule the password breaks, in the order `passwordSchema`
|
|
25
|
+
* checks them. An empty list means the APIs accept it.
|
|
26
26
|
*/
|
|
27
27
|
export function passwordProblems(password: string, strings?: TextOverrides<PasswordStrings>): string[] {
|
|
28
28
|
const text = withDefaults(defaultPasswordStrings, strings);
|