at the end of the day, it was inevitable

This commit is contained in:
Mo Elzubeir
2022-12-09 08:36:26 -06:00
commit 1218570914
1768 changed files with 887087 additions and 0 deletions
@@ -0,0 +1,110 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import HeaderSettings from './HeaderSettings';
import SettingsPopup from './SettingsPopup';
import cx from 'classnames';
import MainTabsLinks from './MainTabsLinks';
import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup';
import HeaderLogo from './HeaderLogo';
import HeaderDots from './HeaderDots';
export class AppHeader extends React.Component {
static propTypes = {
appCommonState: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,
userFirstName: PropTypes.string,
userLastName: PropTypes.string,
userRole: PropTypes.string.isRequired,
restrictions: PropTypes.object.isRequired,
themeOptions: PropTypes.object.isRequired
};
state = {
active: false,
mobile: false,
activeSecondaryMenuMobile: false
};
toggleResponsiveMenu = () => {
this.props.actions.toggleSidebar();
};
activeSearchFunc = () => {
this.setState({ active: !this.state.active });
};
render() {
const {
appCommonState,
restrictions,
actions,
userFirstName,
userLastName,
themeOptions
} = this.props;
const mainTabs = Object.keys(appCommonState.tabs);
const {
headerBackgroundColor,
enableHeaderShadow,
enableMobileMenuSmall
} = themeOptions;
const settingsPopupVisible = appCommonState.isSettingsPopupVisible;
return (
<Fragment>
<CSSTransitionGroup
component="div"
className={cx('app-header', headerBackgroundColor, {
'header-shadow': enableHeaderShadow
})}
transitionName="HeaderAnimation"
transitionAppear
transitionAppearTimeout={1500}
transitionEnter={false}
transitionLeave={false}
>
<HeaderLogo />
<div
className={cx('app-header__content', {
'header-mobile-open': enableMobileMenuSmall
})}
>
<div className="app-header-left" data-tour="app-header-left">
<MainTabsLinks
tabs={appCommonState.tabs}
restrictions={restrictions}
actions={actions}
/>
</div>
<div className="app-header-right">
<HeaderDots
mainTabs={mainTabs}
restrictions={restrictions}
planDetails={restrictions.plans}
/>
<HeaderSettings
isThereSomethingNew={appCommonState.isThereSomethingNew}
langs={appCommonState.langs}
userFirstName={userFirstName}
userLastName={userLastName}
/>
</div>
</div>
{settingsPopupVisible && (
<SettingsPopup
hidePopup={actions.hideUserSettingsPopup}
setErrorMsg={actions.setSettingsPopupError}
changePassword={actions.changeUserPassword}
errorMsg={appCommonState.settingsPopupError}
/>
)}
</CSSTransitionGroup>
</Fragment>
);
}
}
export default AppHeader;
@@ -0,0 +1,99 @@
/* eslint-disable react/jsx-no-bind */
import PropTypes from 'prop-types';
import React, { Fragment } from 'react';
import { Slider } from 'react-burgers';
import cx from 'classnames';
import { faEllipsisV } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Button } from 'reactstrap';
import reduxConnect from '../../../redux/utils/connect';
import translate from 'react-i18next/dist/commonjs/translate';
import { compose } from 'redux';
class AppMobileMenu extends React.Component {
constructor(props) {
super(props);
this.state = {
active: false,
mobile: false,
activeSecondaryMenuMobile: false
};
}
toggleMobileSidebar = () => {
const { setEnableMobileMenu } = this.props.actions;
const { enableMobileMenu } = this.props.appState.themeOptions;
setEnableMobileMenu(!enableMobileMenu);
};
toggleMobileSmall = () => {
const { setEnableMobileMenuSmall } = this.props.actions;
const { enableMobileMenuSmall } = this.props.appState.themeOptions;
setEnableMobileMenuSmall(!enableMobileMenuSmall);
};
state = {
openLeft: false,
openRight: false,
relativeWidth: false,
width: 280,
noTouchOpen: false,
noTouchClose: false
};
changeActive = () => {
this.setState({ active: !this.state.active });
};
render() {
return (
<Fragment>
<div className="app-header__mobile-menu" data-tour="mobile-left-menu">
<div onClick={this.toggleMobileSidebar}>
<Slider
width={26}
lineHeight={2}
lineSpacing={5}
color="#6c757d"
active={this.state.active}
onClick={this.changeActive}
/>
</div>
</div>
<div className="app-header__menu">
<span onClick={this.toggleMobileSmall}>
<Button
size="sm"
className={cx('btn-icon btn-icon-only', {
active: this.state.activeSecondaryMenuMobile
})}
color="primary"
onClick={() =>
this.setState({
activeSecondaryMenuMobile: !this.state
.activeSecondaryMenuMobile
})
}
>
<div className="btn-icon-wrapper">
<FontAwesomeIcon icon={faEllipsisV} />
</div>
</Button>
</span>
</div>
</Fragment>
);
}
}
AppMobileMenu.propTypes = {
actions: PropTypes.object,
appState: PropTypes.object
};
const applyDecorators = compose(
reduxConnect('appState', ['appState']),
translate(['tabsContent'], { wait: true })
);
export default applyDecorators(AppMobileMenu);
@@ -0,0 +1,168 @@
/* eslint-disable no-unused-vars */
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { Link } from 'react-router-dom';
import {
UncontrolledDropdown,
DropdownToggle,
DropdownMenu,
Col,
Row,
Button,
DropdownItem
} from 'reactstrap';
import { IoIosGrid } from 'react-icons/io';
import Notifications from './Notifications';
import LangSettingsMenu from './LangSettingsMenu';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faAngleDown } from '@fortawesome/free-solid-svg-icons';
import { planRoutes } from '../Account/Plans/UserPlans';
import { convertUTCtoLocal } from '../../../common/helper';
class HeaderDots extends React.Component {
static propTypes = {
mainTabs: PropTypes.array.isRequired,
restrictions: PropTypes.object.isRequired,
planDetails: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
};
validateTab(tab) {
if (tab === 'analyze') {
if (!this.props.restrictions) {
return false;
}
const permissions = this.props.restrictions.permissions;
return permissions.analytics;
}
return true;
}
render() {
const { t, mainTabs, planDetails, restrictions } = this.props;
const isFreeAccount = planDetails.price === 0;
const isRTL = document.documentElement.dir === 'rtl';
return (
<div className="header-dots">
<UncontrolledDropdown nav inNavbar>
<DropdownToggle nav>
{t('plans.currentPlan')}
<FontAwesomeIcon className="ml-2 opacity-5" icon={faAngleDown} />
</DropdownToggle>
<DropdownMenu
className={`dropdown-menu-rounded rm-pointers${
isRTL ? ' dropdown-menu-left' : ''
}`}
>
<div className="dropdown-menu-header">
<div className="dropdown-menu-header-inner bg-success">
<div className="menu-header-image opacity-1"></div>
<div className="menu-header-content text-left">
<h5 className="menu-header-title font-weight-bold">
{isFreeAccount
? t('plans.freeBasicAccount')
: `$${planDetails.price}`}
</h5>
{!isFreeAccount && (
<p>
{restrictions.subStartDate && restrictions.subEndDate
? `${convertUTCtoLocal(
isRTL
? restrictions.subEndDate
: restrictions.subStartDate,
'MMM D, YYYY'
)} - ${convertUTCtoLocal(
isRTL
? restrictions.subStartDate
: restrictions.subEndDate,
'MMM D, YYYY'
)}`
: t('plans.perMonth')}
</p>
)}
</div>
</div>
</div>
<DropdownItem
tag={Link}
to={`/app/plans/${planRoutes.update}`}
className="font-weight-bold"
>
<i className="dropdown-icon lnr-rocket opacity-8"> </i>
{t('plans.upgradePlan')}
</DropdownItem>
<DropdownItem tag={Link} to={`/app/plans/${planRoutes.txn}`}>
<i className="dropdown-icon lnr-list"> </i>
{t('plans.yourTransactions')}
</DropdownItem>
{!isFreeAccount && (
<DropdownItem
tag={Link}
to={`/app/plans/${planRoutes.changeCard}`}
>
<i className="dropdown-icon lnr-license"> </i>
{t('plans.changeCard')}
</DropdownItem>
)}
</DropdownMenu>
</UncontrolledDropdown>
<Button
tag={Link}
to={`/app/plans/${planRoutes.update}`}
size="sm"
outline
color="success"
className="align-self-center mr-3 d-none d-lg-block"
>
{t('plans.upgradePlan')}
</Button>
<UncontrolledDropdown className="d-block d-lg-none">
<DropdownToggle className="p-0 mr-2" color="link">
<div className="icon-wrapper icon-wrapper-alt rounded-circle">
<div className="icon-wrapper-bg bg-primary" />
<IoIosGrid color="#3f6ad8" fontSize="23px" />
</div>
</DropdownToggle>
<DropdownMenu
className={`rm-pointers${isRTL ? ' dropdown-menu-left' : ''}`}
>
<div className="grid-menu grid-menu-xl grid-menu-3col">
{mainTabs.map((tab, i) => {
if (!this.validateTab(tab)) return null;
return (
<Col md="12" key={`main-tab-link-${i}`}>
<Button
className="btn-icon-vertical btn-square btn-transition"
outline
color="link"
>
<Link to={'/app/' + tab} className="nav-link">
<Row>
<i
className={
i
? 'lnr lnr-exit-up btn-icon-wrapper mr-1'
: 'lnr-magnifier btn-icon-wrapper mr-1'
}
></i>
<p>{t('tabs.' + tab)}</p>
</Row>
</Link>
</Button>
</Col>
);
})}
</div>
</DropdownMenu>
</UncontrolledDropdown>
<LangSettingsMenu />
<Notifications />
</div>
);
}
}
export default translate(['common'], { wait: true })(HeaderDots);
@@ -0,0 +1,74 @@
import PropTypes from 'prop-types'
import React, { Fragment } from 'react'
import { Slider } from 'react-burgers'
import { compose } from 'redux'
import reduxConnect from '../../../redux/utils/connect'
import translate from 'react-i18next/dist/commonjs/translate'
import AppMobileMenu from './AppMobileMenu'
class HeaderLogo extends React.Component {
constructor (props) {
super(props)
this.state = {
active: false,
mobile: false,
activeSecondaryMenuMobile: false
}
}
toggleEnableClosedSidebar = () => {
const { setEnableClosedSidebar } = this.props.actions
const { enableClosedSidebar } = this.props.appState.themeOptions
setEnableClosedSidebar(!enableClosedSidebar)
}
state = {
openLeft: false,
openRight: false,
relativeWidth: false,
width: 280,
noTouchOpen: false,
noTouchClose: false
}
changeActive = () => {
this.setState({ active: !this.state.active })
}
render () {
return (
<Fragment>
<div className="app-header__logo">
<div className="logo-src ml-0 ml-lg-2" />
<div className="header__pane ml-auto">
<div onClick={this.toggleEnableClosedSidebar}>
<Slider
width={26}
lineHeight={2}
lineSpacing={5}
color="#6c757d"
active={this.state.active}
onClick={this.changeActive}
/>
</div>
</div>
</div>
<AppMobileMenu />
</Fragment>
)
}
}
HeaderLogo.propTypes = {
actions: PropTypes.object,
appState: PropTypes.object
}
const applyDecorators = compose(
reduxConnect('appState', ['appState']),
translate(['tabsContent'], { wait: true })
)
export default applyDecorators(HeaderLogo)
@@ -0,0 +1,84 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import UserSettingsMenu from './UserSettingsMenu';
import { DropdownToggle, DropdownMenu, Dropdown } from 'reactstrap';
import { faAngleDown, faUser } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
export class HeaderSettings extends React.Component {
static propTypes = {
userFirstName: PropTypes.string.isRequired,
userLastName: PropTypes.string.isRequired
};
state = {
isOpen: false
};
toggleUserSettingsDrop = () => {
this.setState((prev) => ({ isOpen: !prev.isOpen }));
};
render() {
const { userFirstName, userLastName } = this.props;
const isRTL = document.documentElement.dir === 'rtl';
return (
<Fragment>
<div className="header-btn-lg pr-0">
<div className="widget-content p-0">
<div className="widget-content-wrapper">
<div className="widget-content-left">
<Dropdown
isOpen={this.state.isOpen}
toggle={this.toggleUserSettingsDrop}
>
<DropdownToggle
color="link"
title="User Profile"
className="d-flex align-items-center p-0"
data-tour="app-header-user-settings"
>
<div className="user-profile">
<FontAwesomeIcon
className="user-profile-icon"
icon={faUser}
/>
</div>
{window.outerWidth >= 768 && (
<FontAwesomeIcon
className="ml-2 opacity-8"
icon={faAngleDown}
/>
)}
</DropdownToggle>
<DropdownMenu
className={`rm-pointers dropdown-menu-lg${
isRTL ? ' dropdown-menu-left' : ''
}`}
>
<UserSettingsMenu
toggleMenu={this.toggleUserSettingsDrop}
userFirstName={userFirstName}
userLastName={userLastName}
/>
</DropdownMenu>
</Dropdown>
</div>
<div className="widget-content-left ml-3 header-user-info">
<div className="widget-heading">
{userFirstName + ' ' + userLastName}
</div>
</div>
</div>
</div>
</div>
</Fragment>
);
}
}
export default translate(['common'], { wait: true })(
React.memo(HeaderSettings)
);
@@ -0,0 +1,100 @@
import React from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import { translate } from 'react-i18next';
import i18n from '../../../i18n';
import {
UncontrolledDropdown,
DropdownToggle,
DropdownMenu,
DropdownItem
} from 'reactstrap';
import reduxConnect from '../../../redux/utils/connect';
import Flag from 'react-flagkit';
const langCountry = {
en: 'US',
ar: 'SA',
fr: 'FR'
};
function LangSettingsMenu(props) {
const {
t,
base: { langs, activeLang },
actions,
direction = ''
} = props;
const chooseLang = (e) => {
const newLang = e.target.dataset.lang;
actions.chooseLanguage(newLang);
i18n.changeLanguage(newLang);
};
const isRTL = document.documentElement.dir === 'rtl';
const dropDownProps = {};
if (direction) {
dropDownProps.direction = direction;
}
return (
<UncontrolledDropdown {...dropDownProps}>
<DropdownToggle className="p-0 mr-2" color="link">
<div className="icon-wrapper icon-wrapper-alt rounded-circle">
<div className="icon-wrapper-bg bg-focus" />
<div className="language-icon">
<Flag
className="mr-3 opacity-8"
country={langCountry[activeLang]}
size="40"
/>
</div>
</div>
</DropdownToggle>
<DropdownMenu
className={`rm-pointers${isRTL ? ' dropdown-menu-left' : ''}`}
>
<div className="dropdown-menu-header">
<div className="dropdown-menu-header-inner pt-4 pb-4 bg-focus">
<div className="menu-header-content text-center text-white">
<h6 className="menu-header-subtitle mt-0">
{t('langs.chooseLanguage')}
</h6>
</div>
</div>
</div>
{langs.map((lang, i) => {
const translateTarget = 'langs.' + lang;
return (
<DropdownItem
key={lang}
active={activeLang === lang}
data-lang={lang}
onClick={chooseLang}
>
<Flag className="mr-3 opacity-8" country={langCountry[lang]} />
{t(translateTarget)}
</DropdownItem>
);
})}
</DropdownMenu>
</UncontrolledDropdown>
);
}
LangSettingsMenu.propTypes = {
t: PropTypes.func.isRequired,
base: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,
direction: PropTypes.string
};
const applyDecorators = compose(
reduxConnect('base', ['common', 'base']),
translate(['common'], { wait: true })
);
export default applyDecorators(LangSettingsMenu);
@@ -0,0 +1,80 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { Link, withRouter } from 'react-router-dom';
import { Nav, NavItem } from 'reactstrap';
import cl from 'classnames';
export class MainTabsLinks extends React.Component {
static propTypes = {
tabs: PropTypes.object.isRequired,
restrictions: PropTypes.object.isRequired,
t: PropTypes.func.isRequired,
actions: PropTypes.object.isRequired,
location: PropTypes.object
};
validateTab = (tab) => {
if (tab === 'analyze') {
if (!this.props.restrictions) {
// to prevent: permissions of `undefined`
return false;
}
const permissions = this.props.restrictions.permissions;
return permissions.analytics;
}
return true;
};
showUpgradeModal = (e) => {
e.preventDefault();
this.props.actions.toggleUpgradeModal();
};
render() {
const { t, tabs, location } = this.props;
return (
<Nav className="header-megamenu">
{Object.keys(tabs).map((tab, i) => {
const firstSubTab =
tabs[tab].items && tabs[tab].items[0] ? tabs[tab].items[0].url : '';
if (!this.validateTab(tab)) {
return (
<NavItem key={tab}>
<a
href="#"
onClick={this.showUpgradeModal}
className={cl('nav-link', {
active: location.pathname.startsWith(`/app/${tab}`)
})}
>
<i className={`nav-link-icon ${tabs[tab].icon}`}> </i>
<p>{t('tabs.' + tab)}</p>
</a>
</NavItem>
);
}
return (
<NavItem key={tab}>
<Link
to={`/app/${tab}/${firstSubTab}`}
className={cl('nav-link', {
active: location.pathname.startsWith(`/app/${tab}`)
})}
>
<i className={`nav-link-icon ${tabs[tab].icon}`}> </i>
<p>{t('tabs.' + tab)}</p>
</Link>
</NavItem>
);
})}
</Nav>
);
}
}
export default translate(['common'], { wait: true })(
withRouter(React.memo(MainTabsLinks))
);
@@ -0,0 +1,160 @@
import React, { Fragment, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import city3 from '../../../styles/utils/images/dropdown-header/city3.jpg';
import PerfectScrollbar from 'react-perfect-scrollbar';
import reduxConnect from '../../../redux/utils/connect';
import cl from 'classnames';
import {
Alert,
Button,
DropdownMenu,
DropdownToggle,
Nav,
NavItem,
UncontrolledDropdown
} from 'reactstrap';
import { Interpolate, translate } from 'react-i18next';
import { compose } from 'redux';
import { IoIosNotificationsOutline } from 'react-icons/io';
function Notifications({ alerts, t, actions }) {
const [alertsList, setAlertsList] = useState([]);
useEffect(() => {
// Empty list when mounts
actions.removeAllAlerts();
}, []);
useEffect(() => {
const newAlerts = alerts
.reverse()
.map((alert) => {
return typeof alert === 'string' ? { message: alert } : alert;
})
.map((alert) => {
const interpolateParameters = alert ? alert.parameters : {};
const i18nKey = alert && `alerts.${alert.type}.${alert.transKey}`;
let type, msg;
type = alert.type ? oldValueMapping[alert.type] : 'warning';
msg = t(i18nKey, {
...interpolateParameters,
defaultValue: alert.message || t('error.unknown')
});
return { type, msg };
});
setAlertsList(newAlerts);
}, [alerts.length]);
const isRTL = document.documentElement.dir === 'rtl';
return (
<UncontrolledDropdown>
<DropdownToggle className="p-0 mr-2" color="link">
<div className="icon-wrapper icon-wrapper-alt rounded-circle">
<div className="icon-wrapper-bg bg-danger" />
<IoIosNotificationsOutline color="#d92550" fontSize="23px" />
<div className="badge badge-dot badge-dot-sm badge-danger">
{alertsList.length > 0 ? t('userSettings.notifications') : ''}
</div>
</div>
</DropdownToggle>
<DropdownMenu
className={cl('dropdown-menu-xl rm-pointers', {
'py-0': alertsList.length < 1,
'dropdown-menu-left': isRTL
})}
>
<div className="dropdown-menu-header mb-0">
<div className="dropdown-menu-header-inner bg-deep-blue">
<div
className="menu-header-image opacity-1"
style={{
backgroundImage: 'url(' + city3 + ')'
}}
/>
<div className="menu-header-content text-dark">
<h5 className="menu-header-title">
{t('userSettings.notifications')}
</h5>
<h6 className="menu-header-subtitle">
<Interpolate
i18nKey={
alertsList.length > 1
? 'userSettings.notificationsSub_plural'
: 'userSettings.notificationsSub'
}
alertLength={alertsList.length}
/>
</h6>
</div>
</div>
</div>
{alertsList.length > 0 && (
<Fragment>
<div className="scroll-area-md">
<PerfectScrollbar>
<div className="p-2">
{alertsList.map((item, i) => (
<Alert
key={i}
className="mb-2"
style={{ wordBreak: 'break-word' }}
color={colorsMapping[item.type]}
>
<p className="font-size-xs font-weight-bold text-uppercase">
{item.type}
</p>
{item.msg}
</Alert>
))}
</div>
</PerfectScrollbar>
</div>
<Nav vertical>
<NavItem className="nav-item-divider" />
<NavItem className="nav-item-btn text-center">
<Button
size="sm"
className="btn-shadow btn-wide btn-pill"
color="focus"
onClick={actions.removeAllAlerts}
>
{t('userSettings.clearAll')}
</Button>
</NavItem>
</Nav>
</Fragment>
)}
</DropdownMenu>
</UncontrolledDropdown>
);
}
const oldValueMapping = {
notice: 'success',
warning: 'warning',
error: 'error'
};
const colorsMapping = {
success: 'success',
warning: 'warning',
error: 'danger'
};
Notifications.propTypes = {
t: PropTypes.func.isRequired,
alerts: PropTypes.array.isRequired,
actions: PropTypes.object.isRequired
};
const applyDecorators = compose(
reduxConnect('alerts', ['common', 'alerts']),
translate(['common'], { wait: true })
);
export default applyDecorators(Notifications);
@@ -0,0 +1,39 @@
import React, { Fragment } from 'react'
import cx from 'classnames'
class SearchBox extends React.Component {
constructor (props) {
super(props)
this.state = {
activeSearch: false
}
}
activeSearchFunc = () => {
this.setState({ activeSearch: !this.state.activeSearch })
}
render () {
return (
<Fragment>
<div className={cx('search-wrapper', {
active: this.state.activeSearch
})}>
<div className="input-holder">
<input type="text" className="search-input" placeholder="Type to search" />
<button onClick={this.activeSearchFunc}
className="search-icon">
<span />
</button>
</div>
<button onClick={this.activeSearchFunc}
className="close" />
</div>
</Fragment>
)
}
}
export default SearchBox
@@ -0,0 +1,122 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import {
Button,
Label,
Input,
FormGroup,
Modal,
ModalHeader,
ModalBody,
ModalFooter
} from 'reactstrap';
export class SettingsPopup extends React.Component {
static propTypes = {
hidePopup: PropTypes.func.isRequired,
setErrorMsg: PropTypes.func.isRequired,
changePassword: PropTypes.func.isRequired,
errorMsg: PropTypes.string,
t: PropTypes.func.isRequired
};
constructor() {
super();
this.state = {
oldPassword: '',
newPassword: '',
confirmPassword: ''
};
}
hidePopup = () => {
this.props.hidePopup();
this.props.setErrorMsg(null);
};
onSubmit = () => {
const { t } = this.props;
const { oldPassword, newPassword, confirmPassword } = this.state;
// need more validations
if (!oldPassword || !newPassword || !confirmPassword) {
return this.props.setErrorMsg(t('userSettings.enterRequiredFields'));
}
if (newPassword !== confirmPassword) {
return this.props.setErrorMsg(t('userSettings.passwordsNotMatched'));
}
if (oldPassword && newPassword) {
this.props.changePassword(newPassword, oldPassword);
}
};
handleChange = (e) => {
const { name, value } = e.target;
this.setState({ [name]: value });
};
render() {
const { t, errorMsg } = this.props;
return (
<Modal isOpen toggle={this.hidePopup} backdrop="static">
<ModalHeader toggle={this.hidePopup}>
{t('userSettings.changePassword')}
</ModalHeader>
<ModalBody>
<FormGroup>
<Label>
{t('userSettings.enterOldPassword')}
<span className="text-danger">*</span>
</Label>
<Input
type="password"
name="oldPassword"
onChange={this.handleChange}
/>
</FormGroup>
<FormGroup>
<Label>
{t('userSettings.enterNewPassword')}
<span className="text-danger">*</span>
</Label>
<Input
type="password"
name="newPassword"
onChange={this.handleChange}
/>
</FormGroup>
<FormGroup>
<Label>
{t('userSettings.retypeNewPassword')}
<span className="text-danger">*</span>
</Label>
<Input
type="password"
name="confirmPassword"
onChange={this.handleChange}
/>
</FormGroup>
<p className="text-danger">{errorMsg}</p>
</ModalBody>
<ModalFooter>
<Button color="light" onClick={this.hidePopup}>
{t('common:commonWords.Cancel')}
</Button>
<Button color="primary" onClick={this.onSubmit}>
{t('userSettings.changePassword')}
</Button>
</ModalFooter>
</Modal>
);
}
}
export default translate(['tabsContent', 'common'], { wait: true })(
SettingsPopup
);
@@ -0,0 +1,55 @@
import React from 'react';
import PropTypes from 'prop-types';
import { NavLink } from 'react-router-dom';
import { translate } from 'react-i18next';
class SubTabWrapper extends React.Component {
static propTypes = {
activeTabName: PropTypes.string.isRequired,
subTabs: PropTypes.array.isRequired,
t: PropTypes.func.isRequired,
children: PropTypes.object
};
render() {
const { t, activeTabName, subTabs, children } = this.props;
return (
<div className="rc-tabs-top position-relative" key="sub-tab-wrapper">
<div role="tablist" className="rc-tabs-bar" tabIndex="0">
<div className="rc-tabs-nav-container">
<div className="rc-tabs-nav-wrap mask-line pt-0">
<div className="rc-tabs-nav-scroll">
<div className="rc-tabs-nav rc-tabs-nav-animated">
{subTabs &&
subTabs.map((subTab) => {
const tabText =
activeTabName === 'dashboard'
? subTab.title
: t('tabs.' + subTab.title);
const fullUrl =
'/app/' + activeTabName + '/' + subTab.url;
return (
<NavLink
to={fullUrl}
key={subTab.url}
activeClassName="rc-tabs-tab-active rc-tabs-ink-bar rc-tabs-ink-bar-animated"
className="rc-tabs-tab"
>
{tabText}
</NavLink>
);
})}
</div>
</div>
</div>
</div>
</div>
{children}
</div>
);
}
}
export default translate(['common'], { wait: true })(SubTabWrapper);
@@ -0,0 +1,165 @@
/* eslint-disable react/jsx-no-bind */
import React from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import { translate } from 'react-i18next';
import { Nav, Button, NavItem, NavLink } from 'reactstrap';
import PerfectScrollbar from 'react-perfect-scrollbar';
import city from '../../../images/city3.jpg';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faUser } from '@fortawesome/free-solid-svg-icons';
import { reduxActions } from '../../../redux/utils/connect';
import tourPages from './WebTourSteps';
import { useHistory } from 'react-router';
import { planRoutes } from '../Account/Plans/UserPlans';
function UserSettingsMenu(props) {
const { push } = useHistory();
function hideMenu() {
props.toggleMenu();
props.actions.setEnableMobileMenuSmall(false);
}
function showUserSettings() {
hideMenu();
props.actions.showUserSettingsPopup();
}
function onLogout() {
hideMenu();
props.actions.logout();
}
function tourGuide(path) {
const win = window.open(`${path}?webtour=true`, '_blank');
win.focus();
// props.actions.toggleWebTour(); for dev
}
function gotToActivePlan() {
hideMenu();
push(`/app/plans/${planRoutes.current}`);
}
const { t } = props;
return (
<React.Fragment>
<div className="dropdown-menu-header">
<div className="dropdown-menu-header-inner bg-info">
<div
className="menu-header-image opacity-2"
style={{
backgroundImage: 'url(' + city + ')'
}}
/>
<div className="menu-header-content text-left">
<div className="widget-content p-0">
<div className="widget-content-wrapper">
<div className="widget-content-left mr-3">
<div className="user-profile">
<FontAwesomeIcon
className="user-profile-icon"
icon={faUser}
/>
</div>
</div>
<div className="widget-content-left">
<div className="widget-heading">
{props.userFirstName + ' ' + props.userLastName}{' '}
</div>
</div>
<div className="widget-content-right ml-auto mr-2">
<Button
className="btn-pill btn-shadow btn-shine"
color="focus"
onClick={onLogout}
>
{t('userSettings.signOut')}
</Button>
</div>
</div>
</div>
</div>
</div>
</div>
{/* <div className="scroll-area-xs"> */}
<div>
<PerfectScrollbar>
<Nav vertical>
<NavItem>
<NavLink
tag={Button}
type="button"
color="link"
className="font-size-md w-100"
onClick={gotToActivePlan}
>
{t('plans.activePlanDetails')}
</NavLink>
</NavItem>
<NavItem>
<NavLink
tag={Button}
type="button"
color="link"
className="font-size-md w-100"
onClick={showUserSettings}
>
{t('userSettings.changePassword')}
</NavLink>
</NavItem>
<NavItem>
<NavLink
className="font-size-md w-100"
href="https://www.socialhose.io/en/user-guide"
rel="noopener noreferrer"
target="_blank"
>
{t('userSettings.userGuide')}
</NavLink>
</NavItem>
<NavItem style={{ textAlign: 'start' }}>
<div className="mt-2 mb-3 mx-3 px-1">
<p className="text-muted font-size-md mb-2">{t('userSettings.guidedTourTooltip')}</p>
<div className="d-flex flex-row flex-wrap pl-3">
{tourPages.map((tour) => (
<Button
key={tour.name}
className="btn-icon-vertical btn-transition btn-transition-alt pt-2 pb-2 mr-2"
outline
color="primary"
onClick={() => tourGuide(tour.to)}
>
<i className={`${tour.icon} btn-icon-wrapper mb-2`} />
{t(`userSettings.${tour.translateKey}`)}
</Button>
))}
</div>
</div>
</NavItem>
</Nav>
</PerfectScrollbar>
</div>
</React.Fragment>
);
}
UserSettingsMenu.propTypes = {
toggleMenu: PropTypes.func.isRequired,
userFirstName: PropTypes.string.isRequired,
userLastName: PropTypes.string.isRequired,
actions: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
};
const applyDecorators = compose(
reduxActions(),
translate(['common'], { wait: true })
);
export default React.memo(applyDecorators(UserSettingsMenu));
@@ -0,0 +1,120 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { find } from 'lodash';
import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock';
import Tour from 'reactour';
import { useHistory, useLocation } from 'react-router';
import reduxConnect from '../../../redux/utils/connect';
import tourPages from './WebTourSteps';
function WebTour({
actions,
store: {
common: { base },
appState: { themeOptions }
}
}) {
const [hasSidebar, setHasSidebar] = useState(window.innerWidth > 991);
const [tourData, setTourData] = useState({ content: [] });
const location = useLocation();
const { replace } = useHistory();
const { isTourOpen = false } = base;
const params = new URLSearchParams(location.search);
const webtour = params.get('webtour');
useEffect(() => {
if (webtour) {
const tour = find(tourPages, {
to: location.pathname
});
if (tour) {
setTourData(tour);
window.gtag && window.gtag('event', 'tutorial_begin', {
name: tour.name
});
actions.toggleWebTour(); // open tour if param is available
}
} else {
actions.toggleWebTour(); // close if param is removed
}
}, [webtour]);
useEffect(() => {
if (isTourOpen) {
if (window.innerWidth > 991) {
!hasSidebar && setHasSidebar(true);
} else {
hasSidebar && setHasSidebar(false);
}
}
}, [window.innerWidth]);
const accentColor = '#0094bd';
function closeWebTour() {
const queryParams = new URLSearchParams(location.search);
if (queryParams.has('webtour')) {
queryParams.delete('webtour');
replace({
search: queryParams.toString()
});
}
}
function getCurrentStep(step) {
const stepState = tourData.content;
const stepDetails = stepState.find((v, i) => i === step);
if (step === stepState.length - 1) {
window.gtag && window.gtag('event', 'tutorial_complete', {
name: tourData.name
});
}
if (!hasSidebar) {
if (stepDetails.needSidebar) {
!themeOptions.enableMobileMenu && actions.setEnableMobileMenu(true);
} else {
themeOptions.enableMobileMenu && actions.setEnableMobileMenu(false);
}
}
}
function disableBody(target) {
disableBodyScroll(target);
}
function enableBody(target) {
enableBodyScroll(target);
}
return (
<Tour
onRequestClose={closeWebTour}
steps={tourData.content}
getCurrentStep={getCurrentStep}
isOpen={isTourOpen && tourData.content && tourData.content.length > 0}
maskClassName="mask"
className="helper"
rounded={5}
startAt={0}
closeWithMask={false}
accentColor={accentColor}
onAfterOpen={disableBody}
onBeforeClose={enableBody}
disableFocusLock
lastStepNextButton={<div className="btn btn-primary">Finish</div>}
/>
);
}
WebTour.propTypes = {
actions: PropTypes.object,
store: PropTypes.object
};
export default reduxConnect()(WebTour);
@@ -0,0 +1,172 @@
import React from 'react';
import i18n from '../../../i18n';
import { Trans } from 'react-i18next';
const baseKey = 'tabsContent:webtour';
const steps = [
{
selector: '',
content: i18n.t(`${baseKey}.search.start`)
},
{
selector: '[data-tour="left-panel"]',
content: i18n.t(`${baseKey}.search.feedsView`),
resizeObservables: ['[data-tour="left-panel"]'],
needSidebar: true,
stepInteraction: false
},
{
selector: '[data-tour="app-header-left"]',
content: () => (
<Trans i18nKey={`${baseKey}.search.mainTabs`}>
There are 3 main pages: <strong>Search</strong> to find content,
<strong>Analyze</strong> to generate reports, and <strong>Share</strong>
to distribute findings via alerts or webfeeds.
</Trans>
),
onlyWeb: true,
stepInteraction: false
},
{
selector: '[data-tour="app-header-user-settings"]',
content: i18n.t(`${baseKey}.search.userSettings`),
stepInteraction: false
},
{
selector: '[data-tour="search-licenses"]',
content: i18n.t(`${baseKey}.search.license`),
stepInteraction: false
},
{
selector: '[data-tour="input-field-search"]',
content: () => (
<p>
<Trans i18nKey={`${baseKey}.search.searchField`}>
A simple boolean search looks like this:
<strong>BMW AND Texas</strong>. Which will find all mentions of bmw
and "texas”.
</Trans>
</p>
)
},
{
selector: '[data-tour="select-date-range"]',
content: i18n.t(`${baseKey}.search.dateRange`),
stepInteraction: false
},
{
selector: '[data-tour="select-media-types"]',
content: i18n.t(`${baseKey}.search.mediaChannels`)
},
{
selector: '[data-tour="advanced-search"]',
content: () => (
<Trans i18nKey={`${baseKey}.search.advancedSearch`}>
Click on <strong>Advanced Search</strong> to uncover the different
options for your search.
</Trans>
),
resizeObservables: ['[data-tour="advanced-search"]']
},
{
selector: '[data-tour="advanced-search"]',
content: () => (
<Trans i18nKey={`${baseKey}.search.emphasis`}>
<strong>Emphasis:</strong> Include or exclude specific words or phrases
in the headline of a news article or a blog post.
</Trans>
),
resizeObservables: ['[data-tour="advanced-search-content"]']
},
{
selector: '[data-tour="advanced-search"]',
content: () => (
<Trans i18nKey={`${baseKey}.search.languages`}>
<strong>Languages:</strong> Capture the content that is tagged with the
following language(s).
</Trans>
),
resizeObservables: ['[data-tour="advanced-search-content"]']
},
{
selector: '[data-tour="advanced-search"]',
content: () => (
<Trans i18nKey={`${baseKey}.search.locations`}>
<strong>Locations:</strong> Include or exclude content that is geotagged
with the following countries or US States.
</Trans>
),
resizeObservables: ['[data-tour="advanced-search-content"]']
},
{
selector: '[data-tour="advanced-search"]',
content: () => (
<Trans i18nKey={`${baseKey}.search.extras`}>
<strong>Extras:</strong> Only show posts with images.
</Trans>
),
resizeObservables: ['[data-tour="advanced-search-content"]']
},
/* {
selector: '[data-tour="search-button"]',
content: () => (
<Fragment>
Click <strong>Search icon</strong>.
</Fragment>
)
}, */
{
selector: '[data-tour="search-buttons"]',
content: i18n.t(`${baseKey}.search.saveSearch`),
stepInteraction: false
}
];
const analyticsSteps = [
{
selector: '',
content: i18n.t(`${baseKey}.analytics.start`)
},
{
selector: '[data-tour="left-panel"]',
content: i18n.t(`${baseKey}.analytics.dragFeed`),
resizeObservables: ['[data-tour="left-panel"]'],
needSidebar: true
},
{
selector: '[data-tour="drop-feeds-box"]',
highlightedSelectors: ['[data-tour="left-panel"]'],
content: i18n.t(`${baseKey}.analytics.drop`)
},
{
selector: '[data-tour="analytics-data-range"]',
content: i18n.t(`${baseKey}.analytics.dateRange`),
observe: '.DateRangePickerInput'
},
{
selector: '[data-tour="create-analytics-button"]',
content: i18n.t(`${baseKey}.analytics.create`)
}
];
const tourPages = [
{
translateKey: 'HowToSearch',
name: 'How to Search',
icon: 'pe-7s-search',
to: '/app/search/search',
showOn: '/app/search/search',
content: steps
},
{
translateKey: 'HowToAnalyze',
name: 'How to Analyze',
icon: 'pe-7s-graph1',
to: '/app/analyze/create',
showOn: '/app/analyze',
content: analyticsSteps
}
];
export default tourPages;