aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/keycloak-theme/KcApp.tsx2
-rw-r--r--src/keycloak-theme/pages/RegisterUserProfile.tsx61
-rw-r--r--src/keycloak-theme/pages/shared/UserProfileCommons.tsx176
3 files changed, 239 insertions, 0 deletions
diff --git a/src/keycloak-theme/KcApp.tsx b/src/keycloak-theme/KcApp.tsx
index efe0cf6..e7f8a0c 100644
--- a/src/keycloak-theme/KcApp.tsx
+++ b/src/keycloak-theme/KcApp.tsx
@@ -12,6 +12,7 @@ console.log(`Values passed by the main app in the URL parameter:`, { foo, bar })
const Login = lazy(()=> import("./pages/Login"));
// If you can, favor register-user-profile.ftl over register.ftl, see: https://docs.keycloakify.dev/realtime-input-validation
const Register = lazy(() => import("./pages/Register"));
+const RegisterUserProfile = lazy(() => import("./pages/RegisterUserProfile"));
const Terms = lazy(() => import("./pages/Terms"));
const MyExtraPage1 = lazy(() => import("./pages/MyExtraPage1"));
const MyExtraPage2 = lazy(() => import("./pages/MyExtraPage2"));
@@ -61,6 +62,7 @@ export default function App(props: { kcContext: KcContext; }) {
switch (kcContext.pageId) {
case "login.ftl": return <Login {...{ kcContext, ...pageProps }} />;
case "register.ftl": return <Register {...{ kcContext, ...pageProps }} />;
+ case "register-user-profile.ftl": return <RegisterUserProfile {...{ kcContext, ...pageProps }} />
case "terms.ftl": return <Terms {...{ kcContext, ...pageProps }} />;
case "my-extra-page-1.ftl": return <MyExtraPage1 {...{ kcContext, ...pageProps }} />;
case "my-extra-page-2.ftl": return <MyExtraPage2 {...{ kcContext, ...pageProps }} />;
diff --git a/src/keycloak-theme/pages/RegisterUserProfile.tsx b/src/keycloak-theme/pages/RegisterUserProfile.tsx
new file mode 100644
index 0000000..492e82b
--- /dev/null
+++ b/src/keycloak-theme/pages/RegisterUserProfile.tsx
@@ -0,0 +1,61 @@
+// Copy pasted from: https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/pages/RegisterUserProfile.tsx
+import { useState } from "react";
+import { clsx } from "keycloakify/lib/tools/clsx";
+import { UserProfileFormFields } from "./shared/UserProfileCommons";
+import type { PageProps } from "keycloakify/lib/KcProps";
+import type { KcContext } from "../kcContext";
+import type { I18n } from "../i18n";
+
+export default function RegisterUserProfile(props: PageProps<Extract<KcContext, { pageId: "register-user-profile.ftl" }>, I18n>) {
+ const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
+
+ const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext;
+
+ const { msg, msgStr } = i18n;
+
+ const [isFomSubmittable, setIsFomSubmittable] = useState(false);
+
+ return (
+ <Template
+ {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
+ displayMessage={messagesPerField.exists("global")}
+ displayRequiredFields={true}
+ headerNode={msg("registerTitle")}
+ formNode={
+ <form id="kc-register-form" className={clsx(kcProps.kcFormClass)} action={url.registrationAction} method="post">
+ <UserProfileFormFields kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} i18n={i18n} {...kcProps} />
+ {recaptchaRequired && (
+ <div className="form-group">
+ <div className={clsx(kcProps.kcInputWrapperClass)}>
+ <div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey} />
+ </div>
+ </div>
+ )}
+ <div className={clsx(kcProps.kcFormGroupClass)} style={{ "marginBottom": 30 }}>
+ <div id="kc-form-options" className={clsx(kcProps.kcFormOptionsClass)}>
+ <div className={clsx(kcProps.kcFormOptionsWrapperClass)}>
+ <span>
+ <a href={url.loginUrl}>{msg("backToLogin")}</a>
+ </span>
+ </div>
+ </div>
+
+ <div id="kc-form-buttons" className={clsx(kcProps.kcFormButtonsClass)}>
+ <input
+ className={clsx(
+ kcProps.kcButtonClass,
+ kcProps.kcButtonPrimaryClass,
+ kcProps.kcButtonBlockClass,
+ kcProps.kcButtonLargeClass
+ )}
+ type="submit"
+ value={msgStr("doRegister")}
+ disabled={!isFomSubmittable}
+ />
+ </div>
+ </div>
+ </form>
+ }
+ />
+ );
+}
diff --git a/src/keycloak-theme/pages/shared/UserProfileCommons.tsx b/src/keycloak-theme/pages/shared/UserProfileCommons.tsx
new file mode 100644
index 0000000..08b9442
--- /dev/null
+++ b/src/keycloak-theme/pages/shared/UserProfileCommons.tsx
@@ -0,0 +1,176 @@
+//NOTE: Copy pasted from: https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/pages/shared/UserProfileCommons.tsx
+
+import { useEffect, Fragment } from "react";
+import type { KcProps } from "keycloakify/lib/KcProps";
+import { clsx } from "keycloakify/lib/tools/clsx";
+import type { I18nBase } from "keycloakify/lib/i18n";
+import type { Attribute } from "keycloakify/lib/getKcContext";
+import { useFormValidation } from "keycloakify/lib/pages/shared/UserProfileCommons";
+
+export type UserProfileFormFieldsProps = {
+ kcContext: Parameters<typeof useFormValidation>[0]["kcContext"];
+ i18n: I18nBase;
+} & KcProps &
+ Partial<Record<"BeforeField" | "AfterField", (props: { attribute: Attribute }) => JSX.Element | null>> & {
+ onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void;
+ };
+
+export function UserProfileFormFields({
+ kcContext,
+ onIsFormSubmittableValueChange,
+ i18n,
+ BeforeField,
+ AfterField,
+ ...props
+}: UserProfileFormFieldsProps) {
+ const { advancedMsg } = i18n;
+
+ const {
+ formValidationState: { fieldStateByAttributeName, isFormSubmittable },
+ formValidationDispatch,
+ attributesWithPassword
+ } = useFormValidation({
+ kcContext,
+ i18n
+ });
+
+ useEffect(() => {
+ onIsFormSubmittableValueChange(isFormSubmittable);
+ }, [isFormSubmittable]);
+
+ let currentGroup = "";
+
+ return (
+ <>
+ {attributesWithPassword.map((attribute, i) => {
+ const { group = "", groupDisplayHeader = "", groupDisplayDescription = "" } = attribute;
+
+ const { value, displayableErrors } = fieldStateByAttributeName[attribute.name];
+
+ const formGroupClassName = clsx(props.kcFormGroupClass, displayableErrors.length !== 0 && props.kcFormGroupErrorClass);
+
+ return (
+ <Fragment key={i}>
+ {group !== currentGroup && (currentGroup = group) !== "" && (
+ <div className={formGroupClassName}>
+ <div className={clsx(props.kcContentWrapperClass)}>
+ <label id={`header-${group}`} className={clsx(props.kcFormGroupHeader)}>
+ {advancedMsg(groupDisplayHeader) || currentGroup}
+ </label>
+ </div>
+ {groupDisplayDescription !== "" && (
+ <div className={clsx(props.kcLabelWrapperClass)}>
+ <label id={`description-${group}`} className={`${clsx(props.kcLabelClass)}`}>
+ {advancedMsg(groupDisplayDescription)}
+ </label>
+ </div>
+ )}
+ </div>
+ )}
+
+ {BeforeField && <BeforeField attribute={attribute} />}
+
+ <div className={formGroupClassName}>
+ <div className={clsx(props.kcLabelWrapperClass)}>
+ <label htmlFor={attribute.name} className={clsx(props.kcLabelClass)}>
+ {advancedMsg(attribute.displayName ?? "")}
+ </label>
+ {attribute.required && <>*</>}
+ </div>
+ <div className={clsx(props.kcInputWrapperClass)}>
+ {(() => {
+ const { options } = attribute.validators;
+
+ if (options !== undefined) {
+ return (
+ <select
+ id={attribute.name}
+ name={attribute.name}
+ onChange={event =>
+ formValidationDispatch({
+ "action": "update value",
+ "name": attribute.name,
+ "newValue": event.target.value
+ })
+ }
+ onBlur={() =>
+ formValidationDispatch({
+ "action": "focus lost",
+ "name": attribute.name
+ })
+ }
+ value={value}
+ >
+ {options.options.map(option => (
+ <option key={option} value={option}>
+ {option}
+ </option>
+ ))}
+ </select>
+ );
+ }
+
+ return (
+ <input
+ type={(() => {
+ switch (attribute.name) {
+ case "password-confirm":
+ case "password":
+ return "password";
+ default:
+ return "text";
+ }
+ })()}
+ id={attribute.name}
+ name={attribute.name}
+ value={value}
+ onChange={event =>
+ formValidationDispatch({
+ "action": "update value",
+ "name": attribute.name,
+ "newValue": event.target.value
+ })
+ }
+ onBlur={() =>
+ formValidationDispatch({
+ "action": "focus lost",
+ "name": attribute.name
+ })
+ }
+ className={clsx(props.kcInputClass)}
+ aria-invalid={displayableErrors.length !== 0}
+ disabled={attribute.readOnly}
+ autoComplete={attribute.autocomplete}
+ />
+ );
+ })()}
+ {displayableErrors.length !== 0 &&
+ (() => {
+ const divId = `input-error-${attribute.name}`;
+
+ return (
+ <>
+ <style>{`#${divId} > span: { display: block; }`}</style>
+ <span
+ id={divId}
+ className={clsx(props.kcInputErrorMessageClass)}
+ style={{
+ "position": displayableErrors.length === 1 ? "absolute" : undefined
+ }}
+ aria-live="polite"
+ >
+ {displayableErrors.map(({ errorMessage }) => errorMessage)}
+ </span>
+ </>
+ );
+ })()}
+ </div>
+ </div>
+ {AfterField && <AfterField attribute={attribute} />}
+ </Fragment>
+ );
+ })}
+ </>
+ );
+}
+