Skip to content

Commit

Permalink
#13 Loading state (#15)
Browse files Browse the repository at this point in the history
* loading state

* jsdoc

* tests
  • Loading branch information
mwarman authored Jul 11, 2024
1 parent d4f86c8 commit eb5ceb6
Show file tree
Hide file tree
Showing 8 changed files with 153 additions and 70 deletions.
54 changes: 35 additions & 19 deletions src/pages/Users/components/UserDetail/AddressDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,64 +10,80 @@ import LoaderSkeleton from 'common/components/Loader/LoaderSkeleton';
/**
* Properties for the `AddressDetail` component.
* @param {Address} [address] - An `Address` object.
* @param {boolean} [isLoading] - Indicates if the `user` is being loaded.
* @see {@link BaseComponentProps}
*/
interface AddressDetailProps extends BaseComponentProps {
address?: Address;
isLoading?: boolean;
}

/**
* The `AddressDetail` component renders a block which displays a single
* `Address`.
*
* If the `address` property is null or undefined, a loading state is rendered.
* If `isLoading` is `true` the loading state is rendered.
*
* If `isLoading` is `false` and the `address` property is provided, the
* address attributes are rendered.
*
* If `isLoading` is `false` and the `address` property is empty, the
* component returns `false` so that the component remains in the React
* hierarchy, but does not render anything.
*
* @param {AddressDetailProps} props - Component properties.
* @returns JSX
* @returns {JSX.Element | false} Returns JSX when loading or a user is
* provided, otherwise returns `false`.
*/
const AddressDetail = ({
address,
className,
isLoading = false,
testid = 'address-detail',
}: AddressDetailProps): JSX.Element => {
}: AddressDetailProps): JSX.Element | false => {
const baseProps = {
className: classNames('address-detail', className),
'data-testid': testid,
};

if (address) {
// success state
if (isLoading) {
// loading state
return (
<div {...baseProps}>
<div className="content">
<div className="content" data-testid={`${testid}-loader`}>
<div className="header">
<IonIcon icon={map} />
<div>Address</div>
<LoaderSkeleton animated heightStyle="1.5rem" widthStyle="10rem" />
</div>
<div>{address.street}</div>
<div>{address.suite}</div>
<div>{address.city}</div>
<div>{address.zipcode}</div>
<LoaderSkeleton animated heightStyle="1rem" widthStyle="20rem" />
<LoaderSkeleton animated heightStyle="1rem" widthStyle="20rem" />
<LoaderSkeleton animated heightStyle="1rem" widthStyle="20rem" />
<LoaderSkeleton animated heightStyle="1rem" widthStyle="20rem" />
</div>
</div>
);
} else {
// loading state
}

if (address) {
// success state
return (
<div {...baseProps}>
<div className="content" data-testid={`${testid}-loader`}>
<div className="content">
<div className="header">
<IonIcon icon={map} />
<LoaderSkeleton animated heightStyle="1.5rem" widthStyle="10rem" />
<div>Address</div>
</div>
<LoaderSkeleton animated heightStyle="1rem" widthStyle="20rem" />
<LoaderSkeleton animated heightStyle="1rem" widthStyle="20rem" />
<LoaderSkeleton animated heightStyle="1rem" widthStyle="20rem" />
<LoaderSkeleton animated heightStyle="1rem" widthStyle="20rem" />
<div>{address.street}</div>
<div>{address.suite}</div>
<div>{address.city}</div>
<div>{address.zipcode}</div>
</div>
</div>
);
}

// not loading and no user
return false;
};

export default AddressDetail;
50 changes: 33 additions & 17 deletions src/pages/Users/components/UserDetail/CompanyDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,62 +10,78 @@ import LoaderSkeleton from 'common/components/Loader/LoaderSkeleton';
/**
* Properties for the `CompanyDetail` component.
* @param {Company} [company] - A `Company` object.
* @param {boolean} [isLoading] - Indicates if the `user` is being loaded.
* @see {@link BaseComponentProps}
*/
interface CompanyDetailProps extends BaseComponentProps {
company?: Company;
isLoading?: boolean;
}

/**
* The `CompanyDetail` component renders a block which provides details about
* a single `Company`.
*
* If the `company` property is null or undefined, a loading state is rendered.
* If `isLoading` is `true` the loading state is rendered.
*
* If `isLoading` is `false` and the `company` property is provided, the
* company attributes are rendered.
*
* If `isLoading` is `false` and the `company` property is empty, the
* component returns `false` so that the component remains in the React
* hierarchy, but does not render anything.
*
* @param {CompanyDetailProps} props - Component properties.
* @returns JSX
* @returns {JSX.Element | false} Returns JSX when loading or a user is
* provided, otherwise returns `false`.
*/
const CompanyDetail = ({
className,
company,
isLoading = false,
testid = 'company-detail',
}: CompanyDetailProps): JSX.Element => {
}: CompanyDetailProps): JSX.Element | false => {
const baseProps = {
className: classNames('company-detail', className),
'data-testid': testid,
};

if (company) {
// success state
if (isLoading) {
// loading state
return (
<div {...baseProps}>
<div className="content">
<div className="content" data-testid={`${testid}-loader`}>
<div className="header">
<IonIcon icon={business} />
<div>Company</div>
<LoaderSkeleton animated heightStyle="1.5rem" widthStyle="10rem" />
</div>
<div className="primary">{company.name}</div>
<div>{company.catchPhrase}</div>
<div>{company.bs}</div>
<LoaderSkeleton animated heightStyle="1rem" widthStyle="20rem" />
<LoaderSkeleton animated heightStyle="1rem" widthStyle="20rem" />
<LoaderSkeleton animated heightStyle="1rem" widthStyle="20rem" />
</div>
</div>
);
} else {
// loading state
}

if (company) {
// success state
return (
<div {...baseProps}>
<div className="content" data-testid={`${testid}-loader`}>
<div className="content">
<div className="header">
<IonIcon icon={business} />
<LoaderSkeleton animated heightStyle="1.5rem" widthStyle="10rem" />
<div>Company</div>
</div>
<LoaderSkeleton animated heightStyle="1rem" widthStyle="20rem" />
<LoaderSkeleton animated heightStyle="1rem" widthStyle="20rem" />
<LoaderSkeleton animated heightStyle="1rem" widthStyle="20rem" />
<div className="primary">{company.name}</div>
<div>{company.catchPhrase}</div>
<div>{company.bs}</div>
</div>
</div>
);
}

// not loading and no user
return false;
};

export default CompanyDetail;
1 change: 0 additions & 1 deletion src/pages/Users/components/UserDetail/UserDetail.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
}

ion-grid {
--ion-grid-columns: 2;
--ion-grid-padding: 0;
--ion-grid-column-padding: 0.25rem;

Expand Down
22 changes: 15 additions & 7 deletions src/pages/Users/components/UserDetail/UserDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ interface UserDetailProps extends BaseComponentProps {
* error message is displayed.
*
* @param {UserDetailProps} props - Component properties.
* @returns JSX
* @returns {JSX.Element} JSX
*/
const UserDetail = ({
className,
testid = 'user-detail',
userId,
}: UserDetailProps): JSX.Element => {
const { data: user, isError } = useGetUser({ userId });
const { data: user, isError, isLoading } = useGetUser({ userId });

const baseProps = {
className: classNames('user-detail', className),
Expand All @@ -57,14 +57,22 @@ const UserDetail = ({
// Success state
return (
<div {...baseProps}>
<UserSummary user={user} testid={`${testid}-user-summary`} />
<UserSummary isLoading={isLoading} user={user} testid={`${testid}-user-summary`} />
<IonGrid>
<IonRow>
<IonCol sizeXs="2" sizeMd="1">
<CompanyDetail company={user?.company} testid={`${testid}-company-detail`} />
<IonCol sizeXs="12" sizeMd="6">
<CompanyDetail
company={user?.company}
isLoading={isLoading}
testid={`${testid}-company-detail`}
/>
</IonCol>
<IonCol sizeXs="2" sizeMd="1">
<AddressDetail address={user?.address} testid={`${testid}-address-detail`} />
<IonCol sizeXs="12" sizeMd="6">
<AddressDetail
address={user?.address}
isLoading={isLoading}
testid={`${testid}-address-detail`}
/>
</IonCol>
</IonRow>
</IonGrid>
Expand Down
57 changes: 37 additions & 20 deletions src/pages/Users/components/UserDetail/UserSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,67 @@ import './UserSummary.scss';
import { BaseComponentProps } from 'common/components/types';
import { User } from 'common/models/user';
import LoaderSkeleton from 'common/components/Loader/LoaderSkeleton';
import { boolean } from 'yup';

/**
* Properties for the `UserSummary` component.
* @param {boolean} [isLoading] - Indicates if the `user` is being loaded.
* @param {User} [user] - A `User` object.
* @see {@link BaseComponentProps}
*/
interface UserSummaryProps extends BaseComponentProps {
isLoading?: boolean;
user?: User;
}

/**
* The `UserSummary` component renders a block containing summary information
* about a single `User` including their name, email, phone, and website.
*
* If the supplied `user` is null or undefined, a loading state is rendered.
* If `isLoading` is `true` the loading state is rendered.
*
* If `isLoading` is `false` and the `user` property is provided, the
* user attributes are rendered.
*
* If `isLoading` is `false` and the `user` property is empty, the
* component returns `false` so that the component remains in the React
* hierarchy, but does not render anything.
*
* @param {UserSummaryProps} props - Component propertiers.
* @returns JSX
* @returns {JSX.Element | false} Returns JSX when loading or a user is
* provided, otherwise returns `false`.
*/
const UserSummary = ({
className,
isLoading = false,
testid = 'user-summary',
user,
}: UserSummaryProps): JSX.Element => {
}: UserSummaryProps): JSX.Element | false => {
const baseProps = {
className: classNames('user-summary', className),
'data-testid': testid,
};

if (isLoading) {
// loading state
return (
<div {...baseProps}>
<div data-testid={`${testid}-loader`}>
<div style={{ marginBottom: '0.5rem' }}>
<LoaderSkeleton animated heightStyle="2rem" widthStyle="16rem" />
</div>
<div style={{ display: 'flex', columnGap: '1rem' }}>
<LoaderSkeleton animated widthStyle="12rem" />
<LoaderSkeleton animated widthStyle="12rem" />
<LoaderSkeleton animated widthStyle="12rem" />
</div>
</div>
</div>
);
}

if (user) {
// successstate
// success state
return (
<div {...baseProps}>
<div className="content">
Expand All @@ -60,23 +90,10 @@ const UserSummary = ({
</div>
</div>
);
} else {
// loading state
return (
<div {...baseProps}>
<div data-testid={`${testid}-loader`}>
<div style={{ marginBottom: '0.5rem' }}>
<LoaderSkeleton animated heightStyle="2rem" widthStyle="16rem" />
</div>
<div style={{ display: 'flex', columnGap: '1rem' }}>
<LoaderSkeleton animated widthStyle="12rem" />
<LoaderSkeleton animated widthStyle="12rem" />
<LoaderSkeleton animated widthStyle="12rem" />
</div>
</div>
</div>
);
}

// not loading and no user
return false;
};

export default UserSummary;
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';

import { render, screen } from 'test/test-utils';
import { render, screen, waitFor } from 'test/test-utils';
import { userFixture1 } from '__fixtures__/users';

import AddressDetail from '../AddressDetail';
Expand All @@ -17,10 +17,19 @@ describe('AddressDetail', () => {

it('should render loading state', async () => {
// ARRANGE
render(<AddressDetail />);
render(<AddressDetail isLoading={true} />);
await screen.findByTestId('address-detail-loader');

// ASSERT
expect(screen.getByTestId('address-detail-loader')).toBeDefined();
});

it('should render empty state', async () => {
// ARRANGE
const { container } = render(<AddressDetail />);
await waitFor(() => expect(container).toBeDefined());

// ASSERT
expect(screen.queryByTestId('address-detail')).toBeNull();
});
});
Loading

0 comments on commit eb5ceb6

Please sign in to comment.