In this article we will build a UI following a design. We will use Figma
to visualize the design, but is also possible to use any other tool that allows you to extract the CSS code from the elements, such as invisionapp
, zeplin
, etc.
Read this in Spanish
Live demo: https://llorentegerman.github.io/react-admin-dashboard/
Repository: https://github.com/llorentegerman/react-admin-dashboard
I will not enter in details about the tool, we only need a design.
- Create an account in https://www.figma.com (free).
- I have selected a random Figma file from https://www.figmafreebies.com (free). The selected file is: Figma Admin Dashboard UI Kit.
I'm using the web version of Figma, so, you have to click
DOWNLOAD FREEBIES
button, and the design will be added to your account. - You can double click on each element and see the css code related to it in the
code
tab that is in the right column.
For this step we will use Create React App:
npx create-react-app react-admin-dashboard
We will use aphrodite for the styles and simple-flexbox to make the layout.
yarn add aphrodite simple-flexbox
or npm install aphrodite simple-flexbox
For this case we can keep a simple structure:
/src
/assets
/components
App.js
We are ready to start, first we need to identify the main blocks of the design. I have decided to split it into 4 main blocks as follows:
1- Sidebar
2- Main Block
3- Header
4- Content
As you can see in the image, blocks 3 and 4 are inside block 2.
We can split the Sidebar in 2 parts, Logo
block, and MenuItem
list.
We need 3 components for this:
1- SidebarComponent
2- LogoComponent
3- MenuItemComponent (list)
We will start defining the Logo and Menu Items
First we need to download the Logo (double click on the logo, go to the Design
tab and click on export button bellow). I downloaded it in svg
format and imported it as a React Component, you can see it and copy it clicking here.
LogoComponent.js
is a Row
centered vertically and horizontally, with the Logo
and the title
.
<Row className={css(styles.container)} horizontal="center" vertical="center">
<Logo />
<span className={css(styles.title)}>Dashboard Kit</span>
</Row>
For styles, we need to import Muli
font family, the easy way is include this line in App.css (we can remove the rest of the content, we don't need it):
@import url('https://fonts.googleapis.com/css?family=Muli');
These are the styles for container
and title
container: {
marginLeft: 32,
marginRight: 32
},
title: {
fontFamily: 'Muli',
fontStyle: 'normal',
fontWeight: 'bold',
fontSize: 19,
lineHeight: '24px',
letterSpacing: '0.4px',
color: '#A4A6B3',
opacity: 0.7,
marginLeft: 12 // <--- necessary to separate title and logo
}
View full file: LogoComponent.js
It represents an item of the menu, it's composed by an icon
, a title
and has different styles depending of its own state (active
, unactive
, hover
). If it's active, has a white bar at the left.
<Row className={css(styles.container, active && styles.activeContainer)} vertical="center">
{active && <div className={css(styles.activeBar)}></div>}
<Icon fill={active && "#DDE2FF"} opacity={!active && "0.4"} />
<span className={css(styles.title, active && styles.activeTitle)}>{title}</span>
</Row>
As you can see, there are some special styles depending on active
property, for example the title
has a different color when active
is true
. For the icons, default fill is #9FA2B4
and default opacity is 1
, these values change depending on the state of the above mentioned property.
A special element that appears when the item is active
, is that white bar on the left (activeBar
).
These are the styles:
activeBar: {
height: 56,
width: 3,
backgroundColor: '#DDE2FF',
position: 'absolute',
left: 0
},
activeContainer: {
backgroundColor: 'rgba(221,226,255, 0.08)'
},
activeTitle: {
color: '#DDE2FF'
},
container: {
height: 56,
cursor: 'pointer',
':hover': {
backgroundColor: 'rgba(221,226,255, 0.08)'
},
paddingLeft: 32,
paddingRight: 32
},
title: {
fontFamily: 'Muli',
fontSize: 16,
lineHeight: '20px',
letterSpacing: '0.2px',
color: '#A4A6B3',
marginLeft: 24
}
View full file: MenuItemComponent.js
As we did with the Logo, we need to download the icons that we will use in this component, it's possible do it from the design or you can copy them from the folder assets
of the repository clicking here.
...
import IconOverview from '../../assets/icon-overview.js';
...
<Column className={css(styles.container)}>
<LogoComponent />
<Column className={css(styles.menuItemList)}>
<MenuItemComponent title="Overview" icon={IconOverview} />
<MenuItemComponent title="Tickets" icon={IconTickets} active />
<MenuItemComponent title="Ideas" icon={IconIdeas} />
<MenuItemComponent title="Contacts" icon={IconContacts} />
<MenuItemComponent title="Agents" icon={IconAgents} />
<MenuItemComponent title="Articles" icon={IconArticles} />
<div className={css(styles.separator)}></div>
<MenuItemComponent title="Settings" icon={IconSettings} />
<MenuItemComponent title="Subscription" icon={IconSubscription} />
</Column>
</Column>
Based on css
extracted from the design, we can define the styles with these 3 classes:
container: {
backgroundColor: '#363740',
width: 255,
paddingTop: 32
},
menuItemList: {
marginTop: 52
},
separator: {
borderTop: '1px solid #DFE0EB',
marginTop: 16,
marginBottom: 16,
opacity: 0.06
}
View full file: SidebarComponent.js
SidebarComponent
is ready, in the repository I have added some onClick
events and a state
to do it interactive, so you can select the differents menu items.
Now we only need to work in App.js
, as we said, has the following structure:
1- Sidebar
2- Main Block
3- Header
4- Content
It can be defined as follows:
<Row className={css(styles.container)}>
<SidebarComponent />
<Column flexGrow={1} className={css(styles.mainBlock)}>
<HeaderComponent title="Title" />
<div className={css(styles.content)}>
<span>Content</span>
</div>
</Column>
</Row>
Styles:
container: {
height: '100vh' // menu has to take all the height of the screen
},
content: {
marginTop: 54
},
mainBlock: {
backgroundColor: '#F7F8FC',
padding: 30
}
To finish, we will define the Header, with the following structure.
1- Row ({ vertical: center, horizontal: space-between })
2- Title
3- Row ({ vertical: center })
4- Icons
5- Separator
6- Row ({ vertical: center })
7- Name
8- Avatar
<Row className={css(styles.container)} vertical="center" horizontal="space-between">
<span className={css(styles.title)}>{title}</span>
<Row vertical="center">
<div className={css(styles.cursorPointer)}>
<IconSearch />
</div>
<div style={{ marginLeft: 25 }} className={css(styles.cursorPointer)}>
<IconBellNew />
</div>
<div className={css(styles.separator)}></div>
<Row vertical="center">
<span className={css(styles.name, styles.cursorPointer)}>Germán Llorente</span>
<img src="https://avatars3.githubusercontent.com/u/21162888?s=460&v=4" alt="avatar" className={css(styles.avatar, styles.cursorPointer)} />
</Row>
</Row>
</Row>
Header styles:
avatar: {
height: 35,
width: 35,
borderRadius: 50,
marginLeft: 14,
border: '1px solid #DFE0EB',
},
container: {
height: 40
},
cursorPointer: {
cursor: 'pointer'
},
name: {
fontFamily: 'Muli',
fontStyle: 'normal',
fontWeight: 600,
fontSize: 14,
lineHeight: '20px',
textAlign: 'right',
letterSpacing: 0.2
},
separator: {
borderLeft: '1px solid #DFE0EB',
marginLeft: 32,
marginRight: 32,
height: 32,
width: 2
},
title: {
fontFamily: 'Muli',
fontStyle: 'normal',
fontWeight: 'bold',
fontSize: 24,
lineHeight: '30px',
letterSpacing: 0.3
}
View full file: HeaderComponent.js
At the moment, our UI is not responsive, and we want it to look like this:
As we don't have a responsive design to follow, we will keep it simple, only one breakpoint at 768px. So any screen less than 768px
will be considered mobile
.
The Sidebar
will be isolated, on this component will be included: Burger button
, Desktop Sidebar
and Mobile Sidebar
.
SidebarComponent
for Desktop
is already explained. In this article we will see how to convert it in a responsive sidebar.
In mobile screen (width <= 768px) SidebarComponent
could have 2 different states: collapsed
(default) or expanded
.
In this state the whole sidebar will be hidden, then the mainBlock
(see App.js) will fill the whole width of the screen.
We need a button to expand
the Sidebar
and we will use a BurgerIcon
for that (to copy the burger icon click here). That button will be in a absolute position, over the header
:
In this state we will show the Sidebar
and an outsideLayer
that will fill the rest of the screen with a semitransparent background, and if you click it the Sidebar
will be closed:
Since the Burger button
will be over the header
we need to add some left-margin
to the Header Title
to avoid this situation:
These are the most important parts of the new styles of HeaderComponent.js
, as you can see I have included media queries to apply some special styles for mobile screens:
name: {
...,
'@media (max-width: 768px)': {
display: 'none' // <--- don't show the name on mobile
}
},
separator: {
...,
'@media (max-width: 768px)': {
marginLeft: 12, // <--- less separation on mobile
marginRight: 12
}
},
title: {
...,
'@media (max-width: 768px)': {
marginLeft: 36 <--- to avoid overlapping with Burger button
},
'@media (max-width: 468px)': {
fontSize: 20 <--- new fontSize for small devices.
}
}
I have also added a new style for the icons wrappers.
View the changes: HeaderComponent.js
View full file: HeaderComponent.js
This component contains all the logic and it will change depending on these two variables:
expanded
: stored in thestate
isMobile
:true
whenwindow.innerWidth <= 768
When the Sidebar
is expanded, there are two differents ways to collapse it, clicking in some MenuItem
or clicking on the outsideLayer
. To manage this behaviour there are 2 methods:
onItemClicked = (item) => {
this.setState({ expanded: false });
return this.props.onChange(item);
}
toggleMenu = () => this.setState(prevState => ({ expanded: !prevState.expanded }));
toggleMenu
will be fired when you click on the Burger button
(if sidebar is collapsed) or when you click on the outsideLayer
(if sidebar is expanded).
Here is the new version of SidebarComponent
:
<div style={{ position: 'relative'}}>
<Row className={css(styles.mainContainer)} breakpoints={{ 768: css(styles.mainContainerMobile) }}>
{(isMobile && !expanded) && this.renderBurger()}
<Column className={css(styles.container)}
breakpoints={{ 768: css(styles.containerMobile, expanded ? styles.show : styles.hide) }}>
<LogoComponent />
<Column className={css(styles.menuItemList)}>
...
<MenuItemComponent
title="Tickets" icon={IconTickets}
onClick={() => this.onItemClicked('Tickets')}
active={this.props.selectedItem === 'Tickets'}
/>
...
</Column>
</Column>
{isMobile && expanded && <div className={css(styles.outsideLayer)} onClick={this.toggleMenu}></div>}
</Row>
</div>
and here is the renderBurger
method:
renderBurger = () => {
return <div onClick={this.toggleMenu} className={css(styles.burgerIcon)}>
<IconBurger />
</div>
}
We are wrapping the component inside a div
with position: relative
, and that is to allow to the Sidebar
fill all the screen, otherwise it will looks like this:
As you can see, we are using the breakpoints
property of simple-flexbox, for example:
<Row className={css(styles.mainContainer)} breakpoints={{ 768: css(styles.mainContainerMobile) }} >
it means that if window.innerWidth <= 768
mainContainerMobile
styles will be applied.
Reading the follow part of the code, you will se that if we are on mobile
screen, and expanded = false
, just the Burger button
will be rendered, and if expanded = true
the Sidebar
and outsideLayer
will be shown.
{(isMobile && !expanded) && this.renderBurger()}
<Column className={css(styles.container)}
breakpoints={{ 768: css(styles.containerMobile, expanded ? styles.show : styles.hide) }}>
...
</Column>
{isMobile && expanded && <div className={css(styles.outsideLayer)} onClick={this.toggleMenu}></div>}
These are the new styles applied to SidebarComponent.js
, check that on mobile
the position of the container
will be absolute
to overlay
the mainBlock
and fill the whole screen. When expanded = false
it will be shifted to the left, out of the screen (left: -255px
), and when expanded = true
it will be shown, shifted to the original position (left: 0px
). You can also see the transition
property to make a smooth display of the element. outsideLayer
will fill the entire screen but will be placed behind the Sidebar
(see zIndex
):
burgerIcon: {
cursor: 'pointer',
position: 'absolute',
left: 24,
top: 34
},
container: {
backgroundColor: '#363740',
width: 255,
paddingTop: 32,
height: 'calc(100% - 32px)'
},
containerMobile: {
transition: 'left 0.5s, right 0.5s',
position: 'absolute',
width: 255,
height: 'calc(100% - 32px)',
zIndex: 901
},
mainContainer: {
height: '100%',
minHeight: '100vh'
},
mainContainerMobile: {
position: 'absolute',
width: '100vw',
minWidth: '100%',
top: 0,
left: 0
},
outsideLayer: {
position: 'absolute',
width: '100vw',
minWidth: '100%',
height: '100%',
backgroundColor: 'rgba(0,0,0,.50)',
zIndex: 900
},
hide: {
left: -255
},
show: {
left: 0
}
View the changes: SidebarComponent.js
View full file: SidebarComponent.js
I have changed the container
styles so that it fills all the full height of the screen:
container: {
height: '100%',
minHeight: '100vh'
}
and I've included an event to re-render the full application at each resize
:
componentDidMount() {
window.addEventListener('resize', this.resize);
}
componentWillUnmount() {
window.removeEventListener('resize', this.resize);
}
resize = () => this.forceUpdate();
Now we will see how to make the content that appears in the design. We want to make it responsive, and we want it to look like this:
As I said before, we don't have a responsive design to follow, so we will keep it simple, a main breakpoint at 768px. We can identify 3 main sections in the content:
1- Row of MiniCards
2- Today's trends (graph + stats)
3- Row with 2 cards:
4- Unresolved tickets
5- Tasks
This is a simple Component, just a Column with a title
and value
. The content of the column has to be centered.
function MiniCardComponent({ className = '', title, value }) {
const composedClassName = `${css(styles.container)} ${className}`; // we could receive some styles from props
return (
<Column flexGrow={1} className={composedClassName} horizontal="center" vertical="center">
<span className={css(styles.title)}>{title}</span>
<span className={css(styles.value)}>{value}</span>
</Column>
);
}
styles:
container: {
backgroundColor: '#FFFFFF',
border: '1px solid #DFE0EB',
borderRadius: 4,
cursor: 'pointer',
height: 70,
maxWidth: 350,
marginRight: 30,
padding: '24px 32px 24px 32px',
':hover': {
borderColor: '#3751FF',
':nth-child(n) > span': {
color: '#3751FF'
}
}
},
title: {
color: '#9FA2B4',
fontFamily: 'Muli',
fontStyle: 'normal',
fontWeight: 'bold',
fontSize: 19,
lineHeight: '24px',
letterSpacing: '0.4px',
marginBottom: 12,
minWidth: 102,
textAlign: 'center'
},
value: {
color: '#252733',
fontFamily: 'Muli',
fontStyle: 'normal',
fontWeight: 'bold',
fontSize: 40,
letterSpacing: '1px',
lineHeight: '50px',
textAlign: 'center'
}
Pay attention to the container
styles, on :hover
we want to change the borderColor
and fontColor
of title
and value
, but, by default aphrodite
sets the styles as !important
so, we cannot change the styles of the children (title
, value
) from their parent (container
). To be able to do that we have to import aphrodite
in a differnet way than we normally do.
import { StyleSheet, css } from 'aphrodite/no-important';
Now we can overwrite the styles of the children
from their parents
.
View full file: MiniCardComponent.js
This component is a Row
with the following structure:
1- Column
3- Row ({ horizontal: space-between })
5- Column with title and subtitle
6- legend
4- Chart
2- Column: list of stats
It can be defined as follows:
<Row flexGrow={1} className={css(styles.container)}
horizontal="center" breakpoints={{ 1024: 'column' }}>
<Column wrap flexGrow={7} flexBasis="735px" className={css(styles.graphSection)}
breakpoints={{ 1024: { width: 'calc(100% - 48px)', flexBasis: 'auto' } }}>
<Row wrap horizontal="space-between">
<Column>
<span className={css(styles.graphTitle)}>Today’s trends</span>
<span className={css(styles.graphSubtitle)}>as of 25 May 2019, 09:41 PM</span>
</Column>
{this.renderLegend('#3751FF', 'Today')}
</Row>
<div className={css(styles.graphContainer)}>
<LineChart
data={data}
viewBoxWidth={500}
pointsStrokeColor="#3751FF"
areaColor="#3751FF"
areaVisible={true}
/>
</div>
</Column>
<Column className={css(styles.separator)} breakpoints={{ 1024: { display: 'none' } }}><div /></Column>
<Column flexGrow={3} flexBasis="342px" breakpoints={{ 1024: css(styles.stats) }}>
{this.renderStat('Resolved', '449')}
{this.renderStat('Received', '426')}
{this.renderStat('Average first response time', '33m')}
{this.renderStat('Average response time', '3h 8m')}
{this.renderStat('Resolution within SLA', '94%')}
</Column>
</Row>
where renderLegend
and renderStat
are defined as follows:
renderLegend(color, title) {
return (<Row vertical="center">
<div style={{ width: 16, border: '2px solid', borderColor: color }}></div>
<span className={css(styles.legendTitle)}>{title}</span>
</Row>);
}
renderStat(title, value) {
return (<Column flexGrow={1} className={css(styles.statContainer)} vertical="center" horizontal="center">
<span className={css(styles.statTitle)}>{title}</span>
<span className={css(styles.statValue)}>{value}</span>
</Column>);
}
for the chart
I have used react-svg-line-chart
, to install it, just type:
yarn add react-svg-line-chart
and these are the styles:
container: {
backgroundColor: '#FFFFFF',
border: '1px solid #DFE0EB',
borderRadius: 4,
cursor: 'pointer'
},
graphContainer: {
marginTop: 24,
marginLeft: 0,
marginRight: 0,
width: '100%'
},
graphSection: {
padding: 24
},
graphSubtitle: {
fontFamily: 'Muli',
fontStyle: 'normal',
fontWeight: 'normal',
fontSize: 12,
lineHeight: '16px',
letterSpacing: '0.1px',
color: '#9FA2B4',
marginTop: 4,
marginRight: 8
},
graphTitle: {
fontFamily: 'Muli',
fontStyle: 'normal',
fontWeight: 'bold',
fontSize: 19,
lineHeight: '24px',
letterSpacing: '0.4px',
color: '#252733'
},
legendTitle: {
fontFamily: 'Muli',
fontStyle: 'normal',
fontWeight: '600',
fontSize: 12,
lineHeight: '15px',
letterSpacing: '0.1px',
color: '#9FA2B4',
marginLeft: 8
},
separator: {
backgroundColor: '#DFE0EB',
width: 1,
minWidth: 1,
},
statContainer: {
borderBottom: '1px solid #DFE0EB',
padding: '24px 32px 24px 32px',
height: 'calc(114px - 48px)',
':last-child': {
border: 'none'
}
},
stats: {
borderTop: '1px solid #DFE0EB',
width: '100%'
},
statTitle: {
fontFamily: 'Muli',
fontStyle: 'normal',
fontWeight: '600',
fontSize: 16,
lineHeight: '22px',
letterSpacing: '0.3px',
textAlign: 'center',
color: '#9FA2B4',
whiteSpace: 'nowrap',
marginBottom: 6
},
statValue: {
fontFamily: 'Muli',
fontStyle: 'normal',
fontWeight: 'bold',
fontSize: 24,
lineHeight: '30px',
letterSpacing: '0.3px',
textAlign: 'center',
color: '#252733'
}
Notice that container
will become a column
when window.innerWidth <= 1024
, so the stats
column will be stacked under the graph
. At same size the separator
will disappear, and graph
and stats
will fill the whole width.
Pay attention to statContainer
style, where we are setting borders for every child
except for the last.
View full file: TodayTrendsComponent.js
As I said before, the 3rd section of the content
page is a Row
with 2 component. These components have many similar characteristics, so we can abstract the design like this:
1- Container (column)
2- Row:
3- Column: title and subtitle (received by props)
4- Link (view details or view all)
5- List of items (received by props)
Code:
<Column flexGrow={1} className={css(styles.container, containerStyles)} breakpoints={{ 426: css(styles.containerMobile) }}>
<Row horizontal="space-between">
<Column>
<span className={css(styles.title)}>{title}</span>
<Row style={{ marginTop: 8, marginBottom: 16 }}>
<span className={css(styles.subtitle)}>{subtitle}</span>
{subtitleTwo && <span className={css(styles.subtitle, styles.subtitle2)}>{subtitleTwo}</span>}
</Row>
</Column>
<span className={css(styles.link)}>{link}</span>
</Row>
{items.map(this.renderItem)}
</Column>
renderItem
:
renderItem(item, index) {
return (<Column flexGrow={1} className={css(styles.itemContainer)} key={`item-${index}`}
breakpoints={{ 426: css(styles.itemContainerMobile) }}>
{item}
</Column>);
}
styles:
container: {
backgroundColor: '#FFFFFF',
border: '1px solid #DFE0EB',
borderRadius: 4,
padding: '24px 32px 12px 32px'
},
containerMobile: {
padding: '12px 16px 6px 16px !important'
},
itemContainer: {
marginLeft: -32,
marginRight: -32,
paddingLeft: 32,
paddingRight: 32,
paddingBottom: 18,
paddingTop: 18,
maxHeight: 22,
borderBottom: '1px solid #DFE0EB',
':last-child': {
borderBottom: 'none'
}
},
itemContainerMobile: {
marginLeft: -16,
marginRight: -16,
paddingLeft: 16,
paddingRight: 16
},
link: {
fontFamily: 'Muli',
fontStyle: 'normal',
fontWeight: '600',
fontSize: 14,
lineHeight: '20px',
letterSpacing: '0.2px',
color: '#3751FF',
textAlign: 'right',
cursor: 'pointer'
},
subtitle: {
fontFamily: 'Muli',
fontStyle: 'normal',
fontWeight: 'normal',
fontSize: 12,
lineHeight: '16px',
letterSpacing: '0.1px',
color: '#9FA2B4'
},
subtitle2: {
color: '#252733',
marginLeft: 2
},
title: {
fontFamily: 'Muli',
fontStyle: 'normal',
fontWeight: 'bold',
fontSize: 19,
lineHeight: '24px',
letterSpacing: '0.4px',
color: '#252733'
}
See in itemContainer
that all items will have border
except the last one.
View full file: CardComponent.js
This component will be done based on CardComponent
, it will look like this:
<CardComponent containerStyles={this.props.containerStyles} title="Unresolved tickets"
link="View details" subtitle="Group:" subtitleTwo="Support"
items={[
this.renderStat('Waiting on Feature Request', 4238),
this.renderStat('Awaiting Customer Response', 1005),
this.renderStat('Awaiting Developer Fix', 914),
this.renderStat('Pending', 281)
]}
/>
where renderStat
is:
renderStat(title, value) {
return (<Row flexGrow={1} horizontal="space-between" vertical="center">
<span className={css(styles.itemTitle)}>{title}</span>
<span className={css(styles.itemTitle, styles.itemValue)}>{value}</span>
</Row>);
}
We need styles only for the title
and value
, all the others are set in CardComponent
:
itemTitle: {
fontFamily: 'Muli',
fontStyle: 'normal',
fontWeight: '600',
fontSize: 14,
lineHeight: '20px',
letterSpacing: '0.2px',
color: '#252733'
},
itemValue: {
color: '#9FA2B4'
}
View full file: UnresolvedTicketsComponent.js
As UnresolvedTicketsComponent
, this component will be done based on CardComponent
, it will look like this:
<CardComponent containerStyles={this.props.containerStyles} title="Tasks" link="View all" subtitle="Today"
items={[
<Row horizontal="space-between" vertical="center">
<span className={css(styles.itemTitle, styles.greyTitle)}>Create new task</span>
{this.renderAddButton()}
</Row>,
...this.state.items.map(this.renderTask)
]}
/>
As you can see, the first item looks different from the others, it has a gray title
and the add button
. The rest of the items are stored in the state:
state = { items: [
{title: 'Finish ticket update', checked: false, tag: TAGS.URGENT },
{title: 'Create new ticket example', checked: false, tag: TAGS.NEW },
{title: 'Update ticket report', checked: true, tag: TAGS.DEFAULT }
]};
These are the possible TAGS:
const TAGS = {
URGENT: { text: 'URGENT', backgroundColor: '#FEC400', color: '#FFFFFF' },
NEW: { text: 'NEW', backgroundColor: '#29CC97', color: '#FFFFFF' },
DEFAULT: { text: 'DEFAULT', backgroundColor: '#F0F1F7', color: '#9FA2B4' },
}
and this is the renderTask
function that will use other 2 functions: renderTag
and renderCheckbox
:
renderTask = ({title, tag = {} }, index) => (
<Row horizontal="space-between" vertical="center">
<Row>
{this.renderCheckbox(index)}
<span className={css(styles.itemTitle)}>{title}</span>
</Row>
{this.renderTag(tag, index)}
</Row>
);
renderTag = ({ text, backgroundColor, color }, index) => (
<Row horizontal="center" vertical="center" style={{ backgroundColor, color }}
className={css(styles.tagStyles)}>
{text}
</Row>
);
renderCheckbox = (index) => <div className={css(styles.checkboxWrapper)}>
{this.state.items[index].checked ? <CheckboxOn /> : <CheckboxOff />}
</div>;
For the checkbox
we are using two new icons that you can copy from here: checkbox-on and checkbox-off,
These are the styles:
addButton: {
backgroundColor: '#F0F1F7',
color: '#9FA2B4',
fontSize: 20,
padding: 7
},
itemTitle: {
color: '#252733',
fontFamily: 'Muli',
fontStyle: 'normal',
fontWeight: '600',
fontSize: 14,
letterSpacing: '0.2px',
lineHeight: '20px'
},
itemValue: {
color: '#9FA2B4'
},
greyTitle: {
color: '#C5C7CD'
},
tagStyles: {
borderRadius: 5,
cursor: 'pointer',
fontFamily: 'Muli',
fontStyle: 'normal',
fontWeight: 'bold',
fontSize: 11,
letterSpacing: '0.5px',
lineHeight: '14px',
padding: '5px 12px 5px 12px'
},
checkboxWrapper: {
cursor: 'pointer',
marginRight: 16
}
You can see in the repository code that I added some events to do this component interactive.
View full file: TasksComponent.js
Now we have to combine these components into one. As I said before is a Column
with 3 sections:
1- Row of MiniCardComponent
2- TodayTrendsComponent
3- Row with 2 components:
4- UnresolvedTicketsComponent
5- TasksComponent
The first section is probably the most complex, because we have to combine some styles. We have 4 cards
, and we always want the same number of cards in each row regardless of the width of the screen. That is:
- 4 cards in a row, or
- 2 cards in each row, in two different rows, or
- 1 card in each row, in four different rows
but we don't want something like this:
I think it's a good idea if we group them into pairs in this way:
<Row className={css(styles.cardsContainer)} wrap flexGrow={1} horizontal="space-between" breakpoints={{ 768: 'column' }}>
<Row className={css(styles.cardRow)} wrap flexGrow={1} horizontal="space-between" breakpoints={{ 384: 'column' }}>
<MiniCardComponent className={css(styles.miniCardContainer)} title="Unresolved" value="60" />
<MiniCardComponent className={css(styles.miniCardContainer)} title="Overdue" value="16" />
</Row>
<Row className={css(styles.cardRow)} wrap flexGrow={1} horizontal="space-between" breakpoints={{ 384: 'column' }}>
<MiniCardComponent className={css(styles.miniCardContainer)} title="Open" value="43" />
<MiniCardComponent className={css(styles.miniCardContainer)} title="On hold" value="64" />
</Row>
</Row>
so, when the main row
is wider than the container, it will be divided into two new rows
, and so on.
For TodayTrendsComponent
is easy, we just need to wrap it in a div
to apply some margins.
<div className={css(styles.todayTrends)}>
<TodayTrendsComponent />
</div>
and the last section is a row
with UnresolvedTicketsComponent
and TasksComponent
that will become a column
when window.innerWidth <= 1024
,
<Row horizontal="space-between" className={css(styles.lastRow)} breakpoints={{ 1024: 'column' }}>
<UnresolvedTicketsComponent containerStyles={styles.unresolvedTickets} />
<TasksComponent containerStyles={styles.tasks} />
</Row>
here is the full code:
<Column>
<Row className={css(styles.cardsContainer)} wrap flexGrow={1} horizontal="space-between" breakpoints={{ 768: 'column' }}>
<Row className={css(styles.cardRow)} wrap flexGrow={1} horizontal="space-between" breakpoints={{ 384: 'column' }}>
<MiniCardComponent className={css(styles.miniCardContainer)} title="Unresolved" value="60" />
<MiniCardComponent className={css(styles.miniCardContainer)} title="Overdue" value="16" />
</Row>
<Row className={css(styles.cardRow)} wrap flexGrow={1} horizontal="space-between" breakpoints={{ 384: 'column' }}>
<MiniCardComponent className={css(styles.miniCardContainer)} title="Open" value="43" />
<MiniCardComponent className={css(styles.miniCardContainer)} title="On hold" value="64" />
</Row>
</Row>
<div className={css(styles.todayTrends)}>
<TodayTrendsComponent />
</div>
<Row horizontal="space-between" className={css(styles.lastRow)} breakpoints={{ 1024: 'column' }}>
<UnresolvedTicketsComponent containerStyles={styles.unresolvedTickets} />
<TasksComponent containerStyles={styles.tasks} />
</Row>
</Column>
styles:
cardsContainer: {
marginRight: -30,
marginTop: -30
},
cardRow: {
marginTop: 30,
'@media (max-width: 768px)': {
marginTop: 0
}
},
miniCardContainer: {
flexGrow: 1,
marginRight: 30,
'@media (max-width: 768px)': {
marginTop: 30,
maxWidth: 'none'
}
},
todayTrends: {
marginTop: 30
},
lastRow: {
marginTop: 30
},
unresolvedTickets: {
marginRight: 30,
'@media (max-width: 1024px)': {
marginRight: 0
}
},
tasks: {
marginTop: 0,
'@media (max-width: 1024px)': {
marginTop: 30,
}
}
pay attention to the negative margins of cardsContainer
as they will absorb the excess margins of the elements that are located on the edges, to avoid this kind of things:
View full file: ContentComponent.js
To finish we have to include the ContentComponent
in our MainComponent
<Row className={css(styles.container)}>
<SidebarComponent selectedItem={selectedItem} onChange={(selectedItem) => this.setState({ selectedItem })} />
<Column flexGrow={1} className={css(styles.mainBlock)}>
<HeaderComponent title={selectedItem} />
<div className={css(styles.content)}>
<ContentComponent />
</div>
</Column>
</Row>
We have to include a change to our Sidebar
, because at the moment, a transparent layer is filling all the screen on mobile so we cannot click any element.
We are applying these styles to the mainContainerMobile
:
mainContainerMobile: {
...
width: '100%',
minWidth: '100vh',
}
but we want those styles only when the Sidebar
is expanded
, so we will apply these changes to our component:
<div style={{ position: 'relative' }}>
<Row className={css(styles.mainContainer)}
breakpoints={{ 768: css(styles.mainContainerMobile, expanded && styles.mainContainerExpanded) }}>
...
</Row>
</div>
styles:
...
mainContainerMobile: {
position: 'absolute',
top: 0,
left: 0
},
mainContainerExpanded: {
width: '100%',
minWidth: '100vh',
}
...
View the changes: SidebarComponent.js
View full file: SidebarComponent.js
This software is released under the MIT License.