at the end of the day, it was inevitable

This commit is contained in:
Mo Elzubeir
2022-12-09 08:36:26 -06:00
commit 1218570914
1768 changed files with 887087 additions and 0 deletions
@@ -0,0 +1,548 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import TimeAgo from 'timeago-react';
import ArticleComment from './ArticleComment';
import {
UncontrolledDropdown,
DropdownToggle,
DropdownMenu,
DropdownItem,
CustomInput,
Button
} from 'reactstrap';
import ShareMenu from './ShareMenu';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faFacebook,
faInstagram,
faPinterest,
faReddit,
faTumblr,
faTwitter,
faYoutube
} from '@fortawesome/free-brands-svg-icons';
import {
faComments,
faEye,
faFrown,
faMeh,
faQuoteLeft,
faShareAlt,
faSmile,
faThumbsDown,
faThumbsUp
} from '@fortawesome/free-solid-svg-icons';
import {
capOnlyFirstLetter,
convertUTCtoLocal,
abbreviateNumber,
notNullAndUnd
} from '../../../../../common/helper';
import SourceIndexInfoPopup from '../SourceIndexSubTab/SourceIndexInfoPopup';
const icons = {
twitter: faTwitter,
facebook: faFacebook,
instagram: faInstagram,
tumblr: faTumblr,
pinterest: faPinterest,
reddit: faReddit,
youtube: faYoutube,
POSITIVE: faSmile,
NEGATIVE: faFrown,
NEUTRAL: faMeh
};
const colors = {
POSITIVE: '#3ac47d',
NEGATIVE: '#FC3939',
NEUTRAL: '#868e96',
twitter: '#1DA1F2',
facebook: '#4267B2',
reddit: '#FF5700',
instagram: '#8a3ab9',
tumblr: '#34526F',
pinterest: '#E60023',
youtube: '#FF0000'
};
export class Article extends React.Component {
constructor() {
super();
this.state = {
shareMenu: false,
imgErr: false,
sourceModal: false
};
this.elemDesc = React.createRef();
}
selectArticle = () => {
this.props.selectArticle(this.props.article);
};
showEmailPopup = () => {
this.props.showEmailPopup([this.props.article]);
};
showCommentPopup = () => {
this.props.showCommentPopup(this.props.article);
};
showDeletePopup = () => {
this.props.showDeletePopup([this.props.article]);
};
showClipPopup = () => {
this.props.showClipPopup([this.props.article]);
};
toggleShareMenu = () => {
this.setState((prev) => ({ shareMenu: !prev.shareMenu }));
};
loadMoreComments = () => {
const {
loadMoreComments,
article: {
id: articleId,
comments: { count: offset }
}
} = this.props;
loadMoreComments(articleId, offset);
};
readLater = () => {
this.props.readArticleLater(this.props.article);
};
onImgError = () => {
this.setState({ imgErr: true });
};
toggleSourceModal = () => {
this.setState((prev) => ({ sourceModal: !prev.sourceModal }));
};
render() {
const { article, t, i18n, showCommentPopup, deleteComment } = this.props;
let {
comments,
id,
source,
sentiment,
permalink,
publisher,
title,
image,
author,
content,
published,
mentions,
tags,
likes,
dislikes,
views,
shares,
categories
} = article;
const { imgErr } = this.state;
const {
data: commentsData,
count: commentsCount, // should get real post comment count
totalCount: commentsTotalCount
} = comments;
const isArticleChosen = !!this.props.selectedArticles.find(
(item) => item.id === id
);
const offsetWidth =
this.elemDesc &&
this.elemDesc.current &&
this.elemDesc.current.offsetWidth;
const hasRightCounters =
notNullAndUnd(likes) ||
notNullAndUnd(dislikes) ||
commentsCount || // add not null and undefined when counter shows
notNullAndUnd(views) ||
notNullAndUnd(shares) ||
notNullAndUnd(mentions);
const isTwitter = source.siteType === 'twitter';
const isInstagram = source.siteType === 'instagram';
let username;
if (isTwitter) {
username =
author.link &&
author.link.match(
/^https?:\/\/(www\.)?twitter\.com\/(#!\/)?([^\/]+)(\/\w+)*$/
);
username = username && username[3];
}
if (isInstagram) {
username =
author.link &&
author.link.match(
/(?:(?:http|https):\/\/)?(?:www\.)?(?:instagram\.com|instagr\.am)\/([A-Za-z0-9-_\.]+)/
);
username = username && username[1];
}
const isRTL = document.documentElement.dir === 'rtl';
return (
<div className="post border b-radius-5 mb-4">
<UncontrolledDropdown className="post__menu">
<DropdownToggle
outline
color="primary"
className="btn-icon btn-icon-only p-1 m-2"
>
<i className="lnr lnr-menu btn-icon-wrapper" />
</DropdownToggle>
<DropdownMenu className={isRTL ? ' dropdown-menu-left' : ''}>
<DropdownItem
className="text-muted"
onClick={this.showCommentPopup}
>
<i className="mr-2 fa fa-comments"> </i>
<span>{t('searchTab.commentBtn')}</span>
</DropdownItem>
<DropdownItem className="text-muted" onClick={this.showClipPopup}>
<i className="mr-2 fa fa-cut"> </i>
<span>{t('searchTab.clipBtn')}</span>
</DropdownItem>
<DropdownItem className="text-muted" onClick={this.readLater}>
<i className="mr-2 fa fa-bookmark"> </i>
<span>{t('searchTab.readLaterBtn')}</span>
</DropdownItem>
<DropdownItem className="text-muted" onClick={this.readLater}>
<i className="mr-2 fa fa-archive"> </i>
<span>{t('searchTab.archiveBtn')}</span>
</DropdownItem>
<DropdownItem className="text-muted" onClick={this.showEmailPopup}>
<i className="mr-2 fa fa-envelope"> </i>
<span>{t('searchTab.emailBtn')}</span>
</DropdownItem>
<DropdownItem className="text-muted" onClick={this.toggleShareMenu}>
<i className="mr-2 fa fa-share-alt"> </i>
<span>{t('searchTab.shareBtn')}</span>
</DropdownItem>
<DropdownItem className="text-muted" onClick={this.showDeletePopup}>
<i className="mr-2 fa fa-trash"> </i>
<span>{t('searchTab.deleteBtn')}</span>
</DropdownItem>
</DropdownMenu>
</UncontrolledDropdown>
<div className="d-flex flex-row">
<div className="post__icons">
<CustomInput
id={'article-check-' + id}
type="checkbox"
className="mb-3"
onChange={this.selectArticle}
checked={isArticleChosen}
/>
{source.siteType && (
<FontAwesomeIcon
title={capOnlyFirstLetter(source.siteType)}
icon={icons[source.siteType]}
size="lg"
className="fa-w-16 mb-3"
color={colors[source.siteType]}
/>
)}
{sentiment && (
<FontAwesomeIcon
title={capOnlyFirstLetter(sentiment)}
icon={icons[sentiment]}
className="mb-3"
size="lg"
color={colors[sentiment]}
/>
)}
</div>
<div className="post_middlepart">
<h2 className="post__title">
{title && (
<a href={permalink} target="_blank" rel="noopener noreferrer">
{title}
</a>
)}
</h2>
<div
ref={this.elemDesc}
className={`post__content${
offsetWidth && offsetWidth < 430 ? ' flex-column' : ''
}`}
>
{image &&
!imgErr &&
(!title && permalink ? (
<a href={permalink} target="_blank" rel="noopener noreferrer">
<img
id={id}
width="180px"
className="post__img mb-2 mb-lg-0 mr-3"
src={image}
onError={this.onImgError}
/>
</a>
) : (
<img
id={id}
width="180px"
className="post__img mb-2 mb-lg-0 mr-3"
src={image}
onError={this.onImgError}
/>
))}
<div>
{author.name ? (
author.link ? (
<a
className="d-inline-block hover-link text-muted mb-2"
href={author.link}
target="_blank"
>
{username ? `@${username}` : author.name}
</a>
) : (
<p className="text-muted mb-2">{author.name}</p>
)
) : null}
{!title && permalink ? (
<a
href={permalink}
target="_blank"
rel="noopener noreferrer"
className="post__desc-link"
>
<p
className="post__desc"
dangerouslySetInnerHTML={{ __html: content }}
></p>
</a>
) : (
<p
className="post__desc"
dangerouslySetInnerHTML={{ __html: content }}
></p>
)}
</div>
</div>
{tags && tags.length && tags.length > 0 && (
<div className="post__tags mt-2">
<strong>{t('searchTab.tags')}</strong>: {tags.join(', ')}
</div>
)}
{categories && categories.length > 0 && (
<p className="post__tags my-2">
<strong>{t('searchTab.categories')}</strong>:{' '}
{categories.join(', ')}
</p>
)}
<div className="post__about-info text-muted mt-3">
{published && (
<Fragment>
<span
className="d-inline-block"
title={convertUTCtoLocal(published, 'MM/DD/YYYY HH:mm:ss')}
>
<TimeAgo
datetime={published}
locale={i18n.language}
opts={{ minInterval: 60 }}
/>
</span>
<span className="mx-2">|</span>
</Fragment>
)}
{source.type && (
<Fragment>
<span>{capOnlyFirstLetter(source.type)}</span>
<span className="mx-2">|</span>
</Fragment>
)}
{source.country && (
<Fragment>
<span>{source.country}</span>
<span className="mx-2">|</span>
</Fragment>
)}
{publisher && (
<Fragment>
<Button
color="link"
className="btn-anchor"
title="Click to see details"
onClick={this.toggleSourceModal}
>
{publisher}
</Button>
<span className="mx-2">|</span>
</Fragment>
)}
{source.title && (
<Fragment>
{publisher ? (
<a
href={source.link}
style={{ overflowWrap: 'anywhere' }}
rel="noopener noreferrer"
target="_blank"
>
{source.title}
</a>
) : (
<Button
color="link"
className="btn-anchor"
title="Click to see details"
onClick={this.toggleSourceModal}
>
{(isTwitter || isInstagram) && author.name
? author.name
: source.title}
</Button>
)}
</Fragment>
)}
</div>
</div>
{hasRightCounters && (
<div className="post__extras p-3">
<div className="post__icons-wrapper">
{notNullAndUnd(likes) && (
<div className="post__icon-metrics mb-1">
<FontAwesomeIcon
title="Likes"
icon={faThumbsUp}
className="text-success"
/>
<p className="ml-2" title={likes}>
{abbreviateNumber(likes)}
</p>
</div>
)}
{notNullAndUnd(dislikes) && (
<div className="post__icon-metrics mb-1">
<FontAwesomeIcon title="Dislikes" icon={faThumbsDown} />
<p className="ml-2" title={dislikes}>
{abbreviateNumber(dislikes)}
</p>
</div>
)}
{/* {notNullAndUnd(commentsCount) && (
Add above line when real comment counts are visible
*/}
{commentsCount ? (
<div className="post__icon-metrics mb-1">
<FontAwesomeIcon title="Comments" icon={faComments} />
<p className="ml-2" title={commentsCount}>
{abbreviateNumber(commentsCount)}
</p>
</div>
) : (
''
)}
{notNullAndUnd(views) && (
<div className="post__icon-metrics mb-1">
<FontAwesomeIcon title="Viwes" icon={faEye} />
<p className="ml-2 text-center" title={views}>
{abbreviateNumber(views)}
</p>
</div>
)}
{notNullAndUnd(shares) && (
<div className="post__icon-metrics mb-1">
<FontAwesomeIcon title="Shares" icon={faShareAlt} />
<p className="ml-2 text-center" title={shares}>
{abbreviateNumber(shares)}
</p>
</div>
)}
{notNullAndUnd(mentions) && (
<div className="post__icon-metrics mb-1">
<FontAwesomeIcon title="Mentions" icon={faQuoteLeft} />
<p className="ml-2 text-center" title={mentions}>
{abbreviateNumber(mentions)}
</p>
</div>
)}
</div>
</div>
)}
</div>
{commentsData && commentsData.length > 0 && (
<div className="post__comments border-top px-3 pb-3">
{commentsData.map((comment) => {
return (
<ArticleComment
article={article}
comment={comment}
showCommentPopup={showCommentPopup}
deleteComment={deleteComment}
key={comment.id}
/>
);
})}
{commentsCount < commentsTotalCount && (
<Button
outline
size="sm"
color="light"
className="mt-2 d-block ml-auto btn-icon"
onClick={this.loadMoreComments}
>
<i className="lnr lnr-chevron-down btn-icon-wrapper" />{' '}
{t('searchTab.moreComments')}
</Button>
)}
</div>
)}
{this.state.shareMenu && (
<ShareMenu article={article} hideMenu={this.toggleShareMenu} />
)}
{this.state.sourceModal && (
<SourceIndexInfoPopup
source={article.source}
hideSourceInfoPopup={this.toggleSourceModal}
/>
)}
</div>
);
}
}
Article.propTypes = {
article: PropTypes.object.isRequired,
selectedArticles: PropTypes.array.isRequired,
selectArticle: PropTypes.func.isRequired,
showEmailPopup: PropTypes.func.isRequired,
showDeletePopup: PropTypes.func.isRequired,
showCommentPopup: PropTypes.func.isRequired,
showClipPopup: PropTypes.func.isRequired,
deleteComment: PropTypes.func.isRequired,
readArticleLater: PropTypes.func.isRequired,
loadMoreComments: PropTypes.func.isRequired,
showShareMenu: PropTypes.func.isRequired,
i18n: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
};
export default translate(['tabsContent'], { wait: true })(Article);
@@ -0,0 +1,66 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate, Interpolate } from 'react-i18next'
import TimeAgo from 'timeago-react'
import { Button } from 'reactstrap'
export class ArticleComment extends React.Component {
static propTypes = {
article: PropTypes.object.isRequired,
comment: PropTypes.func.isRequired,
deleteComment: PropTypes.func.isRequired,
showCommentPopup: PropTypes.func.isRequired,
i18n: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
}
onEdit = () => {
const { showCommentPopup, article, comment } = this.props
showCommentPopup(article, comment)
}
onDelete = () => {
const { deleteComment, article, comment } = this.props
deleteComment(comment.id, article.id)
}
render() {
const { comment, i18n } = this.props
return (
<div className="post__comment mt-2">
<div className="d-flex justify-content-between">
<div>
<cite className="post__commentor mr-3">
<Interpolate
i18nKey="searchTab.commentMetadata"
author={`${comment.author.firstName} ${comment.author.lastName}`}
/>
</cite>
<span className="post__cmttime mr-3 text-muted">
<TimeAgo
datetime={comment.createdAt}
locale={i18n.language}
opts={{ minInterval: 30 }}
/>
</span>
</div>
<div>
<Button color="link" className="p-0" onClick={this.onEdit}>
<i className="lnr lnr-pencil"></i>
</Button>
<Button color="link" className="ml-2 p-0" onClick={this.onDelete}>
<i className="lnr lnr-trash"></i>
</Button>
</div>
</div>
<p className="post__cmt-content">
<strong className="d-block mb-1">{comment.title}</strong>
{comment.content}
</p>
</div>
)
}
}
export default translate(['tabsContent'], { wait: true })(ArticleComment)
@@ -0,0 +1,90 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import ClipDragSource from './ClipDragSource'
import RecentFeed from './RecentFeed'
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'
export class ClipArticlesPopup extends React.Component {
static propTypes = {
hidePopup: PropTypes.func.isRequired,
clipArticles: PropTypes.func.isRequired,
articles: PropTypes.array.isRequired,
recentClipFeeds: PropTypes.array.isRequired,
getRecentClipFeeds: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
}
hidePopupFromOutside = (e) => {
if (e.target === e.currentTarget) this.hidePopup()
}
hidePopup = () => {
this.props.hidePopup()
}
onSubmit = () => {
this.hidePopup()
}
componentWillMount = () => {
this.props.getRecentClipFeeds()
}
onRecentFeedClick = (feed) => {
this.props.clipArticles(feed.id)
this.props.hidePopup()
}
render() {
const { t, articles, recentClipFeeds } = this.props
return (
<Modal
isOpen
toggle={this.hidePopup}
backdrop={false}
modalClassName="pointer-events-none"
>
<ModalHeader toggle={this.hidePopup}>
{t('searchTab.clipPopup.header')}
</ModalHeader>
<ModalBody>
<div className="text-center">
<p>{t('searchTab.clipPopup.hint1')}</p>
<div className="draggable-container">
<ClipDragSource articles={articles} />
</div>
{recentClipFeeds && recentClipFeeds.length > 0 && (
<div className="mt-2">
<p className="mb-2">{t('searchTab.clipPopup.hint2')}</p>
<div className="d-flex justify-content-center flex-wrap">
{recentClipFeeds.map((feed) => {
return (
<RecentFeed
onRecentFeedClick={this.onRecentFeedClick}
key={feed.id}
feed={feed}
/>
)
})}
</div>
</div>
)}
</div>
</ModalBody>
<ModalFooter>
<Button color="light" onClick={this.hidePopup}>
{t('common:commonWords.Cancel')}
</Button>
</ModalFooter>
</Modal>
)
}
}
export default translate(['tabsContent', 'common'], { wait: true })(
ClipArticlesPopup
)
@@ -0,0 +1,69 @@
import React from 'react'
import PropTypes from 'prop-types'
import { TYPES } from '../../../../../../redux/modules/appState/sidebar'
import { Interpolate } from 'react-i18next'
import { DragSource } from 'react-dnd'
const source = {
beginDrag (props, monitor, component) {
setTimeout(() => {
component.setState({
isDragging: true
})
}, 0)
return {
type: TYPES.CLIP_ARTICLE
}
},
endDrag (props, monitor, component) {
component.setState({
isDragging: false
})
}
}
/**
* Specifies which props to inject into component from Drag n Drop.
*/
function collect (connect) {
return {
// Call this function inside render()
// to let React DnD handle the drag events:
connectDragSource: connect.dragSource()
}
}
export class ClipDragSource extends React.Component {
static propTypes = {
articles: PropTypes.array.isRequired,
connectDragSource: PropTypes.func.isRequired
};
constructor (props) {
super(props)
this.state = {
isDragging: false
}
}
render () {
const style = {
visibility: this.state.isDragging ? 'hidden' : 'visible'
}
return this.props.connectDragSource(
<div className="draggable-item" style={style}>
<span className="drag-handle" />
<Interpolate
i18nKey='searchTab.clipPopup.clippedArticles'
count={this.props.articles.length}
/>
</div>
)
}
}
export default DragSource(TYPES.CLIP_ARTICLE, source, collect)(ClipDragSource)
@@ -0,0 +1,26 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Button } from 'reactstrap'
export default class RecentFeed extends React.Component {
static propTypes = {
feed: PropTypes.object.isRequired,
onRecentFeedClick: PropTypes.func.isRequired
};
onClick = () => {
this.props.onRecentFeedClick(this.props.feed)
}
render () {
const { feed } = this.props
return (
<Button color="light" className={'mr-2 mb-2 feed-icon ' + feed.class} onClick={this.onClick}>
{feed.name}
</Button>
)
}
}
@@ -0,0 +1,139 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate, Interpolate } from 'react-i18next';
import TimeAgo from 'timeago-react';
import {
Button,
Input,
Modal,
ModalBody,
ModalFooter,
ModalHeader
} from 'reactstrap';
const initCharactersCount = 5000;
export class CommentArticlePopup extends React.Component {
static propTypes = {
article: PropTypes.object.isRequired,
comment: PropTypes.object,
commentArticle: PropTypes.func.isRequired,
updateComment: PropTypes.func.isRequired,
hidePopup: PropTypes.func.isRequired,
i18n: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
};
constructor(props) {
super(props);
const content = props.comment ? props.comment.content : '';
this.state = {
charactersCount: initCharactersCount - content.length,
title: props.comment ? props.comment.title : '',
comment: content
};
}
handleTitleChange = (e) => {
const { value } = e.target;
this.setState({ title: value });
};
hidePopup = () => {
this.props.hidePopup();
};
onSubmit = () => {
const newComment = {
title: this.state.title,
content: this.state.comment
};
if (this.props.comment) {
//edit exisitng
this.props.updateComment(newComment, this.props.article.id);
} else {
//create new comment
this.props.commentArticle(newComment, this.props.article.id);
}
this.hidePopup();
};
onChangeComment = (e) => {
const charactersCount = initCharactersCount - e.target.value.length;
if (charactersCount >= 0) {
this.setState({
charactersCount: charactersCount,
comment: e.target.value
});
}
};
render() {
const { t, i18n, article, comment } = this.props;
const popupTitle = comment
? t('searchTab.commentPopup.editUserComment')
: t('searchTab.commentPopup.addUserComment');
return (
<Modal isOpen toggle={this.hidePopup} backdrop="static">
<ModalHeader toggle={this.hidePopup}>{popupTitle}</ModalHeader>
<ModalBody>
<div className="mb-3">
<a
className="font-size-lg"
href={article.permalink}
target="_blank"
rel="noopener noreferrer"
>
{article.title}
</a>
<p>{article.author.name}</p>
<p className="font-size-xs text-muted">
<TimeAgo
datetime={article.published}
locale={i18n.language}
opts={{ minInterval: 30 }}
/>
</p>
</div>
<Input
value={this.state.title}
type="text"
className="mb-2"
onChange={this.handleTitleChange}
placeholder={t('searchTab.commentPopup.inputTitlePlaceholder')}
/>
<Input
rows="3"
type="textarea"
value={this.state.comment}
onChange={this.onChangeComment}
placeholder={t('searchTab.commentPopup.commentPlanceholder')}
/>
<p className="font-size-xs text-muted text-right mt-1">
<Interpolate
i18nKey="searchTab.commentPopup.charactersLeft"
count={this.state.charactersCount}
/>
</p>
</ModalBody>
<ModalFooter>
<Button color="light" onClick={this.hidePopup}>
{t('common:commonWords.Cancel')}
</Button>
<Button color="primary" onClick={this.onSubmit}>
{t('common:commonWords.submit')}
</Button>
</ModalFooter>
</Modal>
);
}
}
export default translate(['tabsContent', 'common'], { wait: true })(
CommentArticlePopup
);
@@ -0,0 +1,65 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Interpolate, translate } from 'react-i18next';
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
export class DeleteArticlesPopup extends React.Component {
static propTypes = {
articles: PropTypes.array.isRequired,
activeFeed: PropTypes.object,
hidePopup: PropTypes.func.isRequired,
deleteArticles: PropTypes.func.isRequired,
deleteArticlesFromFeed: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
onSubmit = () => {
const {
articles,
activeFeed,
deleteArticles,
deleteArticlesFromFeed,
hidePopup
} = this.props;
const ids = articles.map((a) => a.id);
if (activeFeed) {
deleteArticlesFromFeed(ids, activeFeed.id);
} else {
deleteArticles(ids);
}
hidePopup();
};
render() {
const { t, articles, hidePopup } = this.props;
return (
<Modal isOpen toggle={hidePopup} backdrop="static">
<ModalHeader toggle={hidePopup}>{t('commonWords.Confirm')}</ModalHeader>
<ModalBody>
<p>
{articles.length > 1 ? (
<Interpolate
t={t}
i18nKey="tabsContent:searchTab.deleteArticlePopupText_plural"
articlesLength={articles.length}
/>
) : (
t('tabsContent:searchTab.deleteArticlePopupText')
)}
</p>
</ModalBody>
<ModalFooter>
<Button color="light" onClick={hidePopup}>
{t('commonWords.Cancel')}
</Button>
<Button color="danger" onClick={this.onSubmit}>
{t('commonWords.Delete')}
</Button>
</ModalFooter>
</Modal>
);
}
}
export default translate(['common'], { wait: true })(DeleteArticlesPopup);
@@ -0,0 +1,209 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import moment from 'moment'
import Select from 'react-select'
import {
Button,
Modal,
ModalHeader,
ModalBody,
Label,
Input,
ModalFooter,
FormGroup,
Col,
Container
} from 'reactstrap'
import QuillEditor from '../../../../common/QuillEditor'
const replyToEmail = 'support@socialhose.io'
export class EmailArticlesPopup extends React.Component {
static propTypes = {
articlesToEmail: PropTypes.array.isRequired,
emailArticles: PropTypes.func.isRequired,
hidePopup: PropTypes.func.isRequired,
recipients: PropTypes.object.isRequired,
loadRecipients: PropTypes.func.isRequired,
children: PropTypes.any,
t: PropTypes.func.isRequired
}
constructor(props) {
super(props)
this.state = {
selectedRecipients: ''
}
this.editorRef = React.createRef()
}
componentWillMount = () => {
!this.props.recipients.all.length && this.props.loadRecipients()
}
componentDidMount = () => {
this.props.loadRecipients()
}
hidePopup = () => {
this.props.hidePopup()
}
collectParams = () => { // need to change with states
const recipients = this.state.selectedRecipients
if (!recipients) return false
return {
emailTo: recipients.map((r) => r.value),
emailReplyTo: document.getElementById('email-reply-to').value,
subject: document.getElementById('email-subject').value,
content: this.editorRef.current && this.editorRef.current.root.innerHTML
}
}
onSubmit = () => {
const params = this.collectParams()
if (params) {
this.props.emailArticles(params)
}
}
changeRecipient = (value) => {
this.setState({
selectedRecipients: value
})
}
validEmails = (str) => {
const re = /\S+@\S+\.\S+/
const arr = str.split(',')
for (let s of arr) {
if (!re.test(s)) {
return false
}
}
return true
}
emailRe = /\S+@\S+\.\S+/
isValidNewOption = ({ label }) => {
return this.emailRe.test(label)
}
promptTextCreator = (label) => {
return label
}
render() {
const { t, articlesToEmail, recipients } = this.props
const { selectedRecipients } = this.state
const recipientsAll = recipients.all.map((recipient) => ({
value: recipient,
label: recipient
}))
return (
<Modal
isOpen
size="lg"
toggle={this.hidePopup}
backdrop="static"
>
<ModalHeader toggle={this.hidePopup}>
{t('searchTab.emailPopup.header')}
</ModalHeader>
<ModalBody>
<Container>
<FormGroup row>
<Label htmlFor="email-to" sm={2}>
{t('searchTab.emailPopup.labelTo')}
</Label>
<Col sm={10}>
{recipients.pending && <i className="fa fa-spinner fa-pulse m-2" />}
{!recipients.pending && (
<Select.Creatable
multi
value={selectedRecipients}
options={recipientsAll}
onChange={this.changeRecipient}
isValidNewOption={this.isValidNewOption}
promptTextCreator={this.promptTextCreator}
noResultsText="Email not valid"
/>
)}
</Col>
</FormGroup>
<FormGroup row>
<Label htmlFor="email-reply-to" sm={2}>
{t('searchTab.emailPopup.labelReplyTo')}
</Label>
<Col sm={10}>
<Input
type="email"
id="email-reply-to"
defaultValue={replyToEmail}
/>
</Col>
</FormGroup>
<FormGroup row>
<Label htmlFor="email-subject" sm={2}>
{t('searchTab.emailPopup.labelSubject')}
</Label>
<Col sm={10}>
<Input type="text" id="email-subject" />
</Col>
</FormGroup>
<div className="email-popup">
<QuillEditor
className="email-popup__articles email-editor"
reference={this.editorRef}
id="email-editor"
>
{articlesToEmail.map((article) => {
return (
<div className="email-popup__article" key={article.id}>
<h2 className="article__title">
<a href={article.source.link}>{article.title}</a>
</h2>
<div className="article__about-info">
<a href={article.source.link} target="blank">
{article.source.title}
</a>{' '}
<span> | </span>
<a href={article.author.link} target="blank">
{article.author.name}
</a>{' '}
<span> | </span>
{moment(article.published).format('LLL')}
</div>
<p className="article__desc">{article.content}</p>
</div>
)
})}
</QuillEditor>
</div>
</Container>
{this.props.children}
</ModalBody>
<ModalFooter>
<Button color="light" onClick={this.hidePopup}>
{t('common:commonWords.Cancel')}
</Button>
<Button color="primary" onClick={this.onSubmit}>
{t('searchTab.emailPopup.submitBtn')}
</Button>
</ModalFooter>
</Modal>
)
}
}
export default translate(['tabsContent', 'common'], { wait: true })(
EmailArticlesPopup
)
@@ -0,0 +1,50 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'
class EmailConfirmPopup extends React.Component {
static propTypes = {
hidePopup: PropTypes.func.isRequired,
hideEmailPopup: PropTypes.func.isRequired,
sendDocumentsByEmail: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
}
hidePopup = () => {
this.props.hidePopup()
}
onSubmit = () => {
this.props.sendDocumentsByEmail()
this.hidePopup()
this.props.hideEmailPopup()
}
render() {
const { t } = this.props
return (
<Modal isOpen toggle={this.hidePopup} backdrop="static">
<ModalHeader toggle={this.hidePopup}>
{t('common:commonWords.Confirm')}
</ModalHeader>
<ModalBody>
<p>{t('searchTab.emailPopup.sendConfirmWithoutSubject')}</p>
</ModalBody>
<ModalFooter>
<Button color="light" onClick={this.hidePopup}>
{t('searchTab.emailPopup.dontSend')}
</Button>
<Button color="warning" onClick={this.onSubmit}>
{t('searchTab.emailPopup.sendAnyway')}
</Button>
</ModalFooter>
</Modal>
)
}
}
export default translate(['tabsContent', 'common'], { wait: true })(
EmailConfirmPopup
)
@@ -0,0 +1,175 @@
/* eslint-disable react/jsx-no-bind */
import React, { Fragment, useState } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import { translate } from 'react-i18next';
import SearchDatesPopup from './SearchDatesPopup';
import { Modal, Button, ModalHeader, ModalBody } from 'reactstrap';
import { IoIosCalendar } from 'react-icons/io';
// previous commented code
// componentWillMount = () => {
// const { actions, userSubscription } = this.props;
// actions.setSearchLastDate(userSubscription);
// };
export function MediaTypes(props) {
const [modal, setModal] = useState(false);
const {
t,
mediaTypes,
actions,
chosenMediaTypes,
toggleMediaType,
toggleAllMediaTypes,
restrictions
} = props;
const allSelected = mediaTypes.length === chosenMediaTypes.length;
function toggle() {
setModal((modal) => !modal);
}
// set only the allowed media types from restrictions initially
function allowPermissions(mediaType) {
if (!restrictions || !restrictions.plans) {
return false;
}
// for selecting all
if (!mediaType) {
return mediaTypes.every((mt) => restrictions.plans[mt]);
}
return restrictions.plans[mediaType];
}
function toggleSingleType(mediaType, value) {
/* const isFree = restrictions.plans.price === 0;
// TODO: remove following restrictions when duplication fixes
const restrictedTemporary =
isFree && ['news', 'blogs'].includes(mediaType) && value;
if (!allowPermissions(mediaType) || restrictedTemporary) { */
if (!allowPermissions(mediaType)) {
return actions.toggleUpgradeModal();
}
toggleMediaType(mediaType, value); // restrict condition
}
function toggleAllTypes() {
// TODO: remove following restrictions when duplication fixes
/* const isFree = restrictions.plans.price === 0;
if (!allowPermissions() || isFree) { */
if (!allowPermissions()) {
return actions.toggleUpgradeModal();
}
toggleAllMediaTypes(!allSelected);
}
/*
const {
chosenSearchDate,
chosenSearchInterval
chosenStartDate,
chosenEndDate
} = props.searchByFiltersState
const isIntervalBetween = chosenSearchInterval === 'between';
const searchDateBtnText = isIntervalBetween &&
chosenStartDate !== '' ||
isIntervalBetween &&
chosenEndDate !== ''
? chosenSearchDate : t('searchTab.userSubscription.' + chosenSearchDate);
*/
return (
<Fragment>
<div className="d-flex justify-content-between align-items-start">
<div data-tour="select-media-types">
<Button
outline
size="sm"
title={allSelected ? 'Click to deselect' : 'Click to select'}
className="btn-pill mb-2 mr-2 px-3"
color={cx('light', { active: allSelected })}
onClick={toggleAllTypes}
>
{t('searchTab.sourceTypes.all')}
</Button>
{mediaTypes.map((mediaType, i) => {
const isMediaTypeChosen =
chosenMediaTypes.indexOf(mediaType) !== -1;
return (
<Button
key={mediaType}
outline
size="sm"
title={
isMediaTypeChosen ? 'Click to deselect' : 'Click to select'
}
className="btn-pill mb-2 mr-2 px-3"
color={cx('light', {
active: isMediaTypeChosen
})}
onClick={() => toggleSingleType(mediaType, !isMediaTypeChosen)}
>
{t('searchTab.sourceTypes.' + mediaType)}
</Button>
);
})}
</div>
<Button
color="link"
className="ml-2"
onClick={toggle}
data-tour="select-date-range"
>
<IoIosCalendar fontSize="24px" />
{/* {t('searchTab.datesRange')} */}
</Button>
</div>
<Modal isOpen={modal} toggle={toggle} data-tour="date-range-modal">
<ModalHeader toggle={toggle}>Select dates</ModalHeader>
<ModalBody>
<SearchDatesPopup
outsideClickIgnoreClass="react-datepicker"
userSubscription={props.userSubscription}
userSubscriptionDate={props.userSubscriptionDate}
searchIntervals={props.searchByFiltersState.searchIntervals}
searchLastDates={props.searchByFiltersState.searchLastDates}
chosenSearchInterval={
props.searchByFiltersState.chosenSearchInterval
}
chosenSearchLastDate={
props.searchByFiltersState.chosenSearchLastDate
}
chosenStartDate={props.searchByFiltersState.chosenStartDate}
chosenEndDate={props.searchByFiltersState.chosenEndDate}
hideSearchDatesPopup={toggle}
setSearchInterval={actions.setSearchInterval}
setSearchLastDate={actions.setSearchLastDate}
setSearchDate={actions.setSearchDate}
setStartDate={actions.setStartDate}
setEndDate={actions.setEndDate}
/>
</ModalBody>
</Modal>
</Fragment>
);
}
MediaTypes.propTypes = {
t: PropTypes.func.isRequired,
mediaTypes: PropTypes.array.isRequired,
chosenMediaTypes: PropTypes.array.isRequired,
toggleMediaType: PropTypes.func.isRequired,
toggleAllMediaTypes: PropTypes.func.isRequired,
restrictions: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,
userSubscriptionDate: PropTypes.string.isRequired,
userSubscription: PropTypes.string.isRequired,
searchByFiltersState: PropTypes.object.isRequired
};
export default translate(['tabsContent'], { wait: true })(MediaTypes);
@@ -0,0 +1,94 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import FiltersTable from '../../../../common/FiltersTable/FiltersTable'
import { Button } from 'reactstrap'
export class RefinePanel extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
advancedFilters: PropTypes.object.isRequired,
selectedFilters: PropTypes.object.isRequired,
clearPending: PropTypes.object.isRequired,
filterPages: PropTypes.object.isRequired,
onRefine: PropTypes.func.isRequired,
actions: PropTypes.object.isRequired
};
onHiderClick = (e) => {
e.preventDefault()
this.props.actions.toggleRefinePanel()
};
onSelectFilter = (groupName, filterValue) => {
this.props.actions.selectRefineFilter(groupName, filterValue)
};
onClearFilters = (groupName) => {
this.props.actions.clearRefineFilters(groupName)
};
onClearAllFilters = () => {
this.props.actions.clearAllRefineFilters()
};
onMoreFilters = (groupName) => {
this.props.actions.loadMoreRefineFilters(groupName)
};
onLessFilters = (groupName) => {
this.props.actions.loadLessRefineFilters(groupName)
};
/* onPressEnter = (e) => {
if (e.keyCode === 13) {
const keyword = document.getElementById('refine-keyword').value
this.props.actions.selectRefineFilter('keyword', keyword)
setTimeout(() => {
this.props.onRefine()
})
}
}; */
render () {
return (
<div className="refine-panel px-4">
<Button
color="light"
title="Hide refine panel"
className="d-block ml-auto mb-3 btn-icon"
onClick={this.onHiderClick}
>
{this.props.t('searchTab.hide')}
</Button>
{/* <Input
type="text"
className="mb-2"
id="refine-keyword"
placeholder={this.props.t('common:advancedFilters.keywordRefine')}
onKeyUp={this.onPressEnter}
/> */}
<FiltersTable
filters={this.props.advancedFilters}
selectedFilters={this.props.selectedFilters}
clearPending={this.props.clearPending}
pages={this.props.filterPages}
callbacks={{
'selectFilter': this.onSelectFilter,
'clearFilters': this.onClearFilters,
'clearAllFilters': this.onClearAllFilters,
'moreFilters': this.onMoreFilters,
'lessFilters': this.onLessFilters,
'refine': this.props.onRefine
}}
/>
</div>
)
}
}
export default translate(['tabsContent'], { wait: true })(RefinePanel)
@@ -0,0 +1,154 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import {
Button,
Modal,
ModalHeader,
ModalBody,
Label,
Input,
ModalFooter,
FormGroup
} from 'reactstrap'
export class SaveFeedPopup extends React.Component {
static propTypes = {
feedCategories: PropTypes.array.isRequired,
saveType: PropTypes.string.isRequired,
toggleSaveFeedPopup: PropTypes.func.isRequired,
addAlert: PropTypes.func.isRequired,
onSaveAsFeed: PropTypes.func.isRequired,
getSidebarCategories: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
}
constructor(props) {
super(props)
this.state = {
isFeedNameError: false,
feedCategoriesKeys: [],
feedName: '',
selectCategory: ''
}
}
componentWillMount = () => {
let nestingCount = -1
this.getCategoriesKeys(this.props.feedCategories, nestingCount)
}
//function that generates new array of categories without nesting
getCategoriesKeys = (categories, nestingCount) => {
nestingCount += 1
categories.forEach((category) => {
if (category.subType === 'deleted_content') return false
const categoryName = '-'.repeat(nestingCount) + ' ' + category.name
const feedCategoriesKeys = this.state.feedCategoriesKeys
feedCategoriesKeys.push({ id: category.id, name: categoryName })
this.setState({
feedCategoriesKeys: feedCategoriesKeys,
selectCategory: feedCategoriesKeys[0].id.toString()
})
if (category.childes.length) {
this.getCategoriesKeys(category.childes, nestingCount)
}
})
}
changeHandler = (e) => {
const { name, value } = e.target
this.setState({ [name]: value })
}
hidePopupFromOutside = (e) => {
if (e.target === e.currentTarget) this.hidePopup()
}
hidePopup = () => {
this.props.toggleSaveFeedPopup()
}
onSubmit = () => {
const { feedName: name, selectCategory: category } = this.state
if (!name || !name.trim()) {
this.setState({ isFeedNameError: true })
return false
}
this.props.onSaveAsFeed(name, category)
this.hidePopup()
}
render() {
const { t } = this.props
const {
feedCategoriesKeys,
isFeedNameError,
feedName,
selectCategory
} = this.state
return (
<Modal isOpen toggle={this.hidePopup} backdrop="static" data-tour="feed-save-modal">
<ModalHeader toggle={this.hidePopup}>
{t('searchTab.saveFeedPopup.' + this.props.saveType)}
</ModalHeader>
<ModalBody>
<FormGroup>
<Label>
{t('searchTab.saveFeedPopup.nameLabel')}<span className="text-danger">*</span>
</Label>
<Input
name="feedName"
type="text"
value={feedName}
onChange={this.changeHandler}
/>
{isFeedNameError && (
<p className="text-danger">
{t('searchTab.saveFeedPopup.feedNameErrorMsg')}
</p>
)}
</FormGroup>
<FormGroup>
<Label>
{t('searchTab.saveFeedPopup.folderLabel')}<span className="text-danger">*</span>
</Label>
<Input
name="selectCategory"
type="select"
value={selectCategory}
onChange={this.changeHandler}
>
{feedCategoriesKeys.map((category) => {
return (
<option key={category.id} value={category.id}>
{category.name}
</option>
)
})}
</Input>
</FormGroup>
</ModalBody>
<ModalFooter>
<Button color="light" onClick={this.hidePopup}>
{t('common:commonWords.Cancel')}
</Button>
<Button color="primary" onClick={this.onSubmit}>
{t('searchTab.saveBtn')}
</Button>
</ModalFooter>
</Modal>
)
}
}
export default translate(['tabsContent', 'common'], { wait: true })(
SaveFeedPopup
)
@@ -0,0 +1,118 @@
import React from 'react'
import PropTypes from 'prop-types'
import { DateRangePicker } from 'react-dates'
import moment from 'moment'
import { getMomentObject } from '../../../../../../common/helper'
export class BetweenDatepickers extends React.Component {
state = {}
static propTypes = {
chosenSearchInterval: PropTypes.string.isRequired,
chosenStartDate: PropTypes.string.isRequired,
chosenEndDate: PropTypes.string.isRequired,
setSearchInterval: PropTypes.func.isRequired,
setSearchDate: PropTypes.func.isRequired,
setStartDate: PropTypes.func.isRequired,
minDate: PropTypes.object,
setEndDate: PropTypes.func.isRequired
}
swapDate = (startDate, endDate) => {
if (startDate.isAfter(endDate)) {
const temp = startDate
startDate = endDate
endDate = temp
}
return { startDate, endDate }
}
/*
setDates = (date, isStartDate) => {
const {
chosenStartDate,
chosenEndDate,
setStartDate,
setEndDate,
setSearchDate
} = this.props
const hasStartDate = !!chosenStartDate
const hasEndDate = !!chosenEndDate
let startDate = hasStartDate ? moment(chosenStartDate) : moment()
let endDate = hasEndDate ? moment(chosenEndDate) : moment()
startDate = isStartDate ? date : startDate
endDate = !isStartDate ? date : endDate
const swappedDate = this.swapDate(startDate, endDate)
startDate = swappedDate.startDate.format('YYYY-MM-DD')
endDate = swappedDate.endDate.format('YYYY-MM-DD')
setStartDate(startDate.format('YYYY-MM-DD'))
setEndDate(endDate.format('YYYY-MM-DD'))
const endDateLabel = hasEndDate ? endDate : 'now'
const startDateLabel = hasStartDate ? startDate : 'until'
let label = isStartDate
? `${startDate} - ${endDateLabel}`
: `${startDateLabel} - ${endDate}`
setSearchDate(label)
} */
setBetweenInterval = () => {
const { chosenSearchInterval, setSearchInterval } = this.props
if (chosenSearchInterval === 'between') return false
setSearchInterval('between')
}
handleDateChange = ({ startDate, endDate }) => {
const { setStartDate, setEndDate } = this.props
setStartDate(startDate ? startDate.format('YYYY-MM-DD') : null)
setEndDate(endDate ? endDate.format('YYYY-MM-DD') : null)
if (startDate && endDate) {
this.setBetweenInterval()
}
}
onFocusChange = (focus) => {
this.setState({ focusedInput: focus })
}
isOutsideRange = (date) => {
const today = moment()
return date.isAfter(today) || date.isBefore(this.props.minDate)
}
render() {
const { chosenStartDate, chosenEndDate } = this.props
const today = moment()
const startDate = getMomentObject(chosenStartDate)
const endDate = getMomentObject(chosenEndDate)
return (
<div className="ml-3">
<DateRangePicker
startDateId="startDate"
endDateId="endDate"
startDate={startDate}
endDate={endDate}
onDatesChange={this.handleDateChange}
focusedInput={this.state.focusedInput}
onFocusChange={this.onFocusChange}
displayFormat="MM/DD/YYYY"
startDatePlaceholderText="Start Date"
endDatePlaceholderText="End Date"
numberOfMonths={1}
maxDate={today}
// eslint-disable-next-line react/jsx-no-bind
isOutsideRange={this.isOutsideRange}
/>
</div>
)
}
}
export default BetweenDatepickers
@@ -0,0 +1,33 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { Col, CustomInput, FormGroup } from 'reactstrap';
export class DuplicatesTab extends React.Component {
static propTypes = {
includeDuplicates: PropTypes.bool.isRequired,
toggleIncludeDuplicates: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
render() {
const { t } = this.props;
return (
<Col sm={12}>
<FormGroup>
<CustomInput
className="checkbox-input-hidden"
type="checkbox"
id="duplicates-check"
checked={this.props.includeDuplicates}
onChange={this.props.toggleIncludeDuplicates}
label={t('searchTab.searchBySection.duplicates.includeDuplicates')}
/>
</FormGroup>
</Col>
);
}
}
export default translate(['tabsContent'], { wait: true })(DuplicatesTab);
@@ -0,0 +1,53 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { Col, FormGroup, Input, Label } from 'reactstrap';
export class EmphasisTab extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
include: PropTypes.string.isRequired,
exclude: PropTypes.string.isRequired,
setHeadlineIncluded: PropTypes.func.isRequired,
setHeadlineExcluded: PropTypes.func.isRequired
};
setHeadInclude = (e) => {
const headline = e.target.value;
this.props.setHeadlineIncluded(headline);
};
setHeadExclude = (e) => {
const headline = e.target.value;
this.props.setHeadlineExcluded(headline);
};
render() {
const { t, include, exclude } = this.props;
return (
<Fragment>
<Col sm="6">
<FormGroup>
<Label>
{t('searchTab.searchBySection.emphasis.headlineLabel')}{' '}
{t('searchTab.searchBySection.emphasis.include')}
</Label>
<Input type="text" value={include} onChange={this.setHeadInclude} />
</FormGroup>
</Col>
<Col sm="6">
<FormGroup>
<Label>
{t('searchTab.searchBySection.emphasis.headlineLabel')}{' '}
{t('searchTab.searchBySection.emphasis.exclude')}
</Label>
<Input type="text" value={exclude} onChange={this.setHeadExclude} />
</FormGroup>
</Col>
</Fragment>
);
}
}
export default translate(['tabsContent'], { wait: true })(EmphasisTab);
@@ -0,0 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { Col, CustomInput, FormGroup } from 'reactstrap';
function ExtrasTab({ t, hasImages, toggleHasImages }) {
return (
<Col sm={12}>
<FormGroup>
<CustomInput
id="has-images-check"
type="checkbox"
className="d-flex"
checked={hasImages}
label={t('searchTab.searchBySection.extras.hasImages')}
onChange={toggleHasImages}
/>
</FormGroup>
</Col>
);
}
ExtrasTab.propTypes = {
hasImages: PropTypes.bool.isRequired,
toggleHasImages: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
export default translate(['tabsContent'], { wait: true })(ExtrasTab);
@@ -0,0 +1,51 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { Col, CustomInput } from 'reactstrap';
export class LangsTab extends React.Component {
static propTypes = {
chosenLanguages: PropTypes.array.isRequired,
searchLanguages: PropTypes.array.isRequired,
toggleLang: PropTypes.func.isRequired,
toggleAllLangs: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
toggleLangs = ({ target: { id, checked } }) => {
this.props.toggleLang(id, checked);
};
toggleAllLangs = (e) => {
this.props.toggleAllLangs(e.target.checked);
};
render() {
const { t } = this.props;
const { searchLanguages, chosenLanguages } = this.props;
return (
<Col sm={12} className="search-by-lang">
<CustomInput
id="article-check-all"
type="checkbox"
label={t('common:language.all')}
checked={searchLanguages.length === chosenLanguages.length}
onChange={this.toggleAllLangs}
/>
{searchLanguages.map((lang) => (
<CustomInput
key={lang}
id={lang}
type="checkbox"
checked={chosenLanguages.indexOf(lang) !== -1}
label={t('common:language.' + lang)}
onChange={this.toggleLangs}
/>
))}
</Col>
);
}
}
export default translate(['tabsContent'], { wait: true })(LangsTab);
@@ -0,0 +1,61 @@
import React from 'react'
import PropTypes from 'prop-types'
import { DragSource } from 'react-dnd'
const Types = {
LOC: 'location'
}
const locationSource = {
beginDrag (props) {
// Return the data describing the dragged item
return { oldDropTargetType: props.dropTargetType }
},
endDrag (props, monitor, component) {
// When dropped on a compatible target, do something
if (monitor.getDropResult() !== null) {
const locFrom = props.dropTargetType
const locTo = monitor.getDropResult().newDropTargetType
const locationType = props.locationType
const location = props.location
props.moveLocation(locFrom, locTo, locationType, location)
}
}
}
/**
* Specifies which props to inject into your component.
*/
function collectDragSource (connect) {
return {
// Call this function inside render()
// to let React DnD handle the drag events:
connectDragSource: connect.dragSource()
}
}
export class LocationsTabList extends React.Component {
static propTypes = {
location: PropTypes.object.isRequired,
dropTargetType: PropTypes.string.isRequired,
moveLocation: PropTypes.func.isRequired,
connectDragSource: PropTypes.func.isRequired
};
render () {
const { connectDragSource } = this.props
const { location } = this.props
return connectDragSource(
<li className="list-group-item cursor-move p-2">
<span className="drag-handle" />
{location.name}
</li>
)
}
}
export default DragSource(Types.LOC, locationSource, collectDragSource)(LocationsTabList)
@@ -0,0 +1,111 @@
/* eslint-disable react/jsx-no-bind */
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import LocationsTabList from './LocationsTabList';
import { Button, Col, Row } from 'reactstrap';
export class LocationsTab extends React.Component {
static propTypes = {
locations: PropTypes.array.isRequired,
locationsToInclude: PropTypes.array.isRequired,
locationsToExclude: PropTypes.array.isRequired,
chosenLocationsType: PropTypes.string.isRequired,
changeLocationsType: PropTypes.func.isRequired,
moveLocation: PropTypes.func.isRequired,
clearLocations: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
constructor(props) {
super(props);
this.state = {
dropdownOpen: false,
dropDownValue: 'country'
};
}
onClearLocations = () => {
this.props.clearLocations();
this.props.changeLocationsType('country');
this.setState({ dropDownValue: 'country' });
};
selectLocation = (value) => {
this.props.changeLocationsType(value);
this.setState({ dropDownValue: value });
};
render() {
const {
locations,
chosenLocationsType,
locationsToInclude,
locationsToExclude
} = this.props;
const { t } = this.props;
const locationsMainList = locations.filter((loc) => {
return loc.type === chosenLocationsType;
});
const includeList = locationsToInclude.filter((loc) => {
return loc.type === chosenLocationsType;
});
const excludeList = locationsToExclude.filter((loc) => {
return loc.type === chosenLocationsType;
});
const { dropDownValue } = this.state;
return (
<Col sm={12}>
<Button
outline
active={dropDownValue === 'country'}
color="secondary"
className="mr-2 mb-3"
onClick={() => this.selectLocation('country')}
>
{t('searchTab.searchBySection.locations.countriesSelect')}
</Button>
<Button
outline
active={dropDownValue === 'state'}
color="secondary"
className="mb-3"
onClick={() => this.selectLocation('state')}
>
{t('searchTab.searchBySection.locations.statesSelect')}
</Button>
<Row className="draggable">
<Col md={4}>
<LocationsTabList
locations={locationsMainList}
chosenLocationsType={chosenLocationsType}
dropTargetType="locations"
moveLocation={this.props.moveLocation}
/>
</Col>
<Col md={4}>
<LocationsTabList
locations={includeList}
chosenLocationsType={chosenLocationsType}
dropTargetType="locationsToInclude"
moveLocation={this.props.moveLocation}
/>
</Col>
<Col md={4}>
<LocationsTabList
locations={excludeList}
chosenLocationsType={chosenLocationsType}
dropTargetType="locationsToExclude"
moveLocation={this.props.moveLocation}
/>
</Col>
</Row>
</Col>
);
}
}
export default translate(['tabsContent'], { wait: true })(LocationsTab);
@@ -0,0 +1,94 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { DropTarget } from 'react-dnd';
import flow from 'lodash/flow';
import LocationItem from './LocationItem';
import {
ListGroup
} from 'reactstrap';
const targetTypes = ['location'];
const locationListTarget = {
drop(props, monitor, component) {
if (monitor.didDrop()) {
//check whether some nested
// target already handled drop
return;
}
return { newDropTargetType: props.dropTargetType };
},
canDrop(props, monitor) {
return props.dropTargetType !== monitor.getItem().oldDropTargetType;
}
};
function collectDropTarget(connect, monitor) {
return {
// Call this function inside render()
// to let React DnD handle the drag events:
connectDropTarget: connect.dropTarget(),
// You can ask the monitor about the current drag state:
itemType: monitor.getItemType()
};
}
export class LocationsTabList extends React.Component {
static propTypes = {
locations: PropTypes.array.isRequired,
chosenLocationsType: PropTypes.string.isRequired,
dropTargetType: PropTypes.string.isRequired,
moveLocation: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
connectDropTarget: PropTypes.func.isRequired
};
render() {
const { locations, chosenLocationsType, dropTargetType } = this.props;
const { t } = this.props;
const { connectDropTarget } = this.props;
locations.forEach((location) => {
location.name = t('common:' + location.type + '.' + location.code);
});
const sortedLocations = locations.sort((a, b) => {
const nameA = a.name.toLowerCase();
const nameB = b.name.toLowerCase();
if (nameA < nameB) {
//sort string ascending
return -1;
}
if (nameA > nameB) {
return 1;
}
return 0;
});
return connectDropTarget(
<div className="scroll-area-md border b-radius-5">
<p className="text-muted border-bottom p-2">{t('searchTab.searchBySection.locations.' + dropTargetType)}</p>
<ListGroup className="p-2">
{sortedLocations.map((location, i) => {
return (
<LocationItem
key={'location-' + i}
location={location}
dropTargetType={dropTargetType}
locationType={chosenLocationsType}
moveLocation={this.props.moveLocation}
/>
);
})}
</ListGroup>
</div>
);
}
}
export default flow(
DropTarget(targetTypes, locationListTarget, collectDropTarget),
translate(['tabsContent'], { wait: true })
)(LocationsTabList);
@@ -0,0 +1,163 @@
import React from 'react';
import PropTypes from 'prop-types';
import SearchByTabs from './SearchByTabs';
import EmphasisTab from './EmphasisTab';
import LangsTab from './LangsTab';
import LocationsTab from './LocationsTab';
import SourcesTab from './SourcesTab';
import SourceListsTab from './SourceListsTab';
import DuplicatesTab from './DuplicatesTab';
import ExtrasTab from './ExtrasTab';
import { translate } from 'react-i18next';
import { Button, Container, Row } from 'reactstrap';
export class SearchBy extends React.Component {
static propTypes = {
userSubscriptionDate: PropTypes.string.isRequired,
userSubscription: PropTypes.string.isRequired,
searchByFiltersState: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
};
constructor(props) {
super(props);
this.state = {
animationDisabled: true,
arrowPosition: true
};
}
onToggleSearchBy = () => {
this.props.actions.toggleSearchBy();
};
render() {
const { t } = this.props;
const { searchByFiltersState, actions } = this.props;
const visibleClass = searchByFiltersState.isSearchByVisible
? ' visible'
: ' closed';
return (
<div
className={'search-by-container mb-3 mb-md-0' + visibleClass}
data-tour="advanced-search"
>
<div className="search-by">
<SearchByTabs
searchByTabs={searchByFiltersState.searchByTabs}
chooseSearchByTab={actions.chooseSearchByTab}
chosenSearchByTab={searchByFiltersState.chosenSearchByTab}
/>
<Container fluid>
<Row className="mb-3" data-tour="advanced-search-content">
{searchByFiltersState.chosenSearchByTab === 'emphasis' && (
<EmphasisTab
include={searchByFiltersState.headlineIncluded}
exclude={searchByFiltersState.headlineExcluded}
setHeadlineIncluded={actions.setHeadlineIncluded}
setHeadlineExcluded={actions.setHeadlineExcluded}
/>
)}
{searchByFiltersState.chosenSearchByTab === 'languages' && (
<LangsTab
searchLanguages={searchByFiltersState.searchLanguages}
chosenLanguages={searchByFiltersState.chosenLanguages}
toggleLang={actions.toggleLang}
toggleAllLangs={actions.toggleAllLangs}
/>
)}
{searchByFiltersState.chosenSearchByTab === 'locations' && (
<LocationsTab
locations={searchByFiltersState.locations}
chosenLocationsType={searchByFiltersState.chosenLocationsType}
locationsToInclude={searchByFiltersState.locationsToInclude}
locationsToExclude={searchByFiltersState.locationsToExclude}
changeLocationsType={actions.changeLocationsType}
moveLocation={actions.moveLocation}
clearLocations={actions.clearLocations}
/>
)}
{searchByFiltersState.chosenSearchByTab === 'sources' && (
<SourcesTab
chosenMediaTypes={searchByFiltersState.chosenMediaTypes}
chosenLanguages={searchByFiltersState.chosenLanguages}
searchBySources={searchByFiltersState.searchBySources}
searchBySourcesType={searchByFiltersState.searchBySourcesType}
selectedSearchBySources={
searchByFiltersState.selectedSearchBySources
}
searchBySourcesQuery={
searchByFiltersState.searchBySourcesQuery
}
setSearchBySourcesQuery={actions.setSearchBySourcesQuery}
getSearchBySources={actions.getSearchBySources}
addSelectedSearchBySource={actions.addSelectedSearchBySource}
removeSelectedSearchBySource={
actions.removeSelectedSearchBySource
}
clearSearchBySources={actions.clearSearchBySources}
includeExcludeSearchBySources={
actions.includeExcludeSearchBySources
}
/>
)}
{searchByFiltersState.chosenSearchByTab === 'sourceLists' && (
<SourceListsTab
searchBySourceLists={
searchByFiltersState.searchBySourceListsAvailable
}
searchBySourceListsToInclude={
searchByFiltersState.searchBySourceListsToInclude
}
searchBySourceListsToExclude={
searchByFiltersState.searchBySourceListsToExclude
}
getSourceLists={actions.getSearchBySourceLists}
moveSourceList={actions.moveSourceList}
/>
)}
{searchByFiltersState.chosenSearchByTab === 'duplicates' && (
<DuplicatesTab
includeDuplicates={searchByFiltersState.includeDuplicates}
toggleIncludeDuplicates={actions.toggleIncludeDuplicates}
/>
)}
{searchByFiltersState.chosenSearchByTab === 'extras' && (
<ExtrasTab
hasImages={searchByFiltersState.hasImages}
toggleHasImages={actions.toggleHasImages}
/>
)}
</Row>
</Container>
</div>
<hr className="mt-0 mb-2" />
<Button
outline
size="sm"
className="font-size-xs"
color="secondary"
onClick={this.onToggleSearchBy}
>
{t('searchTab.searchBySection.searchByBtn')}
{searchByFiltersState.isSearchByVisible ? (
<i className="lnr-chevron-up btn-icon-wrapper"></i>
) : (
<i className="lnr-chevron-down btn-icon-wrapper"></i>
)}
</Button>
</div>
);
}
}
export default translate(['tabsContent'], { wait: true })(SearchBy);
@@ -0,0 +1,40 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Nav, NavLink, NavItem } from 'reactstrap';
import { translate } from 'react-i18next';
export class SearchByTabs extends React.Component {
static propTypes = {
searchByTabs: PropTypes.array.isRequired,
chosenSearchByTab: PropTypes.string.isRequired,
chooseSearchByTab: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
chooseSearchByTab = (newTab) => () => {
this.props.chooseSearchByTab(newTab);
};
render() {
const { searchByTabs } = this.props;
const { t } = this.props;
return (
<Nav tabs className="font-size-xs">
{searchByTabs.map((tab, i) => (
<NavItem key={tab}>
<NavLink
className="d-block"
active={tab === this.props.chosenSearchByTab}
onClick={this.chooseSearchByTab(tab)}
>
{t('searchTab.searchBySection.' + tab + '.title')}
</NavLink>
</NavItem>
))}
</Nav>
);
}
}
export default translate(['tabsContent'], { wait: true })(SearchByTabs);
@@ -0,0 +1,25 @@
import React from 'react'
import PropTypes from 'prop-types'
export class SourceIcon extends React.Component {
static propTypes = {
type: PropTypes.string.isRequired
};
acceptedTypes = ['blogs', 'clippings', 'forums', 'mixed', 'news', 'prints', 'socials', 'user-added', 'user-comments', 'videos'];
render () {
const { type } = this.props
if (!this.acceptedTypes.includes(type)) {
return null
}
return (
<img src={require('../../../../../../images/feed-type-' + type + '.png')} className="source-icon" />
)
}
}
export default SourceIcon
@@ -0,0 +1,56 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import SourceListsTabList from './SourceListsTabList';
import { Col } from 'reactstrap';
export class SourceListsTab extends React.Component {
static propTypes = {
searchBySourceLists: PropTypes.array.isRequired,
searchBySourceListsToInclude: PropTypes.array.isRequired,
searchBySourceListsToExclude: PropTypes.array.isRequired,
getSourceLists: PropTypes.func.isRequired,
moveSourceList: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
componentWillMount = () => {
this.props.getSourceLists({ page: 1, limit: 25 });
};
render() {
const {
searchBySourceLists,
searchBySourceListsToInclude,
searchBySourceListsToExclude
} = this.props;
return (
<Fragment>
<Col md={4}>
<SourceListsTabList
sourceLists={searchBySourceLists}
dropTargetType="searchBySourceListsAvailable"
moveSourceList={this.props.moveSourceList}
/>
</Col>
<Col md={4}>
<SourceListsTabList
sourceLists={searchBySourceListsToInclude}
dropTargetType="searchBySourceListsToInclude"
moveSourceList={this.props.moveSourceList}
/>
</Col>
<Col md={4}>
<SourceListsTabList
sourceLists={searchBySourceListsToExclude}
dropTargetType="searchBySourceListsToExclude"
moveSourceList={this.props.moveSourceList}
/>
</Col>
</Fragment>
);
}
}
export default translate(['tabsContent'], { wait: true })(SourceListsTab);
@@ -0,0 +1,64 @@
import React from 'react';
import PropTypes from 'prop-types';
import { DragSource } from 'react-dnd';
const Types = {
SOURCE_LIST: 'sourceList'
};
const sourceListSource = {
beginDrag(props) {
// Return the data describing the dragged item
return { oldDropTargetType: props.dropTargetType };
},
endDrag(props, monitor, component) {
// When dropped on a compatible target, do something
if (monitor.getDropResult() !== null) {
const from = props.dropTargetType;
const to = monitor.getDropResult().newDropTargetType;
const sourceList = props.sourceList;
props.moveSourceList(from, to, sourceList);
}
}
};
/**
* Specifies which props to inject into your component.
*/
function collectDragSource(connect) {
return {
// Call this function inside render()
// to let React DnD handle the drag events:
connectDragSource: connect.dragSource()
};
}
export class SourceListsTabItem extends React.Component {
static propTypes = {
sourceList: PropTypes.func.isRequired,
dropTargetType: PropTypes.string.isRequired,
moveSourceList: PropTypes.func.isRequired,
connectDragSource: PropTypes.func.isRequired
};
render() {
const { connectDragSource } = this.props;
const { sourceList } = this.props;
return connectDragSource(
<li className="list-group-item cursor-move p-2">
<span className="drag-handle" />
{sourceList.name}
</li>
);
}
}
export default DragSource(
Types.SOURCE_LIST,
sourceListSource,
collectDragSource
)(SourceListsTabItem);
@@ -0,0 +1,75 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { DropTarget } from 'react-dnd';
import flow from 'lodash/flow';
import SourceListsTabItem from './SourceListsTabItem';
import { ListGroup } from 'reactstrap';
const targetTypes = ['sourceList'];
const sourceListTarget = {
drop(props, monitor, component) {
if (monitor.didDrop()) {
//check whether some nested
// target already handled drop
return;
}
return { newDropTargetType: props.dropTargetType };
},
canDrop(props, monitor) {
return props.dropTargetType !== monitor.getItem().oldDropTargetType;
}
};
function collectDropTarget(connect, monitor) {
return {
// Call this function inside render()
// to let React DnD handle the drag events:
connectDropTarget: connect.dropTarget(),
// You can ask the monitor about the current drag state:
itemType: monitor.getItemType()
};
}
export class SourceListsTabList extends React.Component {
static propTypes = {
sourceLists: PropTypes.array.isRequired,
dropTargetType: PropTypes.string.isRequired,
moveSourceList: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
connectDropTarget: PropTypes.func.isRequired
};
render() {
const { sourceLists, dropTargetType } = this.props;
const { t } = this.props;
const { connectDropTarget } = this.props;
return connectDropTarget(
<div className="draggable scroll-area-md border b-radius-5">
<p className="text-muted border-bottom p-2">
{t('searchTab.searchBySection.sourceLists.' + dropTargetType)}
</p>
<ListGroup className="p-2">
{sourceLists.map((sourceList, i) => {
return (
<SourceListsTabItem
key={'sourceList-' + i}
sourceList={sourceList}
dropTargetType={dropTargetType}
moveSourceList={this.props.moveSourceList}
/>
);
})}
</ListGroup>
</div>
);
}
}
export default flow(
DropTarget(targetTypes, sourceListTarget, collectDropTarget),
translate(['tabsContent'], { wait: true })
)(SourceListsTabList);
@@ -0,0 +1,67 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import SourcesTabAvailSources from './SourcesTabAvailSources';
import SourcesTabSelectedSources from './SourcesTabSelectedSources';
import { Col } from 'reactstrap';
export class SourcesTab extends React.Component {
static propTypes = {
chosenMediaTypes: PropTypes.array.isRequired,
chosenLanguages: PropTypes.array.isRequired,
searchBySources: PropTypes.array.isRequired,
selectedSearchBySources: PropTypes.array.isRequired,
searchBySourcesType: PropTypes.string.isRequired,
searchBySourcesQuery: PropTypes.string.isRequired,
setSearchBySourcesQuery: PropTypes.func.isRequired,
getSearchBySources: PropTypes.func.isRequired,
addSelectedSearchBySource: PropTypes.func.isRequired,
removeSelectedSearchBySource: PropTypes.func.isRequired,
clearSearchBySources: PropTypes.func.isRequired,
includeExcludeSearchBySources: PropTypes.func.isRequired
};
render() {
const {
searchBySourcesQuery,
setSearchBySourcesQuery,
chosenMediaTypes,
chosenLanguages,
searchBySources,
getSearchBySources,
addSelectedSearchBySource,
searchBySourcesType,
clearSearchBySources,
selectedSearchBySources,
removeSelectedSearchBySource,
includeExcludeSearchBySources
} = this.props;
return (
<Fragment>
<Col sm={8}>
<SourcesTabAvailSources
searchBySourcesQuery={searchBySourcesQuery}
selectedSources={selectedSearchBySources}
setSearchBySourcesQuery={setSearchBySourcesQuery}
chosenMediaTypes={chosenMediaTypes}
chosenLanguages={chosenLanguages}
availSources={searchBySources}
getSearchBySources={getSearchBySources}
addSelectedSearchBySource={addSelectedSearchBySource}
/>
</Col>
<Col sm={4}>
<SourcesTabSelectedSources
searchBySourcesType={searchBySourcesType}
clearSearchBySources={clearSearchBySources}
selectedSources={selectedSearchBySources}
removeSelectedSearchBySource={removeSelectedSearchBySource}
includeExcludeSearchBySources={includeExcludeSearchBySources}
/>
</Col>
</Fragment>
);
}
}
export default SourcesTab;
@@ -0,0 +1,160 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
// import SourceIcon from './SourceIcon';
import { Button, Input, InputGroup, InputGroupAddon, Table } from 'reactstrap';
import { capitalize } from 'lodash';
import { getTitle } from '../../../../../../common/helper';
import cx from 'classnames';
import { domainNames } from '../SearchSubTab';
export class SourcesTabAvailSources extends React.Component {
static propTypes = {
chosenMediaTypes: PropTypes.array.isRequired,
chosenLanguages: PropTypes.array.isRequired,
availSources: PropTypes.array.isRequired,
selectedSources: PropTypes.array.isRequired,
searchBySourcesQuery: PropTypes.string.isRequired,
setSearchBySourcesQuery: PropTypes.func.isRequired,
getSearchBySources: PropTypes.func.isRequired,
addSelectedSearchBySource: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
componentDidMount = () => {
this.searchSources();
};
searchSources = () => {
const {
chosenLanguages,
chosenMediaTypes,
getSearchBySources,
searchBySourcesQuery
} = this.props;
const query = searchBySourcesQuery;
const dataToSend = {};
dataToSend.page = 1;
dataToSend.limit = 100;
dataToSend.query = query;
dataToSend.filters = {};
const source = []
const domain = []
chosenMediaTypes.map((v) => {
if (domainNames.includes(v)) {
domain.push(`${v}.com`);
} else {
source.push(v);
}
})
dataToSend.filters.publisher = { source, domain };
dataToSend.filters.language = chosenLanguages;
getSearchBySources(dataToSend);
};
chooseSource = (e) => {
const dataset = e.currentTarget.dataset;
const sourceTitle = dataset.sourceTitle;
const sourceType = dataset.sourceType;
const sourceId = dataset.sourceId;
this.props.addSelectedSearchBySource({
title: sourceTitle,
type: sourceType,
id: sourceId
});
};
onChangeSearchInput = (e) => {
const val = e.target.value;
this.props.setSearchBySourcesQuery(val);
};
onEnterSearchInput = (e) => {
if (e.keyCode === 13) this.searchSources();
};
render() {
const { availSources, selectedSources } = this.props;
const { t } = this.props;
return (
<Fragment>
<InputGroup className="mb-3">
<Input
type="text"
id="search-by-sources-input"
value={this.props.searchBySourcesQuery}
onChange={this.onChangeSearchInput}
onKeyUp={this.onEnterSearchInput}
/>
<InputGroupAddon addonType="append">
<Button
color="primary"
className="btn-icon btn-icon-only"
onClick={this.searchSources}
>
<i className="lnr-magnifier btn-icon-wrapper"></i>
</Button>
</InputGroupAddon>
</InputGroup>
<p className="text-muted">
{t('searchTab.searchBySection.sources.availSources')}
</p>
<div className="source-table-wrap border">
<Table striped bordered className="mb-0">
<thead>
<tr>
<th>{t('searchTab.searchBySection.sources.source')}</th>
<th>{t('searchTab.searchBySection.sources.siteType')}</th>
<th>{t('searchTab.searchBySection.sources.mediatype')}</th>
<th>{t('searchTab.searchBySection.sources.lang')}</th>
</tr>
</thead>
<tbody>
{availSources.length > 0 ? (
availSources.map((source, i) => {
return (
<tr
title="Click to select"
className={cx('clickable', {
active:
selectedSources &&
selectedSources.find((v) => v.id === source.id)
})}
data-source-title={source.title}
data-source-type={source.type}
data-source-id={source.id}
onClick={this.chooseSource}
key={i}
>
{/* <td>
<SourceIcon type={source.type} />
</td> */}
<td>{getTitle(source.title)}</td>
<td title={source.url}>
{capitalize(source.siteType) || '-'}
</td>
<td>{capitalize(source.type) || '-'}</td>
<td>{t(`common:language.${source.lang}`)}</td>
</tr>
);
})
) : (
<tr className="p-4 text-center text-black-50">
<td colSpan="4">{t('common:messages.noRows')}</td>
</tr>
)}
</tbody>
</Table>
</div>
</Fragment>
);
}
}
export default translate(['tabsContent'], { wait: true })(
SourcesTabAvailSources
);
@@ -0,0 +1,122 @@
/* eslint-disable react/jsx-no-bind */
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { Button, CustomInput, Table } from 'reactstrap';
import { IoIosCloseCircleOutline } from 'react-icons/io';
import { capitalize } from 'lodash';
import { getTitle } from '../../../../../../common/helper';
export class SourcesTabSelectedSources extends React.Component {
static propTypes = {
searchBySourcesType: PropTypes.string.isRequired,
selectedSources: PropTypes.array.isRequired,
removeSelectedSearchBySource: PropTypes.func.isRequired,
clearSearchBySources: PropTypes.func.isRequired,
includeExcludeSearchBySources: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
removeSource = (sourceId) => {
this.props.removeSelectedSearchBySource(sourceId);
};
includeExclide = (type) => {
this.props.includeExcludeSearchBySources(type);
};
render() {
const { selectedSources } = this.props;
const { t } = this.props;
return (
<Fragment>
<div className="d-flex flex-wrap my-3">
<CustomInput
type="radio"
name="include-exclude-source"
className="d-flex mr-2"
checked={this.props.searchBySourcesType === 'include'}
id="include-sources-radio"
onChange={() => this.includeExclide('include')}
label={t('searchTab.searchBySection.sources.includeText')}
/>
<CustomInput
type="radio"
name="include-exclude-source"
checked={this.props.searchBySourcesType === 'exclude'}
className="d-flex mr-2"
id="exclude-sources-radio"
onChange={() => this.includeExclide('exclude')}
label={t('searchTab.searchBySection.sources.excludeText')}
/>
</div>
<p className="text-muted">
{t('searchTab.searchBySection.sources.selectedSources')}
</p>
<div className="source-table-wrap border">
<Table striped className="mb-0">
<thead>
<tr>
<th>{t('searchTab.searchBySection.sources.source')}</th>
<th>{t('searchTab.searchBySection.sources.mediatype')}</th>
<th style={{ width: '50px' }}></th>
</tr>
</thead>
<tbody>
{selectedSources.length > 0 ? (
selectedSources.map((source, i) => {
return (
<tr key={i}>
{/* <td>
<SourceIcon type={source.type} />
</td> */}
<td>{getTitle(source.title)}</td>
<td>{capitalize(source.type) || '-'}</td>
<td>
<button
title="Remove"
type="button"
className="btn p-0"
onClick={() => this.removeSource(source.id)}
>
<IoIosCloseCircleOutline
size={22}
className="text-danger ml-2"
/>
</button>
</td>
</tr>
);
})
) : (
<tr className="p-4 text-center text-black-50">
<td colSpan="3">
{t('common:messages.noRows')} <br />
{t('searchTab.searchBySection.sources.selectSource')}
</td>
</tr>
)}
</tbody>
</Table>
</div>
{selectedSources.length > 0 && (
<Button
size="sm"
className="d-block ml-auto mt-2 mb-2"
onClick={this.props.clearSearchBySources}
>
{t('searchTab.clearBtn')}
</Button>
)}
</Fragment>
);
}
}
export default translate(['tabsContent'], { wait: true })(
SourcesTabSelectedSources
);
@@ -0,0 +1,178 @@
import React from 'react'
import moment from 'moment'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import BetweenDatepickers from './SearchBy/BetweenDatepickers'
import { compose } from 'redux'
import classnames from 'classnames'
import { Button, CustomInput, FormGroup } from 'reactstrap'
export class SearchDatesPopup extends React.Component {
static propTypes = {
userSubscriptionDate: PropTypes.string.isRequired,
userSubscription: PropTypes.string.isRequired,
t: PropTypes.func.isRequired,
searchIntervals: PropTypes.array.isRequired,
searchLastDates: PropTypes.array.isRequired,
chosenSearchInterval: PropTypes.string.isRequired,
chosenSearchLastDate: PropTypes.string.isRequired,
chosenStartDate: PropTypes.string.isRequired,
chosenEndDate: PropTypes.string.isRequired,
setSearchInterval: PropTypes.func.isRequired,
setSearchLastDate: PropTypes.func.isRequired,
setSearchDate: PropTypes.func.isRequired,
setStartDate: PropTypes.func.isRequired,
setEndDate: PropTypes.func.isRequired
}
setSearchInterval = (e) => {
const chosenInterval = e.target.dataset.interval
const chosenStartDate = this.props.chosenStartDate
const chosenEndDate = this.props.chosenEndDate
const chosenLastDate = this.props.chosenSearchLastDate
const isIntervalBetween = chosenInterval === 'between'
this.props.setSearchInterval(chosenInterval)
if (
(isIntervalBetween && chosenStartDate !== '') ||
(isIntervalBetween && chosenEndDate !== '')
) {
const endDate = chosenEndDate !== '' ? chosenEndDate : 'now'
const startDate = chosenStartDate !== '' ? chosenStartDate : 'until'
this.props.setSearchDate(startDate + ' - ' + endDate)
}
if (chosenInterval === 'all') {
this.props.setSearchDate('all')
}
if (chosenInterval === 'last') {
this.props.setSearchDate(chosenLastDate)
}
}
setLastDate = (e) => {
const chosenLastDate = e.target.dataset.lastDate
const isDisabled = e.target.dataset.disabled === 'true'
if (isDisabled) return false
if (this.props.chosenSearchInterval !== 'last') {
this.props.setSearchInterval('last')
}
this.props.setSearchLastDate(chosenLastDate)
this.props.setSearchDate(chosenLastDate)
}
onReset = () => {
this.props.setSearchInterval('all')
this.props.setSearchDate('all')
this.props.setStartDate('')
this.props.setEndDate('')
}
render() {
const {
t,
chosenSearchInterval,
chosenStartDate,
chosenEndDate,
setSearchInterval,
setSearchDate,
setStartDate,
setEndDate,
chosenSearchLastDate,
searchIntervals,
searchLastDates,
userSubscription
} = this.props
const subscriptionLimitIndex = searchLastDates.indexOf(userSubscription)
const minDate = moment().startOf('day').subtract(
parseInt(userSubscription.slice(0, -1)),
'days'
)
return (
<div>
<div className="d-flex justify-content-between">
<p className="mb-2">
{t('searchTab.searchDates.subscriptionLabel')}:
<strong>
{t('searchTab.userSubscription.' + this.props.userSubscription)}
</strong>
</p>
<div>
<Button color="warning" className="mb-2" onClick={this.onReset}>
{t('searchTab.searchDates.resetBtn')}
</Button>
</div>
</div>
<FormGroup>
{searchIntervals.map((interval, i) => {
return (
<div key={interval}>
<CustomInput
checked={this.props.chosenSearchInterval === interval}
type="radio"
id={'search-interval-' + interval}
data-interval={interval}
name="date-interval"
label={t('searchTab.searchDates.' + interval)}
onChange={this.setSearchInterval}
/>
{interval === 'last' && (
<ul className="search-last-dates mx-3">
{searchLastDates.map((lastDate, i) => {
const isDisabled = i > subscriptionLimitIndex
const isActive =
chosenSearchLastDate === lastDate &&
chosenSearchInterval === 'last'
const className = classnames('search-last-dates__item', {
disabled: isDisabled,
active: isActive
})
return (
<li
key={'last-date-' + i}
data-last-date={lastDate}
data-disabled={isDisabled}
className={className}
onClick={this.setLastDate}
>
{t('searchTab.searchDates.' + lastDate)}
</li>
)
})}
</ul>
)}
{interval === 'between' && (
<BetweenDatepickers
chosenSearchInterval={chosenSearchInterval}
chosenStartDate={chosenStartDate}
chosenEndDate={chosenEndDate}
minDate={minDate}
setSearchInterval={setSearchInterval}
setSearchDate={setSearchDate}
setStartDate={setStartDate}
setEndDate={setEndDate}
/>
)}
</div>
)
})}
</FormGroup>
</div>
)
}
}
const applyDecorators = compose(translate(['tabsContent'], { wait: true }))
export default applyDecorators(SearchDatesPopup)
@@ -0,0 +1,439 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import SearchSubTabHead from './SearchSubTabHead';
import MediaTypes from './MediaTypes';
import SearchingBlock from './SearchingBlock';
import SearchingResults from './SearchingResults';
import SearchBy from './SearchBy/SearchBy';
import RefinePanel from './RefinePanel';
import Restrictions from '../../../../common/Restrictions/Restrictions';
import { parseSearchDays } from '../../../../../common/Common';
import reduxConnect from '../../../../../redux/utils/connect';
import { Card, CardBody, CardTitle } from 'reactstrap';
import { setDocumentData } from '../../../../../common/helper';
import { translate } from 'react-i18next';
import { compose } from 'redux';
export const domainNames = ['reddit', 'twitter', 'instagram'];
class SearchSubTab extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
store: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired
};
get searchState() {
return this.props.store.appState.search;
}
get searchByFiltersState() {
return this.props.store.appState.searchByFilters;
}
get articlesState() {
return this.props.store.appState.articles;
}
get authState() {
return this.props.store.common.auth;
}
componentDidMount() {
setDocumentData('title', 'Search');
}
componentWillUnmount() {
setDocumentData('title');
}
_sendSearchQuery = (page, initialSearch = false) => {
const { actions } = this.props;
const dataToSend = this.gatherSearchQueryData();
if (dataToSend) {
dataToSend.page = page;
dataToSend.advancedFilters = this.gatherAdvancedFilters();
actions.getSearchResults(dataToSend, initialSearch);
}
};
_sendFeedQuery = (page, activeFeed) => {
const { actions } = this.props;
const params = {
page: page,
advancedFilters: this.gatherAdvancedFilters()
};
actions.getFeedResults(params, activeFeed.id);
};
onSearchQuery = () => {
this._sendSearchQuery(1, true);
};
onRefine = () => {
const { activeFeed } = this.searchState;
if (activeFeed) {
this._sendFeedQuery(1, activeFeed);
} else {
this._sendSearchQuery(1);
}
};
onPager = ({ currentPage: page }) => {
const { activeFeed } = this.searchState;
if (activeFeed) {
this._sendFeedQuery(page, activeFeed);
} else {
this._sendSearchQuery(page);
}
};
onSaveAsFeed = (name, category) => {
const dataToSend = this.getFeedData(name, category, 'query_feed');
dataToSend && this.props.actions.saveAsFeed(dataToSend);
};
onSaveFeed = () => {
const { actions } = this.props;
const { activeFeed } = this.searchState;
const dataToSend = this.getFeedData(
activeFeed.name,
activeFeed.category,
activeFeed.subType
);
dataToSend && actions.saveFeed(dataToSend, activeFeed.id);
};
getFeedData = (name, category, feedSubType) => {
let dataToSend = {};
const searchQueryData = this.gatherSearchQueryData();
if (!searchQueryData) return false;
dataToSend.search = searchQueryData;
dataToSend.search.advancedFilters = this.gatherAdvancedFilters();
dataToSend.feed = {
name: name,
category: category,
subType: feedSubType
};
const excludedArticles = this.articlesState.excludedArticles;
if (excludedArticles && excludedArticles.length) {
dataToSend.feed.excludedDocuments = excludedArticles;
}
return dataToSend;
};
gatherSearchQueryData = () => {
const searchState = this.searchState;
const searchByFiltersState = this.searchByFiltersState;
const { userSubscription } = this.authState;
const { actions } = this.props;
let dataToSend = {};
const query = searchState.loadedFeedQuery;
if (!query) {
actions.addAlert({ type: 'error', transKey: 'searchQueryEmpty' });
return false;
}
dataToSend.query = query;
dataToSend.filters = {}; //create filters prop
//setting media types filter
if (searchByFiltersState.chosenMediaTypes.length) {
const source = [];
const domain = [];
searchByFiltersState.chosenMediaTypes.map((v) => {
if (domainNames.includes(v)) {
domain.push(`${v}.com`);
} else {
source.push(v);
}
});
dataToSend.filters.publisher = { source, domain };
} else {
actions.addAlert({ type: 'error', transKey: 'noMediaTypesSelected' });
return false;
}
// setting date filter
const chosenInterval = searchByFiltersState.chosenSearchInterval;
const chosenStartDate = searchByFiltersState.chosenStartDate;
const chosenEndDate = searchByFiltersState.chosenEndDate;
if (chosenInterval === 'between') {
if (chosenStartDate !== '' || chosenEndDate !== '') {
dataToSend.filters.date = {
type: 'between',
start: chosenStartDate,
end: chosenEndDate
};
} else {
dataToSend.filters.date = {
type: 'last',
days:
searchByFiltersState.chosenSearchDate === 'all'
? parseSearchDays(userSubscription)
: parseSearchDays(searchByFiltersState.chosenSearchDate)
};
}
} else if (chosenInterval === 'all') {
dataToSend.filters.date = {
type: 'last',
days: parseSearchDays(userSubscription)
};
} else {
dataToSend.filters.date = {
type: 'last',
days: parseSearchDays(searchByFiltersState.chosenSearchLastDate)
};
}
//adding included or/and excluded headlines filter
const headlineIncluded = searchByFiltersState.headlineIncluded;
const headlineExcluded = searchByFiltersState.headlineExcluded;
if (headlineIncluded.length || headlineExcluded.length) {
dataToSend.filters.headline = {};
}
if (headlineIncluded.length) {
dataToSend.filters.headline.include = headlineIncluded;
}
if (headlineExcluded.length) {
dataToSend.filters.headline.exclude = headlineExcluded;
}
//setting languages filter
const chosenLanguages = searchByFiltersState.chosenLanguages;
if (chosenLanguages.length) {
dataToSend.filters.language = chosenLanguages;
}
//setting locations filter
const locationsToInclude = searchByFiltersState.locationsToInclude;
const locationsToExclude = searchByFiltersState.locationsToExclude;
const countriesToInclude = locationsToInclude.filter((loc) => {
return loc.type === 'country';
});
const statesToInclude = locationsToInclude.filter((loc) => {
return loc.type === 'state';
});
const countriesToExclude = locationsToExclude.filter((loc) => {
return loc.type === 'country';
});
const statesToExclude = locationsToExclude.filter((loc) => {
return loc.type === 'state';
});
if (countriesToInclude.length || countriesToExclude.length) {
dataToSend.filters.country = {};
}
if (statesToInclude.length || statesToExclude.length) {
dataToSend.filters.state = {};
}
if (countriesToInclude.length) {
dataToSend.filters.country.include = countriesToInclude.map((loc) => {
return loc.code;
});
}
if (countriesToExclude.length) {
dataToSend.filters.country.exclude = countriesToExclude.map((loc) => {
return loc.code;
});
}
if (statesToInclude.length) {
dataToSend.filters.state.include = statesToInclude.map((loc) => {
return loc.code;
});
}
if (statesToExclude.length) {
dataToSend.filters.state.exclude = statesToExclude.map((loc) => {
return loc.code;
});
}
//setting source filter
const selectedSearchBySources =
searchByFiltersState.selectedSearchBySources;
if (selectedSearchBySources.length) {
dataToSend.filters.source = {};
dataToSend.filters.source.type = searchByFiltersState.searchBySourcesType;
dataToSend.filters.source.ids = selectedSearchBySources.map((source) => {
return source.id;
});
}
//setting source lists filter
const sourceListsToInclude =
searchByFiltersState.searchBySourceListsToInclude;
const sourceListsToExclude =
searchByFiltersState.searchBySourceListsToExclude;
if (sourceListsToInclude.length || sourceListsToExclude.length) {
dataToSend.filters.sourceList = {};
}
if (sourceListsToInclude.length) {
dataToSend.filters.sourceList.include = sourceListsToInclude.map(
(source) => {
return source.id;
}
);
}
if (sourceListsToExclude.length) {
dataToSend.filters.sourceList.exclude = sourceListsToExclude.map(
(source) => {
return source.id;
}
);
}
//setting duplicates filter
//dataToSend.filters.duplicates = searchByFiltersState.includeDuplicates;
//setting 'has images' filter
dataToSend.filters.hasImage = searchByFiltersState.hasImages;
return dataToSend;
};
gatherAdvancedFilters = () => {
return this.searchState.advancedFilters.selected;
};
render() {
const searchState = this.searchState;
const searchByFiltersState = this.searchByFiltersState;
const {
userSubscription,
userSubscriptionDate,
user: { restrictions }
} = this.authState;
const { store, actions } = this.props;
const feedCategories = store.appState.sidebar.categories;
const articlesState = store.appState.articles;
const { advancedFilters } = searchState;
const activeFeed = searchState.activeFeed;
let isEditSearchVisible =
!searchState.loadedFeedQuery || searchState.isEditingFeed;
if (activeFeed && activeFeed.subType === 'clip_feed') {
isEditSearchVisible = false;
}
const hasActiveFeed = !!activeFeed;
return (
<Fragment>
{!hasActiveFeed && (
<Restrictions
restrictions={restrictions && restrictions.limits}
restrictionsIds={['searchesPerDay', 'savedFeeds']}
/>
)}
<div className="search-tab">
<Card className="main-card mb-3">
<CardBody>
<div className="search-block">
{isEditSearchVisible && (
<div className="search-edit-block">
<SearchingBlock
searchResultsErrors={searchState.searchResultsErrors}
onSearchQuery={this.onSearchQuery}
loadedFeedQuery={searchState.loadedFeedQuery}
actions={actions}
/>
<MediaTypes
mediaTypes={searchByFiltersState.mediaTypes}
chosenMediaTypes={searchByFiltersState.chosenMediaTypes}
actions={actions}
restrictions={restrictions}
searchByFiltersState={searchByFiltersState}
userSubscription={userSubscription}
userSubscriptionDate={userSubscriptionDate}
toggleMediaType={actions.toggleMediaType}
toggleAllMediaTypes={actions.toggleAllMediaTypes}
/>
<SearchBy
userSubscription={userSubscription}
userSubscriptionDate={userSubscriptionDate}
searchByFiltersState={searchByFiltersState}
actions={actions}
/>
</div>
)}
<SearchSubTabHead
isSaveFeedPopupVisible={searchState.isSaveFeedPopupVisible}
isSaving={searchState.isSavingFeed}
feedCategories={feedCategories}
onSaveAsFeed={this.onSaveAsFeed}
toggleSaveFeedPopup={actions.toggleSaveFeedPopup}
addAlert={actions.addAlert}
getSidebarCategories={actions.getSidebarCategories}
activeFeed={activeFeed}
isEditingFeed={searchState.isEditingFeed}
editFeed={actions.editFeed}
setNewSearch={actions.setNewSearch}
renewSearchBy={actions.renewSearchBy}
changeActiveFeedName={actions.changeActiveFeedName}
saveFeed={this.onSaveFeed}
/>
</div>
</CardBody>
</Card>
<Card className="main-card mb-3">
<CardBody>
<CardTitle>{this.props.t('searchTab.results')}</CardTitle>
<div className="search-content">
<SearchingResults
searchState={searchState}
articlesState={articlesState}
actions={actions}
isRefinePanelVisible={advancedFilters.isVisible}
toggleRefinePanel={actions.toggleRefinePanel}
onPager={this.onPager}
/>
{searchState.isLoaded && advancedFilters.isVisible && (
<RefinePanel
advancedFilters={advancedFilters.all}
selectedFilters={advancedFilters.selected}
clearPending={advancedFilters.pending}
filterPages={advancedFilters.pages}
onRefine={this.onRefine}
actions={actions}
/>
)}
</div>
</CardBody>
</Card>
</div>
</Fragment>
);
}
}
const applyDecorators = compose(
reduxConnect(),
translate(['tabsContent'], { wait: true })
);
export default applyDecorators(SearchSubTab);
@@ -0,0 +1,135 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import SaveFeedPopup from './SaveFeedPopup'
import { Button } from 'reactstrap'
export class SearchSubTabHead extends React.Component {
static propTypes = {
feedCategories: PropTypes.array.isRequired,
isSaveFeedPopupVisible: PropTypes.bool.isRequired,
activeFeed: PropTypes.object,
isSaving: PropTypes.bool.isRequired,
isEditingFeed: PropTypes.bool.isRequired,
addAlert: PropTypes.func.isRequired,
toggleSaveFeedPopup: PropTypes.func.isRequired,
onSaveAsFeed: PropTypes.func.isRequired,
getSidebarCategories: PropTypes.func.isRequired,
editFeed: PropTypes.func.isRequired,
setNewSearch: PropTypes.func.isRequired,
renewSearchBy: PropTypes.func.isRequired,
changeActiveFeedName: PropTypes.func.isRequired,
saveFeed: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
}
openSaveFeedPopup = () => {
this.props.toggleSaveFeedPopup()
}
saveFeed = () => {
this.props.saveFeed()
}
onEditFeed = () => {
this.props.editFeed()
}
onNewSearch = () => {
this.props.setNewSearch()
this.props.renewSearchBy()
}
onChangeFeedName = (event) => {
this.props.changeActiveFeedName(event.target.value)
}
render() {
const {
t,
isEditingFeed,
isSaveFeedPopupVisible,
isSaving,
activeFeed
} = this.props
const feedIsLoaded = !!activeFeed
const showEditButton =
!!activeFeed && !isEditingFeed && activeFeed.subType === 'query_feed'
return (
<div>
<div className="d-flex flex-wrap justify-content-between">
<div>
{!isEditingFeed && activeFeed && <h4 className="text-primary mb-2 mb-md-0">{activeFeed.name}</h4>}
</div>
<div className="text-right" data-tour="search-buttons">
<Button
className="btn-icon mb-2 mb-lg-0 ml-2"
color="primary"
onClick={this.onNewSearch}
>
<i className="lnr-plus-circle btn-icon-wrapper"></i>
{t('searchTab.newSearchBtn')}
</Button>
{!feedIsLoaded && (
<Button
className="btn-icon mb-2 mb-lg-0 ml-2"
color="success"
onClick={this.openSaveFeedPopup}
>
<i className="lnr-checkmark-circle btn-icon-wrapper"></i>
{isSaving ? t('searchTab.savingBtn') : t('searchTab.saveBtn')}
</Button>
)}
{feedIsLoaded && isEditingFeed && (
<Button
className="btn-icon mb-2 mb-lg-0 ml-2"
color="success"
onClick={this.saveFeed}
>
<i className="lnr-checkmark-circle btn-icon-wrapper"></i>
{isSaving ? t('searchTab.savingBtn') : t('searchTab.saveBtn')}
</Button>
)}
{feedIsLoaded && isEditingFeed && (
<Button
className="btn-icon mb-2 mb-lg-0 ml-2"
color="success"
onClick={this.openSaveFeedPopup}
>
<i className="lnr-checkmark-circle btn-icon-wrapper"></i>
{t('searchTab.saveAsBtn')}
</Button>
)}
{showEditButton && (
<Button
className="btn-icon mb-2 mb-lg-0 ml-2"
color="warning"
onClick={this.onEditFeed}
>
<i className="lnr-pencil btn-icon-wrapper"></i>
{t('searchTab.editFeedBtn')}
</Button>
)}
</div>
</div>
{isSaveFeedPopupVisible && (
<SaveFeedPopup
saveType="typeSaveAs"
feedCategories={this.props.feedCategories}
toggleSaveFeedPopup={this.props.toggleSaveFeedPopup}
addAlert={this.props.addAlert}
onSaveAsFeed={this.props.onSaveAsFeed}
getSidebarCategories={this.props.getSidebarCategories}
/>
)}
</div>
)
}
}
export default translate(['tabsContent'], { wait: true })(SearchSubTabHead)
@@ -0,0 +1,61 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import { Button, Input, InputGroup, InputGroupAddon } from 'reactstrap'
export class SearchingBlock extends React.Component {
static propTypes = {
searchResultsErrors: PropTypes.array.isRequired,
loadedFeedQuery: PropTypes.string,
onSearchQuery: PropTypes.func.isRequired,
actions: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
}
onPressEnter = (e) => {
if (e.keyCode === 13) {
this.props.onSearchQuery()
}
}
onChangeQuery = (e) => {
const { actions } = this.props
const value = e.target.value;
// replace smart quotation marks with normal
let filterQuotes = value.replace(/[\u2018\u2019]/g, '\'').replace(/[\u201C\u201D]/g, '"')
// add space before operator if not
filterQuotes = filterQuotes.replace(/\s*\+/g, ' +').replace(/\s*\-/g, ' -').trimStart()
actions.changeFeedQuery(filterQuotes)
}
render() {
let { t, loadedFeedQuery } = this.props
loadedFeedQuery = loadedFeedQuery || ''
return (
<div className="search-input-field mb-2">
<InputGroup>
<Input
type="text"
value={loadedFeedQuery}
data-tour="input-field-search"
onChange={this.onChangeQuery}
placeholder={t('searchTab.searchInputPlaceholder')}
onKeyUp={this.onPressEnter}
/>
<InputGroupAddon addonType="append">
<Button
color="primary"
className="btn-icon btn-icon-only px-3"
data-tour="search-button"
onClick={this.props.onSearchQuery}
>
<i className="lnr-magnifier btn-icon-wrapper font-weight-bold"></i>
</Button>
</InputGroupAddon>
</InputGroup>
</div>
)
}
}
export default translate(['tabsContent'], { wait: true })(SearchingBlock)
@@ -0,0 +1,178 @@
import React from 'react'
import PropTypes from 'prop-types'
import SearchingResultsTopPanel from './SearchingResultsTopPanel'
import Article from './Article'
import DeleteArticlesPopup from './DeleteArticlesPopup'
import EmailArticlesPopup from './EmailArticlesPopup'
import CommentArticlePopup from './CommentArticlePopup'
import ClipArticlesPopup from './ClipArticles/ClipArticlesPopup'
import Pager from '../../../../common/Pager/Pager'
import EmailConfirmPopup from './EmailConfirmPopup'
import NoRecords from '../../../../common/NoRecords'
import Loading from '../../../../common/Loading'
import { Interpolate, translate } from 'react-i18next'
export class SearchingResults extends React.Component {
static propTypes = {
searchState: PropTypes.object.isRequired,
articlesState: PropTypes.object.isRequired,
isRefinePanelVisible: PropTypes.bool.isRequired,
toggleRefinePanel: PropTypes.func.isRequired,
onPager: PropTypes.func.isRequired,
actions: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
};
forEachArticle = (cb) => {
const { searchState, articlesState } = this.props
return searchState.searchResults
.filter((article) => !articlesState.excludedArticles.includes(article.id))
.map(cb)
};
render () {
const { searchState, articlesState, actions, t } = this.props
const isSearchResultsLoaded = searchState.searchResults.length > 0
const numPages = Math.ceil(
searchState.searchResultTotalCount / searchState.searchResultLimit
)
const noRecords = searchState.searchResultsPending || !isSearchResultsLoaded || !searchState.isSynced
if (searchState.searchResultsPending) {
return (
<div className="search-results">
<Loading />
</div>
)
}
if (!searchState.isSynced) {
return (
<div className="search-results">
<NoRecords message={t('searchTab.notSynchronized')} />
</div>
)
}
if (searchState.isSynced && !isSearchResultsLoaded) {
return (
<div className="search-results">
<NoRecords message={t('searchTab.noResults')} />
</div>
)
}
return (
<div className="search-results">
<SearchingResultsTopPanel
noRecords={noRecords}
searchResultsCount={searchState.searchResults.length}
selectedArticles={searchState.selectedArticles}
selectAllArticles={actions.selectAllArticles}
showDeleteArticlesPopup={actions.showDeleteArticlesPopup}
showEmailArticlesPopup={actions.showEmailArticlesPopup}
showClipArticlesPopup={actions.showClipArticlesPopup}
isRefinePanelVisible={noRecords ? false : this.props.isRefinePanelVisible}
toggleRefinePanel={this.props.toggleRefinePanel}
/>
{isSearchResultsLoaded &&
<p className="text-muted font-size-xs">
<Interpolate
t={t}
i18nKey="searchTab.articlesCountDivider"
resultsCount={searchState.searchResultCount}
totalCount={searchState.searchResultTotalCount}
/>
</p>
}
<div className="search-results-block mt-1">
{isSearchResultsLoaded &&
this.forEachArticle((article, i) => {
return (
<Article
key={'article-' + i}
article={article}
selectedArticles={searchState.selectedArticles}
selectArticle={actions.selectArticle}
showDeletePopup={actions.showDeleteArticlesPopup}
showEmailPopup={actions.showEmailArticlesPopup}
showCommentPopup={actions.showCommentArticlePopup}
showClipPopup={actions.showClipArticlesPopup}
deleteComment={actions.deleteComment}
readArticleLater={actions.readArticleLater}
loadMoreComments={actions.loadMoreComments}
showShareMenu={actions.showShareMenu}
/>
)
})}
{isSearchResultsLoaded && (
<Pager
pagerAction={this.props.onPager}
currentPage={searchState.searchResultPage}
numPages={numPages}
limitByPage={searchState.searchResultLimit}
hideLimitSelector
/>
)}
</div>
{articlesState.deletePopup.visible && (
<DeleteArticlesPopup
articles={articlesState.deletePopup.articles}
hidePopup={actions.hideDeleteArticlesPopup}
activeFeed={searchState.activeFeed}
deleteArticles={actions.deleteArticles}
deleteArticlesFromFeed={actions.deleteArticlesFromFeed}
addAlert={actions.addAlert}
/>
)}
{articlesState.emailPopup.visible && (
<EmailArticlesPopup
articlesToEmail={articlesState.emailPopup.articles}
emailArticles={actions.emailArticles}
hidePopup={actions.hideEmailArticlesPopup}
addAlert={actions.addAlert}
loadRecipients={actions.loadRecipients}
recipients={articlesState.emailPopup.recipients}
>
{articlesState.emailConfirmPopup.visible && (
<EmailConfirmPopup
hidePopup={actions.hideEmailConfirmPopup}
hideEmailPopup={actions.hideEmailArticlesPopup}
sendDocumentsByEmail={actions.sendDocumentsByEmail}
/>
)}
</EmailArticlesPopup>
)}
{articlesState.commentPopup.visible && (
<CommentArticlePopup
article={articlesState.commentPopup.article}
comment={articlesState.commentPopup.comment}
commentArticle={actions.commentArticle}
updateComment={actions.updateComment}
hidePopup={actions.hideCommentArticlePopup}
addAlert={actions.addAlert}
/>
)}
{articlesState.clipPopup.visible && (
<ClipArticlesPopup
articles={articlesState.clipPopup.articles}
recentClipFeeds={articlesState.recentClipFeeds}
getRecentClipFeeds={actions.getRecentClipFeeds}
hidePopup={actions.hideClipArticlesPopup}
clipArticles={actions.clipArticles}
addAlert={actions.addAlert}
/>
)}
</div>
)
}
}
export default translate(['tabsContent'], { wait: true })(SearchingResults)
@@ -0,0 +1,111 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ButtonGroup, Button, CustomInput } from 'reactstrap';
import { translate } from 'react-i18next';
export class SearchingResultsTopPanel extends React.Component {
static propTypes = {
noRecords: PropTypes.bool,
selectedArticles: PropTypes.array.isRequired,
searchResultsCount: PropTypes.number.isRequired,
selectAllArticles: PropTypes.func.isRequired,
showDeleteArticlesPopup: PropTypes.func.isRequired,
showEmailArticlesPopup: PropTypes.func.isRequired,
showClipArticlesPopup: PropTypes.func.isRequired,
isRefinePanelVisible: PropTypes.bool.isRequired,
toggleRefinePanel: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
onShowClick = (e) => {
e.preventDefault();
this.props.toggleRefinePanel();
};
selectAllArticles = (e) => {
const isChecked = e.target.checked;
if (this.props.searchResultsCount > 0) {
this.props.selectAllArticles(isChecked);
}
};
showDeleteArticlesPopup = () => {
if (this.props.selectedArticles.length > 0) {
this.props.showDeleteArticlesPopup(this.props.selectedArticles);
}
};
showEmailArticlesPopup = () => {
if (this.props.selectedArticles.length > 0) {
this.props.showEmailArticlesPopup(this.props.selectedArticles);
}
};
showClipArticlesPopup = () => {
if (this.props.selectedArticles.length > 0) {
this.props.showClipArticlesPopup(this.props.selectedArticles);
}
};
render() {
const { t, searchResultsCount, noRecords } = this.props;
const chosenArticlesCount = this.props.selectedArticles.length;
const isAllArticlesChosen =
this.props.searchResultsCount > 0
? searchResultsCount === chosenArticlesCount
: false;
if (noRecords) {
return null;
}
return (
<div className="d-flex justify-content-end mb-3 mb-md-0">
<ButtonGroup>
<Button color="light">
<CustomInput
id="toggle-all-results"
type="checkbox"
checked={isAllArticlesChosen}
onChange={this.selectAllArticles}
/>
</Button>
{/* <Button color="secondary">
<i className="fa fa-tag mr-2"> </i>
{t('searchTab.tagBtn')}
</Button> */}
<Button color="secondary" onClick={this.showClipArticlesPopup}>
<i className="fa fa-scissors mr-2"> </i>
{t('searchTab.clipBtn')}
</Button>
<Button color="secondary" onClick={this.showEmailArticlesPopup}>
<i className="fa fa-envelope-o mr-2"> </i>
{t('searchTab.emailBtn')}
</Button>
<Button color="secondary" onClick={this.showDeleteArticlesPopup}>
<i className="fa fa-trash mr-2"> </i>
{t('searchTab.deleteBtn')}
</Button>
</ButtonGroup>
{!this.props.isRefinePanelVisible && (
<Button
color="light"
title="Show refine panel"
className="btn-icon ml-3"
onClick={this.onShowClick}
>
<i className="pe-7s-filter btn-icon-wrapper"></i>
{t('searchTab.filter')}
</Button>
)}
</div>
);
}
}
export default translate(['tabsContent'], { wait: true })(
SearchingResultsTopPanel
);
@@ -0,0 +1,50 @@
import React from 'react'
import PropTypes from 'prop-types'
import {translate} from 'react-i18next'
import onClickOutside from 'react-onclickoutside'
import {compose} from 'redux'
class ShareMenu extends React.Component {
static propTypes = {
article: PropTypes.object.isRequired,
hideMenu: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
handleClickOutside = () => {
this.props.hideMenu()
};
_winOpen = (url) => {
window.open(url, 'share', 'width=600, height=450, top=0, left=0, toolbar=no')
};
onTweet = () => {
this._winOpen('https://twitter.com/intent/tweet?url=' + this.props.article.source.link)
this.props.hideMenu()
};
onYammer = () => {
this._winOpen('https://www.yammer.com/')
this.props.hideMenu()
};
render () {
const { t } = this.props
return (
<div className="article-share-menu">
<a onClick={this.onTweet}>{t('searchTab.tweet')}</a>
<a onClick={this.onYammer}>{t('searchTab.yammer')}</a>
</div>
)
}
}
const applyDecorators = compose(
translate(['tabsContent'], {wait: true}),
onClickOutside
)
export default applyDecorators(ShareMenu)
@@ -0,0 +1,47 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Redirect, Route, Switch, withRouter } from 'react-router-dom';
import SubTabWrapper from '../../AppHeader/SubTabWrapper';
import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup';
import SearchSubTab from './SearchSubTab/SearchSubTab';
import SourceIndexSubTab from './SourceIndexSubTab/SourceIndexSubTab';
import SourceListsSubTab from './SourceListsSubTab/SourceListsSubTab';
class SearchTab extends React.Component {
static propTypes = {
activeTabName: PropTypes.string,
match: PropTypes.object,
subTabs: PropTypes.array
};
render() {
const { activeTabName, subTabs, match } = this.props;
return (
<CSSTransitionGroup
component="div"
transitionName="TabsAnimation"
transitionAppear
transitionAppearTimeout={0}
transitionEnter={false}
transitionLeave={false}
>
<SubTabWrapper activeTabName={activeTabName} subTabs={subTabs}>
<Switch>
<Route path={`${match.url}/search`} component={SearchSubTab} />
<Route
path={`${match.url}/source-index`}
component={SourceIndexSubTab}
/>
<Route
path={`${match.url}/source-lists`}
component={SourceListsSubTab}
/>
<Redirect to={`${match.url}/search`} />
</Switch>
</SubTabWrapper>
</CSSTransitionGroup>
);
}
}
export default withRouter(SearchTab);
@@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { Col } from 'reactstrap';
export class InfoField extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
label: PropTypes.string,
labelValue: PropTypes.string,
children: PropTypes.oneOfType([PropTypes.string, PropTypes.element])
};
render() {
const { t, label, children, labelValue } = this.props;
return (
<li className="row">
<Col sm="4">
<p className="mb-1">{labelValue || t(label)}</p>
</Col>
<Col sm="8">
<p className="mb-1">{children}</p>
</Col>
</li>
);
}
}
export default translate(['tabsContent'], { wait: true })(InfoField);
@@ -0,0 +1,143 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import PopupLayout from '../../../../common/Popups/PopupLayout';
import InfoField from './InfoField';
import {
capOnlyFirstLetter,
getTitle,
notNullAndUnd
} from '../../../../../common/helper';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCheckCircle } from '@fortawesome/free-solid-svg-icons';
class SourceIndexInfoPopup extends React.Component {
static propTypes = {
source: PropTypes.object.isRequired,
hideSourceInfoPopup: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
render() {
let { t, source, hideSourceInfoPopup } = this.props;
/*
const loc = cl(
source.city,
source.state,
source.country && t(`common:country.${source.country}`)
)
.split(' ')
.join(', '); */
/*
source = {
...source,
tags: ['Lorem', 'ipsum', 'dolor', 'ipsum', 'dolor', 'ipsum', 'dolor'],
verified: true,
followers: 3333,
following: 33,
favorites: 333,
title: 'Title',
url: 'URL',
type: 'Type',
subType: 'Sub Type',
lang: 'en',
location: 'Washington, DC',
country: 'US',
spam_probability: '20%',
likes: 3
}; */
return (
<PopupLayout
className="source-info-popup"
title="sourceIndexTab.sourceInfoPopupTitle"
showFooter={false}
onHide={hideSourceInfoPopup}
>
<ul className="container">
<InfoField label="sourceIndexTab.titleLabel">
<a href={source.url} target="_blank" rel="noopener noreferrer">
{getTitle(source.title)}
</a>
</InfoField>
{source.url && (
<InfoField label="sourceIndexTab.homeUrl">{source.url}</InfoField>
)}
{source.type && (
<InfoField label="sourceIndexTab.mediaType">
{capOnlyFirstLetter(source.type)}
</InfoField>
)}
{source.subType && (
<InfoField labelValue="Sub Type">
{capOnlyFirstLetter(source.subType)}
</InfoField>
)}
{source.verified && (
<InfoField labelValue="Verified">
<FontAwesomeIcon
title="Source Verified"
className="text-primary"
icon={faCheckCircle}
/>
</InfoField>
)}
{source.lang && (
<InfoField label="sourceIndexTab.lang">
{t(`common:language.${source.lang}`, '-')}
</InfoField>
)}
{source.location && (
<InfoField labelValue="Location">{source.location}</InfoField>
)}
{source.country && (
<InfoField label="sourceIndexTab.country">
{t(`common:country.${source.country}`)}
</InfoField>
)}
{notNullAndUnd(source.followers) && (
<InfoField labelValue="Followers">{source.followers}</InfoField>
)}
{notNullAndUnd(source.following) && (
<InfoField labelValue="Following">{source.following}</InfoField>
)}
{notNullAndUnd(source.favorites) && (
<InfoField labelValue="Favorites">{source.favorites}</InfoField>
)}
{notNullAndUnd(source.likes) && (
<InfoField labelValue="Likes">{source.likes}</InfoField>
)}
{source.tags && source.tags.length > 0 && (
<InfoField labelValue="Tags">{source.tags.join(', ')}</InfoField>
)}
{source.spam_probability && (
<InfoField labelValue="Spam Probability">
{source.spam_probability}
</InfoField>
)}
{source.source_profiles && (
<InfoField labelValue="Source profiles">
{source.source_profiles.join(', ')}
</InfoField>
)}
</ul>
</PopupLayout>
);
}
}
export default translate(['tabsContent'], { wait: true })(SourceIndexInfoPopup);
@@ -0,0 +1,186 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import SourceIndexTable from './SourceIndexTable'
import SourceIndexUpdatePopup from './SourceIndexUpdatePopup'
import FiltersTable from '../../../../common/FiltersTable/FiltersTable'
import { withRouter } from 'react-router-dom'
import reduxConnect from '../../../../../redux/utils/connect'
import { compose } from 'redux'
import { Button, ButtonGroup, Input, InputGroup, InputGroupAddon } from 'reactstrap'
import { setDocumentData } from '../../../../../common/helper'
class SourceIndexSubTab extends React.Component {
static propTypes = {
sourcesState: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
};
componentDidMount() {
setDocumentData('title', 'Source Index | Search')
}
componentWillUnmount() {
setDocumentData('title')
}
_sourceIndexesState = () => this.props.sourcesState.sourceIndexesState;
_sourceLists = () => this.props.sourcesState.sourceListsState.data;
loadSourceIndexes = (params) => {
this.props.actions.getSourceIndexes(params || null)
};
onSearchSources = () => {
this.loadSourceIndexes()
};
onEnterSearchInput = (e) => {
if (e.keyCode === 13) this.loadSourceIndexes()
};
onChangeSearchInput = (e) => {
this.props.actions.setSourceIndexSearchQuery(e.target.value)
};
onFetchData = (params) => {
this.loadSourceIndexes(params)
};
showAddToListPopup = () => {
const { actions } = this.props
const sourceIndexesState = this._sourceIndexesState()
if (sourceIndexesState.selectedIds.length === 0) {
actions.addAlert({
type: 'notice',
transKey: 'noListsSelected',
id: 'noListsSelected'
})
return false
}
actions.toggleAddSourceToListPopup()
};
onSelectFilter = (groupName, filterValue) => {
this.props.actions.selectSourcesFilter(groupName, filterValue)
};
onClearFilters = (groupName) => {
this.props.actions.clearSourcesFilters(groupName)
};
onClearAllFilters = () => {
this.props.actions.clearAllSourcesFilters()
};
onMoreFilters = (groupName) => {
this.props.actions.loadMoreSourcesFilters(groupName)
};
onLessFilters = (groupName) => {
this.props.actions.loadLessSourcesFilters(groupName)
};
render () {
const { t, actions } = this.props
const sourceIndexesState = this._sourceIndexesState()
const sourceLists = this._sourceLists()
const {
searchQuery,
selectedIds,
chosenListsToAddSources,
chosenSourceToUpdate,
advancedFilters
} = sourceIndexesState
return (
<div className="mb-3">
<InputGroup className="mb-3">
<Input
type="text"
id="source-index-search"
placeholder={t('sourceIndexTab.mainInputPlaceholder')}
value={searchQuery}
onChange={this.onChangeSearchInput}
onKeyUp={this.onEnterSearchInput}
/>
<InputGroupAddon addonType="append">
<Button
color="primary"
className="btn-icon btn-icon-only"
onClick={this.onSearchSources}
>
<i className="lnr-magnifier btn-icon-wrapper"></i>
</Button>
</InputGroupAddon>
</InputGroup>
<ButtonGroup className="mb-3">
<Button
onClick={this.showAddToListPopup}
color="secondary"
>
<i className="fa fa-plus fa-1px for-small mr-1"> </i>{" "}
{t('sourceIndexTab.addToSourceListsBtn')}
</Button>
</ButtonGroup>
<div className="search-content">
<SourceIndexTable
tableState={sourceIndexesState}
type="sourceIndexesState"
onFetch={this.onFetchData}
actions={actions}
/>
<FiltersTable
filters={advancedFilters.all}
pages={advancedFilters.pages}
selectedFilters={advancedFilters.selected}
clearPending={advancedFilters.pending}
callbacks={{
selectFilter: this.onSelectFilter,
clearFilters: this.onClearFilters,
clearAllFilters: this.onClearAllFilters,
moreFilters: this.onMoreFilters,
lessFilters: this.onLessFilters,
refine: this.onSearchSources
}}
/>
</div>
{sourceIndexesState.isAddPopupVisible && (
<SourceIndexUpdatePopup
type="add"
sourceLists={sourceLists}
chosenLists={chosenListsToAddSources}
chosenSourceIndexes={selectedIds}
actions={actions}
/>
)}
{sourceIndexesState.isUpdatePopupVisible && (
<SourceIndexUpdatePopup
type="update"
sourceLists={sourceLists}
chosenLists={chosenSourceToUpdate.listIds}
chosenSourceIndexes={[chosenSourceToUpdate.id]}
updateItemTitle={chosenSourceToUpdate.title}
actions={actions}
/>
)}
</div>
)
}
}
const applyDecorators = compose(
withRouter,
reduxConnect('sourcesState', ['appState', 'sourcesState']),
translate(['tabsContent'], { wait: true })
)
export default applyDecorators(SourceIndexSubTab)
@@ -0,0 +1,195 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate, Interpolate } from 'react-i18next'
import Table from '../../../../common/Table/Table'
import CheckboxCell from '../../../../common/Table/CheckboxCell'
import SortableTh from '../../../../common/Table/SortableTh'
import SourceIndexInfoPopup from './SourceIndexInfoPopup'
import { Button } from 'reactstrap'
import { getTitle } from '../../../../../common/helper'
export class SourceIndexTable extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
tableState: PropTypes.object.isRequired,
type: PropTypes.string.isRequired,
onFetch: PropTypes.func.isRequired,
onDeleteIndex: PropTypes.func,
actions: PropTypes.object.isRequired
};
onFetch = (page, pageSize, sorted) => {
const { tableState, onFetch } = this.props
const params = {
page: page + 1,
limit: pageSize,
query: tableState.searchQuery
}
if (sorted.length) {
const sortedField = sorted[0]
const sort = {
field: sortedField.id,
direction: sortedField.desc ? 'desc' : 'asc'
}
params['sort'] = sort
}
onFetch(params)
};
selectAllAction = (event) => {
const { actions } = this.props
actions.toggleAllSourceIndexes()
};
selectRowAction = (itemId) => {
const { actions } = this.props
actions.toggleSourceIndex(itemId) // TODO
};
showUpdateSourcePopup = (source) => (e) => {
e.preventDefault()
this.props.actions.showUpdateSourcePopup(source)
};
deleteSourceIndex = (source) => (e) => {
e.preventDefault()
this.props.onDeleteIndex(source)
};
toggleInfoPopup = (source) => () => {
const { type, actions } = this.props
actions.toggleInfoSourcePopup(type, source)
};
getColumns = () => {
const {t, type, tableState} = this.props
let columns = [
{
id: 'selectCheckbox',
accessor: '',
sortable: false,
width: 45,
className: 'cw-center-cell',
headerClassName: 'cw-center-cell',
Header: () => {
return (
<CheckboxCell
checked={tableState.isAllSelected}
onChange={this.selectAllAction}
/>
)
},
Cell: ({original}) => {
const isSelected = tableState.selectedIds.includes(original.id)
return (
<CheckboxCell
id={original.id}
checked={isSelected}
onChange={this.selectRowAction}
/>
)
}
}, {
Header: <SortableTh title='sourceIndexTab.name' />,
accessor: 'name',
Cell: ({original}) => {
return (
<Button
color="link"
className="btn-anchor"
title="Click to see details"
onClick={this.toggleInfoPopup(original)}
>
{getTitle(original.title)}
</Button>
)
}
}, {
id: 'mediaType',
Header: <SortableTh title='sourceIndexTab.mediaType' />,
accessor: item => t(`searchTab.sourceTypes.${item.type}`)
}, {
id: 'country',
Header: <SortableTh title='sourceIndexTab.country' />,
accessor: item => {
return item.country ? t(`common:country.${item.country}`) : ''
}
}, {
id: 'action',
Header: t('sourceIndexTab.action'),
sortable: false,
Cell: ({original}) => {
return (
<Button
outline
color="info"
className="border-0"
size="sm"
onClick={this.showUpdateSourcePopup(original)}
>
<Interpolate
i18nKey='sourceIndexTab.actionBtn'
listsCount={original.listIds.length}
/>
</Button>
)
}
}, {
id: 'deleteAction',
Header: t('sourceIndexTab.action'),
sortable: false,
Cell: ({original}) => {
return (
<Button
outline
size="sm"
color="secondary"
className="border-0"
onClick={this.deleteSourceIndex(original)}
>
{t('sourceListsTab.delete')}
</Button>
)
}
}
]
const sourceIndexCols = ['selectCheckbox', 'name', 'mediaType', 'country', 'action']
const sourceOfListCols = ['name', 'mediaType', 'country', 'deleteAction']
let cols = type === 'sourceIndexesState' ? sourceIndexCols : sourceOfListCols
return columns.filter(col => cols.includes(col.id) || cols.includes(col.accessor))
};
render () {
const {tableState} = this.props
const columns = this.getColumns()
const infoPopup = tableState.infoPopup
return (
<div className="sources-table">
<Table
columns={columns}
data={tableState.data}
totalCount={tableState.totalCount}
showTotalCount
limit={tableState.limit}
page={tableState.page}
isLoading={tableState.isLoading}
onFetchData={this.onFetch}
/>
{infoPopup.visible && infoPopup.item &&
<SourceIndexInfoPopup
source={infoPopup.item}
hideSourceInfoPopup={this.toggleInfoPopup(null)}
/>
}
</div>
)
}
}
export default translate(['tabsContent'], { wait: true })(SourceIndexTable)
@@ -0,0 +1,106 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate, Interpolate } from 'react-i18next'
import PopupLayout from '../../../../common/Popups/PopupLayout'
import { CustomInput } from 'reactstrap'
import { getTitle } from '../../../../../common/helper'
export class SourceIndexUpdatePopup extends React.Component {
static propTypes = {
type: PropTypes.string.isRequired,
sourceLists: PropTypes.array.isRequired,
chosenLists: PropTypes.array.isRequired,
chosenSourceIndexes: PropTypes.array.isRequired,
updateItemTitle: PropTypes.string,
actions: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
};
componentWillMount = () => {
const { sourceLists, actions } = this.props
if (sourceLists.length === 0) {
actions.getMainSourceLists({page: 1, limit: 50})
}
};
onChoseList = (e) => {
const { type, chosenLists, actions } = this.props
const isChecked = e.target.checked
const listId = parseInt(e.target.dataset.listId)
const lists = isChecked ? chosenLists.concat(listId) : chosenLists.filter((id) => listId !== id)
const action = type === 'add' ? actions.setChosenListsToAddSources : actions.setChosenListsToUpdateSources
action(lists)
};
onSubmit = () => {
const { actions, chosenSourceIndexes, chosenLists, type } = this.props
actions.addSourcesToList({
sources: chosenSourceIndexes,
sourceLists: chosenLists
}, type === 'add')
};
getBodyTitle () {
const { t, type, updateItemTitle } = this.props
if (type === 'add') {
return <p className="mb-3">{t('sourceListsTab.popup.addToListDesc')}</p>
}
else {
return (
<p className="mb-3">
<Interpolate
i18nKey='sourceListsTab.popup.updateListDesc'
name={getTitle(updateItemTitle)}
/>
</p>
)
}
}
render () {
const { type, sourceLists, chosenLists, actions } = this.props
const isAdd = type === 'add'
const title = isAdd ? 'addToListTitle' : 'updateListTitle'
const submitText = isAdd ? 'addBtn' : 'saveBtn'
const hideAction = isAdd ? actions.toggleAddSourceToListPopup : actions.hideUpdateSourcePopup
return (
<PopupLayout
title={`sourceListsTab.popup.${title}`}
submitText={`sourceListsTab.popup.${submitText}`}
onHide={hideAction}
onSubmit={this.onSubmit}
>
<div>
{this.getBodyTitle()}
{sourceLists.length > 0 &&
<ul className="row">
{sourceLists.map((list, i) => {
const isListChosen = chosenLists.includes(list.id)
return (
<li key={i} className="col-md-4 col-sm-6 mb-2">
<CustomInput
type="checkbox"
id={'sourceListCheck-' + i}
className="d-flex"
data-list-id={list.id}
checked={isListChosen}
onChange={this.onChoseList}
label={list.name}
/>
</li>
)
})}
</ul>
}
</div>
</PopupLayout>
)
}
}
export default translate(['tabsContent', 'common'], { wait: true })(SourceIndexUpdatePopup)
@@ -0,0 +1,101 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import SourceListsAddPopup from './SourceListsAddPopup'
import SourceListsDeletePopup from './SourceListsDeletePopup'
import SourceListsRenamePopup from './SourceListsRenamePopup'
import SourceListsClonePopup from './SourceListsClonePopup'
import SourceListsTable from './SourceListsTable'
import { Button, CustomInput } from 'reactstrap'
export class SourceLists extends React.Component {
static propTypes = {
sourceListsState: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
}
onGlobalOnlyClick = () => {
const { actions, sourceListsState } = this.props
actions.toggleOnlyGlobal()
const params = {
page: sourceListsState.page,
limit: sourceListsState.limit,
onlyShared: !sourceListsState.onlyGlobal,
sort: {
field: sourceListsState.sortByField,
direction: sourceListsState.sortDirection
}
}
actions.getMainSourceLists(params)
}
render() {
const { t, sourceListsState, actions } = this.props
const {
isAddListPopupVisible,
isDeletePopupVisible,
isRenameListPopupVisible,
isCloneListPopupVisible,
listToEdit
} = sourceListsState
return (
<div className="source-lists-tab">
<div className="d-flex justify-content-between align-items-end flex-wrap-reverse flex-sm-nowrap">
<CustomInput
id="show-global"
type="checkbox"
className="d-flex mb-3"
checked={sourceListsState.onlyGlobal}
onChange={this.onGlobalOnlyClick}
label={t('sourceListsTab.showGlobalCheck')}
/>
<Button
color="primary"
className="btn-icon mb-3"
onClick={actions.toggleAddListPopup}
>
<i className="lnr lnr-plus-circle btn-icon-wrapper" />
{t('sourceListsTab.addListBtn')}
</Button>
</div>
<SourceListsTable tableState={sourceListsState} actions={actions} />
{isAddListPopupVisible && (
<SourceListsAddPopup
toggleAddListPopup={actions.toggleAddListPopup}
addSourceList={actions.addSourceList}
/>
)}
{isDeletePopupVisible && (
<SourceListsDeletePopup
listToEdit={listToEdit}
toggleDeleteListPopup={actions.toggleDeleteListPopup}
deleteSourceList={actions.deleteSourceList}
/>
)}
{isRenameListPopupVisible && (
<SourceListsRenamePopup
listToEdit={listToEdit}
toggleRenameListPopup={actions.toggleRenameListPopup}
renameSourceList={actions.renameSourceList}
/>
)}
{isCloneListPopupVisible && (
<SourceListsClonePopup
listToEdit={listToEdit}
toggleCloneListPopup={actions.toggleCloneListPopup}
cloneSourceList={actions.cloneSourceList}
/>
)}
</div>
)
}
}
export default translate(['tabsContent'], { wait: true })(SourceLists)
@@ -0,0 +1,55 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import PopupLayout from '../../../../common/Popups/PopupLayout'
import { FormGroup, Input, Label } from 'reactstrap'
export class SourceListsAddPopup extends React.Component {
static propTypes = {
toggleAddListPopup: PropTypes.func.isRequired,
addSourceList: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
}
state = {
name: ''
}
onSubmit = () => {
const { addSourceList } = this.props
addSourceList(this.state.name)
}
handleChange = (e) => {
const { value } = e.target
this.setState({ name: value })
}
render() {
const { toggleAddListPopup, t } = this.props
return (
<PopupLayout
title="Add a List"
submitText="Submit"
onHide={toggleAddListPopup}
onSubmit={this.onSubmit}
>
<div>
<FormGroup>
<Label>{t('sourceListsTab.popup.enterListName')}</Label>
<Input
type="text"
value={this.state.name}
onChange={this.handleChange}
/>
</FormGroup>
</div>
</PopupLayout>
)
}
}
export default translate(['tabsContent', 'common'], { wait: true })(
SourceListsAddPopup
)
@@ -0,0 +1,65 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import PopupLayout from '../../../../common/Popups/PopupLayout'
import { FormGroup, Input, Label } from 'reactstrap'
export class SourceListsClonePopup extends React.Component {
static propTypes = {
listToEdit: PropTypes.func.isRequired,
toggleCloneListPopup: PropTypes.func.isRequired,
cloneSourceList: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
}
constructor(props) {
super(props)
this.state = {
name:
props.listToEdit && props.listToEdit.name
? `${props.listToEdit.name} (copy)`
: ''
}
}
handleChange = (e) => {
const { value } = e.target
this.setState({
name: value
})
}
onSubmit = () => {
const { listToEdit, cloneSourceList } = this.props
cloneSourceList({
id: listToEdit.id,
name: this.state.name
})
}
render() {
const { toggleCloneListPopup, t } = this.props
return (
<PopupLayout
title="Clone"
submitText="sourceListsTab.popup.cloneListSubmitBtn"
onHide={toggleCloneListPopup}
onSubmit={this.onSubmit}
>
<FormGroup>
<Label>{t('sourceListsTab.popup.renameListTitle')}</Label>
<Input
type="text"
value={this.state.name}
onChange={this.handleChange}
/>
</FormGroup>
</PopupLayout>
)
}
}
export default translate(['tabsContent', 'common'], { wait: true })(
SourceListsClonePopup
)
@@ -0,0 +1,42 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate, Interpolate } from 'react-i18next'
import PopupLayout from '../../../../common/Popups/PopupLayout'
import { getTitle } from '../../../../../common/helper';
export class SourceListsDeletePopup extends React.Component {
static propTypes = {
listToEdit: PropTypes.func.isRequired,
toggleDeleteListPopup: PropTypes.func.isRequired,
deleteSourceList: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
onSubmit = () => {
const { listToEdit, deleteSourceList } = this.props
deleteSourceList(listToEdit)
};
render () {
const { listToEdit, toggleDeleteListPopup } = this.props
const value = listToEdit.name || listToEdit.title || ''
return (
<PopupLayout
title='sourceListsTab.popup.deleteListTitle'
submitText='Delete'
onHide={toggleDeleteListPopup}
onSubmit={this.onSubmit}
submitColor="danger"
>
<Interpolate
i18nKey='sourceListsTab.popup.deleteListDesc'
name={getTitle(value)}
/>
</PopupLayout>
)
}
}
export default translate(['tabsContent', 'common'], { wait: true })(SourceListsDeletePopup)
@@ -0,0 +1,64 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import PopupLayout from '../../../../common/Popups/PopupLayout'
import { FormGroup, Input, Label } from 'reactstrap'
export class SourceListsRenamePopup extends React.Component {
static propTypes = {
listToEdit: PropTypes.func.isRequired,
toggleRenameListPopup: PropTypes.func.isRequired,
renameSourceList: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
}
constructor(props) {
super(props)
this.state = {
name: (props.listToEdit && props.listToEdit.name) || ''
}
}
handleChange = (e) => {
const { value } = e.target
this.setState({
name: value
})
}
onSubmit = () => {
const { listToEdit, renameSourceList } = this.props
const data = {
id: listToEdit.id,
name: this.state.name
}
renameSourceList(data, listToEdit.name)
}
render() {
const { toggleRenameListPopup, t } = this.props
return (
<PopupLayout
title="Rename"
submitText="sourceListsTab.popup.renameListSubmitBtn"
onHide={toggleRenameListPopup}
onSubmit={this.onSubmit}
>
<FormGroup>
<Label>{t('sourceListsTab.popup.renameListTitle')}</Label>
<Input
type="text"
value={this.state.name}
onChange={this.handleChange}
/>
</FormGroup>
</PopupLayout>
)
}
}
export default translate(['tabsContent', 'common'], { wait: true })(
SourceListsRenamePopup
)
@@ -0,0 +1,51 @@
import React, { Fragment } from 'react'
import PropTypes from 'prop-types'
import SourceLists from './SourceLists'
import SourcesOfList from './SourcesOfList'
import { withRouter } from 'react-router-dom'
import reduxConnect from '../../../../../redux/utils/connect'
import { compose } from 'redux'
import { setDocumentData } from '../../../../../common/helper'
class SourceListsSubTab extends React.Component {
static propTypes = {
sourcesState: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired
}
componentDidMount() {
setDocumentData('title', 'Source Lists | Search')
}
componentWillUnmount() {
setDocumentData('title')
}
render() {
const { sourcesState, actions } = this.props
const { sourcesOfListState, sourceListsState } = sourcesState
const sourcesOfListVisible = sourcesOfListState.isSourcesOfListVisible
return (
<Fragment>
{!sourcesOfListVisible && (
<SourceLists sourceListsState={sourceListsState} actions={actions} />
)}
{sourcesOfListVisible && (
<SourcesOfList
sourcesOfListState={sourcesOfListState}
actions={actions}
/>
)}
</Fragment>
)
}
}
const applyDecorators = compose(
withRouter,
reduxConnect('sourcesState', ['appState', 'sourcesState'])
)
export default applyDecorators(SourceListsSubTab)
@@ -0,0 +1,256 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import moment from 'moment'
import Table from '../../../../common/Table/Table'
import SortableTh from '../../../../common/Table/SortableTh'
import { Button } from 'reactstrap'
export class SourceListsTable extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
tableState: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired
}
onFetch = (page, pageSize, sorted) => {
const { actions, tableState } = this.props
const params = {
page: page + 1,
limit: pageSize,
onlyShared: tableState.onlyGlobal
}
if (sorted.length) {
const sortedField = sorted[0]
const sort = {
field: sortedField.id,
direction: sortedField.desc ? 'desc' : 'asc'
}
params['sort'] = sort
}
actions.getMainSourceLists(params)
}
showDeleteListPopup = (item) => () => {
this.props.actions.toggleDeleteListPopup(item)
}
showRenameListPopup = (item) => () => {
this.props.actions.toggleRenameListPopup(item)
}
showCloneListPopup = (item) => () => {
this.props.actions.toggleCloneListPopup(item)
}
showSourcesOfList = (item) => () => {
this.props.actions.showSourcesOfList(item)
}
onShareList = (id) => () => {
this.props.actions.shareSourceList(id)
}
onUnshareList = (id) => () => {
this.props.actions.unshareSourceList(id)
}
getColumns() {
const { t } = this.props
let columns = [
{
Header: <SortableTh title="sourceListsTab.tableLabels.name" />,
accessor: 'name',
Cell: ({ original }) => {
return (
<a
href="#"
onClick={this.showSourcesOfList(original)}
>
{original.name}
</a>
)
}
},
{
id: 'sources',
Header: <SortableTh title="sourceListsTab.tableLabels.sources" />,
accessor: (item) => item.sourceNumber
},
{
id: 'createdBy',
Header: <SortableTh title="sourceListsTab.tableLabels.createdBy" />,
accessor: (item) => `${item.user.firstName} ${item.user.lastName}`
},
{
id: 'lastUpdated',
Header: <SortableTh title="sourceListsTab.tableLabels.lastUpdated" />,
accessor: (item) =>
item.updatedAt && moment(item.updatedAt).format('Do MMM YYYY')
},
{
id: 'lastUpdatedBy',
Header: <SortableTh title="sourceListsTab.tableLabels.lastUpdatedBy" />,
accessor: (item) =>
item.updatedBy &&
`${item.updatedBy.firstName} ${item.updatedBy.lastName}`
},
{
id: 'action',
Header: t('sourceIndexTab.action'),
// sortable: false,
minWidth: 220,
Cell: ({ original }) => {
return (
// <UncontrolledButtonDropdown>
// <DropdownToggle
// // caret
// // className="btn-icon btn-icon-only btn btn-link"
// color="link"
// >
// <i className="lnr-menu-circle btn-icon-wrapper" />
// </DropdownToggle>
// <DropdownMenu>
// <h2>Hello</h2>
// {/* <DropdownItem onClick={this.onUnshareList(original.id)}>
// <i className="dropdown-icon lnr-inbox"> </i>
// <span>{t("sourceListsTab.unshare")}</span>
// </DropdownItem>
// <DropdownItem onClick={this.onShareList(original.id)}>
// <i className="dropdown-icon lnr-file-empty"> </i>
// <span>{t("sourceListsTab.share")}</span>
// </DropdownItem>
// <DropdownItem onClick={this.showRenameListPopup(original)}>
// <i className="dropdown-icon lnr-book"> </i>
// <span>{t("sourceListsTab.rename")}</span>
// </DropdownItem>
// <DropdownItem onClick={this.showCloneListPopup(original)}>
// <i className="dropdown-icon lnr-picture"> </i>
// <span>{t("sourceListsTab.clone")}</span>
// </DropdownItem>
// <DropdownItem onClick={this.showDeleteListPopup(original)}>
// <i className="dropdown-icon lnr-picture"> </i>
// <span>{t("sourceListsTab.delete")}</span>
// </DropdownItem> */}
// </DropdownMenu>
// </UncontrolledButtonDropdown>
// <div className="d-block w-100 text-center">
// <UncontrolledButtonDropdown>
// <DropdownToggle
// caret
// className="btn-icon btn-icon-only btn btn-link"
// color="link"
// >
// <i className="lnr-menu-circle btn-icon-wrapper" />
// </DropdownToggle>
// <DropdownMenu className="rm-pointers dropdown-menu-hover-link">
// <DropdownItem onClick={this.onUnshareList(original.id)}>
// <i className="dropdown-icon lnr-inbox"> </i>
// <span>{t("sourceListsTab.unshare")}</span>
// </DropdownItem>
// <DropdownItem onClick={this.onShareList(original.id)}>
// <i className="dropdown-icon lnr-file-empty"> </i>
// <span>{t("sourceListsTab.share")}</span>
// </DropdownItem>
// <DropdownItem onClick={this.showRenameListPopup(original)}>
// <i className="dropdown-icon lnr-book"> </i>
// <span>{t("sourceListsTab.rename")}</span>
// </DropdownItem>
// <DropdownItem onClick={this.showCloneListPopup(original)}>
// <i className="dropdown-icon lnr-picture"> </i>
// <span>{t("sourceListsTab.clone")}</span>
// </DropdownItem>
// <DropdownItem onClick={this.showDeleteListPopup(original)}>
// <i className="dropdown-icon lnr-picture"> </i>
// <span>{t("sourceListsTab.delete")}</span>
// </DropdownItem>
// </DropdownMenu>
// </UncontrolledButtonDropdown>
// </div>
<div>
<Button
outline
size="sm"
color="info"
className="border-0"
onClick={
original.shared
? this.onUnshareList(original.id)
: this.onShareList(original.id)
}
>
{original.shared
? t('sourceListsTab.unshare')
: t('sourceListsTab.share')}
</Button>
<Button
outline
size="sm"
color="info"
className="border-0"
onClick={this.showRenameListPopup(original)}
>
{t('sourceListsTab.rename')}
</Button>
<Button
outline
size="sm"
color="info"
className="border-0"
onClick={this.showCloneListPopup(original)}
>
{t('sourceListsTab.clone')}
</Button>
<Button
outline
size="sm"
color="secondary"
className="border-0"
onClick={this.showDeleteListPopup(original)}
>
{t('sourceListsTab.delete')}
</Button>
</div>
)
}
}
]
const cols = [
'name',
'sources',
'createdBy',
'lastUpdated',
'lastUpdatedBy',
'action'
]
return columns.filter(
(col) => cols.includes(col.id) || cols.includes(col.accessor)
)
}
render() {
const { tableState } = this.props
const columns = this.getColumns()
return (
<div className="sources-table">
<Table
columns={columns}
data={tableState.data}
totalCount={tableState.totalCount}
showTotalCount
limit={tableState.limit}
page={tableState.page}
isLoading={tableState.isLoading}
onFetchData={this.onFetch}
/>
</div>
)
}
}
export default translate(['tabsContent'], { wait: true })(SourceListsTable)
@@ -0,0 +1,111 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import SourceIndexTable from '../SourceIndexSubTab/SourceIndexTable'
import SourceListsDeletePopup from './SourceListsDeletePopup'
import { Button, Input, InputGroup, InputGroupAddon } from 'reactstrap'
export class SourcesOfList extends React.Component {
static propTypes = {
sourcesOfListState: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
};
componentWillMount = () => {
this.searchSources('')
};
searchSources = (query) => {
const { actions, sourcesOfListState } = this.props
actions.getSourcesOfList(sourcesOfListState.visibleList.id, {
query: query,
page: sourcesOfListState.page,
limit: sourcesOfListState.limit
})
};
onSearchSources = () => {
const query = this.props.sourcesOfListState.searchQuery
this.searchSources(query)
};
onEnterSearchInput = (e) => {
if (e.keyCode === 13) this.onSearchSources()
};
onChangeSearchInput = (e) => {
const val = e.target.value
this.props.actions.setSourcesOfListSearchQuery(val)
};
onFetchData = (params) => {
const { sourcesOfListState, actions } = this.props
actions.getSourcesOfList(sourcesOfListState.visibleList.id, params)
};
onDeleteIndex = (source) => {
const { sourcesOfListState, actions } = this.props
const listId = sourcesOfListState.visibleList.id
actions.updateListSources({
id: source.id,
sourceLists: source.listIds.filter(id => id !== listId)
})
};
render () {
const { t, sourcesOfListState, actions } = this.props
const { searchQuery, visibleList, isDeletePopupVisible, listToEdit } = sourcesOfListState
return (
<div>
<Button className="btn-wide mb-3" size="sm" color="info" onClick={actions.hideSourcesOfList}>
<i className="lnr lnr-chevron-left"> </i>
</Button>
<div className="mb-3">
<p className="text-primary text-uppercase font-weight-bold mb-2">{visibleList.name} ({visibleList.sourceNumber})</p>
<InputGroup>
<Input
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>
</div>
<SourceIndexTable
tableState={sourcesOfListState}
type='sourcesOfListState'
onFetch={this.onFetchData}
onDeleteIndex={actions.toggleDeleteListIndexPopup}
actions={actions}
/>
{isDeletePopupVisible &&
<SourceListsDeletePopup
listToEdit={listToEdit}
toggleDeleteListPopup={actions.toggleDeleteListIndexPopup}
deleteSourceList={this.onDeleteIndex}
/>
}
</div>
)
}
}
export default translate(['tabsContent'], { wait: true })(SourcesOfList)