Preview: http://dashboards.webkom.co/react/airframe
This commit is contained in:
Tomasz Owczarczyk
2019-08-15 00:54:44 +02:00
parent f975443095
commit 37092d1d6c
626 changed files with 56691 additions and 0 deletions

19
.babelrc Executable file
View File

@@ -0,0 +1,19 @@
{
"presets": [
"@babel/preset-env",
"@babel/preset-react",
],
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-syntax-dynamic-import",
"universal-import",
"react-hot-loader/babel"
],
"env": {
"development": {
"plugins": [
"react-hot-loader/babel"
]
}
}
}

26
.eslintrc Executable file
View File

@@ -0,0 +1,26 @@
{
"parser": "babel-eslint",
"plugins": [
"react"
],
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true,
"experimentalObjectRestSpread": true
}
},
"env": {
"es6": true,
"browser": true,
"node": true,
"mocha": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended"
],
"rules": {
}
}

10
.gitignore vendored Executable file
View File

@@ -0,0 +1,10 @@
# build output
dist
# dependencies
node_modules
package-lock.json
yarn.lock
# other
.DS_Store

1
.npmrc Executable file
View File

@@ -0,0 +1 @@
//registry.npmjs.org/:_authToken=57c97b55-1617-4b16-9f1b-2bffb2cdcc7e

14
app/colors.js Executable file
View File

@@ -0,0 +1,14 @@
import _ from 'lodash';
import colors from './colors.scss';
const colorKeys = _
.chain(colors)
.keys()
.filter((colorKey) => (
colorKey.indexOf('bg-') === -1 &&
colorKey.indexOf('fg-') === -1
))
.value();
export default _.pick(colors, colorKeys);

16
app/colors.scss Executable file
View File

@@ -0,0 +1,16 @@
@import "./styles/variables";
@each $name, $color in $dashboard-colors {
.fg-color--#{ $name } {
color: $color;
}
.bg-color--#{ $name } {
background-color: $color;
}
}
:export {
@each $name, $color in $dashboard-colors {
#{$name}: $color
}
}

3
app/common.js Executable file
View File

@@ -0,0 +1,3 @@
import * as CommonDashboardFuncs from '@owczar/dashboard-style--airframe';
export default CommonDashboardFuncs;

View File

@@ -0,0 +1,62 @@
import React from 'react';
import PropTypes from 'prop-types';
import Card from './../Card';
import { Provider } from './context';
export class Accordion extends React.Component {
static propTypes = {
initialOpen: PropTypes.bool,
onToggle: PropTypes.func,
open: PropTypes.bool,
children: PropTypes.node,
className: PropTypes.string
};
constructor(props) {
super(props);
this.state = {
isOpen: props.initialOpen
}
if (props.open !== 'undefined' && props.onToggle === 'undefined') {
throw "Accordion: props.open has to be used combined with props.onToggle " +
"use props.initialOpen to create an uncontrolled Accordion."
}
}
toggleHandler() {
const { onToggle } = this.props;
if (!onToggle) {
this.setState({ isOpen: !this.state.isOpen });
} else {
this.onToggle(!this.props.open);
}
}
isOpen() {
return !this.props.onToggle ?
this.state.isOpen : this.props.open;
}
render() {
/* eslint-disable-next-line no-unused-vars */
const { className, children, initialOpen, ...otherProps } = this.props;
return (
<Provider
value={{
onToggle: this.toggleHandler.bind(this),
isOpen: this.isOpen()
}}
>
<Card className={ className } { ...otherProps }>
{ children }
</Card>
</Provider>
);
}
}

View File

@@ -0,0 +1,24 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Collapse, CardBody } from 'reactstrap';
import { Consumer } from './context';
export const AccordionBody = (props) => (
<Consumer>
{
({ isOpen }) => (
<Collapse isOpen={ isOpen }>
<CardBody className={ classNames(props.className, 'pt-0') }>
{ props.children }
</CardBody>
</Collapse>
)
}
</Consumer>
);
AccordionBody.propTypes = {
children: PropTypes.node,
className: PropTypes.string
};

View File

@@ -0,0 +1,33 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import CardHeader from './../CardHeader';
import { Consumer } from './context';
import classes from './AccordionHeader.scss';
export const AccordionHeader = (props) => (
<Consumer>
{
({ onToggle }) => (
<CardHeader
className={
classNames(
props.className,
classes.header
)
}
onClick={ onToggle}
>
{ props.children }
</CardHeader>
)
}
</Consumer>
);
AccordionHeader.propTypes = {
children: PropTypes.node,
onClick: PropTypes.func,
className: PropTypes.string
};

View File

@@ -0,0 +1,14 @@
@import '../../styles/variables';
div.header {
background: none;
border-bottom: none;
cursor: pointer;
color: $link-color;
text-decoration: $link-decoration;
&:hover {
color: $link-hover-color;
text-decoration: $link-hover-decoration;
}
}

View File

@@ -0,0 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Consumer } from './context';
export const AccordionIndicator = (props) => (
<Consumer>
{
({ isOpen }) => isOpen ?
React.cloneElement(props.open, {
className: classNames(props.className, props.open.props.className)
}) : React.cloneElement(props.closed, {
className: classNames(props.className, props.closed.props.className)
})
}
</Consumer>
);
AccordionIndicator.propTypes = {
open: PropTypes.node,
closed: PropTypes.node,
className: PropTypes.string
}
AccordionIndicator.defaultProps = {
open: <i className="fa fa-fw fa-minus"></i>,
closed: <i className="fa fa-fw fa-plus"></i>
}

View File

@@ -0,0 +1,8 @@
import React from 'react';
const { Provider, Consumer } = React.createContext();
export {
Provider,
Consumer
};

View File

@@ -0,0 +1,10 @@
import { Accordion } from './Accordion';
import { AccordionBody } from './AccordionBody';
import { AccordionHeader } from './AccordionHeader';
import { AccordionIndicator } from './AccordionIndicator';
Accordion.Body = AccordionBody;
Accordion.Header = AccordionHeader;
Accordion.Indicator = AccordionIndicator;
export default Accordion;

20
app/components/App/AppClient.js Executable file
View File

@@ -0,0 +1,20 @@
import React from 'react';
import { hot } from 'react-hot-loader'
import { BrowserRouter as Router } from 'react-router-dom';
import AppLayout from './../../layout/default';
import { RoutedContent } from './../../routes';
const basePath = process.env.BASE_PATH || '/';
const AppClient = () => {
return (
<Router basename={ basePath }>
<AppLayout>
<RoutedContent />
</AppLayout>
</Router>
);
}
export default hot(module)(AppClient);

3
app/components/App/index.js Executable file
View File

@@ -0,0 +1,3 @@
import AppClient from './AppClient';
export default AppClient;

71
app/components/Avatar/Avatar.js Executable file
View File

@@ -0,0 +1,71 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import _ from 'lodash';
const Avatar = (props) => {
const avatarClass = classNames(
'avatar',
`avatar--${ props.size }`,
props.className
);
const addOnsdArr = React.Children.toArray(props.addOns);
const badge = _.find(addOnsdArr, (avatarAddOn) =>
avatarAddOn.type.addOnId === "avatar--badge");
const icons = _.filter(addOnsdArr, (avatarAddOn) =>
avatarAddOn.type.addOnId === "avatar--icon");
const isNested = _.reduce(addOnsdArr, (acc, avatarAddOn) =>
acc || !!avatarAddOn.props.small, false);
return (
<div className={ avatarClass } style={ props.style }>
{
badge && (
<div className="avatar__badge">
{ badge }
</div>
)
}
{
!_.isEmpty(icons) && (() => {
switch(icons.length) {
case 1:
return (
<div className="avatar__icon">
{ _.first(icons) }
</div>
)
default:
return (
<div
className={
classNames({
'avatar__icon--nested': isNested,
}, 'avatar__icon', 'avatar__icon--stack')
}
>
{ icons }
</div>
)
}
})()
}
<div className='avatar__content'>
{ props.children }
</div>
</div>
);
};
Avatar.propTypes = {
size: PropTypes.string,
children: PropTypes.node.isRequired,
addOns: PropTypes.node,
style: PropTypes.object,
className: PropTypes.string
};
Avatar.defaultProps = {
size: "md",
style: {}
};
export { Avatar };

View File

@@ -0,0 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Badge } from 'reactstrap';
const AvatarAddonBadge = (props) => {
const { children, ...badgeProps } = props;
return (
<Badge {...badgeProps}>
{ children }
</Badge>
);
};
AvatarAddonBadge.propTypes = {
children: PropTypes.node,
className: PropTypes.string
};
AvatarAddonBadge.addOnId = "avatar--badge";
export { AvatarAddonBadge };

View File

@@ -0,0 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import avatarColors from './../../colors.scss';
const AvatarAddonIcon = (props) => {
const addOnClass = classNames({
'avatar__icon__inner': props.small
}, avatarColors[`fg-color--${ props.color }`]);
return (
<i className={ classNames(addOnClass, props.className) }></i>
);
};
AvatarAddonIcon.propTypes = {
small: PropTypes.bool,
className: PropTypes.string,
color: PropTypes.string
};
AvatarAddonIcon.defaultProps = {
color: "success"
};
AvatarAddonIcon.addOnId = "avatar--icon";
export { AvatarAddonIcon };

View File

@@ -0,0 +1,65 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Avatar } from './Avatar';
import avatarColors from './../../colors.scss';
const AvatarFont = (props) => {
const {
children,
bgColor,
fgColor,
bgColorCustom,
fgColorCustom,
...avatarProps
} = props;
const parentClass = classNames(
'avatar-font',
`avatar-font--${avatarProps.size}`,
bgColor && avatarColors[`bg-color--${ bgColor }`]
);
const childClass = classNames('avatar-font__text',
fgColor && avatarColors[`fg-color--${ fgColor }`]
);
const parentCustomStyle = bgColorCustom ? {
backgroundColor: bgColorCustom
} : { };
const childCustomStyle = fgColorCustom ? {
color: fgColorCustom
} : { };
const child = (
<span>
{ children }
</span>
);
return (
<Avatar { ...avatarProps }>
<div className={ parentClass } style={parentCustomStyle}>
{
React.cloneElement(child, {
style: childCustomStyle,
className: classNames(child.props.className, childClass)
})
}
</div>
</Avatar>
);
};
AvatarFont.propTypes = {
children: PropTypes.node,
bgColor: PropTypes.string,
fgColor: PropTypes.string,
bgColorCustom: PropTypes.string,
fgColorCustom: PropTypes.string,
...Avatar.propTypes
};
AvatarFont.defaultProps = {
bgColor: '400',
fgColor: 'white',
size: 'md'
};
export { AvatarFont };

View File

@@ -0,0 +1,57 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import _ from 'lodash';
import { Avatar } from './Avatar';
import { AvatarFont } from './AvatarFont';
class AvatarImage extends React.PureComponent {
static propTypes = {
src: PropTypes.string.isRequired,
placeholder: PropTypes.node,
alt: PropTypes.string,
className: PropTypes.string,
..._.omit(Avatar.propTypes, ['children'])
};
static defaultProps = {
placeholder: <i className="fa fa-user fa-fw"></i>
}
constructor(props) {
super(props);
this.state = {
imgLoaded: false
};
}
render() {
const { src, placeholder, alt, className, ...avatarProps } = this.props;
const parentClass = classNames('avatar-image', {
'avatar-image--loaded': this.state.imgLoaded
}, className);
return (
<div className={ parentClass }>
<Avatar className="avatar-image__image" {...avatarProps}>
<img
src={ src }
alt={ alt }
onLoad={ () => { this.setState({ imgLoaded: true }) } }
/>
</Avatar>
{
!this.state.imgLoaded && (
<AvatarFont className="avatar-image__placeholder" {...avatarProps}>
{ placeholder }
</AvatarFont>
)
}
</div>
)
}
}
export { AvatarImage };

17
app/components/Avatar/index.js Executable file
View File

@@ -0,0 +1,17 @@
import { Avatar } from './Avatar';
import { AvatarFont } from './AvatarFont';
import { AvatarImage } from './AvatarImage';
import { AvatarAddonBadge } from './AvatarAddonBadge';
import { AvatarAddonIcon } from './AvatarAddonIcon';
Avatar.Font = AvatarFont;
Avatar.Image = AvatarImage;
const AvatarAddOn = {
Icon: AvatarAddonIcon,
Badge: AvatarAddonBadge
};
export default Avatar;
export { AvatarAddOn };

33
app/components/Card/Card.js Executable file
View File

@@ -0,0 +1,33 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import {
Card as BsCard
} from 'reactstrap';
import classes from './Card.scss';
const Card = (props) => {
const { children, type, color, className, ...otherProps } = props;
const cardClass = classNames(className,
classes['custom-card'],
classes[`custom-card--${ type }`],
color && classes[`custom-card--color-${ color }`]
);
return (
<BsCard className={ cardClass } { ...otherProps }>
{ children }
</BsCard>
);
}
Card.propTypes = {
...BsCard.propTypes,
type: PropTypes.string,
color: PropTypes.string
};
Card.defaultProps = {
type: 'border',
color: null
};
export { Card };

52
app/components/Card/Card.scss Executable file
View File

@@ -0,0 +1,52 @@
@import "../../styles/variables";
.custom-card {
&--border-dash,
&--border-dot,
&--border {
@each $name, $color in $dashboard-colors {
&.custom-card--color-#{$name} {
border-color: $color
}
}
}
&--side-border {
border-left-width: 2px;
@each $name, $color in $dashboard-colors {
&.custom-card--color-#{$name} {
border-left-color: $color
}
}
}
&--background {
color: map-get($dashboard-colors, 'white');
@each $name, $color in $dashboard-colors {
&.custom-card--color-#{$name} {
background-color: $color;
}
}
}
&--border-dash {
border-style: dashed;
border-width: 2px;
}
&--border-dot {
border-style: dotted;
border-width: 2px;
}
&--shadow {
box-shadow: 0 1px 2px 0 rgba(31, 45, 61, 0.07);
}
&--none {
border: none;
background: none;
}
}

3
app/components/Card/index.js Executable file
View File

@@ -0,0 +1,3 @@
import { Card } from './Card';
export default Card;

View File

@@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import {
CardHeader as BsCardHeader
} from 'reactstrap';
import classes from './CardHeader.scss';
const CardHeader = (props) => {
const { type, color, className, children, ...otherProps } = props;
const cardHeaderClass = classNames(className,
classes['custom-card-header'],
type && classes[`custom-card-header--${ type }`],
color && classes[`custom-card-header--color-${ color }`]
);
return (
<BsCardHeader className={ cardHeaderClass } { ...otherProps }>
{ children }
</BsCardHeader>
);
};
CardHeader.propTypes = {
type: PropTypes.string,
color: PropTypes.string,
className: PropTypes.string,
...BsCardHeader.propTypes
};
export { CardHeader };

View File

@@ -0,0 +1,24 @@
@import "../../styles/variables";
.custom-card-header {
&--background {
color: map-get($dashboard-colors, "white");
@each $name, $color in $dashboard-colors {
&.custom-card-header--color-#{$name} {
background-color: $color;
}
}
}
&--border {
@each $name, $color in $dashboard-colors {
border-bottom: 2px solid;
background: none;
&.custom-card-header--color-#{$name} {
border-bottom-color: $color;
}
}
}
}

View File

@@ -0,0 +1,3 @@
import { CardHeader } from './CardHeader';
export default CardHeader;

View File

@@ -0,0 +1,45 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Provider } from './context';
class Checkable extends React.Component {
static propTypes = {
children: PropTypes.node.isRequired,
tag: PropTypes.oneOfType([
PropTypes.func,
PropTypes.string
])
};
static defaultProps = {
tag: "div"
};
constructor(props) {
super(props);
this.state = {
isChecked: false
};
}
render() {
const { tag, children, ...otherProps } = this.props;
const Tag = this.props.tag;
return (
<Provider
value={{
isChecked: this.state.isChecked,
toggle: (enabled) => { this.setState({ isChecked: enabled || !this.state.isChecked }) }
}}
>
<Tag { ...otherProps }>
{ children }
</Tag>
</Provider>
);
}
}
export { Checkable };

View File

@@ -0,0 +1,53 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Input } from 'reactstrap';
import { Consumer } from './context';
class CheckableInput extends React.Component {
static propTypes = {
tag: PropTypes.oneOfType([
PropTypes.func,
PropTypes.string
]),
type: PropTypes.string,
defaultChecked: PropTypes.bool,
toggle: PropTypes.func,
isChecked: PropTypes.bool
};
static defaultProps = {
tag: Input,
type: "checkbox"
};
componentDidMount() {
if (this.props.defaultChecked) {
this.props.toggle(this.props.defaultChecked);
}
}
render() {
const { tag, isChecked, toggle, ...otherProps } = this.props;
const Tag = tag;
return (
<Tag
checked={ isChecked }
onChange={ (e) => { toggle(e.target.checked) } }
{ ...otherProps }
/>
);
}
}
const ContextCheckableInput = (props) => (
<Consumer>
{
(value) => (
<CheckableInput { ...{ ...props, ...value } } />
)
}
</Consumer>
);
export { ContextCheckableInput as CheckableInput };

View File

@@ -0,0 +1,38 @@
import React from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import { Consumer } from './context';
import classes from './CheckableTrigger.scss';
const CheckableTrigger = (props) => {
const { children, tag, className, ...otherProps } = props;
const Tag = tag;
const tagClass = classNames(classes['checkable__trigger'], className);
return (
<Consumer>
{
(value) => (
<Tag { ...otherProps } className={ tagClass } onClick={ () => { value.toggle() } }>
{ children }
</Tag>
)
}
</Consumer>
);
};
CheckableTrigger.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
tag: PropTypes.oneOfType([
PropTypes.func,
PropTypes.string
])
};
CheckableTrigger.defaultProps = {
tag: "div"
};
export { CheckableTrigger };

View File

@@ -0,0 +1,3 @@
.checkable__trigger {
cursor: pointer;
}

View File

@@ -0,0 +1,8 @@
import React from 'react';
const { Provider, Consumer } = React.createContext();
export {
Provider,
Consumer
};

View File

@@ -0,0 +1,8 @@
import { Checkable } from './Checkable';
import { CheckableTrigger } from './CheckableTrigger';
import { CheckableInput } from './CheckableInput';
Checkable.Trigger = CheckableTrigger;
Checkable.Input = CheckableInput;
export default Checkable;

View File

@@ -0,0 +1,17 @@
import React from 'react';
import classNames from 'classnames';
import { CustomInput as RSCustomInput } from 'reactstrap';
const CustomInput = (props) => {
const { className, ...otherProps } = props;
const inputClass = classNames(className, {
'custom-control-empty': !props.label
});
return (
<RSCustomInput className={ inputClass } { ...otherProps } />
);
}
CustomInput.propTypes = { ...RSCustomInput.propTypes };
export { CustomInput };

View File

@@ -0,0 +1,3 @@
import { CustomInput } from './CustomInput';
export default CustomInput;

View File

@@ -0,0 +1,22 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
export const Divider = ({ position, children, className }) => {
const dividerClass = classNames({
'hr-text-center': position === 'center',
'hr-text-right': position === 'right'
}, 'hr-text', className);
return (
<div className={ dividerClass }>
{ children }
</div>
)
}
Divider.propTypes = {
position: PropTypes.string,
className: PropTypes.string,
children: PropTypes.node
}

View File

@@ -0,0 +1,3 @@
import { Divider } from './Divider';
export default Divider;

View File

@@ -0,0 +1,47 @@
import React from 'react';
import _ from 'lodash';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import {
withPageConfig
} from './../../components/Layout';
class EmptyLayout extends React.Component {
static propTypes = {
pageConfig: PropTypes.object.isRequired,
children: PropTypes.node.isRequired,
className: PropTypes.string
};
componentDidMount() {
this.props.pageConfig.setElementsVisibility({
navbarHidden: true,
sidebarHidden: true,
footerHidden: true
});
}
componentWillUnmount() {
this.props.pageConfig.setElementsVisibility({
navbarHidden: false,
sidebarHidden: false,
footerHidden: false
});
}
render() {
const emptyLayoutClass = classNames('fullscreen', this.props.className);
return (
<div className={ emptyLayoutClass }>
{ this.props.children }
</div>
);
}
};
const PageConfigEmptyLayout = withPageConfig(EmptyLayout);
export {
PageConfigEmptyLayout as EmptyLayout
};

View File

@@ -0,0 +1,32 @@
import React from 'react';
import _ from 'lodash';
import PropTypes from 'prop-types';
import classNames from 'classnames';
const EmptyLayoutSection = (props) => {
const sectionClass = classNames(props.className, 'fullscreen__section', {
'fullscreen__section--center': props.center
});
const maxWidth = _.isNumber(props.width) ? `${props.width}px` : props.width;
return (
<div className={ sectionClass }>
{
props.center ?
<div className="fullscrenn__section__child" style={{ maxWidth }}>
{ props.children }
</div> : props.children
}
</div>
)
};
EmptyLayoutSection.propTypes = {
className: PropTypes.string,
children: PropTypes.node.isRequired,
center: PropTypes.bool,
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
};
EmptyLayoutSection.defaultProps = {
width: '420px'
}
export { EmptyLayoutSection };

View File

@@ -0,0 +1,6 @@
import { EmptyLayout } from './EmptyLayout';
import { EmptyLayoutSection } from './EmptyLayoutSection';
EmptyLayout.Section = EmptyLayoutSection;
export default EmptyLayout;

View File

@@ -0,0 +1,17 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { DropdownMenu } from 'reactstrap';
export const ExtendedDropdown = ({ className, ...otherProps }) => {
const classes = classNames(
className,
'extended-dropdown'
);
return (
<DropdownMenu className={ classes } { ...otherProps } />
);
}
ExtendedDropdown.propTypes = {
className: PropTypes.string,
};

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { DropdownContext } from 'reactstrap/es/DropdownContext';
const ExtendedDropdownLink = (props) => {
const { children, ...otherProps } = props;
return (
<DropdownContext.Consumer>
{
({ toggle }) => (
<Link { ...otherProps } onClick={ () => { toggle(); } }>
{ children }
</Link>
)
}
</DropdownContext.Consumer>
);
};
ExtendedDropdownLink.propTypes = { ...Link.propTypes };
ExtendedDropdownLink.defaultProps = { ...Link.defaultProps };
export { ExtendedDropdownLink };

View File

@@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
const ExtendedDropdownSection = (props) => {
const { children, list, className, tag, ...otherProps } = props;
const sectionClass = classNames(
"extended-dropdown__section", className, {
"extended-dropdown__section--list": list
}
);
const Tag = tag;
return (
<Tag className={ sectionClass } { ...otherProps }>
{ children }
</Tag>
);
};
ExtendedDropdownSection.propTypes = {
children: PropTypes.node,
list: PropTypes.bool,
className: PropTypes.string,
tag: PropTypes.oneOfType([PropTypes.string, PropTypes.func])
};
ExtendedDropdownSection.defaultProps = {
tag: "div"
};
export { ExtendedDropdownSection };

View File

@@ -0,0 +1,8 @@
import { ExtendedDropdown } from './ExtendedDropdown';
import { ExtendedDropdownSection } from './ExtendedDropdownSection';
import { ExtendedDropdownLink } from './ExtendedDropdownLink';
ExtendedDropdown.Section = ExtendedDropdownSection;
ExtendedDropdown.Link = ExtendedDropdownLink;
export default ExtendedDropdown;

89
app/components/FloatGrid/Col.js Executable file
View File

@@ -0,0 +1,89 @@
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import classNames from 'classnames';
import { Col as BootstrapCol } from 'reactstrap';
// Twice Smaller than Bootstrap breakpoints
const breakPoints = [
{ id: 'xl', min: 600 },
{ id: 'lg', min: 496, max: 600 },
{ id: 'md', min: 384, max: 496 },
{ id: 'sm', min: 288, max: 384 },
{ id: 'xs', max: 288 }
];
const getCurrentbreakPoint = (width, breakPoints) => {
let output = 'xl';
for (let bp of breakPoints) {
if (
(_.isUndefined(bp.min) || bp.min <= width) &&
(_.isUndefined(bp.max) || bp.max > width)
) {
output = bp.id;
}
}
return output;
};
export class Col extends React.Component {
static propTypes = {
active: PropTypes.bool,
lg: PropTypes.number,
md: PropTypes.number,
sm: PropTypes.number,
xs: PropTypes.number,
xl: PropTypes.number,
xlH: PropTypes.number,
lgH: PropTypes.number,
mdH: PropTypes.number,
smH: PropTypes.number,
xsH: PropTypes.number,
xlX: PropTypes.number,
lgX: PropTypes.number,
mdX: PropTypes.number,
smX: PropTypes.number,
xsX: PropTypes.number,
xlY: PropTypes.number,
lgY: PropTypes.number,
mdY: PropTypes.number,
smY: PropTypes.number,
xsY: PropTypes.number,
trueSize: PropTypes.object,
children: PropTypes.node,
className: PropTypes.string
}
static defaultProps = {
active: true
}
render() {
const { active, children, className, trueSize } = this.props;
const bsColumnProps = _.pick(this.props, ['xl', 'lg', 'md', 'sm', 'xs']);
const otherProps = _.omit(this.props, [..._.keys(Col.propTypes),
'minW', 'maxW', 'minH', 'maxH', 'moved', 'static', 'isDraggable', 'isResizable']);
const floatColBpId = trueSize ? getCurrentbreakPoint(trueSize.wPx, breakPoints) : 'xl';
const floatColClasses = classNames(className, 'float-col',
'float-column', `float-column--size-${floatColBpId}`);
return active ? (
<div { ...otherProps } className={ floatColClasses }>
{ children }
</div>
) : (
<BootstrapCol
{ ...(_.extend(bsColumnProps, otherProps)) }
className={ classNames(className, 'pb-3') }
>
{ children }
</BootstrapCol>
);
}
}

122
app/components/FloatGrid/Grid.js Executable file
View File

@@ -0,0 +1,122 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Container } from 'reactstrap';
import { FloatGridContext } from './floatGridContext';
import './../../styles/components/float-grid.scss';
export class Grid extends React.Component {
static propTypes = {
active: PropTypes.bool,
children: PropTypes.node,
fluid: PropTypes.bool,
rowHeight: PropTypes.number,
className: PropTypes.string
}
static defaultProps = {
active: true,
fluid: false,
rowHeight: 100
}
state = {
gridSize: { w: 0, h: 0 },
gridReady: false,
}
_gridRef = React.createRef();
_resizeDebounceTimeout = 0;
componentDidMount() {
this.setState({
gridSize: {
w: this._gridRef.current.clientWidth,
h: this._gridRef.current.clientHeight
}
});
if (typeof window !== 'undefined') {
window.addEventListener('resize', this._resizeHandler);
}
}
componentDidUpdate(nextProps) {
// HACK
if (
typeof window !== 'undefined' &&
nextProps.fluid !== this.props.fluid
) {
window.dispatchEvent(new Event('resize'));
}
}
componentWillUnmount() {
if (typeof window !== 'undefined') {
window.removeEventListener('resize', this._resizeHandler);
}
}
render() {
const { active, children, fluid, className, rowHeight, ...otherProps } = this.props;
const { gridSize } = this.state;
const modifiedChildren = React.Children.map(children, child => (
React.cloneElement(child, {
...otherProps,
active,
gridSize
})
));
const floatWrapClasses = classNames({
['float-grid-parent__static']: !fluid
}, className, 'float-grid-parent');
return(
<FloatGridContext.Provider
value={{
gridUnitsToPx: (w, h) => {
return {
wPx: w / 12 * gridSize.w,
hPx: h * rowHeight
}
},
active,
gridReady: this.state.gridReady,
setGridReady: () => { this.setState({ gridReady: true }) }
}}
>
{
active ? (
<div
className={ floatWrapClasses }
ref={ this._gridRef }
>
{ modifiedChildren }
</div>
) : (
<div ref={ this._gridRef }>
<Container fluid={ fluid } className={ className } { ...otherProps }>
{ modifiedChildren }
</Container>
</div>
)
}
</FloatGridContext.Provider>
);
}
_resizeHandler = () => {
clearInterval(this._resizeDebounceTimeout);
this._resizeDebounceTimeout = setTimeout(() => {
this.setState({
gridSize: {
w: this._gridRef.current.clientWidth,
h: this._gridRef.current.clientHeight
}
});
}, 1000 / 60 * 10); //Every 10 frames debounce
}
}

View File

@@ -0,0 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FloatGridContext } from './floatGridContext';
export const Ready = ({ children }) => (
<FloatGridContext.Consumer>
{
({ gridReady }) => gridReady ? children : null
}
</FloatGridContext.Consumer>
);
Ready.propTypes = {
children: PropTypes.node
}

207
app/components/FloatGrid/Row.js Executable file
View File

@@ -0,0 +1,207 @@
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import {
WidthProvider,
Responsive
} from 'react-grid-layout';
import { Row as BSRow } from 'reactstrap';
import { FloatGridContext } from './floatGridContext';
const ResponsiveGrid = WidthProvider(Responsive);
const responsiveBreakpoints = {
xl: 1139, lg: 959, md: 719, sm: 539, xs: 0
//xl: Number.MAX_VALUE, lg: 1199, md: 991, sm: 767, xs: 576
};
const breakPointSteps = _.keys(responsiveBreakpoints);
const TOTAL_ROW = 12;
const simplifyChildrenArray = (reactChildren) => _.map(reactChildren, child => (
{ ...child, key: child.key.replace(/\.\$/g, '') }
));
export class Row extends React.Component {
static propTypes = {
children: PropTypes.node,
columns: PropTypes.object,
onLayoutChange: PropTypes.func,
rowHeight: PropTypes.number,
gridSize: PropTypes.object
};
static contextType = FloatGridContext;
_lastLayouts = { };
state = {
trueColSizes: { },
activeLayout: 'xl'
}
initialDebounceTimeout = false;
componentDidUpdate(nextProps) {
if (!_.isEqual(nextProps.gridSize, this.props.gridSize)) {
if (!_.isEmpty(this._lastLayouts)) {
this._updateTrueColSizes(this._lastLayouts[this.state.activeLayout]);
}
}
}
render() {
const { children, rowHeight, onLayoutChange, ...otherProps } = this.props;
const { trueColSizes } = this.state;
if (this.context.active) {
const layouts = this._lastLayouts = this._calculateLayouts(children);
const adjustedChildren = simplifyChildrenArray(
React.Children.map(children, (child) =>
React.cloneElement(child, { trueSize: trueColSizes[child.props.i] })));
return (
<ResponsiveGrid
cols={{ xl: 12, lg: 12, md: 12, sm: 12, xs: 12 }}
breakpoints={ responsiveBreakpoints }
layouts={ layouts }
padding={ [ 0, 0 ] }
margin={ [ 0, 0 ] }
rowHeight={ rowHeight }
onLayoutChange={(currentLayout, allLayouts) => {
// Notify the parent
onLayoutChange(this._transformForChangeHandler(allLayouts));
// Recalculate true sizes
this._updateTrueColSizes(currentLayout);
clearTimeout(this.initialDebounceTimeout);
this.initialDebounceTimeout = setTimeout(() => {
this.context.setGridReady();
}, 0);
}}
onBreakpointChange={(activeLayout) => {
this.setState({ activeLayout });
}}
onResize={
(layout, oldItem, newItem) => {
this.setState({
trueColSizes: {
...trueColSizes,
[newItem.i]: this.context.gridUnitsToPx(newItem.w, newItem.h)
}
});
}
}
{ ...otherProps }
>
{ adjustedChildren }
</ResponsiveGrid>
);
} else {
const adjustedChildren = React.Children.map(children, (child) =>
React.cloneElement(child, { active: false }));
return (
<BSRow>
{ adjustedChildren }
</BSRow>
);
}
}
_updateTrueColSizes = (layout) => {
const { trueColSizes } = this.state;
for (let child of layout) {
trueColSizes[child.i] = this.context.gridUnitsToPx(child.w, child.h);
}
this.setState({ trueColSizes });
}
/**
* Finds the nearest breakpoint relative to the one provided in the
* first param. For example - when the `definition` param contains
* such bps - { md: 6, xs: 8 }, for `breakpoint` - xl/md will return 6
*/
_findClosestBreakpoint = (breakpoint, definition) => {
let found = 12;
for (let bp of _.drop(breakPointSteps, _.indexOf(breakPointSteps, breakpoint))) {
if (!_.isUndefined(definition[bp])) {
found = definition[bp];
}
}
return found;
}
_calculateLayouts = (children) => {
let output = { };
const childrenArray = React.Children.toArray(children);
for (let breakPoint of breakPointSteps) {
let rowChildren = [];
let rowCounter = 0;
let y = 0;
for (let child of childrenArray) {
let bpData = { };
// Save the props for current child and breakpoint
const config = _.pick(child.props, [
'i',
'h', 'minH', 'maxH',
'minW', 'maxW',
breakPoint, `${breakPoint}MinW`, `${breakPoint}MaxW`,
'moved', 'static', 'isResizable', 'isDraggable'
]);
// Calculate the needed definition
bpData = Object.assign(bpData, {
...config,
// Add default heights when none provided
...{
// Set the x to the calculated value or take from the
// props if defined for controlled type
x: _.isUndefined(child.props[`${breakPoint}X`]) ?
rowCounter : child.props[`${breakPoint}X`],
h: child.props[`${breakPoint}H`] || config.h || 1,
minH: config.minH || (config.h || 1),
maxH: config.maxH || (config.h || 1),
},
w: config[breakPoint] ||
this._findClosestBreakpoint(breakPoint, child.props),
// Set the y to the calculated value or take from the
// props if defined for controlled type
y: _.isUndefined(child.props[`${breakPoint}Y`]) ?
y : child.props[`${breakPoint}Y`]
});
rowChildren = [...rowChildren, bpData];
rowCounter += bpData.w;
if (rowCounter + bpData.x > TOTAL_ROW) {
// Increase by the largest found height
y += _.max(_.map(rowChildren, 'h'));
rowCounter = 0;
rowChildren = [];
}
output = {
...output,
[breakPoint]: [...(output[breakPoint] || []), bpData]
}
}
}
return output;
}
/**
* Transform the calculated layout to a structure
* which is provided by `layouts` props
*/
_transformForChangeHandler = (layouts) => {
let output = { };
for (let breakPoint of breakPointSteps) {
const bpLayout = layouts[breakPoint];
for (let element of bpLayout) {
output[element.i] = {
...(output[element.i]),
...(element),
[breakPoint]: element.w,
[`${breakPoint}X`]: element.x,
[`${breakPoint}Y`]: element.y,
[`${breakPoint}H`]: element.h
}
}
}
return output;
}
}

View File

@@ -0,0 +1,3 @@
import React from 'react';
export const FloatGridContext = React.createContext();

View File

@@ -0,0 +1,16 @@
import { Col } from './Col';
import { Row } from './Row';
import { Grid } from './Grid';
import { Ready } from './Ready';
Grid.Col = Col;
Grid.Row = Row;
Grid.Ready = Ready;
export const applyColumn = (columnId, layouts) => ({
...layouts[columnId],
i: columnId,
key: columnId
});
export default Grid;

View File

@@ -0,0 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
import { HolderTextProvider } from './HolderTextProvider';
const HolderIconProvider = (props) => {
const { iconChar, children, ...otherProps } = props;
return (
<HolderTextProvider
font='FontAwesome'
text={ iconChar }
{ ...otherProps }
>
{ children }
</HolderTextProvider>
);
};
HolderIconProvider.propTypes = {
iconChar: PropTypes.string.isRequired,
children: PropTypes.node
};
export { HolderIconProvider };

View File

@@ -0,0 +1,92 @@
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import uid from 'uuid/v4';
import qs from 'query-string';
import colors from './../../colors';
class HolderTextProvider extends React.Component {
static propTypes = {
bg: PropTypes.string,
fg: PropTypes.string,
text: PropTypes.string,
width: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string
]),
height: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string
]),
font: PropTypes.string,
align: PropTypes.string,
outline: PropTypes.bool,
lineWrap: PropTypes.number,
children: PropTypes.node
}
static defaultProps = {
width: '100p',
height: 220,
bg: colors['200'],
fg: colors['500']
}
constructor(props) {
super(props);
this.domId = `holderjs--${uid()}`;
}
componentDidMount() {
this.initPlaceholder();
if (typeof window !== 'undefined') {
window.onload = this.initPlaceholder.bind(this);
}
}
componentDidUpdate() {
this.initPlaceholder();
}
initPlaceholder() {
if (
typeof window !== 'undefined' &&
typeof document !== 'undefined' &&
document.readyState === 'complete'
) {
const Holder = require('holderjs');
const domElement = document.getElementById(this.domId);
if (domElement) {
Holder.run({
domain: 'holder.js',
images: domElement,
object: null,
bgnodes: null,
stylenodes: null
})
return true;
}
}
return false;
}
render() {
const onlyChild = React.Children.only(this.props.children);
const phProps = _.omit(this.props, ['children', 'width', 'height']);
const phPropsQuery = qs.stringify(phProps);
return React.cloneElement(onlyChild, {
'id': this.domId,
'data-src': `holder.js/${this.props.width}x${this.props.height}?${phPropsQuery}`
});
}
}
export { HolderTextProvider };

View File

@@ -0,0 +1,10 @@
import { HolderTextProvider } from './HolderTextProvider';
import { HolderIconProvider } from './HolderIconProvider';
const HolderProvider = {
Text: HolderTextProvider,
Icon: HolderIconProvider
};
export default HolderProvider;

View File

@@ -0,0 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
const IconWithBadge = (props) => {
const { badge, children, className } = props;
const adjustedBadge = React.cloneElement(badge, {
className: classNames(
badge.props.className,
'icon-with-badge__badge'
)
});
const wrapClass = classNames(className, 'icon-with-badge');
return (
<div className={ wrapClass }>
{ children }
{ adjustedBadge }
</div>
);
};
IconWithBadge.propTypes = {
badge: PropTypes.node,
children: PropTypes.node,
className: PropTypes.string
};
export { IconWithBadge };

View File

@@ -0,0 +1,3 @@
import { IconWithBadge } from './IconWithBadge';
export default IconWithBadge;

View File

@@ -0,0 +1,33 @@
import React from 'react';
import _ from 'lodash';
import {
InputGroupAddon as BsInputGroupAddon
} from 'reactstrap';
const InputGroupAddon = (props) => {
const { children, ...otherProps } = props;
const childArr = React.Children.toArray(children);
const isFa = _.some(childArr, (child) =>
React.isValidElement(child) && child.props.className && _.includes(child.props.className, 'fa'));
const isCheckRadio = _.some(childArr, (child) =>
React.isValidElement(child) && (child.props.type === 'radio' || child.props.type === 'checkbox'));
let child = isFa || isCheckRadio ? (
<div className="input-group-text">
{ children }
</div>
) : children;
return (
<BsInputGroupAddon { ...otherProps }>
{ child }
</BsInputGroupAddon>
);
}
InputGroupAddon.propTypes = {
...BsInputGroupAddon.propTypes
};
InputGroupAddon.defaultProps = BsInputGroupAddon.defaultProps;
export { InputGroupAddon };

View File

@@ -0,0 +1,3 @@
import { InputGroupAddon } from './InputGroupAddon';
export default InputGroupAddon;

275
app/components/Layout/Layout.js Executable file
View File

@@ -0,0 +1,275 @@
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Helmet } from 'react-helmet';
import { withRouter } from 'react-router-dom';
import _ from 'lodash';
import { LayoutContent } from './LayoutContent';
import { LayoutNavbar } from './LayoutNavbar';
import { LayoutSidebar } from './LayoutSidebar';
import { PageConfigContext } from './PageConfigContext';
import { ThemeClass } from './../Theme';
import config from './../../../config';
const findChildByType = (children, targetType) => {
let result;
React.Children.forEach(children, (child) => {
if (child.type.layoutPartName === targetType.layoutPartName) {
result = child;
}
});
return result;
};
const findChildrenByType = (children, targetType) => {
return _.filter(React.Children.toArray(children), (child) =>
child.type.layoutPartName === targetType.layoutPartName);
};
const responsiveBreakpoints = {
'xs': { max: 575.8 },
'sm': { min: 576, max: 767.8 },
'md': { min: 768, max: 991.8 },
'lg': { min: 992, max: 1199.8 },
'xl': { min: 1200 }
};
class Layout extends React.Component {
static propTypes = {
children: PropTypes.node,
sidebarSlim: PropTypes.bool,
location: PropTypes.object,
favIcons: PropTypes.array
}
constructor(props) {
super(props);
this.state = {
sidebarHidden: false,
navbarHidden: false,
footerHidden: false,
sidebarCollapsed: false,
screenSize: '',
animationsDisabled: true,
pageTitle: null,
pageDescription: config.siteDescription,
pageKeywords: config.siteKeywords
};
this.lastLgSidebarCollapsed = false;
this.containerRef = React.createRef();
}
componentDidMount() {
// Determine the current window size
// and set it up in the context state
const layoutAdjuster = () => {
const { screenSize } = this.state;
let currentScreenSize;
_.forOwn(responsiveBreakpoints, (value, key) => {
const queryParts = [
`${ _.isUndefined(value.min) ? '' : `(min-width: ${value.min}px)` }`,
`${ _.isUndefined(value.max) ? '' : `(max-width: ${value.max}px)`}`
];
const query = _.compact(queryParts).join(' and ');
if (window.matchMedia(query).matches) {
currentScreenSize = key;
}
});
if (screenSize !== currentScreenSize) {
this.setState({ screenSize: currentScreenSize });
this.updateLayoutOnScreenSize(currentScreenSize);
}
};
// Add window initialization
if (typeof window !== 'undefined') {
window.addEventListener('resize', () => {
setTimeout(layoutAdjuster.bind(this), 0);
});
layoutAdjuster();
window.requestAnimationFrame(() => {
this.setState({ animationsDisabled: false });
});
}
// Add document initialization
if (typeof document !== 'undefined') {
this.bodyElement = document.body;
this.documentElement = document.documentElement;
}
}
componentDidUpdate(prevProps, prevState) {
// Prevent content scrolling in overlay mode
if (
this.bodyElement && this.documentElement && (
this.state.screenSize === 'xs' ||
this.state.screenSize === 'sm' ||
this.state.screenSize === 'md'
)
) {
if (prevState.sidebarCollapsed !== this.state.sidebarCollapsed) {
// Most of the devices
const styleUpdate = this.state.sidebarCollapsed ? {
overflowY: 'auto',
touchAction: 'auto'
}: {
overflowY: 'hidden',
touchAction: 'none'
}
Object.assign(this.bodyElement.style, styleUpdate);
Object.assign(this.documentElement.style, styleUpdate);
}
}
// After location change
if (prevProps.location.pathname !== this.props.location.pathname) {
// Scroll to top
if (this.bodyElement && this.documentElement) {
this.documentElement.scrollTop = this.bodyElement.scrollTop = 0;
}
// Hide the sidebar when in overlay mode
if (
!this.state.sidebarCollapsed && (
this.state.screenSize === 'xs' ||
this.state.screenSize === 'sm' ||
this.state.screenSize === 'md'
)
) {
// Add some time to prevent jank while the dom is updating
setTimeout(() => {
this.setState({ sidebarCollapsed: true });
}, 100);
}
}
// Update positions of STICKY navbars
this.updateNavbarsPositions();
}
updateLayoutOnScreenSize(screenSize) {
if (
screenSize === 'md' ||
screenSize === 'sm' ||
screenSize === 'xs'
) {
// Save for recovering to lg later
this.lastLgSidebarCollapsed = this.state.sidebarCollapsed;
this.setState({ sidebarCollapsed: true });
} else {
this.setState({ sidebarCollapsed: this.lastLgSidebarCollapsed });
}
}
updateNavbarsPositions() {
// eslint-disable-next-line react/no-find-dom-node
const containerElement = ReactDOM.findDOMNode(this.containerRef.current);
if (containerElement) {
const navbarElements = containerElement.querySelectorAll(":scope .layout__navbar");
// Calculate and update style.top of each navbar
let totalNavbarsHeight = 0;
navbarElements.forEach((navbarElement) => {
const navbarBox = navbarElement.getBoundingClientRect();
navbarElement.style.top = `${totalNavbarsHeight}px`;
totalNavbarsHeight += navbarBox.height;
});
}
}
toggleSidebar() {
this.setState({
sidebarCollapsed: !this.state.sidebarCollapsed
});
}
setElementsVisibility(elements) {
this.setState(_.pick(elements, ['sidebarHidden', 'navbarHidden', 'footerHidden']));
}
render() {
const { children, favIcons } = this.props;
const sidebar = findChildByType(children, LayoutSidebar);
const navbars = findChildrenByType(children, LayoutNavbar);
const content = findChildByType(children, LayoutContent);
const otherChildren = _.differenceBy(
React.Children.toArray(children),
[
sidebar,
...navbars,
content
],
'type'
);
const layoutClass = classNames('layout', 'layout--animations-enabled', {
//'layout--only-navbar': this.state.sidebarHidden && !this.state.navbarHidden
});
return (
<PageConfigContext.Provider
value={{
...this.state,
sidebarSlim: !!this.props.sidebarSlim && (
this.state.screenSize === 'lg' ||
this.state.screenSize === 'xl'
),
toggleSidebar: this.toggleSidebar.bind(this),
setElementsVisibility: this.setElementsVisibility.bind(this),
changeMeta: (metaData) => { this.setState(metaData) }
}}
>
<Helmet>
<meta charSet="utf-8" />
<title>{ config.siteTitle + (this.state.pageTitle ? ` - ${this.state.pageTitle}` : '') }</title>
<link rel="canonical" href={ config.siteCannonicalUrl } />
<meta name="description" content={ this.state.pageDescription } />
{
_.map(favIcons, (favIcon, index) => (
<link { ...favIcon } key={ index } />
))
}
</Helmet>
<ThemeClass>
{(themeClass) => (
<div className={ classNames(layoutClass, themeClass) } ref={ this.containerRef }>
{
!this.state.sidebarHidden && sidebar && React.cloneElement(sidebar, {
sidebarSlim: !!this.props.sidebarSlim && this.state.sidebarCollapsed && (
this.state.screenSize === 'lg' || this.state.screenSize === 'xl'
),
sidebarCollapsed: !this.props.sidebarSlim && this.state.sidebarCollapsed
})
}
<div className="layout__wrap">
{ !this.state.navbarHidden && navbars }
{ content }
</div>
{ otherChildren }
</div>
)}
</ThemeClass>
</PageConfigContext.Provider>
);
}
}
const routedLayout = withRouter(Layout);
export { routedLayout as Layout };

View File

@@ -0,0 +1,17 @@
import React from 'react';
import PropTypes from 'prop-types';
const LayoutContent = (props) => (
<div className="layout__content">
{ props.children }
</div>
);
LayoutContent.propTypes = {
children: PropTypes.node
};
LayoutContent.layoutPartName = "content";
export {
LayoutContent
};

View File

@@ -0,0 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
const LayoutNavbar = (props) => {
const navbar = React.Children.only(props.children);
return (
<div className="layout__navbar">
{
React.cloneElement(navbar, { fixed: null })
}
</div>
);
};
LayoutNavbar.propTypes = {
children: PropTypes.node
};
LayoutNavbar.layoutPartName = "navbar";
export {
LayoutNavbar
};

View File

@@ -0,0 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
const LayoutSidebar = (props) => {
const sidebarClass = classNames("layout__sidebar", {
"layout__sidebar--slim": props.sidebarSlim,
"layout__sidebar--collapsed": props.sidebarCollapsed
});
return (
<div className={ sidebarClass }>
{ props.children }
</div>
);
};
LayoutSidebar.propTypes = {
children: PropTypes.node,
sidebarSlim: PropTypes.bool,
sidebarCollapsed: PropTypes.bool
};
LayoutSidebar.layoutPartName = "sidebar";
export {
LayoutSidebar
};

View File

@@ -0,0 +1,7 @@
import React from 'react';
const PageConfigContext = React.createContext();
export {
PageConfigContext
};

17
app/components/Layout/index.js Executable file
View File

@@ -0,0 +1,17 @@
import { Layout } from './Layout';
import { LayoutContent } from './LayoutContent';
import { LayoutNavbar } from './LayoutNavbar';
import { LayoutSidebar } from './LayoutSidebar';
import { withPageConfig } from './withPageConfig';
import { setupPage } from './setupPage';
import { PageConfigContext } from './PageConfigContext';
Layout.Sidebar = LayoutSidebar;
Layout.Navbar = LayoutNavbar;
Layout.Content = LayoutContent;
const PageConfigProvider = PageConfigContext.Provider;
const PageConfigConsumer = PageConfigContext.Consumer;
export default Layout;
export { withPageConfig, setupPage, PageConfigProvider, PageConfigConsumer };

View File

@@ -0,0 +1,30 @@
import React from 'react';
import _ from 'lodash';
import PropTypes from 'prop-types';
import { withPageConfig } from './withPageConfig'
export const setupPage = (startupConfig) =>
(Component) => {
class PageSetupWrap extends React.Component {
static propTypes = {
pageConfig: PropTypes.object
}
componentDidMount() {
this.prevConfig = _.pick(this.props.pageConfig,
['pageTitle', 'pageDescription', 'pageKeywords']);
this.props.pageConfig.changeMeta(startupConfig);
}
componentWillUnmount() {
this.props.pageConfig.changeMeta(this.prevConfig);
}
render() {
return (
<Component { ...this.props } />
)
}
}
return withPageConfig(PageSetupWrap);
};

View File

@@ -0,0 +1,13 @@
import React from 'react';
import { PageConfigContext } from './PageConfigContext';
export const withPageConfig = (Component) => {
const WithPageConfig = (props) => (
<PageConfigContext.Consumer>
{
(pageConfig) => <Component pageConfig={ pageConfig } { ...props } />
}
</PageConfigContext.Consumer>
);
return WithPageConfig;
};

3
app/components/Nav/index.js Executable file
View File

@@ -0,0 +1,3 @@
import { Nav } from './nav';
export default Nav;

25
app/components/Nav/nav.js Executable file
View File

@@ -0,0 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Nav as BsNav } from 'reactstrap';
const Nav = ({ accent, className, ...otherProps }) => {
return (
<BsNav
className={
classNames(className, 'nav', { 'nav-accent': accent })
}
{ ...otherProps }
/>
);
};
Nav.propTypes = {
...BsNav.propTypes,
accent: PropTypes.bool,
};
Nav.defaultProps = {
accent: false
};
export { Nav };

3
app/components/Navbar/index.js Executable file
View File

@@ -0,0 +1,3 @@
import { Navbar } from './navbar';
export default Navbar;

63
app/components/Navbar/navbar.js Executable file
View File

@@ -0,0 +1,63 @@
import React from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import { Navbar as BSNavbar, Container } from 'reactstrap';
const Navbar = ({
themed,
fluid,
shadow,
className,
children,
dark,
light,
color,
...otherProps
}) => {
let navbarClass = classNames({
'navbar-themed': themed || !!color,
'navbar-shadow': shadow,
}, 'navbar-multi-collapse', className);
// When a combination of light or dark is present
// with a color - use a custom class instead of bootstrap's
if ((dark || light) && color) {
navbarClass = classNames(navbarClass,
`navbar-${light ? 'light' : '' }${dark ? 'dark' : ''}-${color}`);
}
return (
<BSNavbar
className={ navbarClass }
/*
Use the dark and light switches
only when color is not set
*/
dark={ dark && !color }
light={ light && !color }
{ ...otherProps }
>
{
<Container className="navbar-collapse-wrap" fluid={ fluid }>
{ children }
</Container>
}
</BSNavbar>
)
};
Navbar.propTypes = {
themed: PropTypes.bool,
fluid: PropTypes.bool,
shadow: PropTypes.bool,
className: PropTypes.string,
children: PropTypes.node,
color: PropTypes.string,
dark: PropTypes.bool,
light: PropTypes.bool
}
Navbar.defaultProps = {
fluid: false,
themed: false,
}
export { Navbar };

View File

@@ -0,0 +1,36 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
const NavbarThemeProvider = ({ style, color, children, className }) => {
const isSingleChild = React.Children.count(children) === 1;
const themeClass = `navbar-${style}-${color}`;
if (isSingleChild) {
const child = React.Children.only(children);
return React.cloneElement(child, {
className: classNames(
child.props.className,
themeClass
),
});
} else {
return (
<div className={ classNames(className, themeClass) }>
{ children }
</div>
);
}
};
NavbarThemeProvider.propTypes = {
children: PropTypes.node.isRequired,
style: PropTypes.string,
color: PropTypes.string,
};
NavbarThemeProvider.defaultProps = {
style: 'light',
color: 'primary',
};
export { NavbarThemeProvider };

View File

@@ -0,0 +1,3 @@
import { NavbarThemeProvider } from './NavbarThemeProvider';
export default NavbarThemeProvider;

View File

@@ -0,0 +1,52 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { UncontrolledDropdown } from 'reactstrap';
import { Provider } from './context';
export class NestedDropdown extends React.Component {
constructor(props) {
super(props);
this.state = {
openId: null
}
}
handleOpen(targetId) {
this.setState({
openId: targetId
});
}
render() {
const { tag: Tag, className, children, ...otherProps } = this.props;
const dropdownClass = classNames(className, 'nested-dropdown');
return (
<Tag { ...otherProps } className={ dropdownClass } >
<Provider
value={{
openId: this.state.openId,
onOpen: this.handleOpen.bind(this)
}}
>
{ children }
</Provider>
</Tag>
);
}
}
NestedDropdown.propTypes = {
tag: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func
]),
className: PropTypes.string,
children: PropTypes.node
};
NestedDropdown.defaultProps = {
tag: UncontrolledDropdown
};

View File

@@ -0,0 +1,82 @@
import React from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import uuid from 'uuid/v4';
import { Consumer } from './context';
class NestedDropdownSubmenu extends React.Component {
componentDidMount() {
this.id = uuid();
}
render() {
const {
tag: Tag,
subMenuTag: SubMenuTag,
title,
children,
className,
openId,
onOpen
} = this.props;
const itemClass = classNames(className, 'nested-dropdown__submenu-item', {
'nested-dropdown__submenu-item--open': openId === this.id
});
const linkClass = classNames('nested-dropdown__submenu-item__link', 'dropdown-item');
return (
<Tag className={ itemClass }>
<a
href="javascript:;"
className={ linkClass }
onClick={ () => { onOpen(this.id) } }
>
{ title }
</a>
<div className="nested-dropdown__submenu-item__menu-wrap">
<SubMenuTag className="nested-dropdown__submenu-item__menu dropdown-menu">
{ children }
</SubMenuTag>
</div>
</Tag>
);
}
}
NestedDropdownSubmenu.propTypes = {
children: PropTypes.node,
title: PropTypes.oneOfType([
PropTypes.string,
PropTypes.node
]),
tag: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func
]),
subMenuTag: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func
]),
className: PropTypes.string,
// Context Provided:
openId: PropTypes.string,
onOpen: PropTypes.func.isRequired
};
NestedDropdownSubmenu.defaultProps = {
tag: "div",
subMenuTag: "div"
};
const ContextNestedDropdownSubmenu = (props) => (
<Consumer>
{
(contextProps) => (
<NestedDropdownSubmenu { ...contextProps } { ...props } />
)
}
</Consumer>
);
export {
ContextNestedDropdownSubmenu as NestedDropdownSubmenu
};

View File

@@ -0,0 +1,8 @@
import React from 'react';
const { Consumer, Provider } = React.createContext();
export {
Consumer,
Provider
};

View File

@@ -0,0 +1,6 @@
import { NestedDropdown } from './NestedDropdown';
import { NestedDropdownSubmenu } from './NestedDropdownSubmenu';
NestedDropdown.Submenu = NestedDropdownSubmenu;
export default NestedDropdown;

View File

@@ -0,0 +1,76 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import _ from 'lodash';
// Safely gets the browser document object,
// returns a simple mock for server rendering purposes
const getDocument = () =>
typeof document === 'undefined' ?
{
querySelector() { return null; }
} : document
/*
Calls an EventHandler when User clicks outside of the child element
*/
class OuterClick extends React.Component {
static propTypes = {
onClickOutside: PropTypes.func,
children: PropTypes.node,
excludedElements: PropTypes.array,
active: PropTypes.bool
}
static defaultProps = {
onClickOutside: () => { },
excludedElements: [],
active: true
}
componentDidMount() {
this.rootElement = getDocument().querySelector('body');
if (this.rootElement) {
this.rootElement.addEventListener('click', this.handleDocumentClick);
this.rootElement.addEventListener('touchstart', this.handleDocumentClick);
}
}
componentWillUnmount() {
if (this.rootElement) {
this.rootElement.removeEventListener('click', this.handleDocumentClick);
this.rootElement.removeEventListener('touchstart', this.handleDocumentClick);
}
}
assignRef(elementRef) {
this.elementRef = elementRef;
}
handleDocumentClick = (evt) => {
if(this.props.active) {
// eslint-disable-next-line react/no-find-dom-node
const domElement = ReactDOM.findDOMNode(this.elementRef);
const isExcluded = _.some(this.props.excludedElements,
// eslint-disable-next-line react/no-find-dom-node
(element) => element && ReactDOM.findDOMNode(element).contains(evt.target));
if (!isExcluded && !domElement.contains(evt.target)) {
this.props.onClickOutside(evt);
}
}
}
render() {
const onlyChild = React.Children.only(this.props.children);
const updatedChild = React.isValidElement(onlyChild) ?
React.cloneElement(onlyChild, { ref: this.assignRef.bind(this) }) : onlyChild;
return updatedChild;
}
}
export { OuterClick };

View File

@@ -0,0 +1,3 @@
import { OuterClick } from './OuterClick';
export default OuterClick;

View File

@@ -0,0 +1,9 @@
import React from 'react';
import classes from './PageLoader.scss';
const PageLoader = () => (
<div className={ classes.pageLoader }>Loading...</div>
);
export default PageLoader;

View File

@@ -0,0 +1,9 @@
.pageLoader {
background-color: cyan;
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
z-index: 1000;
}

View File

@@ -0,0 +1,3 @@
import PageLoader from './PageLoader';
export default PageLoader;

View File

@@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import {
Progress as BsProgress
} from 'reactstrap';
import classes from './Progress.scss';
const Progress = (props) => {
const { children, slim, className, ...otherProps } = props;
const progressClass = classNames(className, {
[classes['slim']]: slim
});
return (
<BsProgress className={ progressClass } { ...otherProps }>
{ !slim && children }
</BsProgress>
);
};
Progress.propTypes = {
slim: PropTypes.bool,
className: PropTypes.string,
children: PropTypes.node
};
export { Progress };

View File

@@ -0,0 +1,3 @@
.slim {
height: 0.3rem;
}

View File

@@ -0,0 +1,3 @@
import { Progress } from './Progress';
export default Progress;

View File

@@ -0,0 +1,12 @@
import { CartesianGrid } from 'recharts';
import styleConfig from './config';
class CustomCartesianGrid extends CartesianGrid {
static defaultProps = {
...CartesianGrid.defaultProps,
...styleConfig.grid,
}
}
export { CustomCartesianGrid as CartesianGrid };

View File

@@ -0,0 +1,40 @@
import React from 'react';
import PropTypes from 'prop-types';
import colors from './../../colors';
const gradientOffset = (data) => {
const dataMax = Math.max(...data.map((i) => i.uv));
const dataMin = Math.min(...data.map((i) => i.uv));
if (dataMax <= 0){
return 0
}
else if (dataMin >= 0){
return 1
}
else{
return dataMax / (dataMax - dataMin);
}
}
export const DefAreaValueColor = (props) => {
const offset = gradientOffset(props.data);
return (
<linearGradient id={ props.id } x1="0" y1="0" x2="0" y2="1">
<stop offset={offset} stopColor={ props.positiveColor } stopOpacity={1}/>
<stop offset={offset} stopColor={ props.negativeColor } stopOpacity={1}/>
</linearGradient>
);
};
DefAreaValueColor.propTypes = {
positiveColor: PropTypes.string,
negativeColor: PropTypes.string,
id: PropTypes.string.isRequired,
data: PropTypes.array.isRequired
};
DefAreaValueColor.defaultProps = {
positiveColor: colors['success-07'],
negativeColor: colors['danger-07']
};

View File

@@ -0,0 +1,10 @@
import { Legend as RCLegend } from 'recharts';
import styleConfig from './config';
export class Legend extends RCLegend {
static defaultProps = {
...RCLegend.defaultProps,
...styleConfig.legend
}
}

View File

@@ -0,0 +1,35 @@
import React from 'react';
import PropTypes from 'prop-types';
import config from './config';
const RADIAN = Math.PI / 180;
export const PieValueLabel = (props) => {
const { cx, cy, midAngle, innerRadius, outerRadius, percent, color } = props;
const radius = innerRadius + (outerRadius - innerRadius) * 0.5;
const x = cx + radius * Math.cos(-midAngle * RADIAN);
const y = cy + radius * Math.sin(-midAngle * RADIAN);
return (
<text
x={ x }
y={ y }
textAnchor={ x > cx ? 'start' : 'end' }
dominantBaseline="central"
fill={ props.color || config.pieLabel.fill }
fontSize={ config.pieLabel.fontSize }
>
{`${(percent * 100).toFixed(0)}%`}
</text>
);
};
PieValueLabel.propTypes = {
cx: PropTypes.number,
cy: PropTypes.number,
midAngle: PropTypes.number,
innerRadius: PropTypes.number,
outerRadius: PropTypes.number,
percent: PropTypes.number,
color: PropTypes.string
};

View File

@@ -0,0 +1,10 @@
import { PolarAngleAxis as RCPolarAngleAxis} from 'recharts';
import styleConfig from './config';
export class PolarAngleAxis extends RCPolarAngleAxis {
static defaultProps = {
...RCPolarAngleAxis.defaultProps,
...styleConfig.polarAngleAxis
}
}

View File

@@ -0,0 +1,12 @@
import { PolarGrid } from 'recharts';
import styleConfig from './config';
class CustomPolarGrid extends PolarGrid {
static defaultProps = {
...PolarGrid.defaultProps,
...styleConfig.polarGrid
}
}
export { CustomPolarGrid as PolarGrid };

View File

@@ -0,0 +1,10 @@
import { PolarRadiusAxis as RCPolarRadiusAxis} from 'recharts';
import styleConfig from './config';
export class PolarRadiusAxis extends RCPolarRadiusAxis {
static defaultProps = {
...RCPolarRadiusAxis.defaultProps,
...styleConfig.polarRadiusAxis
}
}

View File

@@ -0,0 +1,10 @@
import { Tooltip as RCTooltip } from 'recharts';
import styleConfig from './config';
export class Tooltip extends RCTooltip {
static defaultProps = {
...RCTooltip.defaultProps,
...styleConfig.tooltip
}
}

View File

@@ -0,0 +1,20 @@
import React from 'react';
import PropTypes from 'prop-types';
import config from './config';
export const ValueLabel = (props) =>
<text
x={ props.x }
y={ props.y - config.label.fontSize }
textAnchor="middle"
{ ...config.label }
>
{ props.value }
</text>;
ValueLabel.propTypes = {
value: PropTypes.number,
x: PropTypes.number,
y: PropTypes.number
};

View File

@@ -0,0 +1,10 @@
import { XAxis as RCXAxis } from 'recharts';
import styleConfig from './config';
export class XAxis extends RCXAxis {
static defaultProps = {
...RCXAxis.defaultProps,
...styleConfig.axis
}
}

View File

@@ -0,0 +1,10 @@
import { YAxis as RCYAxis } from 'recharts';
import styleConfig from './config';
export class YAxis extends RCYAxis {
static defaultProps = {
...RCYAxis.defaultProps,
...styleConfig.axis
}
}

View File

@@ -0,0 +1,10 @@
import { ZAxis as RCZAxis } from 'recharts';
import styleConfig from './config';
export class ZAxis extends RCZAxis {
static defaultProps = {
...RCZAxis.defaultProps,
...styleConfig.axis
}
}

View File

@@ -0,0 +1,61 @@
// ReCharts styling configuration
import colors from './../../colors';
export default {
grid: {
stroke: colors['400'],
strokeWidth: 1,
strokeDasharray: '1px'
},
polarGrid: {
stroke: colors['400'],
},
axis: {
stroke: colors['500'],
strokeWidth: 1,
style: {
fontSize: '12px'
},
tick: {
// Axis Labels color:
fill: colors['900']
}
},
polarRadiusAxis: {
stroke: colors['400'],
tick: {
fill: colors['900']
}
},
polarAngleAxis: {
tick: {
fill: colors['900']
},
style: {
fontSize: '12px'
}
},
label: {
fontSize: 11,
fill: colors['900']
},
legend: {
wrapperStyle: {
color: colors['900']
}
},
pieLabel: {
fontSize: 12,
fill: colors[100]
},
tooltip: {
cursor: {
fill: colors['primary-01']
},
contentStyle: {
background: colors['900'],
border: `1px solid ${colors['900']}`,
color: colors['white']
}
}
};

View File

@@ -0,0 +1,38 @@
import React from 'react';
import PropTypes from 'prop-types';
import OuterClick from './../OuterClick';
import { withPageConfig } from './../Layout';
import { SidebarContent } from './SidebarContent';
const Sidebar = (props) => (
<React.Fragment>
{ /* Enable OuterClick only in sidebar overlay mode */}
<OuterClick
active={
!props.pageConfig.sidebarCollapsed && (
props.pageConfig.screenSize === 'xs' ||
props.pageConfig.screenSize === 'sm' ||
props.pageConfig.screenSize === 'md'
)
}
onClickOutside={ () => props.pageConfig.toggleSidebar() }
>
<SidebarContent { ...props } />
</OuterClick>
</React.Fragment>
);
Sidebar.propTypes = {
children: PropTypes.node,
slim: PropTypes.bool,
collapsed: PropTypes.bool,
animationsDisabled: PropTypes.bool,
pageConfig: PropTypes.object
};
const cfgSidebar = withPageConfig(Sidebar);
export {
cfgSidebar as Sidebar
};

View File

@@ -0,0 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
const SidebarClose = (props) => (
<div className="sidebar__close">
{ props.children }
</div>
);
SidebarClose.propTypes = {
children: PropTypes.node
};
export { SidebarClose };

View File

@@ -0,0 +1,69 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Common from './../../common';
export class SidebarContent extends React.Component {
static propTypes = {
children: PropTypes.node,
slim: PropTypes.bool,
collapsed: PropTypes.bool,
animationsDisabled: PropTypes.bool,
pageConfig: PropTypes.object
}
sidebarRef = React.createRef();
constructor(props) {
super(props);
this.state = {
entryAnimationFinished: false,
};
}
componentDidMount() {
this.sidebarEntryAnimate = new Common.SidebarEntryAnimate();
this.slimSidebarAnimate = new Common.SlimSidebarAnimate();
this.slimMenuAnimate = new Common.SlimMenuAnimate();
this.sidebarEntryAnimate.assignParentElement(this.sidebarRef.current);
this.slimSidebarAnimate.assignParentElement(this.sidebarRef.current);
this.slimMenuAnimate.assignSidebarElement(this.sidebarRef.current);
this.sidebarEntryAnimate.executeAnimation()
.then(() => {
this.setState({ entryAnimationFinished: true });
});
}
componentWillUnmount() {
this.sidebarEntryAnimate.destroy();
this.slimSidebarAnimate.destroy();
this.slimMenuAnimate.destroy();
}
render() {
const {
animationsDisabled,
collapsed,
pageConfig,
slim,
children,
} = this.props;
const sidebarClass = classNames('sidebar', 'sidebar--animations-enabled', {
'sidebar--slim': slim || pageConfig.sidebarSlim,
'sidebar--collapsed': collapsed || pageConfig.sidebarCollapsed,
'sidebar--animations-disabled': animationsDisabled || pageConfig.animationsDisabled,
'sidebar--animate-entry-complete': this.state.entryAnimationFinished,
});
return (
<div className={ sidebarClass } ref={ this.sidebarRef }>
{ children }
</div>
);
}
}

Some files were not shown because too many files have changed in this diff Show More