8000 feat: add common UI components by huynguyen-hl Β· Pull Request #4 Β· uncefact/tests-untp Β· GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

feat: add common UI components #4

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

Merged
merged 4 commits into from
Jan 26, 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
34 changes: 34 additions & 0 deletions packages/components/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,35 @@
# Mock app components

A collection of reusable React components for general-purpose use.

## Installation

### Install dependencies

Make sure your machine has Node.js version 18 or later installed.

To install the project dependencies, run the following command:

```bash
pnpm install
```

## Storybook

Explore and interact with the components using Storybook.

### Run Storybook

```bash
pnpm run storybook
```

Visit http://localhost:6006/ in your browser to view Storybook.

## Unit Testing

Run unit tests using Jest.

```bash
pnpm test
```
1 change: 1 addition & 0 deletions packages/components/__mocks__/styleMock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = {};
12 changes: 8 additions & 4 deletions packages/components/package.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
{
"name": "@mock-app/components",
"version": "1.0.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"main": "./build/index.js",
"types": "./build/index.d.ts",
"type": "module",
"scripts": {
"start": "react-scripts start",
"build": "tsc",
"build:watch": "tsc -b --watch",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"test": "jest --ci --env=jest-environment-jsdom"
"test": "jest --ci --env=jest-environment-jsdom",
"coverage": "jest --coverage"
},
"eslintConfig": {
"extends": [
Expand Down Expand Up @@ -46,7 +47,10 @@
"@mui/x-date-pickers": "^6.19.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1"
"react-error-boundary": "^4.0.12",
"react-router-dom": "^6.21.3",
"react-scripts": "5.0.1",
"react-toastify": "^10.0.4"
},
"devDependencies": {
"@babel/preset-react": "^7.7.0",
Expand Down
29 changes: 29 additions & 0 deletions packages/components/src/components/Footer/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';
import { Box, Typography } from '@mui/material';

interface IProps {
textColor?: string;
backgroundColor?: string;
}

/**
* Footer component is used to display the footer
*/
export const Footer = ({ textColor = '#000', backgroundColor = '#fff' }: IProps) => {
const currentYear = new Date().getFullYear();

return (
<Box
sx={{
textAlign: 'center',
position: 'fixed',
bottom: 0,
width: '100%',
color: textColor,
backgroundColor,
}}
>
<Typography>Copyright Β© {currentYear}</Typography>
</Box>
);
};
1 change: 1 addition & 0 deletions packages/components/src/components/Footer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Footer } from './Footer';
6D40 144 changes: 144 additions & 0 deletions packages/components/src/components/Header/Header.tsx
< 9E88 tr data-hunk="a6c4a129720b9b14e99975d7dcad0e5653180aa857364c0ceefb41f305accc6b" class="show-top-border">
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import React, { useState, MouseEvent } from 'react';
import { AppBar, Toolbar, Typography, Container, Stack, Box, IconButton, Menu, MenuItem, Button } from '@mui/material';
import { Menu as MenuIcon } from '@mui/icons-material';
import { BrowserRouter, Link } from 'react-router-dom';

interface IProps {
logoTitle?: string;
logoTitleColor?: string;
backgroundColor?: string;
routerLinks: { title: string; path: string }[];
}

/**
* Header component is used to display the header and navigation to other pages
*/
export const Header = ({ routerLinks, logoTitle = 'Logo', logoTitleColor = '#000', backgroundColor = '#fff' }: IProps) => {
const [anchorElNav, setAnchorElNav] = useState<null | HTMLElement>(null);

/**
* open nav menu on mobile.
*/
const handleOpenNavMenu = (event: MouseEvent<HTMLElement>) => {
setAnchorElNav(event.currentTarget);
};

/**
* close nav menu on mobile.
*/
const handleCloseNavMenu = () => {
setAnchorElNav(null);
};

return (
<BrowserRouter>
<AppBar data-testid="header" sx={{ background: backgroundColor }}>
<Container maxWidth="xl">
<Toolbar disableGutters>
{/* Logo on desktop or tablet */}
<Stack
component={Link}
to="/"
sx={{
textDecoration: 'none',
display: { xs: 'none', md: 'flex' },
alignItems: 'center',
flexDirection: 'row',
mr: 2,
}}
>
<Typography
data-testid="logo"
variant="h6"
sx={{
color: logoTitleColor,
}}
>
{logoTitle}
</Typography>
</Stack>
{/* Menu on mobile */}
<Box sx={{ flexGrow: 1, display: { xs: 'flex', md: 'none' } }} data-testid="menu">
<IconButton
data-testid="icon-button"
size="small"
aria-controls="menu-appbar"
aria-haspopup="true"
>
color="inherit"
>
<MenuIcon sx={{ color: logoTitleColor }} />
</IconButton>
<Menu
data-testid="menu-appbar"
id="menu-appbar"
anchorEl={anchorElNav}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
open={Boolean(anchorElNav)}
>
sx={{
display: { xs: 'block', md: 'none' },
}}
>
{routerLinks.map((page) => (
<MenuItem key={page.title} >
<Typography
textAlign="center"
component={Link}
to={page.path}
sx={{ textDecoration: 'none', color: 'inherit' }}
>
{page.title}
</Typography>
</MenuItem>
))}
</Menu>
</Box>
{/* Logo on mobile */}
<Stack
component={Link}
to="/"
sx={{
flexGrow: 1,
textDecoration: 'none',
display: { xs: 'flex', md: 'none' },
flexDirection: 'row',
margin: 'auto',
}}
>
<Typography
variant="h6"
sx={{
color: logoTitleColor,
}}
>
{logoTitle}
</Typography>
</Stack>
{/* Menu item on desktop or tablet */}
<Box sx={{ flexGrow: 1, display: { xs: 'none', md: 'flex' } }} data-testid="menu-desktop">
{routerLinks.map((page) => (
<Button
key={page.title}
component={Link}
to={page.path}
sx={{ color: logoTitleColor, display: 'block' }}
>
{page.title}
</Button>
))}
</Box>
</Toolbar>
</Container>
</AppBar>
</BrowserRouter>
);
};
1 change: 1 addition & 0 deletions packages/components/src/components/Header/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Header } from './Header';
92 changes: 92 additions & 0 deletions packages/components/src/components/Layout/Layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { Container, Box, Typography } from '@mui/material';
import {
Done as DoneIcon,
CancelOutlined as CancelOutlinedIcon
} from '@mui/icons-material';

// Enum defining possible statuses for the layout
export enum LayoutStatus {
success = 'success',
error = 'error',
};

/**
* FallbackErrorContent Component returns a Box component containing the MessageText component with an error status
*/
export const FallbackErrorContent = ({ errorMessage }: {
errorMessage: string;
}) => {
return (
<Box
sx={{
alignItems: 'center',
justifyContent: 'center',
display: 'flex',
flexDirection: 'column',
}}
>
<MessageText status={LayoutStatus.error} text={errorMessage} />
</Box>
);
};

/**
* MessageText Component returns a Box component containing an icon based on the status
* and a Typography component with the provided text
*/
export function MessageText({ status, text }: {
status?: LayoutStatus;
text: string;
}) {
return (
<Box
sx={{
display: 'flex',
justifyContent: 'center',
}}
>
{status === LayoutStatus.error && (
<>
<CancelOutlinedIcon color='error' sx={{ marginRight: '10px' }} />
</>
)}
{status === LayoutStatus.success && (
<>
<DoneIcon color='success' sx={{ marginRight: '10px' }} />
</>
)}
<Typography sx={{ marginBottom: '50px' }}>{text}</Typography>
</Box>
);
}

/**
* Layout component is used to display the header and navigation to other pages
*/
export const Layout = ({ children, errorMessage = 'Something went wrong! Please retry again' }: {
children: React.ReactNode;
errorMessage?: string;
}) => {
return (
/**
* The ErrorBoundary component is used to catch errors anywhere in the component tree
* and provide a fallback UI in case of an error. In this case, the FallbackComponent is specified
* as an inline arrow function that renders the FallbackErrorContent component with the provided errorMessage.
*/
<ErrorBoundary FallbackComponent={() => <FallbackErrorContent errorMessage={errorMessage} />}>
<Container
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
mt: '64px',
mb: '24px',
}}
>
{children}
</Container>
</ErrorBoundary>
);
};
1 change: 1 addition & 0 deletions packages/components/src/components/Layout/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Layout } from './Layout';
30 changes: 30 additions & 0 deletions packages/components/src/components/ToastMessage/ToastMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';

// An enumeration for different toast statuses
export enum Status {
success = 'success',
error = 'error',
warning = 'warning',
info = 'info',
};

// The function for displaying toast messages
export function toastMessage({ status, message }: {
status: Status;
message: string;
}): void {
toast[status](message, {
position: 'top-right',
hideProgressBar: true,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
autoClose: 3000,
});
}

// ToastMessage component for displaying a ToastContainer
export const ToastMessage = () => {
return <ToastContainer />;
};
1 change: 1 addition & 0 deletions packages/components/src/components/ToastMessage/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ToastMessage } from './ToastMessage';
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';

import { Footer } from '../../..';

describe('Footer', () => {
test('should render Footer component', () => {
const currentYear = new Date().getFullYear();
const footerText = `Copyright Β© ${currentYear}`;

render(<Footer />);
expect(screen.getByText(footerText)).toBeInTheDocument();
});
});
Loading
0