at the end of the day, it was inevitable
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import FilterItem from './FilterItem';
|
||||
import { ADV_FILTERS_LIMIT } from '../../../redux/modules/appState/search';
|
||||
import classnames from 'classnames';
|
||||
import { Button } from 'reactstrap';
|
||||
|
||||
export class FilterGroup extends React.Component {
|
||||
static propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
groupName: PropTypes.string.isRequired,
|
||||
filters: PropTypes.array.isRequired,
|
||||
selectedFilters: PropTypes.object,
|
||||
count: PropTypes.number.isRequired,
|
||||
totalCount: PropTypes.number.isRequired,
|
||||
clearPending: PropTypes.bool.isRequired,
|
||||
callbacks: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static translatableGroups = {
|
||||
articleDate: 'articleDate',
|
||||
country: 'country',
|
||||
language: 'language',
|
||||
articleLanguage: 'language',
|
||||
sourceCountry: 'country',
|
||||
sentiment: 'sentiment'
|
||||
};
|
||||
|
||||
translateItem = (itemName) => {
|
||||
return this.props.t(`${this.translateKey}.${itemName}`, {
|
||||
defaultValue: itemName
|
||||
});
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { isExpanded: !!this.props.totalCount };
|
||||
if (this.props.groupName in FilterGroup.translatableGroups) {
|
||||
this.translateKey = FilterGroup.translatableGroups[this.props.groupName];
|
||||
} else {
|
||||
this.translateItem = (x) => x; //equiv function
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate = (prevProps) => {
|
||||
const needUpdate = prevProps.totalCount !== this.props.totalCount;
|
||||
if (needUpdate) {
|
||||
this.setState({
|
||||
isExpanded: !!this.props.totalCount
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
toggleExpand = () => {
|
||||
this.setState({
|
||||
isExpanded: !this.state.isExpanded
|
||||
});
|
||||
};
|
||||
|
||||
onMoreClick = () => {
|
||||
this.props.callbacks.moreFilters(this.props.groupName);
|
||||
};
|
||||
|
||||
onLessClick = () => {
|
||||
this.props.callbacks.lessFilters(this.props.groupName);
|
||||
};
|
||||
|
||||
onRefineClick = () => {
|
||||
this.props.callbacks.refine();
|
||||
};
|
||||
|
||||
onClearClick = () => {
|
||||
this.props.callbacks.clearFilters(this.props.groupName);
|
||||
};
|
||||
|
||||
onItemClick = (filterValue) => {
|
||||
this.props.callbacks.selectFilter(this.props.groupName, filterValue);
|
||||
};
|
||||
|
||||
forEachItem = (fn) => {
|
||||
return this.props.filters && this.props.filters.slice(0, this.props.count).map(fn);
|
||||
};
|
||||
|
||||
render() {
|
||||
const clsName = 'filters-table__group';
|
||||
const { t, selectedFilters, clearPending, count, totalCount } = this.props;
|
||||
|
||||
let includedCounter = 0;
|
||||
let excludedCounter = 0;
|
||||
for (let value in selectedFilters) {
|
||||
if (selectedFilters.hasOwnProperty(value)) {
|
||||
if (selectedFilters[value] === -1) excludedCounter++;
|
||||
if (selectedFilters[value] === 1) includedCounter++;
|
||||
}
|
||||
}
|
||||
|
||||
const moreVisible = count < totalCount;
|
||||
const lessVisible = count > ADV_FILTERS_LIMIT;
|
||||
const hasSelected = !!excludedCounter || !!includedCounter;
|
||||
|
||||
const moreVisibleClass = classnames('filters-table__more', {
|
||||
'filters-table__more--visible': moreVisible,
|
||||
'filters-table__more--hidden': !moreVisible && lessVisible,
|
||||
'filters-table__more--none': !moreVisible && !lessVisible
|
||||
});
|
||||
|
||||
const lessVisibleClass = classnames('filters-table__less', {
|
||||
'filters-table__less--visible': lessVisible,
|
||||
'filters-table__less--hidden': !lessVisible && moreVisible,
|
||||
'filters-table__less--none': !lessVisible && !moreVisible
|
||||
});
|
||||
|
||||
const refineVisibleClass = classnames(
|
||||
'filters-table__more my-2 ml-2 mr-1',
|
||||
{
|
||||
'filters-table__more--none': !hasSelected && !clearPending
|
||||
}
|
||||
);
|
||||
|
||||
const clearVisibleClass = classnames('filters-table__less my-2 mr-2 ml-1', {
|
||||
'filters-table__less--none': !hasSelected
|
||||
});
|
||||
|
||||
const isRTL = document.documentElement.dir === 'rtl';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
clsName + (this.state.isExpanded ? ' ' + clsName + '--expanded' : '')
|
||||
}
|
||||
>
|
||||
<div className="filters-table__head" onClick={this.toggleExpand}>
|
||||
{isRTL ? (
|
||||
<i className="fa fa-chevron-left" />
|
||||
) : (
|
||||
<i className="fa fa-chevron-right" />
|
||||
)}
|
||||
<i className="fa fa-chevron-down" />
|
||||
{t('advancedFilters.' + this.props.groupName, {
|
||||
defaultValue: this.props.groupName
|
||||
})}
|
||||
<div>
|
||||
{!includedCounter && !excludedCounter && (
|
||||
<i style={{ marginRight: '5px' }} className="lnr lnr-cross" />
|
||||
)}
|
||||
{!!includedCounter && (
|
||||
<span className="included"> {includedCounter}</span>
|
||||
)}
|
||||
{!!excludedCounter && (
|
||||
<span className="excluded">-{excludedCounter}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.state.isExpanded && (
|
||||
<div className="filters-table__body">
|
||||
{this.forEachItem((item) => {
|
||||
return (
|
||||
<FilterItem
|
||||
value={item.value}
|
||||
title={this.translateItem(item.value)}
|
||||
key={item.value}
|
||||
count={item.count}
|
||||
selectionState={selectedFilters[item.value]}
|
||||
onItemClick={this.onItemClick}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{clearPending && (
|
||||
<div className="filters-table__clear-msg">
|
||||
{t('filtersTable.clearMessage')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="d-flex justify-content-between">
|
||||
<Button
|
||||
className={moreVisibleClass}
|
||||
color="link"
|
||||
onClick={this.onMoreClick}
|
||||
>
|
||||
<i className="fa fa-angle-double-down" />{' '}
|
||||
{t('filtersTable.more')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className={lessVisibleClass}
|
||||
color="link"
|
||||
onClick={this.onLessClick}
|
||||
>
|
||||
<i className="fa fa-angle-double-up" /> {t('filtersTable.less')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="d-flex justify-content-between">
|
||||
<Button
|
||||
className={refineVisibleClass}
|
||||
color="primary"
|
||||
onClick={this.onRefineClick}
|
||||
>
|
||||
{t('filtersTable.refine')}
|
||||
</Button>
|
||||
|
||||
<Button className={clearVisibleClass} onClick={this.onClearClick}>
|
||||
{t('filtersTable.clear')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['common'], { wait: true })(FilterGroup);
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate } from 'react-i18next'
|
||||
import classnames from 'classnames'
|
||||
|
||||
export class FilterItem extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
count: PropTypes.number.isRequired,
|
||||
onItemClick: PropTypes.func.isRequired,
|
||||
selectionState: PropTypes.number
|
||||
};
|
||||
|
||||
onItemClick = () => {
|
||||
const { onItemClick, value } = this.props
|
||||
onItemClick(value)
|
||||
};
|
||||
|
||||
render () {
|
||||
const { selectionState, title, count } = this.props
|
||||
const mainClass = 'filters-table__item'
|
||||
const classes = classnames(mainClass, {
|
||||
[`${mainClass}--included`]: selectionState === 1,
|
||||
[`${mainClass}--excluded`]: selectionState === -1
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={classes} onClick={this.onItemClick}>
|
||||
{title}
|
||||
<span>{count}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(FilterItem)
|
||||
@@ -0,0 +1,95 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import FilterGroup from './FilterGroup';
|
||||
import { Button } from 'reactstrap';
|
||||
import { arraymove } from '../../../common/helper';
|
||||
|
||||
export class FiltersTable extends React.Component {
|
||||
static propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
filters: PropTypes.object.isRequired,
|
||||
selectedFilters: PropTypes.object.isRequired,
|
||||
clearPending: PropTypes.object.isRequired,
|
||||
pages: PropTypes.object.isRequired,
|
||||
callbacks: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
forEachGroup = (fn) => {
|
||||
const { pages, filters, selectedFilters } = this.props;
|
||||
const filterKeys = Object.keys(filters);
|
||||
|
||||
arraymove(
|
||||
filterKeys,
|
||||
filterKeys.indexOf('sentiment'),
|
||||
filterKeys.indexOf('articleDate') + 1
|
||||
);
|
||||
|
||||
return filterKeys.map((groupName) => {
|
||||
return fn(
|
||||
groupName,
|
||||
filters[groupName],
|
||||
pages[groupName],
|
||||
selectedFilters[groupName] || {}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
onRefineButton = () => {
|
||||
this.props.callbacks.refine();
|
||||
};
|
||||
|
||||
onClearAllButton = () => {
|
||||
this.props.callbacks.clearAllFilters();
|
||||
setTimeout(() => {
|
||||
this.props.callbacks.refine();
|
||||
}, 200);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { callbacks, clearPending, t } = this.props;
|
||||
return (
|
||||
<div className="filters-table-container">
|
||||
<div className="filters-table">
|
||||
{this.forEachGroup(
|
||||
(groupName, groupFilters, groupPage, selectedFilters) => {
|
||||
return (
|
||||
<FilterGroup
|
||||
groupName={groupName}
|
||||
key={groupName}
|
||||
filters={groupFilters}
|
||||
count={groupPage && groupPage.count}
|
||||
totalCount={groupPage && groupPage.totalCount}
|
||||
selectedFilters={selectedFilters}
|
||||
clearPending={groupName in clearPending}
|
||||
callbacks={callbacks}
|
||||
/>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-center mt-2">
|
||||
<Button
|
||||
onClick={this.onRefineButton}
|
||||
className="btn-icon mb-1 mr-1"
|
||||
color="warning"
|
||||
>
|
||||
<i className="lnr lnr-sync btn-icon-wrapper"></i>
|
||||
{t('filtersTable.refine')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={this.onClearAllButton}
|
||||
className="btn-icon mb-1"
|
||||
color="secondary"
|
||||
>
|
||||
<i className="lnr lnr-trash btn-icon-wrapper"></i>
|
||||
{t('filtersTable.clearAll')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['common'], { wait: true })(FiltersTable);
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
|
||||
function Footer({ t }) {
|
||||
return (
|
||||
<footer className="footer">
|
||||
<ul className="footer__list">
|
||||
<li className="footer__item">
|
||||
<a
|
||||
title={t('footer.privacyPolicy')}
|
||||
target="_blank"
|
||||
href="https://www.socialhose.io/en/legal/privacy"
|
||||
className="footer__link"
|
||||
>
|
||||
{t('footer.privacyPolicy')}
|
||||
</a>
|
||||
</li>
|
||||
<li className="footer__item">
|
||||
<a
|
||||
title={t('footer.acceptableUsePolicy')}
|
||||
target="_blank"
|
||||
href="https://www.socialhose.io/en/legal/acceptable-use"
|
||||
className="footer__link"
|
||||
>
|
||||
{t('footer.acceptableUsePolicy')}
|
||||
</a>
|
||||
</li>
|
||||
<li className="footer__item">
|
||||
<a
|
||||
title={t('footer.termsConditions')}
|
||||
target="_blank"
|
||||
href="https://www.socialhose.io/en/legal/terms"
|
||||
className="footer__link"
|
||||
>
|
||||
{t('footer.termsConditions')}
|
||||
</a>
|
||||
</li>
|
||||
<li className="footer__item">
|
||||
{t('footer.copyright', { year: new Date().getFullYear() })}
|
||||
</li>
|
||||
</ul>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
Footer.propTypes = {
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default translate(['loginApp'], { wait: true })(Footer);
|
||||
@@ -0,0 +1,84 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormGroup, Label, CustomInput } from 'reactstrap';
|
||||
import { Interpolate, translate } from 'react-i18next';
|
||||
|
||||
// need changes: err cannot be removed once triggered
|
||||
function Checkbox({
|
||||
t,
|
||||
title,
|
||||
hideTitle,
|
||||
name,
|
||||
formGroupClass,
|
||||
required,
|
||||
trueValue,
|
||||
value,
|
||||
disabled,
|
||||
error,
|
||||
description,
|
||||
handleChange
|
||||
}) {
|
||||
function onChange(e) {
|
||||
const { name, checked } = e.target;
|
||||
let oppValue = null;
|
||||
if (typeof trueValue === 'boolean') oppValue = false;
|
||||
else if (typeof trueValue === 'number') oppValue = 0;
|
||||
handleChange(name, checked ? trueValue : oppValue);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormGroup className={formGroupClass}>
|
||||
{!hideTitle && (
|
||||
<Label>
|
||||
{title}
|
||||
{required ? <span className="text-danger">*</span> : null}
|
||||
</Label>
|
||||
)}
|
||||
<CustomInput
|
||||
id={name}
|
||||
type="checkbox"
|
||||
checked={trueValue === value}
|
||||
title={title}
|
||||
name={name}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
label={description}
|
||||
invalid={error}
|
||||
onChange={onChange}
|
||||
/>
|
||||
{error === true ? (
|
||||
<span className="text-danger">
|
||||
<Interpolate t={t} i18nKey="messages.selectMsg" title={title} />
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-danger">{error}</span>
|
||||
)}
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
Checkbox.defaultProps = {
|
||||
handleChange: () => {},
|
||||
trueValue: true,
|
||||
disabled: false,
|
||||
hideTitle: false,
|
||||
formGroupClass: ''
|
||||
};
|
||||
|
||||
Checkbox.propTypes = {
|
||||
t: PropTypes.func,
|
||||
title: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
type: PropTypes.string,
|
||||
formGroupClass: PropTypes.string,
|
||||
value: PropTypes.bool,
|
||||
hideTitle: PropTypes.bool,
|
||||
trueValue: PropTypes.any,
|
||||
description: PropTypes.any,
|
||||
required: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
error: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]),
|
||||
handleChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default React.memo(translate(['common'], { wait: true })(Checkbox));
|
||||
@@ -0,0 +1,140 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Input as ReactInput,
|
||||
FormGroup,
|
||||
Label,
|
||||
FormText,
|
||||
InputGroup
|
||||
} from 'reactstrap';
|
||||
import { translate } from 'react-i18next';
|
||||
|
||||
function Input({
|
||||
t,
|
||||
title,
|
||||
name,
|
||||
required,
|
||||
disabled,
|
||||
value,
|
||||
type,
|
||||
options,
|
||||
placeholder,
|
||||
error,
|
||||
description,
|
||||
regex,
|
||||
handleChange,
|
||||
handleValidation,
|
||||
validationMessage,
|
||||
inputGroupAddon
|
||||
}) {
|
||||
function onChange(e) {
|
||||
handleChange(e.target.name.trim(''), e.target.value);
|
||||
}
|
||||
|
||||
function onValidate(e) {
|
||||
if (!handleValidation) return;
|
||||
const { value, name } = e.target;
|
||||
let errorMsg = required ? null : undefined;
|
||||
if (!value.replace(/\s/g, '').length && required) {
|
||||
errorMsg = true;
|
||||
} else if (value && regex && !regex.test(value)) {
|
||||
errorMsg = validationMessage || t('messages.invalidMsg', { title });
|
||||
}
|
||||
|
||||
handleValidation(name, errorMsg);
|
||||
}
|
||||
|
||||
let optionsJSX;
|
||||
if (type === 'select' && Array.isArray(options)) {
|
||||
optionsJSX = [
|
||||
<option key="empty" value="">
|
||||
{t('messages.dropdownValue0', { title })}
|
||||
</option>
|
||||
];
|
||||
options.map((option) =>
|
||||
optionsJSX.push(
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{title}
|
||||
{required ? <span className="text-danger">*</span> : null}
|
||||
</Label>
|
||||
{inputGroupAddon ? (
|
||||
<Fragment>
|
||||
<InputGroup>
|
||||
<ReactInput
|
||||
title={title}
|
||||
type={type}
|
||||
name={name}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
invalid={!!error}
|
||||
onChange={onChange}
|
||||
onBlur={onValidate}
|
||||
disabled={disabled}
|
||||
children={optionsJSX}
|
||||
/>
|
||||
{inputGroupAddon}
|
||||
</InputGroup>
|
||||
</Fragment>
|
||||
) : (
|
||||
<ReactInput
|
||||
title={title}
|
||||
type={type}
|
||||
name={name}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
invalid={!!error}
|
||||
onChange={onChange}
|
||||
onBlur={onValidate}
|
||||
disabled={disabled}
|
||||
children={optionsJSX}
|
||||
/>
|
||||
)}
|
||||
{error === true ? (
|
||||
<span className="text-danger">{t('messages.inputMsg', { title })}</span>
|
||||
) : (
|
||||
<span className="text-danger">{error}</span>
|
||||
)}
|
||||
{description && <FormText>{description}</FormText>}
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
Input.defaultProps = {
|
||||
handleChange: () => {},
|
||||
handleValidation: () => {},
|
||||
disabled: false
|
||||
};
|
||||
|
||||
Input.propTypes = {
|
||||
t: PropTypes.func,
|
||||
title: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
type: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
validationMessage: PropTypes.string,
|
||||
required: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
error: PropTypes.oneOfType([
|
||||
PropTypes.bool,
|
||||
PropTypes.object,
|
||||
PropTypes.string
|
||||
]),
|
||||
regex: PropTypes.object,
|
||||
options: PropTypes.any,
|
||||
description: PropTypes.any,
|
||||
inputGroupAddon: PropTypes.any,
|
||||
handleChange: PropTypes.func,
|
||||
handleValidation: PropTypes.func
|
||||
};
|
||||
|
||||
export default React.memo(translate(['common'], { wait: true })(Input));
|
||||
@@ -0,0 +1,93 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormGroup, Label, CustomInput } from 'reactstrap';
|
||||
import { translate } from 'react-i18next';
|
||||
|
||||
function RadioButton({
|
||||
t,
|
||||
title,
|
||||
name,
|
||||
required,
|
||||
value,
|
||||
disabled,
|
||||
error,
|
||||
inline,
|
||||
options,
|
||||
valueKey,
|
||||
labelKey,
|
||||
formClass,
|
||||
handleChange
|
||||
}) {
|
||||
function onChange(e, value) {
|
||||
let errorMsg = required ? null : undefined;
|
||||
if (!value && required) {
|
||||
errorMsg = t('messages.selectMsg', { title: name });
|
||||
} else {
|
||||
errorMsg = false;
|
||||
}
|
||||
handleChange(name, value, errorMsg);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormGroup className={formClass}>
|
||||
<Label className={inline ? 'mr-4' : ''}>
|
||||
{title}
|
||||
{inline ? ':' : null}
|
||||
{required ? <span className="text-danger">*</span> : null}
|
||||
</Label>
|
||||
{options.map((o, i) => (
|
||||
<CustomInput
|
||||
key={`${name}${i}`}
|
||||
id={`${name}${i}`}
|
||||
type="radio"
|
||||
inline={inline}
|
||||
checked={o[valueKey] === value}
|
||||
title={title}
|
||||
name={name}
|
||||
disabled={disabled}
|
||||
label={o[labelKey]}
|
||||
invalid={error}
|
||||
onChange={function (e) {
|
||||
onChange(e, o[valueKey]);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{error === true ? (
|
||||
<span className="text-danger">{t('messages.inputMsg', { title })}</span>
|
||||
) : (
|
||||
<span className="text-danger">{error}</span>
|
||||
)}
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
RadioButton.defaultProps = {
|
||||
options: [],
|
||||
valueKey: 'value',
|
||||
labelKey: 'label',
|
||||
value: null,
|
||||
handleChange: () => {},
|
||||
handleValidation: () => {},
|
||||
disabled: false
|
||||
};
|
||||
|
||||
RadioButton.propTypes = {
|
||||
t: PropTypes.func,
|
||||
title: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
type: PropTypes.string,
|
||||
value: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
|
||||
formClass: PropTypes.string,
|
||||
options: PropTypes.array,
|
||||
labelKey: PropTypes.string,
|
||||
valueKey: PropTypes.string,
|
||||
required: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
inline: PropTypes.bool,
|
||||
error: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]),
|
||||
handleChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default React.memo(
|
||||
translate(['tabsContent'], { wait: true })(RadioButton)
|
||||
);
|
||||
@@ -0,0 +1,5 @@
|
||||
import Input from './Input'
|
||||
import Checkbox from './Checkbox'
|
||||
import RadioButton from './RadioButton'
|
||||
|
||||
export { Input, Checkbox, RadioButton }
|
||||
@@ -0,0 +1,16 @@
|
||||
import React, { Component } from 'react'
|
||||
|
||||
export default class LoadersAdvanced extends Component {
|
||||
render () {
|
||||
return (
|
||||
<div className="out-space">
|
||||
<div className="lds-ellipsis">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
.out-space {
|
||||
position: absolute;
|
||||
background-color: #f7f7f7;
|
||||
opacity: 0.9;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 99999;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.lds-ellipsis {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
.lds-ellipsis div {
|
||||
position: absolute;
|
||||
top: 33px;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 50%;
|
||||
background: $primary;
|
||||
animation-timing-function: cubic-bezier(0, 1, 1, 0);
|
||||
}
|
||||
.lds-ellipsis div:nth-child(1) {
|
||||
left: 8px;
|
||||
animation: lds-ellipsis1 0.6s infinite;
|
||||
}
|
||||
.lds-ellipsis div:nth-child(2) {
|
||||
left: 8px;
|
||||
animation: lds-ellipsis2 0.6s infinite;
|
||||
}
|
||||
.lds-ellipsis div:nth-child(3) {
|
||||
left: 32px;
|
||||
animation: lds-ellipsis2 0.6s infinite;
|
||||
}
|
||||
.lds-ellipsis div:nth-child(4) {
|
||||
left: 56px;
|
||||
animation: lds-ellipsis3 0.6s infinite;
|
||||
}
|
||||
@keyframes lds-ellipsis1 {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes lds-ellipsis3 {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
@keyframes lds-ellipsis2 {
|
||||
0% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate(24px, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import i18n from '../../i18n';
|
||||
|
||||
function Loading({
|
||||
show = true,
|
||||
message = i18n.t('common:commonWords.loading')
|
||||
}) {
|
||||
if (!show) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="d-flex flex-column align-items-center justify-content-center text-muted p-5">
|
||||
<span className="mb-2">
|
||||
<FontAwesomeIcon icon={faSpinner} size="2x" pulse />
|
||||
</span>
|
||||
<p>{message}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Loading.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
message: PropTypes.string
|
||||
};
|
||||
|
||||
export default Loading;
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { IoIosInformationCircle } from 'react-icons/io';
|
||||
import i18n from '../../i18n';
|
||||
|
||||
function NoRecords({
|
||||
show = true,
|
||||
message = i18n.t('common:messages.noResults')
|
||||
}) {
|
||||
if (!show) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="d-flex flex-column align-items-center justify-content-center text-muted p-5">
|
||||
<IoIosInformationCircle className="mb-2" fontSize="32px" />
|
||||
<p>{message}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
NoRecords.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
message: PropTypes.string
|
||||
};
|
||||
|
||||
export default NoRecords;
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Button } from 'reactstrap'
|
||||
|
||||
export default class LimitSelector extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
pagerAction: PropTypes.func.isRequired,
|
||||
limit: PropTypes.number.isRequired,
|
||||
isCurrent: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
onClick = () => {
|
||||
!this.props.isCurrent && this.props.pagerAction({limitByPage: this.props.limit})
|
||||
};
|
||||
|
||||
render () {
|
||||
let className = 'table-pager__limit'
|
||||
if (this.props.isCurrent) {
|
||||
className += ' ' + className + '--current'
|
||||
}
|
||||
|
||||
return (
|
||||
<Button color="primary" className={className} onClick={this.onClick}>
|
||||
{this.props.limit}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { PaginationItem, PaginationLink } from 'reactstrap'
|
||||
|
||||
export default class PageSelector extends React.Component {
|
||||
static propTypes = {
|
||||
pagerAction: PropTypes.func.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
isEllipsis: PropTypes.bool.isRequired,
|
||||
isCurrent: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
onClick = () => {
|
||||
!this.props.isCurrent &&
|
||||
this.props.pagerAction({ currentPage: this.props.index })
|
||||
};
|
||||
|
||||
render () {
|
||||
if (this.props.isEllipsis) {
|
||||
return <span className="table-pager__ellipsis">. . .</span>
|
||||
} else {
|
||||
// const currentClass = this.props.isCurrent
|
||||
// ? ' table-pager__page--current'
|
||||
// : ''
|
||||
return (
|
||||
<PaginationItem active={this.props.isCurrent}>
|
||||
<PaginationLink onClick={this.onClick}>
|
||||
{this.props.index}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate } from 'react-i18next'
|
||||
import LimitSelector from './LimitSelector'
|
||||
import PageSelector from './PageSelector'
|
||||
import { ButtonGroup, Pagination, PaginationItem, PaginationLink } from 'reactstrap'
|
||||
|
||||
export class Pager extends React.Component {
|
||||
static propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
pagerAction: PropTypes.func.isRequired,
|
||||
currentPage: PropTypes.number.isRequired,
|
||||
numPages: PropTypes.number.isRequired,
|
||||
limitByPage: PropTypes.number.isRequired,
|
||||
hideLimitSelector: PropTypes.bool
|
||||
};
|
||||
|
||||
static limits = [10, 25, 50, 100, 200];
|
||||
|
||||
onClickPrevPage = () => {
|
||||
if (this.props.currentPage > 1) {
|
||||
this.props.pagerAction({ currentPage: this.props.currentPage - 1 })
|
||||
}
|
||||
};
|
||||
|
||||
onClickNextPage = () => {
|
||||
if (this.props.currentPage < this.props.numPages) {
|
||||
this.props.pagerAction({ currentPage: this.props.currentPage + 1 })
|
||||
}
|
||||
};
|
||||
|
||||
getPaginationTemplate = (maxLength = 7) => {
|
||||
const { numPages, currentPage } = this.props
|
||||
let res = {}
|
||||
if (numPages === 0) return res
|
||||
|
||||
//always show first, last and current page
|
||||
res[1] = 1
|
||||
res[numPages] = 1
|
||||
res[currentPage] = 1
|
||||
|
||||
if (currentPage <= maxLength - 3) {
|
||||
//show all from 1 to 5
|
||||
for (let i = 2; i < maxLength - 1; i++) {
|
||||
if (i < numPages) res[i] = 1
|
||||
}
|
||||
} else if (currentPage >= numPages - maxLength + 4) {
|
||||
//show last five pages
|
||||
for (let i = numPages - maxLength + 3; i < numPages; i++) {
|
||||
res[i] = 1
|
||||
}
|
||||
} else {
|
||||
//just show neighbours of current page
|
||||
let shift = Math.floor((maxLength - 5) / 2)
|
||||
for (let i = currentPage - shift; i <= currentPage + shift; i++) {
|
||||
res[i] = 1
|
||||
}
|
||||
}
|
||||
//and show ellipsis
|
||||
if (numPages > 1) {
|
||||
if (!res[2]) res[2] = 0
|
||||
if (!res[numPages - 1]) res[numPages - 1] = 0
|
||||
}
|
||||
return res
|
||||
};
|
||||
|
||||
render () {
|
||||
const pages = this.getPaginationTemplate()
|
||||
// const prevDisabledClass =
|
||||
// this.props.currentPage > 1 ? '' : ' table-pager__page--disabled'
|
||||
// const nextDisabledClass =
|
||||
// this.props.currentPage < this.props.numPages
|
||||
// ? ''
|
||||
// : ' table-pager__page--disabled'
|
||||
|
||||
return (
|
||||
<div className="table-pager">
|
||||
<Pagination
|
||||
className="pagination-rounded"
|
||||
aria-label="Page navigation example"
|
||||
>
|
||||
<PaginationItem>
|
||||
<PaginationLink onClick={this.onClickPrevPage} previous />
|
||||
</PaginationItem>
|
||||
|
||||
{Object.keys(pages).map((index) => {
|
||||
return (
|
||||
<PageSelector
|
||||
pagerAction={this.props.pagerAction}
|
||||
key={index}
|
||||
index={parseInt(index)}
|
||||
isEllipsis={pages[index] === 0}
|
||||
isCurrent={parseInt(index) === this.props.currentPage}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
<PaginationItem>
|
||||
<PaginationLink onClick={this.onClickNextPage} next />
|
||||
</PaginationItem>
|
||||
</Pagination>
|
||||
|
||||
{!this.props.hideLimitSelector && (
|
||||
<div className="table-pager__limits">
|
||||
<span>Show</span>
|
||||
<ButtonGroup>
|
||||
{Pager.limits.map((val) => {
|
||||
return (
|
||||
<LimitSelector
|
||||
pagerAction={this.props.pagerAction}
|
||||
key={val}
|
||||
limit={val}
|
||||
isCurrent={val === this.props.limitByPage}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(Pager)
|
||||
@@ -0,0 +1,88 @@
|
||||
import React, { Fragment } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate } from 'react-i18next'
|
||||
import classnames from 'classnames'
|
||||
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'
|
||||
|
||||
export class PopupLayout extends React.Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.element.isRequired,
|
||||
title: PropTypes.string,
|
||||
showFooter: PropTypes.bool,
|
||||
footer: PropTypes.element,
|
||||
cancelText: PropTypes.string,
|
||||
submitText: PropTypes.string,
|
||||
submitColor: PropTypes.string,
|
||||
onHide: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
hidePopup = () => {
|
||||
this.props.onHide()
|
||||
};
|
||||
|
||||
onSubmit = () => {
|
||||
const { onSubmit } = this.props
|
||||
onSubmit() && this.hidePopup()
|
||||
};
|
||||
|
||||
getHeader () {
|
||||
const { t, title = 'common:commonWords.Confirm' } = this.props
|
||||
return (
|
||||
<ModalHeader toggle={this.hidePopup}>
|
||||
{t(title)}
|
||||
</ModalHeader>
|
||||
)
|
||||
}
|
||||
|
||||
getFooter () {
|
||||
const {
|
||||
t,
|
||||
footer,
|
||||
showFooter = true,
|
||||
cancelText = 'common:commonWords.Cancel',
|
||||
submitText = 'common:commonWords.Confirm',
|
||||
submitColor = 'primary'
|
||||
} = this.props
|
||||
if (!showFooter) return null
|
||||
|
||||
const hasFooter = !!footer
|
||||
|
||||
return (
|
||||
<ModalFooter>
|
||||
{hasFooter && footer}
|
||||
{!hasFooter && (
|
||||
<Fragment>
|
||||
<Button color="light" onClick={this.hidePopup}>
|
||||
{t(cancelText)}
|
||||
</Button>
|
||||
<Button color={submitColor} onClick={this.onSubmit}>
|
||||
{t(submitText)}
|
||||
</Button>
|
||||
</Fragment>
|
||||
)}
|
||||
</ModalFooter>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { children, className } = this.props
|
||||
const classes = classnames('popup', className)
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<Modal isOpen toggle={this.hidePopup} backdrop="static">
|
||||
{this.getHeader()}
|
||||
<ModalBody>{children}</ModalBody>
|
||||
{this.getFooter()}
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent', 'common'], { wait: true })(
|
||||
PopupLayout
|
||||
)
|
||||
@@ -0,0 +1,83 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Quill from 'quill'
|
||||
import 'quill/dist/quill.core.css'
|
||||
import 'quill/dist/quill.snow.css'
|
||||
|
||||
function QuillEditor({ id, children, reference, className }) {
|
||||
const editorRef = reference
|
||||
|
||||
useEffect(() => {
|
||||
// all custom font-sizes and font-families should set up in the whitelist first
|
||||
const size = Quill.import('attributors/style/size')
|
||||
const font = Quill.import('formats/font')
|
||||
|
||||
size.whitelist = ['10px', '13px', '16px', '18px', '24px', '32px', '48px']
|
||||
font.whitelist = [
|
||||
'roboto',
|
||||
'lato',
|
||||
'times',
|
||||
'arial',
|
||||
'courier',
|
||||
'georgia',
|
||||
'trebuchet',
|
||||
'verdana'
|
||||
]
|
||||
|
||||
Quill.register(size, true)
|
||||
Quill.register(font, true)
|
||||
|
||||
//all custom labels and font-families are setting up via css, library works that way, in our case we setting up font-families and font-sizes
|
||||
const toolbarOptions = [
|
||||
[
|
||||
{
|
||||
font: [
|
||||
'roboto',
|
||||
'lato',
|
||||
'times',
|
||||
'arial',
|
||||
'courier',
|
||||
'georgia',
|
||||
'trebuchet',
|
||||
'verdana'
|
||||
]
|
||||
},
|
||||
{
|
||||
size: ['10px', '12px', '14px', '16px', '18px', '24px', '32px', '48px']
|
||||
}
|
||||
],
|
||||
['bold', 'italic', 'underline', 'strike'],
|
||||
[{ script: 'sub' }, { script: 'super' }],
|
||||
[{ align: [] }],
|
||||
[{ indent: '-1' }, { indent: '+1' }],
|
||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||
['link', 'image'],
|
||||
[{ color: [] }, { background: [] }],
|
||||
['clean']
|
||||
]
|
||||
|
||||
editorRef.current = new Quill(`#${id}`, {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: toolbarOptions
|
||||
}
|
||||
})
|
||||
|
||||
editorRef.current.focus()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div id={id} className={className}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
QuillEditor.propTypes = {
|
||||
id: PropTypes.string,
|
||||
children: PropTypes.any,
|
||||
reference: PropTypes.any,
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
export default QuillEditor
|
||||
@@ -0,0 +1,246 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import { Row, Col, Progress } from 'reactstrap';
|
||||
|
||||
export class Restrictions extends React.Component {
|
||||
static propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
restrictions: PropTypes.object,
|
||||
restrictionsIds: PropTypes.array
|
||||
};
|
||||
|
||||
getBarColor(percentage) {
|
||||
return percentage > 50
|
||||
? percentage > 75
|
||||
? 'danger'
|
||||
: 'warning'
|
||||
: 'success';
|
||||
}
|
||||
|
||||
render() {
|
||||
const { restrictions, restrictionsIds, t } = this.props;
|
||||
if (!restrictions) return '';
|
||||
let searchLicense = null;
|
||||
let searchLicenseLimit = null;
|
||||
let saveLicense = null;
|
||||
let saveLicenseLimit = null;
|
||||
let alerts = null;
|
||||
let alertsLimit = null;
|
||||
let webFeeds = null;
|
||||
let webFeedsLimit = null;
|
||||
let isAlerts = false;
|
||||
|
||||
restrictionsIds.map((id) => {
|
||||
const restriction = restrictions[id];
|
||||
if (id === 'alerts') {
|
||||
alerts = restriction.current;
|
||||
alertsLimit = restriction.limit;
|
||||
isAlerts = true;
|
||||
}
|
||||
|
||||
if (id === 'searchesPerDay') {
|
||||
searchLicense = restriction.current;
|
||||
searchLicenseLimit = restriction.limit;
|
||||
}
|
||||
|
||||
if (id === 'savedFeeds') {
|
||||
saveLicense = restriction.current;
|
||||
saveLicenseLimit = restriction.limit;
|
||||
}
|
||||
|
||||
if (id === 'webFeeds') {
|
||||
webFeeds = restriction.current;
|
||||
webFeedsLimit = restriction.limit;
|
||||
}
|
||||
});
|
||||
|
||||
const alertPerc = (alerts * 100) / alertsLimit;
|
||||
const searchPerc = (searchLicense * 100) / searchLicenseLimit;
|
||||
const feedPerc = (saveLicense * 100) / saveLicenseLimit;
|
||||
const webFeedPerc = (webFeeds * 100) / webFeedsLimit;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{isAlerts ? (
|
||||
<Row>
|
||||
<Col md="6" xl="4">
|
||||
<div className="card mb-3 widget-content">
|
||||
<div className="widget-content-outer">
|
||||
<div className="widget-content-wrapper">
|
||||
<div className="widget-content-left">
|
||||
<div className="widget-heading">
|
||||
{t('restrictions.alertLicenses')}
|
||||
</div>
|
||||
<div className="widget-subheading">
|
||||
{t('restrictions.perMonth')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="widget-content-right">
|
||||
<div
|
||||
className={`widget-numbers text-${this.getBarColor(
|
||||
alertPerc
|
||||
)}`}
|
||||
>
|
||||
{alertsLimit}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="widget-progress-wrapper">
|
||||
<Progress
|
||||
className="progress-bar-sm progress-bar-animated-alt"
|
||||
color={this.getBarColor(alertPerc)}
|
||||
value={alertPerc}
|
||||
/>
|
||||
<div className="progress-sub-label">
|
||||
{alerts} / {alertsLimit}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
{/* {restrictions.newsletters && (
|
||||
<Col md="6" xl="4">
|
||||
<div className="card mb-3 widget-content">
|
||||
<div className="widget-content-outer">
|
||||
<div className="widget-content-wrapper">
|
||||
<div className="widget-content-left">
|
||||
<div className="widget-heading">{t('restrictions.totalNewsltter')}</div>
|
||||
</div>
|
||||
<div className="widget-content-right">
|
||||
<div className="widget-numbers text-primary">
|
||||
{restrictions.newsletters.limit}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="widget-progress-wrapper">
|
||||
<Progress
|
||||
className="progress-bar-sm progress-bar-animated-alt"
|
||||
color="warning"
|
||||
value={20}
|
||||
/>
|
||||
<div className="progress-sub-label">
|
||||
{restrictions.newsletters.current} /{' '}
|
||||
{restrictions.newsletters.limit}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
)} */}
|
||||
<Col md="6" xl="4">
|
||||
<div className="card mb-3 widget-content">
|
||||
<div className="widget-content-outer">
|
||||
<div className="widget-content-wrapper">
|
||||
<div className="widget-content-left">
|
||||
<div className="widget-heading">
|
||||
{t('restrictions.webfeedLicenses')}
|
||||
</div>
|
||||
<div className="widget-subheading">
|
||||
{t('restrictions.perMonth')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="widget-content-right">
|
||||
<div
|
||||
className={`widget-numbers text-${this.getBarColor(
|
||||
webFeedPerc
|
||||
)}`}
|
||||
>
|
||||
{webFeedsLimit}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="widget-progress-wrapper">
|
||||
<Progress
|
||||
className="progress-bar-sm progress-bar-animated-alt"
|
||||
color={this.getBarColor(webFeedPerc)}
|
||||
value={webFeedPerc}
|
||||
/>
|
||||
<div className="progress-sub-label">
|
||||
{webFeeds} / {webFeedsLimit}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
) : (
|
||||
<Row data-tour="search-licenses">
|
||||
<Col md="6" xl="4">
|
||||
<div className="card mb-3 widget-content">
|
||||
<div className="widget-content-outer">
|
||||
<div className="widget-content-wrapper">
|
||||
<div className="widget-content-left">
|
||||
<div className="widget-heading">
|
||||
{t('restrictions.searchLicenses')}
|
||||
</div>
|
||||
<div className="widget-subheading">
|
||||
{t('restrictions.perDay')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="widget-content-right">
|
||||
<div
|
||||
className={`widget-numbers text-${this.getBarColor(
|
||||
searchPerc
|
||||
)}`}
|
||||
>
|
||||
{searchLicenseLimit}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="widget-progress-wrapper">
|
||||
<Progress
|
||||
className="progress-bar-sm progress-bar-animated-alt"
|
||||
color={this.getBarColor(searchPerc)}
|
||||
value={searchPerc}
|
||||
/>
|
||||
<div className="progress-sub-label">
|
||||
{searchLicense} / {searchLicenseLimit}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col md="6" xl="4">
|
||||
<div className="card mb-3 widget-content">
|
||||
<div className="widget-content-outer">
|
||||
<div className="widget-content-wrapper">
|
||||
<div className="widget-content-left">
|
||||
<div className="widget-heading">
|
||||
{t('restrictions.feedLicenses')}
|
||||
</div>
|
||||
<div className="widget-subheading">
|
||||
{t('restrictions.perMonth')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="widget-content-right">
|
||||
<div
|
||||
className={`widget-numbers text-${this.getBarColor(
|
||||
feedPerc
|
||||
)}`}
|
||||
>
|
||||
{saveLicenseLimit}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="widget-progress-wrapper">
|
||||
<Progress
|
||||
className="progress-bar-sm progress-bar-animated-alt"
|
||||
color={this.getBarColor(feedPerc)}
|
||||
value={feedPerc}
|
||||
/>
|
||||
<div className="progress-sub-label">
|
||||
{saveLicense} / {saveLicenseLimit}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(Restrictions);
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate } from 'react-i18next'
|
||||
|
||||
export class CheckboxCell extends React.Component {
|
||||
static propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
id: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number
|
||||
]),
|
||||
checked: PropTypes.bool.isRequired,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
onChange = () => {
|
||||
this.props.onChange(this.props.id)
|
||||
};
|
||||
|
||||
render () {
|
||||
return (
|
||||
<input type="checkbox" checked={this.props.checked} onChange={this.onChange} />
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(CheckboxCell)
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate } from 'react-i18next'
|
||||
import { Button } from 'reactstrap'
|
||||
|
||||
export class DeleteButton extends React.Component {
|
||||
static propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
id: PropTypes.number.isRequired,
|
||||
onDelete: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
onDelete = () => {
|
||||
this.props.onDelete(this.props.id)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Button color="link" className="text-danger p-0" onClick={this.onDelete}>
|
||||
<i className="lnr lnr-trash" />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(DeleteButton)
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate } from 'react-i18next'
|
||||
import { Button } from 'reactstrap';
|
||||
|
||||
export class LinkCell extends React.Component {
|
||||
static propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
item: PropTypes.object.isRequired,
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.element
|
||||
]),
|
||||
onClick: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
onClick = () => {
|
||||
this.props.onClick(this.props.item)
|
||||
};
|
||||
|
||||
render () {
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
color="link"
|
||||
className="btn-anchor"
|
||||
onClick={this.onClick}
|
||||
>
|
||||
{this.props.children}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(LinkCell)
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate } from 'react-i18next'
|
||||
|
||||
export class SortableTh extends React.Component {
|
||||
static propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
title: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
render () {
|
||||
const { t, title } = this.props
|
||||
|
||||
return (
|
||||
<div className="sortable-th">
|
||||
{t(title)}
|
||||
|
||||
<i className="sorting-icon fa fa-sort" />
|
||||
<i className="sorting-icon fa fa-sort-asc" />
|
||||
<i className="sorting-icon fa fa-sort-desc" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(SortableTh)
|
||||
@@ -0,0 +1,163 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate, Interpolate } from 'react-i18next';
|
||||
import ReactTable from 'react-table';
|
||||
import 'react-table/react-table.css';
|
||||
import Pager from '../Pager/Pager';
|
||||
import { Row, Col, Card, CardBody, CardTitle } from 'reactstrap';
|
||||
import LoadersAdvanced from '../Loader/Loader';
|
||||
|
||||
// const steps = [
|
||||
// { name: 'Account Information' },
|
||||
// { name: 'Payment Information' },
|
||||
// { name: 'Finish Wizard' }
|
||||
// ]
|
||||
|
||||
export class Table extends React.Component {
|
||||
static propTypes = {
|
||||
t: PropTypes.func,
|
||||
data: PropTypes.array.isRequired,
|
||||
columns: PropTypes.array.isRequired,
|
||||
totalCount: PropTypes.number.isRequired,
|
||||
showTotalCount: PropTypes.bool,
|
||||
noCard: PropTypes.bool,
|
||||
limit: PropTypes.number.isRequired,
|
||||
page: PropTypes.number.isRequired,
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
onFetchData: PropTypes.func.isRequired,
|
||||
onRowClick: PropTypes.func,
|
||||
cardTitle: PropTypes.string
|
||||
};
|
||||
|
||||
onFetchData = (state) => {
|
||||
this.props.onFetchData(
|
||||
state.page,
|
||||
state.pageSize,
|
||||
state.sorted,
|
||||
state.filtered
|
||||
);
|
||||
};
|
||||
|
||||
onPageAction = (pageState) => {
|
||||
const { totalCount } = this.props;
|
||||
const gridState = this.refs.grid.state;
|
||||
const { page, pageSize, sorted, filtered } = gridState;
|
||||
let state = { page, pageSize, sorted, filtered };
|
||||
|
||||
if (pageState.limitByPage) {
|
||||
state.pageSize = pageState.limitByPage;
|
||||
}
|
||||
if (pageState.currentPage) {
|
||||
state.page = pageState.currentPage - 1;
|
||||
}
|
||||
if (totalCount < state.pageSize) {
|
||||
state.page = 0;
|
||||
}
|
||||
|
||||
this.onFetchData(state);
|
||||
};
|
||||
|
||||
getPagination = () => {
|
||||
const { showTotalCount = false, totalCount, page, limit } = this.props;
|
||||
const numPages = Math.ceil(totalCount / limit);
|
||||
|
||||
return totalCount > 0 ? (
|
||||
<div className="px-3">
|
||||
{showTotalCount && (
|
||||
<div className="results-table-count-info">
|
||||
<Interpolate
|
||||
i18nKey="sourceIndexTab.showingCounter"
|
||||
startCount={limit * page - (limit - 1)}
|
||||
endCount={limit * page}
|
||||
totalCount={totalCount}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Pager
|
||||
pagerAction={this.onPageAction}
|
||||
currentPage={page}
|
||||
limitByPage={limit}
|
||||
numPages={numPages}
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
getLoading = (props) => {
|
||||
if (!props.loading) return null;
|
||||
|
||||
return <LoadersAdvanced />;
|
||||
// <div className="component-loader"></div>;
|
||||
};
|
||||
|
||||
getTrProps = (state, rowInfo, column, instance) => {
|
||||
const { onRowClick } = this.props;
|
||||
let result = {};
|
||||
if (onRowClick) {
|
||||
result.onClick = (e) => {
|
||||
onRowClick(e, state, rowInfo, column, instance);
|
||||
};
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
NoDataConst = () => (
|
||||
<div className="p-4 text-center text-black-50">
|
||||
{this.props.t('common:messages.noRows', {
|
||||
defaultValue: 'No rows found'
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
render() {
|
||||
const {
|
||||
data,
|
||||
columns,
|
||||
page,
|
||||
limit,
|
||||
isLoading,
|
||||
cardTitle,
|
||||
noCard
|
||||
} = this.props;
|
||||
|
||||
const renderTable = (
|
||||
<ReactTable
|
||||
ref="grid"
|
||||
className="cw-grid -striped"
|
||||
data={data}
|
||||
columns={columns}
|
||||
minRows={0}
|
||||
manual
|
||||
page={page - 1}
|
||||
pageSize={limit}
|
||||
defaultPageSize={10}
|
||||
loading={isLoading}
|
||||
PaginationComponent={this.getPagination}
|
||||
LoadingComponent={this.getLoading}
|
||||
onFetchData={this.onFetchData}
|
||||
getTrProps={this.getTrProps}
|
||||
NoDataComponent={this.NoDataConst}
|
||||
/>
|
||||
);
|
||||
|
||||
if (noCard) {
|
||||
return renderTable;
|
||||
}
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Col md="12">
|
||||
<Card className="main-card mb-3">
|
||||
<CardBody>
|
||||
{cardTitle && <CardTitle>{cardTitle}</CardTitle>}
|
||||
{renderTable}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(Table);
|
||||
@@ -0,0 +1,50 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate } from 'react-i18next'
|
||||
import { ButtonGroup, Button } from 'reactstrap'
|
||||
|
||||
export class Toggler extends React.Component {
|
||||
static propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
turnOnAction: PropTypes.func.isRequired,
|
||||
turnOffAction: PropTypes.func.isRequired,
|
||||
state: PropTypes.bool.isRequired,
|
||||
enabledText: PropTypes.string.isRequired,
|
||||
disabledText: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
onOnClick = () => {
|
||||
!this.props.state && this.props.turnOnAction(this.props.id)
|
||||
};
|
||||
|
||||
onOffClick = () => {
|
||||
this.props.state && this.props.turnOffAction(this.props.id)
|
||||
};
|
||||
|
||||
render () {
|
||||
const { enabledText, disabledText, state, t } = this.props
|
||||
return (
|
||||
<ButtonGroup size="sm">
|
||||
<Button
|
||||
outline
|
||||
color="success"
|
||||
onClick={this.onOnClick}
|
||||
active={state}
|
||||
>
|
||||
{t('toggler.' + enabledText)}
|
||||
</Button>
|
||||
<Button
|
||||
outline
|
||||
color="success"
|
||||
onClick={this.onOffClick}
|
||||
active={!state}
|
||||
>
|
||||
{t('toggler.' + disabledText)}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(Toggler)
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate } from 'react-i18next'
|
||||
|
||||
export class TableHeaderSortItem extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
isSorted: PropTypes.bool.isRequired,
|
||||
sortDirection: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
width: PropTypes.string,
|
||||
sortAction: PropTypes.func.isRequired,
|
||||
fieldName: PropTypes.string.isRequired,
|
||||
textAlign: PropTypes.string
|
||||
};
|
||||
|
||||
onClick = () => {
|
||||
const sortDirection = (!this.props.isSorted || this.props.sortDirection === 'desc') ? 'asc' : 'desc'
|
||||
this.props.sortAction({sortByField: this.props.fieldName, sortDirection: sortDirection})
|
||||
};
|
||||
|
||||
render () {
|
||||
const {t, isSorted, sortDirection} = this.props
|
||||
return (
|
||||
<th style={{
|
||||
textAlign: this.props.textAlign || '',
|
||||
width: this.props.width || 'auto'
|
||||
}}>
|
||||
|
||||
{t(this.props.title)}
|
||||
|
||||
{!isSorted &&
|
||||
<i className="sorting-icon fa fa-sort" onClick={this.onClick}></i>
|
||||
}
|
||||
|
||||
{isSorted && sortDirection === 'asc' &&
|
||||
<i className="sorting-icon fa fa-sort-asc" onClick={this.onClick}></i>
|
||||
}
|
||||
|
||||
{isSorted && sortDirection === 'desc' &&
|
||||
<i className="sorting-icon fa fa-sort-desc" onClick={this.onClick}></i>
|
||||
}
|
||||
|
||||
</th>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(TableHeaderSortItem)
|
||||
@@ -0,0 +1,53 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate, Interpolate } from 'react-i18next'
|
||||
|
||||
export class Alert extends React.Component {
|
||||
static propTypes = {
|
||||
alert: PropTypes.func.isRequired,
|
||||
removeAlert: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
componentDidMount = () => {
|
||||
setTimeout(this.closeAlert, 5000)
|
||||
};
|
||||
|
||||
closeAlert = () => {
|
||||
const { removeAlert, alert } = this.props
|
||||
removeAlert(alert.id)
|
||||
};
|
||||
|
||||
onClose = (e) => {
|
||||
e.preventDefault()
|
||||
this.closeAlert()
|
||||
};
|
||||
|
||||
render () {
|
||||
const { alert } = this.props
|
||||
const interpolateParameters = alert.parameters || {}
|
||||
return (
|
||||
<div className={'alert ' + alert.type}>
|
||||
<div className="alert__head">
|
||||
<h2 className="alert__title">{alert.type}</h2>
|
||||
|
||||
<a href="#" className="alert__close-btn" onClick={this.onClose}>
|
||||
<i className="fa fa-times"> </i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="alert__body">
|
||||
<p className="alert__text">
|
||||
<Interpolate
|
||||
i18nKey={'alerts.' + alert.type + '.' + alert.transKey}
|
||||
{...interpolateParameters}
|
||||
options={{defaultValue: alert.message || 'Unknown error'}}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['common'], { wait: true })(Alert)
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Alert from './Alert'
|
||||
|
||||
export class Alerts extends React.Component {
|
||||
static propTypes = {
|
||||
alerts: PropTypes.array.isRequired,
|
||||
removeAlert: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
forEachAlert = (callback) => {
|
||||
return this.props.alerts
|
||||
.map((alert) => {
|
||||
return (typeof alert === 'string') ? {message: alert} : alert
|
||||
})
|
||||
.map(callback)
|
||||
};
|
||||
|
||||
render () {
|
||||
const { alerts, removeAlert } = this.props
|
||||
if (alerts.length <= 0) return null
|
||||
|
||||
return (
|
||||
<div className="alerts-container">
|
||||
{this.forEachAlert((alert, i) => {
|
||||
return (
|
||||
<Alert
|
||||
key={i}
|
||||
removeAlert={removeAlert}
|
||||
alert={alert}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Alerts
|
||||
@@ -0,0 +1,138 @@
|
||||
export const BarToolbox = {
|
||||
feature: {
|
||||
saveAsImage: {
|
||||
name: 'Socialhose Chart',
|
||||
show: true,
|
||||
title: 'Save as Image',
|
||||
// name: 'Results Over Time', // need to set dynamic
|
||||
type: 'jpeg',
|
||||
backgroundColor: '#FFFFFF',
|
||||
pixelRatio: 2
|
||||
},
|
||||
dataZoom: {
|
||||
show: true,
|
||||
title: {
|
||||
zoom: 'Zoom',
|
||||
back: 'Restore Zoom'
|
||||
}
|
||||
},
|
||||
dataView: {
|
||||
show: true,
|
||||
title: 'View Data',
|
||||
readOnly: true,
|
||||
lang: ['View Data', 'Close', 'Refresh']
|
||||
},
|
||||
magicType: {
|
||||
show: true,
|
||||
title: {
|
||||
line: 'Line Chart',
|
||||
bar: 'Bar Chart',
|
||||
tiled: 'Tiled Chart'
|
||||
},
|
||||
type: ['line', 'bar', 'tiled']
|
||||
},
|
||||
restore: { show: true, title: 'Restore' }
|
||||
}
|
||||
}
|
||||
|
||||
export const PieToolbox = {
|
||||
feature: {
|
||||
saveAsImage: {
|
||||
name: 'Socialhose Chart',
|
||||
show: true,
|
||||
title: 'Save as Image',
|
||||
// name: 'Share of Topics', // need to set dynamic
|
||||
type: 'jpeg',
|
||||
backgroundColor: '#FFFFFF',
|
||||
pixelRatio: 2
|
||||
},
|
||||
dataView: {
|
||||
show: true,
|
||||
readOnly: true,
|
||||
title: 'View Data',
|
||||
lang: ['View Data', 'Close', 'Refresh']
|
||||
},
|
||||
restore: { show: true, title: 'Restore' }
|
||||
}
|
||||
}
|
||||
|
||||
export function getBarOptions(data, labels) {
|
||||
return {
|
||||
tooltip: {
|
||||
show: true
|
||||
},
|
||||
toolbox: BarToolbox,
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: labels
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: data,
|
||||
legend: {
|
||||
y: 'bottom',
|
||||
show: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getPieOptions(data) {
|
||||
return {
|
||||
tooltip: {
|
||||
show: true
|
||||
},
|
||||
toolbox: PieToolbox,
|
||||
series: {
|
||||
type: 'pie',
|
||||
data: data,
|
||||
label: {
|
||||
position: 'outer',
|
||||
alignTo: 'none',
|
||||
bleedMargin: 5
|
||||
},
|
||||
top: '10%',
|
||||
bottom: '10%'
|
||||
},
|
||||
legend: {
|
||||
top: 'bottom',
|
||||
show: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const WordCloudOptions = {
|
||||
type: 'wordCloud',
|
||||
shape: 'circle',
|
||||
sizeRange: [12, 35],
|
||||
rotationRange: [0, 0],
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
top: '10%',
|
||||
bottom: '10%',
|
||||
drawOutOfBound: false,
|
||||
gridSize: 8,
|
||||
textStyle: {
|
||||
normal: {
|
||||
fontFamily: 'sans-serif',
|
||||
fontWeight: 'bold',
|
||||
// Color can be a callback function or a color string
|
||||
color: function () {
|
||||
// Random color
|
||||
return (
|
||||
'rgb(' +
|
||||
[
|
||||
Math.round(Math.random() * 160),
|
||||
Math.round(Math.random() * 160),
|
||||
Math.round(Math.random() * 160)
|
||||
].join(',') +
|
||||
')'
|
||||
)
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
shadowBlur: 1,
|
||||
shadowColor: '#333'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import echarts from 'echarts'
|
||||
import cx from 'classnames'
|
||||
|
||||
function ECharts(props) {
|
||||
const { options, style, className, loading, message } = props
|
||||
const [chart, setChart] = useState(null)
|
||||
const chartRef = useRef()
|
||||
|
||||
useEffect(() => {
|
||||
const chart = echarts.init(chartRef.current, 'westeros')
|
||||
chart.setOption({ ...options, resizeObserver }, true) // second param is for 'noMerge'
|
||||
setChart(chart)
|
||||
if (resizeObserver) resizeObserver.observe(chartRef.current)
|
||||
}, [options])
|
||||
|
||||
useEffect(() => {
|
||||
if (!chart) {
|
||||
return
|
||||
}
|
||||
if (loading) {
|
||||
chart.showLoading()
|
||||
return
|
||||
}
|
||||
|
||||
chart.hideLoading()
|
||||
}, [chart, loading])
|
||||
|
||||
useEffect(() => {
|
||||
if (chart && options && message) {
|
||||
chart.clear()
|
||||
}
|
||||
}, [message])
|
||||
|
||||
const newStyle = {
|
||||
height: 350,
|
||||
...style
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="echarts-parent position-relative">
|
||||
<div
|
||||
ref={chartRef}
|
||||
style={newStyle}
|
||||
className={cx('echarts-react', className)}
|
||||
/>
|
||||
{message ? <div className="no-data">{message}</div> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ECharts.propTypes = {
|
||||
loading: PropTypes.bool,
|
||||
options: PropTypes.any,
|
||||
className: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
message: PropTypes.any
|
||||
}
|
||||
|
||||
const resizeObserver = new window.ResizeObserver((entries) => {
|
||||
entries.map(({ target }) => {
|
||||
const instance = echarts.getInstanceByDom(target)
|
||||
if (instance) {
|
||||
instance.resize()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
export default React.memo(ECharts)
|
||||
@@ -0,0 +1,382 @@
|
||||
{
|
||||
"color": ["#516b91", "#59c4e6", "#edafda", "#93b7e3", "#a5e7f0", "#cbb0e3"],
|
||||
"backgroundColor": "rgba(0,0,0,0)",
|
||||
"textStyle": {},
|
||||
"title": {
|
||||
"textStyle": {
|
||||
"color": "#516b91"
|
||||
},
|
||||
"subtextStyle": {
|
||||
"color": "#93b7e3"
|
||||
}
|
||||
},
|
||||
"line": {
|
||||
"itemStyle": {
|
||||
"borderWidth": "2"
|
||||
},
|
||||
"lineStyle": {
|
||||
"width": "2"
|
||||
},
|
||||
"symbolSize": "6",
|
||||
"symbol": "emptyCircle",
|
||||
"smooth": true
|
||||
},
|
||||
"radar": {
|
||||
"itemStyle": {
|
||||
"borderWidth": "2"
|
||||
},
|
||||
"lineStyle": {
|
||||
"width": "2"
|
||||
},
|
||||
"symbolSize": "6",
|
||||
"symbol": "emptyCircle",
|
||||
"smooth": true
|
||||
},
|
||||
"bar": {
|
||||
"itemStyle": {
|
||||
"barBorderWidth": 0,
|
||||
"barBorderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"pie": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"scatter": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"boxplot": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"parallel": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"sankey": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"funnel": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"gauge": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"candlestick": {
|
||||
"itemStyle": {
|
||||
"color": "#edafda",
|
||||
"color0": "transparent",
|
||||
"borderColor": "#d680bc",
|
||||
"borderColor0": "#8fd3e8",
|
||||
"borderWidth": "2"
|
||||
}
|
||||
},
|
||||
"graph": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
},
|
||||
"lineStyle": {
|
||||
"width": 1,
|
||||
"color": "#aaaaaa"
|
||||
},
|
||||
"symbolSize": "6",
|
||||
"symbol": "emptyCircle",
|
||||
"smooth": true,
|
||||
"color": ["#516b91", "#59c4e6", "#edafda", "#93b7e3", "#a5e7f0", "#cbb0e3"],
|
||||
"label": {
|
||||
"color": "#eeeeee"
|
||||
}
|
||||
},
|
||||
"map": {
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"areaColor": "#f3f3f3",
|
||||
"borderColor": "#516b91",
|
||||
"borderWidth": 0.5
|
||||
},
|
||||
"emphasis": {
|
||||
"areaColor": "#a5e7f0",
|
||||
"borderColor": "#516b91",
|
||||
"borderWidth": 1
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"normal": {
|
||||
"textStyle": {
|
||||
"color": "#000"
|
||||
}
|
||||
},
|
||||
"emphasis": {
|
||||
"textStyle": {
|
||||
"color": "#516b91"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"geo": {
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"areaColor": "#f3f3f3",
|
||||
"borderColor": "#516b91",
|
||||
"borderWidth": 0.5
|
||||
},
|
||||
"emphasis": {
|
||||
"areaColor": "#a5e7f0",
|
||||
"borderColor": "#516b91",
|
||||
"borderWidth": 1
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"normal": {
|
||||
"textStyle": {
|
||||
"color": "#000"
|
||||
}
|
||||
},
|
||||
"emphasis": {
|
||||
"textStyle": {
|
||||
"color": "#516b91"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"categoryAxis": {
|
||||
"axisLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": "#cccccc"
|
||||
}
|
||||
},
|
||||
"axisTick": {
|
||||
"show": false,
|
||||
"lineStyle": {
|
||||
"color": "#333"
|
||||
}
|
||||
},
|
||||
"axisLabel": {
|
||||
"show": true,
|
||||
"textStyle": {
|
||||
"color": "#999999"
|
||||
}
|
||||
},
|
||||
"splitLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": ["#eeeeee"]
|
||||
}
|
||||
},
|
||||
"splitArea": {
|
||||
"show": false,
|
||||
"areaStyle": {
|
||||
"color": ["rgba(250,250,250,0.05)", "rgba(200,200,200,0.02)"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"valueAxis": {
|
||||
"axisLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": "#cccccc"
|
||||
}
|
||||
},
|
||||
"axisTick": {
|
||||
"show": false,
|
||||
"lineStyle": {
|
||||
"color": "#333"
|
||||
}
|
||||
},
|
||||
"axisLabel": {
|
||||
"show": true,
|
||||
"textStyle": {
|
||||
"color": "#999999"
|
||||
}
|
||||
},
|
||||
"splitLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": ["#eeeeee"]
|
||||
}
|
||||
},
|
||||
"splitArea": {
|
||||
"show": false,
|
||||
"areaStyle": {
|
||||
"color": ["rgba(250,250,250,0.05)", "rgba(200,200,200,0.02)"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"logAxis": {
|
||||
"axisLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": "#cccccc"
|
||||
}
|
||||
},
|
||||
"axisTick": {
|
||||
"show": false,
|
||||
"lineStyle": {
|
||||
"color": "#333"
|
||||
}
|
||||
},
|
||||
"axisLabel": {
|
||||
"show": true,
|
||||
"textStyle": {
|
||||
"color": "#999999"
|
||||
}
|
||||
},
|
||||
"splitLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": ["#eeeeee"]
|
||||
}
|
||||
},
|
||||
"splitArea": {
|
||||
"show": false,
|
||||
"areaStyle": {
|
||||
"color": ["rgba(250,250,250,0.05)", "rgba(200,200,200,0.02)"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeAxis": {
|
||||
"axisLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": "#cccccc"
|
||||
}
|
||||
},
|
||||
"axisTick": {
|
||||
"show": false,
|
||||
"lineStyle": {
|
||||
"color": "#333"
|
||||
}
|
||||
},
|
||||
"axisLabel": {
|
||||
"show": true,
|
||||
"textStyle": {
|
||||
"color": "#999999"
|
||||
}
|
||||
},
|
||||
"splitLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": ["#eeeeee"]
|
||||
}
|
||||
},
|
||||
"splitArea": {
|
||||
"show": false,
|
||||
"areaStyle": {
|
||||
"color": ["rgba(250,250,250,0.05)", "rgba(200,200,200,0.02)"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"toolbox": {
|
||||
"iconStyle": {
|
||||
"normal": {
|
||||
"borderColor": "#999999"
|
||||
},
|
||||
"emphasis": {
|
||||
"borderColor": "#516b91"
|
||||
}
|
||||
}
|
||||
},
|
||||
"legend": {
|
||||
"textStyle": {
|
||||
"color": "#999999"
|
||||
}
|
||||
},
|
||||
"tooltip": {
|
||||
"axisPointer": {
|
||||
"lineStyle": {
|
||||
"color": "#cccccc",
|
||||
"width": 1
|
||||
},
|
||||
"crossStyle": {
|
||||
"color": "#cccccc",
|
||||
"width": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeline": {
|
||||
"lineStyle": {
|
||||
"color": "#8fd3e8",
|
||||
"width": 1
|
||||
},
|
||||
"itemStyle": {
|
||||
"normal": {
|
||||
"color": "#8fd3e8",
|
||||
"borderWidth": 1
|
||||
},
|
||||
"emphasis": {
|
||||
"color": "#8fd3e8"
|
||||
}
|
||||
},
|
||||
"controlStyle": {
|
||||
"normal": {
|
||||
"color": "#8fd3e8",
|
||||
"borderColor": "#8fd3e8",
|
||||
"borderWidth": 0.5
|
||||
},
|
||||
"emphasis": {
|
||||
"color": "#8fd3e8",
|
||||
"borderColor": "#8fd3e8",
|
||||
"borderWidth": 0.5
|
||||
}
|
||||
},
|
||||
"checkpointStyle": {
|
||||
"color": "#8fd3e8",
|
||||
"borderColor": "rgba(138,124,168,0.37)"
|
||||
},
|
||||
"label": {
|
||||
"normal": {
|
||||
"textStyle": {
|
||||
"color": "#8fd3e8"
|
||||
}
|
||||
},
|
||||
"emphasis": {
|
||||
"textStyle": {
|
||||
"color": "#8fd3e8"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"visualMap": {
|
||||
"color": ["#516b91", "#59c4e6", "#a5e7f0"]
|
||||
},
|
||||
"dataZoom": {
|
||||
"backgroundColor": "rgba(0,0,0,0)",
|
||||
"dataBackgroundColor": "rgba(255,255,255,0.3)",
|
||||
"fillerColor": "rgba(167,183,204,0.4)",
|
||||
"handleColor": "#a7b7cc",
|
||||
"handleSize": "100%",
|
||||
"textStyle": {
|
||||
"color": "#333333"
|
||||
}
|
||||
},
|
||||
"markPoint": {
|
||||
"label": {
|
||||
"color": "#eeeeee"
|
||||
},
|
||||
"emphasis": {
|
||||
"label": {
|
||||
"color": "#eeeeee"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
function useForm({ errors: initialErrors, ...rest }) {
|
||||
const [form, setForm] = useState(rest);
|
||||
const [errors, setErrors] = useState(initialErrors);
|
||||
|
||||
const handleChange = useCallback((name, value, err = undefined) => {
|
||||
setForm((form) => ({
|
||||
...form,
|
||||
[name]: value
|
||||
}));
|
||||
|
||||
if (errors) {
|
||||
setErrors((errors) => ({
|
||||
...errors,
|
||||
[name]: err !== undefined ? err : errors[name]
|
||||
}));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleValidation = useCallback((name, err) => {
|
||||
setErrors((errors) => ({
|
||||
...errors,
|
||||
[name]: err
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const validateSubmit = useCallback(() => {
|
||||
let failed;
|
||||
for (let val in errors) {
|
||||
const fieldError = errors[val];
|
||||
if (fieldError) {
|
||||
failed = true;
|
||||
} else if (fieldError === null && !form[val] && form[val] !== 0) {
|
||||
failed = true;
|
||||
handleValidation(val, true);
|
||||
}
|
||||
}
|
||||
if (failed) {
|
||||
return false;
|
||||
} else {
|
||||
return cloneDeep(form);
|
||||
}
|
||||
}, [form, errors, handleValidation]);
|
||||
|
||||
function resetForm(values = {}) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { errors, ...formValues } = values;
|
||||
setForm(formValues || rest);
|
||||
setErrors(initialErrors);
|
||||
}
|
||||
|
||||
return {
|
||||
form,
|
||||
handleChange,
|
||||
handleValidation,
|
||||
setForm,
|
||||
validateSubmit,
|
||||
errors,
|
||||
resetForm
|
||||
};
|
||||
}
|
||||
|
||||
export default useForm;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useRef, useEffect } from 'react';
|
||||
|
||||
function useIsMounted() {
|
||||
const isMounted = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
isMounted.current = true;
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return isMounted;
|
||||
}
|
||||
|
||||
export default useIsMounted;
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { isLive } from '../../../common/constants';
|
||||
|
||||
function usePageTracking() {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
|
||||
useEffect(() => {
|
||||
// google analytics
|
||||
if (window.gtag && isLive) {
|
||||
setTimeout(() => {
|
||||
window.gtag('event', 'page_view', {
|
||||
page_location: window.location.href,
|
||||
page_path: location.pathname + location.search
|
||||
// page_title: '<Page Title>',
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
}, [window.gtag, location]);
|
||||
|
||||
useEffect(() => {
|
||||
isLive &&
|
||||
history.listen(() => {
|
||||
// Added to history listen to prevent first pageview call which is called by hubspot tracking script
|
||||
setTimeout(() => { // to wait until document title updates
|
||||
const _hsq = window._hsq;
|
||||
if (location && _hsq) {
|
||||
// hubspot tracking
|
||||
_hsq.push(['setPath', location.pathname + location.search]);
|
||||
_hsq.push(['trackPageView']);
|
||||
}
|
||||
|
||||
if (location && window.lintrk) {
|
||||
// linkedin insight tracking
|
||||
window.lintrk('track');
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default usePageTracking;
|
||||
@@ -0,0 +1,35 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Modal } from 'reactstrap'
|
||||
|
||||
function ModalPopup(props) {
|
||||
const { children, modalProps, show, hideModal, handled } = props
|
||||
const [open, setOpen] = useState(true)
|
||||
|
||||
useEffect(() => setOpen(false)) // when unmounts
|
||||
|
||||
function toggle() {
|
||||
setOpen((prev) => !prev)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={show || open}
|
||||
toggle={handled ? hideModal : toggle}
|
||||
backdrop="static"
|
||||
{...modalProps}
|
||||
>
|
||||
{children && (handled ? children : children(toggle, open))}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
ModalPopup.propTypes = {
|
||||
handled: PropTypes.bool,
|
||||
show: PropTypes.bool,
|
||||
hideModal: PropTypes.func,
|
||||
children: PropTypes.func.isRequired,
|
||||
modalProps: PropTypes.object
|
||||
}
|
||||
|
||||
export default React.memo(ModalPopup)
|
||||
Reference in New Issue
Block a user