Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add login page #155

Merged
merged 1 commit into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions env.ts
Original file line number Diff line number Diff line change
@@ -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(),
});
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@
"@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",
"@placemarkio/geo-viewport": "^1.0.2",
"@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",
Expand Down
14 changes: 14 additions & 0 deletions src/App/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -190,6 +203,7 @@ const wrappedRoutes = {
allSourcesFeeds,
about,
pageNotFound,
login,
};

export const unwrappedRoutes = unwrapRoute(Object.values(wrappedRoutes));
Expand Down
85 changes: 85 additions & 0 deletions src/components/Captcha/index.tsx
Original file line number Diff line number Diff line change
@@ -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<T> = Omit<InputContainerProps, 'input'> & {
name: T,
onChange: (value: string | undefined, name: T) => void;
elementRef?: React.RefObject<HCaptcha>;
};

function HCaptchaInput<T extends string>(props: HCaptchaProps<T>) {
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 (
<InputContainer
actions={actions}
actionsContainerClassName={actionsContainerClassName}
className={className}
disabled={disabled}
error={error}
errorContainerClassName={errorContainerClassName}
hint={hint}
hintContainerClassName={hintContainerClassName}
icons={icons}
iconsContainerClassName={iconsContainerClassName}
inputSectionClassName={inputSectionClassName}
label={label}
readOnly={readOnly}
input={hCaptchaKey && (
<HCaptcha
ref={elementRef}
sitekey={hCaptchaKey}
onVerify={handleVerify}
onError={handleError}
onExpire={handleExpire}
/>
)}
/>
);
}

export default HCaptchaInput;
8 changes: 3 additions & 5 deletions src/components/Navbar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
Button,
Heading,
NavigationTabList,
PageContainer,
Expand Down Expand Up @@ -60,13 +59,12 @@ function Navbar(props: Props) {
>
{strings.appResources}
</NavigationTab>
<Button
name={undefined}
<Link
variant="primary"
onClick={undefined}
to="login"
>
{strings.appLogin}
</Button>
</Link>
</NavigationTabList>
</PageContainer>
<PageContainer
Expand Down
2 changes: 2 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ const {
APP_TITLE,
APP_COMMIT_HASH,
APP_VERSION,
APP_HCAPTCHA_SITEKEY,
} = import.meta.env;

export const environment = APP_ENVIRONMENT;
export const appTitle = APP_TITLE;
export const api = APP_GRAPHQL_API_ENDPOINT;
export const mapboxToken = APP_MAPBOX_ACCESS_TOKEN;
export const hCaptchaKey = APP_HCAPTCHA_SITEKEY;
export const appCommitHash = APP_COMMIT_HASH;
export const appVersion = APP_VERSION;
22 changes: 22 additions & 0 deletions src/views/Login/i18n.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"namespace": "login",
"strings": {
"loginTitle":"IFRC GO - Login",
"loginHeader":"Login",
"loginSubHeader":"If you are staff, member or volunteer of the Red Cross Red Crescent Movement (National Societies, the IFRC and the ICRC) login with you email and password.",
"loginEmailUsername":"Email",
"loginPassword":"Password",
"loginRecoverTitle":"Recover password",
"loginShowUsernameTitle":"Show me my username",
"loginResendValidation":"Re-send validation email",
"loginResendValidationTitle":"I didn't get my validation email",
"loginForgotUserPass":"Forgot your password?",
"loginInvalid":"Invalid username or password",
"loginErrorMessage":"Error: {message}",
"loginButton":"Login",
"loginDontHaveAccount":"Don’t have an account? {signUpLink}",
"loginCreateAccountTitle":"Create new account",
"loginSignUp":"Sign up",
"loginFailureMessage": "Sorry, failed to login!"
}
}
155 changes: 155 additions & 0 deletions src/views/Login/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { useMemo } from 'react';
import {
Button,
PasswordInput,
TextInput,
} from '@ifrc-go/ui';
import { useTranslation } from '@ifrc-go/ui/hooks';
import { resolveToComponent } from '@ifrc-go/ui/utils';
import {
createSubmitHandler,
type ObjectSchema,
requiredStringCondition,
useForm,
} from '@togglecorp/toggle-form';

import HCaptcha from '#components/Captcha';
import Link from '#components/Link';
import Page from '#components/Page';

import i18n from './i18n.json';
import styles from './styles.module.css';

interface FormFields {
email: string;
password: string;
captcha: string;
}
type PartialFormFields = Partial<FormFields>;
type FormSchema = ObjectSchema<PartialFormFields>;
type FormSchemaFields = ReturnType<FormSchema['fields']>;

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: (
<Link
to="login" // FIXME :add Register
withUnderline
>
{strings.loginSignUp}
</Link>
),
},
);

return (
<Page
className={styles.login}
title={strings.loginTitle}
heading={strings.loginHeader}
description={strings.loginSubHeader}
mainSectionClassName={styles.mainSection}
>
<form
className={styles.form}
onSubmit={handleFormSubmit}
>
<div className={styles.fields}>
<TextInput
name="email"
label={strings.loginEmailUsername}
value={formValue.email}
error={fieldError?.email}
onChange={setFieldValue}
withAsterisk
autoFocus
/>
<PasswordInput
name="password"
label={strings.loginPassword}
value={formValue.password}
error={fieldError?.password}
onChange={setFieldValue}
withAsterisk
/>
</div>
<div className={styles.utilityLinks}>
<Link
to="login" // FIXME: ForgotPassword
title={strings.loginRecoverTitle}
withUnderline
>
{strings.loginForgotUserPass}
</Link>
<Link
to="login" // FIXME :LoginResendValidation
title={strings.loginResendValidationTitle}
withUnderline
>
{strings.loginResendValidation}
</Link>
</div>
<div className={styles.actions}>
<HCaptcha
name="captcha"
onChange={setFieldValue}
/>
<Button
name={undefined}
type="submit"
onClick={handleFormSubmit}
>
{strings.loginButton}
</Button>
<div className={styles.signUp}>
{signupInfo}
</div>
</div>
</form>
</Page>
);
}

Component.displayName = 'Login';
39 changes: 39 additions & 0 deletions src/views/Login/styles.module.css
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
}
Loading