Skip to content

Commit

Permalink
[Feature] implement admin page (#5)
Browse files Browse the repository at this point in the history
* refactor: Button

* implement EmployeeList

* add api: get review list

* implement ReviewList
  • Loading branch information
arthur791004 authored Oct 6, 2019
1 parent 343bf0c commit 1b8aa75
Show file tree
Hide file tree
Showing 26 changed files with 646 additions and 22 deletions.
22 changes: 22 additions & 0 deletions client/components/Badge/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { oneOf } from 'prop-types';
import styled from 'styled-components';
import { DANGER } from '@/styles/colors';
import { sizes } from '@/components/Button/utils';

const Badge = styled.div`
${props => sizes(props.size)};
display: inline-block;
margin: 0 8px;
color: white;
background-color: ${DANGER};
`;

Badge.propTypes = {
size: oneOf(['small', 'medium', 'large']),
};

Badge.defaultProps = {
size: 'medium',
};

export default Badge;
58 changes: 58 additions & 0 deletions client/components/Button/Tab.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { bool, oneOf } from 'prop-types';
import styled, { css } from 'styled-components';
import { BLACK, LIGHT_BLACK } from '@/styles/colors';
import { sizes } from './utils';

const activeCSS = css`
color: ${BLACK};
&:after {
transform: scaleX(1);
}
`;

const Tab = styled.button.attrs(() => ({
type: 'button',
}))`
${props => sizes(props.size)};
position: relative;
border: none;
outline: none;
color: ${LIGHT_BLACK};
background-color: transparent;
transition: color 0.3s ease-out;
will-change: color;
cursor: pointer;
&:after {
content: '';
display: block;
width: 50%;
height: 3px;
position: relative;
margin: 10px auto 0;
background-color: ${BLACK};
transform: scaleX(0);
transition: transform 0.15s ease-out;
}
&:hover,
&:active {
color: ${BLACK};
}
${props => props.isActive && activeCSS};
`;

Tab.propTypes = {
size: oneOf(['small', 'medium', 'large']),
isActive: bool,
};

Tab.defaultProps = {
size: 'medium',
isActive: false,
};

export default Tab;
25 changes: 19 additions & 6 deletions client/components/Button/TextButton.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,38 @@
import React from 'react';
import { func } from 'prop-types';
import { string, oneOf } from 'prop-types';
import styled from 'styled-components';
import { BLACK } from '@/styles/colors';
import { sizes } from './utils';

const StyledButton = styled.button`
${props => sizes(props.size)};
border: none;
outline: none;
color: ${props => props.color};
background-color: transparent;
cursor: pointer;
transition: opacity 0.3s ease-out;
transition: background-color 0.3s ease-out;
will-change: background-color;
&:hover,
&:active {
opacity: 0.8;
background-color: rgba(0, 0, 0, 0.08);
}
`;

const TextButton = ({ handleClick, ...props }) => (
<StyledButton {...props} onClick={handleClick} />
const TextButton = ({ size, ...props }) => (
<StyledButton {...props} size={size} />
);

TextButton.propTypes = {
handleClick: func.isRequired,
size: oneOf(['small', 'medium', 'large']),
color: string,
};

TextButton.defaultProps = {
size: 'medium',
color: BLACK,
};

export default TextButton;
12 changes: 6 additions & 6 deletions client/components/Button/index.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
import React from 'react';
import { bool, node } from 'prop-types';
import { bool, node, oneOf } from 'prop-types';
import styled, { css } from 'styled-components';
import { BLACK } from '@/styles/colors';
import { sizes } from './utils';

const loadingCSS = css`
opacity: 0.8;
cursor: progress;
`;

const StyledButton = styled.button`
${props => sizes(props.size)};
position: relative;
height: 42px;
padding: 0 25px;
border-radius: 21px;
line-height: 42px;
font-size: 16px;
color: white;
background-color: ${BLACK};
outline: none;
Expand Down Expand Up @@ -42,10 +40,12 @@ const Button = ({ children, isLoading, ...props }) => (

Button.propTypes = {
children: node.isRequired,
size: oneOf(['small', 'medium', 'large']),
isLoading: bool,
};

Button.defaultProps = {
size: 'medium',
isLoading: false,
};

Expand Down
31 changes: 31 additions & 0 deletions client/components/Button/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { css } from 'styled-components';

const configs = {
small: {
height: 20,
padding: 8,
fontSize: 12,
},
medium: {
height: 32,
padding: 21,
fontSize: 14,
},
large: {
height: 42,
padding: 25,
fontSize: 16,
},
};

export const sizes = size => {
const { height, padding, fontSize } = configs[size] || configs.medium;

return css`
height: ${height}px;
line-height: ${height}px;
border-radius: ${height / 2}px;
padding: 0 ${padding}px;
font-size: ${fontSize}px;
`;
};
8 changes: 8 additions & 0 deletions client/components/Empty/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import styled from 'styled-components';
import { LIGHT_BLACK } from '@/styles/colors';

const Empty = styled.div`
color: ${LIGHT_BLACK};
`;

export default Empty;
4 changes: 3 additions & 1 deletion client/components/ErrorMessage/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ const Container = styled.div`
color: ${DANGER};
`;

const ErrorMessage = ({ children }) => <Container>{children}</Container>;
const ErrorMessage = ({ children, ...props }) => (
<Container {...props}>{children}</Container>
);

ErrorMessage.propTypes = {
children: node.isRequired,
Expand Down
4 changes: 2 additions & 2 deletions client/components/GlobalStyle/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createGlobalStyle } from 'styled-components';
import { DARK_WHITE } from '@/styles/colors';
import { LIGHT_WHITE } from '@/styles/colors';

const GlobalStyle = createGlobalStyle`
html,
Expand All @@ -14,7 +14,7 @@ const GlobalStyle = createGlobalStyle`
body {
font-family: 'Helvetica', 'Arial', 'PingFang TC', 'Heiti TC', 'Microsoft Jhenghei', sans-serif;
font-size: 14px;
background-color: ${DARK_WHITE};
background-color: ${LIGHT_WHITE};
}
a {
Expand Down
77 changes: 77 additions & 0 deletions client/components/Table/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from 'react';
import styled from 'styled-components';
import { bool, string, node } from 'prop-types';
import { BLACK, LIGHT_WHITE, GRAY } from '@/styles/colors';
import Loading from '@/components/Loading';
import ErrorMessage from '@/components/ErrorMessage';

export const TableRow = styled.tr`
height: 56px;
line-height: 56px;
border: 1px solid ${GRAY};
border-top: 0;
&:hover {
background-color: rgba(0, 0, 0, 0.01);
}
`;

export const TableData = styled.td`
padding: 0 16px;
text-align: ${props => props.align || 'left'};
`;

const Error = styled(ErrorMessage)`
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
`;

export const TableContent = ({ isLoading, error, children }) => {
if (isLoading) {
return (
<TableRow>
<Loading as="td" />
<TableData />
</TableRow>
);
} else if (error) {
return (
<TableRow>
<td>
<Error>{error}</Error>
</td>
<TableData />
</TableRow>
);
}

return children;
};

TableContent.propTypes = {
isLoading: bool.isRequired,
error: string.isRequired,
children: node.isRequired,
};

export const TableHead = styled.th`
padding: 0 16px;
text-align: ${props => props.align || 'left'};
`;

export const TableHeader = styled.thead`
color: ${LIGHT_WHITE};
background-color: ${BLACK};
`;

const Table = styled.table`
position: relative;
min-width: 768px;
min-height: 300px;
border-spacing: 0;
border-collapse: collapse;
`;

export default Table;
25 changes: 25 additions & 0 deletions client/containers/EmployeeList/Employee.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import { string, number } from 'prop-types';
import isAdmin from '@/utils/isAdmin';
import TextButton from '@/components/Button/TextButton';
import Badge from '@/components/Badge';
import { TableRow, TableData } from '@/components/Table';

const Employee = ({ email, role }) => (
<TableRow>
<TableData>
<span>{email}</span>
{isAdmin({ role }) && <Badge size="small">admin</Badge>}
</TableData>
<TableData align="right">
<TextButton>Edit</TextButton>
</TableData>
</TableRow>
);

Employee.propTypes = {
email: string.isRequired,
role: number.isRequired,
};

export default Employee;
54 changes: 54 additions & 0 deletions client/containers/EmployeeList/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React, { useEffect } from 'react';
import styled from 'styled-components';
import { useSelector, useDispatch } from 'react-redux';
import {
selectUserList,
selectIsLoading,
selectError,
getUserListThunk,
} from '@/redux/slices/users';
import Table, {
TableHeader,
TableHead,
TableRow,
TableContent,
} from '@/components/Table';
import Employee from './Employee';

const Container = styled.div`
border-radius: 4px;
overflow: hidden;
`;

const EmployeeList = () => {
const userList = useSelector(selectUserList);
const isLoading = useSelector(selectIsLoading);
const error = useSelector(selectError);
const dispatch = useDispatch();

useEffect(() => {
dispatch(getUserListThunk());
}, [dispatch]);

return (
<Container>
<Table>
<TableHeader>
<TableRow>
<TableHead align="left">Email</TableHead>
<TableHead />
</TableRow>
</TableHeader>
<tbody>
<TableContent isLoading={isLoading} error={error}>
{userList.map(({ id, email, role }) => (
<Employee key={id} email={email} role={role} />
))}
</TableContent>
</tbody>
</Table>
</Container>
);
};

export default EmployeeList;
Loading

0 comments on commit 1b8aa75

Please sign in to comment.