From 728628e4450d2402a28c81ef0b68b0ada7da30a3 Mon Sep 17 00:00:00 2001 From: roshni73 Date: Fri, 18 Oct 2024 16:50:39 +0545 Subject: [PATCH] Add login page - Add Captcha for login page - Fix Hacptcha sitekey --- env.ts | 4 +- package.json | 2 + src/App/routes/index.tsx | 14 +++ src/components/Captcha/index.tsx | 85 ++++++++++++++++ src/components/Navbar/index.tsx | 8 +- src/config.ts | 2 + src/views/Login/i18n.json | 22 +++++ src/views/Login/index.tsx | 155 ++++++++++++++++++++++++++++++ src/views/Login/styles.module.css | 39 ++++++++ vite.config.ts | 3 +- yarn.lock | 28 ++++++ 11 files changed, 354 insertions(+), 8 deletions(-) create mode 100644 src/components/Captcha/index.tsx create mode 100644 src/views/Login/i18n.json create mode 100644 src/views/Login/index.tsx create mode 100644 src/views/Login/styles.module.css diff --git a/env.ts b/env.ts index 73576303..9d6baff0 100644 --- a/env.ts +++ b/env.ts @@ -1,9 +1,9 @@ import { defineConfig, Schema } from '@julr/vite-plugin-validate-env'; -// TODO: Integrate .env for CI and remove optional() call on required fields export default defineConfig({ APP_TITLE: Schema.string.optional(), APP_MAPBOX_ACCESS_TOKEN: Schema.string(), APP_GRAPHQL_ENDPOINT: Schema.string.optional(), APP_GOOGLE_ANALYTICS_ID: Schema.string.optional(), -}) + APP_HCAPTCHA_SITEKEY: Schema.string.optional(), +}); \ No newline at end of file diff --git a/package.json b/package.json index 0e56e1a0..6b0f0406 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@apollo/client": "^3.9.9", "@graphql-codegen/introspection": "^4.0.3", "@graphql-codegen/typescript-operations": "^4.2.0", + "@hcaptcha/react-hcaptcha": "^1.11.0", "@ifrc-go/icons": "^1.3.3", "@ifrc-go/ui": "^1.1.2", "@mapbox/mapbox-gl-draw": "^1.4.3", @@ -29,6 +30,7 @@ "@sentry/react": "^7.81.1", "@togglecorp/fujs": "^2.1.1", "@togglecorp/re-map": "^0.2.0-beta-6", + "@togglecorp/toggle-form": "^2.0.4", "@turf/bbox": "^6.5.0", "@turf/circle": "^6.5.0", "graphql": "^16.8.1", diff --git a/src/App/routes/index.tsx b/src/App/routes/index.tsx index c6c6b483..de89600d 100644 --- a/src/App/routes/index.tsx +++ b/src/App/routes/index.tsx @@ -178,6 +178,19 @@ const pageNotFound = customWrapRoute({ }, }); +const login = customWrapRoute({ + parent: rootLayout, + path: 'login', + component: { + render: () => import('#views/Login'), + props: {}, + }, + context: { + title: 'Login', + visibility: 'is-not-authenticated', + }, +}); + const wrappedRoutes = { rootLayout, homeLayout, @@ -190,6 +203,7 @@ const wrappedRoutes = { allSourcesFeeds, about, pageNotFound, + login, }; export const unwrappedRoutes = unwrapRoute(Object.values(wrappedRoutes)); diff --git a/src/components/Captcha/index.tsx b/src/components/Captcha/index.tsx new file mode 100644 index 00000000..f895d056 --- /dev/null +++ b/src/components/Captcha/index.tsx @@ -0,0 +1,85 @@ +import React, { useCallback } from 'react'; +import HCaptcha from '@hcaptcha/react-hcaptcha'; +import { + InputContainer, + InputContainerProps, +} from '@ifrc-go/ui'; + +import { hCaptchaKey } from '#config'; + +export type HCaptchaProps = Omit & { + name: T, + onChange: (value: string | undefined, name: T) => void; + elementRef?: React.RefObject; +}; + +function HCaptchaInput(props: HCaptchaProps) { + const { + actions, + actionsContainerClassName, + className, + disabled, + error, + errorContainerClassName, + hint, + hintContainerClassName, + icons, + iconsContainerClassName, + inputSectionClassName, + label, + readOnly, + name, + onChange, + elementRef, + } = props; + + const handleVerify = useCallback( + (token: string) => { + onChange(token, name); + }, + [onChange, name], + ); + const handleError = useCallback( + (err: string) => { + // eslint-disable-next-line no-console + console.error(err); + onChange(undefined, name); + }, + [onChange, name], + ); + const handleExpire = useCallback( + () => { + onChange(undefined, name); + }, + [onChange, name], + ); + + return ( + + )} + /> + ); +} + +export default HCaptchaInput; diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx index e095a6af..f84881db 100644 --- a/src/components/Navbar/index.tsx +++ b/src/components/Navbar/index.tsx @@ -1,5 +1,4 @@ import { - Button, Heading, NavigationTabList, PageContainer, @@ -60,13 +59,12 @@ function Navbar(props: Props) { > {strings.appResources} - + ; +type FormSchema = ObjectSchema; +type FormSchemaFields = ReturnType; + +const defaultFormValue: PartialFormFields = {}; + +const formSchema: FormSchema = { + fields: (): FormSchemaFields => ({ + email: { + required: true, + requiredValidation: requiredStringCondition, + }, + password: { + required: true, + requiredValidation: requiredStringCondition, + }, + captcha: { + required: true, + requiredValidation: requiredStringCondition, + }, + }), +}; + +// eslint-disable-next-line import/prefer-default-export +export function Component() { + const strings = useTranslation(i18n); + const { + value: formValue, + setFieldValue, + setError, + validate, + } = useForm(formSchema, { value: defaultFormValue }); + + const fieldError: PartialFormFields = {}; + + const handleFormSubmit = useMemo( + () => createSubmitHandler( + validate, + setError, + // FIXME: Add form submission logic here + () => {}, + ), + [validate, setError], + ); + + const signupInfo = resolveToComponent( + strings.loginDontHaveAccount, + { + signUpLink: ( + + {strings.loginSignUp} + + ), + }, + ); + + return ( + +
+
+ + +
+
+ + {strings.loginForgotUserPass} + + + {strings.loginResendValidation} + +
+
+ + +
+ {signupInfo} +
+
+
+
+ ); +} + +Component.displayName = 'Login'; diff --git a/src/views/Login/styles.module.css b/src/views/Login/styles.module.css new file mode 100644 index 00000000..f13ba8b8 --- /dev/null +++ b/src/views/Login/styles.module.css @@ -0,0 +1,39 @@ +.login { + .main-section { + display: flex; + justify-content: center; + + .form { + display: flex; + flex-direction: column; + flex-grow: 1; + gap: var(--go-ui-spacing-xl); + max-width: var(--go-ui-width-content-max); + + .fields { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-xl); + } + + .utility-links { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-sm); + align-items: flex-end; + } + + .actions { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-lg); + align-items: center; + + .sign-up { + display: flex; + gap: var(--go-ui-spacing-sm); + } + } + } + } +} diff --git a/vite.config.ts b/vite.config.ts index 97d5b483..45403fd5 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -9,6 +9,7 @@ import { ValidateEnv as validateEnv } from '@julr/vite-plugin-validate-env'; import { VitePluginRadar } from 'vite-plugin-radar'; import alertHubPackage from './package.json'; +import envConfig from './env'; /* Get commit hash */ const commitHash = execSync('git rev-parse --short HEAD').toString(); @@ -35,7 +36,7 @@ export default defineConfig(({ mode }) => { reactSwc(), tsconfigPaths(), webfontDownload(), - validateEnv(), + validateEnv(envConfig), isProd ? compression() : undefined, VitePluginRadar({ analytics: { diff --git a/yarn.lock b/yarn.lock index 5ca4e2a2..44d5ccf0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -502,6 +502,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.17.9": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" + integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.22.15", "@babel/template@^7.24.0": version "7.24.0" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.0.tgz#c6a524aa93a4a05d66aaf31654258fae69d87d50" @@ -1655,6 +1662,19 @@ resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861" integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== +"@hcaptcha/loader@^1.2.1": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@hcaptcha/loader/-/loader-1.2.4.tgz#541714395a82e27ec0f0e8bd80ef1a0bea141cc3" + integrity sha512-3MNrIy/nWBfyVVvMPBKdKrX7BeadgiimW0AL/a/8TohNtJqxoySKgTJEXOQvYwlHemQpUzFrIsK74ody7JiMYw== + +"@hcaptcha/react-hcaptcha@^1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@hcaptcha/react-hcaptcha/-/react-hcaptcha-1.11.0.tgz#d4520dd3b7ece735bc928dedcda048be477b2dcd" + integrity sha512-UKHtzzVMHLTGwab5pgV96UbcXdyh5Qyq8E0G5DTyXq8txMvuDx7rSyC+BneOjWVW0a7O9VuZmkg/EznVLRE45g== + dependencies: + "@babel/runtime" "^7.17.9" + "@hcaptcha/loader" "^1.2.1" + "@humanwhocodes/config-array@^0.11.14": version "0.11.14" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" @@ -2424,6 +2444,14 @@ "@babel/runtime-corejs3" "^7.22.3" "@togglecorp/fujs" "^2.1.0" +"@togglecorp/toggle-form@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@togglecorp/toggle-form/-/toggle-form-2.0.4.tgz#2098ae24d6a37020d19a60162fff457e7a9eedf3" + integrity sha512-+EzRzXK/PKlisu44yARpxOkoeowz+0oKk2Rl3CdhxtBfTVfzG28aHAklDTubTBssS8hneGBTav2aInCqmwChfg== + dependencies: + "@babel/runtime-corejs3" "^7.22.3" + "@togglecorp/fujs" "^2.1.1" + "@tsconfig/node10@^1.0.7": version "1.0.11" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2"