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,200 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Col, FormGroup, Label, Row } from 'reactstrap';
import { getData } from 'country-list';
import { CardElement } from '@stripe/react-stripe-js';
import { Input } from '../../../common/FormControls';
import { Trans, translate } from 'react-i18next';
const countries = getData().map((v) => ({ label: v.name, value: v.code }));
const cardElementOptions = {
hidePostalCode: true,
style: {
base: {
fontSize: '16px',
color: '#424770'
},
invalid: {
color: '#d92550'
}
}
};
function BillingDetailsForm(props) {
const { form, errors, handleChange, handleValidation, t } = props;
return (
<Row>
<Col md={12}>
<Input
name="name"
title={t('plans.billingForm.fullName')}
type="text"
required
value={form.name}
error={errors.name}
handleChange={handleChange}
handleValidation={handleValidation}
/>
</Col>
<Col md={6}>
<Input
name="line1"
title={t('plans.billingForm.addr1')}
type="text"
required
description={t('plans.billingForm.addr1Desc')}
value={form.line1}
error={errors.line1}
handleChange={handleChange}
handleValidation={handleValidation}
/>
</Col>
<Col md={6}>
<Input
name="line2"
title={t('plans.billingForm.addr2')}
type="text"
description={t('plans.billingForm.addr2Desc')}
value={form.line2}
error={errors.line2}
handleChange={handleChange}
handleValidation={handleValidation}
/>
</Col>
<Col md={6}>
<Input
name="city"
title={t('plans.billingForm.city')}
type="text"
required
description={t('plans.billingForm.cityDesc')}
value={form.city}
error={errors.city}
handleChange={handleChange}
handleValidation={handleValidation}
/>
</Col>
<Col md={6}>
<Input
name="state"
title={t('plans.billingForm.state')}
type="text"
required
description={t('plans.billingForm.stateDesc')}
value={form.state}
error={errors.state}
handleChange={handleChange}
handleValidation={handleValidation}
/>
</Col>
<Col md={6}>
<Input
name="postal_code"
title={t('plans.billingForm.zip')}
type="text"
required
description={t('plans.billingForm.zipDesc')}
value={form.postal_code}
error={errors.postal_code}
handleChange={handleChange}
handleValidation={handleValidation}
/>
</Col>
<Col md={6}>
<Input
name="country"
title={t('plans.billingForm.country')}
type="select"
required
options={countries}
value={form.country}
error={errors.country}
handleChange={handleChange}
handleValidation={handleValidation}
/>
</Col>
<Col md={6}>
<Input
name="email"
title={t('plans.billingForm.email')}
type="email"
required
value={form.email}
error={errors.email}
handleChange={handleChange}
handleValidation={handleValidation}
/>
</Col>
<Col md={6}>
<Input
name="phone"
title={t('plans.billingForm.phone')}
type="tel"
required
description={t('plans.billingForm.phoneDesc')}
value={form.phone}
error={errors.phone}
handleChange={handleChange}
handleValidation={handleValidation}
/>
</Col>
<Col xs={12} className="mb-2">
<FormGroup>
<Label>{t('plans.billingForm.cardHeading')}</Label>
<CardElement
options={cardElementOptions}
className="border b-radius-5 p-3"
/>
</FormGroup>
</Col>
<Col md={12}>
<p className="text-muted">
<Trans i18nKey="plans.billingForm.agreement">
By submitting, you agree to our
<a
title="Privacy Policy"
target="_blank"
href="https://www.socialhose.io/en/legal/privacy"
className="footer__link"
>
Privacy Policy
</a>
<a
title="Terms and Conditions"
target="_blank"
href="https://www.socialhose.io/en/legal/terms"
className="footer__link"
>
Terms & Conditions
</a>
<a
title="Acceptable Use Policy"
target="_blank"
href="https://www.socialhose.io/en/legal/acceptable-use"
className="footer__link"
>
Acceptable Use Policy
</a>
.
</Trans>
</p>
</Col>
</Row>
);
}
BillingDetailsForm.propTypes = {
t: PropTypes.func,
form: PropTypes.object,
errors: PropTypes.object,
handleChange: PropTypes.func,
handleValidation: PropTypes.func
};
export default React.memo(
translate(['tabsContent'], { wait: true })(BillingDetailsForm)
);
@@ -0,0 +1,244 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import useForm from '../../../common/hooks/useForm';
import {
ListGroupItem,
Modal,
ModalBody,
ModalFooter,
ModalHeader,
Row,
Form,
Button,
Col,
ListGroup,
Label
} from 'reactstrap';
import { Checkbox, Input } from '../../../common/FormControls';
import { cancelPlan, cancelPlanHubspot } from '../../../../api/plans/userPlans';
import { planRoutes } from './UserPlans';
const formParams = {
rs1: 1,
rs2: 2,
rs3: 3,
rs4: 4,
rs5: 5,
rs6: 'Other'
};
const initForm = {
[formParams.rs1]: false,
[formParams.rs2]: false,
[formParams.rs3]: false,
[formParams.rs4]: false,
[formParams.rs5]: false,
[formParams.rs6]: false,
Other: false,
content: '',
email: '',
subject: 'Cancellation',
errors: {
email: null
}
};
function CancellationFeedback({ t, actions, isOpen = false, toggle, user }) {
const {
form,
handleChange,
handleValidation,
validateSubmit,
errors
} = useForm(initForm);
const [cancelLoading, setCancelLoading] = useState(false);
const [reasonError, setReasonError] = useState('');
useEffect(() => {
handleChange('email', user.email);
}, [user.email]);
useEffect(() => {
if (Object.values(formParams).some((v) => form[v])) {
setReasonError('');
}
}, [...Object.values(form)]);
function cancelSubscription() {
const obj = validateSubmit();
if (!obj) {
return actions.addAlert({ type: 'error', transKey: 'requiredInfo' });
} else if (!Object.values(formParams).some((v) => obj[v])) {
setReasonError(t('plans.currentPlan.cancelModal.reasonSelect'));
return;
}
setCancelLoading(true);
cancelPlan().then((res) => {
if (res.error) {
res.data
? actions.addAlert(res.data)
: actions.addAlert({ type: 'error', transKey: 'somethingWrong' });
setCancelLoading(false);
return;
}
cancelPlanHubspot({ ...obj });
actions.addAlert({
type: 'notice',
transKey: 'cancelledSubscription'
});
// refresh page on success and move to active plan details
setTimeout(() => {
window.location.pathname = `/app/plans/${planRoutes.current}`;
}, 1000);
});
}
return (
<div>
<Modal size="lg" backdrop="static" isOpen={isOpen} toggle={toggle}>
<ModalHeader toggle={toggle}>
{t('plans.currentPlan.cancelModal.header')}
</ModalHeader>
<ModalBody>
<Row>
<Col md={6} className="mb-3">
<p className="mb-3">
{t('plans.currentPlan.cancelModal.line1', {
firstName: user.firstName
})}
</p>
<p className="mb-2">{t('plans.currentPlan.cancelModal.line2')}</p>
<ListGroup className="text-muted">
<ListGroupItem>
{t('plans.currentPlan.cancelModal.warn1')}
</ListGroupItem>
<ListGroupItem>
{t('plans.currentPlan.cancelModal.warn2')}
</ListGroupItem>
<ListGroupItem>
{t('plans.currentPlan.cancelModal.warn3')}
</ListGroupItem>
<ListGroupItem>
{t('plans.currentPlan.cancelModal.warn4')}
</ListGroupItem>
</ListGroup>
</Col>
<Col md={6} className="mb-3">
<Form>
<p className="mb-4">
{t('plans.currentPlan.cancelModal.feedbackPara')}
</p>
<div>
<Label className="d-inline-block mb-2">
{t('plans.currentPlan.cancelModal.reasonCancellation')}
<span className="text-danger">*</span>
</Label>
<div className="pl-3 mb-3">
<Checkbox
hideTitle
name={formParams.rs1}
formGroupClass="mb-0"
title={t('plans.currentPlan.cancelModal.noNeeds')}
description={t('plans.currentPlan.cancelModal.noNeeds')}
value={form[formParams.rs1]}
error={errors[formParams.rs1]}
handleChange={handleChange}
/>
<Checkbox
hideTitle
name={formParams.rs2}
formGroupClass="mb-0"
title={t('plans.currentPlan.cancelModal.tooNoisy')}
description={t('plans.currentPlan.cancelModal.tooNoisy')}
value={form[formParams.rs2]}
error={errors[formParams.rs2]}
handleChange={handleChange}
/>
<Checkbox
hideTitle
name={formParams.rs3}
formGroupClass="mb-0"
title={t('plans.currentPlan.cancelModal.confusing')}
description={t('plans.currentPlan.cancelModal.confusing')}
value={form[formParams.rs3]}
error={errors[formParams.rs3]}
handleChange={handleChange}
/>
<Checkbox
hideTitle
name={formParams.rs4}
formGroupClass="mb-0"
title={t('plans.currentPlan.cancelModal.expensive')}
description={t('plans.currentPlan.cancelModal.expensive')}
value={form[formParams.rs4]}
error={errors[formParams.rs4]}
handleChange={handleChange}
/>
<Checkbox
hideTitle
name={formParams.rs5}
formGroupClass="mb-0"
title={t('plans.currentPlan.cancelModal.covid')}
description={t('plans.currentPlan.cancelModal.covid')}
value={form[formParams.rs5]}
error={errors[formParams.rs5]}
handleChange={handleChange}
/>
<Checkbox
hideTitle
name={formParams.rs6}
formGroupClass="mb-0"
title={t('plans.currentPlan.cancelModal.other')}
description={t('plans.currentPlan.cancelModal.other')}
value={form[formParams.rs6]}
error={errors[formParams.rs6]}
handleChange={handleChange}
/>
<span className="text-danger">{reasonError}</span>
</div>
</div>
<Input
name="content"
title={t('plans.currentPlan.cancelModal.tellMore')}
type="textarea"
value={form.content}
error={errors.content}
handleChange={handleChange}
handleValidation={handleValidation}
/>
</Form>
</Col>
</Row>
</ModalBody>
<ModalFooter>
<Button color="link" onClick={toggle}>
{t('plans.currentPlan.cancelModal.undoBtn')}
</Button>
<Button
color="danger"
disabled={cancelLoading}
onClick={cancelSubscription}
>
{cancelLoading
? t('plans.currentPlan.cancelModal.loadingBtn')
: t('plans.currentPlan.cancelModal.cancelSubscriptionBtn')}
</Button>
</ModalFooter>
</Modal>
</div>
);
}
CancellationFeedback.propTypes = {
t: PropTypes.func,
actions: PropTypes.object,
isOpen: PropTypes.bool,
toggle: PropTypes.func,
user: PropTypes.object
};
export default CancellationFeedback;
@@ -0,0 +1,185 @@
import React, { Fragment, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { CardElement, useElements, useStripe } from '@stripe/react-stripe-js';
import { Alert, Button, Card, CardBody, CardTitle, Col, Row } from 'reactstrap';
import { reduxActions } from '../../../../redux/utils/connect';
import useForm from '../../../common/hooks/useForm';
import useIsMounted from '../../../common/hooks/useIsMounted';
import BillingDetailsForm from './BillingDetailsForm';
import { changeCardDetails } from '../../../../api/plans/userPlans';
import { planRoutes } from './UserPlans';
import { setDocumentData } from '../../../../common/helper';
import { translate } from 'react-i18next';
const initialForm = {
name: '',
line1: '',
line2: '',
city: '',
state: '',
postal_code: '',
country: '',
email: '',
phone: '',
errors: {
name: null,
line1: null,
city: null,
state: null,
postal_code: null,
country: null,
email: null,
phone: null
}
};
function ChangeCard({ actions, t }) {
const isMounted = useIsMounted();
const stripe = useStripe();
const elements = useElements();
const {
form,
errors,
handleChange,
handleValidation,
validateSubmit
} = useForm(initialForm);
const [paymentError, setPaymentError] = useState(false);
const [loading, setLoading] = useState(false);
useEffect(() => {
setDocumentData('title', 'Change Card');
return () => setDocumentData('title'); // default
}, []);
const submitPayment = async () => {
if (!stripe || !elements) {
// Stripe.js has not loaded yet.
return;
}
setPaymentError(false);
setLoading(true);
const obj = validateSubmit();
if (!obj) {
setLoading(false);
return actions.addAlert({
type: 'error',
transKey: 'requiredInfo'
});
}
const cardElement = elements.getElement(CardElement);
const {
name,
line1,
line2,
city,
state,
postal_code,
country,
email,
phone
} = obj;
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: cardElement,
billing_details: {
name,
email,
phone,
address: {
line1: line1,
line2: line2,
city: city,
state: state,
postal_code: postal_code,
country: country
}
}
});
if (error) {
setPaymentError(error);
setLoading(false);
return;
}
const newObj = {};
newObj.paymentID = paymentMethod.id; //stripe card element ID
const res = await changeCardDetails(newObj);
if (!isMounted.current) {
return;
}
if (res.error) {
res.data
? actions.addAlert(res.data)
: actions.addAlert({ type: 'error', transKey: 'somethingWrong' });
setLoading(false);
return;
}
actions.addAlert({ type: 'notice', transKey: 'cardUpdated' });
// refresh page on success and move to active plan details
setTimeout(() => {
window.location.pathname = `/app/plans/${planRoutes.current}`;
}, 1000);
};
return (
<Col xs="12" lg="8" xl="9">
<Card className="mb-3">
<CardBody>
<CardTitle>{t('plans.changeCard.heading')}</CardTitle>
<p className="text-muted mb-3">{t('plans.changeCard.subText')}</p>
<BillingDetailsForm
form={form}
errors={errors}
handleChange={handleChange}
handleValidation={handleValidation}
/>
<Row className="divider" />
{paymentError && (
<Alert color="danger">
<Fragment>
<p className="font-size-xs font-weight-bold text-uppercase">
{t('plans.changeCard.error')}
</p>
{paymentError.message}
</Fragment>
</Alert>
)}
<div className="text-right">
<Button
type="button"
color="primary"
onClick={submitPayment}
disabled={!stripe || !elements || loading}
className="btn-wide btn-hover-shine mb-2 mb-sm-0"
size="lg"
>
{loading
? t('plans.changeCard.loadingBtn')
: t('plans.changeCard.changeCardBtn')}
</Button>
</div>
</CardBody>
</Card>
</Col>
);
}
ChangeCard.propTypes = {
t: PropTypes.func.isRequired,
actions: PropTypes.object
};
export default reduxActions()(
translate(['tabsContent'], { wait: true })(ChangeCard)
);
@@ -0,0 +1,286 @@
import React, { Fragment, useEffect, useState } from 'react';
import { useHistory } from 'react-router';
import PropTypes from 'prop-types';
import { Button, Card, CardBody, CardTitle, Col, Row } from 'reactstrap';
import reduxConnect from '../../../../redux/utils/connect';
import { planRoutes } from './UserPlans';
import { allMediaTypes } from '../../../../redux/modules/appState/searchByFilters';
import { capitalize } from 'lodash';
import { convertUTCtoLocal, setDocumentData } from '../../../../common/helper';
import { translate } from 'react-i18next';
import CancellationFeedback from './CancellationFeedback';
function CurrentPlan({ actions, user, t }) {
const [cancelModal, setCancelModal] = useState(false);
const { restrictions } = user;
const { push } = useHistory();
useEffect(() => {
setDocumentData('title', 'Active Plan Details');
return () => setDocumentData('title'); // default
}, []);
function changePlan() {
push(`/app/plans/${planRoutes.update}`);
}
function toggleCancelModal() {
setCancelModal((prev) => !prev);
}
const {
plans,
limits,
isPlanCancelled,
subStartDate,
subEndDate
} = restrictions;
const selectedMedias = [];
const notSelectedMedias = [];
allMediaTypes.map((v) => {
if (plans[v]) {
selectedMedias.push(t(`searchTab.sourceTypes.${v}`, capitalize(v)));
} else {
notSelectedMedias.push(t(`searchTab.sourceTypes.${v}`, capitalize(v)));
}
});
const isRTL = document.documentElement.dir === 'rtl';
return (
<Col xs="12" lg="8" xl="9">
<Row>
<Col sm="6" md="4">
<div className="card mb-3 widget-chart text-left">
<div className="widget-chart-content">
<div className="widget-subheading">
{t('plans.currentPlan.subHeading')}
</div>
<div className="widget-numbers">
{plans.price === 0
? t('plans.currentPlan.freePlan')
: `$${plans.price}`}
</div>
<div className="widget-description">
<span>
{plans.price === 0 ? (
<Fragment>&nbsp;</Fragment>
) : subStartDate && subEndDate ? (
`${convertUTCtoLocal(
subStartDate,
'MMM D, YYYY'
)} - ${convertUTCtoLocal(subEndDate, 'MMM D, YYYY')}`
) : (
t('plans.currentPlan.perMonth')
)}
</span>
</div>
</div>
</div>
</Col>
<Col sm="6">
<button
className="card mb-3 widget-chart bg-success text-white text-left"
onClick={changePlan}
>
<div className="widget-chart-content">
<div className="widget-subheading">
{t('plans.currentPlan.changePlan')}
</div>
<div className="widget-numbers font-size-xlg">
{t('plans.currentPlan.upgradeYourPlan')}
</div>
<div className="widget-description">
<span>{t('plans.currentPlan.upgradeText')}</span>
</div>
</div>
</button>
</Col>
<Col xs="12">
<Card>
<CardBody>
<CardTitle>{t('plans.currentPlan.currentPlanDetails')}</CardTitle>
<div className="mb-3">
<p className="text-muted">
{t('plans.currentPlan.selectedMediaTypes')}
</p>
<p className="font-size-xlg">
{selectedMedias.length > 0
? selectedMedias.join(', ')
: t('plans.currentPlan.none')}
{notSelectedMedias.length > 0 ? (
<span className="font-size-md opacity-6 ml-2">
({t('plans.currentPlan.upgradeToGet')}:{' '}
{notSelectedMedias.join(', ')})
</span>
) : (
''
)}
</p>
</div>
<div className="divider" />
<div className="mb-3">
<p className="text-muted mb-2">
{t('plans.currentPlan.selectedLicenses')}
</p>
<Row>
<Col xs="12" sm="6" md="3">
<div className="mb-3 card widget-chart">
{!isRTL ? (
<div className="widget-numbers">
{limits.savedFeeds.current}/{limits.savedFeeds.limit}
</div>
) : (
<div className="widget-numbers">
{limits.savedFeeds.limit}/{limits.savedFeeds.current}
</div>
)}
<div className="widget-subheading mb-3">
{t('plans.currentPlan.feedsLicenses')}
</div>
</div>
</Col>
<Col xs="12" sm="6" md="3">
<div className="mb-3 card widget-chart">
{!isRTL ? (
<div className="widget-numbers">
{limits.searchesPerDay.current}/
{limits.searchesPerDay.limit}
</div>
) : (
<div className="widget-numbers">
{limits.searchesPerDay.limit}/
{limits.searchesPerDay.current}
</div>
)}
<div className="widget-subheading mb-3">
{t('plans.currentPlan.searchLicenses')}
</div>
</div>
</Col>
<Col xs="12" sm="6" md="3">
<div className="mb-3 card widget-chart">
{!isRTL ? (
<div className="widget-numbers">
{limits.webFeeds.current}/{limits.webFeeds.limit}
</div>
) : (
<div className="widget-numbers">
{limits.webFeeds.limit}/{limits.webFeeds.current}
</div>
)}
<div className="widget-subheading mb-3">
{t('plans.currentPlan.webfeedLicenses')}
</div>
</div>
</Col>
<Col xs="12" sm="6" md="3">
<div className="mb-3 card widget-chart">
{!isRTL ? (
<div className="widget-numbers">
{limits.alerts.current}/{limits.alerts.limit}
</div>
) : (
<div className="widget-numbers">
{limits.alerts.limit}/{limits.alerts.current}
</div>
)}
<div className="widget-subheading mb-3">
{t('plans.currentPlan.alertLicenses')}
</div>
</div>
</Col>
<Col xs="12" sm="6" md="3">
<div className="mb-3 card widget-chart">
{!isRTL ? (
<div className="widget-numbers">
{limits.subscriberAccounts.current}/
{limits.subscriberAccounts.limit}
</div>
) : (
<div className="widget-numbers">
{limits.subscriberAccounts.limit}/
{limits.subscriberAccounts.current}
</div>
)}
<div className="widget-subheading mb-3">
{t('plans.currentPlan.userAccounts')}
</div>
</div>
</Col>
</Row>
</div>
<div className="divider" />
<div className="mb-3">
<p className="text-muted">{t('plans.currentPlan.features')}</p>
<p className="font-size-xlg">
{plans.analytics ? (
t('plans.currentPlan.analytics')
) : (
<Fragment>
{t('plans.currentPlan.none')}
<span className="font-size-md opacity-6 ml-2">
({t('plans.currentPlan.upgradeToGet')}:{' '}
{t('plans.currentPlan.analytics')})
</span>
</Fragment>
)}
</p>
</div>
{plans.price > 0 && (
<Fragment>
<div className="divider" />
<div className="mb-3">
{!isPlanCancelled ? (
<div className="text-muted">
<Button
color="danger"
outline
onClick={toggleCancelModal}
>
{t('plans.currentPlan.cancelSubscriptionBtn')}
</Button>
<p className="text-muted mt-2">
{t('plans.currentPlan.cancelWarning')}
</p>
</div>
) : (
<div className="text-muted">
<Button color="secondary" outline disabled>
{t('plans.currentPlan.cancelSubscriptionBtn')}
</Button>
<p className="d-block d-md-inline-block ml-md-3 mt-md-0 mt-2 ml-0 text-muted">
{t('plans.currentPlan.alreadyCancelled')}
</p>
</div>
)}
</div>
</Fragment>
)}
<CancellationFeedback
isOpen={cancelModal}
toggle={toggleCancelModal}
actions={actions}
user={user}
t={t}
/>
</CardBody>
</Card>
</Col>
</Row>
</Col>
);
}
CurrentPlan.propTypes = {
t: PropTypes.func.isRequired,
actions: PropTypes.object,
user: PropTypes.object
};
export default reduxConnect('user', ['common', 'auth', 'user'])(
translate(['tabsContent'], { wait: true })(CurrentPlan)
);
@@ -0,0 +1,168 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import {
Button,
Col,
Modal,
ModalBody,
ModalFooter,
ModalHeader,
Row,
Table
} from 'reactstrap';
import { convertUTCtoLocal } from '../../../../common/helper';
import moment from 'moment';
import { capitalize } from 'lodash';
import { translate } from 'react-i18next';
function ShowTransactionDetails(props) {
const { data, closeModal, t } = props;
const plan = data && data.lines && data.lines.data && data.lines.data[0];
useEffect(() => {
return () => closeModal();
}, []);
return (
<Modal isOpen={!!data && !!plan} toggle={closeModal} size="lg">
<ModalHeader toggle={closeModal}>
{t('plans.transactions.modal.heading')}
</ModalHeader>
<ModalBody>
{data && (
<Row>
<Col xs="12" lg="6" className="mb-3">
<h6 className="mb-3">
{t('plans.transactions.modal.transactionDetails')}
</h6>
<Table striped>
<tbody>
<tr>
<th>{t('plans.transactions.modal.transactionDate')}</th>
<td>
{convertUTCtoLocal(
moment.unix(
data.status_transitions &&
data.status_transitions.paid_at
),
'MM/DD/YYYY hh:mm:ss a'
)}
</td>
</tr>
<tr>
<th>{t('plans.transactions.modal.activationDate')}</th>
<td>
{convertUTCtoLocal(
moment.unix(plan && plan.period.start),
'MM/DD/YYYY'
)}
</td>
</tr>
<tr>
<th>{t('plans.transactions.modal.expirationDate')}</th>
<td>
{convertUTCtoLocal(
moment.unix(plan && plan.period.end),
'MM/DD/YYYY'
)}
</td>
</tr>
<tr>
<th>{t('plans.transactions.modal.amount')}</th>
<td>${data.amount_paid / 100}</td>
</tr>
<tr>
<th>{t('plans.transactions.modal.status')}</th>
<td>{capitalize(data.status)}</td>
</tr>
</tbody>
</Table>
</Col>
<Col xs="12" lg="6" className="mb-3">
<h6 className="mb-3">
{t('plans.transactions.modal.billingDetails')}
</h6>
<Table striped>
<tbody>
<tr>
<th>{t('plans.transactions.modal.name')}</th>
<td>{data.customer_name || '-'}</td>
</tr>
<tr>
<th>{t('plans.transactions.modal.email')}</th>
<td>{data.customer_email || '-'}</td>
</tr>
<tr>
<th>{t('plans.transactions.modal.phone')}</th>
<td>{data.customer_phone || '-'}</td>
</tr>
<tr>
<th>{t('plans.transactions.modal.address')}</th>
<td>{data.customer_address || '-'}</td>
</tr>
<tr>
<th>{t('plans.transactions.modal.invoiceNo')}</th>
<td>
{data.number} (
<a
href={data.hosted_invoice_url}
rel="noopener noreferrer"
target="_blank"
>
{t('plans.transactions.modal.showInvoiceLink')}
</a>
)
</td>
</tr>
</tbody>
</Table>
</Col>
{/* <Col xs="12" lg="6" className="mb-3">
<h6 className="mb-3">Plan Details</h6>
<Table striped>
<tbody>
<tr>
<th>Feeds Licenses</th>
<td>0</td>
</tr>
<tr>
<th>Webfeed Licenses</th>
<td>0</td>
</tr>
<tr>
<th>Newsletter Licenses</th>
<td>0</td>
</tr>
<tr>
<th>User Accounts</th>
<td>0</td>
</tr>
<tr>
<th>Analytics</th>
<td>No</td>
</tr>
</tbody>
</Table>
</Col> */}
</Row>
)}
</ModalBody>
<ModalFooter>
<Button color="link" onClick={closeModal}>
{t('plans.transactions.modal.cancelBtn')}
</Button>
</ModalFooter>
</Modal>
);
}
ShowTransactionDetails.propTypes = {
t: PropTypes.func,
closeModal: PropTypes.func,
data: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]).isRequired
};
export default React.memo(
translate(['tabsContent'], { wait: true })(ShowTransactionDetails)
);
@@ -0,0 +1,749 @@
/* eslint-disable react/jsx-no-bind */
import React, { Fragment, useCallback, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import Slider from 'rc-slider';
import Tooltip from 'rc-tooltip';
import {
Alert,
Button,
Card,
CardBody,
CardTitle,
Col,
Form,
FormGroup,
Label,
Modal,
ModalBody,
ModalFooter,
ModalHeader,
Row
} from 'reactstrap';
import {
licenses,
mediaTypes,
features,
addonFeatures
} from '../../../LoginRegister/Registration/PlanConstants';
import useForm from '../../../common/hooks/useForm';
import { debounce } from 'lodash';
import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
import useIsMounted from '../../../common/hooks/useIsMounted';
import reduxConnect from '../../../../redux/utils/connect';
import {
getPlans,
updatePrice
} from '../../../../api/registration/registration';
import {
updatePlanHubspot,
updatePlanPayment
} from '../../../../api/plans/userPlans';
import { planRoutes } from './UserPlans';
import BillingDetailsForm from './BillingDetailsForm';
import simpleNumberLocalizer from 'react-widgets-simple-number';
import NumberPicker from 'react-widgets/lib/NumberPicker';
import LoadersAdvanced from '../../../common/Loader/Loader';
import { IoIosWarning } from 'react-icons/io';
import { convertUTCtoLocal, setDocumentData } from '../../../../common/helper';
import { translate } from 'react-i18next';
simpleNumberLocalizer();
const Handle = Slider.Handle;
const handle = (props) => {
// eslint-disable-next-line react/prop-types
const { value, dragging, index, ...restProps } = props;
return (
<Tooltip
key={index}
prefixCls="rc-slider-tooltip"
overlay={value}
visible={dragging}
placement="top"
>
<Handle value={value} {...restProps} />
</Tooltip>
);
};
const initialForm = {
savedFeeds: 0,
searchesPerDay: 0,
webFeeds: 0,
alerts: 0,
news: 0,
blog: 0,
reddit: 0,
instagram: 0,
twitter: 0,
analytics: 0,
subscriberAccounts: 0,
masterAccounts: 0
};
const initialPaymentForm = {
name: '',
line1: '',
line2: '',
city: '',
state: '',
postal_code: '',
country: '',
email: '',
phone: '',
errors: {
name: null,
line1: null,
city: null,
state: null,
postal_code: null,
country: null,
email: null,
phone: null
}
};
function UpdatePlan({ actions, restrictions, t }) {
const stripe = useStripe();
const elements = useElements();
const isMounted = useIsMounted();
// first step
const { form, handleChange, resetForm } = useForm(initialForm);
const [updatingPrice, setUpdatingPrice] = useState(true);
const [totalCost, setTotalCost] = useState(' - ');
const [modal, setModal] = useState(false);
const [loading, setLoading] = useState(false);
const [planLoading, setPlanLoading] = useState(true);
const [planError, setPlanError] = useState(false);
const [planList, setPlanList] = useState([]);
const [disableUpdate, setDisableUpdate] = useState(true);
// second step
const [nextStep, setNextStep] = useState(false);
const {
form: paymentForm,
handleChange: handlePaymentForm,
errors: paymentFormErrors,
handleValidation: handlePaymentValidation,
validateSubmit
} = useForm(initialPaymentForm);
const [paymentError, setPaymentError] = useState(false);
const [paymentLoading, setPaymentLoading] = useState(false);
// to update price when input changes
useEffect(() => {
if (planList.length > 0) {
debouncePrice(form);
}
}, [...Object.values(form)]);
const debouncePrice = useCallback(
debounce((form) => {
setUpdatingPrice(true);
updatePrice(form).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || isNaN(res.data.totalPrice)) {
actions.addAlert(res.data);
setUpdatingPrice(false);
setTotalCost('Error');
return;
}
setTotalCost(res.data.totalPrice);
setUpdatingPrice(false);
});
}, 1000),
[isMounted.current]
);
useEffect(() => {
if (!restrictions.isPlanCancelled && !restrictions.isPlanDowngrade) {
setDisableUpdate(false);
} else {
setDisableUpdate(true);
}
}, [restrictions.isPlanCancelled, restrictions.isPlanDowngrade]);
useEffect(() => {
getBillingPlans();
setDocumentData('title', 'Update Plan');
return () => setDocumentData('title'); // default
}, []);
function getBillingPlans() {
setPlanLoading(true);
setPlanError(false);
getPlans().then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data || !res.data.length) {
setPlanError(true);
setPlanLoading(false);
res.data && res.data.length > 0 && actions.addAlert(res.data);
return;
}
setPlanLoading(false);
setPlanList(res.data);
const modified = { ...initialForm };
let selectedPlan = {};
if (restrictions.plans.price > 0) {
selectedPlan = { ...restrictions.plans };
Object.entries(restrictions.limits).map(([key, value]) => {
selectedPlan[key] = value.limit;
});
selectedPlan.blog = selectedPlan.blogs;
delete selectedPlan.blogs;
} else {
selectedPlan = res.data[0];
}
Object.keys(initialForm).map((key) => {
modified[key] =
selectedPlan[key] === undefined
? modified[key]
: selectedPlan[key] === true
? 1
: selectedPlan[key] === false
? 0
: selectedPlan[key];
});
resetForm(modified);
});
}
function changePlan(id) {
const selectedPlan = planList.find((plan) => plan.id === id);
const modified = { ...initialForm };
Object.keys(initialForm).map((key) => {
modified[key] =
selectedPlan[key] === undefined
? modified[key]
: selectedPlan[key] === true
? 1
: selectedPlan[key] === false
? 0
: selectedPlan[key];
});
resetForm(modified);
}
function handleSubmit() {
if (restrictions.isPlanCancelled || restrictions.isPlanDowngrade) {
return;
}
// move to payment page if new basic user
// instruct according to upgrade and downgrade
// if card already stored then only update the plan by showing modal or providing option to change card
setLoading(true);
if (restrictions.isPaymentId) {
setModal(true); // show details of card
} else {
setNextStep(true);
window.scrollTo(0, 0);
}
setLoading(false);
}
function toggle() {
setModal((prev) => !prev);
}
function proceedToDetails() {
toggle();
setNextStep(true);
window.scrollTo(0, 0);
}
const submitPayment = async () => {
if (!stripe || !elements) {
// Stripe.js has not loaded yet.
return;
}
if (restrictions.isPlanCancelled || restrictions.isPlanDowngrade) {
return;
}
setPaymentError(false);
setPaymentLoading(true);
const obj = validateSubmit();
if (!obj) {
setPaymentLoading(false);
return actions.addAlert({
type: 'error',
transKey: 'requiredInfo'
});
}
const cardElement = elements.getElement(CardElement);
const {
name,
line1,
line2,
city,
state,
postal_code,
country,
email,
phone
} = obj;
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: cardElement,
billing_details: {
name,
email,
phone,
address: {
line1: line1,
line2: line2,
city: city,
state: state,
postal_code: postal_code,
country: country
}
}
});
if (error) {
setPaymentError(error);
setPaymentLoading(false);
return;
}
const newObj = { ...form };
newObj.masterAccounts = '1';
newObj.paymentID = paymentMethod.id; //stripe card element ID
const res = await updatePlanPayment(newObj);
if (res.error) {
res.data
? actions.addAlert(res.data)
: actions.addAlert({ type: 'error', transKey: 'somethingWrong' });
setPaymentLoading(false);
return;
}
window.gtag &&
window.gtag('event', 'purchase', {
currency: 'USD',
value: totalCost
});
await updatePlanHubspot({ ...obj, ...form, totalCost });
actions.addAlert({ type: 'notice', transKey: 'planUpdated' });
// refresh page on success and move to active plan details
setTimeout(() => {
window.location.pathname = `/app/plans/${planRoutes.current}`;
}, 1000);
};
const proceedPayment = async () => {
// payment with old card
setLoading(true);
const newObj = { ...form };
newObj.masterAccounts = '1';
const res = await updatePlanPayment(newObj);
if (res.error) {
res.data
? actions.addAlert(res.data)
: actions.addAlert({ type: 'error', transKey: 'somethingWrong' });
setLoading(false);
return;
}
window.gtag &&
window.gtag('event', 'purchase', {
currency: 'USD',
value: totalCost
});
await updatePlanHubspot({ ...form, totalCost });
actions.addAlert({ type: 'notice', transKey: 'planUpdated' });
// refresh page on success and move to active plan details
setTimeout(() => {
window.location.pathname = `/app/plans/${planRoutes.current}`;
}, 1000);
};
function moveBack() {
window.scrollTo(0, 0);
setNextStep(false);
}
if (planError || planLoading) {
return (
<Col xs="12" lg="8" xl="9">
<Card className="h-75 mb-3">
<CardBody>
<CardTitle>{t('plans.updatePlan.heading')}</CardTitle>
{planError && (
<div className="text-danger text-center p-4">
<IoIosWarning
className="d-block mx-auto mb-2"
fontSize="32px"
/>
{t('plans.updatePlan.planLoadingFailed')}{' '}
<Button color="link" onClick={getBillingPlans} className="p-0">
{t('plans.updatePlan.tryAgainBtn')}
</Button>
</div>
)}
</CardBody>
{planLoading && <LoadersAdvanced />}
</Card>
</Col>
);
}
const isRTL = document.documentElement.dir === 'rtl';
return (
<Col xs="12" lg="8" xl="9">
<Card className="mb-3">
{!nextStep ? (
<CardBody>
<CardTitle>{t('plans.updatePlan.heading')}</CardTitle>
<p className="text-muted">
{t('plans.updatePlan.subText')}{' '}
<a
href="https://www.socialhose.io/en/pricing"
rel="noopener noreferrer"
target="_blank"
>
{t('plans.updatePlan.learnMoreBtn')}
</a>
.
</p>
<hr />
<Form>
<Row>
<Col md={12}>
<div className="mb-3">
<h6 className="font-weight-bold mb-3">
{t('plans.updatePlan.prePlans')}
</h6>
<div className="d-flex flex-wrap justify-content-center justify-content-md-start">
{planList.map((plan) => (
<Button
outline
key={plan.id}
color="primary"
type="button"
className="btn-wide btn-lg p-sm-3 mb-2 mr-2"
onClick={() => changePlan(plan.id)}
>
{plan.name}
</Button>
))}
</div>
</div>
<hr />
<div className="mb-3">
<h6 className="font-weight-bold mb-3">
{t('plans.updatePlan.mediaTypes')}
</h6>
<div>
{mediaTypes.map((type) => (
<Button
key={type.name}
size="lg"
type="button"
title={
form[type.name]
? 'Click to deselect'
: 'Click to select'
}
outline={!form[type.name]}
className="btn-pill mb-2 mr-2"
color={form[type.name] ? 'success' : 'light'}
onClick={() =>
handleChange(type.name, !form[type.name])
}
>
{t(`searchTab.sourceTypes.${type.transKey}`)} (
{type.price})
</Button>
))}
</div>
</div>
<hr />
<div className="mb-3">
<h6 className="font-weight-bold mb-3">
{t('plans.updatePlan.licenses')}
</h6>
<Row noGutters className="justify-content-center">
{licenses.map((license) => (
<Col sm={6} key={license.name}>
<div className="p-4 m-2 border b-radius-5 shadow-sm">
<FormGroup>
<div className="d-flex justify-content-between">
<Label title={license.title}>
{t(`plans.currentPlan.${license.transKey}`)}
</Label>
<span className="font-size-lg font-weight-bold text-primary">
{form[license.name]}
</span>
</div>
<Slider
{...license.props}
reverse={isRTL}
handle={handle}
value={form[license.name]}
onChange={(val) =>
handleChange(license.name, val)
}
/>
</FormGroup>
</div>
</Col>
))}
</Row>
</div>
<hr />
<Row>
<Col md="6">
<div className="mb-3">
<h6 className="font-weight-bold mb-3">
{t('plans.updatePlan.features')}
</h6>
<div>
{features.map((type) => (
<Button
key={type.name}
size="lg"
type="button"
title={
form[type.name]
? t('plans.updatePlan.deselectTooltip')
: t('plans.updatePlan.selectTooltip')
}
outline={!form[type.name]}
className="btn-pill mb-2 mr-2"
color={form[type.name] ? 'success' : 'light'}
onClick={() =>
handleChange(type.name, !form[type.name])
}
>
{t(`plans.currentPlan.${type.transKey}`)} (
{type.price})
</Button>
))}
<div className="pl-2">
{features.map((type) =>
form[type.name] ? (
<p
key={type.name}
className="font-size-sm text-muted mb-1"
>
{type.desc}
</p>
) : null
)}
</div>
</div>
</div>
</Col>
<Col md="6">
<div className="mb-3">
<h6 className="font-weight-bold mb-3">
{t('plans.updatePlan.addOns')}
</h6>
<Row className="px-3">
{addonFeatures.map((type) => (
<Col xs="12" key={type.name}>
<FormGroup>
<Label>
{t(`plans.currentPlan.${type.transKey}`)}
</Label>
<NumberPicker
{...type.props}
value={form[type.name]}
onChange={(val) =>
handleChange(type.name, val)
}
/>
</FormGroup>
</Col>
))}
</Row>
</div>
</Col>
</Row>
<div className="widget-content total-price">
<div className="widget-content-wrapper justify-content-start justify-content-md-end mr-5">
<div className="widget-content-left">
<div className="widget-heading">
{t('plans.updatePlan.totalCost')}
</div>
<div className="widget-subheading">
{t('plans.updatePlan.monthly')}
</div>
</div>
<div className="widget-content-right position-relative ml-0 ml-5">
{/* {updatingPrice && (
<div className="widget-numbers position-absolute text-secondary px-3">
<FontAwesomeIcon icon={faSpinner} pulse />
</div>
)} */}
<div
className={`widget-numbers text-warning ${
updatingPrice ? 'opacity-3' : ''
}`}
>
${totalCost}
</div>
</div>
</div>
</div>
</Col>
</Row>
<hr />
{restrictions.isPlanCancelled || restrictions.isPlanDowngrade ? (
<p className="text-danger mb-3">
{t('plans.updatePlan.cancelledWarning', {
text: restrictions.isPlanCancelled
? 'cancelled'
: 'downgraded'
})}{' '}
{restrictions.subStartDate && restrictions.subEndDate
? `(${convertUTCtoLocal(
restrictions.subStartDate,
'MMM D, YYYY'
)} - ${convertUTCtoLocal(
restrictions.subEndDate,
'MMM D, YYYY'
)})`
: ''}
</p>
) : (
''
)}
<div className="text-right">
<Button
type="button"
disabled={updatingPrice || loading || disableUpdate}
onClick={handleSubmit}
className="btn-wide"
color="primary"
size="lg"
>
{loading
? t('plans.updatePlan.continueBtnLoading')
: t('plans.updatePlan.continueBtn')}
</Button>
</div>
</Form>
</CardBody>
) : (
<CardBody>
<CardTitle>{t('plans.updatePlan.billingHeading')}</CardTitle>
<BillingDetailsForm
form={paymentForm}
errors={paymentFormErrors}
handleChange={handlePaymentForm}
handleValidation={handlePaymentValidation}
/>
<Row className="divider" />
{paymentError && (
<Alert color="danger">
<Fragment>
<p className="font-size-xs font-weight-bold text-uppercase">
{t('plans.updatePlan.error')}
</p>
{paymentError.message}
</Fragment>
</Alert>
)}
<div className="d-flex justify-content-between flex-column-reverse flex-sm-row">
<Button
type="button"
color="secondary"
size="lg"
disabled={paymentLoading}
onClick={moveBack}
>
{t('plans.updatePlan.back')}
</Button>
<Button
type="button"
color="primary"
onClick={submitPayment}
disabled={!stripe || !elements || paymentLoading}
className="btn-wide btn-hover-shine mb-2 mb-sm-0"
size="lg"
>
{paymentLoading
? t('plans.updatePlan.payLoading')
: t('plans.updatePlan.payBtn', { totalCost })}
</Button>
</div>
</CardBody>
)}
</Card>
<Modal isOpen={modal} toggle={toggle} backdrop="static">
<ModalHeader toggle={toggle}>
{t('plans.updatePlan.confirmationHeading')}
</ModalHeader>
<ModalBody>
<div>
{restrictions.plans && restrictions.plans.price > 0 ? (
restrictions.plans.price === totalCost ? null : restrictions.plans
.price < totalCost ? (
<p className="text-muted mb-3">
{t('plans.updatePlan.upgradeNotice')}
</p>
) : (
<p className="text-muted mb-3">
{t('plans.updatePlan.downgradeNotice')}
</p>
)
) : null}
<p>{t('plans.updatePlan.alreadyStoredCard')}</p>
</div>
</ModalBody>
<ModalFooter>
<Button color="link" onClick={proceedToDetails} disabled={loading}>
{t('plans.updatePlan.payWithOtherCardBtn')}
</Button>
<Button color="primary" disabled={loading} onClick={proceedPayment}>
{loading
? t('plans.updatePlan.payLoading')
: t('plans.updatePlan.payWithStoredCardBtn')}
</Button>
</ModalFooter>
</Modal>
</Col>
);
}
UpdatePlan.propTypes = {
t: PropTypes.func.isRequired,
actions: PropTypes.object,
restrictions: PropTypes.object
};
export default reduxConnect('restrictions', [
'common',
'auth',
'user',
'restrictions'
])(translate(['tabsContent'], { wait: true })(UpdatePlan));
@@ -0,0 +1,72 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { Button, Modal, ModalBody, ModalHeader } from 'reactstrap';
import { planRoutes } from './UserPlans';
import { Trans, translate } from 'react-i18next';
function UpgradePlanModal({ isModalOpen = false, toggle, t }) {
function toggleModal() {
return toggle();
}
return (
<Modal
isOpen={isModalOpen}
toggle={toggleModal}
modalClassName="zoom-modal"
backdrop="static"
>
<ModalHeader toggle={toggleModal} />
<ModalBody className="px-4 px-sm-5 pb-5">
<div className="text-center">
<div className="display-4 mb-2">
<i className="lnr-rocket text-primary"></i>
</div>
<h3 className="mb-3">{t('plans.upgradeModal.heading')}</h3>
<div className="mb-4">
<p className="text-muted">
<Trans i18nKey="plans.upgradeModal.text">
You have to upgrade your plan to get access of these features.
Take a look at our bite-sized
<strong>à la carte menu options</strong> with monthly billing.
</Trans>{' '}
<a
href="https://www.socialhose.io/en/pricing"
rel="noopener noreferrer"
target="_blank"
>
{t('plans.upgradeModal.learnMore')}
</a>
</p>
</div>
<div>
<Button
tag={Link}
to={`/app/plans/${planRoutes.update}`}
onClick={toggleModal}
className="btn-pill btn-wide d-block mx-auto"
color="success"
size="lg"
>
{t('plans.upgradeModal.upgradeNowBtn')}
</Button>
<Button color="link" size="sm" onClick={toggleModal}>
{t('plans.upgradeModal.maybeLaterBtn')}
</Button>
</div>
</div>
</ModalBody>
</Modal>
);
}
UpgradePlanModal.propTypes = {
isModalOpen: PropTypes.bool,
t: PropTypes.func.isRequired,
toggle: PropTypes.func
};
export default React.memo(
translate(['tabsContent'], { wait: true })(UpgradePlanModal)
);
@@ -0,0 +1,128 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import {
NavLink,
Redirect,
Route,
Switch,
useRouteMatch
} from 'react-router-dom';
import reduxConnect from '../../../../redux/utils/connect';
import ChangeCard from './ChangeCard';
import CurrentPlan from './CurrentPlan';
import UpdatePlan from './UpdatePlan';
import UserTransactions from './UserTransactions';
import { Card, CardBody, Col, Row } from 'reactstrap';
import { translate } from 'react-i18next';
export const planRoutes = {
current: 'current',
changeCard: 'change-card',
txn: 'transactions',
update: 'update'
};
function UserPlans({ actions, restrictions, t }) {
const match = useRouteMatch();
useEffect(() => {
const { setEnableClosedSidebar } = actions;
actions.getRestrictions();
setEnableClosedSidebar(true);
return () => setEnableClosedSidebar(false);
}, []);
return (
<Row>
<Col xs={12} lg={4} xl={3}>
<Card className="mb-3">
<CardBody className="navigation-vertical">
<ul className="navigation-ul">
<li className="navigation-item">
<NavLink
className="navigation-link"
activeClassName="active"
to={`${match.url}/${planRoutes.current}`}
>
<em>
<i className="font-size-lg lnr-file-empty"> </i>
</em>
<span>{t('plans.sidebar.activePlanDetails')}</span>
</NavLink>
</li>
{restrictions.isPaymentId && (
<li>
<NavLink
className="navigation-link"
activeClassName="active"
to={`${match.url}/${planRoutes.changeCard}`}
>
<em>
<i className="font-size-lg lnr-license"> </i>
</em>
<span>{t('plans.sidebar.changeCard')}</span>
</NavLink>
</li>
)}
<li>
<NavLink
className="navigation-link"
activeClassName="active"
to={`${match.url}/${planRoutes.update}`}
>
<em>
<i className="font-size-lg lnr-arrow-up-circle"> </i>
</em>
<span>{t('plans.sidebar.updatePlan')}</span>
</NavLink>
</li>
<li>
<NavLink
className="navigation-link"
activeClassName="active"
to={`${match.url}/${planRoutes.txn}`}
>
<em>
<i className="font-size-lg lnr-list"> </i>
</em>
<span>{t('plans.sidebar.yourTransactions')}</span>
</NavLink>
</li>
</ul>
</CardBody>
</Card>
</Col>
<Switch>
<Route path={`${match.url}/${planRoutes.current}`}>
<CurrentPlan />
</Route>
{restrictions.isPaymentId && (
<Route path={`${match.url}/${planRoutes.changeCard}`}>
<ChangeCard />
</Route>
)}
<Route path={`${match.url}/${planRoutes.txn}`}>
<UserTransactions />
</Route>
<Route path={`${match.url}/${planRoutes.update}`}>
<UpdatePlan />
</Route>
<Redirect to={`${match.url}/current`} />
</Switch>
</Row>
);
}
UserPlans.propTypes = {
t: PropTypes.func.isRequired,
actions: PropTypes.object.isRequired,
restrictions: PropTypes.object.isRequired
};
export default reduxConnect('restrictions', [
'common',
'auth',
'user',
'restrictions'
])(translate(['tabsContent'], { wait: true })(UserPlans));
@@ -0,0 +1,132 @@
/* eslint-disable react/jsx-no-bind */
/* eslint-disable react/prop-types */
import React, { useState, useCallback, useEffect } from 'react';
import PropTypes from 'prop-types';
import { reduxActions } from '../../../../redux/utils/connect';
import {
convertUTCtoLocal,
getQueryParams,
setDocumentData
} from '../../../../common/helper';
import { getTransactions } from '../../../../api/plans/userPlans';
import Table from '../../../common/Table/Table';
import { Button, Col } from 'reactstrap';
import ShowTransactionDetails from './ShowTransactionDetails';
import moment from 'moment';
import { capitalize } from 'lodash';
import { translate } from 'react-i18next';
function UserTransactions(props) {
const [dataSource, setDataSource] = useState({ data: [] });
const [loading, setLoading] = useState(true);
const [selectedData, setSelectedData] = useState(false);
const { actions, t } = props;
useEffect(() => {
setDocumentData('title', 'User Transactions');
return () => setDocumentData('title'); // default
}, []);
const columns = [
{
id: 'activeDate',
Header: t('plans.transactions.activationDate'),
accessor: (d) => d.lines.data[0] && d.lines.data[0].period.start,
Cell: (props) => convertUTCtoLocal(moment.unix(props.value), 'MM/DD/YYYY')
},
{
id: 'expireDate',
Header: t('plans.transactions.expirationDate'),
accessor: (d) => d.lines.data[0] && d.lines.data[0].period.end,
Cell: (props) => convertUTCtoLocal(moment.unix(props.value), 'MM/DD/YYYY')
},
{
id: 'paid_at',
Header: t('plans.transactions.transactionDate'),
accessor: (d) => d.status_transitions.paid_at,
Cell: (props) =>
convertUTCtoLocal(moment.unix(props.value), 'MM/DD/YYYY HH:mm:ss')
},
{
Header: t('plans.transactions.amount'),
accessor: 'amount_paid',
Cell: (props) => (props.value ? `$${props.value / 100}` : '-')
},
{
Header: t('plans.transactions.status'),
accessor: 'status',
Cell: (props) => capitalize(props.value)
},
{
Header: t('plans.transactions.actions'),
accessor: 'id',
Cell: (props) => (
<Button
outline
className="border-0 btn-transition"
color="primary"
size="sm"
onClick={() => setSelectedData(props.original)}
>
{t('plans.transactions.more')}
</Button>
)
}
];
function closeModal() {
setSelectedData(false);
}
const getTransactionList = useCallback((page, pageSize) => {
setLoading(true);
const params = getQueryParams({ page, pageSize });
getTransactions(params).then((res) => {
if (res.error || !res.data || !res.data.success || !res.data.data) {
setDataSource({ data: [] }); // comment this line when API is ready
setLoading(false);
return actions.addAlert({
type: 'error',
transKey: 'somethingWrong'
});
}
// setDataSource(sampleData); // comment this line when API is ready
setDataSource({
data:
res.data.data.data && res.data.data.data.length > 0
? res.data.data.data
: []
});
setLoading(false);
});
}, []);
const { data = [], totalCount = 0, limit = 100, page = 1 } = dataSource;
return (
<Col xs="12" lg="8" xl="9">
<Table
cardTitle={t('plans.transactions.heading')}
columns={columns}
data={data}
totalCount={totalCount}
showTotalCount
limit={limit}
page={page}
isLoading={loading}
onFetchData={getTransactionList}
/>
<ShowTransactionDetails data={selectedData} closeModal={closeModal} />
</Col>
);
}
UserTransactions.propTypes = {
t: PropTypes.func.isRequired,
actions: PropTypes.object
};
export default reduxActions()(
translate(['tabsContent'], { wait: true })(UserTransactions)
);
+226
View File
@@ -0,0 +1,226 @@
import React from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import { withRouter } from 'react-router-dom';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { DndProvider } from 'react-dnd';
import { isMobile } from 'react-device-detect';
import { TouchBackend } from 'react-dnd-touch-backend';
import cx from 'classnames';
import echarts from 'echarts';
import ResizeDetector from 'react-resize-detector';
import AppHeader from './AppHeader/AppHeader';
import WebTour from './AppHeader/WebTour';
import Sidebar from './Sidebar/Sidebar';
import reduxConnect from '../../redux/utils/connect';
// import { NOTIFICATION_SUBSCREENS } from '../../redux/modules/appState/share/tabs';
import LoadersAdvanced from '../common/Loader/Loader';
import WesteronTheme from '../common/charts/WesterosTheme.json';
import 'react-dates/initialize';
import 'react-dates/lib/css/_datepicker.css';
import Footer from '../common/Footer';
import { Button, UncontrolledTooltip } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faQuestion } from '@fortawesome/free-solid-svg-icons';
import { find, map } from 'lodash';
import tourPages from './AppHeader/WebTourSteps';
import { allMediaTypes } from '../../redux/modules/appState/searchByFilters';
import { translate } from 'react-i18next';
import i18n from '../../i18n';
import * as timeago from 'timeago.js';
import ar from 'timeago.js/lib/lang/ar';
import fr from 'timeago.js/lib/lang/fr';
// register it languages for time-ago.
timeago.register('ar', ar);
timeago.register('fr', fr);
const DnDBackend = isMobile ? TouchBackend : HTML5Backend;
class App extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
actions: PropTypes.object.isRequired,
children: PropTypes.element,
history: PropTypes.object.isRequired,
location: PropTypes.object.isRequired,
store: PropTypes.object.isRequired
};
state = {
showSidebar: true,
sidebarAnimationDisabled: true,
closedSmallerSidebar: false,
showTourIcon: false
};
componentDidMount() {
echarts.registerTheme('westeros', WesteronTheme);
this.checkIfTourGuide();
const {
common: { auth }
} = this.props.store;
const activeLang = i18n.language.slice(0, 2);
this.props.actions.chooseLanguage(activeLang);
if (
auth &&
auth.user &&
auth.user.restrictions &&
auth.user.restrictions.plans
) {
const planDetails = auth.user.restrictions.plans;
let allowedMediaTypes = allMediaTypes.filter((v) => planDetails[v]);
/*if (auth.user.restrictions.plans.price === 0) {
// TODO: remove following restrictions when duplication fixes
const restrictedTemporary = ['news', 'blogs'];
allowedMediaTypes = allowedMediaTypes.filter(
(v) => !restrictedTemporary.includes(v)
);
} */
this.props.actions.toggleMediaType(allowedMediaTypes, true);
} else {
this.props.actions.toggleMediaType([], true);
}
}
componentDidUpdate(prevProps) {
if (prevProps.location.pathname !== this.props.location.pathname) {
this.checkIfTourGuide();
}
}
checkIfTourGuide = () => {
const tourCurrentPaths = map(tourPages, 'showOn');
const hasTour = tourCurrentPaths.some((path) =>
this.props.location.pathname.startsWith(path)
);
if (hasTour) {
!this.state.showTourIcon && this.setState({ showTourIcon: true });
return;
}
this.state.showTourIcon && this.setState({ showTourIcon: false });
};
showWebTour = () => {
const tourSendPaths = find(tourPages, (o) =>
this.props.location.pathname.startsWith(o.showOn)
);
if (tourSendPaths) {
// Open in a new tab to reset every redux state
const win = window.open(`${tourSendPaths.to}?webtour=true`, '_blank');
win.focus();
}
};
render() {
const { store, actions, children, t } = this.props;
const { common: commonState, appState } = store;
const { sidebar, themeOptions } = appState;
const { base, auth } = commonState;
const {
colorScheme,
enableFixedHeader,
enableFixedSidebar,
enableFixedFooter,
enableClosedSidebar,
closedSmallerSidebar,
enableMobileMenu,
enablePageTabsAlt
} = themeOptions;
if (!auth.token) {
<LoadersAdvanced />;
}
return (
<ResizeDetector
handleWidth
// eslint-disable-next-line react/jsx-no-bind
render={({ width }) => {
return (
<DndProvider backend={DnDBackend}>
<div
className={cx(
'app-container app-theme-' + colorScheme,
{ 'fixed-header': enableFixedHeader },
{ 'fixed-sidebar': enableFixedSidebar || width < 1250 },
{ 'fixed-footer': enableFixedFooter },
{ 'closed-sidebar': enableClosedSidebar || width < 1250 },
{
'closed-sidebar-mobile':
closedSmallerSidebar || width < 1250
},
{ 'sidebar-mobile-open': enableMobileMenu },
{ 'body-tabs-shadow-btn': enablePageTabsAlt }
)}
>
{this.state.showTourIcon && (
<div>
<Button
id="GuidedTour"
className="floating-icon"
color="warning"
onClick={this.showWebTour}
>
<FontAwesomeIcon
icon={faQuestion}
color="#573a04"
fixedWidth={false}
size="2x"
/>
</Button>
<UncontrolledTooltip placement="left" target={'GuidedTour'}>
{t('userSettings.guidedTourTooltip')}
</UncontrolledTooltip>
</div>
)}
<AppHeader
appCommonState={base}
userFirstName={auth.user.firstName}
userLastName={auth.user.lastName}
restrictions={auth.user.restrictions}
userRole={auth.user.role}
actions={actions}
themeOptions={themeOptions}
/>
<div className="app-main">
<Sidebar
t={t}
sidebarState={sidebar}
themeOptions={themeOptions}
actions={actions}
/>
<div className="app-main__outer">
<div className="app-main__inner">
{children}
<Footer />
</div>
</div>
</div>
<WebTour />
</div>
</DndProvider>
);
}}
/>
);
}
}
const applyDecorators = compose(
translate(['common'], { wait: true }),
withRouter,
reduxConnect()
);
export default applyDecorators(App);
@@ -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;
@@ -0,0 +1,67 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import {
Button,
Modal,
ModalHeader,
ModalBody,
Label,
Input,
ModalFooter
} from 'reactstrap';
export class AddCategoryPopup extends React.Component {
state = {
folderName: ''
};
static propTypes = {
parentId: PropTypes.number.isRequired,
hideAddCategoryPopup: PropTypes.func.isRequired,
addCategory: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
onChangeName = (e) => {
const { value } = e.target; // need validation
this.setState({ folderName: value });
};
hidePopup = () => {
this.props.hideAddCategoryPopup();
};
onSubmit = () => {
const { folderName } = this.state;
this.props.addCategory(folderName, this.props.parentId);
this.props.hideAddCategoryPopup();
};
render() {
const { t } = this.props;
const { folderName } = this.state;
return (
<Modal isOpen toggle={this.hidePopup} backdrop="static">
<ModalHeader toggle={this.hidePopup}>
{t('sidebarPopup.addFolderBtn')}
</ModalHeader>
<ModalBody>
<Label>{t('sidebarPopup.enterFolderName')}</Label>
<Input type="text" value={folderName} onChange={this.onChangeName} />
</ModalBody>
<ModalFooter>
<Button color="light" onClick={this.hidePopup}>
{t('commonWords.Cancel')}
</Button>
<Button color="primary" onClick={this.onSubmit}>
{t('sidebarPopup.addFolderBtn')}
</Button>
</ModalFooter>
</Modal>
);
}
}
export default translate(['common'], { wait: true })(AddCategoryPopup);
@@ -0,0 +1,121 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import Select from 'react-select';
import {
Button,
Modal,
ModalHeader,
ModalBody,
FormGroup,
Label,
Input,
ModalFooter
} from 'reactstrap';
export class AddClippingsFeedPopup extends React.Component {
static propTypes = {
parentId: PropTypes.number.isRequired,
hidePopup: PropTypes.func.isRequired,
addClippingsFeed: PropTypes.func.isRequired,
addAlert: PropTypes.func.isRequired,
categories: PropTypes.array.isRequired,
t: PropTypes.func.isRequired
};
constructor(props) {
super(props);
this.state = {
parentId: props.parentId,
feedName: ''
};
}
onChangeName = (e) => {
const { value } = e.target;
this.setState({ feedName: value });
};
hidePopup = () => {
this.props.hidePopup();
};
onSubmit = () => {
const { parentId } = this.state;
const { addAlert, addClippingsFeed, hidePopup } = this.props;
const { feedName } = this.state;
if (feedName) {
addClippingsFeed(feedName, parentId);
hidePopup();
} else {
addAlert({
type: 'error',
transKey: 'feedNameEmpty'
});
}
};
flattenCategories = (categories, level = '') => {
return categories.reduce((result, category) => {
result.push({
label:
level +
this.props.t(`sidebar.${category.name}`, {
defaultValue: category.name
}),
value: category.id
});
if (category.childes && category.childes.length) {
return result.concat(
this.flattenCategories(category.childes, '- ' + level)
);
}
return result;
}, []);
};
onParentCategorySelect = (value) => {
this.setState({ parentId: value });
};
render() {
const { t, categories } = this.props;
const { parentId, feedName } = this.state;
const options = this.flattenCategories(categories);
return (
<Modal isOpen toggle={this.hidePopup} backdrop="static">
<ModalHeader toggle={this.hidePopup}>
{t('sidebarPopup.addClippingsFeed')}
</ModalHeader>
<ModalBody>
<FormGroup>
<Label>{t('sidebarPopup.feedName')}</Label>
<Input type="text" value={feedName} onChange={this.onChangeName} />
</FormGroup>
<FormGroup>
<Label>{t('sidebarPopup.folder')}</Label>
<Select
onChange={this.onParentCategorySelect}
options={options}
value={parentId}
editable={false}
clearable={false}
simpleValue
/>
</FormGroup>
</ModalBody>
<ModalFooter>
<Button color="light" onClick={this.hidePopup}>
{t('commonWords.Cancel')}
</Button>
<Button color="primary" onClick={this.onSubmit}>
{t('sidebarPopup.addClippingsFeed')}
</Button>
</ModalFooter>
</Modal>
);
}
}
export default translate(['common'], { wait: true })(AddClippingsFeedPopup);
@@ -0,0 +1,58 @@
import React from 'react'
import PropTypes from 'prop-types'
import Category from './Category'
export class Categories extends React.Component {
static propTypes = {
actions: PropTypes.object.isRequired,
areCategoriesLoaded: PropTypes.bool.isRequired,
areFeedsFiltered: PropTypes.bool.isRequired,
categories: PropTypes.array.isRequired,
filteredCategories: PropTypes.array.isRequired
};
hideParentCategoryDrop = () => {}; //empty func for first level categories
render () {
const { areCategoriesLoaded, areFeedsFiltered, actions } = this.props
const {
showDeletePopup, showRenamePopup, showAddCategoryPopup,
showAddClippingsFeedPopup, getFeedResults,
moveCategory, moveFeed, toggleExportFeed,
toggleExportCategory, clipArticles
} = actions
const categories = areFeedsFiltered ? this.props.filteredCategories : this.props.categories
return (
<div className='sidebar-categories'>
{areCategoriesLoaded &&
categories.map((category, i) => {
return (
<Category
hideParentCategoryDrop={this.hideParentCategoryDrop} //set empty func
parentId={-1} //set empty parent category for first level categories
category={category}
categories={categories}
showDeletePopup={showDeletePopup}
showRenamePopup={showRenamePopup}
showAddCategoryPopup={showAddCategoryPopup}
showAddClippingsFeedPopup={showAddClippingsFeedPopup}
getFeedResults={getFeedResults}
moveCategory={moveCategory}
moveFeed={moveFeed}
clipArticles={clipArticles}
key={'main-category' + i}
toggleExportFeed={toggleExportFeed}
toggleExportCategory={toggleExportCategory}
/>
)
})
}
</div>
)
}
}
export default Categories
@@ -0,0 +1,219 @@
import React from 'react';
import PropTypes from 'prop-types';
import { DropTarget, DragSource } from 'react-dnd';
import { compose } from 'redux';
import onClickOutside from 'react-onclickoutside';
import Feed from './Feed';
import CategoryHead from './CategoryHead';
import { TYPES } from '../../../redux/modules/appState/sidebar';
import cx from 'classnames';
const folderSource = {
beginDrag(props) {
return {
type: TYPES.FOLDER,
id: props.category.id,
category: props.category
};
},
canDrag(props) {
return props.category.type === 'directory';
}
};
const targetTypes = [TYPES.FEED, TYPES.FOLDER];
const categoryTarget = {
drop(props, monitor) {
if (monitor.didDrop()) return;
const { category, moveCategory, moveFeed } = props;
const item = monitor.getItem();
const draggedCategoryId = item.id;
const newCategoryId = category.id;
if (item.type === TYPES.FOLDER) {
moveCategory(item.category, newCategoryId);
} else if (item.type === TYPES.FEED) {
moveFeed(draggedCategoryId, newCategoryId);
}
},
canDrop(props, monitor) {
const categoryType = props.category.type;
return (
categoryType !== 'deleted_content' && categoryType !== 'shared_content'
);
}
};
export class CategoryClass extends React.Component {
static propTypes = {
parentId: PropTypes.number.isRequired,
category: PropTypes.object.isRequired,
showDeletePopup: PropTypes.func.isRequired,
showRenamePopup: PropTypes.func.isRequired,
showAddCategoryPopup: PropTypes.func.isRequired,
showAddClippingsFeedPopup: PropTypes.func.isRequired,
hideParentCategoryDrop: PropTypes.func.isRequired,
categories: PropTypes.array.isRequired,
connectDropTarget: PropTypes.func.isRequired,
connectDragSource: PropTypes.func.isRequired,
getFeedResults: PropTypes.func.isRequired,
moveFeed: PropTypes.func.isRequired,
moveCategory: PropTypes.func.isRequired,
clipArticles: PropTypes.func.isRequired,
toggleExportFeed: PropTypes.func.isRequired,
toggleExportCategory: PropTypes.func.isRequired
};
constructor(props) {
super(props);
this.state = {
isCategoryActive: true, // sub menus
isCategoryDropActive: false // more options
};
}
// hide category dropdown if there was click outside
handleClickOutside = () => {
this.state.isCategoryDropActive && this.hideCategoryDropdown();
};
toggleCollapse = (e) => {
if (e.target === e.currentTarget) {
this.setState((prev) => ({
isCategoryActive: !prev.isCategoryActive
}));
}
};
toggleCategoryDropdown = (e) => {
e.preventDefault();
// this.props.hideParentCategoryDrop();
this.setState((prev) => ({
isCategoryDropActive: !prev.isCategoryDropActive
}));
};
hideCategoryDropdown = () => {
this.setState({
isCategoryDropActive: false
});
};
render() {
const {
category,
categories,
connectDropTarget,
connectDragSource,
hideParentCategoryDrop,
parentId,
showDeletePopup,
getFeedResults,
showRenamePopup,
showAddCategoryPopup,
moveCategory,
moveFeed,
showAddClippingsFeedPopup,
clipArticles,
toggleExportFeed,
toggleExportCategory
} = this.props;
const isFeeds = category.feeds.length > 0;
const isChildes = category.childes.length > 0;
const categoryType = category.type;
let categoryActiveClass = this.state.isCategoryActive
? ' active-category'
: '';
return connectDragSource(
connectDropTarget(
<li
className={'metismenu-item ' + categoryType + categoryActiveClass}
onClick={hideParentCategoryDrop}
>
<CategoryHead
toggleCollapse={this.toggleCollapse}
toggleCategoryDropdown={this.toggleCategoryDropdown}
isCategoryDropActive={this.state.isCategoryDropActive}
isCategoryActive={this.state.isCategoryActive}
hideDropDown={this.hideCategoryDropdown}
parentId={parentId}
category={category}
showDeletePopup={showDeletePopup}
showRenamePopup={showRenamePopup}
showAddCategoryPopup={showAddCategoryPopup}
toggleExportCategory={toggleExportCategory}
showAddClippingsFeedPopup={showAddClippingsFeedPopup}
categories={categories}
/>
<ul
className={cx('metismenu-container', {
visible: this.state.isCategoryActive
})}
>
{isFeeds &&
category.feeds.map((feed, i) => {
return (
<Feed
key={'feed' + i}
feed={feed}
showDeletePopup={showDeletePopup}
showRenamePopup={showRenamePopup}
categories={categories}
categoryId={category.id}
hideParentCategoryDrop={this.hideCategoryDropdown}
getFeedResults={getFeedResults}
clipArticles={clipArticles}
toggleExportFeed={toggleExportFeed}
/>
);
})}
{isChildes &&
category.childes.map((_category, i) => {
return (
<Category
key={'category' + i}
showDeletePopup={showDeletePopup}
showRenamePopup={showRenamePopup}
showAddCategoryPopup={showAddCategoryPopup}
showAddClippingsFeedPopup={showAddClippingsFeedPopup}
parentId={category.id}
category={_category}
categories={categories}
hideParentCategoryDrop={this.hideCategoryDropdown}
getFeedResults={getFeedResults}
connectDropTarget={connectDropTarget}
connectDragSource={connectDragSource}
moveCategory={moveCategory}
moveFeed={moveFeed}
clipArticles={clipArticles}
toggleExportFeed={toggleExportFeed}
toggleExportCategory={toggleExportCategory}
/>
);
})}
</ul>
</li>
)
);
}
}
export const Category = compose(
DropTarget(targetTypes, categoryTarget, (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
itemType: monitor.getItemType()
})),
DragSource(TYPES.FOLDER, folderSource, (connect) => ({
connectDragSource: connect.dragSource()
}))
)(onClickOutside(CategoryClass));
export default Category;
@@ -0,0 +1,105 @@
import React from 'react';
import cx from 'classnames';
import PropTypes from 'prop-types';
import SidebarDropdown from './SidebarDropdown';
import { translate } from 'react-i18next';
class CategoryHead extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
showDeletePopup: PropTypes.func.isRequired,
showRenamePopup: PropTypes.func.isRequired,
showAddCategoryPopup: PropTypes.func.isRequired,
showAddClippingsFeedPopup: PropTypes.func.isRequired,
toggleCollapse: PropTypes.func.isRequired,
toggleCategoryDropdown: PropTypes.func.isRequired,
toggleExportCategory: PropTypes.func.isRequired,
isCategoryDropActive: PropTypes.bool.isRequired,
isCategoryActive: PropTypes.bool.isRequired,
hideDropDown: PropTypes.func.isRequired,
parentId: PropTypes.number.isRequired,
category: PropTypes.object.isRequired,
categories: PropTypes.array.isRequired
};
getSidebarName(name) {
const catName = this.props.t(`sidebar.${name}`);
if (catName === `sidebar.${name}`) {
return name;
}
return catName;
}
render() {
const {
isCategoryActive,
isCategoryDropActive,
category,
categories,
showDeletePopup,
showRenamePopup,
showAddCategoryPopup,
showAddClippingsFeedPopup,
toggleExportCategory,
hideDropDown
} = this.props;
const isCategoryDeletedType = category.subType === 'deleted_content';
const categoryAttrId = 'sidebar-category' + category.id;
return (
<div
className="metismenu-link"
id={categoryAttrId}
onClick={this.props.toggleCollapse}
>
{/* <i className="sidebar-category__closed-icon" onClick={this.props.toggleCollapse}> </i>
<i className="sidebar-category__open-icon" onClick={this.props.toggleCollapse}> </i> */}
{isCategoryDeletedType ? (
<i className="metismenu-icon pe-7s-trash"></i>
) : (
<i className="metismenu-icon pe-7s-folder"></i>
)}
{this.getSidebarName(category.name)}
{!isCategoryDeletedType && (
<i
tabIndex="0"
className="metismenu-state-icon font-size-lg opacity-10 pe-7s-more mr-4"
onClick={this.props.toggleCategoryDropdown}
/>
)}
<i
className={cx(
'metismenu-state-icon pe-7s-angle-down pointer-events-none opacity-10',
{
'rotate-minus-90': isCategoryActive
}
)}
/>
{isCategoryDropActive && (
<SidebarDropdown
parentAttrId={categoryAttrId}
categories={categories}
itemId={category.id}
itemSubType={category.subType}
itemType={category.type}
itemName={category.name}
parentId={this.props.parentId}
showDeletePopup={showDeletePopup}
showRenamePopup={showRenamePopup}
showAddCategoryPopup={showAddCategoryPopup}
showAddClippingsPopup={showAddClippingsFeedPopup}
hideDropDown={hideDropDown}
toggleExportCategory={toggleExportCategory}
/>
)}
</div>
);
}
}
export default translate(['common'], { wait: true })(CategoryHead);
@@ -0,0 +1,65 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
export class DeletePopup extends React.Component {
static propTypes = {
itemToDelete: PropTypes.object.isRequired,
hideDeletePopup: PropTypes.func.isRequired,
deleteFeed: PropTypes.func.isRequired,
deleteCategory: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
hidePopup = () => {
this.props.hideDeletePopup();
};
onSubmit = () => {
const {
itemToDelete,
deleteCategory,
deleteFeed,
hideDeletePopup
} = this.props;
switch (this.props.itemToDelete.itemType) {
case 'feed':
deleteFeed(itemToDelete.itemId, itemToDelete.parentId);
break;
case 'directory':
deleteCategory(itemToDelete.itemId);
break;
}
hideDeletePopup();
};
render() {
const itemName = this.props.itemToDelete.itemName;
const itemType = this.props.itemToDelete.itemType;
const { t } = this.props;
return (
<Modal isOpen toggle={this.hidePopup} backdrop="static">
<ModalHeader toggle={this.hidePopup}>
{t('commonWords.Confirm')}
</ModalHeader>
<ModalBody>
<p>
{t('messages.deleteMessage')} {itemType + ' "' + itemName + '"'}
</p>
</ModalBody>
<ModalFooter>
<Button color="light" onClick={this.hidePopup}>
{t('commonWords.Cancel')}
</Button>
<Button color="danger" onClick={this.onSubmit}>
{t('commonWords.Delete')}
</Button>
</ModalFooter>
</Modal>
);
}
}
export default translate(['common'], { wait: true })(DeletePopup);
+162
View File
@@ -0,0 +1,162 @@
/** DRAG SOURCE **/
import React from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import SidebarDropdown from './SidebarDropdown';
import { DragSource, DropTarget } from 'react-dnd';
import onClickOutside from 'react-onclickoutside';
import { TYPES } from '../../../redux/modules/appState/sidebar';
import { withRouter } from 'react-router-dom';
const feedSource = {
beginDrag(props) {
return {
type: TYPES.FEED,
id: props.feed.id,
feed: props.feed,
currentCategoryId: props.categoryId
};
}
};
/**
* Specifies which props to inject into component from Drag n Drop.
*/
function dragCollect(connect) {
return {
// Call this function inside render()
// to let React DnD handle the drag events:
connectDragSource: connect.dragSource()
};
}
/** DROP TARGET **/
const feedTarget = {
drop(props, monitor) {
if (monitor.didDrop()) return;
const { feed, clipArticles } = props;
clipArticles(feed.id);
},
canDrop(props, monitor) {
return props.feed.subType === 'clip_feed';
}
};
function dropCollect(connect, monitor) {
return {
connectDropTarget: connect.dropTarget()
};
}
export class Feed extends React.Component {
static propTypes = {
feed: PropTypes.object.isRequired,
categoryId: PropTypes.number.isRequired,
categories: PropTypes.array.isRequired,
showDeletePopup: PropTypes.func.isRequired,
showRenamePopup: PropTypes.func.isRequired,
hideParentCategoryDrop: PropTypes.func.isRequired,
connectDragSource: PropTypes.func.isRequired,
connectDropTarget: PropTypes.func.isRequired,
getFeedResults: PropTypes.func.isRequired,
clipArticles: PropTypes.func.isRequired,
toggleExportFeed: PropTypes.func.isRequired,
history: PropTypes.object.isRequired
};
constructor(props) {
super(props);
this.state = {
isItemDropActive: false
};
}
//hide feed dropdown if there was click outside
handleClickOutside = () => {
this.state.isItemDropActive && this.hideDropDown();
};
hideDropDown = () => {
this.setState({
isItemDropActive: false
});
};
toggleItemDropdown = (e) => {
e.preventDefault();
this.setState({
isItemDropActive: !this.state.isItemDropActive
});
};
onFeedClick = (e) => {
const { history, getFeedResults, feed } = this.props;
e.preventDefault();
history.push('/app/search/search');
getFeedResults({ page: 1 }, feed.id);
window.scrollTo(0, 0);
};
render() {
const {
feed,
categoryId,
categories,
connectDragSource,
connectDropTarget,
showDeletePopup,
showRenamePopup,
toggleExportFeed
} = this.props;
const feedAttrId = 'sidebar-feed' + feed.id;
const dragAndDrop = compose(connectDragSource, connectDropTarget);
return dragAndDrop(
<li
id={feedAttrId}
onClick={this.props.hideParentCategoryDrop}
className="metismenu-item"
>
<a
href="#"
className={`metismenu-link feed-icon ${feed.class}`}
onClick={this.onFeedClick}
>
{feed.name}
</a>
<i
tabIndex="0"
className="metismenu-state-icon font-size-lg opacity-10 pe-7s-more"
onClick={this.toggleItemDropdown}
></i>
{this.state.isItemDropActive && (
<SidebarDropdown
parentAttrId={feedAttrId}
categories={categories}
itemId={feed.id}
itemType={feed.type}
itemSubType={feed.subType}
itemName={feed.name}
itemExported={feed.exported}
parentId={categoryId}
showDeletePopup={showDeletePopup}
showRenamePopup={showRenamePopup}
toggleExportFeed={toggleExportFeed}
hideDropDown={this.hideDropDown}
/>
)}
</li>
);
}
}
const applyDecorators = compose(
withRouter,
DragSource(TYPES.FEED, feedSource, dragCollect),
DropTarget([TYPES.CLIP_ARTICLE], feedTarget, dropCollect),
onClickOutside
);
export default applyDecorators(Feed);
@@ -0,0 +1,106 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
export class Filter extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
areFeedsFiltered: PropTypes.bool.isRequired,
categories: PropTypes.array.isRequired,
setFilteredCategories: PropTypes.func.isRequired,
clearFilteredCategories: PropTypes.func.isRequired
}
constructor (props) {
super(props)
this.state = {
sidebarAnimationDisabled: true,
activeSearch: false
}
}
activeSearchFunc = () => {
this.setState({ activeSearch: !this.state.activeSearch })
this.clearFilter()
}
filterCategoriesList = (
categories,
searchQuery,
setParentBranchMatchFromParent
) => {
// show category if there is feed
return categories.filter((category) => {
category.branchMatch = false
//function that sets parent branchMatch prop
function setParentBranchMatch (flag) {
category.branchMatch = flag
}
if (category.childes.length > 0) {
category.childes = this.filterCategoriesList(
category.childes,
searchQuery,
setParentBranchMatch
)
}
// filter feeds in category
category.feeds = category.feeds.filter((feed) => {
return feed.name.toLowerCase().indexOf(searchQuery) !== -1
})
// if this category is a child and it has matched feeds or its child have, then we set branchMatch prop of parent
if (
(category.feeds.length > 0 && setParentBranchMatchFromParent) ||
(category.branchMatch && setParentBranchMatchFromParent)
) {
setParentBranchMatchFromParent(true)
}
return category.branchMatch || category.feeds.length > 0
})
}
filterSidebarItems = (e) => {
const searchQuery = e.target.value.toLowerCase()
const categoriesCopy = this.props.categories.slice(0)
if (searchQuery.length) {
const filteredCat = this.filterCategoriesList(categoriesCopy, searchQuery)
this.props.setFilteredCategories(filteredCat)
} else {
this.props.clearFilteredCategories()
}
}
clearFilter = () => {
this.props.clearFilteredCategories()
}
render () {
return (
<div
className={classnames('search-wrapper mb-1', {
active: this.state.activeSearch
})}
>
<div className="input-holder">
<input
type="text"
className="search-input"
placeholder={this.props.t('common:sidebar.typeToSearch')}
onKeyUp={this.filterSidebarItems}
id="sidebar-search"
/>
<button onClick={this.activeSearchFunc} className="search-icon">
<span />
</button>
</div>
<button onClick={this.activeSearchFunc} className="close"></button>
</div>
)
}
}
export default Filter
@@ -0,0 +1,90 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import {
Button,
Input,
Label,
Modal,
ModalBody,
ModalFooter,
ModalHeader
} from 'reactstrap'
export class RenamePopup extends React.Component {
static propTypes = {
itemToRename: PropTypes.object.isRequired,
hideRenamePopup: PropTypes.func.isRequired,
renameFeed: PropTypes.func.isRequired,
renameCategory: PropTypes.func.isRequired,
addAlert: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
}
constructor(props) {
super(props)
this.state = {
itemName: props.itemToRename.itemName
}
}
hidePopup = () => {
this.props.hideRenamePopup()
}
onSubmit = () => {
const newName = this.state.itemName
const {
itemToRename,
renameFeed,
renameCategory,
hideRenamePopup
} = this.props
switch (this.props.itemToRename.itemType) {
case 'feed':
renameFeed(itemToRename.itemId, newName, itemToRename.parentId)
break
case 'directory':
renameCategory(itemToRename.itemId, newName, itemToRename.parentId)
break
}
hideRenamePopup()
}
onChangeName = (e) => {
const { value } = e.target // validation needed
this.setState({
itemName: value
})
}
render() {
const itemName = this.state.itemName
const { t } = this.props
return (
<Modal isOpen toggle={this.hidePopup} backdrop="static">
<ModalHeader toggle={this.hidePopup}>
{t('commonWords.Rename')}
</ModalHeader>
<ModalBody>
<Label>{t('sidebarPopup.enterNamelabel')}</Label>
<Input type="text" value={itemName} onChange={this.onChangeName} />
</ModalBody>
<ModalFooter>
<Button color="light" onClick={this.hidePopup}>
{t('commonWords.Cancel')}
</Button>
<Button color="primary" onClick={this.onSubmit}>
{t('commonWords.Rename')}
</Button>
</ModalFooter>
</Modal>
)
}
}
export default translate(['common'], { wait: true })(RenamePopup)
@@ -0,0 +1,155 @@
import React, { Fragment } from 'react'
import PropTypes from 'prop-types'
import cx from 'classnames'
import Categories from './Categories'
import Filter from './Filter'
import DeletePopup from './DeletePopup'
import RenamePopup from './RenamePopup'
import AddCategoryPopup from './AddCategoryPopup'
import AddClippingsFeedPopup from './AddClippingsFeedPopup'
import LoadersAdvanced from '../../common/Loader/Loader'
import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup'
import PerfectScrollbar from 'react-perfect-scrollbar'
import HeaderLogo from '../AppHeader/HeaderLogo'
export class Sidebar extends React.Component {
static propTypes = {
actions: PropTypes.object.isRequired,
themeOptions: PropTypes.object.isRequired,
backgroundColor: PropTypes.string,
backgroundImage: PropTypes.any,
backgroundImageOpacity: PropTypes.any,
enableBackgroundImage: PropTypes.any,
enableMobileMenu: PropTypes.any,
enableSidebarShadow: PropTypes.any,
setEnableMobileMenu: PropTypes.func,
t: PropTypes.func,
sidebarState: PropTypes.object.isRequired
}
constructor (props) {
super(props)
this.state = {
sidebarAnimationDisabled: true,
activeSearch: false
}
}
toggleMobileSidebar = () => {
let { enableMobileMenu, setEnableMobileMenu } = this.props
setEnableMobileMenu(!enableMobileMenu)
}
componentDidMount = () => {
this.props.actions.getSidebarCategories()
}
activeSearchFunc = () => {
this.setState({ activeSearch: !this.state.activeSearch })
}
render () {
let {
backgroundColor,
enableBackgroundImage,
enableSidebarShadow,
backgroundImage,
backgroundImageOpacity
} = this.props.themeOptions
const { sidebarState, actions } = this.props
return (
<Fragment>
<div
className="sidebar-mobile-overlay"
onClick={this.toggleMobileSidebar}
/>
<CSSTransitionGroup
component="div"
className={cx('app-sidebar', backgroundColor, {
'sidebar-shadow': enableSidebarShadow
})}
transitionName="SidebarAnimation"
transitionAppear
transitionAppearTimeout={1500}
transitionEnter={false}
transitionLeave={false}
>
<HeaderLogo />
{!sidebarState.areCategoriesLoaded && <LoadersAdvanced />}
<PerfectScrollbar>
<div className="app-sidebar__inner mt-3">
<div className="vertical-nav-menu" data-tour="left-panel">
<div className="metismenu-container">
<Filter
t={this.props.t}
categories={sidebarState.categories}
areFeedsFiltered={sidebarState.areFeedsFiltered}
setFilteredCategories={actions.setFilteredCategories}
clearFilteredCategories={actions.clearFilteredCategories}
/>
<Categories
actions={actions}
areCategoriesLoaded={sidebarState.areCategoriesLoaded}
areFeedsFiltered={sidebarState.areFeedsFiltered}
categories={sidebarState.categories}
filteredCategories={sidebarState.filteredCategories}
/>
</div>
{sidebarState.popupVisible.delete && (
<DeletePopup
hideDeletePopup={actions.hideDeletePopup}
deleteFeed={actions.deleteFeed}
deleteCategory={actions.deleteCategory}
itemToDelete={sidebarState.popupItems.delete}
/>
)}
{sidebarState.popupVisible.rename && (
<RenamePopup
addAlert={actions.addAlert}
hideRenamePopup={actions.hideRenamePopup}
renameFeed={actions.renameFeed}
renameCategory={actions.renameCategory}
itemToRename={sidebarState.popupItems.rename}
/>
)}
{sidebarState.popupVisible.addCategory && (
<AddCategoryPopup
hideAddCategoryPopup={actions.hideAddCategoryPopup}
addCategory={actions.addCategory}
parentId={sidebarState.popupItems.addCategory.parentId}
/>
)}
{sidebarState.popupVisible.addClippingsFeed && (
<AddClippingsFeedPopup
parentId={sidebarState.popupItems.addClippingsFeed.parentId}
hidePopup={actions.hideAddClippingsFeedPopup}
addClippingsFeed={actions.addClippingsFeed}
addAlert={actions.addAlert}
categories={sidebarState.categories}
/>
)}
</div>
</div>
</PerfectScrollbar>
<div
className={cx('app-sidebar-bg', backgroundImageOpacity)}
style={{
backgroundImage: enableBackgroundImage
? 'url(' + backgroundImage + ')'
: null
}}
></div>
</CSSTransitionGroup>
</Fragment>
)
}
}
export default React.memo(Sidebar)
@@ -0,0 +1,154 @@
import React from 'react'
import PropTypes from 'prop-types'
import $ from 'jquery'
import { translate } from 'react-i18next'
export class SidebarDropdown extends React.Component {
static propTypes = {
itemName: PropTypes.string.isRequired,
itemSubType: PropTypes.string.isRequired,
itemType: PropTypes.string.isRequired,
itemId: PropTypes.number.isRequired,
itemExported: PropTypes.bool,
parentId: PropTypes.number.isRequired,
parentAttrId: PropTypes.string.isRequired,
showDeletePopup: PropTypes.func.isRequired,
showRenamePopup: PropTypes.func.isRequired,
showAddCategoryPopup: PropTypes.func,
showAddClippingsPopup: PropTypes.func,
toggleExportFeed: PropTypes.func,
toggleExportCategory: PropTypes.func,
t: PropTypes.func.isRequired,
hideDropDown: PropTypes.func.isRequired
};
constructor (props) {
super(props)
this.state = {
dropdownTopPos: 'auto',
dropdownBottomPos: 'auto',
dropdownOpacity: 0
}
}
componentDidMount = () => {
const topPos = $('#' + this.props.parentAttrId).offset().top - $(document).scrollTop()
const dropdownHeight = $('#sidebar-category-dropdown').height()
if ($(window).height() - topPos >= dropdownHeight) {
this.setState({
dropdownTopPos: topPos,
dropdownOpacity: 1
})
} else {
this.setState({
dropdownBottomPos: 5,
dropdownOpacity: 1
})
}
};
onExportToggle = () => {
const {itemId, toggleExportFeed, itemExported, hideDropDown} = this.props
toggleExportFeed(itemId, !itemExported)
hideDropDown()
};
onExportCategoryToggle = () => {
const {itemId, toggleExportCategory, itemExported, hideDropDown} = this.props
toggleExportCategory(itemId, !itemExported)
hideDropDown()
};
onDelete = () => {
this.props.showDeletePopup(this.props.itemId, this.props.itemType, this.props.itemName, this.props.parentId)
};
onRename = () => {
this.props.showRenamePopup(this.props.itemId, this.props.itemType, this.props.itemName, this.props.parentId)
};
onAddCategory = () => {
// set this item id as parent of new category
this.props.showAddCategoryPopup(this.props.itemId)
};
onAddClippingsFeedPopup = () => {
this.props.showAddClippingsPopup(this.props.itemId)
};
render () {
const { itemSubType, t, itemExported } = this.props
let dropdown
switch (itemSubType) {
case 'my_content':
dropdown = <ul id="sidebar-category-dropdown" className="sidebar-category__dropdown" style={{top: this.state.dropdownTopPos, bottom: this.state.dropdownBottomPos, opacity: this.state.dropdownOpacity}}>
<li><a href="#" onClick={this.onAddClippingsFeedPopup}>{t('sidebarDropdown.AddClippingsFeed')}</a></li>
<li><a href="#" onClick={this.onAddCategory}>{t('sidebarDropdown.AddFolder')}</a></li>
{/*<li><a href="#">{t('sidebarDropdown.DownloadSearchCriteria')}</a></li>
<li><a href="#">{t('sidebarDropdown.EditSearchTemplate')}</a></li>*/}
<li><a href="#" onClick={this.onExportCategoryToggle}>{t(itemExported ? 'sidebarDropdown.UnexportFeeds' : 'sidebarDropdown.ExportFeeds')}</a></li>
{/*<li><a href="#">{t('sidebarDropdown.ViewUserComments')}</a></li>*/}
</ul>
break
case 'shared_content':
dropdown = <ul id="sidebar-category-dropdown" className="sidebar-category__dropdown" style={{top: this.state.dropdownTopPos, bottom: this.state.dropdownBottomPos, opacity: this.state.dropdownOpacity}}>
<li><a href="#" onClick={this.onAddClippingsFeedPopup}>{t('sidebarDropdown.AddClippingsFeed')}</a></li>
<li><a href="#" onClick={this.onAddCategory}>{t('sidebarDropdown.AddFolder')}</a></li>
{/*<li><a href="#">{t('sidebarDropdown.DownloadSearchCriteria')}</a></li>*/}
<li><a href="#" onClick={this.onExportCategoryToggle}>{t(itemExported ? 'sidebarDropdown.UnexportFeeds' : 'sidebarDropdown.ExportFeeds')}</a></li>
{/*<li><a href="#">{t('sidebarDropdown.ViewUserComments')}</a></li>*/}
</ul>
break
case 'custom':
dropdown = <ul id="sidebar-category-dropdown" className="sidebar-category__dropdown" style={{top: this.state.dropdownTopPos, bottom: this.state.dropdownBottomPos, opacity: this.state.dropdownOpacity}}>
<li><a href="#" onClick={this.onAddClippingsFeedPopup}>{t('sidebarDropdown.AddClippingsFeed')}</a></li>
<li><a href="#" onClick={this.onAddCategory}>{t('sidebarDropdown.AddFolder')}</a></li>
{/*<li><a href="#">{t('sidebarDropdown.DownloadSearchCriteria')}</a></li>*/}
<li><a href="#" onClick={this.onExportCategoryToggle}>{t(itemExported ? 'sidebarDropdown.UnexportFeeds' : 'sidebarDropdown.ExportFeeds')}</a></li>
<li><a href="#" onClick={this.onRename}>{t('sidebarDropdown.RenameFolder')}</a></li>
{/*<li><a href="#">{t('sidebarDropdown.ViewUserComments')}</a></li>*/}
<li><a href="#" onClick={this.onDelete}>{t('sidebarDropdown.DeleteFolder')}</a></li>
</ul>
break
case 'query_feed':
dropdown = <ul id="sidebar-category-dropdown" className="sidebar-category__dropdown" style={{top: this.state.dropdownTopPos, bottom: this.state.dropdownBottomPos, opacity: this.state.dropdownOpacity}}>
{/*<li><a href="#">{t('sidebarDropdown.AddArticle')}</a></li>
<li><a href="#">{t('sidebarDropdown.AddToDashboard')}</a></li>
<li><a href="#">{t('sidebarDropdown.AnalyzeFeed')}</a></li>
<li><a href="#">{t('sidebarDropdown.DownloadArticleData')}</a></li>
<li><a href="#">{t('sidebarDropdown.DownloadFeedStatistics')}</a></li>
<li><a href="#">{t('sidebarDropdown.DownloadSearchCriteria')}</a></li>*/}
<li><a href="#" onClick={this.onExportToggle}>{t(itemExported ? 'sidebarDropdown.UnexportFeed' : 'sidebarDropdown.ExportFeed')}</a></li>
<li><a href="#" onClick={this.onRename}>{t('sidebarDropdown.RenameFeed')}</a></li>
<li><a href="#" onClick={this.onDelete}>{t('sidebarDropdown.DeleteFeed')}</a></li>
</ul>
break
case 'clip_feed':
dropdown = <ul id="sidebar-category-dropdown" className="sidebar-category__dropdown" style={{top: this.state.dropdownTopPos, bottom: this.state.dropdownBottomPos, opacity: this.state.dropdownOpacity}}>
{/*<li><a href="#">{t('sidebarDropdown.AddArticle')}</a></li>
<li><a href="#">{t('sidebarDropdown.AddToDashboard')}</a></li>
<li><a href="#">{t('sidebarDropdown.AnalyzeFeed')}</a></li>
<li><a href="#">{t('sidebarDropdown.DownloadArticleData')}</a></li>
<li><a href="#">{t('sidebarDropdown.DownloadFeedStatistics')}</a></li>
<li><a href="#">{t('sidebarDropdown.DownloadSearchCriteria')}</a></li>*/}
<li><a href="#" onClick={this.onExportToggle}>{t(itemExported ? 'sidebarDropdown.UnexportFeed' : 'sidebarDropdown.ExportFeed')}</a></li>
<li><a href="#" onClick={this.onRename}>{t('sidebarDropdown.RenameFeed')}</a></li>
<li><a href="#" onClick={this.onDelete}>{t('sidebarDropdown.DeleteFeed')}</a></li>
</ul>
break
}
return (
dropdown
)
}
}
export default translate(['common'], { wait: true })(SidebarDropdown)
@@ -0,0 +1,56 @@
import React from 'react';
import PropTypes from 'prop-types';
import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup';
import SubTabWrapper from '../../AppHeader/SubTabWrapper';
import { Redirect, Route, Switch, withRouter } from 'react-router-dom';
import ShowCharts from './CreateAnalysisSubTab/ShowCharts';
import SavedAnalysisSubTab from './SavedAnalysisSubTab/SavedAnalysisSubTab';
import CreateAnalysisSubTab from './CreateAnalysisSubTab/CreateAnalysisSubTab';
function AnalyzeTab(props) {
const { subTabs, allowAnalytics, history, activeTabName, match } = props;
if (!allowAnalytics) {
history.push('/app/search/search');
return null;
}
return (
<CSSTransitionGroup
component="div"
transitionName="TabsAnimation"
transitionAppear
transitionAppearTimeout={0}
transitionEnter={false}
transitionLeave={false}
>
<SubTabWrapper activeTabName={activeTabName} subTabs={subTabs}>
<Switch>
{/* <Route path={`${match.url}/welcome`} component={WelcomeSubTab} /> */}
<Route path={`${match.url}/saved`} component={SavedAnalysisSubTab} />
<Route
path={`${match.url}/create`}
component={CreateAnalysisSubTab}
/>
<Route
path={`${match.url}/edit/:id`}
component={CreateAnalysisSubTab}
/>
<Route path={`${match.url}/:id`} component={ShowCharts} />
<Redirect to={`${match.url}/saved`} />
</Switch>
</SubTabWrapper>
</CSSTransitionGroup>
);
}
AnalyzeTab.propTypes = {
activeTabName: PropTypes.string,
children: PropTypes.any,
history: PropTypes.object,
match: PropTypes.object,
allowAnalytics: PropTypes.bool,
subTabs: PropTypes.array
};
export default withRouter(AnalyzeTab);
@@ -0,0 +1,293 @@
import React, { Fragment, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import {
Button,
Form,
FormGroup,
Label,
Modal,
ModalBody,
ModalFooter,
ModalHeader
} from 'reactstrap';
import Select from 'react-select';
import { Input, Checkbox, RadioButton } from '../../../../common/FormControls';
import useForm from '../../../../common/hooks/useForm.js';
import { EXTRAS } from '../../../../../redux/modules/appState/share/forms/alertForm';
import { createAlertAPI } from '../../../../../api/analytics/createAnalytics';
import { getCurrentTimezone, timezones } from '../../../../../common/Timezones';
import { compose } from 'redux';
import reduxConnect from '../../../../../redux/utils/connect';
import translate from 'react-i18next/dist/commonjs/translate';
import { THEME_TYPES } from '../../../../../redux/modules/appState/share/forms/notificationForm';
const initialForm = {
name: '',
recipients: [],
subject: '',
automatedSubject: false,
unsubscribeNotification: false,
published: false,
allowUnsubscribe: false,
articleExtracts: EXTRAS.CONTEXTUAL,
highlight: false,
showSourceCountry: false,
showUserComments: false,
themeType: THEME_TYPES.PLAIN,
sendWhenEmpty: false,
timezone: getCurrentTimezone(),
notificationType: 'alert',
// automatic: [], // auto schedule
// sentUntil: '',
errors: {
name: null
}
};
function AlertDialog(props) {
const { toggle, isOpen, alertCharts, actions, resetAlertChart, user } = props;
const [loading, setLoading] = useState(false);
const {
form,
handleChange,
handleValidation,
errors,
validateSubmit,
resetForm
} = useForm(initialForm);
function handleSubmit() {
const obj = validateSubmit();
if (!obj) {
return actions.addAlert({ type: 'error', transKey: 'requiredInfo' });
}
setLoading(true);
if (obj.automatedSubject) {
delete obj.subject;
}
obj.sources = alertCharts.map((chart) => ({
id: chart.id,
type: 'chart'
}));
createAlertAPI(obj).then((res) => {
if (res.error) {
res.data
? actions.addAlert(res.data)
: actions.addAlert({ type: 'error', transKey: 'somethingWrong' });
setLoading(false);
return;
}
actions.addAlert({ type: 'notice', transKey: 'alertSaved' });
setLoading(false);
toggle();
resetForm();
resetAlertChart();
});
}
useEffect(() => {
if (form.recipients && user.recipient && user.recipient.id) {
handleChange('recipients', [user.recipient.id]);
}
return () => resetForm();
}, []);
return (
<Modal isOpen={isOpen} toggle={toggle} backdrop="static" size="lg">
<ModalHeader toggle={toggle}>Create Alert</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<Label>Selected Charts</Label>
<div className="b-radius-5 bg-light p-2">
{alertCharts.map((chart, i, arr) => (
<Fragment key={chart.name}>
<span className="d-inline-block mr-1">
{chart.name}
{arr.length - 1 !== i ? ', ' : ''}
</span>
</Fragment>
))}
</div>
</FormGroup>
<Input
name="name"
title="Name"
required
value={form.name}
error={errors.name}
handleChange={handleChange}
handleValidation={handleValidation}
/>
<Checkbox
name="automatedSubject"
title="Automated Subject"
description="Use automated email subject based on the feeds"
value={form.automatedSubject}
error={errors.automatedSubject}
handleChange={handleChange}
/>
{!form.automatedSubject && (
<Input
name="subject"
title="Email Subject"
value={form.subject}
error={errors.subject}
handleChange={handleChange}
handleValidation={handleValidation}
/>
)}
<Checkbox
name="published"
title="Publish"
description="Alerts and Newsletters that are Published are available for other users to subscribe"
value={form.published}
error={errors.publish}
handleChange={handleChange}
/>
<Checkbox
name="allowUnsubscribe"
title="Unsubscribe Link"
description="Allow recipients to unsubscribe from Alert"
value={form.allowUnsubscribe}
error={errors.allowUnsubscribe}
handleChange={handleChange}
/>
<Checkbox
name="unsubscribeNotification"
title="Notifications"
description="Notify creator when recipients unsubscribe"
value={form.unsubscribeNotification}
error={errors.unsubscribeNotification}
handleChange={handleChange}
/>
<FormGroup className="radio-options">
<Label>Options</Label>
<RadioButton
name="articleExtracts"
title="Article Extracts"
formClass="mb-0"
options={[
{ label: 'Contextual extract', value: EXTRAS.CONTEXTUAL },
{ label: 'Start of text extract', value: EXTRAS.START },
{ label: 'No article extract', value: EXTRAS.NO }
]}
inline
value={form.articleExtracts}
error={errors.articleExtracts}
handleChange={handleChange}
/>
<RadioButton
name="highlight"
title="Highlight Keywords"
formClass="mb-0"
options={[
{ label: 'Yes', value: true },
{ label: 'No', value: false }
]}
inline
value={form.highlight}
error={errors.highlight}
handleChange={handleChange}
/>
<RadioButton
name="showSourceCountry"
title="Show Source Country"
formClass="mb-0"
options={[
{ label: 'Yes', value: true },
{ label: 'No', value: false }
]}
inline
value={form.showSourceCountry}
error={errors.showSourceCountry}
handleChange={handleChange}
/>
<RadioButton
name="showUserComments"
title="Show User Comments"
formClass="mb-0"
options={[
{ label: 'Yes', value: true },
{ label: 'No', value: false }
]}
inline
value={form.showUserComments}
error={errors.showUserComments}
handleChange={handleChange}
/>
<RadioButton
name="themeType"
title="Layout"
formClass="mb-0"
options={[
{ label: 'Enhanced HTML', value: THEME_TYPES.ENHANCED },
{ label: 'Plain HTML', value: THEME_TYPES.PLAIN }
]}
inline
value={form.themeType}
error={errors.themeType}
handleChange={handleChange}
/>
<RadioButton
name="sendWhenEmpty"
title="Send When Empty"
formClass="mb-0"
options={[
{ label: 'Yes', value: true },
{ label: 'No', value: false }
]}
inline
value={form.sendWhenEmpty}
error={errors.sendWhenEmpty}
handleChange={handleChange}
/>
</FormGroup>
<FormGroup>
<Label>Timezone</Label>
<Select
className="timezone-select"
value={form.timezone}
options={timezones}
clearable={false}
onChange={function (v) {
handleChange('timezone', v.value);
}}
/>
</FormGroup>
{/* <FormGroup>
<Label>Automatic</Label>
<Scheduling state={state.scheduling} actions={actions} />
</FormGroup> */}
</Form>
</ModalBody>
<ModalFooter>
<Button color="link" onClick={toggle}>
Cancel
</Button>
<Button color="primary" disabled={loading} onClick={handleSubmit}>
{loading ? 'Loading...' : 'Submit'}
</Button>
</ModalFooter>
</Modal>
);
}
AlertDialog.propTypes = {
toggle: PropTypes.func,
resetAlertChart: PropTypes.func,
isOpen: PropTypes.bool,
alertCharts: PropTypes.array,
user: PropTypes.object,
actions: PropTypes.object
};
const applyDecorators = compose(
reduxConnect('user', ['common', 'auth', 'user']),
translate(['tabsContent'], { wait: true })
);
export default applyDecorators(AlertDialog);
@@ -0,0 +1,83 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
Card,
CardBody,
CardHeader,
DropdownItem,
DropdownMenu,
DropdownToggle,
UncontrolledButtonDropdown
} from 'reactstrap';
import cx from 'classnames';
import { IoIosMenu } from 'react-icons/io';
function ChartWrapper(props) {
let { title, children, menus } = props;
const hasShowMore = menus.find((menu) => !menu.hide && menu.showInMore);
// TODO: hide alert until API is ready
menus = menus.filter((menu) => menu.title);
const isRTL = document.documentElement.dir === 'rtl';
return (
<Card className="mb-3">
<CardHeader>
{title && <div>{title}</div>}
<div className="btn-actions-pane-right actions-icon-btn">
<div className="align-content-center d-flex d-inline-flex">
{menus &&
menus.map((menu) =>
!menu.hide && !menu.showInMore && menu.icon ? (
<button
key={menu.title}
title={menu.title}
className="btn btn-icon-only mr-2 p-0"
onClick={menu.fn}
disabled={!menu.fn}
>
<menu.icon size={menu.size || 16} />
</button>
) : null
)}
</div>
{menus && hasShowMore && (
<UncontrolledButtonDropdown>
<DropdownToggle className="btn-icon btn-icon-only" color="link">
<div className="btn-icon-wrapper">
<IoIosMenu size={24} />
</div>
</DropdownToggle>
<DropdownMenu
className={`dropdown-menu-shadow dropdown-menu-hover-link${
isRTL ? ' dropdown-menu-left' : ''
}`}
>
{menus.map((menu) =>
!menu.hide && menu.showInMore ? (
<DropdownItem onClick={menu.fn} key={menu.title}>
{menu.icon && (
<i className={cx('dropdown-icon', menu.icon)}></i>
)}
<span>{menu.title}</span>
</DropdownItem>
) : null
)}
</DropdownMenu>
</UncontrolledButtonDropdown>
)}
</div>
</CardHeader>
<CardBody>{children}</CardBody>
</Card>
);
}
ChartWrapper.propTypes = {
title: PropTypes.string,
children: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
menus: PropTypes.array
};
export default ChartWrapper;
@@ -0,0 +1,272 @@
/* eslint-disable react/jsx-no-bind */
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { DateRangePicker } from 'react-dates';
import { translate } from 'react-i18next';
import { compose } from 'redux';
import { useHistory, useParams } from 'react-router-dom';
import {
Button,
Card,
CardBody,
CardTitle,
Col,
FormGroup,
InputGroup,
Label,
Row
} from 'reactstrap';
import Loader from 'react-loader-advanced';
import { Loader as LoaderAnim } from 'react-loaders';
import { useDrop } from 'react-dnd';
import { IoIosCloseCircleOutline } from 'react-icons/io';
import {
addEditAnalyticsAPI,
getAnalyticDetailsAPI
} from '../../../../../api/analytics/createAnalytics';
import { TYPES } from '../../../../../redux/modules/appState/sidebar';
import reduxConnect from '../../../../../redux/utils/connect';
import useIsMounted from '../../../../common/hooks/useIsMounted';
import { subChartCategories } from './ShowCharts';
import { getMomentObject, setDocumentData } from '../../../../../common/helper';
const initialState = {
feeds: [],
startDate: null,
endDate: null
};
const spinner = <LoaderAnim color="#ffffff" type="ball-pulse" />;
function CreateAnalysisSubTab({ t, actions }) {
const isMounted = useIsMounted();
const history = useHistory();
const { id } = useParams();
const [form, setForm] = useState(initialState);
const [error, setError] = useState();
const [loading, setLoading] = useState(false);
const [fetching, setFetching] = useState(!!id);
const [focusedInput, setFocusedInput] = useState();
const [{ canDrop, isOver }, drop] = useDrop({
accept: [TYPES.FEED, TYPES.CLIP_ARTICLE],
drop: droppedFeeds,
canDrop: canDroppable,
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop()
})
});
function getAnalyticData() {
setFetching(true);
getAnalyticDetailsAPI(id).then((res) => {
if (!isMounted.current) {
return;
}
if (res.error || !res.data || !res.data.context) {
setFetching(false);
res.data
? actions.addAlert(res.data)
: actions.addAlert({ type: 'error', transKey: 'somethingWrong' });
history.push('/app/analyze/saved');
return;
}
const { context } = res.data;
const date = context && context.rawFilters && context.rawFilters.date;
setForm({
feeds: context.feeds.map((item) => ({
feed: { name: item.name },
id: item.id
})),
startDate: getMomentObject(date && date.start),
endDate: getMomentObject(date && date.end)
});
setFetching(false);
});
}
useEffect(() => {
setDocumentData('title', `${id ? 'Update' : 'Create'} Analysis | Analyze`);
return () => {
setDocumentData('title');
};
}, []);
useEffect(() => {
if (id) {
getAnalyticData();
}
}, [id]);
function canDroppable(item) {
if (form.feeds.find((val) => val.id === item.id)) {
return false;
}
return true;
}
function droppedFeeds(item) {
if (form.feeds.find((val) => val.id === item.id)) {
return;
}
setForm((prev) => ({ ...prev, feeds: [...prev.feeds, item] }));
}
function removeFeeds(id) {
setForm((prev) => {
const modifiedFeeds = form.feeds.filter((val) => val.id !== id);
return { ...prev, feeds: modifiedFeeds };
});
}
const isActive = canDrop && isOver;
function handleSubmit() {
const isValid = Object.values(form).every((value) =>
value ? (Array.isArray(value) ? value.length > 0 : true) : false
);
if (!isValid) {
return setError(t('common:alerts.error.requiredInfo'));
}
setError(false);
setLoading(true);
addEditAnalyticsAPI(form, id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.id) {
// on error
setLoading(false);
setError(res.errorMessage);
return;
}
actions.resetAlertChart();
setLoading(false);
history.push(`/app/analyze/${res.data.id}/${subChartCategories[0].path}`);
});
}
function handleDateChange({ startDate, endDate }) {
setForm((prev) => ({ ...prev, startDate, endDate }));
}
function onFocusChange(focus) {
setFocusedInput(focus);
}
function isOutsideRange() {
return false;
}
const isRTL = document.documentElement.dir === 'rtl';
return (
<Card className="mb-3">
<Loader message={spinner} show={fetching}>
<CardBody>
<CardTitle>
{id ? t('analyzeTab.updateDetails') : t('analyzeTab.enterDetails')}
</CardTitle>
<Row>
<Col sm="12">
<FormGroup data-tour="drop-feeds-box">
<div>
{form.feeds.length > 0 && (
<div className="mb-3">
<Label>{t('analyzeTab.selectedFeeds')}</Label>
<div>
{form.feeds.map((item) => (
<div
key={item.id}
className="bg-light d-inline d-inline-flex align-items-center mr-2 p-2 text-dark"
>
<p>{item.feed.name}</p>
<button
className="btn p-0"
onClick={function () {
removeFeeds(item.id);
}}
>
<IoIosCloseCircleOutline
size={22}
className="text-danger ml-2"
/>
</button>
</div>
))}
</div>
</div>
)}
<Label>{t('analyzeTab.selectFeeds')}</Label>
<div ref={drop} className="dropzone-wrapper">
<div>
<div className="dropzone-content">
<p>
{isActive
? t('analyzeTab.releaseDesc')
: t('analyzeTab.dropDesc')}
</p>
</div>
</div>
</div>
</div>
</FormGroup>
<FormGroup data-tour="analytics-data-range">
<Label className="mr-sm-2">{t('analyzeTab.dateRange')}</Label>
<InputGroup>
<DateRangePicker
startDateId="startDate"
endDateId="endDate"
startDate={form.startDate}
endDate={form.endDate}
onDatesChange={handleDateChange}
focusedInput={focusedInput}
onFocusChange={onFocusChange}
displayFormat="MM/DD/YYYY"
startDatePlaceholderText={t('analyzeTab.startDatePlaceholder')}
endDatePlaceholderText={t('analyzeTab.endDatePlaceholder')}
numberOfMonths={1}
isOutsideRange={isOutsideRange}
isRTL={isRTL}
/>
</InputGroup>
</FormGroup>
{error && <div className="text-danger mb-2">{error}</div>}
<Button
className="mb-2 mr-2 btn-icon"
color="primary"
disabled={loading}
data-tour="create-analytics-button"
onClick={handleSubmit}
>
{loading
? 'Loading...'
: id
? t('analyzeTab.updateBtn')
: t('analyzeTab.createBtn')}
</Button>
</Col>
</Row>
</CardBody>
</Loader>
</Card>
);
}
CreateAnalysisSubTab.propTypes = {
t: PropTypes.func.isRequired,
actions: PropTypes.object
};
const applyDecorators = compose(
reduxConnect(),
translate(['tabsContent'], { wait: true })
);
export default applyDecorators(CreateAnalysisSubTab);
@@ -0,0 +1,276 @@
import React, {
useState,
useCallback,
Fragment,
useEffect,
useMemo
} from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import {
NavLink,
Redirect,
Route,
Switch,
useHistory,
useParams
} from 'react-router-dom';
import {
Button,
DropdownItem,
DropdownMenu,
DropdownToggle,
UncontrolledDropdown
} from 'reactstrap';
import { IoIosTrash } from 'react-icons/io';
import {
Results,
Performance,
Influencers,
Sentiment,
Themes,
Demographics
// WorldMap
} from './Tabs';
import AlertDialog from './AlertDialog';
import reduxConnect from '../../../../../redux/utils/connect';
import translate from 'react-i18next/dist/commonjs/translate';
import { compose } from 'redux';
import { getAnalyticDetailsAPI } from '../../../../../api/analytics/createAnalytics';
import useIsMounted from '../../../../common/hooks/useIsMounted';
import { setDocumentData } from '../../../../../common/helper';
import { Interpolate } from 'react-i18next';
// exported for routing
export const subChartCategories = [
{
title: 'Overview',
transKey: 'overview',
path: 'overview',
component: Results
},
{
title: 'Performance',
transKey: 'performance',
path: 'performance',
component: Performance
},
{
title: 'Influencers',
transKey: 'influencers',
path: 'influencers',
component: Influencers
},
{
title: 'Sentiment',
transKey: 'sentiment',
path: 'sentiment',
component: Sentiment
},
{ title: 'Themes', transKey: 'themes', path: 'themes', component: Themes },
{
title: 'Demographics',
transKey: 'demographics',
path: 'demographics',
component: Demographics
}
// { title: 'World Map', transKey: 'worldMap', path: 'worldmap', component: WorldMap }
];
function ShowCharts({ analyze, actions, t }) {
const isMounted = useIsMounted();
const history = useHistory();
const params = useParams();
const [chartData, setChartData] = useState({});
const [alertModal, setAlertModal] = useState(false);
const [fetching, setFetching] = useState(true);
const [feedData, setFeedData] = useState(null);
const { removeAlertChart, resetAlertChart } = actions;
const { alertCharts } = analyze;
useEffect(() => {
setDocumentData('title', 'View Analysis | Analyze');
return () => {
setDocumentData('title');
};
}, []);
useEffect(() => {
if (!params.id || isNaN(params.id)) {
history.push('/app/analyze/saved');
} else {
getAnalyticData();
}
return () => resetAlertChart(); // reset store
}, [params.id]);
const updateResult = useCallback((data, chartName) => {
setChartData((prev) => ({ ...prev, [chartName]: data }));
}, []);
const subChartRoutes = useMemo(() => {
return subChartCategories.map(({ path, component: SubChart }) => (
<Route exact key={path} path={`/app/analyze/${params.id}/${path}`}>
<SubChart
id={params.id}
feedData={feedData}
chartData={chartData}
updateResult={updateResult}
/>
</Route>
));
}, [updateResult, chartData, feedData, params.id]);
function getAnalyticData() {
setFetching(true);
getAnalyticDetailsAPI(params.id).then((res) => {
if (!isMounted.current) {
return;
}
if (res.error || !res.data || !res.data.context) {
setFetching(false);
res.data
? actions.addAlert(res.data)
: actions.addAlert({ type: 'error', transKey: 'somethingWrong' });
history.push('/app/analyze/saved');
return;
}
const { context } = res.data;
const date = context && context.rawFilters && context.rawFilters.date;
setFeedData({
feeds: context.feeds.map((item) => ({
feed: item.name,
id: item.id
})),
startDate: date && date.start,
endDate: date && date.end
});
setFetching(false);
});
}
function toggleModal() {
setAlertModal((prev) => !prev);
}
if (fetching) {
return 'Loading...';
}
const isRTL = document.documentElement.dir === 'rtl';
return (
<Fragment>
<div
className="d-flex"
style={{ position: 'absolute', top: 0, right: 0 }}
>
{alertCharts && alertCharts.length > 0 && (
<UncontrolledDropdown className="d-inline-block">
<DropdownToggle color="info" className="btn-shadow" caret>
<Interpolate
t={t}
i18nKey="analyzeTab.createAlert"
alertsLength={alertCharts.length}
/>
</DropdownToggle>
<DropdownMenu
className={`dropdown-menu-right rm-pointers dropdown-menu-shadow dropdown-menu-hover-link${
isRTL ? ' dropdown-menu-left' : ''
}`}
>
<DropdownItem header>
{t('analyzeTab.selectedCharts')}
</DropdownItem>
{alertCharts.map((chart, i) => (
<div className="dropdown-item" key={`${chart.name}_${i}}`}>
<span>
{chart.name}
{isNaN(chart.id) ? '' : ` (#${chart.id})`}
</span>
<Button
className="btn-icon btn-icon-only ml-auto mr-2 p-1"
color="danger"
onClick={function () {
removeAlertChart({ name: chart.name, id: chart.id });
}}
>
<IoIosTrash fontSize="1rem" className="ml-auto" />
</Button>
</div>
))}
<DropdownItem divider />
<div className="p-2 pr-3 text-right">
<Button
className="btn-shadow btn-sm"
color="primary"
onClick={toggleModal}
>
{t('analyzeTab.createAlertBtn')}
</Button>
</div>
</DropdownMenu>
</UncontrolledDropdown>
)}
{/*
<Button
className="btn-icon ml-2"
color="info"
// change style for mobile view
>
<IoIosSave className="btn-icon-wrapper" />
Save
</Button> */}
</div>
<div className="btn-actions-pane-right mask-line overflow-auto mb-3 pl-3">
{subChartCategories.map((cat, i, arr) => (
<Button
key={cat.title}
title={cat.title}
tag={NavLink}
to={`/app/analyze/${params.id}/${cat.path}`}
size="sm"
outline
color="primary"
className={cx('btn-pill btn-wide', {
'mr-1 ml-1': i !== 0 && i !== arr.length - 1
})}
activeClassName="active"
>
{t(`analyzeTab.overviewCharts.${cat.transKey}`)}
</Button>
))}
</div>
<AlertDialog
isOpen={alertModal}
toggle={toggleModal}
alertCharts={alertCharts}
resetAlertChart={resetAlertChart}
/>
<Switch>
{subChartRoutes}
<Redirect
to={`/app/analyze/${params.id}/${subChartCategories[0].path}`}
/>
</Switch>
</Fragment>
);
}
ShowCharts.propTypes = {
t: PropTypes.func.isRequired,
analyze: PropTypes.object,
actions: PropTypes.object
};
const applyDecorators = compose(
reduxConnect('analyze', ['appState', 'analyze']),
translate(['tabsContent'], { wait: true })
);
export default applyDecorators(ShowCharts);
@@ -0,0 +1,357 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Col, Row } from 'reactstrap';
import ECharts from '../../../../../common/charts/ECharts';
import ChartWrapper from '../ChartWrapper';
import {
getBarOptions,
getPieOptions
} from '../../../../../common/charts/ChartsOptions';
import { IoIosAdd, IoIosRefresh, IoIosCheckmark } from 'react-icons/io';
import reduxConnect from '../../../../../../redux/utils/connect';
import translate from 'react-i18next/dist/commonjs/translate';
import { compose } from 'redux';
import { getOverviewPieAPI } from '../../../../../../api/analytics/createAnalytics';
import useIsMounted from '../../../../../common/hooks/useIsMounted';
const initialBar = {
data: [],
error: undefined,
loading: true,
vertical: false
};
const initialPie = { data: [], error: undefined, loading: true };
function Demographics(props) {
const { actions, analyze, feedData, id, t } = props;
const isMounted = useIsMounted();
const [barCountriesData, setBarCountriesData] = useState(initialBar);
const [barLanguagesData, setBarLanguagesData] = useState(initialBar);
const [genderData, setGenderData] = useState(initialPie);
useEffect(() => {
if (!id) {
return;
}
// getCountriesData()
getLanguagesData();
getGenderData();
}, []);
useEffect(() => {
if (barCountriesData.data) {
setBarCountriesData((prev) => ({
...prev,
data: {
...prev.data,
xAxis: prev.data.yAxis,
yAxis: prev.data.xAxis
}
}));
}
}, [barCountriesData.vertical]);
useEffect(() => {
if (barLanguagesData.data) {
setBarLanguagesData((prev) => ({
...prev,
data: {
...prev.data,
xAxis: prev.data.yAxis,
yAxis: prev.data.xAxis
}
}));
}
}, [barLanguagesData.vertical]);
function updateResult(foo, id) {
switch (id) {
case cn.first:
// getCountriesData()
return;
case cn.second:
getLanguagesData();
return;
case cn.third:
getGenderData();
return;
default:
return;
}
}
/* Uncomment when country chart shows up
function getCountriesData() {
setBarCountriesData((prev) => ({ ...prev, loading: true }))
getOverviewPieAPI('country', id).then((res) => {
if (!isMounted.current) {
return false
}
if (res.error || !res.data.data) {
// on error
setBarCountriesData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}))
return
}
const { data } = res.data
const barOptions = {}
const errors = {}
Object.entries(data).forEach((feed) => {
const [name, value] = feed
const labels = ['Results']
const datasets = Object.keys(value).map((item) => ({
name: item,
type: 'bar',
data: [value[item]]
}))
if (!datasets || (Array.isArray(datasets) && datasets.length < 1)) {
errors[name] = t('analyzeTab.noData');
}
barOptions[name] = getBarOptions(datasets, labels)
})
setBarCountriesData({
data: barOptions,
error: errors,
loading: false,
vertical: false
})
})
} */
function getLanguagesData() {
setBarLanguagesData((prev) => ({ ...prev, loading: true }));
getOverviewPieAPI('language', id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// on error
setBarLanguagesData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const barOptions = {};
const errors = {};
Object.entries(data).forEach((feed) => {
const [name, value] = feed;
const labels = ['Results'];
const datasets = Object.keys(value).map((item) => ({
name: item,
type: 'bar',
data: [value[item]]
}));
if (!datasets || (Array.isArray(datasets) && datasets.length < 1)) {
errors[name] = t('analyzeTab.noData');
}
barOptions[name] = getBarOptions(datasets, labels);
});
setBarLanguagesData({
data: barOptions,
error: errors,
loading: false,
vertical: false
});
});
}
function getGenderData() {
setGenderData((prev) => ({ ...prev, loading: true }));
getOverviewPieAPI('gender', id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// on error
setGenderData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const pieOptions = {};
const errors = {};
Object.entries(data).forEach((feed) => {
const [name, value] = feed;
if (!value || (Array.isArray(value) && value.length < 1)) {
errors[name] = t('analyzeTab.noData');
}
pieOptions[name] = getPieOptions(
Object.entries(value).map((v) => ({
name: v[0],
value: v[1]
}))
);
});
setGenderData({
data: pieOptions,
error: errors,
loading: false
});
});
}
function changeVertical(name, id) {
name === cn.first
? setBarCountriesData((prev) => ({ ...prev, vertical: !prev.vertical }))
: setBarLanguagesData((prev) => ({ ...prev, vertical: !prev.vertical }));
}
const hideChartAlert = (name, id) =>
analyze.alertCharts.find((v) => v.name === name && v.id === id);
const hideChartPieAlert = (id) =>
analyze.alertCharts.find((v) => v.name === cn.third && v.id === id);
const barchartMenus = (name, id) => [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: name, id }),
showInMore: false,
hide: hideChartAlert(name, id)
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChartAlert(name, id)
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, name),
showInMore: false
},
/* {
title: t('analyzeTab.chartMenus.addToDashboard'),
fn: () => {},
showInMore: true
}, */
{
title: t('analyzeTab.chartMenus.toggleHV'),
fn: () => changeVertical(name, id),
showInMore: true
}
];
const piechartMenus = (id) => [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.third, id }),
showInMore: false,
hide: hideChartPieAlert(id)
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChartPieAlert(id)
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.third),
showInMore: false
}
// { title: t('analyzeTab.chartMenus.addToDashboard'), fn: () => {}, showInMore: true }
];
return (
<Row>
{/* {feedData.feeds.map((feed) => (
<Col key={feed.id} md="6">
<ChartWrapper
title={`${t('analyzeTab.charts.topLanguages')} (${feed.feed})`}
menus={barchartMenus(cn.first, feed.id)}
>
<ECharts
xLabel={barCountriesData.labels}
loading={barCountriesData.loading}
options={barCountriesData.data[feed.feed]}
message={
barCountriesData.error && barCountriesData.error[feed.feed]
}
/>
</ChartWrapper>
</Col>
))} */}
{feedData.feeds.map((feed) => (
<Col key={feed.id} md="6">
<ChartWrapper
title={`${t('analyzeTab.charts.topLanguages')} (${feed.feed})`}
menus={barchartMenus(cn.second, feed.id)}
>
<ECharts
xLabel={barLanguagesData.labels}
loading={barLanguagesData.loading}
options={barLanguagesData.data[feed.feed]}
message={
barLanguagesData.error && barLanguagesData.error[feed.feed]
}
/>
</ChartWrapper>
</Col>
))}
{feedData.feeds.map((feed) => (
<Col key={feed.id} md="6">
<ChartWrapper
title={`${t('analyzeTab.charts.gender')} (${feed.feed})`}
menus={piechartMenus(feed.id)}
>
<ECharts
loading={genderData.loading}
options={genderData.data[feed.feed]}
message={genderData.error && genderData.error[feed.feed]}
/>
</ChartWrapper>
</Col>
))}
</Row>
);
}
const cn = {
first: 'Top Countries',
second: 'Top Languages',
third: 'Gender'
};
Demographics.propTypes = {
t: PropTypes.func.isRequired,
chartData: PropTypes.object,
actions: PropTypes.object,
id: PropTypes.string,
feedData: PropTypes.object,
analyze: PropTypes.object
};
const applyDecorators = compose(
reduxConnect('analyze', ['appState', 'analyze']),
translate(['tabsContent'], { wait: true })
);
export default applyDecorators(React.memo(Demographics));
@@ -0,0 +1,290 @@
/* eslint-disable react/prop-types */
import React, {
useState,
useCallback,
Fragment,
useEffect,
useMemo
} from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { compose } from 'redux';
import { Table } from '../../../../../common/Table/Table';
import { getInfluencersAPI } from '../../../../../../api/analytics/createAnalytics';
import { reduxActions } from '../../../../../../redux/utils/connect';
import {
getQueryParams,
removeHttpsUrl,
capOnlyFirstLetter,
getValidHttpUrl
} from '../../../../../../common/helper';
import i18n from '../../../../../../i18n';
function Influencers(props) {
const [dataSource, setDataSource] = useState(null);
const [loading, setLoading] = useState(true);
const [filter] = useState(filtersNames[1].id);
const { t, actions, id, feedData } = props;
useEffect(() => {
if (!id || !dataSource) {
return;
}
getInfluencers(); //called from table
}, [filter]);
const getDetailsColumns = (id) => {
return id === filtersNames[0].id ? sourceDetails : authorDetails;
};
const authorDetails = useMemo(
() => [
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.rank'),
accessor: 'source_hashcode',
Cell: (row) => (
<div style={{ textAlign: 'center' }}>{row.index + 1}</div>
),
minWidth: 52
},
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.influencers'),
accessor: 'influence',
Cell: (row) =>
getValidHttpUrl(row.value) ? (
<a
target="_blank"
rel="nofollow noopener"
href={getValidHttpUrl(row.value)}
>
{row.original && row.original.author_name}
</a>
) : (
removeHttpsUrl(row.value)
),
minWidth: 130
},
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.sourceType'),
accessor: 'source_type',
Cell: (row) => capOnlyFirstLetter(row.value),
minWidth: 102
}
],
[i18n.language]
);
const sourceDetails = useMemo(
() => [
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.rank'),
accessor: 'source_hashcode',
Cell: (row) => (
<div style={{ textAlign: 'center' }}>{row.index + 1}</div>
),
minWidth: 52
},
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.influencers'),
accessor: 'influence',
Cell: (row) =>
getValidHttpUrl(row.value) ? (
<a
target="_blank"
rel="nofollow noopener"
href={getValidHttpUrl(row.value)}
>
{removeHttpsUrl(row.value)}
</a>
) : (
removeHttpsUrl(row.value)
),
minWidth: 130
},
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.sourceType'),
accessor: 'source_type',
Cell: (row) => capOnlyFirstLetter(row.value),
minWidth: 102
}
],
[i18n.language]
);
const sentimentColumns = useMemo(
() => [
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.total'),
// accessor: d => d.nop.total
accessor: 'totalSentiment',
minWidth: 52,
Cell: (row) => (
<div style={{ textAlign: 'center' }}>{row.value || 0}</div>
)
},
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.positive'),
accessor: 'POSITIVE',
minWidth: 78,
Cell: (row) => (
<div style={{ textAlign: 'center' }}>{row.value || 0}</div>
)
},
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.neutral'),
accessor: 'NEUTRAL',
minWidth: 78,
Cell: (row) => (
<div style={{ textAlign: 'center' }}>{row.value || 0}</div>
)
},
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.negative'),
accessor: 'NEGATIVE',
minWidth: 78,
Cell: (row) => (
<div style={{ textAlign: 'center' }}>{row.value || 0}</div>
)
}
],
[i18n.language]
);
const reachColumns = useMemo(
() => [
/* {
Header: i18n.t('tabsContent:analyzeTab.influencerCols.reach'),
accessor: 'reach',
minWidth: 65,
Cell: (row) => <div style={{ textAlign: 'center' }}>{row.value || 0}</div>
}, */
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.engagement'),
accessor: 'engagement',
minWidth: 105,
Cell: (row) => (
<div style={{ textAlign: 'center' }}>{row.value || 0}</div>
)
}
/* {
Header: i18n.t('tabsContent:analyzeTab.influencerCols.engagementPerMention'),
accessor: 'engagement_per_mention',
Cell: (row) => <div style={{ textAlign: 'center' }}>{row.value || 0}</div>
} */
],
[i18n.language]
);
const columnsList = useMemo(
() => [
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.details'),
headerClassName: 'text-center',
columns: getDetailsColumns(filter)
},
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.sentiments'),
headerClassName: 'text-center',
columns: sentimentColumns
},
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.reach'),
headerClassName: 'text-center',
columns: reachColumns
}
],
[filter, i18n.language]
);
const getInfluencers = useCallback(
(page = 0, pageSize = 10) => {
setLoading(true);
const filterParams = getQueryParams({ page, pageSize });
getInfluencersAPI(id, filter, filterParams).then((res) => {
// if (false) {
if (res.error || res.data === null || !res.data.data) {
setLoading(false);
return actions.addAlert({
type: 'error',
transKey: 'somethingWrong'
});
}
const tableData = {};
res.data.data.forEach((v) => {
tableData[v.name] = v.data;
});
setDataSource(tableData);
setLoading(false);
});
},
[id, filter]
);
return (
<Fragment>
{/* <ButtonGroup size="sm" className="mb-3 d-block text-right">
{filtersNames.map((item) => (
<Button
outline
key={item.id}
title={item.name}
color="secondary"
onClick={function () {
setFilter(item.id)
}}
active={filter === item.id}
>
{item.name}
</Button>
))}
</ButtonGroup> */}
{feedData.feeds.map((feed) => {
let tableData = dataSource;
if (!tableData || !tableData[feed.feed]) {
tableData = { [feed.feed]: [] };
// uncomment for pagination
// tableData[feed.feed] = { data: [], totalCount: 0, limit: 0, page: 0 }
}
const { totalCount = 0, limit = 0, page = 0 } = tableData[feed.feed];
return (
<Table
key={feed.id}
t={t}
cardTitle={`${t('analyzeTab.charts.topInfluencers')} (${
feed.feed
})`}
columns={columnsList}
data={tableData[feed.feed]}
totalCount={totalCount}
showTotalCount
limit={limit}
page={page}
isLoading={loading}
onFetchData={getInfluencers}
/>
);
})}
</Fragment>
);
}
const filtersNames = [
{ name: 'Source', id: 0 },
{ name: 'Author', id: 1 }
];
Influencers.propTypes = {
t: PropTypes.func.isRequired,
feedData: PropTypes.object,
id: PropTypes.string,
actions: PropTypes.object
};
const applyDecorators = compose(
translate(['tabsContent'], { wait: true }),
reduxActions()
);
export default applyDecorators(Influencers);
@@ -0,0 +1,722 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Col, Row } from 'reactstrap';
import ECharts from '../../../../../common/charts/ECharts';
import ChartWrapper from '../ChartWrapper';
import {
getBarOptions,
getPieOptions
} from '../../../../../common/charts/ChartsOptions';
import { IoIosAdd, IoIosRefresh, IoIosCheckmark } from 'react-icons/io';
import reduxConnect from '../../../../../../redux/utils/connect';
import translate from 'react-i18next/dist/commonjs/translate';
import { compose } from 'redux';
import {
getEngagementsAPI,
getEngagementsTimeAPI,
getOverviewBarAPI,
getOverviewPieAPI
} from '../../../../../../api/analytics/createAnalytics';
import useIsMounted from '../../../../../common/hooks/useIsMounted';
function Performance(props) {
const { actions, analyze, feedData, id, t } = props;
const isMounted = useIsMounted();
const [barData, setBarData] = useState({
data: [],
error: undefined,
loading: true,
vertical: false
});
const [engBarData, setEngBarData] = useState({
data: [],
error: undefined,
loading: true,
vertical: false
});
const [potentialBarData, setPotentialBarData] = useState({
data: [],
error: undefined,
loading: true,
vertical: false
});
const [sentimentBar, setSentimentBar] = useState({
data: [],
error: undefined,
loading: true
});
const [pieMentions, setpieMentions] = useState({
data: [],
error: undefined,
loading: true
});
const [pieEng, setpieEng] = useState({
data: [],
error: undefined,
loading: true
});
/* const [pieReach, setpieReach] = useState({
data: [],
error: undefined,
loading: true
}); */
useEffect(() => {
// pass filter
if (!id) {
return;
}
getBarChart();
getEngBarChart();
// getPotentialChart()
getSentimentChart();
getpieMentions();
getpieEngg();
// getpieReach()
}, []);
function updateResult(foo, id) {
switch (id) {
case cn.first:
getBarChart();
return;
case cn.second:
getEngBarChart();
return;
case cn.third:
// getPotentialChart() // Uncomment when API has data
return;
case cn.fourth:
getSentimentChart();
return;
case cn.fifth:
getpieMentions();
return;
case cn.sixth:
getpieEngg();
return;
case cn.seventh:
// getpieReach() // Uncomment when API has data
return;
default:
return;
}
}
useEffect(() => {
if (barData.data) {
setBarData((prev) => ({
...prev,
data: {
...prev.data,
xAxis: prev.data.yAxis,
yAxis: prev.data.xAxis
}
}));
}
}, [barData.vertical]);
useEffect(() => {
if (engBarData.data) {
setEngBarData((prev) => ({
...prev,
data: {
...prev.data,
xAxis: prev.data.yAxis,
yAxis: prev.data.xAxis
}
}));
}
}, [engBarData.vertical]);
useEffect(() => {
if (potentialBarData.data) {
setPotentialBarData((prev) => ({
...prev,
data: {
...prev.data,
xAxis: prev.data.yAxis,
yAxis: prev.data.xAxis
}
}));
}
}, [potentialBarData.vertical]);
function getBarChart() {
setBarData((prev) => ({ ...prev, loading: true }));
getOverviewBarAPI('none', id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// on error
setBarData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const labels = Object.keys(data[0].data);
const datasets = data.map((item) => ({
name: item.name,
type: barData.vertical ? 'bar' : 'line',
smooth: true,
data: Object.values(item.data)
}));
const barOptions = getBarOptions(datasets, labels);
setBarData({
data: barOptions,
error: false,
loading: false,
vertical: false
});
});
}
function getEngBarChart() {
setEngBarData((prev) => ({ ...prev, loading: true }));
getEngagementsTimeAPI(id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// on error
setEngBarData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const labels = Object.keys(data[0].data);
const datasets = data.map((item) => ({
name: item.name,
type: barData.vertical ? 'bar' : 'line',
smooth: true,
data: Object.values(item.data)
}));
const barOptions = getBarOptions(datasets, labels);
setEngBarData({
data: barOptions,
error: false,
loading: false,
vertical: false
});
});
}
/*
function getPotentialChart() {
setPotentialBarData((prev) => ({ ...prev, loading: true }));
getOverviewBarAPI('none', id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// on error
setPotentialBarData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const labels = Object.keys(data);
const datasets = {
name: 'Potential reach over time',
type: potentialBarData.vertical ? 'bar' : 'line',
smooth: true,
data: Object.values(data)
};
const barOptions = getBarOptions(datasets, labels);
setPotentialBarData({
data: barOptions,
error: false,
loading: false,
vertical: false
});
});
} */
function getSentimentChart() {
setSentimentBar((prev) => ({ ...prev, loading: true }));
getOverviewPieAPI('sentiment', id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// on error
setSentimentBar((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const barOptions = {};
Object.keys(data).forEach((feed) => {
const labels = ['Results'];
const datasets = ['POSITIVE', 'NEGATIVE', 'NEUTRAL'].map((item) => ({
name: item,
type: 'bar',
data: [data[feed][item]]
}));
barOptions[feed] = getBarOptions(datasets, labels);
});
setSentimentBar({
data: barOptions,
error: false,
loading: false,
vertical: false
});
});
}
function getpieMentions() {
setpieMentions((prev) => ({ ...prev, loading: true }));
getOverviewPieAPI('none', id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// alert on error
setpieMentions((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const pieOptions = getPieOptions(
Object.entries(data).map((v) => ({ name: v[0], value: v[1] }))
);
setpieMentions({
data: pieOptions,
error: false,
loading: false
});
});
}
function getpieEngg() {
setpieEng((prev) => ({ ...prev, loading: true }));
getEngagementsAPI(id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// alert on error
setpieEng((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
// condition for other filter than 0
const { data } = res.data;
const pieOptions = getPieOptions(
Object.entries(data).map((v) => ({ name: v[0], value: v[1] }))
);
setpieEng({
data: pieOptions,
error: false,
loading: false
});
});
}
/*
function getpieReach() {
setpieReach((prev) => ({ ...prev, loading: true }));
getOverviewPieAPI('none', id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// alert on error
setpieReach((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const pieOptions = getPieOptions(
Object.entries(data).map((v) => ({ name: v[0], value: v[1] }))
);
setpieReach({
data: pieOptions,
error: false,
loading: false
});
});
} */
function changeVertical(chart) {
switch (chart) {
case cn.first:
setBarData((prev) => ({ ...prev, vertical: !prev.vertical }));
return;
case cn.second:
setEngBarData((prev) => ({ ...prev, vertical: !prev.vertical }));
return;
case cn.third:
setPotentialBarData((prev) => ({ ...prev, vertical: !prev.vertical }));
return;
default:
return;
}
}
const hideChart1Alert = analyze.alertCharts.find((v) => v.name === cn.first);
const hideChart2Alert = analyze.alertCharts.find((v) => v.name === cn.second);
// const hideChart3Alert = analyze.alertCharts.find((v) => v.name === cn.third);
const hideChart4Alert = (id) =>
analyze.alertCharts.find((v) => v.name === cn.fourth && v.id === id);
const hideChart5Alert = analyze.alertCharts.find((v) => v.name === cn.fifth);
const hideChart6Alert = analyze.alertCharts.find((v) => v.name === cn.sixth);
/* const hideChart7Alert = analyze.alertCharts.find(
(v) => v.name === cn.seventh
); */
const barchart1Menus = [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.first, id: 'none' }),
showInMore: false,
hide: hideChart1Alert
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart1Alert
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.first),
showInMore: false
},
/* {
title: t('analyzeTab.chartMenus.addToDashboard'),
fn: () => {},
showInMore: true
}, */
{
title: t('analyzeTab.chartMenus.toggleHV'),
fn: () => changeVertical(cn.first),
showInMore: true
}
];
const barchart2Menus = [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.second, id: 'none' }),
showInMore: false,
hide: hideChart2Alert
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart2Alert
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.second),
showInMore: false
},
/* {
title: t('analyzeTab.chartMenus.addToDashboard'),
fn: () => {},
showInMore: true
}, */
{
title: t('analyzeTab.chartMenus.toggleHV'),
fn: () => changeVertical(cn.second),
showInMore: true
}
];
/*
const barchart3Menus = [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.third, id: 'none' }),
showInMore: false,
hide: hideChart3Alert
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart3Alert
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.third),
showInMore: false
},
{
title: t('analyzeTab.chartMenus.addToDashboard'),
fn: () => {},
showInMore: true
},
{
title: t('analyzeTab.chartMenus.toggleHV'),
fn: () => changeVertical(cn.third),
showInMore: true
}
];
*/
function barchart4Menus(id) {
return [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.fourth, id }),
showInMore: false,
hide: hideChart4Alert(id)
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart4Alert(id)
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.fourth, id),
showInMore: false
}
/* {
title: t('analyzeTab.chartMenus.addToDashboard'),
fn: () => {},
showInMore: true
} */
];
}
const pieChart1 = [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.fifth, id: 'none' }),
showInMore: false,
hide: hideChart5Alert
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart5Alert
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.fifth),
showInMore: false
}
// { title: t('analyzeTab.chartMenus.addToDashboard'), fn: () => {}, showInMore: true }
];
const pieChart2 = [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.sixth, id: 'none' }),
showInMore: false,
hide: hideChart6Alert
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart6Alert
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.sixth),
showInMore: false
}
// { title: t('analyzeTab.chartMenus.addToDashboard'), fn: () => {}, showInMore: true }
];
/*
const pieChart3 = [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.seventh, id: 'none' }),
showInMore: false,
hide: hideChart7Alert
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart7Alert
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.seventh),
showInMore: false
}
// { title: t('analyzeTab.chartMenus.addToDashboard'), fn: () => {}, showInMore: true }
];
*/
return (
<Row>
<Col md="8">
<ChartWrapper
title={t('analyzeTab.charts.mentionsOverTime')}
menus={barchart1Menus}
>
<ECharts
xLabel={barData.labels}
loading={barData.loading}
options={barData.data}
/>
</ChartWrapper>
</Col>
<Col md="4">
<ChartWrapper title={t('analyzeTab.charts.mentions')} menus={pieChart1}>
<ECharts
xLabel={pieMentions.labels}
loading={pieMentions.loading}
options={pieMentions.data}
/>
</ChartWrapper>
</Col>
<Col md="8">
<ChartWrapper
title={t('analyzeTab.charts.engagementOverTime')}
menus={barchart2Menus}
>
<ECharts
xLabel={engBarData.labels}
loading={engBarData.loading}
options={engBarData.data}
/>
</ChartWrapper>
</Col>
<Col md="4">
<ChartWrapper
title={t('analyzeTab.charts.engagement')}
menus={pieChart2}
>
<ECharts
xLabel={pieEng.labels}
loading={pieEng.loading}
options={pieEng.data}
/>
</ChartWrapper>
</Col>
{/* <Col md="8">
<ChartWrapper title={t('analyzeTab.charts.potentialReachOverTime')} menus={barchart3Menus}>
<ECharts
xLabel={potentialBarData.labels}
loading={potentialBarData.loading}
options={potentialBarData.data}
/>
</ChartWrapper>
</Col>
<Col md="4">
<ChartWrapper title={t('analyzeTab.charts.potentialReach')} menus={pieChart3}>
<ECharts
xLabel={pieReach.labels}
loading={pieReach.loading}
options={pieReach.data}
/>
</ChartWrapper>
</Col> */}
{feedData.feeds.map((feed) => (
<Col md="12" key={feed.id}>
<ChartWrapper
title={`${t('analyzeTab.charts.proportionofSentiment')} (${
feed.feed
})`}
menus={barchart4Menus(feed.id)}
>
<ECharts
xLabel={sentimentBar.labels}
loading={sentimentBar.loading}
options={sentimentBar.data[feed.feed]}
/>
</ChartWrapper>
</Col>
))}
</Row>
);
}
const cn = {
first: 'Mentions over time',
second: 'Engagement over time',
third: 'Potential reach over time',
fourth: 'Proportion of sentiment',
fifth: 'Mentions',
sixth: 'Engagement',
seventh: 'Potential Reach'
};
Performance.propTypes = {
chartData: PropTypes.object,
actions: PropTypes.object,
feedData: PropTypes.object,
id: PropTypes.string,
analyze: PropTypes.object,
t: PropTypes.func
};
const applyDecorators = compose(
reduxConnect('analyze', ['appState', 'analyze']),
translate(['tabsContent'], { wait: true })
);
export default applyDecorators(React.memo(Performance));
@@ -0,0 +1,403 @@
import React, { Fragment, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Button, ButtonGroup, Col, Row } from 'reactstrap';
import ECharts from '../../../../../common/charts/ECharts';
import ChartWrapper from '../ChartWrapper';
import {
getBarOptions,
getPieOptions
} from '../../../../../common/charts/ChartsOptions';
import { IoIosAdd, IoIosRefresh, IoIosCheckmark } from 'react-icons/io';
import reduxConnect from '../../../../../../redux/utils/connect';
import translate from 'react-i18next/dist/commonjs/translate';
import { compose } from 'redux';
import {
getOverviewBarAPI,
getOverviewPieAPI
} from '../../../../../../api/analytics/createAnalytics';
import useIsMounted from '../../../../../common/hooks/useIsMounted';
const initialBar = {
data: [],
error: undefined,
loading: true,
vertical: false
};
const initialPie = { data: [], error: undefined, loading: true };
function ResultsTab(props) {
const { actions, analyze, feedData, id, t } = props;
const isMounted = useIsMounted();
const [barData, setBarData] = useState(initialBar);
const [barTimeData, setBarTimeData] = useState(initialBar);
const [pieData, setPieData] = useState(initialPie);
const [pieTimeData, setPieTimeData] = useState(initialPie);
const [filter, setFilter] = useState(filtersNames[0].id);
useEffect(() => {
if (!id) {
return;
}
if (filter === filtersNames[0].id) {
getBarChart();
getPieChart();
} else {
getBarChartFeeds();
getPieChartFeeds();
}
}, [filter]);
useEffect(() => {
if (barData.data) {
setBarData((prev) => ({
...prev,
data: {
...prev.data,
xAxis: prev.data.yAxis,
yAxis: prev.data.xAxis
}
}));
}
}, [barData.vertical]);
function updateResult(foo, id) {
switch (id) {
case cn.first:
filter === filtersNames[0].id ? getBarChart() : getBarChartFeeds();
return;
case cn.second:
filter === filtersNames[0].id ? getPieChart() : getPieChartFeeds();
return;
}
}
function getBarChart() {
setBarData((prev) => ({ ...prev, loading: true }));
getOverviewBarAPI(filter, id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// on error
setBarData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const labels = data[0] ? Object.keys(data[0].data) : [];
const datasets = data.map((item) => ({
name: item.name,
type: barData.vertical ? 'bar' : 'line',
smooth: true,
data: Object.values(item.data)
}));
const barOptions = getBarOptions(datasets, labels);
setBarData({
data: barOptions,
error: false,
loading: false,
vertical: false
});
});
}
function getBarChartFeeds() {
setBarTimeData((prev) => ({ ...prev, loading: true }));
getOverviewBarAPI(filter, id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// on error
setBarTimeData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const barOptions = {};
const errors = {};
data.map((feed) => {
const { name, data } = feed;
if (!data || (Array.isArray(data) && data.length < 1)) {
errors[name] = t('analyzeTab.noData');
return;
}
const labels = Object.keys(data[0].data).sort();
const datasets = data.map((item) => ({
name: item.name,
type: barTimeData.vertical ? 'bar' : 'line',
smooth: true,
data: labels.map((v) => item.data[v])
}));
barOptions[name] = getBarOptions(datasets, labels);
});
setBarTimeData({
data: barOptions,
error: errors,
loading: false,
vertical: false
});
});
}
function getPieChart() {
setPieData((prev) => ({ ...prev, loading: true }));
getOverviewPieAPI(filter, id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// alert on error
setPieData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const pieOptions = getPieOptions(
Object.entries(data).map((v) => ({ name: v[0], value: v[1] }))
);
setPieData({
data: pieOptions,
error: false,
loading: false
});
});
}
function getPieChartFeeds() {
setPieTimeData((prev) => ({ ...prev, loading: true }));
getOverviewPieAPI(filter, id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// alert on error
setPieTimeData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const pieOptions = {};
const errors = {};
Object.entries(data).forEach((feed) => {
const [name, value] = feed;
if (!value || (Array.isArray(value) && value.length < 1)) {
errors[name] = t('analyzeTab.noData');
}
pieOptions[name] = getPieOptions(
Object.entries(value).map((v) => ({
name: v[0],
value: v[1]
}))
);
});
setPieTimeData({
data: pieOptions,
error: errors,
loading: false
});
});
}
function changeVertical() {
setBarData((prev) => ({ ...prev, vertical: !prev.vertical }));
}
const hideChart1Alert = (id) =>
analyze.alertCharts.find((v) => v.name === cn.first && v.id === id);
const hideChart2Alert = (id) =>
analyze.alertCharts.find((v) => v.name === cn.second && v.id === id);
const barchartMenus = (id) => [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.first, id }),
showInMore: false,
hide: hideChart1Alert(id)
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart1Alert(id)
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.first),
showInMore: false
},
/* {
title: t('analyzeTab.chartMenus.addToDashboard'),
fn: () => {},
showInMore: true
}, */
{
title: 'Toggle Horizontal/Vertical',
fn: changeVertical,
showInMore: true
}
];
const piechartMenus = (id) => [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.second, id }),
showInMore: false,
hide: hideChart2Alert(id)
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart2Alert(id)
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.second),
showInMore: false
}
// { title: t('analyzeTab.chartMenus.addToDashboard'), fn: () => {}, showInMore: true }
];
return (
<Fragment>
<div className="mask-line overflow-auto white-space-nowrap pl-3 mb-3">
<ButtonGroup size="sm">
{filtersNames.map((item) => (
<Button
outline
key={item.id}
title={item.name}
color="secondary"
onClick={function () {
setFilter(item.id);
}}
active={filter === item.id}
>
{t(`analyzeTab.overviewCharts.${item.transKey}`)}
</Button>
))}
</ButtonGroup>
</div>
{filter === filtersNames[0].id ? ( // feeds in single graph
<Row>
<Col md="8">
<ChartWrapper
title={t('analyzeTab.charts.mentionsOverTime')}
menus={barchartMenus('none')}
>
<ECharts
xLabel={barData.labels}
loading={barData.loading}
options={barData.data}
/>
</ChartWrapper>
</Col>
<Col md="4">
<ChartWrapper
title={t('analyzeTab.charts.mentions')}
menus={piechartMenus('none')}
>
<ECharts loading={pieData.loading} options={pieData.data} />
</ChartWrapper>
</Col>
</Row>
) : (
feedData.feeds.map((feed) => (
<Row key={feed.id}>
<Col md="8">
<ChartWrapper
title={`${t('analyzeTab.charts.mentionsOverTime')} (${
feed.feed
})`}
menus={barchartMenus(feed.id)}
>
<ECharts
xLabel={barTimeData.labels}
loading={barTimeData.loading}
options={barTimeData.data && barTimeData.data[feed.feed]}
message={barTimeData.error && barTimeData.error[feed.feed]}
/>
</ChartWrapper>
</Col>
<Col md="4">
<ChartWrapper
title={`${t('analyzeTab.charts.mentions')} (${feed.feed})`}
menus={piechartMenus(feed.id)}
>
<ECharts
loading={pieTimeData.loading}
options={pieTimeData.data[feed.feed]}
message={pieTimeData.error && pieTimeData.error[feed.feed]}
/>
</ChartWrapper>
</Col>
</Row>
))
)}
</Fragment>
);
}
const cn = {
first: 'Mentions Over Time',
second: 'Share of Mentions'
};
const filtersNames = [
{ name: 'None', transKey: 'none', id: 'none' },
{ name: 'Media Types', transKey: 'mediaTypes', id: 'media' },
{ name: 'Sentiments', transKey: 'sentiments', id: 'sentiment' },
// { name: 'Countries', transKey:'countries', id: 'country' },
{ name: 'Languages', transKey: 'languages', id: 'language' }
];
ResultsTab.propTypes = {
actions: PropTypes.object,
id: PropTypes.string,
t: PropTypes.func,
feedData: PropTypes.object,
analyze: PropTypes.object
};
const applyDecorators = compose(
reduxConnect('analyze', ['appState', 'analyze']),
translate(['tabsContent'], { wait: true })
);
export default applyDecorators(React.memo(ResultsTab));
@@ -0,0 +1,255 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Col, Row } from 'reactstrap';
import ECharts from '../../../../../common/charts/ECharts';
import ChartWrapper from '../ChartWrapper';
import {
getBarOptions,
getPieOptions
} from '../../../../../common/charts/ChartsOptions';
import { IoIosAdd, IoIosRefresh, IoIosCheckmark } from 'react-icons/io';
import reduxConnect from '../../../../../../redux/utils/connect';
import translate from 'react-i18next/dist/commonjs/translate';
import { compose } from 'redux';
import {
getOverviewBarAPI,
getOverviewPieAPI
} from '../../../../../../api/analytics/createAnalytics';
import useIsMounted from '../../../../../common/hooks/useIsMounted';
const initialBar = {
data: [],
error: undefined,
loading: true,
vertical: false
};
const initialPie = { data: [], error: undefined, loading: true };
function Sentiment(props) {
const { actions, analyze, feedData, id, t } = props;
const isMounted = useIsMounted();
const [barData, setBarData] = useState(initialBar);
const [pieData, setPieData] = useState(initialPie);
useEffect(() => {
if (!id) {
return;
}
getBarChart();
getPieChart();
}, []);
useEffect(() => {
if (barData.data) {
setBarData((prev) => ({
...prev,
data: {
...prev.data,
xAxis: prev.data.yAxis,
yAxis: prev.data.xAxis
}
}));
}
}, [barData.vertical]);
function updateResult(foo, id) {
switch (id) {
case cn.first:
getBarChart();
return;
case cn.second:
getPieChart();
return;
}
}
function getBarChart() {
setBarData((prev) => ({ ...prev, loading: true }));
getOverviewBarAPI('sentiment', id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// on error
setBarData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const barOptions = {};
data.forEach((feed) => {
const { name, data } = feed;
const labels = Object.keys(data[0].data).sort();
const datasets = data.map((item) => ({
name: item.name,
type: barData.vertical ? 'bar' : 'line',
smooth: true,
data: labels.map((v) => item.data[v])
}));
barOptions[name] = getBarOptions(datasets, labels);
});
setBarData({
data: barOptions,
error: false,
loading: false,
vertical: false
});
});
}
function getPieChart() {
setPieData((prev) => ({ ...prev, loading: true }));
getOverviewPieAPI('sentiment', id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// alert on error
setPieData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const pieOptions = {};
Object.entries(data).forEach((feed) => {
const [name, value] = feed;
pieOptions[name] = getPieOptions(
Object.entries(value).map((v) => ({
name: v[0],
value: v[1]
}))
);
});
setPieData({
data: pieOptions,
error: false,
loading: false
});
});
}
function changeVertical() {
setBarData((prev) => ({ ...prev, vertical: !prev.vertical }));
}
const hideChart1Alert = (id) =>
analyze.alertCharts.find((v) => v.name === cn.first && v.id === id);
const hideChart2Alert = (id) =>
analyze.alertCharts.find((v) => v.name === cn.second && v.id === id);
const barchartMenus = (id) => [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.first, id }),
showInMore: false,
hide: hideChart1Alert(id)
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart1Alert(id)
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.first),
showInMore: false
},
/* {
title: t('analyzeTab.chartMenus.addToDashboard'),
fn: () => {},
showInMore: true
}, */
{
title: 'Toggle Horizontal/Vertical',
fn: changeVertical,
showInMore: true
}
];
const piechartMenus = (id) => [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.second, id }),
showInMore: false,
hide: hideChart2Alert(id)
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart2Alert(id)
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.second),
showInMore: false
}
// { title: t('analyzeTab.chartMenus.addToDashboard'), fn: () => {}, showInMore: true }
];
return feedData.feeds.map((feed) => (
<Row key={feed.id}>
<Col md="8">
<ChartWrapper
title={`${t('analyzeTab.charts.sentimentOverTime')} (${feed.feed})`}
menus={barchartMenus(feed.id)}
>
<ECharts
xLabel={barData.labels}
loading={barData.loading}
options={barData.data[feed.feed]}
/>
</ChartWrapper>
</Col>
<Col md="4">
<ChartWrapper
title={`${t('analyzeTab.charts.shareofSentiment')} (${feed.feed})`}
menus={piechartMenus(feed.id)}
>
<ECharts
loading={pieData.loading}
options={pieData.data[feed.feed]}
/>
</ChartWrapper>
</Col>
</Row>
));
}
const cn = {
first: 'Sentiment Over Time',
second: 'Share of Sentiment'
};
Sentiment.propTypes = {
actions: PropTypes.object,
feedData: PropTypes.object,
analyze: PropTypes.object,
t: PropTypes.func
};
const applyDecorators = compose(
reduxConnect('analyze', ['appState', 'analyze']),
translate(['tabsContent'], { wait: true })
);
export default applyDecorators(React.memo(Sentiment));
@@ -0,0 +1,284 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Col, Row } from 'reactstrap';
import ECharts from '../../../../../common/charts/ECharts';
import 'echarts-wordcloud';
import { capitalize } from 'lodash';
import ChartWrapper from '../ChartWrapper';
import {
getBarOptions,
PieToolbox,
WordCloudOptions
} from '../../../../../common/charts/ChartsOptions';
import { IoIosAdd, IoIosRefresh, IoIosCheckmark } from 'react-icons/io';
import reduxConnect from '../../../../../../redux/utils/connect';
import translate from 'react-i18next/dist/commonjs/translate';
import { compose } from 'redux';
import {
getThemesCloudAPI,
getThemesTimeAPI
} from '../../../../../../api/analytics/createAnalytics';
import useIsMounted from '../../../../../common/hooks/useIsMounted';
import { capFirstLetter } from '../../../../../../common/helper';
const initialBar = {
data: [],
error: undefined,
loading: true,
vertical: false
};
const initialPie = { data: [], error: undefined, loading: true };
function Themes(props) {
const { actions, analyze, feedData, id, t } = props;
const isMounted = useIsMounted();
const [barData, setBarData] = useState(initialBar);
const [wordData, setWordData] = useState(initialPie);
useEffect(() => {
if (!id) {
return;
}
getBarChart();
getWordCloud();
}, []);
useEffect(() => {
if (barData.data) {
setBarData((prev) => ({
...prev,
data: {
...prev.data,
xAxis: prev.data.yAxis,
yAxis: prev.data.xAxis
}
}));
}
}, [barData.vertical]);
function updateResult(foo, id) {
switch (id) {
case cn.first:
getBarChart();
return;
case cn.second:
getWordCloud();
return;
}
}
function getBarChart() {
setBarData((prev) => ({ ...prev, loading: true }));
getThemesTimeAPI(id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// on error
setBarData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
let labels = null;
const barOptions = {};
const errors = {};
data.forEach((feedData) => {
const { name, data } = feedData;
const datasets = data.map((item) => ({
name: capitalize(item.name),
type: barData.vertical ? 'bar' : 'line',
smooth: true,
data: Object.values(item.data)
}));
if (!labels && data && data[0] && data[0].data) {
labels = Object.keys(data[0].data);
}
barOptions[name] = getBarOptions(datasets, labels);
if (!datasets || (Array.isArray(datasets) && datasets.length < 1)) {
errors[name] = t('analyzeTab.noData');
}
});
setBarData({
data: barOptions,
error: errors,
loading: false,
vertical: false
});
});
}
function getWordCloud() {
setWordData((prev) => ({ ...prev, loading: true }));
getThemesCloudAPI(id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// alert on error
setWordData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const cloudOptions = {};
const errors = {};
data.forEach((feed) => {
const { name, data } = feed;
if (!data || (Array.isArray(data) && data.length < 1)) {
errors[name] = t('analyzeTab.noData');
}
cloudOptions[name] = {
tooltip: {
show: true
},
toolbox: PieToolbox,
series: [
{
...WordCloudOptions,
data: Object.entries(data).map((v) => ({
name: capFirstLetter(v[0]),
value: v[1]
}))
}
]
};
});
setWordData({
data: cloudOptions,
error: false,
loading: false
});
});
}
function changeVertical() {
setBarData((prev) => ({ ...prev, vertical: !prev.vertical }));
}
const hideChart1Alert = (id) =>
analyze.alertCharts.find((v) => v.name === cn.first && v.id === id);
const hideChart2Alert = (id) =>
analyze.alertCharts.find((v) => v.name === cn.second && v.id === id);
const barchartMenus = (id) => [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.first, id }),
showInMore: false,
hide: hideChart1Alert(id)
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart1Alert(id)
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.first),
showInMore: false
},
/* {
title: t('analyzeTab.chartMenus.addToDashboard'),
fn: () => {},
showInMore: true
}, */
{
title: 'Toggle Horizontal/Vertical',
fn: changeVertical,
showInMore: true
}
];
const wordCloudMenus = (id) => [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.second, id }),
showInMore: false,
hide: hideChart2Alert(id)
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart2Alert(id)
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.second),
showInMore: false
}
// { title: t('analyzeTab.chartMenus.addToDashboard'), fn: () => {}, showInMore: true }
];
return feedData.feeds.map((feed) => (
<Row key={feed.id}>
<Col md="8">
<ChartWrapper
title={`${t('analyzeTab.charts.themesOverTime')} (${feed.feed})`}
menus={barchartMenus(feed.id)}
>
<ECharts
xLabel={barData.labels}
loading={barData.loading}
options={barData.data[feed.feed]}
message={barData.error && barData.error[feed.feed]}
/>
</ChartWrapper>
</Col>
<Col md="4">
<ChartWrapper
title={`${t('analyzeTab.charts.topThemes')} (${feed.feed})`}
menus={wordCloudMenus(feed.id)}
>
<ECharts
loading={wordData.loading}
options={wordData.data[feed.feed]}
message={barData.error && barData.error[feed.feed]}
/>
</ChartWrapper>
</Col>
</Row>
));
}
const cn = {
first: 'Themes over time',
second: 'Top Themes'
};
Themes.propTypes = {
chartData: PropTypes.object,
actions: PropTypes.object,
feedData: PropTypes.object,
t: PropTypes.func,
analyze: PropTypes.object
};
const applyDecorators = compose(
reduxConnect('analyze', ['appState', 'analyze']),
translate(['tabsContent'], { wait: true })
);
export default applyDecorators(React.memo(Themes));
@@ -0,0 +1,208 @@
import React, { useEffect, useRef, useState, Fragment } from 'react';
import PropTypes from 'prop-types';
import { Row, Col, ButtonGroup, Button } from 'reactstrap';
import 'leaflet/dist/leaflet.css';
import L from 'leaflet';
import 'leaflet-dvf/dist/leaflet-dvf';
// keep above 3 in sequence
import ChartWrapper from '../ChartWrapper';
import { getWorldMapAPI } from '../../../../../../api/analytics/createAnalytics';
import useIsMounted from '../../../../../common/hooks/useIsMounted';
import { translate } from 'react-i18next';
const initialPie = {
data: [],
error: undefined,
loading: true,
selected: undefined
};
function WorldMap(props) {
const { id, t } = props;
const mapRef = useRef();
const isMounted = useIsMounted();
const [pieData, setPieData] = useState(initialPie);
const [markers, setMarkers] = useState([]);
const feedNames = (pieData.data && Object.keys(pieData.data)) || [];
useEffect(() => {
mapRef.current = L.map('leaflet-map', {
center: [0, 0],
zoom: 2,
layers: [
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
noWrap: true,
attribution:
'&copy; <a target="_blank" noreferrer noopener href="http://osm.org/copyright">OpenStreetMap</a> contributors'
})
]
});
mapRef.current.whenReady(getMapSentiments);
}, []);
useEffect(() => {
const { data, selected, error } = pieData;
const selectedData = data[feedNames[selected]];
const hasErr = error && error[feedNames[selected]];
clearMap();
if (selectedData && !hasErr) {
// loop to add marker
const markersList = [];
selectedData.forEach((data) => {
const [lat, lng] = getLatLong(data.LatLng);
if (!lat || !lng) {
return;
}
let pieChartMarker = new L.PieChartMarker(new L.LatLng(lat, lng), {
...options,
data: {
positive: data.POSITIVE,
negative: data.NEGATIVE,
neutral: data.NEUTRAL
}
});
pieChartMarker.addTo(mapRef.current);
markersList.push(pieChartMarker);
});
// eslint-disable-next-line new-cap
const group = new L.featureGroup(markersList);
mapRef.current.fitBounds(group.getBounds());
setMarkers(markersList);
}
}, [pieData.data, pieData.selected]);
function getLatLong(str) {
const [lat, lng] = str.split(', ');
return [lat && parseFloat(lat), lng && parseFloat(lng)];
}
function clearMap() {
if (mapRef.current) {
markers.forEach((v) => {
mapRef.current.removeLayer(v);
});
}
}
function getMapSentiments() {
setPieData((prev) => ({ ...prev, loading: true }));
getWorldMapAPI(id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// alert on error
setPieData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const dataValues = {};
const errors = {};
data.map((feed) => {
const { name, data } = feed;
if (!data || (Array.isArray(data) && data.length < 1)) {
errors[name] = t('analyzeTab.noData');
}
dataValues[name] = data;
});
setPieData({
data: dataValues,
error: errors,
loading: false,
selected: 0
});
});
}
const style = {
height: 'max(300px, calc(100vh - 200px))'
};
return (
<Row>
<Col md="12">
<ChartWrapper title="Distribution by Sentiments">
<Fragment>
<ButtonGroup size="sm" className="d-block mb-2 text-right">
{feedNames.map((name, i) => (
<Button
outline
key={name}
title={name}
color="secondary"
onClick={function () {
setPieData((prev) => ({
...prev,
selected: i
}));
}}
active={pieData.selected === i}
>
{name}
</Button>
))}
</ButtonGroup>
<div className="position-relative">
<div id="leaflet-map" style={style} />
{pieData.error && pieData.error[feedNames[pieData.selected]] ? (
<div className="no-data" style={{ zIndex: 1000 }}>
{pieData.error[feedNames[pieData.selected]]}
</div>
) : null}
</div>
</Fragment>
</ChartWrapper>
</Col>
</Row>
);
}
const options = {
stroke: false,
fillOpacity: 0.7,
radius: 20,
gradient: false,
chartOptions: {
positive: {
fillColor: '#00FF00',
displayText: function (value) {
return value.toFixed(0);
}
},
negative: {
fillColor: '#FF0000',
displayText: function (value) {
return value.toFixed(0);
}
},
neutral: {
fillColor: '#000000',
displayText: function (value) {
return value.toFixed(0);
}
}
}
// Other L.Path style options
};
WorldMap.propTypes = {
actions: PropTypes.object,
feedData: PropTypes.object,
id: PropTypes.string,
t: PropTypes.func.isRequired,
analyze: PropTypes.object
};
export default translate(['tabsContent'], { wait: true })(WorldMap);
@@ -0,0 +1,17 @@
import Results from './Results'
import Performance from './Performance'
import Influencers from './Influencers'
import Sentiment from './Sentiment'
import Themes from './Themes'
import Demographics from './Demographics'
import WorldMap from './WorldMap'
export {
Results,
Performance,
Influencers,
Sentiment,
Themes,
Demographics,
WorldMap
}
@@ -0,0 +1,58 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
import { deleteAnalytics } from '../../../../../api/analytics/savedAnalytics';
import { translate } from 'react-i18next';
function DeleteDialog(props) {
const [loading, setLoading] = useState(false);
const { actions, data, toggle, fetchData, t } = props;
function handleSubmit() {
setLoading(true);
deleteAnalytics(data.value).then((res) => {
if (res.error) {
res.data
? actions.addAlert(res.data)
: actions.addAlert({ type: 'error', transKey: 'somethingWrong' });
setLoading(false);
return;
}
actions.addAlert({ type: 'notice', transKey: 'analyticsDeleted' });
setLoading(false);
toggle();
fetchData();
});
}
return (
<Modal isOpen={!!data} toggle={toggle} backdrop="static">
<ModalHeader toggle={toggle}>
{t('tabsContent:analyzeTab.deleteAnalysis')}
</ModalHeader>
<ModalBody>
<div>
<p>{t('messages.deleteMessage')}</p>
</div>
</ModalBody>
<ModalFooter>
<Button color="link" onClick={toggle}>
{t('commonWords.Cancel')}
</Button>
<Button color="danger" disabled={loading} onClick={handleSubmit}>
{loading ? t('commonWords.loading') : t('commonWords.Delete')}
</Button>
</ModalFooter>
</Modal>
);
}
DeleteDialog.propTypes = {
toggle: PropTypes.func,
t: PropTypes.func.isRequired,
data: PropTypes.object.isRequired,
fetchData: PropTypes.func,
actions: PropTypes.object
};
export default React.memo(translate(['common'], { wait: true })(DeleteDialog));
@@ -0,0 +1,170 @@
/* eslint-disable react/prop-types */
import React, {
useState,
useCallback,
useMemo,
Fragment,
useEffect
} from 'react';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { compose } from 'redux';
import { Table } from '../../../../common/Table/Table';
import { savedAnalytics } from '../../../../../api/analytics/savedAnalytics';
import reduxConnect from '../../../../../redux/utils/connect';
import {
getDate,
getQueryParams,
setDocumentData
} from '../../../../../common/helper';
import { Button } from 'reactstrap';
import DeleteDialog from './DeleteDialog';
import i18n from '../../../../../i18n';
function SavedAnalysisSubTab(props) {
const [dataSource, setDataSource] = useState({ data: [] });
const [loading, setLoading] = useState(true);
const [deleteValues, setDeleteValues] = useState(false);
const { t, actions } = props;
useEffect(() => {
setDocumentData('title', 'Saved Analysis | Analyze');
return () => {
setDocumentData('title');
};
}, []);
const columns = useMemo(() => {
const columnsList = [
{
id: 'feeds',
Header: t('analyzeTab.savedAnalytics.feeds'),
accessor: (d) => d.context.feeds,
Cell: (props) =>
props.value ? props.value.map((v) => v.name).join(', ') : ''
},
{
id: 'date',
Header: t('analyzeTab.savedAnalytics.dateRange'),
accessor: (d) => d.context.rawFilters.date,
Cell: (props) =>
props.value
? `${getDate(props.value.start, 'MM/DD/YYYY')} to ${getDate(
props.value.end,
'MM/DD/YYYY'
)}`
: '-'
},
{
Header: t('analyzeTab.savedAnalytics.createdAt'),
accessor: 'createdAt',
Cell: (props) => getDate(props.value, 'MM/DD/YYYY')
},
{
Header: t('analyzeTab.savedAnalytics.actions'),
accessor: 'id',
Cell: (props) => getActions(props)
}
];
return columnsList;
}, [getActions, i18n.language]);
const getActions = useCallback((props) => {
return (
<div>
<Button
outline
className="border-0 btn-transition"
color="primary"
size="sm"
tag={Link}
to={`/app/analyze/${props.value}/overview`}
>
{t('analyzeTab.savedAnalytics.view')}
</Button>
<Button
outline
className="border-0 btn-transition"
color="secondary"
tag={Link}
to={`/app/analyze/edit/${props.value}`}
>
{t('analyzeTab.savedAnalytics.edit')}
</Button>
<Button
outline
className="border-0 btn-transition"
color="secondary"
onClick={function () {
setDeleteValues(props);
}}
>
{t('analyzeTab.savedAnalytics.delete')}
</Button>
</div>
);
}, []);
const getSavedList = useCallback(
(page, pageSize) => {
setLoading(true);
const params = getQueryParams({ page, pageSize });
savedAnalytics(params).then((res) => {
if (res.error || res.data === null || !res.data) {
setLoading(false);
return actions.addAlert({
type: 'error',
transKey: 'somethingWrong'
});
}
res.data.length > 0 && setDataSource(res.data[0]);
setLoading(false);
});
},
[savedAnalytics]
);
const { data = [], totalCount = 0, limit = 10, page = 1 } = dataSource;
return (
<Fragment>
<Table
t={t}
cardTitle={t('analyzeTab.savedAnalysis')}
columns={columns}
data={data}
totalCount={totalCount}
showTotalCount
limit={limit}
page={page}
isLoading={loading}
onFetchData={getSavedList}
/>
{deleteValues && (
<DeleteDialog
data={deleteValues}
actions={actions}
toggle={function () {
setDeleteValues(false);
}}
fetchData={function () {
getSavedList(dataSource.page - 1, dataSource.limit);
}}
/>
)}
</Fragment>
);
}
SavedAnalysisSubTab.propTypes = {
t: PropTypes.func.isRequired,
actions: PropTypes.object
};
const applyDecorators = compose(
translate(['tabsContent'], { wait: true }),
reduxConnect()
);
export default applyDecorators(SavedAnalysisSubTab);
@@ -0,0 +1,61 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import { Link } from 'react-router-dom'
import { compose } from 'redux'
import { Card, Col, Row } from 'reactstrap'
class WelcomeSubTab extends React.Component {
render () {
const { t } = this.props
return (
<Card className="py-md-5 mb-3">
<Row className="justify-content-center no-gutters">
<Col sm="6" md="4" xl="4" className="m-4">
<div className="border b-radius-5 text-center p-4">
<div className="icon-wrapper mb-4 rounded-circle">
<div className="icon-wrapper-bg bg-primary" />
<i className="lnr-plus-circle text-primary" />
</div>
<div>
<h5 className="mb-5">{t('analyzeTab.createNewAnalysis')}</h5>
<Link
to="/app/analyze/create"
className="btn btn-primary btn-block fsize-1 btn-lg mr-1"
>
{t('analyzeTab.go')}
</Link>
</div>
</div>
</Col>
<Col sm="6" md="4" xl="4" className="m-4">
<div className="border b-radius-5 text-center p-4">
<div className="icon-wrapper mb-4 rounded-circle">
<div className="icon-wrapper-bg bg-primary" />
<i className="lnr-list text-primary" />
</div>
<div>
<h5 className="mb-5">{t('analyzeTab.viewSavedAnalysis')}</h5>
<Link
to="/app/analyze/saved"
className="btn btn-primary btn-block fsize-1 btn-lg mr-1"
>
{t('analyzeTab.view')}
</Link>
</div>
</div>
</Col>
</Row>
</Card>
)
}
}
WelcomeSubTab.propTypes = {
t: PropTypes.func.isRequired
}
const applyDecorators = compose(translate(['tabsContent'], { wait: true }))
export default applyDecorators(WelcomeSubTab)
@@ -0,0 +1,548 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import TimeAgo from 'timeago-react';
import ArticleComment from './ArticleComment';
import {
UncontrolledDropdown,
DropdownToggle,
DropdownMenu,
DropdownItem,
CustomInput,
Button
} from 'reactstrap';
import ShareMenu from './ShareMenu';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faFacebook,
faInstagram,
faPinterest,
faReddit,
faTumblr,
faTwitter,
faYoutube
} from '@fortawesome/free-brands-svg-icons';
import {
faComments,
faEye,
faFrown,
faMeh,
faQuoteLeft,
faShareAlt,
faSmile,
faThumbsDown,
faThumbsUp
} from '@fortawesome/free-solid-svg-icons';
import {
capOnlyFirstLetter,
convertUTCtoLocal,
abbreviateNumber,
notNullAndUnd
} from '../../../../../common/helper';
import SourceIndexInfoPopup from '../SourceIndexSubTab/SourceIndexInfoPopup';
const icons = {
twitter: faTwitter,
facebook: faFacebook,
instagram: faInstagram,
tumblr: faTumblr,
pinterest: faPinterest,
reddit: faReddit,
youtube: faYoutube,
POSITIVE: faSmile,
NEGATIVE: faFrown,
NEUTRAL: faMeh
};
const colors = {
POSITIVE: '#3ac47d',
NEGATIVE: '#FC3939',
NEUTRAL: '#868e96',
twitter: '#1DA1F2',
facebook: '#4267B2',
reddit: '#FF5700',
instagram: '#8a3ab9',
tumblr: '#34526F',
pinterest: '#E60023',
youtube: '#FF0000'
};
export class Article extends React.Component {
constructor() {
super();
this.state = {
shareMenu: false,
imgErr: false,
sourceModal: false
};
this.elemDesc = React.createRef();
}
selectArticle = () => {
this.props.selectArticle(this.props.article);
};
showEmailPopup = () => {
this.props.showEmailPopup([this.props.article]);
};
showCommentPopup = () => {
this.props.showCommentPopup(this.props.article);
};
showDeletePopup = () => {
this.props.showDeletePopup([this.props.article]);
};
showClipPopup = () => {
this.props.showClipPopup([this.props.article]);
};
toggleShareMenu = () => {
this.setState((prev) => ({ shareMenu: !prev.shareMenu }));
};
loadMoreComments = () => {
const {
loadMoreComments,
article: {
id: articleId,
comments: { count: offset }
}
} = this.props;
loadMoreComments(articleId, offset);
};
readLater = () => {
this.props.readArticleLater(this.props.article);
};
onImgError = () => {
this.setState({ imgErr: true });
};
toggleSourceModal = () => {
this.setState((prev) => ({ sourceModal: !prev.sourceModal }));
};
render() {
const { article, t, i18n, showCommentPopup, deleteComment } = this.props;
let {
comments,
id,
source,
sentiment,
permalink,
publisher,
title,
image,
author,
content,
published,
mentions,
tags,
likes,
dislikes,
views,
shares,
categories
} = article;
const { imgErr } = this.state;
const {
data: commentsData,
count: commentsCount, // should get real post comment count
totalCount: commentsTotalCount
} = comments;
const isArticleChosen = !!this.props.selectedArticles.find(
(item) => item.id === id
);
const offsetWidth =
this.elemDesc &&
this.elemDesc.current &&
this.elemDesc.current.offsetWidth;
const hasRightCounters =
notNullAndUnd(likes) ||
notNullAndUnd(dislikes) ||
commentsCount || // add not null and undefined when counter shows
notNullAndUnd(views) ||
notNullAndUnd(shares) ||
notNullAndUnd(mentions);
const isTwitter = source.siteType === 'twitter';
const isInstagram = source.siteType === 'instagram';
let username;
if (isTwitter) {
username =
author.link &&
author.link.match(
/^https?:\/\/(www\.)?twitter\.com\/(#!\/)?([^\/]+)(\/\w+)*$/
);
username = username && username[3];
}
if (isInstagram) {
username =
author.link &&
author.link.match(
/(?:(?:http|https):\/\/)?(?:www\.)?(?:instagram\.com|instagr\.am)\/([A-Za-z0-9-_\.]+)/
);
username = username && username[1];
}
const isRTL = document.documentElement.dir === 'rtl';
return (
<div className="post border b-radius-5 mb-4">
<UncontrolledDropdown className="post__menu">
<DropdownToggle
outline
color="primary"
className="btn-icon btn-icon-only p-1 m-2"
>
<i className="lnr lnr-menu btn-icon-wrapper" />
</DropdownToggle>
<DropdownMenu className={isRTL ? ' dropdown-menu-left' : ''}>
<DropdownItem
className="text-muted"
onClick={this.showCommentPopup}
>
<i className="mr-2 fa fa-comments"> </i>
<span>{t('searchTab.commentBtn')}</span>
</DropdownItem>
<DropdownItem className="text-muted" onClick={this.showClipPopup}>
<i className="mr-2 fa fa-cut"> </i>
<span>{t('searchTab.clipBtn')}</span>
</DropdownItem>
<DropdownItem className="text-muted" onClick={this.readLater}>
<i className="mr-2 fa fa-bookmark"> </i>
<span>{t('searchTab.readLaterBtn')}</span>
</DropdownItem>
<DropdownItem className="text-muted" onClick={this.readLater}>
<i className="mr-2 fa fa-archive"> </i>
<span>{t('searchTab.archiveBtn')}</span>
</DropdownItem>
<DropdownItem className="text-muted" onClick={this.showEmailPopup}>
<i className="mr-2 fa fa-envelope"> </i>
<span>{t('searchTab.emailBtn')}</span>
</DropdownItem>
<DropdownItem className="text-muted" onClick={this.toggleShareMenu}>
<i className="mr-2 fa fa-share-alt"> </i>
<span>{t('searchTab.shareBtn')}</span>
</DropdownItem>
<DropdownItem className="text-muted" onClick={this.showDeletePopup}>
<i className="mr-2 fa fa-trash"> </i>
<span>{t('searchTab.deleteBtn')}</span>
</DropdownItem>
</DropdownMenu>
</UncontrolledDropdown>
<div className="d-flex flex-row">
<div className="post__icons">
<CustomInput
id={'article-check-' + id}
type="checkbox"
className="mb-3"
onChange={this.selectArticle}
checked={isArticleChosen}
/>
{source.siteType && (
<FontAwesomeIcon
title={capOnlyFirstLetter(source.siteType)}
icon={icons[source.siteType]}
size="lg"
className="fa-w-16 mb-3"
color={colors[source.siteType]}
/>
)}
{sentiment && (
<FontAwesomeIcon
title={capOnlyFirstLetter(sentiment)}
icon={icons[sentiment]}
className="mb-3"
size="lg"
color={colors[sentiment]}
/>
)}
</div>
<div className="post_middlepart">
<h2 className="post__title">
{title && (
<a href={permalink} target="_blank" rel="noopener noreferrer">
{title}
</a>
)}
</h2>
<div
ref={this.elemDesc}
className={`post__content${
offsetWidth && offsetWidth < 430 ? ' flex-column' : ''
}`}
>
{image &&
!imgErr &&
(!title && permalink ? (
<a href={permalink} target="_blank" rel="noopener noreferrer">
<img
id={id}
width="180px"
className="post__img mb-2 mb-lg-0 mr-3"
src={image}
onError={this.onImgError}
/>
</a>
) : (
<img
id={id}
width="180px"
className="post__img mb-2 mb-lg-0 mr-3"
src={image}
onError={this.onImgError}
/>
))}
<div>
{author.name ? (
author.link ? (
<a
className="d-inline-block hover-link text-muted mb-2"
href={author.link}
target="_blank"
>
{username ? `@${username}` : author.name}
</a>
) : (
<p className="text-muted mb-2">{author.name}</p>
)
) : null}
{!title && permalink ? (
<a
href={permalink}
target="_blank"
rel="noopener noreferrer"
className="post__desc-link"
>
<p
className="post__desc"
dangerouslySetInnerHTML={{ __html: content }}
></p>
</a>
) : (
<p
className="post__desc"
dangerouslySetInnerHTML={{ __html: content }}
></p>
)}
</div>
</div>
{tags && tags.length && tags.length > 0 && (
<div className="post__tags mt-2">
<strong>{t('searchTab.tags')}</strong>: {tags.join(', ')}
</div>
)}
{categories && categories.length > 0 && (
<p className="post__tags my-2">
<strong>{t('searchTab.categories')}</strong>:{' '}
{categories.join(', ')}
</p>
)}
<div className="post__about-info text-muted mt-3">
{published && (
<Fragment>
<span
className="d-inline-block"
title={convertUTCtoLocal(published, 'MM/DD/YYYY HH:mm:ss')}
>
<TimeAgo
datetime={published}
locale={i18n.language}
opts={{ minInterval: 60 }}
/>
</span>
<span className="mx-2">|</span>
</Fragment>
)}
{source.type && (
<Fragment>
<span>{capOnlyFirstLetter(source.type)}</span>
<span className="mx-2">|</span>
</Fragment>
)}
{source.country && (
<Fragment>
<span>{source.country}</span>
<span className="mx-2">|</span>
</Fragment>
)}
{publisher && (
<Fragment>
<Button
color="link"
className="btn-anchor"
title="Click to see details"
onClick={this.toggleSourceModal}
>
{publisher}
</Button>
<span className="mx-2">|</span>
</Fragment>
)}
{source.title && (
<Fragment>
{publisher ? (
<a
href={source.link}
style={{ overflowWrap: 'anywhere' }}
rel="noopener noreferrer"
target="_blank"
>
{source.title}
</a>
) : (
<Button
color="link"
className="btn-anchor"
title="Click to see details"
onClick={this.toggleSourceModal}
>
{(isTwitter || isInstagram) && author.name
? author.name
: source.title}
</Button>
)}
</Fragment>
)}
</div>
</div>
{hasRightCounters && (
<div className="post__extras p-3">
<div className="post__icons-wrapper">
{notNullAndUnd(likes) && (
<div className="post__icon-metrics mb-1">
<FontAwesomeIcon
title="Likes"
icon={faThumbsUp}
className="text-success"
/>
<p className="ml-2" title={likes}>
{abbreviateNumber(likes)}
</p>
</div>
)}
{notNullAndUnd(dislikes) && (
<div className="post__icon-metrics mb-1">
<FontAwesomeIcon title="Dislikes" icon={faThumbsDown} />
<p className="ml-2" title={dislikes}>
{abbreviateNumber(dislikes)}
</p>
</div>
)}
{/* {notNullAndUnd(commentsCount) && (
Add above line when real comment counts are visible
*/}
{commentsCount ? (
<div className="post__icon-metrics mb-1">
<FontAwesomeIcon title="Comments" icon={faComments} />
<p className="ml-2" title={commentsCount}>
{abbreviateNumber(commentsCount)}
</p>
</div>
) : (
''
)}
{notNullAndUnd(views) && (
<div className="post__icon-metrics mb-1">
<FontAwesomeIcon title="Viwes" icon={faEye} />
<p className="ml-2 text-center" title={views}>
{abbreviateNumber(views)}
</p>
</div>
)}
{notNullAndUnd(shares) && (
<div className="post__icon-metrics mb-1">
<FontAwesomeIcon title="Shares" icon={faShareAlt} />
<p className="ml-2 text-center" title={shares}>
{abbreviateNumber(shares)}
</p>
</div>
)}
{notNullAndUnd(mentions) && (
<div className="post__icon-metrics mb-1">
<FontAwesomeIcon title="Mentions" icon={faQuoteLeft} />
<p className="ml-2 text-center" title={mentions}>
{abbreviateNumber(mentions)}
</p>
</div>
)}
</div>
</div>
)}
</div>
{commentsData && commentsData.length > 0 && (
<div className="post__comments border-top px-3 pb-3">
{commentsData.map((comment) => {
return (
<ArticleComment
article={article}
comment={comment}
showCommentPopup={showCommentPopup}
deleteComment={deleteComment}
key={comment.id}
/>
);
})}
{commentsCount < commentsTotalCount && (
<Button
outline
size="sm"
color="light"
className="mt-2 d-block ml-auto btn-icon"
onClick={this.loadMoreComments}
>
<i className="lnr lnr-chevron-down btn-icon-wrapper" />{' '}
{t('searchTab.moreComments')}
</Button>
)}
</div>
)}
{this.state.shareMenu && (
<ShareMenu article={article} hideMenu={this.toggleShareMenu} />
)}
{this.state.sourceModal && (
<SourceIndexInfoPopup
source={article.source}
hideSourceInfoPopup={this.toggleSourceModal}
/>
)}
</div>
);
}
}
Article.propTypes = {
article: PropTypes.object.isRequired,
selectedArticles: PropTypes.array.isRequired,
selectArticle: PropTypes.func.isRequired,
showEmailPopup: PropTypes.func.isRequired,
showDeletePopup: PropTypes.func.isRequired,
showCommentPopup: PropTypes.func.isRequired,
showClipPopup: PropTypes.func.isRequired,
deleteComment: PropTypes.func.isRequired,
readArticleLater: PropTypes.func.isRequired,
loadMoreComments: PropTypes.func.isRequired,
showShareMenu: PropTypes.func.isRequired,
i18n: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
};
export default translate(['tabsContent'], { wait: true })(Article);
@@ -0,0 +1,66 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate, Interpolate } from 'react-i18next'
import TimeAgo from 'timeago-react'
import { Button } from 'reactstrap'
export class ArticleComment extends React.Component {
static propTypes = {
article: PropTypes.object.isRequired,
comment: PropTypes.func.isRequired,
deleteComment: PropTypes.func.isRequired,
showCommentPopup: PropTypes.func.isRequired,
i18n: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
}
onEdit = () => {
const { showCommentPopup, article, comment } = this.props
showCommentPopup(article, comment)
}
onDelete = () => {
const { deleteComment, article, comment } = this.props
deleteComment(comment.id, article.id)
}
render() {
const { comment, i18n } = this.props
return (
<div className="post__comment mt-2">
<div className="d-flex justify-content-between">
<div>
<cite className="post__commentor mr-3">
<Interpolate
i18nKey="searchTab.commentMetadata"
author={`${comment.author.firstName} ${comment.author.lastName}`}
/>
</cite>
<span className="post__cmttime mr-3 text-muted">
<TimeAgo
datetime={comment.createdAt}
locale={i18n.language}
opts={{ minInterval: 30 }}
/>
</span>
</div>
<div>
<Button color="link" className="p-0" onClick={this.onEdit}>
<i className="lnr lnr-pencil"></i>
</Button>
<Button color="link" className="ml-2 p-0" onClick={this.onDelete}>
<i className="lnr lnr-trash"></i>
</Button>
</div>
</div>
<p className="post__cmt-content">
<strong className="d-block mb-1">{comment.title}</strong>
{comment.content}
</p>
</div>
)
}
}
export default translate(['tabsContent'], { wait: true })(ArticleComment)
@@ -0,0 +1,90 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import ClipDragSource from './ClipDragSource'
import RecentFeed from './RecentFeed'
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'
export class ClipArticlesPopup extends React.Component {
static propTypes = {
hidePopup: PropTypes.func.isRequired,
clipArticles: PropTypes.func.isRequired,
articles: PropTypes.array.isRequired,
recentClipFeeds: PropTypes.array.isRequired,
getRecentClipFeeds: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
}
hidePopupFromOutside = (e) => {
if (e.target === e.currentTarget) this.hidePopup()
}
hidePopup = () => {
this.props.hidePopup()
}
onSubmit = () => {
this.hidePopup()
}
componentWillMount = () => {
this.props.getRecentClipFeeds()
}
onRecentFeedClick = (feed) => {
this.props.clipArticles(feed.id)
this.props.hidePopup()
}
render() {
const { t, articles, recentClipFeeds } = this.props
return (
<Modal
isOpen
toggle={this.hidePopup}
backdrop={false}
modalClassName="pointer-events-none"
>
<ModalHeader toggle={this.hidePopup}>
{t('searchTab.clipPopup.header')}
</ModalHeader>
<ModalBody>
<div className="text-center">
<p>{t('searchTab.clipPopup.hint1')}</p>
<div className="draggable-container">
<ClipDragSource articles={articles} />
</div>
{recentClipFeeds && recentClipFeeds.length > 0 && (
<div className="mt-2">
<p className="mb-2">{t('searchTab.clipPopup.hint2')}</p>
<div className="d-flex justify-content-center flex-wrap">
{recentClipFeeds.map((feed) => {
return (
<RecentFeed
onRecentFeedClick={this.onRecentFeedClick}
key={feed.id}
feed={feed}
/>
)
})}
</div>
</div>
)}
</div>
</ModalBody>
<ModalFooter>
<Button color="light" onClick={this.hidePopup}>
{t('common:commonWords.Cancel')}
</Button>
</ModalFooter>
</Modal>
)
}
}
export default translate(['tabsContent', 'common'], { wait: true })(
ClipArticlesPopup
)
@@ -0,0 +1,69 @@
import React from 'react'
import PropTypes from 'prop-types'
import { TYPES } from '../../../../../../redux/modules/appState/sidebar'
import { Interpolate } from 'react-i18next'
import { DragSource } from 'react-dnd'
const source = {
beginDrag (props, monitor, component) {
setTimeout(() => {
component.setState({
isDragging: true
})
}, 0)
return {
type: TYPES.CLIP_ARTICLE
}
},
endDrag (props, monitor, component) {
component.setState({
isDragging: false
})
}
}
/**
* Specifies which props to inject into component from Drag n Drop.
*/
function collect (connect) {
return {
// Call this function inside render()
// to let React DnD handle the drag events:
connectDragSource: connect.dragSource()
}
}
export class ClipDragSource extends React.Component {
static propTypes = {
articles: PropTypes.array.isRequired,
connectDragSource: PropTypes.func.isRequired
};
constructor (props) {
super(props)
this.state = {
isDragging: false
}
}
render () {
const style = {
visibility: this.state.isDragging ? 'hidden' : 'visible'
}
return this.props.connectDragSource(
<div className="draggable-item" style={style}>
<span className="drag-handle" />
<Interpolate
i18nKey='searchTab.clipPopup.clippedArticles'
count={this.props.articles.length}
/>
</div>
)
}
}
export default DragSource(TYPES.CLIP_ARTICLE, source, collect)(ClipDragSource)
@@ -0,0 +1,26 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Button } from 'reactstrap'
export default class RecentFeed extends React.Component {
static propTypes = {
feed: PropTypes.object.isRequired,
onRecentFeedClick: PropTypes.func.isRequired
};
onClick = () => {
this.props.onRecentFeedClick(this.props.feed)
}
render () {
const { feed } = this.props
return (
<Button color="light" className={'mr-2 mb-2 feed-icon ' + feed.class} onClick={this.onClick}>
{feed.name}
</Button>
)
}
}
@@ -0,0 +1,139 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate, Interpolate } from 'react-i18next';
import TimeAgo from 'timeago-react';
import {
Button,
Input,
Modal,
ModalBody,
ModalFooter,
ModalHeader
} from 'reactstrap';
const initCharactersCount = 5000;
export class CommentArticlePopup extends React.Component {
static propTypes = {
article: PropTypes.object.isRequired,
comment: PropTypes.object,
commentArticle: PropTypes.func.isRequired,
updateComment: PropTypes.func.isRequired,
hidePopup: PropTypes.func.isRequired,
i18n: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
};
constructor(props) {
super(props);
const content = props.comment ? props.comment.content : '';
this.state = {
charactersCount: initCharactersCount - content.length,
title: props.comment ? props.comment.title : '',
comment: content
};
}
handleTitleChange = (e) => {
const { value } = e.target;
this.setState({ title: value });
};
hidePopup = () => {
this.props.hidePopup();
};
onSubmit = () => {
const newComment = {
title: this.state.title,
content: this.state.comment
};
if (this.props.comment) {
//edit exisitng
this.props.updateComment(newComment, this.props.article.id);
} else {
//create new comment
this.props.commentArticle(newComment, this.props.article.id);
}
this.hidePopup();
};
onChangeComment = (e) => {
const charactersCount = initCharactersCount - e.target.value.length;
if (charactersCount >= 0) {
this.setState({
charactersCount: charactersCount,
comment: e.target.value
});
}
};
render() {
const { t, i18n, article, comment } = this.props;
const popupTitle = comment
? t('searchTab.commentPopup.editUserComment')
: t('searchTab.commentPopup.addUserComment');
return (
<Modal isOpen toggle={this.hidePopup} backdrop="static">
<ModalHeader toggle={this.hidePopup}>{popupTitle}</ModalHeader>
<ModalBody>
<div className="mb-3">
<a
className="font-size-lg"
href={article.permalink}
target="_blank"
rel="noopener noreferrer"
>
{article.title}
</a>
<p>{article.author.name}</p>
<p className="font-size-xs text-muted">
<TimeAgo
datetime={article.published}
locale={i18n.language}
opts={{ minInterval: 30 }}
/>
</p>
</div>
<Input
value={this.state.title}
type="text"
className="mb-2"
onChange={this.handleTitleChange}
placeholder={t('searchTab.commentPopup.inputTitlePlaceholder')}
/>
<Input
rows="3"
type="textarea"
value={this.state.comment}
onChange={this.onChangeComment}
placeholder={t('searchTab.commentPopup.commentPlanceholder')}
/>
<p className="font-size-xs text-muted text-right mt-1">
<Interpolate
i18nKey="searchTab.commentPopup.charactersLeft"
count={this.state.charactersCount}
/>
</p>
</ModalBody>
<ModalFooter>
<Button color="light" onClick={this.hidePopup}>
{t('common:commonWords.Cancel')}
</Button>
<Button color="primary" onClick={this.onSubmit}>
{t('common:commonWords.submit')}
</Button>
</ModalFooter>
</Modal>
);
}
}
export default translate(['tabsContent', 'common'], { wait: true })(
CommentArticlePopup
);
@@ -0,0 +1,65 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Interpolate, translate } from 'react-i18next';
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
export class DeleteArticlesPopup extends React.Component {
static propTypes = {
articles: PropTypes.array.isRequired,
activeFeed: PropTypes.object,
hidePopup: PropTypes.func.isRequired,
deleteArticles: PropTypes.func.isRequired,
deleteArticlesFromFeed: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
onSubmit = () => {
const {
articles,
activeFeed,
deleteArticles,
deleteArticlesFromFeed,
hidePopup
} = this.props;
const ids = articles.map((a) => a.id);
if (activeFeed) {
deleteArticlesFromFeed(ids, activeFeed.id);
} else {
deleteArticles(ids);
}
hidePopup();
};
render() {
const { t, articles, hidePopup } = this.props;
return (
<Modal isOpen toggle={hidePopup} backdrop="static">
<ModalHeader toggle={hidePopup}>{t('commonWords.Confirm')}</ModalHeader>
<ModalBody>
<p>
{articles.length > 1 ? (
<Interpolate
t={t}
i18nKey="tabsContent:searchTab.deleteArticlePopupText_plural"
articlesLength={articles.length}
/>
) : (
t('tabsContent:searchTab.deleteArticlePopupText')
)}
</p>
</ModalBody>
<ModalFooter>
<Button color="light" onClick={hidePopup}>
{t('commonWords.Cancel')}
</Button>
<Button color="danger" onClick={this.onSubmit}>
{t('commonWords.Delete')}
</Button>
</ModalFooter>
</Modal>
);
}
}
export default translate(['common'], { wait: true })(DeleteArticlesPopup);
@@ -0,0 +1,209 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import moment from 'moment'
import Select from 'react-select'
import {
Button,
Modal,
ModalHeader,
ModalBody,
Label,
Input,
ModalFooter,
FormGroup,
Col,
Container
} from 'reactstrap'
import QuillEditor from '../../../../common/QuillEditor'
const replyToEmail = 'support@socialhose.io'
export class EmailArticlesPopup extends React.Component {
static propTypes = {
articlesToEmail: PropTypes.array.isRequired,
emailArticles: PropTypes.func.isRequired,
hidePopup: PropTypes.func.isRequired,
recipients: PropTypes.object.isRequired,
loadRecipients: PropTypes.func.isRequired,
children: PropTypes.any,
t: PropTypes.func.isRequired
}
constructor(props) {
super(props)
this.state = {
selectedRecipients: ''
}
this.editorRef = React.createRef()
}
componentWillMount = () => {
!this.props.recipients.all.length && this.props.loadRecipients()
}
componentDidMount = () => {
this.props.loadRecipients()
}
hidePopup = () => {
this.props.hidePopup()
}
collectParams = () => { // need to change with states
const recipients = this.state.selectedRecipients
if (!recipients) return false
return {
emailTo: recipients.map((r) => r.value),
emailReplyTo: document.getElementById('email-reply-to').value,
subject: document.getElementById('email-subject').value,
content: this.editorRef.current && this.editorRef.current.root.innerHTML
}
}
onSubmit = () => {
const params = this.collectParams()
if (params) {
this.props.emailArticles(params)
}
}
changeRecipient = (value) => {
this.setState({
selectedRecipients: value
})
}
validEmails = (str) => {
const re = /\S+@\S+\.\S+/
const arr = str.split(',')
for (let s of arr) {
if (!re.test(s)) {
return false
}
}
return true
}
emailRe = /\S+@\S+\.\S+/
isValidNewOption = ({ label }) => {
return this.emailRe.test(label)
}
promptTextCreator = (label) => {
return label
}
render() {
const { t, articlesToEmail, recipients } = this.props
const { selectedRecipients } = this.state
const recipientsAll = recipients.all.map((recipient) => ({
value: recipient,
label: recipient
}))
return (
<Modal
isOpen
size="lg"
toggle={this.hidePopup}
backdrop="static"
>
<ModalHeader toggle={this.hidePopup}>
{t('searchTab.emailPopup.header')}
</ModalHeader>
<ModalBody>
<Container>
<FormGroup row>
<Label htmlFor="email-to" sm={2}>
{t('searchTab.emailPopup.labelTo')}
</Label>
<Col sm={10}>
{recipients.pending && <i className="fa fa-spinner fa-pulse m-2" />}
{!recipients.pending && (
<Select.Creatable
multi
value={selectedRecipients}
options={recipientsAll}
onChange={this.changeRecipient}
isValidNewOption={this.isValidNewOption}
promptTextCreator={this.promptTextCreator}
noResultsText="Email not valid"
/>
)}
</Col>
</FormGroup>
<FormGroup row>
<Label htmlFor="email-reply-to" sm={2}>
{t('searchTab.emailPopup.labelReplyTo')}
</Label>
<Col sm={10}>
<Input
type="email"
id="email-reply-to"
defaultValue={replyToEmail}
/>
</Col>
</FormGroup>
<FormGroup row>
<Label htmlFor="email-subject" sm={2}>
{t('searchTab.emailPopup.labelSubject')}
</Label>
<Col sm={10}>
<Input type="text" id="email-subject" />
</Col>
</FormGroup>
<div className="email-popup">
<QuillEditor
className="email-popup__articles email-editor"
reference={this.editorRef}
id="email-editor"
>
{articlesToEmail.map((article) => {
return (
<div className="email-popup__article" key={article.id}>
<h2 className="article__title">
<a href={article.source.link}>{article.title}</a>
</h2>
<div className="article__about-info">
<a href={article.source.link} target="blank">
{article.source.title}
</a>{' '}
<span> | </span>
<a href={article.author.link} target="blank">
{article.author.name}
</a>{' '}
<span> | </span>
{moment(article.published).format('LLL')}
</div>
<p className="article__desc">{article.content}</p>
</div>
)
})}
</QuillEditor>
</div>
</Container>
{this.props.children}
</ModalBody>
<ModalFooter>
<Button color="light" onClick={this.hidePopup}>
{t('common:commonWords.Cancel')}
</Button>
<Button color="primary" onClick={this.onSubmit}>
{t('searchTab.emailPopup.submitBtn')}
</Button>
</ModalFooter>
</Modal>
)
}
}
export default translate(['tabsContent', 'common'], { wait: true })(
EmailArticlesPopup
)
@@ -0,0 +1,50 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'
class EmailConfirmPopup extends React.Component {
static propTypes = {
hidePopup: PropTypes.func.isRequired,
hideEmailPopup: PropTypes.func.isRequired,
sendDocumentsByEmail: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
}
hidePopup = () => {
this.props.hidePopup()
}
onSubmit = () => {
this.props.sendDocumentsByEmail()
this.hidePopup()
this.props.hideEmailPopup()
}
render() {
const { t } = this.props
return (
<Modal isOpen toggle={this.hidePopup} backdrop="static">
<ModalHeader toggle={this.hidePopup}>
{t('common:commonWords.Confirm')}
</ModalHeader>
<ModalBody>
<p>{t('searchTab.emailPopup.sendConfirmWithoutSubject')}</p>
</ModalBody>
<ModalFooter>
<Button color="light" onClick={this.hidePopup}>
{t('searchTab.emailPopup.dontSend')}
</Button>
<Button color="warning" onClick={this.onSubmit}>
{t('searchTab.emailPopup.sendAnyway')}
</Button>
</ModalFooter>
</Modal>
)
}
}
export default translate(['tabsContent', 'common'], { wait: true })(
EmailConfirmPopup
)
@@ -0,0 +1,175 @@
/* eslint-disable react/jsx-no-bind */
import React, { Fragment, useState } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import { translate } from 'react-i18next';
import SearchDatesPopup from './SearchDatesPopup';
import { Modal, Button, ModalHeader, ModalBody } from 'reactstrap';
import { IoIosCalendar } from 'react-icons/io';
// previous commented code
// componentWillMount = () => {
// const { actions, userSubscription } = this.props;
// actions.setSearchLastDate(userSubscription);
// };
export function MediaTypes(props) {
const [modal, setModal] = useState(false);
const {
t,
mediaTypes,
actions,
chosenMediaTypes,
toggleMediaType,
toggleAllMediaTypes,
restrictions
} = props;
const allSelected = mediaTypes.length === chosenMediaTypes.length;
function toggle() {
setModal((modal) => !modal);
}
// set only the allowed media types from restrictions initially
function allowPermissions(mediaType) {
if (!restrictions || !restrictions.plans) {
return false;
}
// for selecting all
if (!mediaType) {
return mediaTypes.every((mt) => restrictions.plans[mt]);
}
return restrictions.plans[mediaType];
}
function toggleSingleType(mediaType, value) {
/* const isFree = restrictions.plans.price === 0;
// TODO: remove following restrictions when duplication fixes
const restrictedTemporary =
isFree && ['news', 'blogs'].includes(mediaType) && value;
if (!allowPermissions(mediaType) || restrictedTemporary) { */
if (!allowPermissions(mediaType)) {
return actions.toggleUpgradeModal();
}
toggleMediaType(mediaType, value); // restrict condition
}
function toggleAllTypes() {
// TODO: remove following restrictions when duplication fixes
/* const isFree = restrictions.plans.price === 0;
if (!allowPermissions() || isFree) { */
if (!allowPermissions()) {
return actions.toggleUpgradeModal();
}
toggleAllMediaTypes(!allSelected);
}
/*
const {
chosenSearchDate,
chosenSearchInterval
chosenStartDate,
chosenEndDate
} = props.searchByFiltersState
const isIntervalBetween = chosenSearchInterval === 'between';
const searchDateBtnText = isIntervalBetween &&
chosenStartDate !== '' ||
isIntervalBetween &&
chosenEndDate !== ''
? chosenSearchDate : t('searchTab.userSubscription.' + chosenSearchDate);
*/
return (
<Fragment>
<div className="d-flex justify-content-between align-items-start">
<div data-tour="select-media-types">
<Button
outline
size="sm"
title={allSelected ? 'Click to deselect' : 'Click to select'}
className="btn-pill mb-2 mr-2 px-3"
color={cx('light', { active: allSelected })}
onClick={toggleAllTypes}
>
{t('searchTab.sourceTypes.all')}
</Button>
{mediaTypes.map((mediaType, i) => {
const isMediaTypeChosen =
chosenMediaTypes.indexOf(mediaType) !== -1;
return (
<Button
key={mediaType}
outline
size="sm"
title={
isMediaTypeChosen ? 'Click to deselect' : 'Click to select'
}
className="btn-pill mb-2 mr-2 px-3"
color={cx('light', {
active: isMediaTypeChosen
})}
onClick={() => toggleSingleType(mediaType, !isMediaTypeChosen)}
>
{t('searchTab.sourceTypes.' + mediaType)}
</Button>
);
})}
</div>
<Button
color="link"
className="ml-2"
onClick={toggle}
data-tour="select-date-range"
>
<IoIosCalendar fontSize="24px" />
{/* {t('searchTab.datesRange')} */}
</Button>
</div>
<Modal isOpen={modal} toggle={toggle} data-tour="date-range-modal">
<ModalHeader toggle={toggle}>Select dates</ModalHeader>
<ModalBody>
<SearchDatesPopup
outsideClickIgnoreClass="react-datepicker"
userSubscription={props.userSubscription}
userSubscriptionDate={props.userSubscriptionDate}
searchIntervals={props.searchByFiltersState.searchIntervals}
searchLastDates={props.searchByFiltersState.searchLastDates}
chosenSearchInterval={
props.searchByFiltersState.chosenSearchInterval
}
chosenSearchLastDate={
props.searchByFiltersState.chosenSearchLastDate
}
chosenStartDate={props.searchByFiltersState.chosenStartDate}
chosenEndDate={props.searchByFiltersState.chosenEndDate}
hideSearchDatesPopup={toggle}
setSearchInterval={actions.setSearchInterval}
setSearchLastDate={actions.setSearchLastDate}
setSearchDate={actions.setSearchDate}
setStartDate={actions.setStartDate}
setEndDate={actions.setEndDate}
/>
</ModalBody>
</Modal>
</Fragment>
);
}
MediaTypes.propTypes = {
t: PropTypes.func.isRequired,
mediaTypes: PropTypes.array.isRequired,
chosenMediaTypes: PropTypes.array.isRequired,
toggleMediaType: PropTypes.func.isRequired,
toggleAllMediaTypes: PropTypes.func.isRequired,
restrictions: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,
userSubscriptionDate: PropTypes.string.isRequired,
userSubscription: PropTypes.string.isRequired,
searchByFiltersState: PropTypes.object.isRequired
};
export default translate(['tabsContent'], { wait: true })(MediaTypes);
@@ -0,0 +1,94 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import FiltersTable from '../../../../common/FiltersTable/FiltersTable'
import { Button } from 'reactstrap'
export class RefinePanel extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
advancedFilters: PropTypes.object.isRequired,
selectedFilters: PropTypes.object.isRequired,
clearPending: PropTypes.object.isRequired,
filterPages: PropTypes.object.isRequired,
onRefine: PropTypes.func.isRequired,
actions: PropTypes.object.isRequired
};
onHiderClick = (e) => {
e.preventDefault()
this.props.actions.toggleRefinePanel()
};
onSelectFilter = (groupName, filterValue) => {
this.props.actions.selectRefineFilter(groupName, filterValue)
};
onClearFilters = (groupName) => {
this.props.actions.clearRefineFilters(groupName)
};
onClearAllFilters = () => {
this.props.actions.clearAllRefineFilters()
};
onMoreFilters = (groupName) => {
this.props.actions.loadMoreRefineFilters(groupName)
};
onLessFilters = (groupName) => {
this.props.actions.loadLessRefineFilters(groupName)
};
/* onPressEnter = (e) => {
if (e.keyCode === 13) {
const keyword = document.getElementById('refine-keyword').value
this.props.actions.selectRefineFilter('keyword', keyword)
setTimeout(() => {
this.props.onRefine()
})
}
}; */
render () {
return (
<div className="refine-panel px-4">
<Button
color="light"
title="Hide refine panel"
className="d-block ml-auto mb-3 btn-icon"
onClick={this.onHiderClick}
>
{this.props.t('searchTab.hide')}
</Button>
{/* <Input
type="text"
className="mb-2"
id="refine-keyword"
placeholder={this.props.t('common:advancedFilters.keywordRefine')}
onKeyUp={this.onPressEnter}
/> */}
<FiltersTable
filters={this.props.advancedFilters}
selectedFilters={this.props.selectedFilters}
clearPending={this.props.clearPending}
pages={this.props.filterPages}
callbacks={{
'selectFilter': this.onSelectFilter,
'clearFilters': this.onClearFilters,
'clearAllFilters': this.onClearAllFilters,
'moreFilters': this.onMoreFilters,
'lessFilters': this.onLessFilters,
'refine': this.props.onRefine
}}
/>
</div>
)
}
}
export default translate(['tabsContent'], { wait: true })(RefinePanel)
@@ -0,0 +1,154 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import {
Button,
Modal,
ModalHeader,
ModalBody,
Label,
Input,
ModalFooter,
FormGroup
} from 'reactstrap'
export class SaveFeedPopup extends React.Component {
static propTypes = {
feedCategories: PropTypes.array.isRequired,
saveType: PropTypes.string.isRequired,
toggleSaveFeedPopup: PropTypes.func.isRequired,
addAlert: PropTypes.func.isRequired,
onSaveAsFeed: PropTypes.func.isRequired,
getSidebarCategories: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
}
constructor(props) {
super(props)
this.state = {
isFeedNameError: false,
feedCategoriesKeys: [],
feedName: '',
selectCategory: ''
}
}
componentWillMount = () => {
let nestingCount = -1
this.getCategoriesKeys(this.props.feedCategories, nestingCount)
}
//function that generates new array of categories without nesting
getCategoriesKeys = (categories, nestingCount) => {
nestingCount += 1
categories.forEach((category) => {
if (category.subType === 'deleted_content') return false
const categoryName = '-'.repeat(nestingCount) + ' ' + category.name
const feedCategoriesKeys = this.state.feedCategoriesKeys
feedCategoriesKeys.push({ id: category.id, name: categoryName })
this.setState({
feedCategoriesKeys: feedCategoriesKeys,
selectCategory: feedCategoriesKeys[0].id.toString()
})
if (category.childes.length) {
this.getCategoriesKeys(category.childes, nestingCount)
}
})
}
changeHandler = (e) => {
const { name, value } = e.target
this.setState({ [name]: value })
}
hidePopupFromOutside = (e) => {
if (e.target === e.currentTarget) this.hidePopup()
}
hidePopup = () => {
this.props.toggleSaveFeedPopup()
}
onSubmit = () => {
const { feedName: name, selectCategory: category } = this.state
if (!name || !name.trim()) {
this.setState({ isFeedNameError: true })
return false
}
this.props.onSaveAsFeed(name, category)
this.hidePopup()
}
render() {
const { t } = this.props
const {
feedCategoriesKeys,
isFeedNameError,
feedName,
selectCategory
} = this.state
return (
<Modal isOpen toggle={this.hidePopup} backdrop="static" data-tour="feed-save-modal">
<ModalHeader toggle={this.hidePopup}>
{t('searchTab.saveFeedPopup.' + this.props.saveType)}
</ModalHeader>
<ModalBody>
<FormGroup>
<Label>
{t('searchTab.saveFeedPopup.nameLabel')}<span className="text-danger">*</span>
</Label>
<Input
name="feedName"
type="text"
value={feedName}
onChange={this.changeHandler}
/>
{isFeedNameError && (
<p className="text-danger">
{t('searchTab.saveFeedPopup.feedNameErrorMsg')}
</p>
)}
</FormGroup>
<FormGroup>
<Label>
{t('searchTab.saveFeedPopup.folderLabel')}<span className="text-danger">*</span>
</Label>
<Input
name="selectCategory"
type="select"
value={selectCategory}
onChange={this.changeHandler}
>
{feedCategoriesKeys.map((category) => {
return (
<option key={category.id} value={category.id}>
{category.name}
</option>
)
})}
</Input>
</FormGroup>
</ModalBody>
<ModalFooter>
<Button color="light" onClick={this.hidePopup}>
{t('common:commonWords.Cancel')}
</Button>
<Button color="primary" onClick={this.onSubmit}>
{t('searchTab.saveBtn')}
</Button>
</ModalFooter>
</Modal>
)
}
}
export default translate(['tabsContent', 'common'], { wait: true })(
SaveFeedPopup
)
@@ -0,0 +1,118 @@
import React from 'react'
import PropTypes from 'prop-types'
import { DateRangePicker } from 'react-dates'
import moment from 'moment'
import { getMomentObject } from '../../../../../../common/helper'
export class BetweenDatepickers extends React.Component {
state = {}
static propTypes = {
chosenSearchInterval: PropTypes.string.isRequired,
chosenStartDate: PropTypes.string.isRequired,
chosenEndDate: PropTypes.string.isRequired,
setSearchInterval: PropTypes.func.isRequired,
setSearchDate: PropTypes.func.isRequired,
setStartDate: PropTypes.func.isRequired,
minDate: PropTypes.object,
setEndDate: PropTypes.func.isRequired
}
swapDate = (startDate, endDate) => {
if (startDate.isAfter(endDate)) {
const temp = startDate
startDate = endDate
endDate = temp
}
return { startDate, endDate }
}
/*
setDates = (date, isStartDate) => {
const {
chosenStartDate,
chosenEndDate,
setStartDate,
setEndDate,
setSearchDate
} = this.props
const hasStartDate = !!chosenStartDate
const hasEndDate = !!chosenEndDate
let startDate = hasStartDate ? moment(chosenStartDate) : moment()
let endDate = hasEndDate ? moment(chosenEndDate) : moment()
startDate = isStartDate ? date : startDate
endDate = !isStartDate ? date : endDate
const swappedDate = this.swapDate(startDate, endDate)
startDate = swappedDate.startDate.format('YYYY-MM-DD')
endDate = swappedDate.endDate.format('YYYY-MM-DD')
setStartDate(startDate.format('YYYY-MM-DD'))
setEndDate(endDate.format('YYYY-MM-DD'))
const endDateLabel = hasEndDate ? endDate : 'now'
const startDateLabel = hasStartDate ? startDate : 'until'
let label = isStartDate
? `${startDate} - ${endDateLabel}`
: `${startDateLabel} - ${endDate}`
setSearchDate(label)
} */
setBetweenInterval = () => {
const { chosenSearchInterval, setSearchInterval } = this.props
if (chosenSearchInterval === 'between') return false
setSearchInterval('between')
}
handleDateChange = ({ startDate, endDate }) => {
const { setStartDate, setEndDate } = this.props
setStartDate(startDate ? startDate.format('YYYY-MM-DD') : null)
setEndDate(endDate ? endDate.format('YYYY-MM-DD') : null)
if (startDate && endDate) {
this.setBetweenInterval()
}
}
onFocusChange = (focus) => {
this.setState({ focusedInput: focus })
}
isOutsideRange = (date) => {
const today = moment()
return date.isAfter(today) || date.isBefore(this.props.minDate)
}
render() {
const { chosenStartDate, chosenEndDate } = this.props
const today = moment()
const startDate = getMomentObject(chosenStartDate)
const endDate = getMomentObject(chosenEndDate)
return (
<div className="ml-3">
<DateRangePicker
startDateId="startDate"
endDateId="endDate"
startDate={startDate}
endDate={endDate}
onDatesChange={this.handleDateChange}
focusedInput={this.state.focusedInput}
onFocusChange={this.onFocusChange}
displayFormat="MM/DD/YYYY"
startDatePlaceholderText="Start Date"
endDatePlaceholderText="End Date"
numberOfMonths={1}
maxDate={today}
// eslint-disable-next-line react/jsx-no-bind
isOutsideRange={this.isOutsideRange}
/>
</div>
)
}
}
export default BetweenDatepickers
@@ -0,0 +1,33 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { Col, CustomInput, FormGroup } from 'reactstrap';
export class DuplicatesTab extends React.Component {
static propTypes = {
includeDuplicates: PropTypes.bool.isRequired,
toggleIncludeDuplicates: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
render() {
const { t } = this.props;
return (
<Col sm={12}>
<FormGroup>
<CustomInput
className="checkbox-input-hidden"
type="checkbox"
id="duplicates-check"
checked={this.props.includeDuplicates}
onChange={this.props.toggleIncludeDuplicates}
label={t('searchTab.searchBySection.duplicates.includeDuplicates')}
/>
</FormGroup>
</Col>
);
}
}
export default translate(['tabsContent'], { wait: true })(DuplicatesTab);
@@ -0,0 +1,53 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { Col, FormGroup, Input, Label } from 'reactstrap';
export class EmphasisTab extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
include: PropTypes.string.isRequired,
exclude: PropTypes.string.isRequired,
setHeadlineIncluded: PropTypes.func.isRequired,
setHeadlineExcluded: PropTypes.func.isRequired
};
setHeadInclude = (e) => {
const headline = e.target.value;
this.props.setHeadlineIncluded(headline);
};
setHeadExclude = (e) => {
const headline = e.target.value;
this.props.setHeadlineExcluded(headline);
};
render() {
const { t, include, exclude } = this.props;
return (
<Fragment>
<Col sm="6">
<FormGroup>
<Label>
{t('searchTab.searchBySection.emphasis.headlineLabel')}{' '}
{t('searchTab.searchBySection.emphasis.include')}
</Label>
<Input type="text" value={include} onChange={this.setHeadInclude} />
</FormGroup>
</Col>
<Col sm="6">
<FormGroup>
<Label>
{t('searchTab.searchBySection.emphasis.headlineLabel')}{' '}
{t('searchTab.searchBySection.emphasis.exclude')}
</Label>
<Input type="text" value={exclude} onChange={this.setHeadExclude} />
</FormGroup>
</Col>
</Fragment>
);
}
}
export default translate(['tabsContent'], { wait: true })(EmphasisTab);
@@ -0,0 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { Col, CustomInput, FormGroup } from 'reactstrap';
function ExtrasTab({ t, hasImages, toggleHasImages }) {
return (
<Col sm={12}>
<FormGroup>
<CustomInput
id="has-images-check"
type="checkbox"
className="d-flex"
checked={hasImages}
label={t('searchTab.searchBySection.extras.hasImages')}
onChange={toggleHasImages}
/>
</FormGroup>
</Col>
);
}
ExtrasTab.propTypes = {
hasImages: PropTypes.bool.isRequired,
toggleHasImages: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
export default translate(['tabsContent'], { wait: true })(ExtrasTab);
@@ -0,0 +1,51 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { Col, CustomInput } from 'reactstrap';
export class LangsTab extends React.Component {
static propTypes = {
chosenLanguages: PropTypes.array.isRequired,
searchLanguages: PropTypes.array.isRequired,
toggleLang: PropTypes.func.isRequired,
toggleAllLangs: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
toggleLangs = ({ target: { id, checked } }) => {
this.props.toggleLang(id, checked);
};
toggleAllLangs = (e) => {
this.props.toggleAllLangs(e.target.checked);
};
render() {
const { t } = this.props;
const { searchLanguages, chosenLanguages } = this.props;
return (
<Col sm={12} className="search-by-lang">
<CustomInput
id="article-check-all"
type="checkbox"
label={t('common:language.all')}
checked={searchLanguages.length === chosenLanguages.length}
onChange={this.toggleAllLangs}
/>
{searchLanguages.map((lang) => (
<CustomInput
key={lang}
id={lang}
type="checkbox"
checked={chosenLanguages.indexOf(lang) !== -1}
label={t('common:language.' + lang)}
onChange={this.toggleLangs}
/>
))}
</Col>
);
}
}
export default translate(['tabsContent'], { wait: true })(LangsTab);
@@ -0,0 +1,61 @@
import React from 'react'
import PropTypes from 'prop-types'
import { DragSource } from 'react-dnd'
const Types = {
LOC: 'location'
}
const locationSource = {
beginDrag (props) {
// Return the data describing the dragged item
return { oldDropTargetType: props.dropTargetType }
},
endDrag (props, monitor, component) {
// When dropped on a compatible target, do something
if (monitor.getDropResult() !== null) {
const locFrom = props.dropTargetType
const locTo = monitor.getDropResult().newDropTargetType
const locationType = props.locationType
const location = props.location
props.moveLocation(locFrom, locTo, locationType, location)
}
}
}
/**
* Specifies which props to inject into your component.
*/
function collectDragSource (connect) {
return {
// Call this function inside render()
// to let React DnD handle the drag events:
connectDragSource: connect.dragSource()
}
}
export class LocationsTabList extends React.Component {
static propTypes = {
location: PropTypes.object.isRequired,
dropTargetType: PropTypes.string.isRequired,
moveLocation: PropTypes.func.isRequired,
connectDragSource: PropTypes.func.isRequired
};
render () {
const { connectDragSource } = this.props
const { location } = this.props
return connectDragSource(
<li className="list-group-item cursor-move p-2">
<span className="drag-handle" />
{location.name}
</li>
)
}
}
export default DragSource(Types.LOC, locationSource, collectDragSource)(LocationsTabList)
@@ -0,0 +1,111 @@
/* eslint-disable react/jsx-no-bind */
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import LocationsTabList from './LocationsTabList';
import { Button, Col, Row } from 'reactstrap';
export class LocationsTab extends React.Component {
static propTypes = {
locations: PropTypes.array.isRequired,
locationsToInclude: PropTypes.array.isRequired,
locationsToExclude: PropTypes.array.isRequired,
chosenLocationsType: PropTypes.string.isRequired,
changeLocationsType: PropTypes.func.isRequired,
moveLocation: PropTypes.func.isRequired,
clearLocations: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
constructor(props) {
super(props);
this.state = {
dropdownOpen: false,
dropDownValue: 'country'
};
}
onClearLocations = () => {
this.props.clearLocations();
this.props.changeLocationsType('country');
this.setState({ dropDownValue: 'country' });
};
selectLocation = (value) => {
this.props.changeLocationsType(value);
this.setState({ dropDownValue: value });
};
render() {
const {
locations,
chosenLocationsType,
locationsToInclude,
locationsToExclude
} = this.props;
const { t } = this.props;
const locationsMainList = locations.filter((loc) => {
return loc.type === chosenLocationsType;
});
const includeList = locationsToInclude.filter((loc) => {
return loc.type === chosenLocationsType;
});
const excludeList = locationsToExclude.filter((loc) => {
return loc.type === chosenLocationsType;
});
const { dropDownValue } = this.state;
return (
<Col sm={12}>
<Button
outline
active={dropDownValue === 'country'}
color="secondary"
className="mr-2 mb-3"
onClick={() => this.selectLocation('country')}
>
{t('searchTab.searchBySection.locations.countriesSelect')}
</Button>
<Button
outline
active={dropDownValue === 'state'}
color="secondary"
className="mb-3"
onClick={() => this.selectLocation('state')}
>
{t('searchTab.searchBySection.locations.statesSelect')}
</Button>
<Row className="draggable">
<Col md={4}>
<LocationsTabList
locations={locationsMainList}
chosenLocationsType={chosenLocationsType}
dropTargetType="locations"
moveLocation={this.props.moveLocation}
/>
</Col>
<Col md={4}>
<LocationsTabList
locations={includeList}
chosenLocationsType={chosenLocationsType}
dropTargetType="locationsToInclude"
moveLocation={this.props.moveLocation}
/>
</Col>
<Col md={4}>
<LocationsTabList
locations={excludeList}
chosenLocationsType={chosenLocationsType}
dropTargetType="locationsToExclude"
moveLocation={this.props.moveLocation}
/>
</Col>
</Row>
</Col>
);
}
}
export default translate(['tabsContent'], { wait: true })(LocationsTab);
@@ -0,0 +1,94 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { DropTarget } from 'react-dnd';
import flow from 'lodash/flow';
import LocationItem from './LocationItem';
import {
ListGroup
} from 'reactstrap';
const targetTypes = ['location'];
const locationListTarget = {
drop(props, monitor, component) {
if (monitor.didDrop()) {
//check whether some nested
// target already handled drop
return;
}
return { newDropTargetType: props.dropTargetType };
},
canDrop(props, monitor) {
return props.dropTargetType !== monitor.getItem().oldDropTargetType;
}
};
function collectDropTarget(connect, monitor) {
return {
// Call this function inside render()
// to let React DnD handle the drag events:
connectDropTarget: connect.dropTarget(),
// You can ask the monitor about the current drag state:
itemType: monitor.getItemType()
};
}
export class LocationsTabList extends React.Component {
static propTypes = {
locations: PropTypes.array.isRequired,
chosenLocationsType: PropTypes.string.isRequired,
dropTargetType: PropTypes.string.isRequired,
moveLocation: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
connectDropTarget: PropTypes.func.isRequired
};
render() {
const { locations, chosenLocationsType, dropTargetType } = this.props;
const { t } = this.props;
const { connectDropTarget } = this.props;
locations.forEach((location) => {
location.name = t('common:' + location.type + '.' + location.code);
});
const sortedLocations = locations.sort((a, b) => {
const nameA = a.name.toLowerCase();
const nameB = b.name.toLowerCase();
if (nameA < nameB) {
//sort string ascending
return -1;
}
if (nameA > nameB) {
return 1;
}
return 0;
});
return connectDropTarget(
<div className="scroll-area-md border b-radius-5">
<p className="text-muted border-bottom p-2">{t('searchTab.searchBySection.locations.' + dropTargetType)}</p>
<ListGroup className="p-2">
{sortedLocations.map((location, i) => {
return (
<LocationItem
key={'location-' + i}
location={location}
dropTargetType={dropTargetType}
locationType={chosenLocationsType}
moveLocation={this.props.moveLocation}
/>
);
})}
</ListGroup>
</div>
);
}
}
export default flow(
DropTarget(targetTypes, locationListTarget, collectDropTarget),
translate(['tabsContent'], { wait: true })
)(LocationsTabList);
@@ -0,0 +1,163 @@
import React from 'react';
import PropTypes from 'prop-types';
import SearchByTabs from './SearchByTabs';
import EmphasisTab from './EmphasisTab';
import LangsTab from './LangsTab';
import LocationsTab from './LocationsTab';
import SourcesTab from './SourcesTab';
import SourceListsTab from './SourceListsTab';
import DuplicatesTab from './DuplicatesTab';
import ExtrasTab from './ExtrasTab';
import { translate } from 'react-i18next';
import { Button, Container, Row } from 'reactstrap';
export class SearchBy extends React.Component {
static propTypes = {
userSubscriptionDate: PropTypes.string.isRequired,
userSubscription: PropTypes.string.isRequired,
searchByFiltersState: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
};
constructor(props) {
super(props);
this.state = {
animationDisabled: true,
arrowPosition: true
};
}
onToggleSearchBy = () => {
this.props.actions.toggleSearchBy();
};
render() {
const { t } = this.props;
const { searchByFiltersState, actions } = this.props;
const visibleClass = searchByFiltersState.isSearchByVisible
? ' visible'
: ' closed';
return (
<div
className={'search-by-container mb-3 mb-md-0' + visibleClass}
data-tour="advanced-search"
>
<div className="search-by">
<SearchByTabs
searchByTabs={searchByFiltersState.searchByTabs}
chooseSearchByTab={actions.chooseSearchByTab}
chosenSearchByTab={searchByFiltersState.chosenSearchByTab}
/>
<Container fluid>
<Row className="mb-3" data-tour="advanced-search-content">
{searchByFiltersState.chosenSearchByTab === 'emphasis' && (
<EmphasisTab
include={searchByFiltersState.headlineIncluded}
exclude={searchByFiltersState.headlineExcluded}
setHeadlineIncluded={actions.setHeadlineIncluded}
setHeadlineExcluded={actions.setHeadlineExcluded}
/>
)}
{searchByFiltersState.chosenSearchByTab === 'languages' && (
<LangsTab
searchLanguages={searchByFiltersState.searchLanguages}
chosenLanguages={searchByFiltersState.chosenLanguages}
toggleLang={actions.toggleLang}
toggleAllLangs={actions.toggleAllLangs}
/>
)}
{searchByFiltersState.chosenSearchByTab === 'locations' && (
<LocationsTab
locations={searchByFiltersState.locations}
chosenLocationsType={searchByFiltersState.chosenLocationsType}
locationsToInclude={searchByFiltersState.locationsToInclude}
locationsToExclude={searchByFiltersState.locationsToExclude}
changeLocationsType={actions.changeLocationsType}
moveLocation={actions.moveLocation}
clearLocations={actions.clearLocations}
/>
)}
{searchByFiltersState.chosenSearchByTab === 'sources' && (
<SourcesTab
chosenMediaTypes={searchByFiltersState.chosenMediaTypes}
chosenLanguages={searchByFiltersState.chosenLanguages}
searchBySources={searchByFiltersState.searchBySources}
searchBySourcesType={searchByFiltersState.searchBySourcesType}
selectedSearchBySources={
searchByFiltersState.selectedSearchBySources
}
searchBySourcesQuery={
searchByFiltersState.searchBySourcesQuery
}
setSearchBySourcesQuery={actions.setSearchBySourcesQuery}
getSearchBySources={actions.getSearchBySources}
addSelectedSearchBySource={actions.addSelectedSearchBySource}
removeSelectedSearchBySource={
actions.removeSelectedSearchBySource
}
clearSearchBySources={actions.clearSearchBySources}
includeExcludeSearchBySources={
actions.includeExcludeSearchBySources
}
/>
)}
{searchByFiltersState.chosenSearchByTab === 'sourceLists' && (
<SourceListsTab
searchBySourceLists={
searchByFiltersState.searchBySourceListsAvailable
}
searchBySourceListsToInclude={
searchByFiltersState.searchBySourceListsToInclude
}
searchBySourceListsToExclude={
searchByFiltersState.searchBySourceListsToExclude
}
getSourceLists={actions.getSearchBySourceLists}
moveSourceList={actions.moveSourceList}
/>
)}
{searchByFiltersState.chosenSearchByTab === 'duplicates' && (
<DuplicatesTab
includeDuplicates={searchByFiltersState.includeDuplicates}
toggleIncludeDuplicates={actions.toggleIncludeDuplicates}
/>
)}
{searchByFiltersState.chosenSearchByTab === 'extras' && (
<ExtrasTab
hasImages={searchByFiltersState.hasImages}
toggleHasImages={actions.toggleHasImages}
/>
)}
</Row>
</Container>
</div>
<hr className="mt-0 mb-2" />
<Button
outline
size="sm"
className="font-size-xs"
color="secondary"
onClick={this.onToggleSearchBy}
>
{t('searchTab.searchBySection.searchByBtn')}
{searchByFiltersState.isSearchByVisible ? (
<i className="lnr-chevron-up btn-icon-wrapper"></i>
) : (
<i className="lnr-chevron-down btn-icon-wrapper"></i>
)}
</Button>
</div>
);
}
}
export default translate(['tabsContent'], { wait: true })(SearchBy);
@@ -0,0 +1,40 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Nav, NavLink, NavItem } from 'reactstrap';
import { translate } from 'react-i18next';
export class SearchByTabs extends React.Component {
static propTypes = {
searchByTabs: PropTypes.array.isRequired,
chosenSearchByTab: PropTypes.string.isRequired,
chooseSearchByTab: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
chooseSearchByTab = (newTab) => () => {
this.props.chooseSearchByTab(newTab);
};
render() {
const { searchByTabs } = this.props;
const { t } = this.props;
return (
<Nav tabs className="font-size-xs">
{searchByTabs.map((tab, i) => (
<NavItem key={tab}>
<NavLink
className="d-block"
active={tab === this.props.chosenSearchByTab}
onClick={this.chooseSearchByTab(tab)}
>
{t('searchTab.searchBySection.' + tab + '.title')}
</NavLink>
</NavItem>
))}
</Nav>
);
}
}
export default translate(['tabsContent'], { wait: true })(SearchByTabs);
@@ -0,0 +1,25 @@
import React from 'react'
import PropTypes from 'prop-types'
export class SourceIcon extends React.Component {
static propTypes = {
type: PropTypes.string.isRequired
};
acceptedTypes = ['blogs', 'clippings', 'forums', 'mixed', 'news', 'prints', 'socials', 'user-added', 'user-comments', 'videos'];
render () {
const { type } = this.props
if (!this.acceptedTypes.includes(type)) {
return null
}
return (
<img src={require('../../../../../../images/feed-type-' + type + '.png')} className="source-icon" />
)
}
}
export default SourceIcon
@@ -0,0 +1,56 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import SourceListsTabList from './SourceListsTabList';
import { Col } from 'reactstrap';
export class SourceListsTab extends React.Component {
static propTypes = {
searchBySourceLists: PropTypes.array.isRequired,
searchBySourceListsToInclude: PropTypes.array.isRequired,
searchBySourceListsToExclude: PropTypes.array.isRequired,
getSourceLists: PropTypes.func.isRequired,
moveSourceList: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
componentWillMount = () => {
this.props.getSourceLists({ page: 1, limit: 25 });
};
render() {
const {
searchBySourceLists,
searchBySourceListsToInclude,
searchBySourceListsToExclude
} = this.props;
return (
<Fragment>
<Col md={4}>
<SourceListsTabList
sourceLists={searchBySourceLists}
dropTargetType="searchBySourceListsAvailable"
moveSourceList={this.props.moveSourceList}
/>
</Col>
<Col md={4}>
<SourceListsTabList
sourceLists={searchBySourceListsToInclude}
dropTargetType="searchBySourceListsToInclude"
moveSourceList={this.props.moveSourceList}
/>
</Col>
<Col md={4}>
<SourceListsTabList
sourceLists={searchBySourceListsToExclude}
dropTargetType="searchBySourceListsToExclude"
moveSourceList={this.props.moveSourceList}
/>
</Col>
</Fragment>
);
}
}
export default translate(['tabsContent'], { wait: true })(SourceListsTab);
@@ -0,0 +1,64 @@
import React from 'react';
import PropTypes from 'prop-types';
import { DragSource } from 'react-dnd';
const Types = {
SOURCE_LIST: 'sourceList'
};
const sourceListSource = {
beginDrag(props) {
// Return the data describing the dragged item
return { oldDropTargetType: props.dropTargetType };
},
endDrag(props, monitor, component) {
// When dropped on a compatible target, do something
if (monitor.getDropResult() !== null) {
const from = props.dropTargetType;
const to = monitor.getDropResult().newDropTargetType;
const sourceList = props.sourceList;
props.moveSourceList(from, to, sourceList);
}
}
};
/**
* Specifies which props to inject into your component.
*/
function collectDragSource(connect) {
return {
// Call this function inside render()
// to let React DnD handle the drag events:
connectDragSource: connect.dragSource()
};
}
export class SourceListsTabItem extends React.Component {
static propTypes = {
sourceList: PropTypes.func.isRequired,
dropTargetType: PropTypes.string.isRequired,
moveSourceList: PropTypes.func.isRequired,
connectDragSource: PropTypes.func.isRequired
};
render() {
const { connectDragSource } = this.props;
const { sourceList } = this.props;
return connectDragSource(
<li className="list-group-item cursor-move p-2">
<span className="drag-handle" />
{sourceList.name}
</li>
);
}
}
export default DragSource(
Types.SOURCE_LIST,
sourceListSource,
collectDragSource
)(SourceListsTabItem);
@@ -0,0 +1,75 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { DropTarget } from 'react-dnd';
import flow from 'lodash/flow';
import SourceListsTabItem from './SourceListsTabItem';
import { ListGroup } from 'reactstrap';
const targetTypes = ['sourceList'];
const sourceListTarget = {
drop(props, monitor, component) {
if (monitor.didDrop()) {
//check whether some nested
// target already handled drop
return;
}
return { newDropTargetType: props.dropTargetType };
},
canDrop(props, monitor) {
return props.dropTargetType !== monitor.getItem().oldDropTargetType;
}
};
function collectDropTarget(connect, monitor) {
return {
// Call this function inside render()
// to let React DnD handle the drag events:
connectDropTarget: connect.dropTarget(),
// You can ask the monitor about the current drag state:
itemType: monitor.getItemType()
};
}
export class SourceListsTabList extends React.Component {
static propTypes = {
sourceLists: PropTypes.array.isRequired,
dropTargetType: PropTypes.string.isRequired,
moveSourceList: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
connectDropTarget: PropTypes.func.isRequired
};
render() {
const { sourceLists, dropTargetType } = this.props;
const { t } = this.props;
const { connectDropTarget } = this.props;
return connectDropTarget(
<div className="draggable scroll-area-md border b-radius-5">
<p className="text-muted border-bottom p-2">
{t('searchTab.searchBySection.sourceLists.' + dropTargetType)}
</p>
<ListGroup className="p-2">
{sourceLists.map((sourceList, i) => {
return (
<SourceListsTabItem
key={'sourceList-' + i}
sourceList={sourceList}
dropTargetType={dropTargetType}
moveSourceList={this.props.moveSourceList}
/>
);
})}
</ListGroup>
</div>
);
}
}
export default flow(
DropTarget(targetTypes, sourceListTarget, collectDropTarget),
translate(['tabsContent'], { wait: true })
)(SourceListsTabList);
@@ -0,0 +1,67 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import SourcesTabAvailSources from './SourcesTabAvailSources';
import SourcesTabSelectedSources from './SourcesTabSelectedSources';
import { Col } from 'reactstrap';
export class SourcesTab extends React.Component {
static propTypes = {
chosenMediaTypes: PropTypes.array.isRequired,
chosenLanguages: PropTypes.array.isRequired,
searchBySources: PropTypes.array.isRequired,
selectedSearchBySources: PropTypes.array.isRequired,
searchBySourcesType: PropTypes.string.isRequired,
searchBySourcesQuery: PropTypes.string.isRequired,
setSearchBySourcesQuery: PropTypes.func.isRequired,
getSearchBySources: PropTypes.func.isRequired,
addSelectedSearchBySource: PropTypes.func.isRequired,
removeSelectedSearchBySource: PropTypes.func.isRequired,
clearSearchBySources: PropTypes.func.isRequired,
includeExcludeSearchBySources: PropTypes.func.isRequired
};
render() {
const {
searchBySourcesQuery,
setSearchBySourcesQuery,
chosenMediaTypes,
chosenLanguages,
searchBySources,
getSearchBySources,
addSelectedSearchBySource,
searchBySourcesType,
clearSearchBySources,
selectedSearchBySources,
removeSelectedSearchBySource,
includeExcludeSearchBySources
} = this.props;
return (
<Fragment>
<Col sm={8}>
<SourcesTabAvailSources
searchBySourcesQuery={searchBySourcesQuery}
selectedSources={selectedSearchBySources}
setSearchBySourcesQuery={setSearchBySourcesQuery}
chosenMediaTypes={chosenMediaTypes}
chosenLanguages={chosenLanguages}
availSources={searchBySources}
getSearchBySources={getSearchBySources}
addSelectedSearchBySource={addSelectedSearchBySource}
/>
</Col>
<Col sm={4}>
<SourcesTabSelectedSources
searchBySourcesType={searchBySourcesType}
clearSearchBySources={clearSearchBySources}
selectedSources={selectedSearchBySources}
removeSelectedSearchBySource={removeSelectedSearchBySource}
includeExcludeSearchBySources={includeExcludeSearchBySources}
/>
</Col>
</Fragment>
);
}
}
export default SourcesTab;
@@ -0,0 +1,160 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
// import SourceIcon from './SourceIcon';
import { Button, Input, InputGroup, InputGroupAddon, Table } from 'reactstrap';
import { capitalize } from 'lodash';
import { getTitle } from '../../../../../../common/helper';
import cx from 'classnames';
import { domainNames } from '../SearchSubTab';
export class SourcesTabAvailSources extends React.Component {
static propTypes = {
chosenMediaTypes: PropTypes.array.isRequired,
chosenLanguages: PropTypes.array.isRequired,
availSources: PropTypes.array.isRequired,
selectedSources: PropTypes.array.isRequired,
searchBySourcesQuery: PropTypes.string.isRequired,
setSearchBySourcesQuery: PropTypes.func.isRequired,
getSearchBySources: PropTypes.func.isRequired,
addSelectedSearchBySource: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
componentDidMount = () => {
this.searchSources();
};
searchSources = () => {
const {
chosenLanguages,
chosenMediaTypes,
getSearchBySources,
searchBySourcesQuery
} = this.props;
const query = searchBySourcesQuery;
const dataToSend = {};
dataToSend.page = 1;
dataToSend.limit = 100;
dataToSend.query = query;
dataToSend.filters = {};
const source = []
const domain = []
chosenMediaTypes.map((v) => {
if (domainNames.includes(v)) {
domain.push(`${v}.com`);
} else {
source.push(v);
}
})
dataToSend.filters.publisher = { source, domain };
dataToSend.filters.language = chosenLanguages;
getSearchBySources(dataToSend);
};
chooseSource = (e) => {
const dataset = e.currentTarget.dataset;
const sourceTitle = dataset.sourceTitle;
const sourceType = dataset.sourceType;
const sourceId = dataset.sourceId;
this.props.addSelectedSearchBySource({
title: sourceTitle,
type: sourceType,
id: sourceId
});
};
onChangeSearchInput = (e) => {
const val = e.target.value;
this.props.setSearchBySourcesQuery(val);
};
onEnterSearchInput = (e) => {
if (e.keyCode === 13) this.searchSources();
};
render() {
const { availSources, selectedSources } = this.props;
const { t } = this.props;
return (
<Fragment>
<InputGroup className="mb-3">
<Input
type="text"
id="search-by-sources-input"
value={this.props.searchBySourcesQuery}
onChange={this.onChangeSearchInput}
onKeyUp={this.onEnterSearchInput}
/>
<InputGroupAddon addonType="append">
<Button
color="primary"
className="btn-icon btn-icon-only"
onClick={this.searchSources}
>
<i className="lnr-magnifier btn-icon-wrapper"></i>
</Button>
</InputGroupAddon>
</InputGroup>
<p className="text-muted">
{t('searchTab.searchBySection.sources.availSources')}
</p>
<div className="source-table-wrap border">
<Table striped bordered className="mb-0">
<thead>
<tr>
<th>{t('searchTab.searchBySection.sources.source')}</th>
<th>{t('searchTab.searchBySection.sources.siteType')}</th>
<th>{t('searchTab.searchBySection.sources.mediatype')}</th>
<th>{t('searchTab.searchBySection.sources.lang')}</th>
</tr>
</thead>
<tbody>
{availSources.length > 0 ? (
availSources.map((source, i) => {
return (
<tr
title="Click to select"
className={cx('clickable', {
active:
selectedSources &&
selectedSources.find((v) => v.id === source.id)
})}
data-source-title={source.title}
data-source-type={source.type}
data-source-id={source.id}
onClick={this.chooseSource}
key={i}
>
{/* <td>
<SourceIcon type={source.type} />
</td> */}
<td>{getTitle(source.title)}</td>
<td title={source.url}>
{capitalize(source.siteType) || '-'}
</td>
<td>{capitalize(source.type) || '-'}</td>
<td>{t(`common:language.${source.lang}`)}</td>
</tr>
);
})
) : (
<tr className="p-4 text-center text-black-50">
<td colSpan="4">{t('common:messages.noRows')}</td>
</tr>
)}
</tbody>
</Table>
</div>
</Fragment>
);
}
}
export default translate(['tabsContent'], { wait: true })(
SourcesTabAvailSources
);
@@ -0,0 +1,122 @@
/* eslint-disable react/jsx-no-bind */
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { Button, CustomInput, Table } from 'reactstrap';
import { IoIosCloseCircleOutline } from 'react-icons/io';
import { capitalize } from 'lodash';
import { getTitle } from '../../../../../../common/helper';
export class SourcesTabSelectedSources extends React.Component {
static propTypes = {
searchBySourcesType: PropTypes.string.isRequired,
selectedSources: PropTypes.array.isRequired,
removeSelectedSearchBySource: PropTypes.func.isRequired,
clearSearchBySources: PropTypes.func.isRequired,
includeExcludeSearchBySources: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
removeSource = (sourceId) => {
this.props.removeSelectedSearchBySource(sourceId);
};
includeExclide = (type) => {
this.props.includeExcludeSearchBySources(type);
};
render() {
const { selectedSources } = this.props;
const { t } = this.props;
return (
<Fragment>
<div className="d-flex flex-wrap my-3">
<CustomInput
type="radio"
name="include-exclude-source"
className="d-flex mr-2"
checked={this.props.searchBySourcesType === 'include'}
id="include-sources-radio"
onChange={() => this.includeExclide('include')}
label={t('searchTab.searchBySection.sources.includeText')}
/>
<CustomInput
type="radio"
name="include-exclude-source"
checked={this.props.searchBySourcesType === 'exclude'}
className="d-flex mr-2"
id="exclude-sources-radio"
onChange={() => this.includeExclide('exclude')}
label={t('searchTab.searchBySection.sources.excludeText')}
/>
</div>
<p className="text-muted">
{t('searchTab.searchBySection.sources.selectedSources')}
</p>
<div className="source-table-wrap border">
<Table striped className="mb-0">
<thead>
<tr>
<th>{t('searchTab.searchBySection.sources.source')}</th>
<th>{t('searchTab.searchBySection.sources.mediatype')}</th>
<th style={{ width: '50px' }}></th>
</tr>
</thead>
<tbody>
{selectedSources.length > 0 ? (
selectedSources.map((source, i) => {
return (
<tr key={i}>
{/* <td>
<SourceIcon type={source.type} />
</td> */}
<td>{getTitle(source.title)}</td>
<td>{capitalize(source.type) || '-'}</td>
<td>
<button
title="Remove"
type="button"
className="btn p-0"
onClick={() => this.removeSource(source.id)}
>
<IoIosCloseCircleOutline
size={22}
className="text-danger ml-2"
/>
</button>
</td>
</tr>
);
})
) : (
<tr className="p-4 text-center text-black-50">
<td colSpan="3">
{t('common:messages.noRows')} <br />
{t('searchTab.searchBySection.sources.selectSource')}
</td>
</tr>
)}
</tbody>
</Table>
</div>
{selectedSources.length > 0 && (
<Button
size="sm"
className="d-block ml-auto mt-2 mb-2"
onClick={this.props.clearSearchBySources}
>
{t('searchTab.clearBtn')}
</Button>
)}
</Fragment>
);
}
}
export default translate(['tabsContent'], { wait: true })(
SourcesTabSelectedSources
);
@@ -0,0 +1,178 @@
import React from 'react'
import moment from 'moment'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import BetweenDatepickers from './SearchBy/BetweenDatepickers'
import { compose } from 'redux'
import classnames from 'classnames'
import { Button, CustomInput, FormGroup } from 'reactstrap'
export class SearchDatesPopup extends React.Component {
static propTypes = {
userSubscriptionDate: PropTypes.string.isRequired,
userSubscription: PropTypes.string.isRequired,
t: PropTypes.func.isRequired,
searchIntervals: PropTypes.array.isRequired,
searchLastDates: PropTypes.array.isRequired,
chosenSearchInterval: PropTypes.string.isRequired,
chosenSearchLastDate: PropTypes.string.isRequired,
chosenStartDate: PropTypes.string.isRequired,
chosenEndDate: PropTypes.string.isRequired,
setSearchInterval: PropTypes.func.isRequired,
setSearchLastDate: PropTypes.func.isRequired,
setSearchDate: PropTypes.func.isRequired,
setStartDate: PropTypes.func.isRequired,
setEndDate: PropTypes.func.isRequired
}
setSearchInterval = (e) => {
const chosenInterval = e.target.dataset.interval
const chosenStartDate = this.props.chosenStartDate
const chosenEndDate = this.props.chosenEndDate
const chosenLastDate = this.props.chosenSearchLastDate
const isIntervalBetween = chosenInterval === 'between'
this.props.setSearchInterval(chosenInterval)
if (
(isIntervalBetween && chosenStartDate !== '') ||
(isIntervalBetween && chosenEndDate !== '')
) {
const endDate = chosenEndDate !== '' ? chosenEndDate : 'now'
const startDate = chosenStartDate !== '' ? chosenStartDate : 'until'
this.props.setSearchDate(startDate + ' - ' + endDate)
}
if (chosenInterval === 'all') {
this.props.setSearchDate('all')
}
if (chosenInterval === 'last') {
this.props.setSearchDate(chosenLastDate)
}
}
setLastDate = (e) => {
const chosenLastDate = e.target.dataset.lastDate
const isDisabled = e.target.dataset.disabled === 'true'
if (isDisabled) return false
if (this.props.chosenSearchInterval !== 'last') {
this.props.setSearchInterval('last')
}
this.props.setSearchLastDate(chosenLastDate)
this.props.setSearchDate(chosenLastDate)
}
onReset = () => {
this.props.setSearchInterval('all')
this.props.setSearchDate('all')
this.props.setStartDate('')
this.props.setEndDate('')
}
render() {
const {
t,
chosenSearchInterval,
chosenStartDate,
chosenEndDate,
setSearchInterval,
setSearchDate,
setStartDate,
setEndDate,
chosenSearchLastDate,
searchIntervals,
searchLastDates,
userSubscription
} = this.props
const subscriptionLimitIndex = searchLastDates.indexOf(userSubscription)
const minDate = moment().startOf('day').subtract(
parseInt(userSubscription.slice(0, -1)),
'days'
)
return (
<div>
<div className="d-flex justify-content-between">
<p className="mb-2">
{t('searchTab.searchDates.subscriptionLabel')}:
<strong>
{t('searchTab.userSubscription.' + this.props.userSubscription)}
</strong>
</p>
<div>
<Button color="warning" className="mb-2" onClick={this.onReset}>
{t('searchTab.searchDates.resetBtn')}
</Button>
</div>
</div>
<FormGroup>
{searchIntervals.map((interval, i) => {
return (
<div key={interval}>
<CustomInput
checked={this.props.chosenSearchInterval === interval}
type="radio"
id={'search-interval-' + interval}
data-interval={interval}
name="date-interval"
label={t('searchTab.searchDates.' + interval)}
onChange={this.setSearchInterval}
/>
{interval === 'last' && (
<ul className="search-last-dates mx-3">
{searchLastDates.map((lastDate, i) => {
const isDisabled = i > subscriptionLimitIndex
const isActive =
chosenSearchLastDate === lastDate &&
chosenSearchInterval === 'last'
const className = classnames('search-last-dates__item', {
disabled: isDisabled,
active: isActive
})
return (
<li
key={'last-date-' + i}
data-last-date={lastDate}
data-disabled={isDisabled}
className={className}
onClick={this.setLastDate}
>
{t('searchTab.searchDates.' + lastDate)}
</li>
)
})}
</ul>
)}
{interval === 'between' && (
<BetweenDatepickers
chosenSearchInterval={chosenSearchInterval}
chosenStartDate={chosenStartDate}
chosenEndDate={chosenEndDate}
minDate={minDate}
setSearchInterval={setSearchInterval}
setSearchDate={setSearchDate}
setStartDate={setStartDate}
setEndDate={setEndDate}
/>
)}
</div>
)
})}
</FormGroup>
</div>
)
}
}
const applyDecorators = compose(translate(['tabsContent'], { wait: true }))
export default applyDecorators(SearchDatesPopup)
@@ -0,0 +1,439 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import SearchSubTabHead from './SearchSubTabHead';
import MediaTypes from './MediaTypes';
import SearchingBlock from './SearchingBlock';
import SearchingResults from './SearchingResults';
import SearchBy from './SearchBy/SearchBy';
import RefinePanel from './RefinePanel';
import Restrictions from '../../../../common/Restrictions/Restrictions';
import { parseSearchDays } from '../../../../../common/Common';
import reduxConnect from '../../../../../redux/utils/connect';
import { Card, CardBody, CardTitle } from 'reactstrap';
import { setDocumentData } from '../../../../../common/helper';
import { translate } from 'react-i18next';
import { compose } from 'redux';
export const domainNames = ['reddit', 'twitter', 'instagram'];
class SearchSubTab extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
store: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired
};
get searchState() {
return this.props.store.appState.search;
}
get searchByFiltersState() {
return this.props.store.appState.searchByFilters;
}
get articlesState() {
return this.props.store.appState.articles;
}
get authState() {
return this.props.store.common.auth;
}
componentDidMount() {
setDocumentData('title', 'Search');
}
componentWillUnmount() {
setDocumentData('title');
}
_sendSearchQuery = (page, initialSearch = false) => {
const { actions } = this.props;
const dataToSend = this.gatherSearchQueryData();
if (dataToSend) {
dataToSend.page = page;
dataToSend.advancedFilters = this.gatherAdvancedFilters();
actions.getSearchResults(dataToSend, initialSearch);
}
};
_sendFeedQuery = (page, activeFeed) => {
const { actions } = this.props;
const params = {
page: page,
advancedFilters: this.gatherAdvancedFilters()
};
actions.getFeedResults(params, activeFeed.id);
};
onSearchQuery = () => {
this._sendSearchQuery(1, true);
};
onRefine = () => {
const { activeFeed } = this.searchState;
if (activeFeed) {
this._sendFeedQuery(1, activeFeed);
} else {
this._sendSearchQuery(1);
}
};
onPager = ({ currentPage: page }) => {
const { activeFeed } = this.searchState;
if (activeFeed) {
this._sendFeedQuery(page, activeFeed);
} else {
this._sendSearchQuery(page);
}
};
onSaveAsFeed = (name, category) => {
const dataToSend = this.getFeedData(name, category, 'query_feed');
dataToSend && this.props.actions.saveAsFeed(dataToSend);
};
onSaveFeed = () => {
const { actions } = this.props;
const { activeFeed } = this.searchState;
const dataToSend = this.getFeedData(
activeFeed.name,
activeFeed.category,
activeFeed.subType
);
dataToSend && actions.saveFeed(dataToSend, activeFeed.id);
};
getFeedData = (name, category, feedSubType) => {
let dataToSend = {};
const searchQueryData = this.gatherSearchQueryData();
if (!searchQueryData) return false;
dataToSend.search = searchQueryData;
dataToSend.search.advancedFilters = this.gatherAdvancedFilters();
dataToSend.feed = {
name: name,
category: category,
subType: feedSubType
};
const excludedArticles = this.articlesState.excludedArticles;
if (excludedArticles && excludedArticles.length) {
dataToSend.feed.excludedDocuments = excludedArticles;
}
return dataToSend;
};
gatherSearchQueryData = () => {
const searchState = this.searchState;
const searchByFiltersState = this.searchByFiltersState;
const { userSubscription } = this.authState;
const { actions } = this.props;
let dataToSend = {};
const query = searchState.loadedFeedQuery;
if (!query) {
actions.addAlert({ type: 'error', transKey: 'searchQueryEmpty' });
return false;
}
dataToSend.query = query;
dataToSend.filters = {}; //create filters prop
//setting media types filter
if (searchByFiltersState.chosenMediaTypes.length) {
const source = [];
const domain = [];
searchByFiltersState.chosenMediaTypes.map((v) => {
if (domainNames.includes(v)) {
domain.push(`${v}.com`);
} else {
source.push(v);
}
});
dataToSend.filters.publisher = { source, domain };
} else {
actions.addAlert({ type: 'error', transKey: 'noMediaTypesSelected' });
return false;
}
// setting date filter
const chosenInterval = searchByFiltersState.chosenSearchInterval;
const chosenStartDate = searchByFiltersState.chosenStartDate;
const chosenEndDate = searchByFiltersState.chosenEndDate;
if (chosenInterval === 'between') {
if (chosenStartDate !== '' || chosenEndDate !== '') {
dataToSend.filters.date = {
type: 'between',
start: chosenStartDate,
end: chosenEndDate
};
} else {
dataToSend.filters.date = {
type: 'last',
days:
searchByFiltersState.chosenSearchDate === 'all'
? parseSearchDays(userSubscription)
: parseSearchDays(searchByFiltersState.chosenSearchDate)
};
}
} else if (chosenInterval === 'all') {
dataToSend.filters.date = {
type: 'last',
days: parseSearchDays(userSubscription)
};
} else {
dataToSend.filters.date = {
type: 'last',
days: parseSearchDays(searchByFiltersState.chosenSearchLastDate)
};
}
//adding included or/and excluded headlines filter
const headlineIncluded = searchByFiltersState.headlineIncluded;
const headlineExcluded = searchByFiltersState.headlineExcluded;
if (headlineIncluded.length || headlineExcluded.length) {
dataToSend.filters.headline = {};
}
if (headlineIncluded.length) {
dataToSend.filters.headline.include = headlineIncluded;
}
if (headlineExcluded.length) {
dataToSend.filters.headline.exclude = headlineExcluded;
}
//setting languages filter
const chosenLanguages = searchByFiltersState.chosenLanguages;
if (chosenLanguages.length) {
dataToSend.filters.language = chosenLanguages;
}
//setting locations filter
const locationsToInclude = searchByFiltersState.locationsToInclude;
const locationsToExclude = searchByFiltersState.locationsToExclude;
const countriesToInclude = locationsToInclude.filter((loc) => {
return loc.type === 'country';
});
const statesToInclude = locationsToInclude.filter((loc) => {
return loc.type === 'state';
});
const countriesToExclude = locationsToExclude.filter((loc) => {
return loc.type === 'country';
});
const statesToExclude = locationsToExclude.filter((loc) => {
return loc.type === 'state';
});
if (countriesToInclude.length || countriesToExclude.length) {
dataToSend.filters.country = {};
}
if (statesToInclude.length || statesToExclude.length) {
dataToSend.filters.state = {};
}
if (countriesToInclude.length) {
dataToSend.filters.country.include = countriesToInclude.map((loc) => {
return loc.code;
});
}
if (countriesToExclude.length) {
dataToSend.filters.country.exclude = countriesToExclude.map((loc) => {
return loc.code;
});
}
if (statesToInclude.length) {
dataToSend.filters.state.include = statesToInclude.map((loc) => {
return loc.code;
});
}
if (statesToExclude.length) {
dataToSend.filters.state.exclude = statesToExclude.map((loc) => {
return loc.code;
});
}
//setting source filter
const selectedSearchBySources =
searchByFiltersState.selectedSearchBySources;
if (selectedSearchBySources.length) {
dataToSend.filters.source = {};
dataToSend.filters.source.type = searchByFiltersState.searchBySourcesType;
dataToSend.filters.source.ids = selectedSearchBySources.map((source) => {
return source.id;
});
}
//setting source lists filter
const sourceListsToInclude =
searchByFiltersState.searchBySourceListsToInclude;
const sourceListsToExclude =
searchByFiltersState.searchBySourceListsToExclude;
if (sourceListsToInclude.length || sourceListsToExclude.length) {
dataToSend.filters.sourceList = {};
}
if (sourceListsToInclude.length) {
dataToSend.filters.sourceList.include = sourceListsToInclude.map(
(source) => {
return source.id;
}
);
}
if (sourceListsToExclude.length) {
dataToSend.filters.sourceList.exclude = sourceListsToExclude.map(
(source) => {
return source.id;
}
);
}
//setting duplicates filter
//dataToSend.filters.duplicates = searchByFiltersState.includeDuplicates;
//setting 'has images' filter
dataToSend.filters.hasImage = searchByFiltersState.hasImages;
return dataToSend;
};
gatherAdvancedFilters = () => {
return this.searchState.advancedFilters.selected;
};
render() {
const searchState = this.searchState;
const searchByFiltersState = this.searchByFiltersState;
const {
userSubscription,
userSubscriptionDate,
user: { restrictions }
} = this.authState;
const { store, actions } = this.props;
const feedCategories = store.appState.sidebar.categories;
const articlesState = store.appState.articles;
const { advancedFilters } = searchState;
const activeFeed = searchState.activeFeed;
let isEditSearchVisible =
!searchState.loadedFeedQuery || searchState.isEditingFeed;
if (activeFeed && activeFeed.subType === 'clip_feed') {
isEditSearchVisible = false;
}
const hasActiveFeed = !!activeFeed;
return (
<Fragment>
{!hasActiveFeed && (
<Restrictions
restrictions={restrictions && restrictions.limits}
restrictionsIds={['searchesPerDay', 'savedFeeds']}
/>
)}
<div className="search-tab">
<Card className="main-card mb-3">
<CardBody>
<div className="search-block">
{isEditSearchVisible && (
<div className="search-edit-block">
<SearchingBlock
searchResultsErrors={searchState.searchResultsErrors}
onSearchQuery={this.onSearchQuery}
loadedFeedQuery={searchState.loadedFeedQuery}
actions={actions}
/>
<MediaTypes
mediaTypes={searchByFiltersState.mediaTypes}
chosenMediaTypes={searchByFiltersState.chosenMediaTypes}
actions={actions}
restrictions={restrictions}
searchByFiltersState={searchByFiltersState}
userSubscription={userSubscription}
userSubscriptionDate={userSubscriptionDate}
toggleMediaType={actions.toggleMediaType}
toggleAllMediaTypes={actions.toggleAllMediaTypes}
/>
<SearchBy
userSubscription={userSubscription}
userSubscriptionDate={userSubscriptionDate}
searchByFiltersState={searchByFiltersState}
actions={actions}
/>
</div>
)}
<SearchSubTabHead
isSaveFeedPopupVisible={searchState.isSaveFeedPopupVisible}
isSaving={searchState.isSavingFeed}
feedCategories={feedCategories}
onSaveAsFeed={this.onSaveAsFeed}
toggleSaveFeedPopup={actions.toggleSaveFeedPopup}
addAlert={actions.addAlert}
getSidebarCategories={actions.getSidebarCategories}
activeFeed={activeFeed}
isEditingFeed={searchState.isEditingFeed}
editFeed={actions.editFeed}
setNewSearch={actions.setNewSearch}
renewSearchBy={actions.renewSearchBy}
changeActiveFeedName={actions.changeActiveFeedName}
saveFeed={this.onSaveFeed}
/>
</div>
</CardBody>
</Card>
<Card className="main-card mb-3">
<CardBody>
<CardTitle>{this.props.t('searchTab.results')}</CardTitle>
<div className="search-content">
<SearchingResults
searchState={searchState}
articlesState={articlesState}
actions={actions}
isRefinePanelVisible={advancedFilters.isVisible}
toggleRefinePanel={actions.toggleRefinePanel}
onPager={this.onPager}
/>
{searchState.isLoaded && advancedFilters.isVisible && (
<RefinePanel
advancedFilters={advancedFilters.all}
selectedFilters={advancedFilters.selected}
clearPending={advancedFilters.pending}
filterPages={advancedFilters.pages}
onRefine={this.onRefine}
actions={actions}
/>
)}
</div>
</CardBody>
</Card>
</div>
</Fragment>
);
}
}
const applyDecorators = compose(
reduxConnect(),
translate(['tabsContent'], { wait: true })
);
export default applyDecorators(SearchSubTab);
@@ -0,0 +1,135 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import SaveFeedPopup from './SaveFeedPopup'
import { Button } from 'reactstrap'
export class SearchSubTabHead extends React.Component {
static propTypes = {
feedCategories: PropTypes.array.isRequired,
isSaveFeedPopupVisible: PropTypes.bool.isRequired,
activeFeed: PropTypes.object,
isSaving: PropTypes.bool.isRequired,
isEditingFeed: PropTypes.bool.isRequired,
addAlert: PropTypes.func.isRequired,
toggleSaveFeedPopup: PropTypes.func.isRequired,
onSaveAsFeed: PropTypes.func.isRequired,
getSidebarCategories: PropTypes.func.isRequired,
editFeed: PropTypes.func.isRequired,
setNewSearch: PropTypes.func.isRequired,
renewSearchBy: PropTypes.func.isRequired,
changeActiveFeedName: PropTypes.func.isRequired,
saveFeed: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
}
openSaveFeedPopup = () => {
this.props.toggleSaveFeedPopup()
}
saveFeed = () => {
this.props.saveFeed()
}
onEditFeed = () => {
this.props.editFeed()
}
onNewSearch = () => {
this.props.setNewSearch()
this.props.renewSearchBy()
}
onChangeFeedName = (event) => {
this.props.changeActiveFeedName(event.target.value)
}
render() {
const {
t,
isEditingFeed,
isSaveFeedPopupVisible,
isSaving,
activeFeed
} = this.props
const feedIsLoaded = !!activeFeed
const showEditButton =
!!activeFeed && !isEditingFeed && activeFeed.subType === 'query_feed'
return (
<div>
<div className="d-flex flex-wrap justify-content-between">
<div>
{!isEditingFeed && activeFeed && <h4 className="text-primary mb-2 mb-md-0">{activeFeed.name}</h4>}
</div>
<div className="text-right" data-tour="search-buttons">
<Button
className="btn-icon mb-2 mb-lg-0 ml-2"
color="primary"
onClick={this.onNewSearch}
>
<i className="lnr-plus-circle btn-icon-wrapper"></i>
{t('searchTab.newSearchBtn')}
</Button>
{!feedIsLoaded && (
<Button
className="btn-icon mb-2 mb-lg-0 ml-2"
color="success"
onClick={this.openSaveFeedPopup}
>
<i className="lnr-checkmark-circle btn-icon-wrapper"></i>
{isSaving ? t('searchTab.savingBtn') : t('searchTab.saveBtn')}
</Button>
)}
{feedIsLoaded && isEditingFeed && (
<Button
className="btn-icon mb-2 mb-lg-0 ml-2"
color="success"
onClick={this.saveFeed}
>
<i className="lnr-checkmark-circle btn-icon-wrapper"></i>
{isSaving ? t('searchTab.savingBtn') : t('searchTab.saveBtn')}
</Button>
)}
{feedIsLoaded && isEditingFeed && (
<Button
className="btn-icon mb-2 mb-lg-0 ml-2"
color="success"
onClick={this.openSaveFeedPopup}
>
<i className="lnr-checkmark-circle btn-icon-wrapper"></i>
{t('searchTab.saveAsBtn')}
</Button>
)}
{showEditButton && (
<Button
className="btn-icon mb-2 mb-lg-0 ml-2"
color="warning"
onClick={this.onEditFeed}
>
<i className="lnr-pencil btn-icon-wrapper"></i>
{t('searchTab.editFeedBtn')}
</Button>
)}
</div>
</div>
{isSaveFeedPopupVisible && (
<SaveFeedPopup
saveType="typeSaveAs"
feedCategories={this.props.feedCategories}
toggleSaveFeedPopup={this.props.toggleSaveFeedPopup}
addAlert={this.props.addAlert}
onSaveAsFeed={this.props.onSaveAsFeed}
getSidebarCategories={this.props.getSidebarCategories}
/>
)}
</div>
)
}
}
export default translate(['tabsContent'], { wait: true })(SearchSubTabHead)
@@ -0,0 +1,61 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import { Button, Input, InputGroup, InputGroupAddon } from 'reactstrap'
export class SearchingBlock extends React.Component {
static propTypes = {
searchResultsErrors: PropTypes.array.isRequired,
loadedFeedQuery: PropTypes.string,
onSearchQuery: PropTypes.func.isRequired,
actions: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
}
onPressEnter = (e) => {
if (e.keyCode === 13) {
this.props.onSearchQuery()
}
}
onChangeQuery = (e) => {
const { actions } = this.props
const value = e.target.value;
// replace smart quotation marks with normal
let filterQuotes = value.replace(/[\u2018\u2019]/g, '\'').replace(/[\u201C\u201D]/g, '"')
// add space before operator if not
filterQuotes = filterQuotes.replace(/\s*\+/g, ' +').replace(/\s*\-/g, ' -').trimStart()
actions.changeFeedQuery(filterQuotes)
}
render() {
let { t, loadedFeedQuery } = this.props
loadedFeedQuery = loadedFeedQuery || ''
return (
<div className="search-input-field mb-2">
<InputGroup>
<Input
type="text"
value={loadedFeedQuery}
data-tour="input-field-search"
onChange={this.onChangeQuery}
placeholder={t('searchTab.searchInputPlaceholder')}
onKeyUp={this.onPressEnter}
/>
<InputGroupAddon addonType="append">
<Button
color="primary"
className="btn-icon btn-icon-only px-3"
data-tour="search-button"
onClick={this.props.onSearchQuery}
>
<i className="lnr-magnifier btn-icon-wrapper font-weight-bold"></i>
</Button>
</InputGroupAddon>
</InputGroup>
</div>
)
}
}
export default translate(['tabsContent'], { wait: true })(SearchingBlock)
@@ -0,0 +1,178 @@
import React from 'react'
import PropTypes from 'prop-types'
import SearchingResultsTopPanel from './SearchingResultsTopPanel'
import Article from './Article'
import DeleteArticlesPopup from './DeleteArticlesPopup'
import EmailArticlesPopup from './EmailArticlesPopup'
import CommentArticlePopup from './CommentArticlePopup'
import ClipArticlesPopup from './ClipArticles/ClipArticlesPopup'
import Pager from '../../../../common/Pager/Pager'
import EmailConfirmPopup from './EmailConfirmPopup'
import NoRecords from '../../../../common/NoRecords'
import Loading from '../../../../common/Loading'
import { Interpolate, translate } from 'react-i18next'
export class SearchingResults extends React.Component {
static propTypes = {
searchState: PropTypes.object.isRequired,
articlesState: PropTypes.object.isRequired,
isRefinePanelVisible: PropTypes.bool.isRequired,
toggleRefinePanel: PropTypes.func.isRequired,
onPager: PropTypes.func.isRequired,
actions: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
};
forEachArticle = (cb) => {
const { searchState, articlesState } = this.props
return searchState.searchResults
.filter((article) => !articlesState.excludedArticles.includes(article.id))
.map(cb)
};
render () {
const { searchState, articlesState, actions, t } = this.props
const isSearchResultsLoaded = searchState.searchResults.length > 0
const numPages = Math.ceil(
searchState.searchResultTotalCount / searchState.searchResultLimit
)
const noRecords = searchState.searchResultsPending || !isSearchResultsLoaded || !searchState.isSynced
if (searchState.searchResultsPending) {
return (
<div className="search-results">
<Loading />
</div>
)
}
if (!searchState.isSynced) {
return (
<div className="search-results">
<NoRecords message={t('searchTab.notSynchronized')} />
</div>
)
}
if (searchState.isSynced && !isSearchResultsLoaded) {
return (
<div className="search-results">
<NoRecords message={t('searchTab.noResults')} />
</div>
)
}
return (
<div className="search-results">
<SearchingResultsTopPanel
noRecords={noRecords}
searchResultsCount={searchState.searchResults.length}
selectedArticles={searchState.selectedArticles}
selectAllArticles={actions.selectAllArticles}
showDeleteArticlesPopup={actions.showDeleteArticlesPopup}
showEmailArticlesPopup={actions.showEmailArticlesPopup}
showClipArticlesPopup={actions.showClipArticlesPopup}
isRefinePanelVisible={noRecords ? false : this.props.isRefinePanelVisible}
toggleRefinePanel={this.props.toggleRefinePanel}
/>
{isSearchResultsLoaded &&
<p className="text-muted font-size-xs">
<Interpolate
t={t}
i18nKey="searchTab.articlesCountDivider"
resultsCount={searchState.searchResultCount}
totalCount={searchState.searchResultTotalCount}
/>
</p>
}
<div className="search-results-block mt-1">
{isSearchResultsLoaded &&
this.forEachArticle((article, i) => {
return (
<Article
key={'article-' + i}
article={article}
selectedArticles={searchState.selectedArticles}
selectArticle={actions.selectArticle}
showDeletePopup={actions.showDeleteArticlesPopup}
showEmailPopup={actions.showEmailArticlesPopup}
showCommentPopup={actions.showCommentArticlePopup}
showClipPopup={actions.showClipArticlesPopup}
deleteComment={actions.deleteComment}
readArticleLater={actions.readArticleLater}
loadMoreComments={actions.loadMoreComments}
showShareMenu={actions.showShareMenu}
/>
)
})}
{isSearchResultsLoaded && (
<Pager
pagerAction={this.props.onPager}
currentPage={searchState.searchResultPage}
numPages={numPages}
limitByPage={searchState.searchResultLimit}
hideLimitSelector
/>
)}
</div>
{articlesState.deletePopup.visible && (
<DeleteArticlesPopup
articles={articlesState.deletePopup.articles}
hidePopup={actions.hideDeleteArticlesPopup}
activeFeed={searchState.activeFeed}
deleteArticles={actions.deleteArticles}
deleteArticlesFromFeed={actions.deleteArticlesFromFeed}
addAlert={actions.addAlert}
/>
)}
{articlesState.emailPopup.visible && (
<EmailArticlesPopup
articlesToEmail={articlesState.emailPopup.articles}
emailArticles={actions.emailArticles}
hidePopup={actions.hideEmailArticlesPopup}
addAlert={actions.addAlert}
loadRecipients={actions.loadRecipients}
recipients={articlesState.emailPopup.recipients}
>
{articlesState.emailConfirmPopup.visible && (
<EmailConfirmPopup
hidePopup={actions.hideEmailConfirmPopup}
hideEmailPopup={actions.hideEmailArticlesPopup}
sendDocumentsByEmail={actions.sendDocumentsByEmail}
/>
)}
</EmailArticlesPopup>
)}
{articlesState.commentPopup.visible && (
<CommentArticlePopup
article={articlesState.commentPopup.article}
comment={articlesState.commentPopup.comment}
commentArticle={actions.commentArticle}
updateComment={actions.updateComment}
hidePopup={actions.hideCommentArticlePopup}
addAlert={actions.addAlert}
/>
)}
{articlesState.clipPopup.visible && (
<ClipArticlesPopup
articles={articlesState.clipPopup.articles}
recentClipFeeds={articlesState.recentClipFeeds}
getRecentClipFeeds={actions.getRecentClipFeeds}
hidePopup={actions.hideClipArticlesPopup}
clipArticles={actions.clipArticles}
addAlert={actions.addAlert}
/>
)}
</div>
)
}
}
export default translate(['tabsContent'], { wait: true })(SearchingResults)
@@ -0,0 +1,111 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ButtonGroup, Button, CustomInput } from 'reactstrap';
import { translate } from 'react-i18next';
export class SearchingResultsTopPanel extends React.Component {
static propTypes = {
noRecords: PropTypes.bool,
selectedArticles: PropTypes.array.isRequired,
searchResultsCount: PropTypes.number.isRequired,
selectAllArticles: PropTypes.func.isRequired,
showDeleteArticlesPopup: PropTypes.func.isRequired,
showEmailArticlesPopup: PropTypes.func.isRequired,
showClipArticlesPopup: PropTypes.func.isRequired,
isRefinePanelVisible: PropTypes.bool.isRequired,
toggleRefinePanel: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
onShowClick = (e) => {
e.preventDefault();
this.props.toggleRefinePanel();
};
selectAllArticles = (e) => {
const isChecked = e.target.checked;
if (this.props.searchResultsCount > 0) {
this.props.selectAllArticles(isChecked);
}
};
showDeleteArticlesPopup = () => {
if (this.props.selectedArticles.length > 0) {
this.props.showDeleteArticlesPopup(this.props.selectedArticles);
}
};
showEmailArticlesPopup = () => {
if (this.props.selectedArticles.length > 0) {
this.props.showEmailArticlesPopup(this.props.selectedArticles);
}
};
showClipArticlesPopup = () => {
if (this.props.selectedArticles.length > 0) {
this.props.showClipArticlesPopup(this.props.selectedArticles);
}
};
render() {
const { t, searchResultsCount, noRecords } = this.props;
const chosenArticlesCount = this.props.selectedArticles.length;
const isAllArticlesChosen =
this.props.searchResultsCount > 0
? searchResultsCount === chosenArticlesCount
: false;
if (noRecords) {
return null;
}
return (
<div className="d-flex justify-content-end mb-3 mb-md-0">
<ButtonGroup>
<Button color="light">
<CustomInput
id="toggle-all-results"
type="checkbox"
checked={isAllArticlesChosen}
onChange={this.selectAllArticles}
/>
</Button>
{/* <Button color="secondary">
<i className="fa fa-tag mr-2"> </i>
{t('searchTab.tagBtn')}
</Button> */}
<Button color="secondary" onClick={this.showClipArticlesPopup}>
<i className="fa fa-scissors mr-2"> </i>
{t('searchTab.clipBtn')}
</Button>
<Button color="secondary" onClick={this.showEmailArticlesPopup}>
<i className="fa fa-envelope-o mr-2"> </i>
{t('searchTab.emailBtn')}
</Button>
<Button color="secondary" onClick={this.showDeleteArticlesPopup}>
<i className="fa fa-trash mr-2"> </i>
{t('searchTab.deleteBtn')}
</Button>
</ButtonGroup>
{!this.props.isRefinePanelVisible && (
<Button
color="light"
title="Show refine panel"
className="btn-icon ml-3"
onClick={this.onShowClick}
>
<i className="pe-7s-filter btn-icon-wrapper"></i>
{t('searchTab.filter')}
</Button>
)}
</div>
);
}
}
export default translate(['tabsContent'], { wait: true })(
SearchingResultsTopPanel
);
@@ -0,0 +1,50 @@
import React from 'react'
import PropTypes from 'prop-types'
import {translate} from 'react-i18next'
import onClickOutside from 'react-onclickoutside'
import {compose} from 'redux'
class ShareMenu extends React.Component {
static propTypes = {
article: PropTypes.object.isRequired,
hideMenu: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
handleClickOutside = () => {
this.props.hideMenu()
};
_winOpen = (url) => {
window.open(url, 'share', 'width=600, height=450, top=0, left=0, toolbar=no')
};
onTweet = () => {
this._winOpen('https://twitter.com/intent/tweet?url=' + this.props.article.source.link)
this.props.hideMenu()
};
onYammer = () => {
this._winOpen('https://www.yammer.com/')
this.props.hideMenu()
};
render () {
const { t } = this.props
return (
<div className="article-share-menu">
<a onClick={this.onTweet}>{t('searchTab.tweet')}</a>
<a onClick={this.onYammer}>{t('searchTab.yammer')}</a>
</div>
)
}
}
const applyDecorators = compose(
translate(['tabsContent'], {wait: true}),
onClickOutside
)
export default applyDecorators(ShareMenu)
@@ -0,0 +1,47 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Redirect, Route, Switch, withRouter } from 'react-router-dom';
import SubTabWrapper from '../../AppHeader/SubTabWrapper';
import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup';
import SearchSubTab from './SearchSubTab/SearchSubTab';
import SourceIndexSubTab from './SourceIndexSubTab/SourceIndexSubTab';
import SourceListsSubTab from './SourceListsSubTab/SourceListsSubTab';
class SearchTab extends React.Component {
static propTypes = {
activeTabName: PropTypes.string,
match: PropTypes.object,
subTabs: PropTypes.array
};
render() {
const { activeTabName, subTabs, match } = this.props;
return (
<CSSTransitionGroup
component="div"
transitionName="TabsAnimation"
transitionAppear
transitionAppearTimeout={0}
transitionEnter={false}
transitionLeave={false}
>
<SubTabWrapper activeTabName={activeTabName} subTabs={subTabs}>
<Switch>
<Route path={`${match.url}/search`} component={SearchSubTab} />
<Route
path={`${match.url}/source-index`}
component={SourceIndexSubTab}
/>
<Route
path={`${match.url}/source-lists`}
component={SourceListsSubTab}
/>
<Redirect to={`${match.url}/search`} />
</Switch>
</SubTabWrapper>
</CSSTransitionGroup>
);
}
}
export default withRouter(SearchTab);
@@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { Col } from 'reactstrap';
export class InfoField extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
label: PropTypes.string,
labelValue: PropTypes.string,
children: PropTypes.oneOfType([PropTypes.string, PropTypes.element])
};
render() {
const { t, label, children, labelValue } = this.props;
return (
<li className="row">
<Col sm="4">
<p className="mb-1">{labelValue || t(label)}</p>
</Col>
<Col sm="8">
<p className="mb-1">{children}</p>
</Col>
</li>
);
}
}
export default translate(['tabsContent'], { wait: true })(InfoField);
@@ -0,0 +1,143 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import PopupLayout from '../../../../common/Popups/PopupLayout';
import InfoField from './InfoField';
import {
capOnlyFirstLetter,
getTitle,
notNullAndUnd
} from '../../../../../common/helper';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCheckCircle } from '@fortawesome/free-solid-svg-icons';
class SourceIndexInfoPopup extends React.Component {
static propTypes = {
source: PropTypes.object.isRequired,
hideSourceInfoPopup: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
render() {
let { t, source, hideSourceInfoPopup } = this.props;
/*
const loc = cl(
source.city,
source.state,
source.country && t(`common:country.${source.country}`)
)
.split(' ')
.join(', '); */
/*
source = {
...source,
tags: ['Lorem', 'ipsum', 'dolor', 'ipsum', 'dolor', 'ipsum', 'dolor'],
verified: true,
followers: 3333,
following: 33,
favorites: 333,
title: 'Title',
url: 'URL',
type: 'Type',
subType: 'Sub Type',
lang: 'en',
location: 'Washington, DC',
country: 'US',
spam_probability: '20%',
likes: 3
}; */
return (
<PopupLayout
className="source-info-popup"
title="sourceIndexTab.sourceInfoPopupTitle"
showFooter={false}
onHide={hideSourceInfoPopup}
>
<ul className="container">
<InfoField label="sourceIndexTab.titleLabel">
<a href={source.url} target="_blank" rel="noopener noreferrer">
{getTitle(source.title)}
</a>
</InfoField>
{source.url && (
<InfoField label="sourceIndexTab.homeUrl">{source.url}</InfoField>
)}
{source.type && (
<InfoField label="sourceIndexTab.mediaType">
{capOnlyFirstLetter(source.type)}
</InfoField>
)}
{source.subType && (
<InfoField labelValue="Sub Type">
{capOnlyFirstLetter(source.subType)}
</InfoField>
)}
{source.verified && (
<InfoField labelValue="Verified">
<FontAwesomeIcon
title="Source Verified"
className="text-primary"
icon={faCheckCircle}
/>
</InfoField>
)}
{source.lang && (
<InfoField label="sourceIndexTab.lang">
{t(`common:language.${source.lang}`, '-')}
</InfoField>
)}
{source.location && (
<InfoField labelValue="Location">{source.location}</InfoField>
)}
{source.country && (
<InfoField label="sourceIndexTab.country">
{t(`common:country.${source.country}`)}
</InfoField>
)}
{notNullAndUnd(source.followers) && (
<InfoField labelValue="Followers">{source.followers}</InfoField>
)}
{notNullAndUnd(source.following) && (
<InfoField labelValue="Following">{source.following}</InfoField>
)}
{notNullAndUnd(source.favorites) && (
<InfoField labelValue="Favorites">{source.favorites}</InfoField>
)}
{notNullAndUnd(source.likes) && (
<InfoField labelValue="Likes">{source.likes}</InfoField>
)}
{source.tags && source.tags.length > 0 && (
<InfoField labelValue="Tags">{source.tags.join(', ')}</InfoField>
)}
{source.spam_probability && (
<InfoField labelValue="Spam Probability">
{source.spam_probability}
</InfoField>
)}
{source.source_profiles && (
<InfoField labelValue="Source profiles">
{source.source_profiles.join(', ')}
</InfoField>
)}
</ul>
</PopupLayout>
);
}
}
export default translate(['tabsContent'], { wait: true })(SourceIndexInfoPopup);
@@ -0,0 +1,186 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import SourceIndexTable from './SourceIndexTable'
import SourceIndexUpdatePopup from './SourceIndexUpdatePopup'
import FiltersTable from '../../../../common/FiltersTable/FiltersTable'
import { withRouter } from 'react-router-dom'
import reduxConnect from '../../../../../redux/utils/connect'
import { compose } from 'redux'
import { Button, ButtonGroup, Input, InputGroup, InputGroupAddon } from 'reactstrap'
import { setDocumentData } from '../../../../../common/helper'
class SourceIndexSubTab extends React.Component {
static propTypes = {
sourcesState: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
};
componentDidMount() {
setDocumentData('title', 'Source Index | Search')
}
componentWillUnmount() {
setDocumentData('title')
}
_sourceIndexesState = () => this.props.sourcesState.sourceIndexesState;
_sourceLists = () => this.props.sourcesState.sourceListsState.data;
loadSourceIndexes = (params) => {
this.props.actions.getSourceIndexes(params || null)
};
onSearchSources = () => {
this.loadSourceIndexes()
};
onEnterSearchInput = (e) => {
if (e.keyCode === 13) this.loadSourceIndexes()
};
onChangeSearchInput = (e) => {
this.props.actions.setSourceIndexSearchQuery(e.target.value)
};
onFetchData = (params) => {
this.loadSourceIndexes(params)
};
showAddToListPopup = () => {
const { actions } = this.props
const sourceIndexesState = this._sourceIndexesState()
if (sourceIndexesState.selectedIds.length === 0) {
actions.addAlert({
type: 'notice',
transKey: 'noListsSelected',
id: 'noListsSelected'
})
return false
}
actions.toggleAddSourceToListPopup()
};
onSelectFilter = (groupName, filterValue) => {
this.props.actions.selectSourcesFilter(groupName, filterValue)
};
onClearFilters = (groupName) => {
this.props.actions.clearSourcesFilters(groupName)
};
onClearAllFilters = () => {
this.props.actions.clearAllSourcesFilters()
};
onMoreFilters = (groupName) => {
this.props.actions.loadMoreSourcesFilters(groupName)
};
onLessFilters = (groupName) => {
this.props.actions.loadLessSourcesFilters(groupName)
};
render () {
const { t, actions } = this.props
const sourceIndexesState = this._sourceIndexesState()
const sourceLists = this._sourceLists()
const {
searchQuery,
selectedIds,
chosenListsToAddSources,
chosenSourceToUpdate,
advancedFilters
} = sourceIndexesState
return (
<div className="mb-3">
<InputGroup className="mb-3">
<Input
type="text"
id="source-index-search"
placeholder={t('sourceIndexTab.mainInputPlaceholder')}
value={searchQuery}
onChange={this.onChangeSearchInput}
onKeyUp={this.onEnterSearchInput}
/>
<InputGroupAddon addonType="append">
<Button
color="primary"
className="btn-icon btn-icon-only"
onClick={this.onSearchSources}
>
<i className="lnr-magnifier btn-icon-wrapper"></i>
</Button>
</InputGroupAddon>
</InputGroup>
<ButtonGroup className="mb-3">
<Button
onClick={this.showAddToListPopup}
color="secondary"
>
<i className="fa fa-plus fa-1px for-small mr-1"> </i>{" "}
{t('sourceIndexTab.addToSourceListsBtn')}
</Button>
</ButtonGroup>
<div className="search-content">
<SourceIndexTable
tableState={sourceIndexesState}
type="sourceIndexesState"
onFetch={this.onFetchData}
actions={actions}
/>
<FiltersTable
filters={advancedFilters.all}
pages={advancedFilters.pages}
selectedFilters={advancedFilters.selected}
clearPending={advancedFilters.pending}
callbacks={{
selectFilter: this.onSelectFilter,
clearFilters: this.onClearFilters,
clearAllFilters: this.onClearAllFilters,
moreFilters: this.onMoreFilters,
lessFilters: this.onLessFilters,
refine: this.onSearchSources
}}
/>
</div>
{sourceIndexesState.isAddPopupVisible && (
<SourceIndexUpdatePopup
type="add"
sourceLists={sourceLists}
chosenLists={chosenListsToAddSources}
chosenSourceIndexes={selectedIds}
actions={actions}
/>
)}
{sourceIndexesState.isUpdatePopupVisible && (
<SourceIndexUpdatePopup
type="update"
sourceLists={sourceLists}
chosenLists={chosenSourceToUpdate.listIds}
chosenSourceIndexes={[chosenSourceToUpdate.id]}
updateItemTitle={chosenSourceToUpdate.title}
actions={actions}
/>
)}
</div>
)
}
}
const applyDecorators = compose(
withRouter,
reduxConnect('sourcesState', ['appState', 'sourcesState']),
translate(['tabsContent'], { wait: true })
)
export default applyDecorators(SourceIndexSubTab)
@@ -0,0 +1,195 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate, Interpolate } from 'react-i18next'
import Table from '../../../../common/Table/Table'
import CheckboxCell from '../../../../common/Table/CheckboxCell'
import SortableTh from '../../../../common/Table/SortableTh'
import SourceIndexInfoPopup from './SourceIndexInfoPopup'
import { Button } from 'reactstrap'
import { getTitle } from '../../../../../common/helper'
export class SourceIndexTable extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
tableState: PropTypes.object.isRequired,
type: PropTypes.string.isRequired,
onFetch: PropTypes.func.isRequired,
onDeleteIndex: PropTypes.func,
actions: PropTypes.object.isRequired
};
onFetch = (page, pageSize, sorted) => {
const { tableState, onFetch } = this.props
const params = {
page: page + 1,
limit: pageSize,
query: tableState.searchQuery
}
if (sorted.length) {
const sortedField = sorted[0]
const sort = {
field: sortedField.id,
direction: sortedField.desc ? 'desc' : 'asc'
}
params['sort'] = sort
}
onFetch(params)
};
selectAllAction = (event) => {
const { actions } = this.props
actions.toggleAllSourceIndexes()
};
selectRowAction = (itemId) => {
const { actions } = this.props
actions.toggleSourceIndex(itemId) // TODO
};
showUpdateSourcePopup = (source) => (e) => {
e.preventDefault()
this.props.actions.showUpdateSourcePopup(source)
};
deleteSourceIndex = (source) => (e) => {
e.preventDefault()
this.props.onDeleteIndex(source)
};
toggleInfoPopup = (source) => () => {
const { type, actions } = this.props
actions.toggleInfoSourcePopup(type, source)
};
getColumns = () => {
const {t, type, tableState} = this.props
let columns = [
{
id: 'selectCheckbox',
accessor: '',
sortable: false,
width: 45,
className: 'cw-center-cell',
headerClassName: 'cw-center-cell',
Header: () => {
return (
<CheckboxCell
checked={tableState.isAllSelected}
onChange={this.selectAllAction}
/>
)
},
Cell: ({original}) => {
const isSelected = tableState.selectedIds.includes(original.id)
return (
<CheckboxCell
id={original.id}
checked={isSelected}
onChange={this.selectRowAction}
/>
)
}
}, {
Header: <SortableTh title='sourceIndexTab.name' />,
accessor: 'name',
Cell: ({original}) => {
return (
<Button
color="link"
className="btn-anchor"
title="Click to see details"
onClick={this.toggleInfoPopup(original)}
>
{getTitle(original.title)}
</Button>
)
}
}, {
id: 'mediaType',
Header: <SortableTh title='sourceIndexTab.mediaType' />,
accessor: item => t(`searchTab.sourceTypes.${item.type}`)
}, {
id: 'country',
Header: <SortableTh title='sourceIndexTab.country' />,
accessor: item => {
return item.country ? t(`common:country.${item.country}`) : ''
}
}, {
id: 'action',
Header: t('sourceIndexTab.action'),
sortable: false,
Cell: ({original}) => {
return (
<Button
outline
color="info"
className="border-0"
size="sm"
onClick={this.showUpdateSourcePopup(original)}
>
<Interpolate
i18nKey='sourceIndexTab.actionBtn'
listsCount={original.listIds.length}
/>
</Button>
)
}
}, {
id: 'deleteAction',
Header: t('sourceIndexTab.action'),
sortable: false,
Cell: ({original}) => {
return (
<Button
outline
size="sm"
color="secondary"
className="border-0"
onClick={this.deleteSourceIndex(original)}
>
{t('sourceListsTab.delete')}
</Button>
)
}
}
]
const sourceIndexCols = ['selectCheckbox', 'name', 'mediaType', 'country', 'action']
const sourceOfListCols = ['name', 'mediaType', 'country', 'deleteAction']
let cols = type === 'sourceIndexesState' ? sourceIndexCols : sourceOfListCols
return columns.filter(col => cols.includes(col.id) || cols.includes(col.accessor))
};
render () {
const {tableState} = this.props
const columns = this.getColumns()
const infoPopup = tableState.infoPopup
return (
<div className="sources-table">
<Table
columns={columns}
data={tableState.data}
totalCount={tableState.totalCount}
showTotalCount
limit={tableState.limit}
page={tableState.page}
isLoading={tableState.isLoading}
onFetchData={this.onFetch}
/>
{infoPopup.visible && infoPopup.item &&
<SourceIndexInfoPopup
source={infoPopup.item}
hideSourceInfoPopup={this.toggleInfoPopup(null)}
/>
}
</div>
)
}
}
export default translate(['tabsContent'], { wait: true })(SourceIndexTable)
@@ -0,0 +1,106 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate, Interpolate } from 'react-i18next'
import PopupLayout from '../../../../common/Popups/PopupLayout'
import { CustomInput } from 'reactstrap'
import { getTitle } from '../../../../../common/helper'
export class SourceIndexUpdatePopup extends React.Component {
static propTypes = {
type: PropTypes.string.isRequired,
sourceLists: PropTypes.array.isRequired,
chosenLists: PropTypes.array.isRequired,
chosenSourceIndexes: PropTypes.array.isRequired,
updateItemTitle: PropTypes.string,
actions: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
};
componentWillMount = () => {
const { sourceLists, actions } = this.props
if (sourceLists.length === 0) {
actions.getMainSourceLists({page: 1, limit: 50})
}
};
onChoseList = (e) => {
const { type, chosenLists, actions } = this.props
const isChecked = e.target.checked
const listId = parseInt(e.target.dataset.listId)
const lists = isChecked ? chosenLists.concat(listId) : chosenLists.filter((id) => listId !== id)
const action = type === 'add' ? actions.setChosenListsToAddSources : actions.setChosenListsToUpdateSources
action(lists)
};
onSubmit = () => {
const { actions, chosenSourceIndexes, chosenLists, type } = this.props
actions.addSourcesToList({
sources: chosenSourceIndexes,
sourceLists: chosenLists
}, type === 'add')
};
getBodyTitle () {
const { t, type, updateItemTitle } = this.props
if (type === 'add') {
return <p className="mb-3">{t('sourceListsTab.popup.addToListDesc')}</p>
}
else {
return (
<p className="mb-3">
<Interpolate
i18nKey='sourceListsTab.popup.updateListDesc'
name={getTitle(updateItemTitle)}
/>
</p>
)
}
}
render () {
const { type, sourceLists, chosenLists, actions } = this.props
const isAdd = type === 'add'
const title = isAdd ? 'addToListTitle' : 'updateListTitle'
const submitText = isAdd ? 'addBtn' : 'saveBtn'
const hideAction = isAdd ? actions.toggleAddSourceToListPopup : actions.hideUpdateSourcePopup
return (
<PopupLayout
title={`sourceListsTab.popup.${title}`}
submitText={`sourceListsTab.popup.${submitText}`}
onHide={hideAction}
onSubmit={this.onSubmit}
>
<div>
{this.getBodyTitle()}
{sourceLists.length > 0 &&
<ul className="row">
{sourceLists.map((list, i) => {
const isListChosen = chosenLists.includes(list.id)
return (
<li key={i} className="col-md-4 col-sm-6 mb-2">
<CustomInput
type="checkbox"
id={'sourceListCheck-' + i}
className="d-flex"
data-list-id={list.id}
checked={isListChosen}
onChange={this.onChoseList}
label={list.name}
/>
</li>
)
})}
</ul>
}
</div>
</PopupLayout>
)
}
}
export default translate(['tabsContent', 'common'], { wait: true })(SourceIndexUpdatePopup)
@@ -0,0 +1,101 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import SourceListsAddPopup from './SourceListsAddPopup'
import SourceListsDeletePopup from './SourceListsDeletePopup'
import SourceListsRenamePopup from './SourceListsRenamePopup'
import SourceListsClonePopup from './SourceListsClonePopup'
import SourceListsTable from './SourceListsTable'
import { Button, CustomInput } from 'reactstrap'
export class SourceLists extends React.Component {
static propTypes = {
sourceListsState: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
}
onGlobalOnlyClick = () => {
const { actions, sourceListsState } = this.props
actions.toggleOnlyGlobal()
const params = {
page: sourceListsState.page,
limit: sourceListsState.limit,
onlyShared: !sourceListsState.onlyGlobal,
sort: {
field: sourceListsState.sortByField,
direction: sourceListsState.sortDirection
}
}
actions.getMainSourceLists(params)
}
render() {
const { t, sourceListsState, actions } = this.props
const {
isAddListPopupVisible,
isDeletePopupVisible,
isRenameListPopupVisible,
isCloneListPopupVisible,
listToEdit
} = sourceListsState
return (
<div className="source-lists-tab">
<div className="d-flex justify-content-between align-items-end flex-wrap-reverse flex-sm-nowrap">
<CustomInput
id="show-global"
type="checkbox"
className="d-flex mb-3"
checked={sourceListsState.onlyGlobal}
onChange={this.onGlobalOnlyClick}
label={t('sourceListsTab.showGlobalCheck')}
/>
<Button
color="primary"
className="btn-icon mb-3"
onClick={actions.toggleAddListPopup}
>
<i className="lnr lnr-plus-circle btn-icon-wrapper" />
{t('sourceListsTab.addListBtn')}
</Button>
</div>
<SourceListsTable tableState={sourceListsState} actions={actions} />
{isAddListPopupVisible && (
<SourceListsAddPopup
toggleAddListPopup={actions.toggleAddListPopup}
addSourceList={actions.addSourceList}
/>
)}
{isDeletePopupVisible && (
<SourceListsDeletePopup
listToEdit={listToEdit}
toggleDeleteListPopup={actions.toggleDeleteListPopup}
deleteSourceList={actions.deleteSourceList}
/>
)}
{isRenameListPopupVisible && (
<SourceListsRenamePopup
listToEdit={listToEdit}
toggleRenameListPopup={actions.toggleRenameListPopup}
renameSourceList={actions.renameSourceList}
/>
)}
{isCloneListPopupVisible && (
<SourceListsClonePopup
listToEdit={listToEdit}
toggleCloneListPopup={actions.toggleCloneListPopup}
cloneSourceList={actions.cloneSourceList}
/>
)}
</div>
)
}
}
export default translate(['tabsContent'], { wait: true })(SourceLists)
@@ -0,0 +1,55 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import PopupLayout from '../../../../common/Popups/PopupLayout'
import { FormGroup, Input, Label } from 'reactstrap'
export class SourceListsAddPopup extends React.Component {
static propTypes = {
toggleAddListPopup: PropTypes.func.isRequired,
addSourceList: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
}
state = {
name: ''
}
onSubmit = () => {
const { addSourceList } = this.props
addSourceList(this.state.name)
}
handleChange = (e) => {
const { value } = e.target
this.setState({ name: value })
}
render() {
const { toggleAddListPopup, t } = this.props
return (
<PopupLayout
title="Add a List"
submitText="Submit"
onHide={toggleAddListPopup}
onSubmit={this.onSubmit}
>
<div>
<FormGroup>
<Label>{t('sourceListsTab.popup.enterListName')}</Label>
<Input
type="text"
value={this.state.name}
onChange={this.handleChange}
/>
</FormGroup>
</div>
</PopupLayout>
)
}
}
export default translate(['tabsContent', 'common'], { wait: true })(
SourceListsAddPopup
)
@@ -0,0 +1,65 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import PopupLayout from '../../../../common/Popups/PopupLayout'
import { FormGroup, Input, Label } from 'reactstrap'
export class SourceListsClonePopup extends React.Component {
static propTypes = {
listToEdit: PropTypes.func.isRequired,
toggleCloneListPopup: PropTypes.func.isRequired,
cloneSourceList: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
}
constructor(props) {
super(props)
this.state = {
name:
props.listToEdit && props.listToEdit.name
? `${props.listToEdit.name} (copy)`
: ''
}
}
handleChange = (e) => {
const { value } = e.target
this.setState({
name: value
})
}
onSubmit = () => {
const { listToEdit, cloneSourceList } = this.props
cloneSourceList({
id: listToEdit.id,
name: this.state.name
})
}
render() {
const { toggleCloneListPopup, t } = this.props
return (
<PopupLayout
title="Clone"
submitText="sourceListsTab.popup.cloneListSubmitBtn"
onHide={toggleCloneListPopup}
onSubmit={this.onSubmit}
>
<FormGroup>
<Label>{t('sourceListsTab.popup.renameListTitle')}</Label>
<Input
type="text"
value={this.state.name}
onChange={this.handleChange}
/>
</FormGroup>
</PopupLayout>
)
}
}
export default translate(['tabsContent', 'common'], { wait: true })(
SourceListsClonePopup
)
@@ -0,0 +1,42 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate, Interpolate } from 'react-i18next'
import PopupLayout from '../../../../common/Popups/PopupLayout'
import { getTitle } from '../../../../../common/helper';
export class SourceListsDeletePopup extends React.Component {
static propTypes = {
listToEdit: PropTypes.func.isRequired,
toggleDeleteListPopup: PropTypes.func.isRequired,
deleteSourceList: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
onSubmit = () => {
const { listToEdit, deleteSourceList } = this.props
deleteSourceList(listToEdit)
};
render () {
const { listToEdit, toggleDeleteListPopup } = this.props
const value = listToEdit.name || listToEdit.title || ''
return (
<PopupLayout
title='sourceListsTab.popup.deleteListTitle'
submitText='Delete'
onHide={toggleDeleteListPopup}
onSubmit={this.onSubmit}
submitColor="danger"
>
<Interpolate
i18nKey='sourceListsTab.popup.deleteListDesc'
name={getTitle(value)}
/>
</PopupLayout>
)
}
}
export default translate(['tabsContent', 'common'], { wait: true })(SourceListsDeletePopup)
@@ -0,0 +1,64 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import PopupLayout from '../../../../common/Popups/PopupLayout'
import { FormGroup, Input, Label } from 'reactstrap'
export class SourceListsRenamePopup extends React.Component {
static propTypes = {
listToEdit: PropTypes.func.isRequired,
toggleRenameListPopup: PropTypes.func.isRequired,
renameSourceList: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
}
constructor(props) {
super(props)
this.state = {
name: (props.listToEdit && props.listToEdit.name) || ''
}
}
handleChange = (e) => {
const { value } = e.target
this.setState({
name: value
})
}
onSubmit = () => {
const { listToEdit, renameSourceList } = this.props
const data = {
id: listToEdit.id,
name: this.state.name
}
renameSourceList(data, listToEdit.name)
}
render() {
const { toggleRenameListPopup, t } = this.props
return (
<PopupLayout
title="Rename"
submitText="sourceListsTab.popup.renameListSubmitBtn"
onHide={toggleRenameListPopup}
onSubmit={this.onSubmit}
>
<FormGroup>
<Label>{t('sourceListsTab.popup.renameListTitle')}</Label>
<Input
type="text"
value={this.state.name}
onChange={this.handleChange}
/>
</FormGroup>
</PopupLayout>
)
}
}
export default translate(['tabsContent', 'common'], { wait: true })(
SourceListsRenamePopup
)
@@ -0,0 +1,51 @@
import React, { Fragment } from 'react'
import PropTypes from 'prop-types'
import SourceLists from './SourceLists'
import SourcesOfList from './SourcesOfList'
import { withRouter } from 'react-router-dom'
import reduxConnect from '../../../../../redux/utils/connect'
import { compose } from 'redux'
import { setDocumentData } from '../../../../../common/helper'
class SourceListsSubTab extends React.Component {
static propTypes = {
sourcesState: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired
}
componentDidMount() {
setDocumentData('title', 'Source Lists | Search')
}
componentWillUnmount() {
setDocumentData('title')
}
render() {
const { sourcesState, actions } = this.props
const { sourcesOfListState, sourceListsState } = sourcesState
const sourcesOfListVisible = sourcesOfListState.isSourcesOfListVisible
return (
<Fragment>
{!sourcesOfListVisible && (
<SourceLists sourceListsState={sourceListsState} actions={actions} />
)}
{sourcesOfListVisible && (
<SourcesOfList
sourcesOfListState={sourcesOfListState}
actions={actions}
/>
)}
</Fragment>
)
}
}
const applyDecorators = compose(
withRouter,
reduxConnect('sourcesState', ['appState', 'sourcesState'])
)
export default applyDecorators(SourceListsSubTab)
@@ -0,0 +1,256 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import moment from 'moment'
import Table from '../../../../common/Table/Table'
import SortableTh from '../../../../common/Table/SortableTh'
import { Button } from 'reactstrap'
export class SourceListsTable extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
tableState: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired
}
onFetch = (page, pageSize, sorted) => {
const { actions, tableState } = this.props
const params = {
page: page + 1,
limit: pageSize,
onlyShared: tableState.onlyGlobal
}
if (sorted.length) {
const sortedField = sorted[0]
const sort = {
field: sortedField.id,
direction: sortedField.desc ? 'desc' : 'asc'
}
params['sort'] = sort
}
actions.getMainSourceLists(params)
}
showDeleteListPopup = (item) => () => {
this.props.actions.toggleDeleteListPopup(item)
}
showRenameListPopup = (item) => () => {
this.props.actions.toggleRenameListPopup(item)
}
showCloneListPopup = (item) => () => {
this.props.actions.toggleCloneListPopup(item)
}
showSourcesOfList = (item) => () => {
this.props.actions.showSourcesOfList(item)
}
onShareList = (id) => () => {
this.props.actions.shareSourceList(id)
}
onUnshareList = (id) => () => {
this.props.actions.unshareSourceList(id)
}
getColumns() {
const { t } = this.props
let columns = [
{
Header: <SortableTh title="sourceListsTab.tableLabels.name" />,
accessor: 'name',
Cell: ({ original }) => {
return (
<a
href="#"
onClick={this.showSourcesOfList(original)}
>
{original.name}
</a>
)
}
},
{
id: 'sources',
Header: <SortableTh title="sourceListsTab.tableLabels.sources" />,
accessor: (item) => item.sourceNumber
},
{
id: 'createdBy',
Header: <SortableTh title="sourceListsTab.tableLabels.createdBy" />,
accessor: (item) => `${item.user.firstName} ${item.user.lastName}`
},
{
id: 'lastUpdated',
Header: <SortableTh title="sourceListsTab.tableLabels.lastUpdated" />,
accessor: (item) =>
item.updatedAt && moment(item.updatedAt).format('Do MMM YYYY')
},
{
id: 'lastUpdatedBy',
Header: <SortableTh title="sourceListsTab.tableLabels.lastUpdatedBy" />,
accessor: (item) =>
item.updatedBy &&
`${item.updatedBy.firstName} ${item.updatedBy.lastName}`
},
{
id: 'action',
Header: t('sourceIndexTab.action'),
// sortable: false,
minWidth: 220,
Cell: ({ original }) => {
return (
// <UncontrolledButtonDropdown>
// <DropdownToggle
// // caret
// // className="btn-icon btn-icon-only btn btn-link"
// color="link"
// >
// <i className="lnr-menu-circle btn-icon-wrapper" />
// </DropdownToggle>
// <DropdownMenu>
// <h2>Hello</h2>
// {/* <DropdownItem onClick={this.onUnshareList(original.id)}>
// <i className="dropdown-icon lnr-inbox"> </i>
// <span>{t("sourceListsTab.unshare")}</span>
// </DropdownItem>
// <DropdownItem onClick={this.onShareList(original.id)}>
// <i className="dropdown-icon lnr-file-empty"> </i>
// <span>{t("sourceListsTab.share")}</span>
// </DropdownItem>
// <DropdownItem onClick={this.showRenameListPopup(original)}>
// <i className="dropdown-icon lnr-book"> </i>
// <span>{t("sourceListsTab.rename")}</span>
// </DropdownItem>
// <DropdownItem onClick={this.showCloneListPopup(original)}>
// <i className="dropdown-icon lnr-picture"> </i>
// <span>{t("sourceListsTab.clone")}</span>
// </DropdownItem>
// <DropdownItem onClick={this.showDeleteListPopup(original)}>
// <i className="dropdown-icon lnr-picture"> </i>
// <span>{t("sourceListsTab.delete")}</span>
// </DropdownItem> */}
// </DropdownMenu>
// </UncontrolledButtonDropdown>
// <div className="d-block w-100 text-center">
// <UncontrolledButtonDropdown>
// <DropdownToggle
// caret
// className="btn-icon btn-icon-only btn btn-link"
// color="link"
// >
// <i className="lnr-menu-circle btn-icon-wrapper" />
// </DropdownToggle>
// <DropdownMenu className="rm-pointers dropdown-menu-hover-link">
// <DropdownItem onClick={this.onUnshareList(original.id)}>
// <i className="dropdown-icon lnr-inbox"> </i>
// <span>{t("sourceListsTab.unshare")}</span>
// </DropdownItem>
// <DropdownItem onClick={this.onShareList(original.id)}>
// <i className="dropdown-icon lnr-file-empty"> </i>
// <span>{t("sourceListsTab.share")}</span>
// </DropdownItem>
// <DropdownItem onClick={this.showRenameListPopup(original)}>
// <i className="dropdown-icon lnr-book"> </i>
// <span>{t("sourceListsTab.rename")}</span>
// </DropdownItem>
// <DropdownItem onClick={this.showCloneListPopup(original)}>
// <i className="dropdown-icon lnr-picture"> </i>
// <span>{t("sourceListsTab.clone")}</span>
// </DropdownItem>
// <DropdownItem onClick={this.showDeleteListPopup(original)}>
// <i className="dropdown-icon lnr-picture"> </i>
// <span>{t("sourceListsTab.delete")}</span>
// </DropdownItem>
// </DropdownMenu>
// </UncontrolledButtonDropdown>
// </div>
<div>
<Button
outline
size="sm"
color="info"
className="border-0"
onClick={
original.shared
? this.onUnshareList(original.id)
: this.onShareList(original.id)
}
>
{original.shared
? t('sourceListsTab.unshare')
: t('sourceListsTab.share')}
</Button>
<Button
outline
size="sm"
color="info"
className="border-0"
onClick={this.showRenameListPopup(original)}
>
{t('sourceListsTab.rename')}
</Button>
<Button
outline
size="sm"
color="info"
className="border-0"
onClick={this.showCloneListPopup(original)}
>
{t('sourceListsTab.clone')}
</Button>
<Button
outline
size="sm"
color="secondary"
className="border-0"
onClick={this.showDeleteListPopup(original)}
>
{t('sourceListsTab.delete')}
</Button>
</div>
)
}
}
]
const cols = [
'name',
'sources',
'createdBy',
'lastUpdated',
'lastUpdatedBy',
'action'
]
return columns.filter(
(col) => cols.includes(col.id) || cols.includes(col.accessor)
)
}
render() {
const { tableState } = this.props
const columns = this.getColumns()
return (
<div className="sources-table">
<Table
columns={columns}
data={tableState.data}
totalCount={tableState.totalCount}
showTotalCount
limit={tableState.limit}
page={tableState.page}
isLoading={tableState.isLoading}
onFetchData={this.onFetch}
/>
</div>
)
}
}
export default translate(['tabsContent'], { wait: true })(SourceListsTable)

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