Skip to content

Commit

Permalink
feat: Material UI
Browse files Browse the repository at this point in the history
feat: AppXxx components
feat: AppStore
feat: Routing
feat: Utils
feat: Tests
  • Loading branch information
karpolan committed Apr 21, 2021
1 parent 4e2a997 commit eb15e2b
Show file tree
Hide file tree
Showing 55 changed files with 2,549 additions and 69 deletions.
21 changes: 15 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "currency-exchange",
"version": "0.1.2",
"version": "0.2.3",
"description": "Currency exchange widget runner",
"author": {
"name": "Anton Karpenko",
Expand All @@ -18,18 +18,27 @@
"url": "https://github.com/karpolan/currency-exchange.git"
},
"dependencies": {
"@material-ui/core": "^4.11.3",
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.57",
"date-fns": "^2.21.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"typescript": "^4.1.2",
"validate.js": "^0.13.1",
"web-vitals": "^1.0.1"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@types/jest": "^26.0.15",
"@types/node": "^12.0.0",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "4.0.3",
"typescript": "^4.1.2",
"web-vitals": "^1.0.1"
"@types/react-router-dom": "^5.1.7"
},
"scripts": {
"start": "react-scripts start",
Expand Down
3 changes: 2 additions & 1 deletion public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Currency Exchange</title>
<meta name="description" content="Web site created using create-react-app" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
Expand All @@ -13,7 +14,7 @@
<link rel="mask-icon" href="/img/favicon/safari-pinned-tab.svg" color="#5bbad5" />
<meta name="msapplication-config" content="/browserconfig.xml" />
<meta name="msapplication-TileColor" content="#da532c" />
<title>Currency Exchange</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
Expand Down
33 changes: 0 additions & 33 deletions src/App.css

This file was deleted.

9 changes: 0 additions & 9 deletions src/App.test.tsx

This file was deleted.

30 changes: 16 additions & 14 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
import './App.css';
import { AppStore } from './store';
import { AppRouter, Routes } from './routes';
import { ErrorBoundary } from './components';
import { AppThemeProvider } from './theme';

/**
* Renders Main Application
* Root Application Component
* @class App
*/
const App = () => {
return (
<div className="App">
<header>
Currency Exchange Sample
</header>
<main>
Main content here...
</main>
<footer>
Copyright &copy; <a href="https://karpolan.com" target="_blank" rel="noreferrer noopener">KARPOLAN</a>
</footer>
</div>
<ErrorBoundary name="App">
<AppStore>
<AppThemeProvider>
<AppRouter>
<Routes />
</AppRouter>
</AppThemeProvider>
</AppStore>
</ErrorBoundary>
);
}
};

export default App;
31 changes: 31 additions & 0 deletions src/components/AppAlert/AppAlert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import clsx from 'clsx';
import { Theme, makeStyles } from '@material-ui/core/styles';
import MuiAlert, { AlertProps as MuiAlertProps } from '@material-ui/lab/Alert';

const APP_ALERT_SEVERITY = 'info'; // 'error' | 'info'| 'success' | 'warning'
const APP_ALERT_VARIANT = 'standard'; // 'filled' | 'outlined' | 'standard'

const useStyles = makeStyles((theme: Theme) => ({
root: {
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
},
}));

/**
* Application styled Alert component
*/
const AppAlert: React.FC<MuiAlertProps> = ({
severity = APP_ALERT_SEVERITY,
variant = APP_ALERT_VARIANT,
className,
onClose,
...restOfProps
}) => {
const classes = useStyles();
const classRoot = clsx(classes.root, className);

return <MuiAlert className={classRoot} severity={severity} variant={variant} onClose={onClose} {...restOfProps} />;
};

export default AppAlert;
3 changes: 3 additions & 0 deletions src/components/AppAlert/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import AppAlert from './AppAlert';

export { AppAlert as default, AppAlert };
81 changes: 81 additions & 0 deletions src/components/AppButton/AppButton.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { render, screen } from '@testing-library/react';
import AppButton from './AppButton';
import { ColorName } from '../../utils/style';

/**
* Test specific color for AppButton
* @param {string} colorName - name of the color, one of ColorName type
* @param {string} [expectedClassName] - optional value to be found in className (color "true" may use "success" class name)
* @param {boolean} [ignoreClassName] - optional flag to ignore className (color "inherit" doesn't use any class name)
*/
function testButtonColor(colorName: string, expectedClassName = colorName, ignoreClassName = false) {
it(`supports "${colorName}" color`, async () => {
let text = `${colorName} button`;
await render(<AppButton color={colorName as ColorName}>{text}</AppButton>);

let span = await screen.getByText(text); // <span> with specific text
expect(span).toBeDefined();

let button = await span.closest('button'); // parent <button> element
expect(button).toBeDefined();
if (!ignoreClassName) {
expect(button?.className?.includes(`makeStyles-${expectedClassName}`)).toBeTruthy(); // There is "makeStyles-[expectedClassName]-xxx" class
}
});
}

describe('AppButton component', () => {
// beforeEach(() => {});

it('renders itself', async () => {
let text = 'sample button';
await render(<AppButton>{text}</AppButton>);
let span = await screen.getByText(text);
expect(span).toBeDefined();
expect(span).toHaveTextContent(text);
let button = await span.closest('button'); // parent <button> element
expect(button).toBeDefined();
expect(button).toHaveAttribute('type', 'button'); // not "submit" or "input" by default
});

testButtonColor('primary');
testButtonColor('secondary');
testButtonColor('error');
testButtonColor('warning');
testButtonColor('info');
testButtonColor('success');
testButtonColor('true');
testButtonColor('false');

testButtonColor('default');
testButtonColor('inherit', 'default', true);

it('supports className property', async () => {
let text = 'button with specific class';
let className = 'someClassName';
await render(<AppButton className={className}>{text}</AppButton>);
let span = await screen.getByText(text);
expect(span).toBeDefined();
let button = await span.closest('button'); // parent <button> element
expect(button).toBeDefined();
expect(button).toHaveClass(className);
});

it('supports label property', async () => {
let text = 'button with label';
await render(<AppButton label={text} />);
let span = await screen.getByText(text);
expect(span).toBeDefined();
let button = await span.closest('button'); // parent <button> element
expect(button).toBeDefined();
});

it('supports text property', async () => {
let text = 'button with text';
await render(<AppButton text={text} />);
let span = await screen.getByText(text);
expect(span).toBeDefined();
let button = await span.closest('button'); // parent <button> element
expect(button).toBeDefined();
});
});
64 changes: 64 additions & 0 deletions src/components/AppButton/AppButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import clsx from 'clsx';
import { Theme, makeStyles } from '@material-ui/core/styles';
import Box from '@material-ui/core/Box';
import Button, { ButtonProps } from '@material-ui/core/Button';
import { buttonStylesByNames, ColorName } from '../../utils/style';

const APP_BUTTON_VARIANT = 'contained'; // | 'text' | 'outlined'

const useStyles = makeStyles((theme: Theme) => ({
box: {
display: 'inline-block',
},
// Add "filled" styles for Material UI names 'primary', 'secondary', 'warning', and so on
...buttonStylesByNames(theme),
}));

interface Props extends Omit<ButtonProps, 'color'> {
color?: ColorName | 'inherit';
label?: string; // Alternate to text
text?: string; // Alternate to label
m?: number;
mt?: number;
mb?: number;
ml?: number;
mr?: number;
// Missing props
component?: React.ElementType; // Could be RouterLink, AppLink, etc.
to?: string; // Link prop
href?: string; // Link prop
}

/**
* Application styled Material UI Button
* @class AppButton
* @param {string} [color] - name of color from Material UI palette 'primary', 'secondary', 'warning', and so on
* @param {string} [children] - content to render, overrides .label and .text
* @param {string} [label] - text to render, alternate to .text
* @param {string} [text] - text to render, alternate to .label
*/
const AppButton: React.FC<Props> = ({
className,
children,
color = 'default',
label,
text,
m = 0,
mt = 1,
mb = 1,
ml = 1,
mr = 1,
...restOfProps
}) => {
const classes = useStyles();
const classButton = clsx(classes[color as ColorName], className);
return (
<Box {...{ m, mt, mb, ml, mr }} className={classes.box}>
<Button className={classButton} variant={APP_BUTTON_VARIANT} {...restOfProps}>
{children || label || text}
</Button>
</Box>
);
};

export default AppButton;
3 changes: 3 additions & 0 deletions src/components/AppButton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import AppButton from './AppButton';

export { AppButton as default, AppButton };
67 changes: 67 additions & 0 deletions src/components/AppIcon/AppIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { SvgIcon } from '@material-ui/core';
// SVG assets
import { ReactComponent as LogoIcon } from './logo.svg';
// Material Icons
import DefaultIcon from '@material-ui/icons/MoreHoriz';
import SettingsIcon from '@material-ui/icons/Settings';
import VisibilityIcon from '@material-ui/icons/Visibility';
import VisibilityOffIcon from '@material-ui/icons/VisibilityOff';
import MenuIcon from '@material-ui/icons/Menu';
import CloseIcon from '@material-ui/icons/Close';
import DayNightIcon from '@material-ui/icons/Brightness4';
import NightIcon from '@material-ui/icons/Brightness3';
import DayIcon from '@material-ui/icons/Brightness5';
import SearchIcon from '@material-ui/icons/Search';
import InfoIcon from '@material-ui/icons/Info';
import HomeIcon from '@material-ui/icons/Home';
import AccountCircle from '@material-ui/icons/AccountCircle';
import PersonAddIcon from '@material-ui/icons/PersonAdd';
import PersonIcon from '@material-ui/icons/Person';

/**
* How to use:
* 1. Import all required MUI or other SVG icons into this file.
* 2. Add icons with "unique lowercase names" into ICONS object.
* 3. Use icons everywhere in the App by their names in <AppIcon name="xxx" /> component
* Important: properties of ICONS object MUST be lowercase!
* Note: You can use camelCase or UPPERCASE in the <AppIcon name="someIconByName" /> component
*/
const ICONS: Record<string, React.ComponentType> = {
default: DefaultIcon,
logo: () => (
<SvgIcon>
<LogoIcon />
</SvgIcon>
),
close: CloseIcon,
menu: MenuIcon,
settings: SettingsIcon,
visibilityon: VisibilityIcon,
visibilityoff: VisibilityOffIcon,
daynight: DayNightIcon,
night: NightIcon,
day: DayIcon,
search: SearchIcon,
info: InfoIcon,
home: HomeIcon,
account: AccountCircle,
signup: PersonAddIcon,
login: PersonIcon,
};

/**
* Renders SVG icon by given Icon name
* @param {string} [props.name] - name of the Icon to render
* @param {string} [props.icon] - name of the Icon to render
*/
interface Props {
name?: string; // Icon's name
icon?: string; // Icon's name alternate prop
}
const AppIcon: React.FC<Props> = ({ name, icon, ...restOfProps }) => {
const iconName = (name || icon || 'default').trim().toLowerCase();
const ComponentToRender = ICONS[iconName] || DefaultIcon;
return <ComponentToRender {...restOfProps} />;
};

export default AppIcon;
Loading

0 comments on commit eb15e2b

Please sign in to comment.