at the end of the day, it was inevitable
This commit is contained in:
@@ -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> </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)
|
||||
);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
+293
@@ -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);
|
||||
+83
@@ -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;
|
||||
+272
@@ -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);
|
||||
+276
@@ -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);
|
||||
+357
@@ -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));
|
||||
+290
@@ -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);
|
||||
+722
@@ -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));
|
||||
+403
@@ -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));
|
||||
+255
@@ -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));
|
||||
+284
@@ -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));
|
||||
+208
@@ -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:
|
||||
'© <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);
|
||||
+17
@@ -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
|
||||
}
|
||||
+58
@@ -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));
|
||||
+170
@@ -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)
|
||||
+90
@@ -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
|
||||
)
|
||||
+69
@@ -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)
|
||||
+26
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
+139
@@ -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
|
||||
)
|
||||
+118
@@ -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
|
||||
+33
@@ -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);
|
||||
+53
@@ -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);
|
||||
+61
@@ -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)
|
||||
+111
@@ -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);
|
||||
+94
@@ -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);
|
||||
+40
@@ -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
|
||||
+56
@@ -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);
|
||||
+64
@@ -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);
|
||||
+75
@@ -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;
|
||||
+160
@@ -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
|
||||
);
|
||||
+122
@@ -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)
|
||||
+111
@@ -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);
|
||||
+143
@@ -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);
|
||||
+186
@@ -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)
|
||||
+195
@@ -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)
|
||||
+106
@@ -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)
|
||||
+55
@@ -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
|
||||
)
|
||||
+65
@@ -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
|
||||
)
|
||||
+42
@@ -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)
|
||||
|
||||
+64
@@ -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
|
||||
)
|
||||
+51
@@ -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)
|
||||
+256
@@ -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
Reference in New Issue
Block a user