diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/keycloak-theme/KcApp.tsx | 2 | ||||
| -rw-r--r-- | src/keycloak-theme/pages/RegisterUserProfile.tsx | 61 | ||||
| -rw-r--r-- | src/keycloak-theme/pages/shared/UserProfileCommons.tsx | 176 |
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> + ); + })} + </> + ); +} + |