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

18 Edit a user #21

Merged
merged 25 commits into from
Jul 21, 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
52 changes: 52 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@types/react-router-dom": "5.3.3",
"axios": "1.7.2",
"classnames": "2.5.1",
"formik": "2.4.6",
"ionicons": "7.4.0",
"lodash": "4.17.21",
"react": "18.3.1",
Expand Down
2 changes: 0 additions & 2 deletions src/common/components/Block/Block.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@
.block-title {
font-size: 1.25rem;
font-weight: 500;
line-height: 1.75rem;
margin-bottom: 0.5rem;
}
.block-content {
color: var(--ion-color-medium);
line-height: 1.5rem;
margin-bottom: 0.5rem;
}
}
35 changes: 35 additions & 0 deletions src/common/components/Content/Container.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
.container {
width: 100%;

margin-left: auto;
margin-right: auto;

// xs
@media (max-width: 575px) {
}

// sm
@media (min-width: 576px) {
}

// md
@media (min-width: 768px) {
&.fixed {
width: 720px;
}
}

// lg
@media (min-width: 992px) {
&.fixed {
width: 960px;
}
}

// xl
@media (min-width: 1200px) {
&.fixed {
width: 1140px;
}
}
}
44 changes: 44 additions & 0 deletions src/common/components/Content/Container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { PropsWithChildren } from 'react';
import classNames from 'classnames';

import { BaseComponentProps } from '../types';
import classes from './Container.module.scss';

/**
* Properties for the `Container` component.
* @param {boolean} [fixed] - Indicates if a `fixed` sized container is used.
* @see {@link BaseComponentProps}
* @see {@link PropsWithChildren}
*/
interface ContainerProps extends BaseComponentProps, PropsWithChildren {
fixed?: boolean;
}

/**
* The `Container` component controls the width of the enclosed content.
*
* A standard `Container` is 100% wide.
*
* A `fixed` `Container` is 100% wide at extra-small and small viewports. It
* has a fixed width and is centered at medium, large, and extra-large viewports.
*
* @param {ContainerProps} props - Component properties.
* @returns {JSX.Element} Returns JSX.
*/
const Container = ({
children,
className,
fixed = false,
testid = 'container',
}: ContainerProps): JSX.Element => {
return (
<div
className={classNames(classes.container, { [classes.fixed]: fixed }, className)}
data-testid={testid}
>
{children}
</div>
);
};

export default Container;
14 changes: 14 additions & 0 deletions src/common/components/Content/PageHeader.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.page-header {
border-bottom-color: var(--ion-color-light);
border-bottom-style: solid;
border-bottom-width: 1px;

padding: 0.25rem 0.5rem;

align-items: center;
justify-content: space-between;

.title {
font-size: 2rem;
}
}
59 changes: 59 additions & 0 deletions src/common/components/Content/PageHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { ReactNode } from 'react';
import { IonButtons, IonRow } from '@ionic/react';
import classNames from 'classnames';

import { BaseComponentProps } from '../types';
import classes from './PageHeader.module.scss';

/**
* Properties for the `PageHeader` component.
* @param {ReactNode} title - A title.
* @param {ReactNode} [buttons] - One or more buttons.
* @see {@link BaseComponentProps}
*/
interface PageHeaderProps extends BaseComponentProps {
title: ReactNode;
buttons?: ReactNode;
}

/**
* The `PageHeader` component displays a block intended for the top of a page.
* The block displays the page title and an optional collection of buttons.
*
* When provided, the buttons will be rendered at the far right of the page
* header.
*
* Example:
* ```
* <PageHeader
* title="Users"
* buttons={
* <>
* <IonButton>
* <IonIcon slot="icon-only" icon={add} />
* </IonButton>
* </>
* }
* />
* ```
*
* @param {PageHeaderProps} props - Component properties.
* @returns {JSX.Element} Returns JSX.
*/
const PageHeader = ({
buttons,
className,
testid = 'page-header',
title,
}: PageHeaderProps): JSX.Element => {
return (
<IonRow className={classNames(classes['page-header'], className)} data-testid={testid}>
<div className={classes.title} data-testid={`${testid}-title`}>
{title}
</div>
{buttons && <IonButtons>{buttons}</IonButtons>}
</IonRow>
);
};

export default PageHeader;
7 changes: 6 additions & 1 deletion src/common/components/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ReactNode } from 'react';
import {
IonBackButton,
IonButton,
Expand All @@ -15,16 +16,19 @@ import logo from 'assets/logo_ls.png';
* Properties for the `Header` component.
* @param {boolean} [backButton] - Optional. Indicates the back button
* should be rendered.
* @param {ReactNode} [buttons] - Optional. One or more buttons, specific
* to the page.
* @param {string} [defaultHref] - Optional. The default back navigation
* href if there is no history in the route stack.
* @param {string} [title] - Optional. The header title.
*/
interface HeaderProps extends Pick<HTMLIonBackButtonElement, 'defaultHref'> {
backButton?: boolean;
buttons?: ReactNode;
title?: string;
}

const Header = ({ backButton = false, defaultHref, title }: HeaderProps): JSX.Element => {
const Header = ({ backButton = false, buttons, defaultHref, title }: HeaderProps): JSX.Element => {
const testid = 'header-app';

return (
Expand All @@ -51,6 +55,7 @@ const Header = ({ backButton = false, defaultHref, title }: HeaderProps): JSX.El
className="ion-hide-md-down"
data-testid={`${testid}-button-menu`}
></IonMenuButton>
{buttons}
</IonButtons>
</IonToolbar>
</IonHeader>
Expand Down
45 changes: 45 additions & 0 deletions src/common/components/Input/Input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { InputInputEventDetail, IonInput } from '@ionic/react';
import classNames from 'classnames';

import { BaseComponentProps } from '../types';
import { useField } from 'formik';

/**
* Properties for the `Input` component.
* @see {@link BaseComponentProps}
* @see {@link IonInput}
*/
interface InputProps
extends BaseComponentProps,
Omit<React.ComponentPropsWithoutRef<typeof IonInput>, 'name'>,
Required<Pick<React.ComponentPropsWithoutRef<typeof IonInput>, 'name'>> {}

/**
* The `Input` component renders a standardized `IonInput` which is integrated
* with Formik.
* @param {InputProps} props - Component properties.
* @returns {JSX.Element} JSX
*/
const Input = ({ className, testid = 'input', ...props }: InputProps): JSX.Element => {
const [field, meta, helpers] = useField(props.name);

return (
<IonInput
className={classNames(
className,
{ 'ion-touched': meta.touched },
{ 'ion-invalid': meta.error },
{ 'ion-valid': meta.touched && !meta.error },
)}
onIonInput={async (e: CustomEvent<InputInputEventDetail>) =>
await helpers.setValue(e.detail.value)
}
data-testid={testid}
{...field}
{...props}
errorText={meta.error}
></IonInput>
);
};

export default Input;
44 changes: 44 additions & 0 deletions src/common/components/Input/__tests__/Input.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { describe, expect, it } from 'vitest';
import userEvent from '@testing-library/user-event';
import { Form, Formik } from 'formik';

import { render, screen } from 'test/test-utils';

import Input from '../Input';

describe('Input', () => {
it('should render successfully', async () => {
// ARRANGE
render(
<Formik initialValues={{ testField: '' }} onSubmit={() => {}}>
<Form>
<Input name="testField" />
</Form>
</Formik>,
);
await screen.findByTestId('input');

// ASSERT
expect(screen.getByTestId('input')).toBeDefined();
});

it('should change value when typing', async () => {
// ARRANGE
const value = 'hello';
render(
<Formik initialValues={{ testField: '' }} onSubmit={() => {}}>
<Form>
<Input name="testField" label="Test Field" />
</Form>
</Formik>,
);
await screen.findByTestId('input');

// ACT
await userEvent.type(screen.getByLabelText('Test Field'), value);

// ASSERT
expect(screen.getByTestId('input')).toBeDefined();
expect(screen.getByTestId('input')).toHaveValue(value);
});
});
Loading