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
+4
View File
@@ -0,0 +1,4 @@
{
"presets": ["es2015", "react", "stage-0"],
"plugins": ["transform-runtime", "add-module-exports"]
}
+30
View File
@@ -0,0 +1,30 @@
# http://editorconfig.org
# A special property that should be specified at the top of the file outside of
# any sections. Set to true to stop .editor config file search on current file
root = true
[*]
# Indentation style
# Possible values - tab, space
indent_style = space
# Indentation size in single-spaced characters
# Possible values - an integer, tab
indent_size = 2
# Line ending file format
# Possible values - lf, crlf, cr
end_of_line = lf
# File character encoding
# Possible values - latin1, utf-8, utf-16be, utf-16le
charset = utf-8
# Denotes whether to trim whitespace at the end of lines
# Possible values - true, false
trim_trailing_whitespace = true
# Denotes whether file should end with a newline
# Possible values - true, false
insert_final_newline = true
+3
View File
@@ -0,0 +1,3 @@
node_modules/**
app/index.html
config/**
+22
View File
@@ -0,0 +1,22 @@
{
"parser": "babel-eslint",
"extends": ["standard", "standard-react"],
"env": {
"browser": true
},
"globals": {
"__DEV__": false,
"__PROD__": false,
"__PLAYER_DEBUG__": false,
"__BASENAME__": false
},
"rules": {
"semi": 0,
"spaced-comment": 0,
"brace-style": 0,
"no-trailing-spaces": 0,
"padded-blocks": 0,
"quotes": [2, "single"],
"space-before-function-paren": "off" // ignore
}
}
+10
View File
@@ -0,0 +1,10 @@
app
config
server
webpack
.babelrc
.editorconfig
.eslintignore
.eslintrc
.gitignore
.npmignore
+11
View File
@@ -0,0 +1,11 @@
# dependencies
/node_modules
# testing
/coverage
# production
/build
/dist
/environments
/config
+7
View File
@@ -0,0 +1,7 @@
{
"endOfLine": "lf",
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "none",
"bracketSpacing": true
}
+36
View File
@@ -0,0 +1,36 @@
# SOCIALHOSE - Front-end
This folder is the front-end codebase for SOCIALHOSE.
## Requirements
- node `>=5.0.0`
- npm `^3.0.0`
## Overview
The project is using React 16.13.1 which supports hooks but due to early versions of ESLint and Webpack, it may not support few things.
The frontend design follows the [ArchitectUI](https://dashboardpack.com/theme-details/architectui-dashboard-react-pro). Here are the [downloadable ZIP files](<(https://github.com/melzubeir/socialhose/issues/59#issuecomment-702164269)>) which also contain the design for RTL language.
There is also Admin portal to manage users but it is not the part of this folder or React.
## Getting Started
This short guide will help you get started with setting this project up on your development machine.
| Command | Description |
| --------------- | --------------------------------------------------- |
| `npm start` | Start development server on `http://localhost:5085` |
| `npm run build` | Create a build for the production at `/web/dist` |
If we want to make a build locally for the first time, then follow the instructions given in `README.md` located at project's root directory (under Docker heading) which will generate a build in `/web/dist` and it will be served on `http://localhost:8081/`.
Whereas in development, one has to follow the above steps for very first time and then use `npm run start` to start development server on `http://localhost:5085`.
---
**Note:**
Resolve or disable ESLint errors before creating the build. If there are any remaining ESlint errors are there then it will fail to generate the build. (if there are any errors/warnings, then `npm start` will show while server is running)
---
+21
View File
@@ -0,0 +1,21 @@
import {createApi} from '../common/Common'
export const getSavedAnalysesApi = createApi('GET', '/api/v1/analytic', {
inputData: (data) => {
if (data.sort !== 'numberCharts') {
data.sort = 's.' + data.sort
}
return data
},
rejectData: (defHandler, xhr) => {
return defHandler(xhr, undefined, 'Cannot get saved analyses')
}
})
export const deleteSavedAnalysesApi = createApi('DELETE', '/api/v1/analytic/delete', {
inputData: (ids) => {
return JSON.stringify({ids: ids})
},
rejectData: (defHandler, xhr) => {
return defHandler(xhr, undefined, 'Cannot delete saved analyses')
}
})
@@ -0,0 +1,165 @@
import { sum } from 'lodash'
import { convertlocaltoUTC } from '../../common/helper'
import { get, post, put } from '../httpInterceptor/httpInterceptor'
const formatValues = (data) => {
let newObj = {}
for (const [key, value] of Object.entries(data)) {
for (let v in value) {
newObj[v] = newObj[v]
? { ...newObj[v], [key]: value[v] }
: { [key]: value[v] }
}
}
Object.keys(data).map((dt) => {
for (let item in newObj) {
newObj[item][dt] = newObj[item][dt] || 0
}
})
const obj = Object.keys(newObj).map((v) => ({
name: v,
data: newObj[v]
}))
return obj
}
export const addEditAnalyticsAPI = async (data, id) => {
let url = `/analysis${id ? `/${id}` : ''}`
const bodyObj = {}
// bodyObj.filters = [] // need changes
bodyObj.filters = {
date: {
type: 'between',
start: convertlocaltoUTC(data.startDate, 'YYYY-MM-DD'),
end: convertlocaltoUTC(data.endDate, 'YYYY-MM-DD')
}
}
bodyObj.feeds = data.feeds.map((val) => val.id)
const func = id ? put : post
const res = await func(url, bodyObj)
console.log('API Response :: addEditAnalytics ::: ', res)
return res
}
export const getAnalyticDetailsAPI = async (id) => {
let url = `/analysis/${id}`
const res = await get(url)
console.log('API Response :: getAnalyticDetails ::: ', res)
return res
}
export const createAlertAPI = async (data) => {
let url = '/notifications'
const res = await post(url, data)
console.log('API Response :: creteAnalytics ::: ', res)
return res
}
/* Chart APIs */
export const getOverviewBarAPI = async (type = 'none', id) => {
let isOther = type !== 'none'
let url = isOther
? `/mention-over-time-bar-graph/${id}`
: `/mention-bar-graph/${id}`
const res = await post(url, isOther ? { type } : undefined)
if (isOther && res.data && res.data.data) {
res.data.data = res.data.data.map((feed) => ({
name: feed.name,
data: formatValues(feed.data)
}))
}
console.log('API Response :: getOverviewBarAPI ::: ', res)
return res
}
/* Used for Overview, Performance, Sentiment, Demographics */
export const getOverviewPieAPI = async (type = 'none', id) => {
let isOther = type !== 'none'
let url = isOther
? `/mention-over-time-pie-graph/${id}`
: `/mention-pie-graph/${id}`
const res = await post(url, isOther ? { type } : undefined)
console.log('API Response :: getOverviewPieAPI ::: ', res)
return res
}
export const getInfluencersAPI = async (id, filter, data = undefined) => {
let url = `/influencer/${id}`
if (filter === 1) {
data = { isAuthorType: true }
}
const res = await post(url, data)
console.log('API Response :: getInfluencersAPI ::: ', res)
return res
}
export const getEngagementsTimeAPI = async (id) => {
let url = `/engagement-over-time-bar-graph/${id}`
const res = await post(url)
console.log('API Response :: getEngagementsTimeAPI ::: ', res)
return res
}
export const getEngagementsAPI = async (id) => {
let url = `/engagement-over-time-pie-graph/${id}`
const res = await post(url)
console.log('API Response :: getEngagementsAPI ::: ', res)
return res
}
/* Themes */
export const getThemesTimeAPI = async (id) => {
let url = `/theme-over-time-bar-graph/${id}`
const res = await post(url)
const { data } = res.data
let newData = data
if (data) {
newData = data.map((feedData) => {
const { name, data } = feedData
let dataTotal = data.map((theme) => {
const { name, data } = theme
const total = sum(Object.values(data))
return { name, data, total }
})
dataTotal = topN(dataTotal, 5)
return { name, data: dataTotal }
})
}
res.data.data = newData
console.log('API Response :: getThemesTimeAPI ::: ', res)
return res
}
function topN(arr, n) {
if (n > arr.length) {
return arr
}
return arr
.slice()
.sort((a, b) => {
return b.total - a.total
})
.slice(0, n)
}
export const getThemesCloudAPI = async (id) => {
let url = `/theme-over-time-pie-graph/${id}`
const res = await post(url)
console.log('API Response :: getThemesCloudAPI ::: ', res)
return res
}
/* World Map */
export const getWorldMapAPI = async (id) => {
let url = `/world-map/${id}`
const res = await post(url)
console.log('API Response :: getWorldMapAPI ::: ', res)
return res
}
@@ -0,0 +1,15 @@
import { get, del } from '../httpInterceptor/httpInterceptor'
export const savedAnalytics = async (params) => {
let url = '/analysis'
const res = await get(url, params)
console.log('API Response :: savedAnalytics ::: ', res)
return res
}
export const deleteAnalytics = async (id) => {
let url = `/analysis/${id}`
const res = await del(url)
console.log('API Response :: deleteAnalytics ::: ', res)
return res
}
+52
View File
@@ -0,0 +1,52 @@
import {createApi} from '../common/Common'
/**
* payload: {ids: [...]}
*/
export const deleteDocumentsFromFeed = createApi('POST', '/api/v1/feed/{feedId}/documents/delete', {
inputData: (idsArray) => JSON.stringify({ids: idsArray}),
urlData: (params, feedId) => ({feedId})
})
/**
* payload: {emailTo, emailReplyTo, subject, content}
*/
export const sendDocumentsByEmail = createApi('POST', '/api/v1/documents/email', {
})
/**
* payload: {title, comment}
*/
export const commentDocument = createApi('POST', '/api/v1/documents/{documentId}/comments', {
urlData: (params, documentId) => ({documentId})
})
/**
* payload: {ids: []}
*/
export const clipDocuments = createApi('POST', '/api/v1/feed/{feedId}/documents/clip', {
urlData: (params, feedId) => ({feedId}),
inputData: (idsArray) => JSON.stringify({ids: idsArray})
})
/**
* payload: {title, comment}
*/
export const updateComment = createApi('PUT', '/api/v1/comments/{commentId}', {
urlData: (params, commentId) => ({commentId})
})
export const deleteComment = createApi('DELETE', '/api/v1/comments/{commentId}', {
urlData: (params, commentId) => ({commentId})
})
export const getComments = createApi('GET', '/api/v1/documents/{documentId}/comments', {
inputData: (params) => params,
urlData: (params, documentId) => ({documentId})
})
export const readLater = createApi('POST', '/api/v1/feed/readLater/{documentId}', {
urlData: (params, documentId) => ({documentId})
})
export const getRecentClipFeeds = createApi('GET', '/api/v1/feed/recentClip')
+68
View File
@@ -0,0 +1,68 @@
import {createApi, mockApi} from '../common/Common'
const base = '/api/v1/dashboards'
/*class DashboardWidget {
id: number,
type: "feed" | "chart" | "video" | "youtube",
name?: string,
source?: Feed | Chart,
limit?: number,
url?: string
}
class Dashboard {
id: ...
name: string,
layout: any,
widgets: DashboardWidget[]
}
*/
//export const getDashboards = createApi('GET', base);
export const getDashboards = mockApi([
{id: 1, name: 'My Dashboard', layout: '{ver: 1, left: [1, 2], right: [3, 4]}', widgets: [
{id: 1, type: 'feed', name: 'Widget1', source: {id: 1}, limit: 5},
{id: 2, type: 'feed', name: 'Widget2', source: {id: 2}, limit: 5},
{id: 3, type: 'feed', name: 'Widget3', source: {id: 3}, limit: 5},
{id: 4, type: 'feed', name: 'Widget4', source: {id: 4}, limit: 5}
]},
{id: 2, name: 'Dashboard 2', layout: '{ver: 1, left: [5, 6, 7], right: [8]}', widgets: [
{id: 5, type: 'feed', name: 'Widget5', source: {id: 1}, limit: 5},
{id: 6, type: 'feed', name: 'Widget6', source: {id: 2}, limit: 5},
{id: 7, type: 'feed', name: 'Widget7', source: {id: 3}, limit: 5},
{id: 8, type: 'feed', name: 'Widget8', source: {id: 4}, limit: 5}
]},
{id: 44, name: 'Not_my_dashboard', layout: '{ver: 1, left: [], right: [9, 10, 11, 12]}', widgets: [
{id: 9, type: 'feed', name: 'Widget9', source: {id: 1}, limit: 5},
{id: 10, type: 'feed', name: 'Widget10', source: {id: 2}, limit: 5},
{id: 11, type: 'feed', name: 'Widget11', source: {id: 3}, limit: 5},
{id: 12, type: 'feed', name: 'Widget12', source: {id: 4}, limit: 5}
]}
])
//payload = {name}
export const createDashboard = createApi('POST', base)
//payload = dashboard widget
export const createDashboardWidget = createApi('POST', `${base}/{dashboardId}/widgets`, {
urlData: (payload, dashboardId) => ({dashboardId})
})
export const getVideoWidgetUrl = createApi('GET', `${base}/{dashboardId}/widgets/{widgetId}/video`, {
urlData: (payload, dashboardId, widgetId) => ({dashboardId, widgetId})
})
//payload = dashboard without widgets
export const updateDashboard = createApi('PUT', `${base}/{dashboardId}`, {
urlData: (payload, dashboardId) => ({dashboardId})
})
//payload = dashboard widget
export const updateDashboardWidget = createApi('PUT', `${base}/{dashboardId}/widgets/{widgetId}`, {
urlData: (payload, dashboardId, widgetId) => ({dashboardId, widgetId})
})
export const deleteDashboardWidget = createApi('DELETE', `${base}/{dashboardId}`)
export const deleteDashboard = createApi('DELETE', `${base}/{dashboardId}`)
+4
View File
@@ -0,0 +1,4 @@
// import {createApi} from '../common/Common'
// const baseUrl = '/api/v1/receivers'
+54
View File
@@ -0,0 +1,54 @@
import {createApi} from '../common/Common'
const root = '/api/v1/feed'
/**
* payload: {feed: {name: string, category: id, subType: string}, search: {query: string, filters: Object, advancedFilters: Object}}
*/
export const createFeed = createApi('POST', root)
/**
* payload: {feed: {name: string, category: id, subType: string}, search: {query: string, filters: Object, advancedFilters: Object}}
*/
export const saveFeed = createApi('PUT', `${root}/{feedId}`, {
urlData: (data, feedId) => ({feedId})
})
/**
* payload = {name: string}
*/
export const renameFeed = createApi('PUT', `${root}/{feedId}/rename`, {
urlData: (payload, feedId) => ({feedId})
})
export const moveFeed = createApi('POST', `${root}/{feedId}/move_to/{categoryId}`, {
urlData: (payload, feedId, categoryId) => ({feedId, categoryId})
})
export const deleteFeed = createApi('DELETE', `${root}/{feedId}`, {
urlData: (payload, feedId) => ({feedId})
})
/**
* payload: {page: number, advancedFilters: Object}
*/
export const getFeedSearchResults = createApi('POST', `${root}/{feedId}/documents`, {
urlData: (params, feedId) => ({feedId})
})
/**
* payload = {export: bool}
*/
export const toggleExportFeed = createApi('PUT', `${root}/{feedId}/toggleExport`, {
urlData: (payload, feedId) => ({feedId})
})
/**
* payload = {export: bool}
*/
export const toggleExportCategory = createApi('PUT', `${root}/toggleExport/{categoryId}`, {
urlData: (payload, categoryId) => ({categoryId})
})
export const loadExportedFeeds = createApi('GET', `${root}/exported`)
+17
View File
@@ -0,0 +1,17 @@
import {createApi} from '../common/Common'
const baseUrl = '/api/v1/recipients/groups'
export const getItems = createApi('GET', baseUrl, {
inputData: (data) => data
})
export const createItem = createApi('POST', baseUrl)
export const updateItem = createApi('PUT', baseUrl + '/{groupId}', {
urlData: (data, groupId) => ({groupId})
})
export const deleteItems = createApi('POST', baseUrl + '/delete')
export const activateItems = createApi('PUT', baseUrl + '/active')
@@ -0,0 +1,122 @@
import axios from 'axios';
import apiBase from '../../appConfig';
import i18n from '../../i18n';
export const get = (
url,
params,
isPublic = false,
responseType = null,
passedFullURL = false
) => {
let apiUrl = passedFullURL
? `${apiBase.apiUrl}${url}`
: `${apiBase.apiUrl}/api/v1${url}`;
const axiosInstance = axios.create();
const axiosObj = {
method: 'get',
url: apiUrl,
params: params
};
if (isPublic) {
// apis in which no authentication needed
axiosInstance.transformRequest = (data, headers) => {
delete headers.common['Authorization'];
};
}
if (responseType) axiosObj.responseType = responseType;
return axiosInstance(axiosObj)
.then((response) => handleResponse(response))
.catch((error) => handleError(error));
};
export function put(...rest) {
return dataRequest('put', ...rest);
}
export function post(...rest) {
return dataRequest('post', ...rest);
}
export function del(...rest) {
return dataRequest('delete', ...rest);
}
const dataRequest = (
type = 'post',
url,
bodyObj = undefined,
isPublic = false,
mediaFile = false,
passedFullURL = false
) => {
const apiUrl = passedFullURL
? `${apiBase.apiUrl}${url}`
: `${apiBase.apiUrl}/api/v1${url}`;
if (mediaFile) {
const formData = new FormData();
Object.keys(bodyObj).map((key) => {
formData.append(key, bodyObj[key]);
});
bodyObj = formData;
}
const axiosInstance = axios.create();
const axiosObj = {
method: type,
url: apiUrl,
data: bodyObj
};
if (isPublic) {
// apis in which no authentication needed
axiosInstance.transformRequest = (data, headers) => {
delete headers.common['Authorization'];
};
}
return axiosInstance(axiosObj)
.then((response) => handleResponse(response))
.catch((error) => handleError(error));
};
export const handleResponse = (response) => {
if (
response.data &&
(response.data.code === 403 || response.data.code === 404)
) {
return {
error: true,
errorMessage: response.data.message,
data: response.message
};
}
return {
error: false,
data: response.data
};
};
export const handleError = (error) => {
const { response } = error;
let errorMsg = i18n.t('common:alerts.error.somethingWrong');
if (response && response.status === 422) {
if (response.data.message) errorMsg = response.data.message;
} else if (response && response.status === 401) {
// Unauthorized
}
console.log('API Error ::: ', JSON.stringify(response));
return {
error: true,
errorMessage: errorMsg,
data: response ? response.data.errors : null,
status: response ? response.status : null
};
};
+68
View File
@@ -0,0 +1,68 @@
import $ from 'jquery'
import {createApi} from '../common/Common'
import config from '../appConfig'
import { errorConstants } from '../common/constants'
import i18n from '../i18n'
export const login = (userData) => {
return new Promise((resolve, reject) => {
$.ajax({
type: 'POST',
url: config.apiUrl + '/security/token/create',
dataType: 'json',
data: JSON.stringify({
email: userData.email,
password: userData.password
}),
success: function (data) {
resolve(data)
},
error: function (jqXHR, textStatus, errorThrown) {
const errMessage =
jqXHR.responseJSON &&
jqXHR.responseJSON.errors &&
jqXHR.responseJSON.errors
.map((err) =>
i18n.t(`loginApp:errorMessages.${errorConstants[err]}`, {
defaultValue: err || ''
})
)
.join(' ');
console.log(errorThrown + ': Error ' + jqXHR.status, 'jsonAPIERROR');
reject({
msg: errMessage || i18n.t('common:alerts.error.somethingWrong')
});
}
})
})
}
export const loginRefresh = (refreshToken) => {
return new Promise((resolve, reject) => {
$.ajax({
type: 'POST',
url: config.apiUrl + '/security/token/refresh',
dataType: 'json',
data: JSON.stringify({
refreshToken: refreshToken
}),
success: function (data) {
resolve(data)
},
error: function (jqXHR, textStatus, errorThrown) {
console.log(errorThrown + ': Error ' + jqXHR.status, 'jsonAPIERROR')
reject({msg: 'Your session is expired, please login again'})
/* if (jqXHR.status === 401) {
reject({msg: 'Your session is expired, please login again'});
} else {
reject({msg: 'Login error, please login again'});
} */
}
})
})
}
export const getRestrictions = createApi('GET', '/api/v1/users/current/restrictions', {
inputData: (data) => data
})
+38
View File
@@ -0,0 +1,38 @@
import {createApi} from '../common/Common'
const baseUrl = '/api/v1/notifications'
export const getItems = createApi('GET', baseUrl, {
inputData: (data) => data
})
export const getItem = createApi('GET', baseUrl + '/{id}', {
inputData: () => {},
urlData: (data, id) => ({id})
})
export const createItem = createApi('POST', baseUrl)
export const updateItem = createApi('PUT', baseUrl + '/{id}', {
urlData: (data, id) => ({id})
})
export const deleteItems = createApi('POST', baseUrl + '/delete')
export const activateItems = createApi('PUT', baseUrl + '/active')
export const publishItems = createApi('PUT', baseUrl + '/published')
export const subscribeItems = createApi('POST', baseUrl + '/subscribe')
export const getAllItems = createApi('GET', baseUrl + '/all', {
inputData: (data) => data
})
export const getFilters = createApi('GET', baseUrl + '/filters', {
inputData: (data) => data
})
export const getHistory = createApi('GET', baseUrl + '/{notificationId}/history', {
inputData: (data) => data,
urlData: (data, notificationId) => ({notificationId})
})
+154
View File
@@ -0,0 +1,154 @@
import axios from 'axios';
import { cloneDeep } from 'lodash';
import appConfig from '../../appConfig';
import { hubspotBaseURL } from '../../common/constants';
import { getHPContext } from '../../common/helper';
import {
get,
handleError,
handleResponse,
post
} from '../httpInterceptor/httpInterceptor';
export const cancelPlan = async () => {
let url = '/users/cancel/plan';
const res = await post(url);
console.log('API Response :: cancelPlan ::: ', res);
return res;
};
export const getTransactions = async (params) => {
let url = '/users/invoices';
const res = await get(url, params);
console.log('API Response :: getTransactions ::: ', res);
return res;
};
export const updatePlanPayment = async (data) => {
let url = '/users/update/plan';
const res = await post(url, data);
console.log('API Response :: updatePlanPayment ::: ', res);
return res;
};
export const changeCardDetails = async (data) => {
let url = '/users/card/change';
const res = await post(url, data);
console.log('API Response :: changeCard ::: ', res);
return res;
};
// submit update plan data to Hubspot form API
export const updatePlanHubspot = (dataObj) => {
const { hubSpotportalID } = appConfig;
if (!hubSpotportalID) {
return Promise.resolve('No IDs');
}
const data = cloneDeep(dataObj);
data.line1 = data.line2 ? [data.line1, data.line2].join(', ') : data.line1;
const hubSpotFormURL = `${hubspotBaseURL}/47b0e83d-0e26-4528-8822-9aec64db35e8`;
const hubSpotMapping = {
savedFeeds: 'feed_licenses',
searchesPerDay: 'search_licenses',
webFeeds: 'webfeed_licenses',
alerts: 'alert_licenses',
subscriberAccounts: 'user_accounts',
line1: 'address',
city: 'city',
state: 'state',
postal_code: 'zip',
country: 'country',
phone: 'phone',
email: 'email',
totalCost: 'amount'
};
const mediaTypesMapping = {
news: 'News',
blog: 'Blogs',
reddit: 'Reddit',
twitter: 'Twitter',
instagram: 'Instagram'
};
const mediaTypes = Object.keys(mediaTypesMapping)
.filter((key) => data[key])
.map((v) => mediaTypesMapping[v])
.join(';');
const newObj = Object.keys(hubSpotMapping)
.filter((key) => data[key])
.map((key) => ({
name: hubSpotMapping[key],
value: data[key]
}));
newObj.push({
name: 'media_types',
value: mediaTypes
});
newObj.push({
name: 'analytics',
value: data['analytics'] && data['analytics'] !== 0
});
return axios
.post(hubSpotFormURL, {
fields: newObj,
context: getHPContext()
})
.then((response) => handleResponse(response))
.catch((error) => handleError(error));
};
// submit cancel plan data to Hubspot form API
export const cancelPlanHubspot = (dataObj) => {
const { hubSpotportalID } = appConfig;
if (!hubSpotportalID) {
return Promise.resolve('No IDs');
}
const data = cloneDeep(dataObj);
const hubSpotFormURL = `${hubspotBaseURL}/4d2496c3-0535-4723-8b5e-bd04e7903338`;
const hubSpotMapping = {
email: 'email',
content: 'TICKET.content',
subject: 'TICKET.subject'
};
const reason = {
1: '1',
2: '2',
3: '3',
4: '4',
5: '5',
Other: 'Other'
};
const reasonValues = Object.keys(reason)
.filter((key) => data[key])
.map((v) => reason[v])
.join(';');
const newObj = Object.keys(hubSpotMapping)
.filter((key) => data[key])
.map((key) => ({
name: hubSpotMapping[key],
value: data[key]
}));
newObj.push({
name: 'cancelreason',
value: reasonValues
});
return axios
.post(hubSpotFormURL, {
fields: newObj,
context: getHPContext()
})
.then((response) => handleResponse(response))
.catch((error) => handleError(error));
};
+12
View File
@@ -0,0 +1,12 @@
import {createApi} from '../common/Common'
const baseUrl = '/api/v1/receivers'
export const getItems = createApi('GET', baseUrl, {
inputData: (data) => data
})
export const getEmailHistory = createApi('GET', baseUrl + '/{id}/emailHistory', {
urlData: (payload, receiverId) => ({id: receiverId}),
inputData: data => data
})
+17
View File
@@ -0,0 +1,17 @@
import {createApi} from '../common/Common'
const baseUrl = '/api/v1/recipients'
export const getItems = createApi('GET', baseUrl, {
inputData: (data) => data
})
export const createItem = createApi('POST', baseUrl)
export const updateItem = createApi('PUT', baseUrl + '/{recipientId}', {
urlData: (data, recipientId) => ({recipientId})
})
export const deleteItems = createApi('POST', baseUrl + '/delete')
export const activateItems = createApi('PUT', baseUrl + '/active')
@@ -0,0 +1,72 @@
import axios from 'axios';
import appConfig from '../../appConfig';
import {
get,
handleError,
handleResponse,
post
} from '../httpInterceptor/httpInterceptor';
import { getHPContext } from '../../common/helper';
import { hubspotBaseURL } from '../../common/constants';
export const getPlans = async () => {
const url = '/security/plans';
const res = await get(url, null, true, null, true);
console.log('API Response :: getPlans ::: ', res);
return res;
};
export const updatePrice = async (data) => {
let url = '/security/cost_calculation';
const res = await post(url, data, true, null, true);
console.log('API Response :: updatePrice ::: ', res);
return res;
};
export const registerUser = async (data) => {
let url = '/security/registration';
const res = await post(url, data, true, null, true);
console.log('API Response :: registerUser ::: ', res);
return res;
};
export const activeAccount = async (token) => {
let url = `/security/registration/confirm/${token}`;
const res = await post(url, undefined, true, null, true);
console.log('API Response :: activeAccount ::: ', res);
return res;
};
// submit data for form API
export const submitHubspot = (data) => {
const { hubSpotportalID } = appConfig;
if (!hubSpotportalID) {
return Promise.resolve('No IDs');
}
const hubSpotFormURL = `${hubspotBaseURL}/070e31d4-8e6d-480d-89b2-872a6bb28ff4`;
const hubSpotMapping = {
email: 'email',
firstName: 'firstname',
lastName: 'lastname',
companyName: 'company',
jobFunction: 'job_function',
numberOfEmployee: 'numemployees',
industry: 'industry',
websiteUrl: 'website',
lifecyclestage: 'lifecyclestage'
};
const newObj = Object.keys(hubSpotMapping).map((key) => ({
name: hubSpotMapping[key],
value: data[key]
}));
return axios
.post(hubSpotFormURL, {
fields: newObj,
context: getHPContext()
})
.then((response) => handleResponse(response))
.catch((error) => handleError(error));
};
+16
View File
@@ -0,0 +1,16 @@
import {createApi} from '../common/Common'
const root = '/security/registration'
export const getBillingPlans = createApi('GET', `${root}/plans`)
export const sendRegistrationRequest = createApi('POST', root)
export const finishRegistration = createApi('POST', `${root}/finish`)
export const autocompleteOrganizationName = createApi('GET', `${root}/organizationAutocomplete`, {
inputData: (organizationName) => ({organizationName})
})
export const requestPasswordReset = createApi('POST', '/security/resetting/request')
export const confirmPasswordReset = createApi('POST', '/security/resetting/confirm')
+85
View File
@@ -0,0 +1,85 @@
import axios from 'axios'
import { cloneDeep } from 'lodash'
import appConfig from '../appConfig'
import {createApi} from '../common/Common'
import { hubspotBaseURL } from '../common/constants'
import { getHPContext } from '../common/helper'
import { handleError, handleResponse } from './httpInterceptor/httpInterceptor'
const slRoot = '/api/v1/source-list'
export const searchQuery = createApi('POST', '/api/v1/query/search', {})
export const searchSources = createApi('POST', '/api/v1/source-index/', {})
export const addSourcesToLists = createApi('POST', '/api/v1/source-index/add-to-sources-list', {})
export const replaceSourceListsForSource = createApi('POST', '/api/v1/source-index/{id}/list', {
urlData: (params) => ({id: params.id}),
inputData: (params) => JSON.stringify({sourceLists: params.sourceLists})
})
export const getSourceLists = createApi('POST', `${slRoot}/list`, {})
export const addSourceLists = createApi('POST', `${slRoot}/`, {
inputData: (name) => JSON.stringify({name})
})
export const renameSourceLists = createApi('PUT', `${slRoot}/{id}`, {
urlData: (params) => ({id: params.id}),
inputData: (params) => JSON.stringify({name: params.name})
})
export const cloneSourceLists = createApi('POST', `${slRoot}/{id}/clone`, {
urlData: (params) => ({id: params.id}),
inputData: (params) => JSON.stringify({name: params.name})
})
export const deleteSourceLists = createApi('DELETE', `${slRoot}/{id}`, {
urlData: (id) => ({id}),
inputData: () => {}
})
export const getSourcesOfList = createApi('POST', `${slRoot}/{id}/sources/search`, {
urlData: (data, id) => ({id})
})
export const shareSourceList = createApi('POST', `${slRoot}/{id}/share`, {
urlData: (id) => ({id}),
inputData: () => null
})
export const unshareSourceList = createApi('POST', `${slRoot}/{id}/unshare`, {
urlData: (id) => ({id}),
inputData: () => null
})
// submit search queries to Hubspot form API for free user
export const submitSearchHubspot = (dataObj) => {
const { hubSpotportalID } = appConfig;
if (!hubSpotportalID) {
return Promise.resolve('No IDs');
}
const data = cloneDeep(dataObj);
const hubSpotFormURL = `${hubspotBaseURL}/3f297902-d32d-44bb-89a6-12af1c7b886e`;
const hubSpotMapping = {
email: 'email',
searchquery: 'searchquery'
// raw_query: 'raw_query'
};
const newObj = Object.keys(hubSpotMapping)
.filter((key) => data[key])
.map((key) => ({
name: hubSpotMapping[key],
value: data[key]
}));
return axios
.post(hubSpotFormURL, {
fields: newObj,
context: getHPContext()
})
.then((response) => handleResponse(response))
.catch((error) => handleError(error));
};
+22
View File
@@ -0,0 +1,22 @@
import {createApi} from '../common/Common'
export const getCategories = createApi('GET', '/api/v1/categories')
//payload = {name, parent}
export const addCategory = createApi('POST', '/api/v1/categories', {
urlData: (payload, feedId) => ({feedId})
})
//payload = {name, parent}
export const renameCategory = createApi('PUT', '/api/v1/categories/{categoryId}', {
urlData: (payload, categoryId) => ({categoryId})
})
export const moveCategory = createApi('POST', '/api/v1/categories/{categoryId}/move_to/{newCategoryId}', {
urlData: (payload, categoryId, newCategoryId) => ({categoryId, newCategoryId})
})
//payload = {name, parent}
export const deleteCategory = createApi('DELETE', '/api/v1/categories/{categoryId}', {
urlData: (payload, categoryId) => ({categoryId})
})
+7
View File
@@ -0,0 +1,7 @@
import {createApi} from '../common/Common'
const baseUrl = '/api/v1/notifications/themes'
export const getDefaultItem = createApi('GET', baseUrl + '/default', {
inputData: (data) => data
})
+5
View File
@@ -0,0 +1,5 @@
import {createApi} from '../common/Common'
const root = '/api/v1/users'
export const changePassword = createApi('POST', `${root}/change-password`)
+13
View File
@@ -0,0 +1,13 @@
const appConfig = {
appEnv: 'local',
apiUrl: 'http://localhost:8081',
gtagID: 'G-XXXXXXXXX', // Global Tag for Google Analytics
gtagID2: 'UA-XXXXXXXXX',
fbPixelID: '123456',
hubSpotID: '123456',
insightTagID: '',
stripeKey: '',
hubSpotportalID: ''
};
export default appConfig;
+13
View File
@@ -0,0 +1,13 @@
const appConfig = {
appEnv: 'staging',
apiUrl: 'http://stage.socialhose.io',
gtagID: 'G-XXXXXXXXX', // Global Tag for Google Analytics
gtagID2: 'UA-XXXXXXXXX',
fbPixelID: '123456',
hubSpotID: '123456',
insightTagID: '',
stripeKey: '',
hubSpotportalID: ''
};
export default appConfig;
+140
View File
@@ -0,0 +1,140 @@
import $ from 'jquery'
import config from '../appConfig'
export const parseSearchDays = function (date) {
const period = date.slice(-1)
const dateNum = parseInt(date)
if (period === 'd') {
return dateNum
} else {
let daysCount = 0
for (let i = 0; i <= dateNum; i++) {
const date = new Date(new Date().setFullYear(new Date().getFullYear() - i))
daysCount += date.getFullYear() % 4 === 0 ? 366 : 365
console.log(daysCount)
}
return daysCount
}
}
export const makeStickySidebar = function ({component, sidebarSelector, footerSelector, sidebarTopMargin, sidebarBottomMargin}) {
const sidebarEl = $(sidebarSelector)
const footerEl = $(footerSelector)
const sidebarTopPos = parseInt(sidebarEl.css('top'))
const sidebarBottomPos = parseInt(sidebarEl.css('bottom'))
let windowHeight = $(window).height()
let docScrollTop = $(document).scrollTop()
$(window).on('resize', function () {
windowHeight = $(window).height() //recalc win height
_updateSidebarPosition()
})
$(window).on('scroll', function () {
docScrollTop = $(document).scrollTop() //recalc scroll top
_updateSidebarPosition()
})
_updateSidebarPosition()
function _updateSidebarPosition () {
const footerTop = footerEl.offset().top
const windowBottomPos = docScrollTop + windowHeight
// check if document scrollTop position cross sidebar top position with margin
//if so we set sidebar top position to its margin value
if (docScrollTop < sidebarTopPos - sidebarTopMargin) {
sidebarEl.css('top', sidebarTopPos - docScrollTop)
} else {
sidebarEl.css('top', sidebarTopMargin)
}
//fixing overlapping on footer
if (windowBottomPos >= footerTop + sidebarBottomMargin) {
sidebarEl.css('bottom', sidebarBottomPos - footerTop + windowBottomPos)
} else {
sidebarEl.css('bottom', sidebarBottomPos)
}
}
component.componentWillUnmount = function () {
$(window).off('resize')
$(window).off('scroll')
}
return _updateSidebarPosition
}
//default handler - returns errors field from server response or throw error with given text
const defaultApiErrorHandler = (jqXHR, transKey = 'unknown', message = 'Unknown error') => {
if (jqXHR.status === 402) {
return []
}
if (jqXHR.responseJSON && jqXHR.responseJSON.errors && jqXHR.responseJSON.errors.length) {
return jqXHR.responseJSON.errors
} else {
return [{type: 'error', transKey: transKey, message: message}]
}
}
export const createApi = (httpMethod, url,
{
urlData = false,
inputData = (payload) => JSON.stringify(payload),
resolveData = (response) => response,
rejectData = (defHandler, jqXHR) => { return defHandler(jqXHR) }
} = {}
) => {
return (token, payload, ...args) => {
let requestUrl = url
if (typeof urlData === 'function') {
const urlParams = urlData(payload, ...args)
console.log('%c urlParams=' + JSON.stringify(urlParams), 'color: green')
requestUrl = url.replace(/\{(.*?)\}/g, function (match, field) {
return urlParams[field]
})
}
return new Promise((resolve, reject) => {
let ajaxOptions = {
type: httpMethod,
url: config.apiUrl + requestUrl,
dataType: 'json',
contentType: 'application/json',
data: inputData(payload),
success: function (data) {
resolve(resolveData(data))
},
error: function (jqXHR, textStatus, errorThrown) {
console.log(`%c [API Error] HTTP ${jqXHR.status}, ${errorThrown}`, 'background: red; color: yellow')
reject(rejectData(defaultApiErrorHandler, jqXHR, textStatus, errorThrown))
}
}
if (token) {
ajaxOptions.headers = {
Authorization: 'Bearer ' + token
}
}
// Used for backend debugging :)
if (__DEV__) {
ajaxOptions['xhrFields'] = {
withCredentials: true
}
}
$.ajax(ajaxOptions)
})
}
}
export const mockApi = (fakeData, timeout = 2000) => () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(fakeData)
}, timeout)
})
}
+27
View File
@@ -0,0 +1,27 @@
export const normalize = function (arr, entityCallback) {
let ids = []
let entities = {}
arr.forEach((item, i) => {
if (item.id) {
ids.push(item.id)
if (entityCallback) {
entities[item.id] = entityCallback(item, item.id)
}
else {
entities[item.id] = item
}
} else {
ids.push(i.toString())
if (entityCallback) {
entities[i] = entityCallback(item, i)
}
else {
entities[i] = item
}
}
})
return {ids, entities}
}
+30
View File
@@ -0,0 +1,30 @@
export const padLeft = function (string, total) {
if (typeof string !== 'string') {
throw new Error('First parameter must be a string')
}
if (typeof total !== 'number') {
throw new Error('Second parameter must be a integer')
}
return new Array(total - string.length + 1).join('0') + string
}
export const addOrdinalSuffix = function (num) {
if (typeof num !== 'number') {
return num
}
const j = num % 10
const k = num % 100
if (j === 1 && k !== 11) {
return num + 'st'
}
if (j === 2 && k !== 12) {
return num + 'nd'
}
if (j === 3 && k !== 13) {
return num + 'rd'
}
return num + 'th'
}
+21
View File
@@ -0,0 +1,21 @@
import moment from 'moment-timezone'
const getZonesNames = function () {
return moment.tz.names()
}
export const getCurrentTimezone = function () {
return moment.tz.guess()
}
const getTimezones = function () {
const names = getZonesNames()
return names.map(name => {
const zone = moment.tz.zone(name)
const utc = moment.parseZone(zone).format('Z')
const label = `(UTC ${utc}) ${name}`
return {value: name, label}
})
}
export const timezones = getTimezones()
+20
View File
@@ -0,0 +1,20 @@
import appConfig from '../appConfig.js';
const { appEnv, hubSpotportalID } = appConfig;
// when `npm start` server in local
export const isDevelopment = process.env.NODE_ENV === 'development';
// when `npm run build`
export const isProduction = process.env.NODE_ENV === 'production';
// when run locally or build is generated for corresponding sites
export const isLive = appEnv === 'live';
export const isStaging = appEnv === 'staging';
export const isLocal = appEnv === 'local';
export const errorConstants = {
'Bad credentials.': 'badCredentials'
};
export const hubspotBaseURL = `https://api.hsforms.com/submissions/v3/integration/submit/${hubSpotportalID}`;
+183
View File
@@ -0,0 +1,183 @@
import moment from 'moment';
import { cloneDeep } from 'lodash';
import axios from 'axios';
import Cookies from 'cookies-js';
// append scripts in body
export const appendScriptLink = (sources) => {
sources.map((src) => {
const script = document.createElement('script');
script.type = 'text/javascript';
script.setAttribute('src', src);
script.setAttribute('async', true);
document.body.appendChild(script);
});
};
// load script into body part
export const loadScript = (source) => {
const s = document.createElement('script');
s.type = 'text/javascript';
s.async = true;
s.innerHTML = source;
document.body.appendChild(s);
};
// set document element value
export const setDocumentData = (tag, value) => {
if (tag === 'title') {
document.title = value
? `${value} | SOCIALHOSE.IO App`
: 'Social Listening Platform | Social Analytics | SOCIALHOSE.IO App';
}
};
// convert UTC date to Local date
export const convertUTCtoLocal = (date, format = 'YYYY-MM-DD HH:mm:ss') => {
if (!date) return '';
const utcDate = moment.utc(date).format(); //is used to consider input as UTC if timezone offset is not passed
return moment(utcDate).format(format);
};
// convert Local date to UTC date
export const convertlocaltoUTC = (date, format = 'YYYY-MM-DD HH:mm:ss') => {
if (!date) return '';
return moment.utc(date).format(format);
};
// get date
export const getDate = (date, format = 'YYYY-MM-DD HH:mm:ss') => {
return !date ? moment().format(format) : moment(date).format(format);
};
export const getMomentObject = (date) => {
return date ? (moment.isMoment(date) ? date : moment(date)) : null;
};
export const getQueryParams = (obj) => {
if (!obj) {
return null;
}
const { page, pageSize = 10, sorted, searchQuery = undefined } = obj;
const params = {
page: page + 1,
limit: pageSize,
query: searchQuery
};
if (sorted && sorted.length) {
const sortedField = sorted[0];
const sort = {
field: sortedField.id,
direction: sortedField.desc ? 'desc' : 'asc'
};
params['sort'] = sort;
}
return params;
};
export function removeHttpsUrl(url) {
return !url ? '' : url.replace(/(^\w+:|^)\/\//, '');
}
export function capOnlyFirstLetter(string) {
// lodash: capitalize
return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase();
}
export function capFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
export function getValidHttpUrl(string) {
let url;
try {
url = new URL(string);
} catch (_) {
return false;
}
url.protocol = 'https:';
return url.toString();
}
export function getArray(obj, key = 'name', value = 'value') {
return Object.entries(obj).map((v) => ({
[key]: v[0],
[value]: v[1]
}));
}
// get title for source index table
export function getTitle(prevTitle) {
if (prevTitle && prevTitle.replace(/!+/g, '').trim().length > 0) {
return prevTitle;
}
return '[No Name]';
}
export function abbreviateNumber(num) {
if (num >= 1000000000) {
return (num / 1000000000).toFixed(1).replace(/\.0$/, '') + 'G';
}
if (num >= 1000000) {
return (num / 1000000).toFixed(1).replace(/\.0$/, '') + 'M';
}
if (num >= 1000) {
return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'K';
}
return num;
}
export function notNullAndUnd(value) {
return value !== null && value !== undefined;
}
export function validateForm(form, errors, handleValidation) {
let failed;
for (let val in errors) {
const fieldError = errors[val];
if (fieldError) {
failed = true;
} else if (fieldError === null && !form[val] && form[val] !== 0) {
failed = true;
handleValidation(val, true);
}
}
if (failed) {
return false;
} else {
return cloneDeep(form);
}
}
// get IP
export function getIP() {
return localStorage.getItem('ip');
}
export const setIP = async () => {
try {
const res = await axios.get('https://api.ipify.org/?format=json');
res.data && res.data.ip && localStorage.setItem('ip', res.data.ip);
} catch (error) {
console.log(error);
}
};
export function getHPContext() {
return {
hutk: Cookies.get('hubspotutk') || undefined,
ipAddress: getIP() || undefined
};
}
export function arraymove(arr, fromIndex, toIndex) {
if (fromIndex === -1 || toIndex === -1) {
return;
}
var element = arr[fromIndex];
arr.splice(fromIndex, 1);
arr.splice(toIndex, 0, element);
}
+56
View File
@@ -0,0 +1,56 @@
import React from 'react';
import appConfig from '../appConfig';
const { gtagID, gtagID2, fbPixelID, hubSpotID, insightTagID } = appConfig;
export const gtagScriptURL = (
<script
async
src={`https://www.googletagmanager.com/gtag/js?id=${gtagID}`}
></script>
);
export const gtagScript = (
<script>{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${gtagID}', { send_page_view: false });
gtag('config', '${gtagID2}', { send_page_view: false });
`}</script>
);
export const fbPixelScript = (
<script>{`
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window,document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', '${fbPixelID}');
fbq('track', 'PageView');
`}</script>
);
export const hubspotTracking = (
<script
type="text/javascript"
id="hs-script-loader"
async
defer
src={`//js.hs-scripts.com/${hubSpotID}.js`}
></script>
);
export const linkedInsightTag = [
<script key="insight-tag" type="text/javascript">{`
_linkedin_partner_id = "${insightTagID}"; window._linkedin_data_partner_ids = window._linkedin_data_partner_ids || []; window._linkedin_data_partner_ids.push(_linkedin_partner_id);
`}</script>,
<script key="insight-tag-2" type="text/javascript">{`
(function(){var s = document.getElementsByTagName("script")[0]; var b = document.createElement("script"); b.type = "text/javascript";b.async = true; b.src = "https://snap.licdn.com/li.lms-analytics/insight.min.js"; s.parentNode.insertBefore(b, s);})();
`}</script>
];
@@ -0,0 +1,200 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Col, FormGroup, Label, Row } from 'reactstrap';
import { getData } from 'country-list';
import { CardElement } from '@stripe/react-stripe-js';
import { Input } from '../../../common/FormControls';
import { Trans, translate } from 'react-i18next';
const countries = getData().map((v) => ({ label: v.name, value: v.code }));
const cardElementOptions = {
hidePostalCode: true,
style: {
base: {
fontSize: '16px',
color: '#424770'
},
invalid: {
color: '#d92550'
}
}
};
function BillingDetailsForm(props) {
const { form, errors, handleChange, handleValidation, t } = props;
return (
<Row>
<Col md={12}>
<Input
name="name"
title={t('plans.billingForm.fullName')}
type="text"
required
value={form.name}
error={errors.name}
handleChange={handleChange}
handleValidation={handleValidation}
/>
</Col>
<Col md={6}>
<Input
name="line1"
title={t('plans.billingForm.addr1')}
type="text"
required
description={t('plans.billingForm.addr1Desc')}
value={form.line1}
error={errors.line1}
handleChange={handleChange}
handleValidation={handleValidation}
/>
</Col>
<Col md={6}>
<Input
name="line2"
title={t('plans.billingForm.addr2')}
type="text"
description={t('plans.billingForm.addr2Desc')}
value={form.line2}
error={errors.line2}
handleChange={handleChange}
handleValidation={handleValidation}
/>
</Col>
<Col md={6}>
<Input
name="city"
title={t('plans.billingForm.city')}
type="text"
required
description={t('plans.billingForm.cityDesc')}
value={form.city}
error={errors.city}
handleChange={handleChange}
handleValidation={handleValidation}
/>
</Col>
<Col md={6}>
<Input
name="state"
title={t('plans.billingForm.state')}
type="text"
required
description={t('plans.billingForm.stateDesc')}
value={form.state}
error={errors.state}
handleChange={handleChange}
handleValidation={handleValidation}
/>
</Col>
<Col md={6}>
<Input
name="postal_code"
title={t('plans.billingForm.zip')}
type="text"
required
description={t('plans.billingForm.zipDesc')}
value={form.postal_code}
error={errors.postal_code}
handleChange={handleChange}
handleValidation={handleValidation}
/>
</Col>
<Col md={6}>
<Input
name="country"
title={t('plans.billingForm.country')}
type="select"
required
options={countries}
value={form.country}
error={errors.country}
handleChange={handleChange}
handleValidation={handleValidation}
/>
</Col>
<Col md={6}>
<Input
name="email"
title={t('plans.billingForm.email')}
type="email"
required
value={form.email}
error={errors.email}
handleChange={handleChange}
handleValidation={handleValidation}
/>
</Col>
<Col md={6}>
<Input
name="phone"
title={t('plans.billingForm.phone')}
type="tel"
required
description={t('plans.billingForm.phoneDesc')}
value={form.phone}
error={errors.phone}
handleChange={handleChange}
handleValidation={handleValidation}
/>
</Col>
<Col xs={12} className="mb-2">
<FormGroup>
<Label>{t('plans.billingForm.cardHeading')}</Label>
<CardElement
options={cardElementOptions}
className="border b-radius-5 p-3"
/>
</FormGroup>
</Col>
<Col md={12}>
<p className="text-muted">
<Trans i18nKey="plans.billingForm.agreement">
By submitting, you agree to our
<a
title="Privacy Policy"
target="_blank"
href="https://www.socialhose.io/en/legal/privacy"
className="footer__link"
>
Privacy Policy
</a>
<a
title="Terms and Conditions"
target="_blank"
href="https://www.socialhose.io/en/legal/terms"
className="footer__link"
>
Terms & Conditions
</a>
<a
title="Acceptable Use Policy"
target="_blank"
href="https://www.socialhose.io/en/legal/acceptable-use"
className="footer__link"
>
Acceptable Use Policy
</a>
.
</Trans>
</p>
</Col>
</Row>
);
}
BillingDetailsForm.propTypes = {
t: PropTypes.func,
form: PropTypes.object,
errors: PropTypes.object,
handleChange: PropTypes.func,
handleValidation: PropTypes.func
};
export default React.memo(
translate(['tabsContent'], { wait: true })(BillingDetailsForm)
);
@@ -0,0 +1,244 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import useForm from '../../../common/hooks/useForm';
import {
ListGroupItem,
Modal,
ModalBody,
ModalFooter,
ModalHeader,
Row,
Form,
Button,
Col,
ListGroup,
Label
} from 'reactstrap';
import { Checkbox, Input } from '../../../common/FormControls';
import { cancelPlan, cancelPlanHubspot } from '../../../../api/plans/userPlans';
import { planRoutes } from './UserPlans';
const formParams = {
rs1: 1,
rs2: 2,
rs3: 3,
rs4: 4,
rs5: 5,
rs6: 'Other'
};
const initForm = {
[formParams.rs1]: false,
[formParams.rs2]: false,
[formParams.rs3]: false,
[formParams.rs4]: false,
[formParams.rs5]: false,
[formParams.rs6]: false,
Other: false,
content: '',
email: '',
subject: 'Cancellation',
errors: {
email: null
}
};
function CancellationFeedback({ t, actions, isOpen = false, toggle, user }) {
const {
form,
handleChange,
handleValidation,
validateSubmit,
errors
} = useForm(initForm);
const [cancelLoading, setCancelLoading] = useState(false);
const [reasonError, setReasonError] = useState('');
useEffect(() => {
handleChange('email', user.email);
}, [user.email]);
useEffect(() => {
if (Object.values(formParams).some((v) => form[v])) {
setReasonError('');
}
}, [...Object.values(form)]);
function cancelSubscription() {
const obj = validateSubmit();
if (!obj) {
return actions.addAlert({ type: 'error', transKey: 'requiredInfo' });
} else if (!Object.values(formParams).some((v) => obj[v])) {
setReasonError(t('plans.currentPlan.cancelModal.reasonSelect'));
return;
}
setCancelLoading(true);
cancelPlan().then((res) => {
if (res.error) {
res.data
? actions.addAlert(res.data)
: actions.addAlert({ type: 'error', transKey: 'somethingWrong' });
setCancelLoading(false);
return;
}
cancelPlanHubspot({ ...obj });
actions.addAlert({
type: 'notice',
transKey: 'cancelledSubscription'
});
// refresh page on success and move to active plan details
setTimeout(() => {
window.location.pathname = `/app/plans/${planRoutes.current}`;
}, 1000);
});
}
return (
<div>
<Modal size="lg" backdrop="static" isOpen={isOpen} toggle={toggle}>
<ModalHeader toggle={toggle}>
{t('plans.currentPlan.cancelModal.header')}
</ModalHeader>
<ModalBody>
<Row>
<Col md={6} className="mb-3">
<p className="mb-3">
{t('plans.currentPlan.cancelModal.line1', {
firstName: user.firstName
})}
</p>
<p className="mb-2">{t('plans.currentPlan.cancelModal.line2')}</p>
<ListGroup className="text-muted">
<ListGroupItem>
{t('plans.currentPlan.cancelModal.warn1')}
</ListGroupItem>
<ListGroupItem>
{t('plans.currentPlan.cancelModal.warn2')}
</ListGroupItem>
<ListGroupItem>
{t('plans.currentPlan.cancelModal.warn3')}
</ListGroupItem>
<ListGroupItem>
{t('plans.currentPlan.cancelModal.warn4')}
</ListGroupItem>
</ListGroup>
</Col>
<Col md={6} className="mb-3">
<Form>
<p className="mb-4">
{t('plans.currentPlan.cancelModal.feedbackPara')}
</p>
<div>
<Label className="d-inline-block mb-2">
{t('plans.currentPlan.cancelModal.reasonCancellation')}
<span className="text-danger">*</span>
</Label>
<div className="pl-3 mb-3">
<Checkbox
hideTitle
name={formParams.rs1}
formGroupClass="mb-0"
title={t('plans.currentPlan.cancelModal.noNeeds')}
description={t('plans.currentPlan.cancelModal.noNeeds')}
value={form[formParams.rs1]}
error={errors[formParams.rs1]}
handleChange={handleChange}
/>
<Checkbox
hideTitle
name={formParams.rs2}
formGroupClass="mb-0"
title={t('plans.currentPlan.cancelModal.tooNoisy')}
description={t('plans.currentPlan.cancelModal.tooNoisy')}
value={form[formParams.rs2]}
error={errors[formParams.rs2]}
handleChange={handleChange}
/>
<Checkbox
hideTitle
name={formParams.rs3}
formGroupClass="mb-0"
title={t('plans.currentPlan.cancelModal.confusing')}
description={t('plans.currentPlan.cancelModal.confusing')}
value={form[formParams.rs3]}
error={errors[formParams.rs3]}
handleChange={handleChange}
/>
<Checkbox
hideTitle
name={formParams.rs4}
formGroupClass="mb-0"
title={t('plans.currentPlan.cancelModal.expensive')}
description={t('plans.currentPlan.cancelModal.expensive')}
value={form[formParams.rs4]}
error={errors[formParams.rs4]}
handleChange={handleChange}
/>
<Checkbox
hideTitle
name={formParams.rs5}
formGroupClass="mb-0"
title={t('plans.currentPlan.cancelModal.covid')}
description={t('plans.currentPlan.cancelModal.covid')}
value={form[formParams.rs5]}
error={errors[formParams.rs5]}
handleChange={handleChange}
/>
<Checkbox
hideTitle
name={formParams.rs6}
formGroupClass="mb-0"
title={t('plans.currentPlan.cancelModal.other')}
description={t('plans.currentPlan.cancelModal.other')}
value={form[formParams.rs6]}
error={errors[formParams.rs6]}
handleChange={handleChange}
/>
<span className="text-danger">{reasonError}</span>
</div>
</div>
<Input
name="content"
title={t('plans.currentPlan.cancelModal.tellMore')}
type="textarea"
value={form.content}
error={errors.content}
handleChange={handleChange}
handleValidation={handleValidation}
/>
</Form>
</Col>
</Row>
</ModalBody>
<ModalFooter>
<Button color="link" onClick={toggle}>
{t('plans.currentPlan.cancelModal.undoBtn')}
</Button>
<Button
color="danger"
disabled={cancelLoading}
onClick={cancelSubscription}
>
{cancelLoading
? t('plans.currentPlan.cancelModal.loadingBtn')
: t('plans.currentPlan.cancelModal.cancelSubscriptionBtn')}
</Button>
</ModalFooter>
</Modal>
</div>
);
}
CancellationFeedback.propTypes = {
t: PropTypes.func,
actions: PropTypes.object,
isOpen: PropTypes.bool,
toggle: PropTypes.func,
user: PropTypes.object
};
export default CancellationFeedback;
@@ -0,0 +1,185 @@
import React, { Fragment, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { CardElement, useElements, useStripe } from '@stripe/react-stripe-js';
import { Alert, Button, Card, CardBody, CardTitle, Col, Row } from 'reactstrap';
import { reduxActions } from '../../../../redux/utils/connect';
import useForm from '../../../common/hooks/useForm';
import useIsMounted from '../../../common/hooks/useIsMounted';
import BillingDetailsForm from './BillingDetailsForm';
import { changeCardDetails } from '../../../../api/plans/userPlans';
import { planRoutes } from './UserPlans';
import { setDocumentData } from '../../../../common/helper';
import { translate } from 'react-i18next';
const initialForm = {
name: '',
line1: '',
line2: '',
city: '',
state: '',
postal_code: '',
country: '',
email: '',
phone: '',
errors: {
name: null,
line1: null,
city: null,
state: null,
postal_code: null,
country: null,
email: null,
phone: null
}
};
function ChangeCard({ actions, t }) {
const isMounted = useIsMounted();
const stripe = useStripe();
const elements = useElements();
const {
form,
errors,
handleChange,
handleValidation,
validateSubmit
} = useForm(initialForm);
const [paymentError, setPaymentError] = useState(false);
const [loading, setLoading] = useState(false);
useEffect(() => {
setDocumentData('title', 'Change Card');
return () => setDocumentData('title'); // default
}, []);
const submitPayment = async () => {
if (!stripe || !elements) {
// Stripe.js has not loaded yet.
return;
}
setPaymentError(false);
setLoading(true);
const obj = validateSubmit();
if (!obj) {
setLoading(false);
return actions.addAlert({
type: 'error',
transKey: 'requiredInfo'
});
}
const cardElement = elements.getElement(CardElement);
const {
name,
line1,
line2,
city,
state,
postal_code,
country,
email,
phone
} = obj;
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: cardElement,
billing_details: {
name,
email,
phone,
address: {
line1: line1,
line2: line2,
city: city,
state: state,
postal_code: postal_code,
country: country
}
}
});
if (error) {
setPaymentError(error);
setLoading(false);
return;
}
const newObj = {};
newObj.paymentID = paymentMethod.id; //stripe card element ID
const res = await changeCardDetails(newObj);
if (!isMounted.current) {
return;
}
if (res.error) {
res.data
? actions.addAlert(res.data)
: actions.addAlert({ type: 'error', transKey: 'somethingWrong' });
setLoading(false);
return;
}
actions.addAlert({ type: 'notice', transKey: 'cardUpdated' });
// refresh page on success and move to active plan details
setTimeout(() => {
window.location.pathname = `/app/plans/${planRoutes.current}`;
}, 1000);
};
return (
<Col xs="12" lg="8" xl="9">
<Card className="mb-3">
<CardBody>
<CardTitle>{t('plans.changeCard.heading')}</CardTitle>
<p className="text-muted mb-3">{t('plans.changeCard.subText')}</p>
<BillingDetailsForm
form={form}
errors={errors}
handleChange={handleChange}
handleValidation={handleValidation}
/>
<Row className="divider" />
{paymentError && (
<Alert color="danger">
<Fragment>
<p className="font-size-xs font-weight-bold text-uppercase">
{t('plans.changeCard.error')}
</p>
{paymentError.message}
</Fragment>
</Alert>
)}
<div className="text-right">
<Button
type="button"
color="primary"
onClick={submitPayment}
disabled={!stripe || !elements || loading}
className="btn-wide btn-hover-shine mb-2 mb-sm-0"
size="lg"
>
{loading
? t('plans.changeCard.loadingBtn')
: t('plans.changeCard.changeCardBtn')}
</Button>
</div>
</CardBody>
</Card>
</Col>
);
}
ChangeCard.propTypes = {
t: PropTypes.func.isRequired,
actions: PropTypes.object
};
export default reduxActions()(
translate(['tabsContent'], { wait: true })(ChangeCard)
);
@@ -0,0 +1,286 @@
import React, { Fragment, useEffect, useState } from 'react';
import { useHistory } from 'react-router';
import PropTypes from 'prop-types';
import { Button, Card, CardBody, CardTitle, Col, Row } from 'reactstrap';
import reduxConnect from '../../../../redux/utils/connect';
import { planRoutes } from './UserPlans';
import { allMediaTypes } from '../../../../redux/modules/appState/searchByFilters';
import { capitalize } from 'lodash';
import { convertUTCtoLocal, setDocumentData } from '../../../../common/helper';
import { translate } from 'react-i18next';
import CancellationFeedback from './CancellationFeedback';
function CurrentPlan({ actions, user, t }) {
const [cancelModal, setCancelModal] = useState(false);
const { restrictions } = user;
const { push } = useHistory();
useEffect(() => {
setDocumentData('title', 'Active Plan Details');
return () => setDocumentData('title'); // default
}, []);
function changePlan() {
push(`/app/plans/${planRoutes.update}`);
}
function toggleCancelModal() {
setCancelModal((prev) => !prev);
}
const {
plans,
limits,
isPlanCancelled,
subStartDate,
subEndDate
} = restrictions;
const selectedMedias = [];
const notSelectedMedias = [];
allMediaTypes.map((v) => {
if (plans[v]) {
selectedMedias.push(t(`searchTab.sourceTypes.${v}`, capitalize(v)));
} else {
notSelectedMedias.push(t(`searchTab.sourceTypes.${v}`, capitalize(v)));
}
});
const isRTL = document.documentElement.dir === 'rtl';
return (
<Col xs="12" lg="8" xl="9">
<Row>
<Col sm="6" md="4">
<div className="card mb-3 widget-chart text-left">
<div className="widget-chart-content">
<div className="widget-subheading">
{t('plans.currentPlan.subHeading')}
</div>
<div className="widget-numbers">
{plans.price === 0
? t('plans.currentPlan.freePlan')
: `$${plans.price}`}
</div>
<div className="widget-description">
<span>
{plans.price === 0 ? (
<Fragment>&nbsp;</Fragment>
) : subStartDate && subEndDate ? (
`${convertUTCtoLocal(
subStartDate,
'MMM D, YYYY'
)} - ${convertUTCtoLocal(subEndDate, 'MMM D, YYYY')}`
) : (
t('plans.currentPlan.perMonth')
)}
</span>
</div>
</div>
</div>
</Col>
<Col sm="6">
<button
className="card mb-3 widget-chart bg-success text-white text-left"
onClick={changePlan}
>
<div className="widget-chart-content">
<div className="widget-subheading">
{t('plans.currentPlan.changePlan')}
</div>
<div className="widget-numbers font-size-xlg">
{t('plans.currentPlan.upgradeYourPlan')}
</div>
<div className="widget-description">
<span>{t('plans.currentPlan.upgradeText')}</span>
</div>
</div>
</button>
</Col>
<Col xs="12">
<Card>
<CardBody>
<CardTitle>{t('plans.currentPlan.currentPlanDetails')}</CardTitle>
<div className="mb-3">
<p className="text-muted">
{t('plans.currentPlan.selectedMediaTypes')}
</p>
<p className="font-size-xlg">
{selectedMedias.length > 0
? selectedMedias.join(', ')
: t('plans.currentPlan.none')}
{notSelectedMedias.length > 0 ? (
<span className="font-size-md opacity-6 ml-2">
({t('plans.currentPlan.upgradeToGet')}:{' '}
{notSelectedMedias.join(', ')})
</span>
) : (
''
)}
</p>
</div>
<div className="divider" />
<div className="mb-3">
<p className="text-muted mb-2">
{t('plans.currentPlan.selectedLicenses')}
</p>
<Row>
<Col xs="12" sm="6" md="3">
<div className="mb-3 card widget-chart">
{!isRTL ? (
<div className="widget-numbers">
{limits.savedFeeds.current}/{limits.savedFeeds.limit}
</div>
) : (
<div className="widget-numbers">
{limits.savedFeeds.limit}/{limits.savedFeeds.current}
</div>
)}
<div className="widget-subheading mb-3">
{t('plans.currentPlan.feedsLicenses')}
</div>
</div>
</Col>
<Col xs="12" sm="6" md="3">
<div className="mb-3 card widget-chart">
{!isRTL ? (
<div className="widget-numbers">
{limits.searchesPerDay.current}/
{limits.searchesPerDay.limit}
</div>
) : (
<div className="widget-numbers">
{limits.searchesPerDay.limit}/
{limits.searchesPerDay.current}
</div>
)}
<div className="widget-subheading mb-3">
{t('plans.currentPlan.searchLicenses')}
</div>
</div>
</Col>
<Col xs="12" sm="6" md="3">
<div className="mb-3 card widget-chart">
{!isRTL ? (
<div className="widget-numbers">
{limits.webFeeds.current}/{limits.webFeeds.limit}
</div>
) : (
<div className="widget-numbers">
{limits.webFeeds.limit}/{limits.webFeeds.current}
</div>
)}
<div className="widget-subheading mb-3">
{t('plans.currentPlan.webfeedLicenses')}
</div>
</div>
</Col>
<Col xs="12" sm="6" md="3">
<div className="mb-3 card widget-chart">
{!isRTL ? (
<div className="widget-numbers">
{limits.alerts.current}/{limits.alerts.limit}
</div>
) : (
<div className="widget-numbers">
{limits.alerts.limit}/{limits.alerts.current}
</div>
)}
<div className="widget-subheading mb-3">
{t('plans.currentPlan.alertLicenses')}
</div>
</div>
</Col>
<Col xs="12" sm="6" md="3">
<div className="mb-3 card widget-chart">
{!isRTL ? (
<div className="widget-numbers">
{limits.subscriberAccounts.current}/
{limits.subscriberAccounts.limit}
</div>
) : (
<div className="widget-numbers">
{limits.subscriberAccounts.limit}/
{limits.subscriberAccounts.current}
</div>
)}
<div className="widget-subheading mb-3">
{t('plans.currentPlan.userAccounts')}
</div>
</div>
</Col>
</Row>
</div>
<div className="divider" />
<div className="mb-3">
<p className="text-muted">{t('plans.currentPlan.features')}</p>
<p className="font-size-xlg">
{plans.analytics ? (
t('plans.currentPlan.analytics')
) : (
<Fragment>
{t('plans.currentPlan.none')}
<span className="font-size-md opacity-6 ml-2">
({t('plans.currentPlan.upgradeToGet')}:{' '}
{t('plans.currentPlan.analytics')})
</span>
</Fragment>
)}
</p>
</div>
{plans.price > 0 && (
<Fragment>
<div className="divider" />
<div className="mb-3">
{!isPlanCancelled ? (
<div className="text-muted">
<Button
color="danger"
outline
onClick={toggleCancelModal}
>
{t('plans.currentPlan.cancelSubscriptionBtn')}
</Button>
<p className="text-muted mt-2">
{t('plans.currentPlan.cancelWarning')}
</p>
</div>
) : (
<div className="text-muted">
<Button color="secondary" outline disabled>
{t('plans.currentPlan.cancelSubscriptionBtn')}
</Button>
<p className="d-block d-md-inline-block ml-md-3 mt-md-0 mt-2 ml-0 text-muted">
{t('plans.currentPlan.alreadyCancelled')}
</p>
</div>
)}
</div>
</Fragment>
)}
<CancellationFeedback
isOpen={cancelModal}
toggle={toggleCancelModal}
actions={actions}
user={user}
t={t}
/>
</CardBody>
</Card>
</Col>
</Row>
</Col>
);
}
CurrentPlan.propTypes = {
t: PropTypes.func.isRequired,
actions: PropTypes.object,
user: PropTypes.object
};
export default reduxConnect('user', ['common', 'auth', 'user'])(
translate(['tabsContent'], { wait: true })(CurrentPlan)
);
@@ -0,0 +1,168 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import {
Button,
Col,
Modal,
ModalBody,
ModalFooter,
ModalHeader,
Row,
Table
} from 'reactstrap';
import { convertUTCtoLocal } from '../../../../common/helper';
import moment from 'moment';
import { capitalize } from 'lodash';
import { translate } from 'react-i18next';
function ShowTransactionDetails(props) {
const { data, closeModal, t } = props;
const plan = data && data.lines && data.lines.data && data.lines.data[0];
useEffect(() => {
return () => closeModal();
}, []);
return (
<Modal isOpen={!!data && !!plan} toggle={closeModal} size="lg">
<ModalHeader toggle={closeModal}>
{t('plans.transactions.modal.heading')}
</ModalHeader>
<ModalBody>
{data && (
<Row>
<Col xs="12" lg="6" className="mb-3">
<h6 className="mb-3">
{t('plans.transactions.modal.transactionDetails')}
</h6>
<Table striped>
<tbody>
<tr>
<th>{t('plans.transactions.modal.transactionDate')}</th>
<td>
{convertUTCtoLocal(
moment.unix(
data.status_transitions &&
data.status_transitions.paid_at
),
'MM/DD/YYYY hh:mm:ss a'
)}
</td>
</tr>
<tr>
<th>{t('plans.transactions.modal.activationDate')}</th>
<td>
{convertUTCtoLocal(
moment.unix(plan && plan.period.start),
'MM/DD/YYYY'
)}
</td>
</tr>
<tr>
<th>{t('plans.transactions.modal.expirationDate')}</th>
<td>
{convertUTCtoLocal(
moment.unix(plan && plan.period.end),
'MM/DD/YYYY'
)}
</td>
</tr>
<tr>
<th>{t('plans.transactions.modal.amount')}</th>
<td>${data.amount_paid / 100}</td>
</tr>
<tr>
<th>{t('plans.transactions.modal.status')}</th>
<td>{capitalize(data.status)}</td>
</tr>
</tbody>
</Table>
</Col>
<Col xs="12" lg="6" className="mb-3">
<h6 className="mb-3">
{t('plans.transactions.modal.billingDetails')}
</h6>
<Table striped>
<tbody>
<tr>
<th>{t('plans.transactions.modal.name')}</th>
<td>{data.customer_name || '-'}</td>
</tr>
<tr>
<th>{t('plans.transactions.modal.email')}</th>
<td>{data.customer_email || '-'}</td>
</tr>
<tr>
<th>{t('plans.transactions.modal.phone')}</th>
<td>{data.customer_phone || '-'}</td>
</tr>
<tr>
<th>{t('plans.transactions.modal.address')}</th>
<td>{data.customer_address || '-'}</td>
</tr>
<tr>
<th>{t('plans.transactions.modal.invoiceNo')}</th>
<td>
{data.number} (
<a
href={data.hosted_invoice_url}
rel="noopener noreferrer"
target="_blank"
>
{t('plans.transactions.modal.showInvoiceLink')}
</a>
)
</td>
</tr>
</tbody>
</Table>
</Col>
{/* <Col xs="12" lg="6" className="mb-3">
<h6 className="mb-3">Plan Details</h6>
<Table striped>
<tbody>
<tr>
<th>Feeds Licenses</th>
<td>0</td>
</tr>
<tr>
<th>Webfeed Licenses</th>
<td>0</td>
</tr>
<tr>
<th>Newsletter Licenses</th>
<td>0</td>
</tr>
<tr>
<th>User Accounts</th>
<td>0</td>
</tr>
<tr>
<th>Analytics</th>
<td>No</td>
</tr>
</tbody>
</Table>
</Col> */}
</Row>
)}
</ModalBody>
<ModalFooter>
<Button color="link" onClick={closeModal}>
{t('plans.transactions.modal.cancelBtn')}
</Button>
</ModalFooter>
</Modal>
);
}
ShowTransactionDetails.propTypes = {
t: PropTypes.func,
closeModal: PropTypes.func,
data: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]).isRequired
};
export default React.memo(
translate(['tabsContent'], { wait: true })(ShowTransactionDetails)
);
@@ -0,0 +1,749 @@
/* eslint-disable react/jsx-no-bind */
import React, { Fragment, useCallback, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import Slider from 'rc-slider';
import Tooltip from 'rc-tooltip';
import {
Alert,
Button,
Card,
CardBody,
CardTitle,
Col,
Form,
FormGroup,
Label,
Modal,
ModalBody,
ModalFooter,
ModalHeader,
Row
} from 'reactstrap';
import {
licenses,
mediaTypes,
features,
addonFeatures
} from '../../../LoginRegister/Registration/PlanConstants';
import useForm from '../../../common/hooks/useForm';
import { debounce } from 'lodash';
import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
import useIsMounted from '../../../common/hooks/useIsMounted';
import reduxConnect from '../../../../redux/utils/connect';
import {
getPlans,
updatePrice
} from '../../../../api/registration/registration';
import {
updatePlanHubspot,
updatePlanPayment
} from '../../../../api/plans/userPlans';
import { planRoutes } from './UserPlans';
import BillingDetailsForm from './BillingDetailsForm';
import simpleNumberLocalizer from 'react-widgets-simple-number';
import NumberPicker from 'react-widgets/lib/NumberPicker';
import LoadersAdvanced from '../../../common/Loader/Loader';
import { IoIosWarning } from 'react-icons/io';
import { convertUTCtoLocal, setDocumentData } from '../../../../common/helper';
import { translate } from 'react-i18next';
simpleNumberLocalizer();
const Handle = Slider.Handle;
const handle = (props) => {
// eslint-disable-next-line react/prop-types
const { value, dragging, index, ...restProps } = props;
return (
<Tooltip
key={index}
prefixCls="rc-slider-tooltip"
overlay={value}
visible={dragging}
placement="top"
>
<Handle value={value} {...restProps} />
</Tooltip>
);
};
const initialForm = {
savedFeeds: 0,
searchesPerDay: 0,
webFeeds: 0,
alerts: 0,
news: 0,
blog: 0,
reddit: 0,
instagram: 0,
twitter: 0,
analytics: 0,
subscriberAccounts: 0,
masterAccounts: 0
};
const initialPaymentForm = {
name: '',
line1: '',
line2: '',
city: '',
state: '',
postal_code: '',
country: '',
email: '',
phone: '',
errors: {
name: null,
line1: null,
city: null,
state: null,
postal_code: null,
country: null,
email: null,
phone: null
}
};
function UpdatePlan({ actions, restrictions, t }) {
const stripe = useStripe();
const elements = useElements();
const isMounted = useIsMounted();
// first step
const { form, handleChange, resetForm } = useForm(initialForm);
const [updatingPrice, setUpdatingPrice] = useState(true);
const [totalCost, setTotalCost] = useState(' - ');
const [modal, setModal] = useState(false);
const [loading, setLoading] = useState(false);
const [planLoading, setPlanLoading] = useState(true);
const [planError, setPlanError] = useState(false);
const [planList, setPlanList] = useState([]);
const [disableUpdate, setDisableUpdate] = useState(true);
// second step
const [nextStep, setNextStep] = useState(false);
const {
form: paymentForm,
handleChange: handlePaymentForm,
errors: paymentFormErrors,
handleValidation: handlePaymentValidation,
validateSubmit
} = useForm(initialPaymentForm);
const [paymentError, setPaymentError] = useState(false);
const [paymentLoading, setPaymentLoading] = useState(false);
// to update price when input changes
useEffect(() => {
if (planList.length > 0) {
debouncePrice(form);
}
}, [...Object.values(form)]);
const debouncePrice = useCallback(
debounce((form) => {
setUpdatingPrice(true);
updatePrice(form).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || isNaN(res.data.totalPrice)) {
actions.addAlert(res.data);
setUpdatingPrice(false);
setTotalCost('Error');
return;
}
setTotalCost(res.data.totalPrice);
setUpdatingPrice(false);
});
}, 1000),
[isMounted.current]
);
useEffect(() => {
if (!restrictions.isPlanCancelled && !restrictions.isPlanDowngrade) {
setDisableUpdate(false);
} else {
setDisableUpdate(true);
}
}, [restrictions.isPlanCancelled, restrictions.isPlanDowngrade]);
useEffect(() => {
getBillingPlans();
setDocumentData('title', 'Update Plan');
return () => setDocumentData('title'); // default
}, []);
function getBillingPlans() {
setPlanLoading(true);
setPlanError(false);
getPlans().then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data || !res.data.length) {
setPlanError(true);
setPlanLoading(false);
res.data && res.data.length > 0 && actions.addAlert(res.data);
return;
}
setPlanLoading(false);
setPlanList(res.data);
const modified = { ...initialForm };
let selectedPlan = {};
if (restrictions.plans.price > 0) {
selectedPlan = { ...restrictions.plans };
Object.entries(restrictions.limits).map(([key, value]) => {
selectedPlan[key] = value.limit;
});
selectedPlan.blog = selectedPlan.blogs;
delete selectedPlan.blogs;
} else {
selectedPlan = res.data[0];
}
Object.keys(initialForm).map((key) => {
modified[key] =
selectedPlan[key] === undefined
? modified[key]
: selectedPlan[key] === true
? 1
: selectedPlan[key] === false
? 0
: selectedPlan[key];
});
resetForm(modified);
});
}
function changePlan(id) {
const selectedPlan = planList.find((plan) => plan.id === id);
const modified = { ...initialForm };
Object.keys(initialForm).map((key) => {
modified[key] =
selectedPlan[key] === undefined
? modified[key]
: selectedPlan[key] === true
? 1
: selectedPlan[key] === false
? 0
: selectedPlan[key];
});
resetForm(modified);
}
function handleSubmit() {
if (restrictions.isPlanCancelled || restrictions.isPlanDowngrade) {
return;
}
// move to payment page if new basic user
// instruct according to upgrade and downgrade
// if card already stored then only update the plan by showing modal or providing option to change card
setLoading(true);
if (restrictions.isPaymentId) {
setModal(true); // show details of card
} else {
setNextStep(true);
window.scrollTo(0, 0);
}
setLoading(false);
}
function toggle() {
setModal((prev) => !prev);
}
function proceedToDetails() {
toggle();
setNextStep(true);
window.scrollTo(0, 0);
}
const submitPayment = async () => {
if (!stripe || !elements) {
// Stripe.js has not loaded yet.
return;
}
if (restrictions.isPlanCancelled || restrictions.isPlanDowngrade) {
return;
}
setPaymentError(false);
setPaymentLoading(true);
const obj = validateSubmit();
if (!obj) {
setPaymentLoading(false);
return actions.addAlert({
type: 'error',
transKey: 'requiredInfo'
});
}
const cardElement = elements.getElement(CardElement);
const {
name,
line1,
line2,
city,
state,
postal_code,
country,
email,
phone
} = obj;
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: cardElement,
billing_details: {
name,
email,
phone,
address: {
line1: line1,
line2: line2,
city: city,
state: state,
postal_code: postal_code,
country: country
}
}
});
if (error) {
setPaymentError(error);
setPaymentLoading(false);
return;
}
const newObj = { ...form };
newObj.masterAccounts = '1';
newObj.paymentID = paymentMethod.id; //stripe card element ID
const res = await updatePlanPayment(newObj);
if (res.error) {
res.data
? actions.addAlert(res.data)
: actions.addAlert({ type: 'error', transKey: 'somethingWrong' });
setPaymentLoading(false);
return;
}
window.gtag &&
window.gtag('event', 'purchase', {
currency: 'USD',
value: totalCost
});
await updatePlanHubspot({ ...obj, ...form, totalCost });
actions.addAlert({ type: 'notice', transKey: 'planUpdated' });
// refresh page on success and move to active plan details
setTimeout(() => {
window.location.pathname = `/app/plans/${planRoutes.current}`;
}, 1000);
};
const proceedPayment = async () => {
// payment with old card
setLoading(true);
const newObj = { ...form };
newObj.masterAccounts = '1';
const res = await updatePlanPayment(newObj);
if (res.error) {
res.data
? actions.addAlert(res.data)
: actions.addAlert({ type: 'error', transKey: 'somethingWrong' });
setLoading(false);
return;
}
window.gtag &&
window.gtag('event', 'purchase', {
currency: 'USD',
value: totalCost
});
await updatePlanHubspot({ ...form, totalCost });
actions.addAlert({ type: 'notice', transKey: 'planUpdated' });
// refresh page on success and move to active plan details
setTimeout(() => {
window.location.pathname = `/app/plans/${planRoutes.current}`;
}, 1000);
};
function moveBack() {
window.scrollTo(0, 0);
setNextStep(false);
}
if (planError || planLoading) {
return (
<Col xs="12" lg="8" xl="9">
<Card className="h-75 mb-3">
<CardBody>
<CardTitle>{t('plans.updatePlan.heading')}</CardTitle>
{planError && (
<div className="text-danger text-center p-4">
<IoIosWarning
className="d-block mx-auto mb-2"
fontSize="32px"
/>
{t('plans.updatePlan.planLoadingFailed')}{' '}
<Button color="link" onClick={getBillingPlans} className="p-0">
{t('plans.updatePlan.tryAgainBtn')}
</Button>
</div>
)}
</CardBody>
{planLoading && <LoadersAdvanced />}
</Card>
</Col>
);
}
const isRTL = document.documentElement.dir === 'rtl';
return (
<Col xs="12" lg="8" xl="9">
<Card className="mb-3">
{!nextStep ? (
<CardBody>
<CardTitle>{t('plans.updatePlan.heading')}</CardTitle>
<p className="text-muted">
{t('plans.updatePlan.subText')}{' '}
<a
href="https://www.socialhose.io/en/pricing"
rel="noopener noreferrer"
target="_blank"
>
{t('plans.updatePlan.learnMoreBtn')}
</a>
.
</p>
<hr />
<Form>
<Row>
<Col md={12}>
<div className="mb-3">
<h6 className="font-weight-bold mb-3">
{t('plans.updatePlan.prePlans')}
</h6>
<div className="d-flex flex-wrap justify-content-center justify-content-md-start">
{planList.map((plan) => (
<Button
outline
key={plan.id}
color="primary"
type="button"
className="btn-wide btn-lg p-sm-3 mb-2 mr-2"
onClick={() => changePlan(plan.id)}
>
{plan.name}
</Button>
))}
</div>
</div>
<hr />
<div className="mb-3">
<h6 className="font-weight-bold mb-3">
{t('plans.updatePlan.mediaTypes')}
</h6>
<div>
{mediaTypes.map((type) => (
<Button
key={type.name}
size="lg"
type="button"
title={
form[type.name]
? 'Click to deselect'
: 'Click to select'
}
outline={!form[type.name]}
className="btn-pill mb-2 mr-2"
color={form[type.name] ? 'success' : 'light'}
onClick={() =>
handleChange(type.name, !form[type.name])
}
>
{t(`searchTab.sourceTypes.${type.transKey}`)} (
{type.price})
</Button>
))}
</div>
</div>
<hr />
<div className="mb-3">
<h6 className="font-weight-bold mb-3">
{t('plans.updatePlan.licenses')}
</h6>
<Row noGutters className="justify-content-center">
{licenses.map((license) => (
<Col sm={6} key={license.name}>
<div className="p-4 m-2 border b-radius-5 shadow-sm">
<FormGroup>
<div className="d-flex justify-content-between">
<Label title={license.title}>
{t(`plans.currentPlan.${license.transKey}`)}
</Label>
<span className="font-size-lg font-weight-bold text-primary">
{form[license.name]}
</span>
</div>
<Slider
{...license.props}
reverse={isRTL}
handle={handle}
value={form[license.name]}
onChange={(val) =>
handleChange(license.name, val)
}
/>
</FormGroup>
</div>
</Col>
))}
</Row>
</div>
<hr />
<Row>
<Col md="6">
<div className="mb-3">
<h6 className="font-weight-bold mb-3">
{t('plans.updatePlan.features')}
</h6>
<div>
{features.map((type) => (
<Button
key={type.name}
size="lg"
type="button"
title={
form[type.name]
? t('plans.updatePlan.deselectTooltip')
: t('plans.updatePlan.selectTooltip')
}
outline={!form[type.name]}
className="btn-pill mb-2 mr-2"
color={form[type.name] ? 'success' : 'light'}
onClick={() =>
handleChange(type.name, !form[type.name])
}
>
{t(`plans.currentPlan.${type.transKey}`)} (
{type.price})
</Button>
))}
<div className="pl-2">
{features.map((type) =>
form[type.name] ? (
<p
key={type.name}
className="font-size-sm text-muted mb-1"
>
{type.desc}
</p>
) : null
)}
</div>
</div>
</div>
</Col>
<Col md="6">
<div className="mb-3">
<h6 className="font-weight-bold mb-3">
{t('plans.updatePlan.addOns')}
</h6>
<Row className="px-3">
{addonFeatures.map((type) => (
<Col xs="12" key={type.name}>
<FormGroup>
<Label>
{t(`plans.currentPlan.${type.transKey}`)}
</Label>
<NumberPicker
{...type.props}
value={form[type.name]}
onChange={(val) =>
handleChange(type.name, val)
}
/>
</FormGroup>
</Col>
))}
</Row>
</div>
</Col>
</Row>
<div className="widget-content total-price">
<div className="widget-content-wrapper justify-content-start justify-content-md-end mr-5">
<div className="widget-content-left">
<div className="widget-heading">
{t('plans.updatePlan.totalCost')}
</div>
<div className="widget-subheading">
{t('plans.updatePlan.monthly')}
</div>
</div>
<div className="widget-content-right position-relative ml-0 ml-5">
{/* {updatingPrice && (
<div className="widget-numbers position-absolute text-secondary px-3">
<FontAwesomeIcon icon={faSpinner} pulse />
</div>
)} */}
<div
className={`widget-numbers text-warning ${
updatingPrice ? 'opacity-3' : ''
}`}
>
${totalCost}
</div>
</div>
</div>
</div>
</Col>
</Row>
<hr />
{restrictions.isPlanCancelled || restrictions.isPlanDowngrade ? (
<p className="text-danger mb-3">
{t('plans.updatePlan.cancelledWarning', {
text: restrictions.isPlanCancelled
? 'cancelled'
: 'downgraded'
})}{' '}
{restrictions.subStartDate && restrictions.subEndDate
? `(${convertUTCtoLocal(
restrictions.subStartDate,
'MMM D, YYYY'
)} - ${convertUTCtoLocal(
restrictions.subEndDate,
'MMM D, YYYY'
)})`
: ''}
</p>
) : (
''
)}
<div className="text-right">
<Button
type="button"
disabled={updatingPrice || loading || disableUpdate}
onClick={handleSubmit}
className="btn-wide"
color="primary"
size="lg"
>
{loading
? t('plans.updatePlan.continueBtnLoading')
: t('plans.updatePlan.continueBtn')}
</Button>
</div>
</Form>
</CardBody>
) : (
<CardBody>
<CardTitle>{t('plans.updatePlan.billingHeading')}</CardTitle>
<BillingDetailsForm
form={paymentForm}
errors={paymentFormErrors}
handleChange={handlePaymentForm}
handleValidation={handlePaymentValidation}
/>
<Row className="divider" />
{paymentError && (
<Alert color="danger">
<Fragment>
<p className="font-size-xs font-weight-bold text-uppercase">
{t('plans.updatePlan.error')}
</p>
{paymentError.message}
</Fragment>
</Alert>
)}
<div className="d-flex justify-content-between flex-column-reverse flex-sm-row">
<Button
type="button"
color="secondary"
size="lg"
disabled={paymentLoading}
onClick={moveBack}
>
{t('plans.updatePlan.back')}
</Button>
<Button
type="button"
color="primary"
onClick={submitPayment}
disabled={!stripe || !elements || paymentLoading}
className="btn-wide btn-hover-shine mb-2 mb-sm-0"
size="lg"
>
{paymentLoading
? t('plans.updatePlan.payLoading')
: t('plans.updatePlan.payBtn', { totalCost })}
</Button>
</div>
</CardBody>
)}
</Card>
<Modal isOpen={modal} toggle={toggle} backdrop="static">
<ModalHeader toggle={toggle}>
{t('plans.updatePlan.confirmationHeading')}
</ModalHeader>
<ModalBody>
<div>
{restrictions.plans && restrictions.plans.price > 0 ? (
restrictions.plans.price === totalCost ? null : restrictions.plans
.price < totalCost ? (
<p className="text-muted mb-3">
{t('plans.updatePlan.upgradeNotice')}
</p>
) : (
<p className="text-muted mb-3">
{t('plans.updatePlan.downgradeNotice')}
</p>
)
) : null}
<p>{t('plans.updatePlan.alreadyStoredCard')}</p>
</div>
</ModalBody>
<ModalFooter>
<Button color="link" onClick={proceedToDetails} disabled={loading}>
{t('plans.updatePlan.payWithOtherCardBtn')}
</Button>
<Button color="primary" disabled={loading} onClick={proceedPayment}>
{loading
? t('plans.updatePlan.payLoading')
: t('plans.updatePlan.payWithStoredCardBtn')}
</Button>
</ModalFooter>
</Modal>
</Col>
);
}
UpdatePlan.propTypes = {
t: PropTypes.func.isRequired,
actions: PropTypes.object,
restrictions: PropTypes.object
};
export default reduxConnect('restrictions', [
'common',
'auth',
'user',
'restrictions'
])(translate(['tabsContent'], { wait: true })(UpdatePlan));
@@ -0,0 +1,72 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { Button, Modal, ModalBody, ModalHeader } from 'reactstrap';
import { planRoutes } from './UserPlans';
import { Trans, translate } from 'react-i18next';
function UpgradePlanModal({ isModalOpen = false, toggle, t }) {
function toggleModal() {
return toggle();
}
return (
<Modal
isOpen={isModalOpen}
toggle={toggleModal}
modalClassName="zoom-modal"
backdrop="static"
>
<ModalHeader toggle={toggleModal} />
<ModalBody className="px-4 px-sm-5 pb-5">
<div className="text-center">
<div className="display-4 mb-2">
<i className="lnr-rocket text-primary"></i>
</div>
<h3 className="mb-3">{t('plans.upgradeModal.heading')}</h3>
<div className="mb-4">
<p className="text-muted">
<Trans i18nKey="plans.upgradeModal.text">
You have to upgrade your plan to get access of these features.
Take a look at our bite-sized
<strong>à la carte menu options</strong> with monthly billing.
</Trans>{' '}
<a
href="https://www.socialhose.io/en/pricing"
rel="noopener noreferrer"
target="_blank"
>
{t('plans.upgradeModal.learnMore')}
</a>
</p>
</div>
<div>
<Button
tag={Link}
to={`/app/plans/${planRoutes.update}`}
onClick={toggleModal}
className="btn-pill btn-wide d-block mx-auto"
color="success"
size="lg"
>
{t('plans.upgradeModal.upgradeNowBtn')}
</Button>
<Button color="link" size="sm" onClick={toggleModal}>
{t('plans.upgradeModal.maybeLaterBtn')}
</Button>
</div>
</div>
</ModalBody>
</Modal>
);
}
UpgradePlanModal.propTypes = {
isModalOpen: PropTypes.bool,
t: PropTypes.func.isRequired,
toggle: PropTypes.func
};
export default React.memo(
translate(['tabsContent'], { wait: true })(UpgradePlanModal)
);
@@ -0,0 +1,128 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import {
NavLink,
Redirect,
Route,
Switch,
useRouteMatch
} from 'react-router-dom';
import reduxConnect from '../../../../redux/utils/connect';
import ChangeCard from './ChangeCard';
import CurrentPlan from './CurrentPlan';
import UpdatePlan from './UpdatePlan';
import UserTransactions from './UserTransactions';
import { Card, CardBody, Col, Row } from 'reactstrap';
import { translate } from 'react-i18next';
export const planRoutes = {
current: 'current',
changeCard: 'change-card',
txn: 'transactions',
update: 'update'
};
function UserPlans({ actions, restrictions, t }) {
const match = useRouteMatch();
useEffect(() => {
const { setEnableClosedSidebar } = actions;
actions.getRestrictions();
setEnableClosedSidebar(true);
return () => setEnableClosedSidebar(false);
}, []);
return (
<Row>
<Col xs={12} lg={4} xl={3}>
<Card className="mb-3">
<CardBody className="navigation-vertical">
<ul className="navigation-ul">
<li className="navigation-item">
<NavLink
className="navigation-link"
activeClassName="active"
to={`${match.url}/${planRoutes.current}`}
>
<em>
<i className="font-size-lg lnr-file-empty"> </i>
</em>
<span>{t('plans.sidebar.activePlanDetails')}</span>
</NavLink>
</li>
{restrictions.isPaymentId && (
<li>
<NavLink
className="navigation-link"
activeClassName="active"
to={`${match.url}/${planRoutes.changeCard}`}
>
<em>
<i className="font-size-lg lnr-license"> </i>
</em>
<span>{t('plans.sidebar.changeCard')}</span>
</NavLink>
</li>
)}
<li>
<NavLink
className="navigation-link"
activeClassName="active"
to={`${match.url}/${planRoutes.update}`}
>
<em>
<i className="font-size-lg lnr-arrow-up-circle"> </i>
</em>
<span>{t('plans.sidebar.updatePlan')}</span>
</NavLink>
</li>
<li>
<NavLink
className="navigation-link"
activeClassName="active"
to={`${match.url}/${planRoutes.txn}`}
>
<em>
<i className="font-size-lg lnr-list"> </i>
</em>
<span>{t('plans.sidebar.yourTransactions')}</span>
</NavLink>
</li>
</ul>
</CardBody>
</Card>
</Col>
<Switch>
<Route path={`${match.url}/${planRoutes.current}`}>
<CurrentPlan />
</Route>
{restrictions.isPaymentId && (
<Route path={`${match.url}/${planRoutes.changeCard}`}>
<ChangeCard />
</Route>
)}
<Route path={`${match.url}/${planRoutes.txn}`}>
<UserTransactions />
</Route>
<Route path={`${match.url}/${planRoutes.update}`}>
<UpdatePlan />
</Route>
<Redirect to={`${match.url}/current`} />
</Switch>
</Row>
);
}
UserPlans.propTypes = {
t: PropTypes.func.isRequired,
actions: PropTypes.object.isRequired,
restrictions: PropTypes.object.isRequired
};
export default reduxConnect('restrictions', [
'common',
'auth',
'user',
'restrictions'
])(translate(['tabsContent'], { wait: true })(UserPlans));
@@ -0,0 +1,132 @@
/* eslint-disable react/jsx-no-bind */
/* eslint-disable react/prop-types */
import React, { useState, useCallback, useEffect } from 'react';
import PropTypes from 'prop-types';
import { reduxActions } from '../../../../redux/utils/connect';
import {
convertUTCtoLocal,
getQueryParams,
setDocumentData
} from '../../../../common/helper';
import { getTransactions } from '../../../../api/plans/userPlans';
import Table from '../../../common/Table/Table';
import { Button, Col } from 'reactstrap';
import ShowTransactionDetails from './ShowTransactionDetails';
import moment from 'moment';
import { capitalize } from 'lodash';
import { translate } from 'react-i18next';
function UserTransactions(props) {
const [dataSource, setDataSource] = useState({ data: [] });
const [loading, setLoading] = useState(true);
const [selectedData, setSelectedData] = useState(false);
const { actions, t } = props;
useEffect(() => {
setDocumentData('title', 'User Transactions');
return () => setDocumentData('title'); // default
}, []);
const columns = [
{
id: 'activeDate',
Header: t('plans.transactions.activationDate'),
accessor: (d) => d.lines.data[0] && d.lines.data[0].period.start,
Cell: (props) => convertUTCtoLocal(moment.unix(props.value), 'MM/DD/YYYY')
},
{
id: 'expireDate',
Header: t('plans.transactions.expirationDate'),
accessor: (d) => d.lines.data[0] && d.lines.data[0].period.end,
Cell: (props) => convertUTCtoLocal(moment.unix(props.value), 'MM/DD/YYYY')
},
{
id: 'paid_at',
Header: t('plans.transactions.transactionDate'),
accessor: (d) => d.status_transitions.paid_at,
Cell: (props) =>
convertUTCtoLocal(moment.unix(props.value), 'MM/DD/YYYY HH:mm:ss')
},
{
Header: t('plans.transactions.amount'),
accessor: 'amount_paid',
Cell: (props) => (props.value ? `$${props.value / 100}` : '-')
},
{
Header: t('plans.transactions.status'),
accessor: 'status',
Cell: (props) => capitalize(props.value)
},
{
Header: t('plans.transactions.actions'),
accessor: 'id',
Cell: (props) => (
<Button
outline
className="border-0 btn-transition"
color="primary"
size="sm"
onClick={() => setSelectedData(props.original)}
>
{t('plans.transactions.more')}
</Button>
)
}
];
function closeModal() {
setSelectedData(false);
}
const getTransactionList = useCallback((page, pageSize) => {
setLoading(true);
const params = getQueryParams({ page, pageSize });
getTransactions(params).then((res) => {
if (res.error || !res.data || !res.data.success || !res.data.data) {
setDataSource({ data: [] }); // comment this line when API is ready
setLoading(false);
return actions.addAlert({
type: 'error',
transKey: 'somethingWrong'
});
}
// setDataSource(sampleData); // comment this line when API is ready
setDataSource({
data:
res.data.data.data && res.data.data.data.length > 0
? res.data.data.data
: []
});
setLoading(false);
});
}, []);
const { data = [], totalCount = 0, limit = 100, page = 1 } = dataSource;
return (
<Col xs="12" lg="8" xl="9">
<Table
cardTitle={t('plans.transactions.heading')}
columns={columns}
data={data}
totalCount={totalCount}
showTotalCount
limit={limit}
page={page}
isLoading={loading}
onFetchData={getTransactionList}
/>
<ShowTransactionDetails data={selectedData} closeModal={closeModal} />
</Col>
);
}
UserTransactions.propTypes = {
t: PropTypes.func.isRequired,
actions: PropTypes.object
};
export default reduxActions()(
translate(['tabsContent'], { wait: true })(UserTransactions)
);
+226
View File
@@ -0,0 +1,226 @@
import React from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import { withRouter } from 'react-router-dom';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { DndProvider } from 'react-dnd';
import { isMobile } from 'react-device-detect';
import { TouchBackend } from 'react-dnd-touch-backend';
import cx from 'classnames';
import echarts from 'echarts';
import ResizeDetector from 'react-resize-detector';
import AppHeader from './AppHeader/AppHeader';
import WebTour from './AppHeader/WebTour';
import Sidebar from './Sidebar/Sidebar';
import reduxConnect from '../../redux/utils/connect';
// import { NOTIFICATION_SUBSCREENS } from '../../redux/modules/appState/share/tabs';
import LoadersAdvanced from '../common/Loader/Loader';
import WesteronTheme from '../common/charts/WesterosTheme.json';
import 'react-dates/initialize';
import 'react-dates/lib/css/_datepicker.css';
import Footer from '../common/Footer';
import { Button, UncontrolledTooltip } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faQuestion } from '@fortawesome/free-solid-svg-icons';
import { find, map } from 'lodash';
import tourPages from './AppHeader/WebTourSteps';
import { allMediaTypes } from '../../redux/modules/appState/searchByFilters';
import { translate } from 'react-i18next';
import i18n from '../../i18n';
import * as timeago from 'timeago.js';
import ar from 'timeago.js/lib/lang/ar';
import fr from 'timeago.js/lib/lang/fr';
// register it languages for time-ago.
timeago.register('ar', ar);
timeago.register('fr', fr);
const DnDBackend = isMobile ? TouchBackend : HTML5Backend;
class App extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
actions: PropTypes.object.isRequired,
children: PropTypes.element,
history: PropTypes.object.isRequired,
location: PropTypes.object.isRequired,
store: PropTypes.object.isRequired
};
state = {
showSidebar: true,
sidebarAnimationDisabled: true,
closedSmallerSidebar: false,
showTourIcon: false
};
componentDidMount() {
echarts.registerTheme('westeros', WesteronTheme);
this.checkIfTourGuide();
const {
common: { auth }
} = this.props.store;
const activeLang = i18n.language.slice(0, 2);
this.props.actions.chooseLanguage(activeLang);
if (
auth &&
auth.user &&
auth.user.restrictions &&
auth.user.restrictions.plans
) {
const planDetails = auth.user.restrictions.plans;
let allowedMediaTypes = allMediaTypes.filter((v) => planDetails[v]);
/*if (auth.user.restrictions.plans.price === 0) {
// TODO: remove following restrictions when duplication fixes
const restrictedTemporary = ['news', 'blogs'];
allowedMediaTypes = allowedMediaTypes.filter(
(v) => !restrictedTemporary.includes(v)
);
} */
this.props.actions.toggleMediaType(allowedMediaTypes, true);
} else {
this.props.actions.toggleMediaType([], true);
}
}
componentDidUpdate(prevProps) {
if (prevProps.location.pathname !== this.props.location.pathname) {
this.checkIfTourGuide();
}
}
checkIfTourGuide = () => {
const tourCurrentPaths = map(tourPages, 'showOn');
const hasTour = tourCurrentPaths.some((path) =>
this.props.location.pathname.startsWith(path)
);
if (hasTour) {
!this.state.showTourIcon && this.setState({ showTourIcon: true });
return;
}
this.state.showTourIcon && this.setState({ showTourIcon: false });
};
showWebTour = () => {
const tourSendPaths = find(tourPages, (o) =>
this.props.location.pathname.startsWith(o.showOn)
);
if (tourSendPaths) {
// Open in a new tab to reset every redux state
const win = window.open(`${tourSendPaths.to}?webtour=true`, '_blank');
win.focus();
}
};
render() {
const { store, actions, children, t } = this.props;
const { common: commonState, appState } = store;
const { sidebar, themeOptions } = appState;
const { base, auth } = commonState;
const {
colorScheme,
enableFixedHeader,
enableFixedSidebar,
enableFixedFooter,
enableClosedSidebar,
closedSmallerSidebar,
enableMobileMenu,
enablePageTabsAlt
} = themeOptions;
if (!auth.token) {
<LoadersAdvanced />;
}
return (
<ResizeDetector
handleWidth
// eslint-disable-next-line react/jsx-no-bind
render={({ width }) => {
return (
<DndProvider backend={DnDBackend}>
<div
className={cx(
'app-container app-theme-' + colorScheme,
{ 'fixed-header': enableFixedHeader },
{ 'fixed-sidebar': enableFixedSidebar || width < 1250 },
{ 'fixed-footer': enableFixedFooter },
{ 'closed-sidebar': enableClosedSidebar || width < 1250 },
{
'closed-sidebar-mobile':
closedSmallerSidebar || width < 1250
},
{ 'sidebar-mobile-open': enableMobileMenu },
{ 'body-tabs-shadow-btn': enablePageTabsAlt }
)}
>
{this.state.showTourIcon && (
<div>
<Button
id="GuidedTour"
className="floating-icon"
color="warning"
onClick={this.showWebTour}
>
<FontAwesomeIcon
icon={faQuestion}
color="#573a04"
fixedWidth={false}
size="2x"
/>
</Button>
<UncontrolledTooltip placement="left" target={'GuidedTour'}>
{t('userSettings.guidedTourTooltip')}
</UncontrolledTooltip>
</div>
)}
<AppHeader
appCommonState={base}
userFirstName={auth.user.firstName}
userLastName={auth.user.lastName}
restrictions={auth.user.restrictions}
userRole={auth.user.role}
actions={actions}
themeOptions={themeOptions}
/>
<div className="app-main">
<Sidebar
t={t}
sidebarState={sidebar}
themeOptions={themeOptions}
actions={actions}
/>
<div className="app-main__outer">
<div className="app-main__inner">
{children}
<Footer />
</div>
</div>
</div>
<WebTour />
</div>
</DndProvider>
);
}}
/>
);
}
}
const applyDecorators = compose(
translate(['common'], { wait: true }),
withRouter,
reduxConnect()
);
export default applyDecorators(App);
@@ -0,0 +1,110 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import HeaderSettings from './HeaderSettings';
import SettingsPopup from './SettingsPopup';
import cx from 'classnames';
import MainTabsLinks from './MainTabsLinks';
import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup';
import HeaderLogo from './HeaderLogo';
import HeaderDots from './HeaderDots';
export class AppHeader extends React.Component {
static propTypes = {
appCommonState: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,
userFirstName: PropTypes.string,
userLastName: PropTypes.string,
userRole: PropTypes.string.isRequired,
restrictions: PropTypes.object.isRequired,
themeOptions: PropTypes.object.isRequired
};
state = {
active: false,
mobile: false,
activeSecondaryMenuMobile: false
};
toggleResponsiveMenu = () => {
this.props.actions.toggleSidebar();
};
activeSearchFunc = () => {
this.setState({ active: !this.state.active });
};
render() {
const {
appCommonState,
restrictions,
actions,
userFirstName,
userLastName,
themeOptions
} = this.props;
const mainTabs = Object.keys(appCommonState.tabs);
const {
headerBackgroundColor,
enableHeaderShadow,
enableMobileMenuSmall
} = themeOptions;
const settingsPopupVisible = appCommonState.isSettingsPopupVisible;
return (
<Fragment>
<CSSTransitionGroup
component="div"
className={cx('app-header', headerBackgroundColor, {
'header-shadow': enableHeaderShadow
})}
transitionName="HeaderAnimation"
transitionAppear
transitionAppearTimeout={1500}
transitionEnter={false}
transitionLeave={false}
>
<HeaderLogo />
<div
className={cx('app-header__content', {
'header-mobile-open': enableMobileMenuSmall
})}
>
<div className="app-header-left" data-tour="app-header-left">
<MainTabsLinks
tabs={appCommonState.tabs}
restrictions={restrictions}
actions={actions}
/>
</div>
<div className="app-header-right">
<HeaderDots
mainTabs={mainTabs}
restrictions={restrictions}
planDetails={restrictions.plans}
/>
<HeaderSettings
isThereSomethingNew={appCommonState.isThereSomethingNew}
langs={appCommonState.langs}
userFirstName={userFirstName}
userLastName={userLastName}
/>
</div>
</div>
{settingsPopupVisible && (
<SettingsPopup
hidePopup={actions.hideUserSettingsPopup}
setErrorMsg={actions.setSettingsPopupError}
changePassword={actions.changeUserPassword}
errorMsg={appCommonState.settingsPopupError}
/>
)}
</CSSTransitionGroup>
</Fragment>
);
}
}
export default AppHeader;
@@ -0,0 +1,99 @@
/* eslint-disable react/jsx-no-bind */
import PropTypes from 'prop-types';
import React, { Fragment } from 'react';
import { Slider } from 'react-burgers';
import cx from 'classnames';
import { faEllipsisV } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Button } from 'reactstrap';
import reduxConnect from '../../../redux/utils/connect';
import translate from 'react-i18next/dist/commonjs/translate';
import { compose } from 'redux';
class AppMobileMenu extends React.Component {
constructor(props) {
super(props);
this.state = {
active: false,
mobile: false,
activeSecondaryMenuMobile: false
};
}
toggleMobileSidebar = () => {
const { setEnableMobileMenu } = this.props.actions;
const { enableMobileMenu } = this.props.appState.themeOptions;
setEnableMobileMenu(!enableMobileMenu);
};
toggleMobileSmall = () => {
const { setEnableMobileMenuSmall } = this.props.actions;
const { enableMobileMenuSmall } = this.props.appState.themeOptions;
setEnableMobileMenuSmall(!enableMobileMenuSmall);
};
state = {
openLeft: false,
openRight: false,
relativeWidth: false,
width: 280,
noTouchOpen: false,
noTouchClose: false
};
changeActive = () => {
this.setState({ active: !this.state.active });
};
render() {
return (
<Fragment>
<div className="app-header__mobile-menu" data-tour="mobile-left-menu">
<div onClick={this.toggleMobileSidebar}>
<Slider
width={26}
lineHeight={2}
lineSpacing={5}
color="#6c757d"
active={this.state.active}
onClick={this.changeActive}
/>
</div>
</div>
<div className="app-header__menu">
<span onClick={this.toggleMobileSmall}>
<Button
size="sm"
className={cx('btn-icon btn-icon-only', {
active: this.state.activeSecondaryMenuMobile
})}
color="primary"
onClick={() =>
this.setState({
activeSecondaryMenuMobile: !this.state
.activeSecondaryMenuMobile
})
}
>
<div className="btn-icon-wrapper">
<FontAwesomeIcon icon={faEllipsisV} />
</div>
</Button>
</span>
</div>
</Fragment>
);
}
}
AppMobileMenu.propTypes = {
actions: PropTypes.object,
appState: PropTypes.object
};
const applyDecorators = compose(
reduxConnect('appState', ['appState']),
translate(['tabsContent'], { wait: true })
);
export default applyDecorators(AppMobileMenu);
@@ -0,0 +1,168 @@
/* eslint-disable no-unused-vars */
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { Link } from 'react-router-dom';
import {
UncontrolledDropdown,
DropdownToggle,
DropdownMenu,
Col,
Row,
Button,
DropdownItem
} from 'reactstrap';
import { IoIosGrid } from 'react-icons/io';
import Notifications from './Notifications';
import LangSettingsMenu from './LangSettingsMenu';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faAngleDown } from '@fortawesome/free-solid-svg-icons';
import { planRoutes } from '../Account/Plans/UserPlans';
import { convertUTCtoLocal } from '../../../common/helper';
class HeaderDots extends React.Component {
static propTypes = {
mainTabs: PropTypes.array.isRequired,
restrictions: PropTypes.object.isRequired,
planDetails: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
};
validateTab(tab) {
if (tab === 'analyze') {
if (!this.props.restrictions) {
return false;
}
const permissions = this.props.restrictions.permissions;
return permissions.analytics;
}
return true;
}
render() {
const { t, mainTabs, planDetails, restrictions } = this.props;
const isFreeAccount = planDetails.price === 0;
const isRTL = document.documentElement.dir === 'rtl';
return (
<div className="header-dots">
<UncontrolledDropdown nav inNavbar>
<DropdownToggle nav>
{t('plans.currentPlan')}
<FontAwesomeIcon className="ml-2 opacity-5" icon={faAngleDown} />
</DropdownToggle>
<DropdownMenu
className={`dropdown-menu-rounded rm-pointers${
isRTL ? ' dropdown-menu-left' : ''
}`}
>
<div className="dropdown-menu-header">
<div className="dropdown-menu-header-inner bg-success">
<div className="menu-header-image opacity-1"></div>
<div className="menu-header-content text-left">
<h5 className="menu-header-title font-weight-bold">
{isFreeAccount
? t('plans.freeBasicAccount')
: `$${planDetails.price}`}
</h5>
{!isFreeAccount && (
<p>
{restrictions.subStartDate && restrictions.subEndDate
? `${convertUTCtoLocal(
isRTL
? restrictions.subEndDate
: restrictions.subStartDate,
'MMM D, YYYY'
)} - ${convertUTCtoLocal(
isRTL
? restrictions.subStartDate
: restrictions.subEndDate,
'MMM D, YYYY'
)}`
: t('plans.perMonth')}
</p>
)}
</div>
</div>
</div>
<DropdownItem
tag={Link}
to={`/app/plans/${planRoutes.update}`}
className="font-weight-bold"
>
<i className="dropdown-icon lnr-rocket opacity-8"> </i>
{t('plans.upgradePlan')}
</DropdownItem>
<DropdownItem tag={Link} to={`/app/plans/${planRoutes.txn}`}>
<i className="dropdown-icon lnr-list"> </i>
{t('plans.yourTransactions')}
</DropdownItem>
{!isFreeAccount && (
<DropdownItem
tag={Link}
to={`/app/plans/${planRoutes.changeCard}`}
>
<i className="dropdown-icon lnr-license"> </i>
{t('plans.changeCard')}
</DropdownItem>
)}
</DropdownMenu>
</UncontrolledDropdown>
<Button
tag={Link}
to={`/app/plans/${planRoutes.update}`}
size="sm"
outline
color="success"
className="align-self-center mr-3 d-none d-lg-block"
>
{t('plans.upgradePlan')}
</Button>
<UncontrolledDropdown className="d-block d-lg-none">
<DropdownToggle className="p-0 mr-2" color="link">
<div className="icon-wrapper icon-wrapper-alt rounded-circle">
<div className="icon-wrapper-bg bg-primary" />
<IoIosGrid color="#3f6ad8" fontSize="23px" />
</div>
</DropdownToggle>
<DropdownMenu
className={`rm-pointers${isRTL ? ' dropdown-menu-left' : ''}`}
>
<div className="grid-menu grid-menu-xl grid-menu-3col">
{mainTabs.map((tab, i) => {
if (!this.validateTab(tab)) return null;
return (
<Col md="12" key={`main-tab-link-${i}`}>
<Button
className="btn-icon-vertical btn-square btn-transition"
outline
color="link"
>
<Link to={'/app/' + tab} className="nav-link">
<Row>
<i
className={
i
? 'lnr lnr-exit-up btn-icon-wrapper mr-1'
: 'lnr-magnifier btn-icon-wrapper mr-1'
}
></i>
<p>{t('tabs.' + tab)}</p>
</Row>
</Link>
</Button>
</Col>
);
})}
</div>
</DropdownMenu>
</UncontrolledDropdown>
<LangSettingsMenu />
<Notifications />
</div>
);
}
}
export default translate(['common'], { wait: true })(HeaderDots);
@@ -0,0 +1,74 @@
import PropTypes from 'prop-types'
import React, { Fragment } from 'react'
import { Slider } from 'react-burgers'
import { compose } from 'redux'
import reduxConnect from '../../../redux/utils/connect'
import translate from 'react-i18next/dist/commonjs/translate'
import AppMobileMenu from './AppMobileMenu'
class HeaderLogo extends React.Component {
constructor (props) {
super(props)
this.state = {
active: false,
mobile: false,
activeSecondaryMenuMobile: false
}
}
toggleEnableClosedSidebar = () => {
const { setEnableClosedSidebar } = this.props.actions
const { enableClosedSidebar } = this.props.appState.themeOptions
setEnableClosedSidebar(!enableClosedSidebar)
}
state = {
openLeft: false,
openRight: false,
relativeWidth: false,
width: 280,
noTouchOpen: false,
noTouchClose: false
}
changeActive = () => {
this.setState({ active: !this.state.active })
}
render () {
return (
<Fragment>
<div className="app-header__logo">
<div className="logo-src ml-0 ml-lg-2" />
<div className="header__pane ml-auto">
<div onClick={this.toggleEnableClosedSidebar}>
<Slider
width={26}
lineHeight={2}
lineSpacing={5}
color="#6c757d"
active={this.state.active}
onClick={this.changeActive}
/>
</div>
</div>
</div>
<AppMobileMenu />
</Fragment>
)
}
}
HeaderLogo.propTypes = {
actions: PropTypes.object,
appState: PropTypes.object
}
const applyDecorators = compose(
reduxConnect('appState', ['appState']),
translate(['tabsContent'], { wait: true })
)
export default applyDecorators(HeaderLogo)
@@ -0,0 +1,84 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import UserSettingsMenu from './UserSettingsMenu';
import { DropdownToggle, DropdownMenu, Dropdown } from 'reactstrap';
import { faAngleDown, faUser } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
export class HeaderSettings extends React.Component {
static propTypes = {
userFirstName: PropTypes.string.isRequired,
userLastName: PropTypes.string.isRequired
};
state = {
isOpen: false
};
toggleUserSettingsDrop = () => {
this.setState((prev) => ({ isOpen: !prev.isOpen }));
};
render() {
const { userFirstName, userLastName } = this.props;
const isRTL = document.documentElement.dir === 'rtl';
return (
<Fragment>
<div className="header-btn-lg pr-0">
<div className="widget-content p-0">
<div className="widget-content-wrapper">
<div className="widget-content-left">
<Dropdown
isOpen={this.state.isOpen}
toggle={this.toggleUserSettingsDrop}
>
<DropdownToggle
color="link"
title="User Profile"
className="d-flex align-items-center p-0"
data-tour="app-header-user-settings"
>
<div className="user-profile">
<FontAwesomeIcon
className="user-profile-icon"
icon={faUser}
/>
</div>
{window.outerWidth >= 768 && (
<FontAwesomeIcon
className="ml-2 opacity-8"
icon={faAngleDown}
/>
)}
</DropdownToggle>
<DropdownMenu
className={`rm-pointers dropdown-menu-lg${
isRTL ? ' dropdown-menu-left' : ''
}`}
>
<UserSettingsMenu
toggleMenu={this.toggleUserSettingsDrop}
userFirstName={userFirstName}
userLastName={userLastName}
/>
</DropdownMenu>
</Dropdown>
</div>
<div className="widget-content-left ml-3 header-user-info">
<div className="widget-heading">
{userFirstName + ' ' + userLastName}
</div>
</div>
</div>
</div>
</div>
</Fragment>
);
}
}
export default translate(['common'], { wait: true })(
React.memo(HeaderSettings)
);
@@ -0,0 +1,100 @@
import React from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import { translate } from 'react-i18next';
import i18n from '../../../i18n';
import {
UncontrolledDropdown,
DropdownToggle,
DropdownMenu,
DropdownItem
} from 'reactstrap';
import reduxConnect from '../../../redux/utils/connect';
import Flag from 'react-flagkit';
const langCountry = {
en: 'US',
ar: 'SA',
fr: 'FR'
};
function LangSettingsMenu(props) {
const {
t,
base: { langs, activeLang },
actions,
direction = ''
} = props;
const chooseLang = (e) => {
const newLang = e.target.dataset.lang;
actions.chooseLanguage(newLang);
i18n.changeLanguage(newLang);
};
const isRTL = document.documentElement.dir === 'rtl';
const dropDownProps = {};
if (direction) {
dropDownProps.direction = direction;
}
return (
<UncontrolledDropdown {...dropDownProps}>
<DropdownToggle className="p-0 mr-2" color="link">
<div className="icon-wrapper icon-wrapper-alt rounded-circle">
<div className="icon-wrapper-bg bg-focus" />
<div className="language-icon">
<Flag
className="mr-3 opacity-8"
country={langCountry[activeLang]}
size="40"
/>
</div>
</div>
</DropdownToggle>
<DropdownMenu
className={`rm-pointers${isRTL ? ' dropdown-menu-left' : ''}`}
>
<div className="dropdown-menu-header">
<div className="dropdown-menu-header-inner pt-4 pb-4 bg-focus">
<div className="menu-header-content text-center text-white">
<h6 className="menu-header-subtitle mt-0">
{t('langs.chooseLanguage')}
</h6>
</div>
</div>
</div>
{langs.map((lang, i) => {
const translateTarget = 'langs.' + lang;
return (
<DropdownItem
key={lang}
active={activeLang === lang}
data-lang={lang}
onClick={chooseLang}
>
<Flag className="mr-3 opacity-8" country={langCountry[lang]} />
{t(translateTarget)}
</DropdownItem>
);
})}
</DropdownMenu>
</UncontrolledDropdown>
);
}
LangSettingsMenu.propTypes = {
t: PropTypes.func.isRequired,
base: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,
direction: PropTypes.string
};
const applyDecorators = compose(
reduxConnect('base', ['common', 'base']),
translate(['common'], { wait: true })
);
export default applyDecorators(LangSettingsMenu);
@@ -0,0 +1,80 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { Link, withRouter } from 'react-router-dom';
import { Nav, NavItem } from 'reactstrap';
import cl from 'classnames';
export class MainTabsLinks extends React.Component {
static propTypes = {
tabs: PropTypes.object.isRequired,
restrictions: PropTypes.object.isRequired,
t: PropTypes.func.isRequired,
actions: PropTypes.object.isRequired,
location: PropTypes.object
};
validateTab = (tab) => {
if (tab === 'analyze') {
if (!this.props.restrictions) {
// to prevent: permissions of `undefined`
return false;
}
const permissions = this.props.restrictions.permissions;
return permissions.analytics;
}
return true;
};
showUpgradeModal = (e) => {
e.preventDefault();
this.props.actions.toggleUpgradeModal();
};
render() {
const { t, tabs, location } = this.props;
return (
<Nav className="header-megamenu">
{Object.keys(tabs).map((tab, i) => {
const firstSubTab =
tabs[tab].items && tabs[tab].items[0] ? tabs[tab].items[0].url : '';
if (!this.validateTab(tab)) {
return (
<NavItem key={tab}>
<a
href="#"
onClick={this.showUpgradeModal}
className={cl('nav-link', {
active: location.pathname.startsWith(`/app/${tab}`)
})}
>
<i className={`nav-link-icon ${tabs[tab].icon}`}> </i>
<p>{t('tabs.' + tab)}</p>
</a>
</NavItem>
);
}
return (
<NavItem key={tab}>
<Link
to={`/app/${tab}/${firstSubTab}`}
className={cl('nav-link', {
active: location.pathname.startsWith(`/app/${tab}`)
})}
>
<i className={`nav-link-icon ${tabs[tab].icon}`}> </i>
<p>{t('tabs.' + tab)}</p>
</Link>
</NavItem>
);
})}
</Nav>
);
}
}
export default translate(['common'], { wait: true })(
withRouter(React.memo(MainTabsLinks))
);
@@ -0,0 +1,160 @@
import React, { Fragment, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import city3 from '../../../styles/utils/images/dropdown-header/city3.jpg';
import PerfectScrollbar from 'react-perfect-scrollbar';
import reduxConnect from '../../../redux/utils/connect';
import cl from 'classnames';
import {
Alert,
Button,
DropdownMenu,
DropdownToggle,
Nav,
NavItem,
UncontrolledDropdown
} from 'reactstrap';
import { Interpolate, translate } from 'react-i18next';
import { compose } from 'redux';
import { IoIosNotificationsOutline } from 'react-icons/io';
function Notifications({ alerts, t, actions }) {
const [alertsList, setAlertsList] = useState([]);
useEffect(() => {
// Empty list when mounts
actions.removeAllAlerts();
}, []);
useEffect(() => {
const newAlerts = alerts
.reverse()
.map((alert) => {
return typeof alert === 'string' ? { message: alert } : alert;
})
.map((alert) => {
const interpolateParameters = alert ? alert.parameters : {};
const i18nKey = alert && `alerts.${alert.type}.${alert.transKey}`;
let type, msg;
type = alert.type ? oldValueMapping[alert.type] : 'warning';
msg = t(i18nKey, {
...interpolateParameters,
defaultValue: alert.message || t('error.unknown')
});
return { type, msg };
});
setAlertsList(newAlerts);
}, [alerts.length]);
const isRTL = document.documentElement.dir === 'rtl';
return (
<UncontrolledDropdown>
<DropdownToggle className="p-0 mr-2" color="link">
<div className="icon-wrapper icon-wrapper-alt rounded-circle">
<div className="icon-wrapper-bg bg-danger" />
<IoIosNotificationsOutline color="#d92550" fontSize="23px" />
<div className="badge badge-dot badge-dot-sm badge-danger">
{alertsList.length > 0 ? t('userSettings.notifications') : ''}
</div>
</div>
</DropdownToggle>
<DropdownMenu
className={cl('dropdown-menu-xl rm-pointers', {
'py-0': alertsList.length < 1,
'dropdown-menu-left': isRTL
})}
>
<div className="dropdown-menu-header mb-0">
<div className="dropdown-menu-header-inner bg-deep-blue">
<div
className="menu-header-image opacity-1"
style={{
backgroundImage: 'url(' + city3 + ')'
}}
/>
<div className="menu-header-content text-dark">
<h5 className="menu-header-title">
{t('userSettings.notifications')}
</h5>
<h6 className="menu-header-subtitle">
<Interpolate
i18nKey={
alertsList.length > 1
? 'userSettings.notificationsSub_plural'
: 'userSettings.notificationsSub'
}
alertLength={alertsList.length}
/>
</h6>
</div>
</div>
</div>
{alertsList.length > 0 && (
<Fragment>
<div className="scroll-area-md">
<PerfectScrollbar>
<div className="p-2">
{alertsList.map((item, i) => (
<Alert
key={i}
className="mb-2"
style={{ wordBreak: 'break-word' }}
color={colorsMapping[item.type]}
>
<p className="font-size-xs font-weight-bold text-uppercase">
{item.type}
</p>
{item.msg}
</Alert>
))}
</div>
</PerfectScrollbar>
</div>
<Nav vertical>
<NavItem className="nav-item-divider" />
<NavItem className="nav-item-btn text-center">
<Button
size="sm"
className="btn-shadow btn-wide btn-pill"
color="focus"
onClick={actions.removeAllAlerts}
>
{t('userSettings.clearAll')}
</Button>
</NavItem>
</Nav>
</Fragment>
)}
</DropdownMenu>
</UncontrolledDropdown>
);
}
const oldValueMapping = {
notice: 'success',
warning: 'warning',
error: 'error'
};
const colorsMapping = {
success: 'success',
warning: 'warning',
error: 'danger'
};
Notifications.propTypes = {
t: PropTypes.func.isRequired,
alerts: PropTypes.array.isRequired,
actions: PropTypes.object.isRequired
};
const applyDecorators = compose(
reduxConnect('alerts', ['common', 'alerts']),
translate(['common'], { wait: true })
);
export default applyDecorators(Notifications);
@@ -0,0 +1,39 @@
import React, { Fragment } from 'react'
import cx from 'classnames'
class SearchBox extends React.Component {
constructor (props) {
super(props)
this.state = {
activeSearch: false
}
}
activeSearchFunc = () => {
this.setState({ activeSearch: !this.state.activeSearch })
}
render () {
return (
<Fragment>
<div className={cx('search-wrapper', {
active: this.state.activeSearch
})}>
<div className="input-holder">
<input type="text" className="search-input" placeholder="Type to search" />
<button onClick={this.activeSearchFunc}
className="search-icon">
<span />
</button>
</div>
<button onClick={this.activeSearchFunc}
className="close" />
</div>
</Fragment>
)
}
}
export default SearchBox
@@ -0,0 +1,122 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import {
Button,
Label,
Input,
FormGroup,
Modal,
ModalHeader,
ModalBody,
ModalFooter
} from 'reactstrap';
export class SettingsPopup extends React.Component {
static propTypes = {
hidePopup: PropTypes.func.isRequired,
setErrorMsg: PropTypes.func.isRequired,
changePassword: PropTypes.func.isRequired,
errorMsg: PropTypes.string,
t: PropTypes.func.isRequired
};
constructor() {
super();
this.state = {
oldPassword: '',
newPassword: '',
confirmPassword: ''
};
}
hidePopup = () => {
this.props.hidePopup();
this.props.setErrorMsg(null);
};
onSubmit = () => {
const { t } = this.props;
const { oldPassword, newPassword, confirmPassword } = this.state;
// need more validations
if (!oldPassword || !newPassword || !confirmPassword) {
return this.props.setErrorMsg(t('userSettings.enterRequiredFields'));
}
if (newPassword !== confirmPassword) {
return this.props.setErrorMsg(t('userSettings.passwordsNotMatched'));
}
if (oldPassword && newPassword) {
this.props.changePassword(newPassword, oldPassword);
}
};
handleChange = (e) => {
const { name, value } = e.target;
this.setState({ [name]: value });
};
render() {
const { t, errorMsg } = this.props;
return (
<Modal isOpen toggle={this.hidePopup} backdrop="static">
<ModalHeader toggle={this.hidePopup}>
{t('userSettings.changePassword')}
</ModalHeader>
<ModalBody>
<FormGroup>
<Label>
{t('userSettings.enterOldPassword')}
<span className="text-danger">*</span>
</Label>
<Input
type="password"
name="oldPassword"
onChange={this.handleChange}
/>
</FormGroup>
<FormGroup>
<Label>
{t('userSettings.enterNewPassword')}
<span className="text-danger">*</span>
</Label>
<Input
type="password"
name="newPassword"
onChange={this.handleChange}
/>
</FormGroup>
<FormGroup>
<Label>
{t('userSettings.retypeNewPassword')}
<span className="text-danger">*</span>
</Label>
<Input
type="password"
name="confirmPassword"
onChange={this.handleChange}
/>
</FormGroup>
<p className="text-danger">{errorMsg}</p>
</ModalBody>
<ModalFooter>
<Button color="light" onClick={this.hidePopup}>
{t('common:commonWords.Cancel')}
</Button>
<Button color="primary" onClick={this.onSubmit}>
{t('userSettings.changePassword')}
</Button>
</ModalFooter>
</Modal>
);
}
}
export default translate(['tabsContent', 'common'], { wait: true })(
SettingsPopup
);
@@ -0,0 +1,55 @@
import React from 'react';
import PropTypes from 'prop-types';
import { NavLink } from 'react-router-dom';
import { translate } from 'react-i18next';
class SubTabWrapper extends React.Component {
static propTypes = {
activeTabName: PropTypes.string.isRequired,
subTabs: PropTypes.array.isRequired,
t: PropTypes.func.isRequired,
children: PropTypes.object
};
render() {
const { t, activeTabName, subTabs, children } = this.props;
return (
<div className="rc-tabs-top position-relative" key="sub-tab-wrapper">
<div role="tablist" className="rc-tabs-bar" tabIndex="0">
<div className="rc-tabs-nav-container">
<div className="rc-tabs-nav-wrap mask-line pt-0">
<div className="rc-tabs-nav-scroll">
<div className="rc-tabs-nav rc-tabs-nav-animated">
{subTabs &&
subTabs.map((subTab) => {
const tabText =
activeTabName === 'dashboard'
? subTab.title
: t('tabs.' + subTab.title);
const fullUrl =
'/app/' + activeTabName + '/' + subTab.url;
return (
<NavLink
to={fullUrl}
key={subTab.url}
activeClassName="rc-tabs-tab-active rc-tabs-ink-bar rc-tabs-ink-bar-animated"
className="rc-tabs-tab"
>
{tabText}
</NavLink>
);
})}
</div>
</div>
</div>
</div>
</div>
{children}
</div>
);
}
}
export default translate(['common'], { wait: true })(SubTabWrapper);
@@ -0,0 +1,165 @@
/* eslint-disable react/jsx-no-bind */
import React from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import { translate } from 'react-i18next';
import { Nav, Button, NavItem, NavLink } from 'reactstrap';
import PerfectScrollbar from 'react-perfect-scrollbar';
import city from '../../../images/city3.jpg';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faUser } from '@fortawesome/free-solid-svg-icons';
import { reduxActions } from '../../../redux/utils/connect';
import tourPages from './WebTourSteps';
import { useHistory } from 'react-router';
import { planRoutes } from '../Account/Plans/UserPlans';
function UserSettingsMenu(props) {
const { push } = useHistory();
function hideMenu() {
props.toggleMenu();
props.actions.setEnableMobileMenuSmall(false);
}
function showUserSettings() {
hideMenu();
props.actions.showUserSettingsPopup();
}
function onLogout() {
hideMenu();
props.actions.logout();
}
function tourGuide(path) {
const win = window.open(`${path}?webtour=true`, '_blank');
win.focus();
// props.actions.toggleWebTour(); for dev
}
function gotToActivePlan() {
hideMenu();
push(`/app/plans/${planRoutes.current}`);
}
const { t } = props;
return (
<React.Fragment>
<div className="dropdown-menu-header">
<div className="dropdown-menu-header-inner bg-info">
<div
className="menu-header-image opacity-2"
style={{
backgroundImage: 'url(' + city + ')'
}}
/>
<div className="menu-header-content text-left">
<div className="widget-content p-0">
<div className="widget-content-wrapper">
<div className="widget-content-left mr-3">
<div className="user-profile">
<FontAwesomeIcon
className="user-profile-icon"
icon={faUser}
/>
</div>
</div>
<div className="widget-content-left">
<div className="widget-heading">
{props.userFirstName + ' ' + props.userLastName}{' '}
</div>
</div>
<div className="widget-content-right ml-auto mr-2">
<Button
className="btn-pill btn-shadow btn-shine"
color="focus"
onClick={onLogout}
>
{t('userSettings.signOut')}
</Button>
</div>
</div>
</div>
</div>
</div>
</div>
{/* <div className="scroll-area-xs"> */}
<div>
<PerfectScrollbar>
<Nav vertical>
<NavItem>
<NavLink
tag={Button}
type="button"
color="link"
className="font-size-md w-100"
onClick={gotToActivePlan}
>
{t('plans.activePlanDetails')}
</NavLink>
</NavItem>
<NavItem>
<NavLink
tag={Button}
type="button"
color="link"
className="font-size-md w-100"
onClick={showUserSettings}
>
{t('userSettings.changePassword')}
</NavLink>
</NavItem>
<NavItem>
<NavLink
className="font-size-md w-100"
href="https://www.socialhose.io/en/user-guide"
rel="noopener noreferrer"
target="_blank"
>
{t('userSettings.userGuide')}
</NavLink>
</NavItem>
<NavItem style={{ textAlign: 'start' }}>
<div className="mt-2 mb-3 mx-3 px-1">
<p className="text-muted font-size-md mb-2">{t('userSettings.guidedTourTooltip')}</p>
<div className="d-flex flex-row flex-wrap pl-3">
{tourPages.map((tour) => (
<Button
key={tour.name}
className="btn-icon-vertical btn-transition btn-transition-alt pt-2 pb-2 mr-2"
outline
color="primary"
onClick={() => tourGuide(tour.to)}
>
<i className={`${tour.icon} btn-icon-wrapper mb-2`} />
{t(`userSettings.${tour.translateKey}`)}
</Button>
))}
</div>
</div>
</NavItem>
</Nav>
</PerfectScrollbar>
</div>
</React.Fragment>
);
}
UserSettingsMenu.propTypes = {
toggleMenu: PropTypes.func.isRequired,
userFirstName: PropTypes.string.isRequired,
userLastName: PropTypes.string.isRequired,
actions: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
};
const applyDecorators = compose(
reduxActions(),
translate(['common'], { wait: true })
);
export default React.memo(applyDecorators(UserSettingsMenu));
@@ -0,0 +1,120 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { find } from 'lodash';
import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock';
import Tour from 'reactour';
import { useHistory, useLocation } from 'react-router';
import reduxConnect from '../../../redux/utils/connect';
import tourPages from './WebTourSteps';
function WebTour({
actions,
store: {
common: { base },
appState: { themeOptions }
}
}) {
const [hasSidebar, setHasSidebar] = useState(window.innerWidth > 991);
const [tourData, setTourData] = useState({ content: [] });
const location = useLocation();
const { replace } = useHistory();
const { isTourOpen = false } = base;
const params = new URLSearchParams(location.search);
const webtour = params.get('webtour');
useEffect(() => {
if (webtour) {
const tour = find(tourPages, {
to: location.pathname
});
if (tour) {
setTourData(tour);
window.gtag && window.gtag('event', 'tutorial_begin', {
name: tour.name
});
actions.toggleWebTour(); // open tour if param is available
}
} else {
actions.toggleWebTour(); // close if param is removed
}
}, [webtour]);
useEffect(() => {
if (isTourOpen) {
if (window.innerWidth > 991) {
!hasSidebar && setHasSidebar(true);
} else {
hasSidebar && setHasSidebar(false);
}
}
}, [window.innerWidth]);
const accentColor = '#0094bd';
function closeWebTour() {
const queryParams = new URLSearchParams(location.search);
if (queryParams.has('webtour')) {
queryParams.delete('webtour');
replace({
search: queryParams.toString()
});
}
}
function getCurrentStep(step) {
const stepState = tourData.content;
const stepDetails = stepState.find((v, i) => i === step);
if (step === stepState.length - 1) {
window.gtag && window.gtag('event', 'tutorial_complete', {
name: tourData.name
});
}
if (!hasSidebar) {
if (stepDetails.needSidebar) {
!themeOptions.enableMobileMenu && actions.setEnableMobileMenu(true);
} else {
themeOptions.enableMobileMenu && actions.setEnableMobileMenu(false);
}
}
}
function disableBody(target) {
disableBodyScroll(target);
}
function enableBody(target) {
enableBodyScroll(target);
}
return (
<Tour
onRequestClose={closeWebTour}
steps={tourData.content}
getCurrentStep={getCurrentStep}
isOpen={isTourOpen && tourData.content && tourData.content.length > 0}
maskClassName="mask"
className="helper"
rounded={5}
startAt={0}
closeWithMask={false}
accentColor={accentColor}
onAfterOpen={disableBody}
onBeforeClose={enableBody}
disableFocusLock
lastStepNextButton={<div className="btn btn-primary">Finish</div>}
/>
);
}
WebTour.propTypes = {
actions: PropTypes.object,
store: PropTypes.object
};
export default reduxConnect()(WebTour);
@@ -0,0 +1,172 @@
import React from 'react';
import i18n from '../../../i18n';
import { Trans } from 'react-i18next';
const baseKey = 'tabsContent:webtour';
const steps = [
{
selector: '',
content: i18n.t(`${baseKey}.search.start`)
},
{
selector: '[data-tour="left-panel"]',
content: i18n.t(`${baseKey}.search.feedsView`),
resizeObservables: ['[data-tour="left-panel"]'],
needSidebar: true,
stepInteraction: false
},
{
selector: '[data-tour="app-header-left"]',
content: () => (
<Trans i18nKey={`${baseKey}.search.mainTabs`}>
There are 3 main pages: <strong>Search</strong> to find content,
<strong>Analyze</strong> to generate reports, and <strong>Share</strong>
to distribute findings via alerts or webfeeds.
</Trans>
),
onlyWeb: true,
stepInteraction: false
},
{
selector: '[data-tour="app-header-user-settings"]',
content: i18n.t(`${baseKey}.search.userSettings`),
stepInteraction: false
},
{
selector: '[data-tour="search-licenses"]',
content: i18n.t(`${baseKey}.search.license`),
stepInteraction: false
},
{
selector: '[data-tour="input-field-search"]',
content: () => (
<p>
<Trans i18nKey={`${baseKey}.search.searchField`}>
A simple boolean search looks like this:
<strong>BMW AND Texas</strong>. Which will find all mentions of bmw
and "texas”.
</Trans>
</p>
)
},
{
selector: '[data-tour="select-date-range"]',
content: i18n.t(`${baseKey}.search.dateRange`),
stepInteraction: false
},
{
selector: '[data-tour="select-media-types"]',
content: i18n.t(`${baseKey}.search.mediaChannels`)
},
{
selector: '[data-tour="advanced-search"]',
content: () => (
<Trans i18nKey={`${baseKey}.search.advancedSearch`}>
Click on <strong>Advanced Search</strong> to uncover the different
options for your search.
</Trans>
),
resizeObservables: ['[data-tour="advanced-search"]']
},
{
selector: '[data-tour="advanced-search"]',
content: () => (
<Trans i18nKey={`${baseKey}.search.emphasis`}>
<strong>Emphasis:</strong> Include or exclude specific words or phrases
in the headline of a news article or a blog post.
</Trans>
),
resizeObservables: ['[data-tour="advanced-search-content"]']
},
{
selector: '[data-tour="advanced-search"]',
content: () => (
<Trans i18nKey={`${baseKey}.search.languages`}>
<strong>Languages:</strong> Capture the content that is tagged with the
following language(s).
</Trans>
),
resizeObservables: ['[data-tour="advanced-search-content"]']
},
{
selector: '[data-tour="advanced-search"]',
content: () => (
<Trans i18nKey={`${baseKey}.search.locations`}>
<strong>Locations:</strong> Include or exclude content that is geotagged
with the following countries or US States.
</Trans>
),
resizeObservables: ['[data-tour="advanced-search-content"]']
},
{
selector: '[data-tour="advanced-search"]',
content: () => (
<Trans i18nKey={`${baseKey}.search.extras`}>
<strong>Extras:</strong> Only show posts with images.
</Trans>
),
resizeObservables: ['[data-tour="advanced-search-content"]']
},
/* {
selector: '[data-tour="search-button"]',
content: () => (
<Fragment>
Click <strong>Search icon</strong>.
</Fragment>
)
}, */
{
selector: '[data-tour="search-buttons"]',
content: i18n.t(`${baseKey}.search.saveSearch`),
stepInteraction: false
}
];
const analyticsSteps = [
{
selector: '',
content: i18n.t(`${baseKey}.analytics.start`)
},
{
selector: '[data-tour="left-panel"]',
content: i18n.t(`${baseKey}.analytics.dragFeed`),
resizeObservables: ['[data-tour="left-panel"]'],
needSidebar: true
},
{
selector: '[data-tour="drop-feeds-box"]',
highlightedSelectors: ['[data-tour="left-panel"]'],
content: i18n.t(`${baseKey}.analytics.drop`)
},
{
selector: '[data-tour="analytics-data-range"]',
content: i18n.t(`${baseKey}.analytics.dateRange`),
observe: '.DateRangePickerInput'
},
{
selector: '[data-tour="create-analytics-button"]',
content: i18n.t(`${baseKey}.analytics.create`)
}
];
const tourPages = [
{
translateKey: 'HowToSearch',
name: 'How to Search',
icon: 'pe-7s-search',
to: '/app/search/search',
showOn: '/app/search/search',
content: steps
},
{
translateKey: 'HowToAnalyze',
name: 'How to Analyze',
icon: 'pe-7s-graph1',
to: '/app/analyze/create',
showOn: '/app/analyze',
content: analyticsSteps
}
];
export default tourPages;
@@ -0,0 +1,67 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import {
Button,
Modal,
ModalHeader,
ModalBody,
Label,
Input,
ModalFooter
} from 'reactstrap';
export class AddCategoryPopup extends React.Component {
state = {
folderName: ''
};
static propTypes = {
parentId: PropTypes.number.isRequired,
hideAddCategoryPopup: PropTypes.func.isRequired,
addCategory: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
onChangeName = (e) => {
const { value } = e.target; // need validation
this.setState({ folderName: value });
};
hidePopup = () => {
this.props.hideAddCategoryPopup();
};
onSubmit = () => {
const { folderName } = this.state;
this.props.addCategory(folderName, this.props.parentId);
this.props.hideAddCategoryPopup();
};
render() {
const { t } = this.props;
const { folderName } = this.state;
return (
<Modal isOpen toggle={this.hidePopup} backdrop="static">
<ModalHeader toggle={this.hidePopup}>
{t('sidebarPopup.addFolderBtn')}
</ModalHeader>
<ModalBody>
<Label>{t('sidebarPopup.enterFolderName')}</Label>
<Input type="text" value={folderName} onChange={this.onChangeName} />
</ModalBody>
<ModalFooter>
<Button color="light" onClick={this.hidePopup}>
{t('commonWords.Cancel')}
</Button>
<Button color="primary" onClick={this.onSubmit}>
{t('sidebarPopup.addFolderBtn')}
</Button>
</ModalFooter>
</Modal>
);
}
}
export default translate(['common'], { wait: true })(AddCategoryPopup);
@@ -0,0 +1,121 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import Select from 'react-select';
import {
Button,
Modal,
ModalHeader,
ModalBody,
FormGroup,
Label,
Input,
ModalFooter
} from 'reactstrap';
export class AddClippingsFeedPopup extends React.Component {
static propTypes = {
parentId: PropTypes.number.isRequired,
hidePopup: PropTypes.func.isRequired,
addClippingsFeed: PropTypes.func.isRequired,
addAlert: PropTypes.func.isRequired,
categories: PropTypes.array.isRequired,
t: PropTypes.func.isRequired
};
constructor(props) {
super(props);
this.state = {
parentId: props.parentId,
feedName: ''
};
}
onChangeName = (e) => {
const { value } = e.target;
this.setState({ feedName: value });
};
hidePopup = () => {
this.props.hidePopup();
};
onSubmit = () => {
const { parentId } = this.state;
const { addAlert, addClippingsFeed, hidePopup } = this.props;
const { feedName } = this.state;
if (feedName) {
addClippingsFeed(feedName, parentId);
hidePopup();
} else {
addAlert({
type: 'error',
transKey: 'feedNameEmpty'
});
}
};
flattenCategories = (categories, level = '') => {
return categories.reduce((result, category) => {
result.push({
label:
level +
this.props.t(`sidebar.${category.name}`, {
defaultValue: category.name
}),
value: category.id
});
if (category.childes && category.childes.length) {
return result.concat(
this.flattenCategories(category.childes, '- ' + level)
);
}
return result;
}, []);
};
onParentCategorySelect = (value) => {
this.setState({ parentId: value });
};
render() {
const { t, categories } = this.props;
const { parentId, feedName } = this.state;
const options = this.flattenCategories(categories);
return (
<Modal isOpen toggle={this.hidePopup} backdrop="static">
<ModalHeader toggle={this.hidePopup}>
{t('sidebarPopup.addClippingsFeed')}
</ModalHeader>
<ModalBody>
<FormGroup>
<Label>{t('sidebarPopup.feedName')}</Label>
<Input type="text" value={feedName} onChange={this.onChangeName} />
</FormGroup>
<FormGroup>
<Label>{t('sidebarPopup.folder')}</Label>
<Select
onChange={this.onParentCategorySelect}
options={options}
value={parentId}
editable={false}
clearable={false}
simpleValue
/>
</FormGroup>
</ModalBody>
<ModalFooter>
<Button color="light" onClick={this.hidePopup}>
{t('commonWords.Cancel')}
</Button>
<Button color="primary" onClick={this.onSubmit}>
{t('sidebarPopup.addClippingsFeed')}
</Button>
</ModalFooter>
</Modal>
);
}
}
export default translate(['common'], { wait: true })(AddClippingsFeedPopup);
@@ -0,0 +1,58 @@
import React from 'react'
import PropTypes from 'prop-types'
import Category from './Category'
export class Categories extends React.Component {
static propTypes = {
actions: PropTypes.object.isRequired,
areCategoriesLoaded: PropTypes.bool.isRequired,
areFeedsFiltered: PropTypes.bool.isRequired,
categories: PropTypes.array.isRequired,
filteredCategories: PropTypes.array.isRequired
};
hideParentCategoryDrop = () => {}; //empty func for first level categories
render () {
const { areCategoriesLoaded, areFeedsFiltered, actions } = this.props
const {
showDeletePopup, showRenamePopup, showAddCategoryPopup,
showAddClippingsFeedPopup, getFeedResults,
moveCategory, moveFeed, toggleExportFeed,
toggleExportCategory, clipArticles
} = actions
const categories = areFeedsFiltered ? this.props.filteredCategories : this.props.categories
return (
<div className='sidebar-categories'>
{areCategoriesLoaded &&
categories.map((category, i) => {
return (
<Category
hideParentCategoryDrop={this.hideParentCategoryDrop} //set empty func
parentId={-1} //set empty parent category for first level categories
category={category}
categories={categories}
showDeletePopup={showDeletePopup}
showRenamePopup={showRenamePopup}
showAddCategoryPopup={showAddCategoryPopup}
showAddClippingsFeedPopup={showAddClippingsFeedPopup}
getFeedResults={getFeedResults}
moveCategory={moveCategory}
moveFeed={moveFeed}
clipArticles={clipArticles}
key={'main-category' + i}
toggleExportFeed={toggleExportFeed}
toggleExportCategory={toggleExportCategory}
/>
)
})
}
</div>
)
}
}
export default Categories
@@ -0,0 +1,219 @@
import React from 'react';
import PropTypes from 'prop-types';
import { DropTarget, DragSource } from 'react-dnd';
import { compose } from 'redux';
import onClickOutside from 'react-onclickoutside';
import Feed from './Feed';
import CategoryHead from './CategoryHead';
import { TYPES } from '../../../redux/modules/appState/sidebar';
import cx from 'classnames';
const folderSource = {
beginDrag(props) {
return {
type: TYPES.FOLDER,
id: props.category.id,
category: props.category
};
},
canDrag(props) {
return props.category.type === 'directory';
}
};
const targetTypes = [TYPES.FEED, TYPES.FOLDER];
const categoryTarget = {
drop(props, monitor) {
if (monitor.didDrop()) return;
const { category, moveCategory, moveFeed } = props;
const item = monitor.getItem();
const draggedCategoryId = item.id;
const newCategoryId = category.id;
if (item.type === TYPES.FOLDER) {
moveCategory(item.category, newCategoryId);
} else if (item.type === TYPES.FEED) {
moveFeed(draggedCategoryId, newCategoryId);
}
},
canDrop(props, monitor) {
const categoryType = props.category.type;
return (
categoryType !== 'deleted_content' && categoryType !== 'shared_content'
);
}
};
export class CategoryClass extends React.Component {
static propTypes = {
parentId: PropTypes.number.isRequired,
category: PropTypes.object.isRequired,
showDeletePopup: PropTypes.func.isRequired,
showRenamePopup: PropTypes.func.isRequired,
showAddCategoryPopup: PropTypes.func.isRequired,
showAddClippingsFeedPopup: PropTypes.func.isRequired,
hideParentCategoryDrop: PropTypes.func.isRequired,
categories: PropTypes.array.isRequired,
connectDropTarget: PropTypes.func.isRequired,
connectDragSource: PropTypes.func.isRequired,
getFeedResults: PropTypes.func.isRequired,
moveFeed: PropTypes.func.isRequired,
moveCategory: PropTypes.func.isRequired,
clipArticles: PropTypes.func.isRequired,
toggleExportFeed: PropTypes.func.isRequired,
toggleExportCategory: PropTypes.func.isRequired
};
constructor(props) {
super(props);
this.state = {
isCategoryActive: true, // sub menus
isCategoryDropActive: false // more options
};
}
// hide category dropdown if there was click outside
handleClickOutside = () => {
this.state.isCategoryDropActive && this.hideCategoryDropdown();
};
toggleCollapse = (e) => {
if (e.target === e.currentTarget) {
this.setState((prev) => ({
isCategoryActive: !prev.isCategoryActive
}));
}
};
toggleCategoryDropdown = (e) => {
e.preventDefault();
// this.props.hideParentCategoryDrop();
this.setState((prev) => ({
isCategoryDropActive: !prev.isCategoryDropActive
}));
};
hideCategoryDropdown = () => {
this.setState({
isCategoryDropActive: false
});
};
render() {
const {
category,
categories,
connectDropTarget,
connectDragSource,
hideParentCategoryDrop,
parentId,
showDeletePopup,
getFeedResults,
showRenamePopup,
showAddCategoryPopup,
moveCategory,
moveFeed,
showAddClippingsFeedPopup,
clipArticles,
toggleExportFeed,
toggleExportCategory
} = this.props;
const isFeeds = category.feeds.length > 0;
const isChildes = category.childes.length > 0;
const categoryType = category.type;
let categoryActiveClass = this.state.isCategoryActive
? ' active-category'
: '';
return connectDragSource(
connectDropTarget(
<li
className={'metismenu-item ' + categoryType + categoryActiveClass}
onClick={hideParentCategoryDrop}
>
<CategoryHead
toggleCollapse={this.toggleCollapse}
toggleCategoryDropdown={this.toggleCategoryDropdown}
isCategoryDropActive={this.state.isCategoryDropActive}
isCategoryActive={this.state.isCategoryActive}
hideDropDown={this.hideCategoryDropdown}
parentId={parentId}
category={category}
showDeletePopup={showDeletePopup}
showRenamePopup={showRenamePopup}
showAddCategoryPopup={showAddCategoryPopup}
toggleExportCategory={toggleExportCategory}
showAddClippingsFeedPopup={showAddClippingsFeedPopup}
categories={categories}
/>
<ul
className={cx('metismenu-container', {
visible: this.state.isCategoryActive
})}
>
{isFeeds &&
category.feeds.map((feed, i) => {
return (
<Feed
key={'feed' + i}
feed={feed}
showDeletePopup={showDeletePopup}
showRenamePopup={showRenamePopup}
categories={categories}
categoryId={category.id}
hideParentCategoryDrop={this.hideCategoryDropdown}
getFeedResults={getFeedResults}
clipArticles={clipArticles}
toggleExportFeed={toggleExportFeed}
/>
);
})}
{isChildes &&
category.childes.map((_category, i) => {
return (
<Category
key={'category' + i}
showDeletePopup={showDeletePopup}
showRenamePopup={showRenamePopup}
showAddCategoryPopup={showAddCategoryPopup}
showAddClippingsFeedPopup={showAddClippingsFeedPopup}
parentId={category.id}
category={_category}
categories={categories}
hideParentCategoryDrop={this.hideCategoryDropdown}
getFeedResults={getFeedResults}
connectDropTarget={connectDropTarget}
connectDragSource={connectDragSource}
moveCategory={moveCategory}
moveFeed={moveFeed}
clipArticles={clipArticles}
toggleExportFeed={toggleExportFeed}
toggleExportCategory={toggleExportCategory}
/>
);
})}
</ul>
</li>
)
);
}
}
export const Category = compose(
DropTarget(targetTypes, categoryTarget, (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
itemType: monitor.getItemType()
})),
DragSource(TYPES.FOLDER, folderSource, (connect) => ({
connectDragSource: connect.dragSource()
}))
)(onClickOutside(CategoryClass));
export default Category;
@@ -0,0 +1,105 @@
import React from 'react';
import cx from 'classnames';
import PropTypes from 'prop-types';
import SidebarDropdown from './SidebarDropdown';
import { translate } from 'react-i18next';
class CategoryHead extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
showDeletePopup: PropTypes.func.isRequired,
showRenamePopup: PropTypes.func.isRequired,
showAddCategoryPopup: PropTypes.func.isRequired,
showAddClippingsFeedPopup: PropTypes.func.isRequired,
toggleCollapse: PropTypes.func.isRequired,
toggleCategoryDropdown: PropTypes.func.isRequired,
toggleExportCategory: PropTypes.func.isRequired,
isCategoryDropActive: PropTypes.bool.isRequired,
isCategoryActive: PropTypes.bool.isRequired,
hideDropDown: PropTypes.func.isRequired,
parentId: PropTypes.number.isRequired,
category: PropTypes.object.isRequired,
categories: PropTypes.array.isRequired
};
getSidebarName(name) {
const catName = this.props.t(`sidebar.${name}`);
if (catName === `sidebar.${name}`) {
return name;
}
return catName;
}
render() {
const {
isCategoryActive,
isCategoryDropActive,
category,
categories,
showDeletePopup,
showRenamePopup,
showAddCategoryPopup,
showAddClippingsFeedPopup,
toggleExportCategory,
hideDropDown
} = this.props;
const isCategoryDeletedType = category.subType === 'deleted_content';
const categoryAttrId = 'sidebar-category' + category.id;
return (
<div
className="metismenu-link"
id={categoryAttrId}
onClick={this.props.toggleCollapse}
>
{/* <i className="sidebar-category__closed-icon" onClick={this.props.toggleCollapse}> </i>
<i className="sidebar-category__open-icon" onClick={this.props.toggleCollapse}> </i> */}
{isCategoryDeletedType ? (
<i className="metismenu-icon pe-7s-trash"></i>
) : (
<i className="metismenu-icon pe-7s-folder"></i>
)}
{this.getSidebarName(category.name)}
{!isCategoryDeletedType && (
<i
tabIndex="0"
className="metismenu-state-icon font-size-lg opacity-10 pe-7s-more mr-4"
onClick={this.props.toggleCategoryDropdown}
/>
)}
<i
className={cx(
'metismenu-state-icon pe-7s-angle-down pointer-events-none opacity-10',
{
'rotate-minus-90': isCategoryActive
}
)}
/>
{isCategoryDropActive && (
<SidebarDropdown
parentAttrId={categoryAttrId}
categories={categories}
itemId={category.id}
itemSubType={category.subType}
itemType={category.type}
itemName={category.name}
parentId={this.props.parentId}
showDeletePopup={showDeletePopup}
showRenamePopup={showRenamePopup}
showAddCategoryPopup={showAddCategoryPopup}
showAddClippingsPopup={showAddClippingsFeedPopup}
hideDropDown={hideDropDown}
toggleExportCategory={toggleExportCategory}
/>
)}
</div>
);
}
}
export default translate(['common'], { wait: true })(CategoryHead);
@@ -0,0 +1,65 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
export class DeletePopup extends React.Component {
static propTypes = {
itemToDelete: PropTypes.object.isRequired,
hideDeletePopup: PropTypes.func.isRequired,
deleteFeed: PropTypes.func.isRequired,
deleteCategory: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
hidePopup = () => {
this.props.hideDeletePopup();
};
onSubmit = () => {
const {
itemToDelete,
deleteCategory,
deleteFeed,
hideDeletePopup
} = this.props;
switch (this.props.itemToDelete.itemType) {
case 'feed':
deleteFeed(itemToDelete.itemId, itemToDelete.parentId);
break;
case 'directory':
deleteCategory(itemToDelete.itemId);
break;
}
hideDeletePopup();
};
render() {
const itemName = this.props.itemToDelete.itemName;
const itemType = this.props.itemToDelete.itemType;
const { t } = this.props;
return (
<Modal isOpen toggle={this.hidePopup} backdrop="static">
<ModalHeader toggle={this.hidePopup}>
{t('commonWords.Confirm')}
</ModalHeader>
<ModalBody>
<p>
{t('messages.deleteMessage')} {itemType + ' "' + itemName + '"'}
</p>
</ModalBody>
<ModalFooter>
<Button color="light" onClick={this.hidePopup}>
{t('commonWords.Cancel')}
</Button>
<Button color="danger" onClick={this.onSubmit}>
{t('commonWords.Delete')}
</Button>
</ModalFooter>
</Modal>
);
}
}
export default translate(['common'], { wait: true })(DeletePopup);
+162
View File
@@ -0,0 +1,162 @@
/** DRAG SOURCE **/
import React from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import SidebarDropdown from './SidebarDropdown';
import { DragSource, DropTarget } from 'react-dnd';
import onClickOutside from 'react-onclickoutside';
import { TYPES } from '../../../redux/modules/appState/sidebar';
import { withRouter } from 'react-router-dom';
const feedSource = {
beginDrag(props) {
return {
type: TYPES.FEED,
id: props.feed.id,
feed: props.feed,
currentCategoryId: props.categoryId
};
}
};
/**
* Specifies which props to inject into component from Drag n Drop.
*/
function dragCollect(connect) {
return {
// Call this function inside render()
// to let React DnD handle the drag events:
connectDragSource: connect.dragSource()
};
}
/** DROP TARGET **/
const feedTarget = {
drop(props, monitor) {
if (monitor.didDrop()) return;
const { feed, clipArticles } = props;
clipArticles(feed.id);
},
canDrop(props, monitor) {
return props.feed.subType === 'clip_feed';
}
};
function dropCollect(connect, monitor) {
return {
connectDropTarget: connect.dropTarget()
};
}
export class Feed extends React.Component {
static propTypes = {
feed: PropTypes.object.isRequired,
categoryId: PropTypes.number.isRequired,
categories: PropTypes.array.isRequired,
showDeletePopup: PropTypes.func.isRequired,
showRenamePopup: PropTypes.func.isRequired,
hideParentCategoryDrop: PropTypes.func.isRequired,
connectDragSource: PropTypes.func.isRequired,
connectDropTarget: PropTypes.func.isRequired,
getFeedResults: PropTypes.func.isRequired,
clipArticles: PropTypes.func.isRequired,
toggleExportFeed: PropTypes.func.isRequired,
history: PropTypes.object.isRequired
};
constructor(props) {
super(props);
this.state = {
isItemDropActive: false
};
}
//hide feed dropdown if there was click outside
handleClickOutside = () => {
this.state.isItemDropActive && this.hideDropDown();
};
hideDropDown = () => {
this.setState({
isItemDropActive: false
});
};
toggleItemDropdown = (e) => {
e.preventDefault();
this.setState({
isItemDropActive: !this.state.isItemDropActive
});
};
onFeedClick = (e) => {
const { history, getFeedResults, feed } = this.props;
e.preventDefault();
history.push('/app/search/search');
getFeedResults({ page: 1 }, feed.id);
window.scrollTo(0, 0);
};
render() {
const {
feed,
categoryId,
categories,
connectDragSource,
connectDropTarget,
showDeletePopup,
showRenamePopup,
toggleExportFeed
} = this.props;
const feedAttrId = 'sidebar-feed' + feed.id;
const dragAndDrop = compose(connectDragSource, connectDropTarget);
return dragAndDrop(
<li
id={feedAttrId}
onClick={this.props.hideParentCategoryDrop}
className="metismenu-item"
>
<a
href="#"
className={`metismenu-link feed-icon ${feed.class}`}
onClick={this.onFeedClick}
>
{feed.name}
</a>
<i
tabIndex="0"
className="metismenu-state-icon font-size-lg opacity-10 pe-7s-more"
onClick={this.toggleItemDropdown}
></i>
{this.state.isItemDropActive && (
<SidebarDropdown
parentAttrId={feedAttrId}
categories={categories}
itemId={feed.id}
itemType={feed.type}
itemSubType={feed.subType}
itemName={feed.name}
itemExported={feed.exported}
parentId={categoryId}
showDeletePopup={showDeletePopup}
showRenamePopup={showRenamePopup}
toggleExportFeed={toggleExportFeed}
hideDropDown={this.hideDropDown}
/>
)}
</li>
);
}
}
const applyDecorators = compose(
withRouter,
DragSource(TYPES.FEED, feedSource, dragCollect),
DropTarget([TYPES.CLIP_ARTICLE], feedTarget, dropCollect),
onClickOutside
);
export default applyDecorators(Feed);
@@ -0,0 +1,106 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
export class Filter extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
areFeedsFiltered: PropTypes.bool.isRequired,
categories: PropTypes.array.isRequired,
setFilteredCategories: PropTypes.func.isRequired,
clearFilteredCategories: PropTypes.func.isRequired
}
constructor (props) {
super(props)
this.state = {
sidebarAnimationDisabled: true,
activeSearch: false
}
}
activeSearchFunc = () => {
this.setState({ activeSearch: !this.state.activeSearch })
this.clearFilter()
}
filterCategoriesList = (
categories,
searchQuery,
setParentBranchMatchFromParent
) => {
// show category if there is feed
return categories.filter((category) => {
category.branchMatch = false
//function that sets parent branchMatch prop
function setParentBranchMatch (flag) {
category.branchMatch = flag
}
if (category.childes.length > 0) {
category.childes = this.filterCategoriesList(
category.childes,
searchQuery,
setParentBranchMatch
)
}
// filter feeds in category
category.feeds = category.feeds.filter((feed) => {
return feed.name.toLowerCase().indexOf(searchQuery) !== -1
})
// if this category is a child and it has matched feeds or its child have, then we set branchMatch prop of parent
if (
(category.feeds.length > 0 && setParentBranchMatchFromParent) ||
(category.branchMatch && setParentBranchMatchFromParent)
) {
setParentBranchMatchFromParent(true)
}
return category.branchMatch || category.feeds.length > 0
})
}
filterSidebarItems = (e) => {
const searchQuery = e.target.value.toLowerCase()
const categoriesCopy = this.props.categories.slice(0)
if (searchQuery.length) {
const filteredCat = this.filterCategoriesList(categoriesCopy, searchQuery)
this.props.setFilteredCategories(filteredCat)
} else {
this.props.clearFilteredCategories()
}
}
clearFilter = () => {
this.props.clearFilteredCategories()
}
render () {
return (
<div
className={classnames('search-wrapper mb-1', {
active: this.state.activeSearch
})}
>
<div className="input-holder">
<input
type="text"
className="search-input"
placeholder={this.props.t('common:sidebar.typeToSearch')}
onKeyUp={this.filterSidebarItems}
id="sidebar-search"
/>
<button onClick={this.activeSearchFunc} className="search-icon">
<span />
</button>
</div>
<button onClick={this.activeSearchFunc} className="close"></button>
</div>
)
}
}
export default Filter
@@ -0,0 +1,90 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import {
Button,
Input,
Label,
Modal,
ModalBody,
ModalFooter,
ModalHeader
} from 'reactstrap'
export class RenamePopup extends React.Component {
static propTypes = {
itemToRename: PropTypes.object.isRequired,
hideRenamePopup: PropTypes.func.isRequired,
renameFeed: PropTypes.func.isRequired,
renameCategory: PropTypes.func.isRequired,
addAlert: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
}
constructor(props) {
super(props)
this.state = {
itemName: props.itemToRename.itemName
}
}
hidePopup = () => {
this.props.hideRenamePopup()
}
onSubmit = () => {
const newName = this.state.itemName
const {
itemToRename,
renameFeed,
renameCategory,
hideRenamePopup
} = this.props
switch (this.props.itemToRename.itemType) {
case 'feed':
renameFeed(itemToRename.itemId, newName, itemToRename.parentId)
break
case 'directory':
renameCategory(itemToRename.itemId, newName, itemToRename.parentId)
break
}
hideRenamePopup()
}
onChangeName = (e) => {
const { value } = e.target // validation needed
this.setState({
itemName: value
})
}
render() {
const itemName = this.state.itemName
const { t } = this.props
return (
<Modal isOpen toggle={this.hidePopup} backdrop="static">
<ModalHeader toggle={this.hidePopup}>
{t('commonWords.Rename')}
</ModalHeader>
<ModalBody>
<Label>{t('sidebarPopup.enterNamelabel')}</Label>
<Input type="text" value={itemName} onChange={this.onChangeName} />
</ModalBody>
<ModalFooter>
<Button color="light" onClick={this.hidePopup}>
{t('commonWords.Cancel')}
</Button>
<Button color="primary" onClick={this.onSubmit}>
{t('commonWords.Rename')}
</Button>
</ModalFooter>
</Modal>
)
}
}
export default translate(['common'], { wait: true })(RenamePopup)
@@ -0,0 +1,155 @@
import React, { Fragment } from 'react'
import PropTypes from 'prop-types'
import cx from 'classnames'
import Categories from './Categories'
import Filter from './Filter'
import DeletePopup from './DeletePopup'
import RenamePopup from './RenamePopup'
import AddCategoryPopup from './AddCategoryPopup'
import AddClippingsFeedPopup from './AddClippingsFeedPopup'
import LoadersAdvanced from '../../common/Loader/Loader'
import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup'
import PerfectScrollbar from 'react-perfect-scrollbar'
import HeaderLogo from '../AppHeader/HeaderLogo'
export class Sidebar extends React.Component {
static propTypes = {
actions: PropTypes.object.isRequired,
themeOptions: PropTypes.object.isRequired,
backgroundColor: PropTypes.string,
backgroundImage: PropTypes.any,
backgroundImageOpacity: PropTypes.any,
enableBackgroundImage: PropTypes.any,
enableMobileMenu: PropTypes.any,
enableSidebarShadow: PropTypes.any,
setEnableMobileMenu: PropTypes.func,
t: PropTypes.func,
sidebarState: PropTypes.object.isRequired
}
constructor (props) {
super(props)
this.state = {
sidebarAnimationDisabled: true,
activeSearch: false
}
}
toggleMobileSidebar = () => {
let { enableMobileMenu, setEnableMobileMenu } = this.props
setEnableMobileMenu(!enableMobileMenu)
}
componentDidMount = () => {
this.props.actions.getSidebarCategories()
}
activeSearchFunc = () => {
this.setState({ activeSearch: !this.state.activeSearch })
}
render () {
let {
backgroundColor,
enableBackgroundImage,
enableSidebarShadow,
backgroundImage,
backgroundImageOpacity
} = this.props.themeOptions
const { sidebarState, actions } = this.props
return (
<Fragment>
<div
className="sidebar-mobile-overlay"
onClick={this.toggleMobileSidebar}
/>
<CSSTransitionGroup
component="div"
className={cx('app-sidebar', backgroundColor, {
'sidebar-shadow': enableSidebarShadow
})}
transitionName="SidebarAnimation"
transitionAppear
transitionAppearTimeout={1500}
transitionEnter={false}
transitionLeave={false}
>
<HeaderLogo />
{!sidebarState.areCategoriesLoaded && <LoadersAdvanced />}
<PerfectScrollbar>
<div className="app-sidebar__inner mt-3">
<div className="vertical-nav-menu" data-tour="left-panel">
<div className="metismenu-container">
<Filter
t={this.props.t}
categories={sidebarState.categories}
areFeedsFiltered={sidebarState.areFeedsFiltered}
setFilteredCategories={actions.setFilteredCategories}
clearFilteredCategories={actions.clearFilteredCategories}
/>
<Categories
actions={actions}
areCategoriesLoaded={sidebarState.areCategoriesLoaded}
areFeedsFiltered={sidebarState.areFeedsFiltered}
categories={sidebarState.categories}
filteredCategories={sidebarState.filteredCategories}
/>
</div>
{sidebarState.popupVisible.delete && (
<DeletePopup
hideDeletePopup={actions.hideDeletePopup}
deleteFeed={actions.deleteFeed}
deleteCategory={actions.deleteCategory}
itemToDelete={sidebarState.popupItems.delete}
/>
)}
{sidebarState.popupVisible.rename && (
<RenamePopup
addAlert={actions.addAlert}
hideRenamePopup={actions.hideRenamePopup}
renameFeed={actions.renameFeed}
renameCategory={actions.renameCategory}
itemToRename={sidebarState.popupItems.rename}
/>
)}
{sidebarState.popupVisible.addCategory && (
<AddCategoryPopup
hideAddCategoryPopup={actions.hideAddCategoryPopup}
addCategory={actions.addCategory}
parentId={sidebarState.popupItems.addCategory.parentId}
/>
)}
{sidebarState.popupVisible.addClippingsFeed && (
<AddClippingsFeedPopup
parentId={sidebarState.popupItems.addClippingsFeed.parentId}
hidePopup={actions.hideAddClippingsFeedPopup}
addClippingsFeed={actions.addClippingsFeed}
addAlert={actions.addAlert}
categories={sidebarState.categories}
/>
)}
</div>
</div>
</PerfectScrollbar>
<div
className={cx('app-sidebar-bg', backgroundImageOpacity)}
style={{
backgroundImage: enableBackgroundImage
? 'url(' + backgroundImage + ')'
: null
}}
></div>
</CSSTransitionGroup>
</Fragment>
)
}
}
export default React.memo(Sidebar)
@@ -0,0 +1,154 @@
import React from 'react'
import PropTypes from 'prop-types'
import $ from 'jquery'
import { translate } from 'react-i18next'
export class SidebarDropdown extends React.Component {
static propTypes = {
itemName: PropTypes.string.isRequired,
itemSubType: PropTypes.string.isRequired,
itemType: PropTypes.string.isRequired,
itemId: PropTypes.number.isRequired,
itemExported: PropTypes.bool,
parentId: PropTypes.number.isRequired,
parentAttrId: PropTypes.string.isRequired,
showDeletePopup: PropTypes.func.isRequired,
showRenamePopup: PropTypes.func.isRequired,
showAddCategoryPopup: PropTypes.func,
showAddClippingsPopup: PropTypes.func,
toggleExportFeed: PropTypes.func,
toggleExportCategory: PropTypes.func,
t: PropTypes.func.isRequired,
hideDropDown: PropTypes.func.isRequired
};
constructor (props) {
super(props)
this.state = {
dropdownTopPos: 'auto',
dropdownBottomPos: 'auto',
dropdownOpacity: 0
}
}
componentDidMount = () => {
const topPos = $('#' + this.props.parentAttrId).offset().top - $(document).scrollTop()
const dropdownHeight = $('#sidebar-category-dropdown').height()
if ($(window).height() - topPos >= dropdownHeight) {
this.setState({
dropdownTopPos: topPos,
dropdownOpacity: 1
})
} else {
this.setState({
dropdownBottomPos: 5,
dropdownOpacity: 1
})
}
};
onExportToggle = () => {
const {itemId, toggleExportFeed, itemExported, hideDropDown} = this.props
toggleExportFeed(itemId, !itemExported)
hideDropDown()
};
onExportCategoryToggle = () => {
const {itemId, toggleExportCategory, itemExported, hideDropDown} = this.props
toggleExportCategory(itemId, !itemExported)
hideDropDown()
};
onDelete = () => {
this.props.showDeletePopup(this.props.itemId, this.props.itemType, this.props.itemName, this.props.parentId)
};
onRename = () => {
this.props.showRenamePopup(this.props.itemId, this.props.itemType, this.props.itemName, this.props.parentId)
};
onAddCategory = () => {
// set this item id as parent of new category
this.props.showAddCategoryPopup(this.props.itemId)
};
onAddClippingsFeedPopup = () => {
this.props.showAddClippingsPopup(this.props.itemId)
};
render () {
const { itemSubType, t, itemExported } = this.props
let dropdown
switch (itemSubType) {
case 'my_content':
dropdown = <ul id="sidebar-category-dropdown" className="sidebar-category__dropdown" style={{top: this.state.dropdownTopPos, bottom: this.state.dropdownBottomPos, opacity: this.state.dropdownOpacity}}>
<li><a href="#" onClick={this.onAddClippingsFeedPopup}>{t('sidebarDropdown.AddClippingsFeed')}</a></li>
<li><a href="#" onClick={this.onAddCategory}>{t('sidebarDropdown.AddFolder')}</a></li>
{/*<li><a href="#">{t('sidebarDropdown.DownloadSearchCriteria')}</a></li>
<li><a href="#">{t('sidebarDropdown.EditSearchTemplate')}</a></li>*/}
<li><a href="#" onClick={this.onExportCategoryToggle}>{t(itemExported ? 'sidebarDropdown.UnexportFeeds' : 'sidebarDropdown.ExportFeeds')}</a></li>
{/*<li><a href="#">{t('sidebarDropdown.ViewUserComments')}</a></li>*/}
</ul>
break
case 'shared_content':
dropdown = <ul id="sidebar-category-dropdown" className="sidebar-category__dropdown" style={{top: this.state.dropdownTopPos, bottom: this.state.dropdownBottomPos, opacity: this.state.dropdownOpacity}}>
<li><a href="#" onClick={this.onAddClippingsFeedPopup}>{t('sidebarDropdown.AddClippingsFeed')}</a></li>
<li><a href="#" onClick={this.onAddCategory}>{t('sidebarDropdown.AddFolder')}</a></li>
{/*<li><a href="#">{t('sidebarDropdown.DownloadSearchCriteria')}</a></li>*/}
<li><a href="#" onClick={this.onExportCategoryToggle}>{t(itemExported ? 'sidebarDropdown.UnexportFeeds' : 'sidebarDropdown.ExportFeeds')}</a></li>
{/*<li><a href="#">{t('sidebarDropdown.ViewUserComments')}</a></li>*/}
</ul>
break
case 'custom':
dropdown = <ul id="sidebar-category-dropdown" className="sidebar-category__dropdown" style={{top: this.state.dropdownTopPos, bottom: this.state.dropdownBottomPos, opacity: this.state.dropdownOpacity}}>
<li><a href="#" onClick={this.onAddClippingsFeedPopup}>{t('sidebarDropdown.AddClippingsFeed')}</a></li>
<li><a href="#" onClick={this.onAddCategory}>{t('sidebarDropdown.AddFolder')}</a></li>
{/*<li><a href="#">{t('sidebarDropdown.DownloadSearchCriteria')}</a></li>*/}
<li><a href="#" onClick={this.onExportCategoryToggle}>{t(itemExported ? 'sidebarDropdown.UnexportFeeds' : 'sidebarDropdown.ExportFeeds')}</a></li>
<li><a href="#" onClick={this.onRename}>{t('sidebarDropdown.RenameFolder')}</a></li>
{/*<li><a href="#">{t('sidebarDropdown.ViewUserComments')}</a></li>*/}
<li><a href="#" onClick={this.onDelete}>{t('sidebarDropdown.DeleteFolder')}</a></li>
</ul>
break
case 'query_feed':
dropdown = <ul id="sidebar-category-dropdown" className="sidebar-category__dropdown" style={{top: this.state.dropdownTopPos, bottom: this.state.dropdownBottomPos, opacity: this.state.dropdownOpacity}}>
{/*<li><a href="#">{t('sidebarDropdown.AddArticle')}</a></li>
<li><a href="#">{t('sidebarDropdown.AddToDashboard')}</a></li>
<li><a href="#">{t('sidebarDropdown.AnalyzeFeed')}</a></li>
<li><a href="#">{t('sidebarDropdown.DownloadArticleData')}</a></li>
<li><a href="#">{t('sidebarDropdown.DownloadFeedStatistics')}</a></li>
<li><a href="#">{t('sidebarDropdown.DownloadSearchCriteria')}</a></li>*/}
<li><a href="#" onClick={this.onExportToggle}>{t(itemExported ? 'sidebarDropdown.UnexportFeed' : 'sidebarDropdown.ExportFeed')}</a></li>
<li><a href="#" onClick={this.onRename}>{t('sidebarDropdown.RenameFeed')}</a></li>
<li><a href="#" onClick={this.onDelete}>{t('sidebarDropdown.DeleteFeed')}</a></li>
</ul>
break
case 'clip_feed':
dropdown = <ul id="sidebar-category-dropdown" className="sidebar-category__dropdown" style={{top: this.state.dropdownTopPos, bottom: this.state.dropdownBottomPos, opacity: this.state.dropdownOpacity}}>
{/*<li><a href="#">{t('sidebarDropdown.AddArticle')}</a></li>
<li><a href="#">{t('sidebarDropdown.AddToDashboard')}</a></li>
<li><a href="#">{t('sidebarDropdown.AnalyzeFeed')}</a></li>
<li><a href="#">{t('sidebarDropdown.DownloadArticleData')}</a></li>
<li><a href="#">{t('sidebarDropdown.DownloadFeedStatistics')}</a></li>
<li><a href="#">{t('sidebarDropdown.DownloadSearchCriteria')}</a></li>*/}
<li><a href="#" onClick={this.onExportToggle}>{t(itemExported ? 'sidebarDropdown.UnexportFeed' : 'sidebarDropdown.ExportFeed')}</a></li>
<li><a href="#" onClick={this.onRename}>{t('sidebarDropdown.RenameFeed')}</a></li>
<li><a href="#" onClick={this.onDelete}>{t('sidebarDropdown.DeleteFeed')}</a></li>
</ul>
break
}
return (
dropdown
)
}
}
export default translate(['common'], { wait: true })(SidebarDropdown)
@@ -0,0 +1,56 @@
import React from 'react';
import PropTypes from 'prop-types';
import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup';
import SubTabWrapper from '../../AppHeader/SubTabWrapper';
import { Redirect, Route, Switch, withRouter } from 'react-router-dom';
import ShowCharts from './CreateAnalysisSubTab/ShowCharts';
import SavedAnalysisSubTab from './SavedAnalysisSubTab/SavedAnalysisSubTab';
import CreateAnalysisSubTab from './CreateAnalysisSubTab/CreateAnalysisSubTab';
function AnalyzeTab(props) {
const { subTabs, allowAnalytics, history, activeTabName, match } = props;
if (!allowAnalytics) {
history.push('/app/search/search');
return null;
}
return (
<CSSTransitionGroup
component="div"
transitionName="TabsAnimation"
transitionAppear
transitionAppearTimeout={0}
transitionEnter={false}
transitionLeave={false}
>
<SubTabWrapper activeTabName={activeTabName} subTabs={subTabs}>
<Switch>
{/* <Route path={`${match.url}/welcome`} component={WelcomeSubTab} /> */}
<Route path={`${match.url}/saved`} component={SavedAnalysisSubTab} />
<Route
path={`${match.url}/create`}
component={CreateAnalysisSubTab}
/>
<Route
path={`${match.url}/edit/:id`}
component={CreateAnalysisSubTab}
/>
<Route path={`${match.url}/:id`} component={ShowCharts} />
<Redirect to={`${match.url}/saved`} />
</Switch>
</SubTabWrapper>
</CSSTransitionGroup>
);
}
AnalyzeTab.propTypes = {
activeTabName: PropTypes.string,
children: PropTypes.any,
history: PropTypes.object,
match: PropTypes.object,
allowAnalytics: PropTypes.bool,
subTabs: PropTypes.array
};
export default withRouter(AnalyzeTab);
@@ -0,0 +1,293 @@
import React, { Fragment, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import {
Button,
Form,
FormGroup,
Label,
Modal,
ModalBody,
ModalFooter,
ModalHeader
} from 'reactstrap';
import Select from 'react-select';
import { Input, Checkbox, RadioButton } from '../../../../common/FormControls';
import useForm from '../../../../common/hooks/useForm.js';
import { EXTRAS } from '../../../../../redux/modules/appState/share/forms/alertForm';
import { createAlertAPI } from '../../../../../api/analytics/createAnalytics';
import { getCurrentTimezone, timezones } from '../../../../../common/Timezones';
import { compose } from 'redux';
import reduxConnect from '../../../../../redux/utils/connect';
import translate from 'react-i18next/dist/commonjs/translate';
import { THEME_TYPES } from '../../../../../redux/modules/appState/share/forms/notificationForm';
const initialForm = {
name: '',
recipients: [],
subject: '',
automatedSubject: false,
unsubscribeNotification: false,
published: false,
allowUnsubscribe: false,
articleExtracts: EXTRAS.CONTEXTUAL,
highlight: false,
showSourceCountry: false,
showUserComments: false,
themeType: THEME_TYPES.PLAIN,
sendWhenEmpty: false,
timezone: getCurrentTimezone(),
notificationType: 'alert',
// automatic: [], // auto schedule
// sentUntil: '',
errors: {
name: null
}
};
function AlertDialog(props) {
const { toggle, isOpen, alertCharts, actions, resetAlertChart, user } = props;
const [loading, setLoading] = useState(false);
const {
form,
handleChange,
handleValidation,
errors,
validateSubmit,
resetForm
} = useForm(initialForm);
function handleSubmit() {
const obj = validateSubmit();
if (!obj) {
return actions.addAlert({ type: 'error', transKey: 'requiredInfo' });
}
setLoading(true);
if (obj.automatedSubject) {
delete obj.subject;
}
obj.sources = alertCharts.map((chart) => ({
id: chart.id,
type: 'chart'
}));
createAlertAPI(obj).then((res) => {
if (res.error) {
res.data
? actions.addAlert(res.data)
: actions.addAlert({ type: 'error', transKey: 'somethingWrong' });
setLoading(false);
return;
}
actions.addAlert({ type: 'notice', transKey: 'alertSaved' });
setLoading(false);
toggle();
resetForm();
resetAlertChart();
});
}
useEffect(() => {
if (form.recipients && user.recipient && user.recipient.id) {
handleChange('recipients', [user.recipient.id]);
}
return () => resetForm();
}, []);
return (
<Modal isOpen={isOpen} toggle={toggle} backdrop="static" size="lg">
<ModalHeader toggle={toggle}>Create Alert</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<Label>Selected Charts</Label>
<div className="b-radius-5 bg-light p-2">
{alertCharts.map((chart, i, arr) => (
<Fragment key={chart.name}>
<span className="d-inline-block mr-1">
{chart.name}
{arr.length - 1 !== i ? ', ' : ''}
</span>
</Fragment>
))}
</div>
</FormGroup>
<Input
name="name"
title="Name"
required
value={form.name}
error={errors.name}
handleChange={handleChange}
handleValidation={handleValidation}
/>
<Checkbox
name="automatedSubject"
title="Automated Subject"
description="Use automated email subject based on the feeds"
value={form.automatedSubject}
error={errors.automatedSubject}
handleChange={handleChange}
/>
{!form.automatedSubject && (
<Input
name="subject"
title="Email Subject"
value={form.subject}
error={errors.subject}
handleChange={handleChange}
handleValidation={handleValidation}
/>
)}
<Checkbox
name="published"
title="Publish"
description="Alerts and Newsletters that are Published are available for other users to subscribe"
value={form.published}
error={errors.publish}
handleChange={handleChange}
/>
<Checkbox
name="allowUnsubscribe"
title="Unsubscribe Link"
description="Allow recipients to unsubscribe from Alert"
value={form.allowUnsubscribe}
error={errors.allowUnsubscribe}
handleChange={handleChange}
/>
<Checkbox
name="unsubscribeNotification"
title="Notifications"
description="Notify creator when recipients unsubscribe"
value={form.unsubscribeNotification}
error={errors.unsubscribeNotification}
handleChange={handleChange}
/>
<FormGroup className="radio-options">
<Label>Options</Label>
<RadioButton
name="articleExtracts"
title="Article Extracts"
formClass="mb-0"
options={[
{ label: 'Contextual extract', value: EXTRAS.CONTEXTUAL },
{ label: 'Start of text extract', value: EXTRAS.START },
{ label: 'No article extract', value: EXTRAS.NO }
]}
inline
value={form.articleExtracts}
error={errors.articleExtracts}
handleChange={handleChange}
/>
<RadioButton
name="highlight"
title="Highlight Keywords"
formClass="mb-0"
options={[
{ label: 'Yes', value: true },
{ label: 'No', value: false }
]}
inline
value={form.highlight}
error={errors.highlight}
handleChange={handleChange}
/>
<RadioButton
name="showSourceCountry"
title="Show Source Country"
formClass="mb-0"
options={[
{ label: 'Yes', value: true },
{ label: 'No', value: false }
]}
inline
value={form.showSourceCountry}
error={errors.showSourceCountry}
handleChange={handleChange}
/>
<RadioButton
name="showUserComments"
title="Show User Comments"
formClass="mb-0"
options={[
{ label: 'Yes', value: true },
{ label: 'No', value: false }
]}
inline
value={form.showUserComments}
error={errors.showUserComments}
handleChange={handleChange}
/>
<RadioButton
name="themeType"
title="Layout"
formClass="mb-0"
options={[
{ label: 'Enhanced HTML', value: THEME_TYPES.ENHANCED },
{ label: 'Plain HTML', value: THEME_TYPES.PLAIN }
]}
inline
value={form.themeType}
error={errors.themeType}
handleChange={handleChange}
/>
<RadioButton
name="sendWhenEmpty"
title="Send When Empty"
formClass="mb-0"
options={[
{ label: 'Yes', value: true },
{ label: 'No', value: false }
]}
inline
value={form.sendWhenEmpty}
error={errors.sendWhenEmpty}
handleChange={handleChange}
/>
</FormGroup>
<FormGroup>
<Label>Timezone</Label>
<Select
className="timezone-select"
value={form.timezone}
options={timezones}
clearable={false}
onChange={function (v) {
handleChange('timezone', v.value);
}}
/>
</FormGroup>
{/* <FormGroup>
<Label>Automatic</Label>
<Scheduling state={state.scheduling} actions={actions} />
</FormGroup> */}
</Form>
</ModalBody>
<ModalFooter>
<Button color="link" onClick={toggle}>
Cancel
</Button>
<Button color="primary" disabled={loading} onClick={handleSubmit}>
{loading ? 'Loading...' : 'Submit'}
</Button>
</ModalFooter>
</Modal>
);
}
AlertDialog.propTypes = {
toggle: PropTypes.func,
resetAlertChart: PropTypes.func,
isOpen: PropTypes.bool,
alertCharts: PropTypes.array,
user: PropTypes.object,
actions: PropTypes.object
};
const applyDecorators = compose(
reduxConnect('user', ['common', 'auth', 'user']),
translate(['tabsContent'], { wait: true })
);
export default applyDecorators(AlertDialog);
@@ -0,0 +1,83 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
Card,
CardBody,
CardHeader,
DropdownItem,
DropdownMenu,
DropdownToggle,
UncontrolledButtonDropdown
} from 'reactstrap';
import cx from 'classnames';
import { IoIosMenu } from 'react-icons/io';
function ChartWrapper(props) {
let { title, children, menus } = props;
const hasShowMore = menus.find((menu) => !menu.hide && menu.showInMore);
// TODO: hide alert until API is ready
menus = menus.filter((menu) => menu.title);
const isRTL = document.documentElement.dir === 'rtl';
return (
<Card className="mb-3">
<CardHeader>
{title && <div>{title}</div>}
<div className="btn-actions-pane-right actions-icon-btn">
<div className="align-content-center d-flex d-inline-flex">
{menus &&
menus.map((menu) =>
!menu.hide && !menu.showInMore && menu.icon ? (
<button
key={menu.title}
title={menu.title}
className="btn btn-icon-only mr-2 p-0"
onClick={menu.fn}
disabled={!menu.fn}
>
<menu.icon size={menu.size || 16} />
</button>
) : null
)}
</div>
{menus && hasShowMore && (
<UncontrolledButtonDropdown>
<DropdownToggle className="btn-icon btn-icon-only" color="link">
<div className="btn-icon-wrapper">
<IoIosMenu size={24} />
</div>
</DropdownToggle>
<DropdownMenu
className={`dropdown-menu-shadow dropdown-menu-hover-link${
isRTL ? ' dropdown-menu-left' : ''
}`}
>
{menus.map((menu) =>
!menu.hide && menu.showInMore ? (
<DropdownItem onClick={menu.fn} key={menu.title}>
{menu.icon && (
<i className={cx('dropdown-icon', menu.icon)}></i>
)}
<span>{menu.title}</span>
</DropdownItem>
) : null
)}
</DropdownMenu>
</UncontrolledButtonDropdown>
)}
</div>
</CardHeader>
<CardBody>{children}</CardBody>
</Card>
);
}
ChartWrapper.propTypes = {
title: PropTypes.string,
children: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
menus: PropTypes.array
};
export default ChartWrapper;
@@ -0,0 +1,272 @@
/* eslint-disable react/jsx-no-bind */
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { DateRangePicker } from 'react-dates';
import { translate } from 'react-i18next';
import { compose } from 'redux';
import { useHistory, useParams } from 'react-router-dom';
import {
Button,
Card,
CardBody,
CardTitle,
Col,
FormGroup,
InputGroup,
Label,
Row
} from 'reactstrap';
import Loader from 'react-loader-advanced';
import { Loader as LoaderAnim } from 'react-loaders';
import { useDrop } from 'react-dnd';
import { IoIosCloseCircleOutline } from 'react-icons/io';
import {
addEditAnalyticsAPI,
getAnalyticDetailsAPI
} from '../../../../../api/analytics/createAnalytics';
import { TYPES } from '../../../../../redux/modules/appState/sidebar';
import reduxConnect from '../../../../../redux/utils/connect';
import useIsMounted from '../../../../common/hooks/useIsMounted';
import { subChartCategories } from './ShowCharts';
import { getMomentObject, setDocumentData } from '../../../../../common/helper';
const initialState = {
feeds: [],
startDate: null,
endDate: null
};
const spinner = <LoaderAnim color="#ffffff" type="ball-pulse" />;
function CreateAnalysisSubTab({ t, actions }) {
const isMounted = useIsMounted();
const history = useHistory();
const { id } = useParams();
const [form, setForm] = useState(initialState);
const [error, setError] = useState();
const [loading, setLoading] = useState(false);
const [fetching, setFetching] = useState(!!id);
const [focusedInput, setFocusedInput] = useState();
const [{ canDrop, isOver }, drop] = useDrop({
accept: [TYPES.FEED, TYPES.CLIP_ARTICLE],
drop: droppedFeeds,
canDrop: canDroppable,
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop()
})
});
function getAnalyticData() {
setFetching(true);
getAnalyticDetailsAPI(id).then((res) => {
if (!isMounted.current) {
return;
}
if (res.error || !res.data || !res.data.context) {
setFetching(false);
res.data
? actions.addAlert(res.data)
: actions.addAlert({ type: 'error', transKey: 'somethingWrong' });
history.push('/app/analyze/saved');
return;
}
const { context } = res.data;
const date = context && context.rawFilters && context.rawFilters.date;
setForm({
feeds: context.feeds.map((item) => ({
feed: { name: item.name },
id: item.id
})),
startDate: getMomentObject(date && date.start),
endDate: getMomentObject(date && date.end)
});
setFetching(false);
});
}
useEffect(() => {
setDocumentData('title', `${id ? 'Update' : 'Create'} Analysis | Analyze`);
return () => {
setDocumentData('title');
};
}, []);
useEffect(() => {
if (id) {
getAnalyticData();
}
}, [id]);
function canDroppable(item) {
if (form.feeds.find((val) => val.id === item.id)) {
return false;
}
return true;
}
function droppedFeeds(item) {
if (form.feeds.find((val) => val.id === item.id)) {
return;
}
setForm((prev) => ({ ...prev, feeds: [...prev.feeds, item] }));
}
function removeFeeds(id) {
setForm((prev) => {
const modifiedFeeds = form.feeds.filter((val) => val.id !== id);
return { ...prev, feeds: modifiedFeeds };
});
}
const isActive = canDrop && isOver;
function handleSubmit() {
const isValid = Object.values(form).every((value) =>
value ? (Array.isArray(value) ? value.length > 0 : true) : false
);
if (!isValid) {
return setError(t('common:alerts.error.requiredInfo'));
}
setError(false);
setLoading(true);
addEditAnalyticsAPI(form, id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.id) {
// on error
setLoading(false);
setError(res.errorMessage);
return;
}
actions.resetAlertChart();
setLoading(false);
history.push(`/app/analyze/${res.data.id}/${subChartCategories[0].path}`);
});
}
function handleDateChange({ startDate, endDate }) {
setForm((prev) => ({ ...prev, startDate, endDate }));
}
function onFocusChange(focus) {
setFocusedInput(focus);
}
function isOutsideRange() {
return false;
}
const isRTL = document.documentElement.dir === 'rtl';
return (
<Card className="mb-3">
<Loader message={spinner} show={fetching}>
<CardBody>
<CardTitle>
{id ? t('analyzeTab.updateDetails') : t('analyzeTab.enterDetails')}
</CardTitle>
<Row>
<Col sm="12">
<FormGroup data-tour="drop-feeds-box">
<div>
{form.feeds.length > 0 && (
<div className="mb-3">
<Label>{t('analyzeTab.selectedFeeds')}</Label>
<div>
{form.feeds.map((item) => (
<div
key={item.id}
className="bg-light d-inline d-inline-flex align-items-center mr-2 p-2 text-dark"
>
<p>{item.feed.name}</p>
<button
className="btn p-0"
onClick={function () {
removeFeeds(item.id);
}}
>
<IoIosCloseCircleOutline
size={22}
className="text-danger ml-2"
/>
</button>
</div>
))}
</div>
</div>
)}
<Label>{t('analyzeTab.selectFeeds')}</Label>
<div ref={drop} className="dropzone-wrapper">
<div>
<div className="dropzone-content">
<p>
{isActive
? t('analyzeTab.releaseDesc')
: t('analyzeTab.dropDesc')}
</p>
</div>
</div>
</div>
</div>
</FormGroup>
<FormGroup data-tour="analytics-data-range">
<Label className="mr-sm-2">{t('analyzeTab.dateRange')}</Label>
<InputGroup>
<DateRangePicker
startDateId="startDate"
endDateId="endDate"
startDate={form.startDate}
endDate={form.endDate}
onDatesChange={handleDateChange}
focusedInput={focusedInput}
onFocusChange={onFocusChange}
displayFormat="MM/DD/YYYY"
startDatePlaceholderText={t('analyzeTab.startDatePlaceholder')}
endDatePlaceholderText={t('analyzeTab.endDatePlaceholder')}
numberOfMonths={1}
isOutsideRange={isOutsideRange}
isRTL={isRTL}
/>
</InputGroup>
</FormGroup>
{error && <div className="text-danger mb-2">{error}</div>}
<Button
className="mb-2 mr-2 btn-icon"
color="primary"
disabled={loading}
data-tour="create-analytics-button"
onClick={handleSubmit}
>
{loading
? 'Loading...'
: id
? t('analyzeTab.updateBtn')
: t('analyzeTab.createBtn')}
</Button>
</Col>
</Row>
</CardBody>
</Loader>
</Card>
);
}
CreateAnalysisSubTab.propTypes = {
t: PropTypes.func.isRequired,
actions: PropTypes.object
};
const applyDecorators = compose(
reduxConnect(),
translate(['tabsContent'], { wait: true })
);
export default applyDecorators(CreateAnalysisSubTab);
@@ -0,0 +1,276 @@
import React, {
useState,
useCallback,
Fragment,
useEffect,
useMemo
} from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import {
NavLink,
Redirect,
Route,
Switch,
useHistory,
useParams
} from 'react-router-dom';
import {
Button,
DropdownItem,
DropdownMenu,
DropdownToggle,
UncontrolledDropdown
} from 'reactstrap';
import { IoIosTrash } from 'react-icons/io';
import {
Results,
Performance,
Influencers,
Sentiment,
Themes,
Demographics
// WorldMap
} from './Tabs';
import AlertDialog from './AlertDialog';
import reduxConnect from '../../../../../redux/utils/connect';
import translate from 'react-i18next/dist/commonjs/translate';
import { compose } from 'redux';
import { getAnalyticDetailsAPI } from '../../../../../api/analytics/createAnalytics';
import useIsMounted from '../../../../common/hooks/useIsMounted';
import { setDocumentData } from '../../../../../common/helper';
import { Interpolate } from 'react-i18next';
// exported for routing
export const subChartCategories = [
{
title: 'Overview',
transKey: 'overview',
path: 'overview',
component: Results
},
{
title: 'Performance',
transKey: 'performance',
path: 'performance',
component: Performance
},
{
title: 'Influencers',
transKey: 'influencers',
path: 'influencers',
component: Influencers
},
{
title: 'Sentiment',
transKey: 'sentiment',
path: 'sentiment',
component: Sentiment
},
{ title: 'Themes', transKey: 'themes', path: 'themes', component: Themes },
{
title: 'Demographics',
transKey: 'demographics',
path: 'demographics',
component: Demographics
}
// { title: 'World Map', transKey: 'worldMap', path: 'worldmap', component: WorldMap }
];
function ShowCharts({ analyze, actions, t }) {
const isMounted = useIsMounted();
const history = useHistory();
const params = useParams();
const [chartData, setChartData] = useState({});
const [alertModal, setAlertModal] = useState(false);
const [fetching, setFetching] = useState(true);
const [feedData, setFeedData] = useState(null);
const { removeAlertChart, resetAlertChart } = actions;
const { alertCharts } = analyze;
useEffect(() => {
setDocumentData('title', 'View Analysis | Analyze');
return () => {
setDocumentData('title');
};
}, []);
useEffect(() => {
if (!params.id || isNaN(params.id)) {
history.push('/app/analyze/saved');
} else {
getAnalyticData();
}
return () => resetAlertChart(); // reset store
}, [params.id]);
const updateResult = useCallback((data, chartName) => {
setChartData((prev) => ({ ...prev, [chartName]: data }));
}, []);
const subChartRoutes = useMemo(() => {
return subChartCategories.map(({ path, component: SubChart }) => (
<Route exact key={path} path={`/app/analyze/${params.id}/${path}`}>
<SubChart
id={params.id}
feedData={feedData}
chartData={chartData}
updateResult={updateResult}
/>
</Route>
));
}, [updateResult, chartData, feedData, params.id]);
function getAnalyticData() {
setFetching(true);
getAnalyticDetailsAPI(params.id).then((res) => {
if (!isMounted.current) {
return;
}
if (res.error || !res.data || !res.data.context) {
setFetching(false);
res.data
? actions.addAlert(res.data)
: actions.addAlert({ type: 'error', transKey: 'somethingWrong' });
history.push('/app/analyze/saved');
return;
}
const { context } = res.data;
const date = context && context.rawFilters && context.rawFilters.date;
setFeedData({
feeds: context.feeds.map((item) => ({
feed: item.name,
id: item.id
})),
startDate: date && date.start,
endDate: date && date.end
});
setFetching(false);
});
}
function toggleModal() {
setAlertModal((prev) => !prev);
}
if (fetching) {
return 'Loading...';
}
const isRTL = document.documentElement.dir === 'rtl';
return (
<Fragment>
<div
className="d-flex"
style={{ position: 'absolute', top: 0, right: 0 }}
>
{alertCharts && alertCharts.length > 0 && (
<UncontrolledDropdown className="d-inline-block">
<DropdownToggle color="info" className="btn-shadow" caret>
<Interpolate
t={t}
i18nKey="analyzeTab.createAlert"
alertsLength={alertCharts.length}
/>
</DropdownToggle>
<DropdownMenu
className={`dropdown-menu-right rm-pointers dropdown-menu-shadow dropdown-menu-hover-link${
isRTL ? ' dropdown-menu-left' : ''
}`}
>
<DropdownItem header>
{t('analyzeTab.selectedCharts')}
</DropdownItem>
{alertCharts.map((chart, i) => (
<div className="dropdown-item" key={`${chart.name}_${i}}`}>
<span>
{chart.name}
{isNaN(chart.id) ? '' : ` (#${chart.id})`}
</span>
<Button
className="btn-icon btn-icon-only ml-auto mr-2 p-1"
color="danger"
onClick={function () {
removeAlertChart({ name: chart.name, id: chart.id });
}}
>
<IoIosTrash fontSize="1rem" className="ml-auto" />
</Button>
</div>
))}
<DropdownItem divider />
<div className="p-2 pr-3 text-right">
<Button
className="btn-shadow btn-sm"
color="primary"
onClick={toggleModal}
>
{t('analyzeTab.createAlertBtn')}
</Button>
</div>
</DropdownMenu>
</UncontrolledDropdown>
)}
{/*
<Button
className="btn-icon ml-2"
color="info"
// change style for mobile view
>
<IoIosSave className="btn-icon-wrapper" />
Save
</Button> */}
</div>
<div className="btn-actions-pane-right mask-line overflow-auto mb-3 pl-3">
{subChartCategories.map((cat, i, arr) => (
<Button
key={cat.title}
title={cat.title}
tag={NavLink}
to={`/app/analyze/${params.id}/${cat.path}`}
size="sm"
outline
color="primary"
className={cx('btn-pill btn-wide', {
'mr-1 ml-1': i !== 0 && i !== arr.length - 1
})}
activeClassName="active"
>
{t(`analyzeTab.overviewCharts.${cat.transKey}`)}
</Button>
))}
</div>
<AlertDialog
isOpen={alertModal}
toggle={toggleModal}
alertCharts={alertCharts}
resetAlertChart={resetAlertChart}
/>
<Switch>
{subChartRoutes}
<Redirect
to={`/app/analyze/${params.id}/${subChartCategories[0].path}`}
/>
</Switch>
</Fragment>
);
}
ShowCharts.propTypes = {
t: PropTypes.func.isRequired,
analyze: PropTypes.object,
actions: PropTypes.object
};
const applyDecorators = compose(
reduxConnect('analyze', ['appState', 'analyze']),
translate(['tabsContent'], { wait: true })
);
export default applyDecorators(ShowCharts);
@@ -0,0 +1,357 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Col, Row } from 'reactstrap';
import ECharts from '../../../../../common/charts/ECharts';
import ChartWrapper from '../ChartWrapper';
import {
getBarOptions,
getPieOptions
} from '../../../../../common/charts/ChartsOptions';
import { IoIosAdd, IoIosRefresh, IoIosCheckmark } from 'react-icons/io';
import reduxConnect from '../../../../../../redux/utils/connect';
import translate from 'react-i18next/dist/commonjs/translate';
import { compose } from 'redux';
import { getOverviewPieAPI } from '../../../../../../api/analytics/createAnalytics';
import useIsMounted from '../../../../../common/hooks/useIsMounted';
const initialBar = {
data: [],
error: undefined,
loading: true,
vertical: false
};
const initialPie = { data: [], error: undefined, loading: true };
function Demographics(props) {
const { actions, analyze, feedData, id, t } = props;
const isMounted = useIsMounted();
const [barCountriesData, setBarCountriesData] = useState(initialBar);
const [barLanguagesData, setBarLanguagesData] = useState(initialBar);
const [genderData, setGenderData] = useState(initialPie);
useEffect(() => {
if (!id) {
return;
}
// getCountriesData()
getLanguagesData();
getGenderData();
}, []);
useEffect(() => {
if (barCountriesData.data) {
setBarCountriesData((prev) => ({
...prev,
data: {
...prev.data,
xAxis: prev.data.yAxis,
yAxis: prev.data.xAxis
}
}));
}
}, [barCountriesData.vertical]);
useEffect(() => {
if (barLanguagesData.data) {
setBarLanguagesData((prev) => ({
...prev,
data: {
...prev.data,
xAxis: prev.data.yAxis,
yAxis: prev.data.xAxis
}
}));
}
}, [barLanguagesData.vertical]);
function updateResult(foo, id) {
switch (id) {
case cn.first:
// getCountriesData()
return;
case cn.second:
getLanguagesData();
return;
case cn.third:
getGenderData();
return;
default:
return;
}
}
/* Uncomment when country chart shows up
function getCountriesData() {
setBarCountriesData((prev) => ({ ...prev, loading: true }))
getOverviewPieAPI('country', id).then((res) => {
if (!isMounted.current) {
return false
}
if (res.error || !res.data.data) {
// on error
setBarCountriesData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}))
return
}
const { data } = res.data
const barOptions = {}
const errors = {}
Object.entries(data).forEach((feed) => {
const [name, value] = feed
const labels = ['Results']
const datasets = Object.keys(value).map((item) => ({
name: item,
type: 'bar',
data: [value[item]]
}))
if (!datasets || (Array.isArray(datasets) && datasets.length < 1)) {
errors[name] = t('analyzeTab.noData');
}
barOptions[name] = getBarOptions(datasets, labels)
})
setBarCountriesData({
data: barOptions,
error: errors,
loading: false,
vertical: false
})
})
} */
function getLanguagesData() {
setBarLanguagesData((prev) => ({ ...prev, loading: true }));
getOverviewPieAPI('language', id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// on error
setBarLanguagesData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const barOptions = {};
const errors = {};
Object.entries(data).forEach((feed) => {
const [name, value] = feed;
const labels = ['Results'];
const datasets = Object.keys(value).map((item) => ({
name: item,
type: 'bar',
data: [value[item]]
}));
if (!datasets || (Array.isArray(datasets) && datasets.length < 1)) {
errors[name] = t('analyzeTab.noData');
}
barOptions[name] = getBarOptions(datasets, labels);
});
setBarLanguagesData({
data: barOptions,
error: errors,
loading: false,
vertical: false
});
});
}
function getGenderData() {
setGenderData((prev) => ({ ...prev, loading: true }));
getOverviewPieAPI('gender', id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// on error
setGenderData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const pieOptions = {};
const errors = {};
Object.entries(data).forEach((feed) => {
const [name, value] = feed;
if (!value || (Array.isArray(value) && value.length < 1)) {
errors[name] = t('analyzeTab.noData');
}
pieOptions[name] = getPieOptions(
Object.entries(value).map((v) => ({
name: v[0],
value: v[1]
}))
);
});
setGenderData({
data: pieOptions,
error: errors,
loading: false
});
});
}
function changeVertical(name, id) {
name === cn.first
? setBarCountriesData((prev) => ({ ...prev, vertical: !prev.vertical }))
: setBarLanguagesData((prev) => ({ ...prev, vertical: !prev.vertical }));
}
const hideChartAlert = (name, id) =>
analyze.alertCharts.find((v) => v.name === name && v.id === id);
const hideChartPieAlert = (id) =>
analyze.alertCharts.find((v) => v.name === cn.third && v.id === id);
const barchartMenus = (name, id) => [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: name, id }),
showInMore: false,
hide: hideChartAlert(name, id)
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChartAlert(name, id)
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, name),
showInMore: false
},
/* {
title: t('analyzeTab.chartMenus.addToDashboard'),
fn: () => {},
showInMore: true
}, */
{
title: t('analyzeTab.chartMenus.toggleHV'),
fn: () => changeVertical(name, id),
showInMore: true
}
];
const piechartMenus = (id) => [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.third, id }),
showInMore: false,
hide: hideChartPieAlert(id)
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChartPieAlert(id)
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.third),
showInMore: false
}
// { title: t('analyzeTab.chartMenus.addToDashboard'), fn: () => {}, showInMore: true }
];
return (
<Row>
{/* {feedData.feeds.map((feed) => (
<Col key={feed.id} md="6">
<ChartWrapper
title={`${t('analyzeTab.charts.topLanguages')} (${feed.feed})`}
menus={barchartMenus(cn.first, feed.id)}
>
<ECharts
xLabel={barCountriesData.labels}
loading={barCountriesData.loading}
options={barCountriesData.data[feed.feed]}
message={
barCountriesData.error && barCountriesData.error[feed.feed]
}
/>
</ChartWrapper>
</Col>
))} */}
{feedData.feeds.map((feed) => (
<Col key={feed.id} md="6">
<ChartWrapper
title={`${t('analyzeTab.charts.topLanguages')} (${feed.feed})`}
menus={barchartMenus(cn.second, feed.id)}
>
<ECharts
xLabel={barLanguagesData.labels}
loading={barLanguagesData.loading}
options={barLanguagesData.data[feed.feed]}
message={
barLanguagesData.error && barLanguagesData.error[feed.feed]
}
/>
</ChartWrapper>
</Col>
))}
{feedData.feeds.map((feed) => (
<Col key={feed.id} md="6">
<ChartWrapper
title={`${t('analyzeTab.charts.gender')} (${feed.feed})`}
menus={piechartMenus(feed.id)}
>
<ECharts
loading={genderData.loading}
options={genderData.data[feed.feed]}
message={genderData.error && genderData.error[feed.feed]}
/>
</ChartWrapper>
</Col>
))}
</Row>
);
}
const cn = {
first: 'Top Countries',
second: 'Top Languages',
third: 'Gender'
};
Demographics.propTypes = {
t: PropTypes.func.isRequired,
chartData: PropTypes.object,
actions: PropTypes.object,
id: PropTypes.string,
feedData: PropTypes.object,
analyze: PropTypes.object
};
const applyDecorators = compose(
reduxConnect('analyze', ['appState', 'analyze']),
translate(['tabsContent'], { wait: true })
);
export default applyDecorators(React.memo(Demographics));
@@ -0,0 +1,290 @@
/* eslint-disable react/prop-types */
import React, {
useState,
useCallback,
Fragment,
useEffect,
useMemo
} from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { compose } from 'redux';
import { Table } from '../../../../../common/Table/Table';
import { getInfluencersAPI } from '../../../../../../api/analytics/createAnalytics';
import { reduxActions } from '../../../../../../redux/utils/connect';
import {
getQueryParams,
removeHttpsUrl,
capOnlyFirstLetter,
getValidHttpUrl
} from '../../../../../../common/helper';
import i18n from '../../../../../../i18n';
function Influencers(props) {
const [dataSource, setDataSource] = useState(null);
const [loading, setLoading] = useState(true);
const [filter] = useState(filtersNames[1].id);
const { t, actions, id, feedData } = props;
useEffect(() => {
if (!id || !dataSource) {
return;
}
getInfluencers(); //called from table
}, [filter]);
const getDetailsColumns = (id) => {
return id === filtersNames[0].id ? sourceDetails : authorDetails;
};
const authorDetails = useMemo(
() => [
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.rank'),
accessor: 'source_hashcode',
Cell: (row) => (
<div style={{ textAlign: 'center' }}>{row.index + 1}</div>
),
minWidth: 52
},
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.influencers'),
accessor: 'influence',
Cell: (row) =>
getValidHttpUrl(row.value) ? (
<a
target="_blank"
rel="nofollow noopener"
href={getValidHttpUrl(row.value)}
>
{row.original && row.original.author_name}
</a>
) : (
removeHttpsUrl(row.value)
),
minWidth: 130
},
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.sourceType'),
accessor: 'source_type',
Cell: (row) => capOnlyFirstLetter(row.value),
minWidth: 102
}
],
[i18n.language]
);
const sourceDetails = useMemo(
() => [
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.rank'),
accessor: 'source_hashcode',
Cell: (row) => (
<div style={{ textAlign: 'center' }}>{row.index + 1}</div>
),
minWidth: 52
},
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.influencers'),
accessor: 'influence',
Cell: (row) =>
getValidHttpUrl(row.value) ? (
<a
target="_blank"
rel="nofollow noopener"
href={getValidHttpUrl(row.value)}
>
{removeHttpsUrl(row.value)}
</a>
) : (
removeHttpsUrl(row.value)
),
minWidth: 130
},
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.sourceType'),
accessor: 'source_type',
Cell: (row) => capOnlyFirstLetter(row.value),
minWidth: 102
}
],
[i18n.language]
);
const sentimentColumns = useMemo(
() => [
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.total'),
// accessor: d => d.nop.total
accessor: 'totalSentiment',
minWidth: 52,
Cell: (row) => (
<div style={{ textAlign: 'center' }}>{row.value || 0}</div>
)
},
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.positive'),
accessor: 'POSITIVE',
minWidth: 78,
Cell: (row) => (
<div style={{ textAlign: 'center' }}>{row.value || 0}</div>
)
},
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.neutral'),
accessor: 'NEUTRAL',
minWidth: 78,
Cell: (row) => (
<div style={{ textAlign: 'center' }}>{row.value || 0}</div>
)
},
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.negative'),
accessor: 'NEGATIVE',
minWidth: 78,
Cell: (row) => (
<div style={{ textAlign: 'center' }}>{row.value || 0}</div>
)
}
],
[i18n.language]
);
const reachColumns = useMemo(
() => [
/* {
Header: i18n.t('tabsContent:analyzeTab.influencerCols.reach'),
accessor: 'reach',
minWidth: 65,
Cell: (row) => <div style={{ textAlign: 'center' }}>{row.value || 0}</div>
}, */
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.engagement'),
accessor: 'engagement',
minWidth: 105,
Cell: (row) => (
<div style={{ textAlign: 'center' }}>{row.value || 0}</div>
)
}
/* {
Header: i18n.t('tabsContent:analyzeTab.influencerCols.engagementPerMention'),
accessor: 'engagement_per_mention',
Cell: (row) => <div style={{ textAlign: 'center' }}>{row.value || 0}</div>
} */
],
[i18n.language]
);
const columnsList = useMemo(
() => [
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.details'),
headerClassName: 'text-center',
columns: getDetailsColumns(filter)
},
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.sentiments'),
headerClassName: 'text-center',
columns: sentimentColumns
},
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.reach'),
headerClassName: 'text-center',
columns: reachColumns
}
],
[filter, i18n.language]
);
const getInfluencers = useCallback(
(page = 0, pageSize = 10) => {
setLoading(true);
const filterParams = getQueryParams({ page, pageSize });
getInfluencersAPI(id, filter, filterParams).then((res) => {
// if (false) {
if (res.error || res.data === null || !res.data.data) {
setLoading(false);
return actions.addAlert({
type: 'error',
transKey: 'somethingWrong'
});
}
const tableData = {};
res.data.data.forEach((v) => {
tableData[v.name] = v.data;
});
setDataSource(tableData);
setLoading(false);
});
},
[id, filter]
);
return (
<Fragment>
{/* <ButtonGroup size="sm" className="mb-3 d-block text-right">
{filtersNames.map((item) => (
<Button
outline
key={item.id}
title={item.name}
color="secondary"
onClick={function () {
setFilter(item.id)
}}
active={filter === item.id}
>
{item.name}
</Button>
))}
</ButtonGroup> */}
{feedData.feeds.map((feed) => {
let tableData = dataSource;
if (!tableData || !tableData[feed.feed]) {
tableData = { [feed.feed]: [] };
// uncomment for pagination
// tableData[feed.feed] = { data: [], totalCount: 0, limit: 0, page: 0 }
}
const { totalCount = 0, limit = 0, page = 0 } = tableData[feed.feed];
return (
<Table
key={feed.id}
t={t}
cardTitle={`${t('analyzeTab.charts.topInfluencers')} (${
feed.feed
})`}
columns={columnsList}
data={tableData[feed.feed]}
totalCount={totalCount}
showTotalCount
limit={limit}
page={page}
isLoading={loading}
onFetchData={getInfluencers}
/>
);
})}
</Fragment>
);
}
const filtersNames = [
{ name: 'Source', id: 0 },
{ name: 'Author', id: 1 }
];
Influencers.propTypes = {
t: PropTypes.func.isRequired,
feedData: PropTypes.object,
id: PropTypes.string,
actions: PropTypes.object
};
const applyDecorators = compose(
translate(['tabsContent'], { wait: true }),
reduxActions()
);
export default applyDecorators(Influencers);
@@ -0,0 +1,722 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Col, Row } from 'reactstrap';
import ECharts from '../../../../../common/charts/ECharts';
import ChartWrapper from '../ChartWrapper';
import {
getBarOptions,
getPieOptions
} from '../../../../../common/charts/ChartsOptions';
import { IoIosAdd, IoIosRefresh, IoIosCheckmark } from 'react-icons/io';
import reduxConnect from '../../../../../../redux/utils/connect';
import translate from 'react-i18next/dist/commonjs/translate';
import { compose } from 'redux';
import {
getEngagementsAPI,
getEngagementsTimeAPI,
getOverviewBarAPI,
getOverviewPieAPI
} from '../../../../../../api/analytics/createAnalytics';
import useIsMounted from '../../../../../common/hooks/useIsMounted';
function Performance(props) {
const { actions, analyze, feedData, id, t } = props;
const isMounted = useIsMounted();
const [barData, setBarData] = useState({
data: [],
error: undefined,
loading: true,
vertical: false
});
const [engBarData, setEngBarData] = useState({
data: [],
error: undefined,
loading: true,
vertical: false
});
const [potentialBarData, setPotentialBarData] = useState({
data: [],
error: undefined,
loading: true,
vertical: false
});
const [sentimentBar, setSentimentBar] = useState({
data: [],
error: undefined,
loading: true
});
const [pieMentions, setpieMentions] = useState({
data: [],
error: undefined,
loading: true
});
const [pieEng, setpieEng] = useState({
data: [],
error: undefined,
loading: true
});
/* const [pieReach, setpieReach] = useState({
data: [],
error: undefined,
loading: true
}); */
useEffect(() => {
// pass filter
if (!id) {
return;
}
getBarChart();
getEngBarChart();
// getPotentialChart()
getSentimentChart();
getpieMentions();
getpieEngg();
// getpieReach()
}, []);
function updateResult(foo, id) {
switch (id) {
case cn.first:
getBarChart();
return;
case cn.second:
getEngBarChart();
return;
case cn.third:
// getPotentialChart() // Uncomment when API has data
return;
case cn.fourth:
getSentimentChart();
return;
case cn.fifth:
getpieMentions();
return;
case cn.sixth:
getpieEngg();
return;
case cn.seventh:
// getpieReach() // Uncomment when API has data
return;
default:
return;
}
}
useEffect(() => {
if (barData.data) {
setBarData((prev) => ({
...prev,
data: {
...prev.data,
xAxis: prev.data.yAxis,
yAxis: prev.data.xAxis
}
}));
}
}, [barData.vertical]);
useEffect(() => {
if (engBarData.data) {
setEngBarData((prev) => ({
...prev,
data: {
...prev.data,
xAxis: prev.data.yAxis,
yAxis: prev.data.xAxis
}
}));
}
}, [engBarData.vertical]);
useEffect(() => {
if (potentialBarData.data) {
setPotentialBarData((prev) => ({
...prev,
data: {
...prev.data,
xAxis: prev.data.yAxis,
yAxis: prev.data.xAxis
}
}));
}
}, [potentialBarData.vertical]);
function getBarChart() {
setBarData((prev) => ({ ...prev, loading: true }));
getOverviewBarAPI('none', id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// on error
setBarData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const labels = Object.keys(data[0].data);
const datasets = data.map((item) => ({
name: item.name,
type: barData.vertical ? 'bar' : 'line',
smooth: true,
data: Object.values(item.data)
}));
const barOptions = getBarOptions(datasets, labels);
setBarData({
data: barOptions,
error: false,
loading: false,
vertical: false
});
});
}
function getEngBarChart() {
setEngBarData((prev) => ({ ...prev, loading: true }));
getEngagementsTimeAPI(id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// on error
setEngBarData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const labels = Object.keys(data[0].data);
const datasets = data.map((item) => ({
name: item.name,
type: barData.vertical ? 'bar' : 'line',
smooth: true,
data: Object.values(item.data)
}));
const barOptions = getBarOptions(datasets, labels);
setEngBarData({
data: barOptions,
error: false,
loading: false,
vertical: false
});
});
}
/*
function getPotentialChart() {
setPotentialBarData((prev) => ({ ...prev, loading: true }));
getOverviewBarAPI('none', id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// on error
setPotentialBarData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const labels = Object.keys(data);
const datasets = {
name: 'Potential reach over time',
type: potentialBarData.vertical ? 'bar' : 'line',
smooth: true,
data: Object.values(data)
};
const barOptions = getBarOptions(datasets, labels);
setPotentialBarData({
data: barOptions,
error: false,
loading: false,
vertical: false
});
});
} */
function getSentimentChart() {
setSentimentBar((prev) => ({ ...prev, loading: true }));
getOverviewPieAPI('sentiment', id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// on error
setSentimentBar((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const barOptions = {};
Object.keys(data).forEach((feed) => {
const labels = ['Results'];
const datasets = ['POSITIVE', 'NEGATIVE', 'NEUTRAL'].map((item) => ({
name: item,
type: 'bar',
data: [data[feed][item]]
}));
barOptions[feed] = getBarOptions(datasets, labels);
});
setSentimentBar({
data: barOptions,
error: false,
loading: false,
vertical: false
});
});
}
function getpieMentions() {
setpieMentions((prev) => ({ ...prev, loading: true }));
getOverviewPieAPI('none', id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// alert on error
setpieMentions((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const pieOptions = getPieOptions(
Object.entries(data).map((v) => ({ name: v[0], value: v[1] }))
);
setpieMentions({
data: pieOptions,
error: false,
loading: false
});
});
}
function getpieEngg() {
setpieEng((prev) => ({ ...prev, loading: true }));
getEngagementsAPI(id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// alert on error
setpieEng((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
// condition for other filter than 0
const { data } = res.data;
const pieOptions = getPieOptions(
Object.entries(data).map((v) => ({ name: v[0], value: v[1] }))
);
setpieEng({
data: pieOptions,
error: false,
loading: false
});
});
}
/*
function getpieReach() {
setpieReach((prev) => ({ ...prev, loading: true }));
getOverviewPieAPI('none', id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// alert on error
setpieReach((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const pieOptions = getPieOptions(
Object.entries(data).map((v) => ({ name: v[0], value: v[1] }))
);
setpieReach({
data: pieOptions,
error: false,
loading: false
});
});
} */
function changeVertical(chart) {
switch (chart) {
case cn.first:
setBarData((prev) => ({ ...prev, vertical: !prev.vertical }));
return;
case cn.second:
setEngBarData((prev) => ({ ...prev, vertical: !prev.vertical }));
return;
case cn.third:
setPotentialBarData((prev) => ({ ...prev, vertical: !prev.vertical }));
return;
default:
return;
}
}
const hideChart1Alert = analyze.alertCharts.find((v) => v.name === cn.first);
const hideChart2Alert = analyze.alertCharts.find((v) => v.name === cn.second);
// const hideChart3Alert = analyze.alertCharts.find((v) => v.name === cn.third);
const hideChart4Alert = (id) =>
analyze.alertCharts.find((v) => v.name === cn.fourth && v.id === id);
const hideChart5Alert = analyze.alertCharts.find((v) => v.name === cn.fifth);
const hideChart6Alert = analyze.alertCharts.find((v) => v.name === cn.sixth);
/* const hideChart7Alert = analyze.alertCharts.find(
(v) => v.name === cn.seventh
); */
const barchart1Menus = [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.first, id: 'none' }),
showInMore: false,
hide: hideChart1Alert
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart1Alert
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.first),
showInMore: false
},
/* {
title: t('analyzeTab.chartMenus.addToDashboard'),
fn: () => {},
showInMore: true
}, */
{
title: t('analyzeTab.chartMenus.toggleHV'),
fn: () => changeVertical(cn.first),
showInMore: true
}
];
const barchart2Menus = [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.second, id: 'none' }),
showInMore: false,
hide: hideChart2Alert
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart2Alert
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.second),
showInMore: false
},
/* {
title: t('analyzeTab.chartMenus.addToDashboard'),
fn: () => {},
showInMore: true
}, */
{
title: t('analyzeTab.chartMenus.toggleHV'),
fn: () => changeVertical(cn.second),
showInMore: true
}
];
/*
const barchart3Menus = [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.third, id: 'none' }),
showInMore: false,
hide: hideChart3Alert
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart3Alert
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.third),
showInMore: false
},
{
title: t('analyzeTab.chartMenus.addToDashboard'),
fn: () => {},
showInMore: true
},
{
title: t('analyzeTab.chartMenus.toggleHV'),
fn: () => changeVertical(cn.third),
showInMore: true
}
];
*/
function barchart4Menus(id) {
return [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.fourth, id }),
showInMore: false,
hide: hideChart4Alert(id)
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart4Alert(id)
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.fourth, id),
showInMore: false
}
/* {
title: t('analyzeTab.chartMenus.addToDashboard'),
fn: () => {},
showInMore: true
} */
];
}
const pieChart1 = [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.fifth, id: 'none' }),
showInMore: false,
hide: hideChart5Alert
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart5Alert
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.fifth),
showInMore: false
}
// { title: t('analyzeTab.chartMenus.addToDashboard'), fn: () => {}, showInMore: true }
];
const pieChart2 = [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.sixth, id: 'none' }),
showInMore: false,
hide: hideChart6Alert
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart6Alert
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.sixth),
showInMore: false
}
// { title: t('analyzeTab.chartMenus.addToDashboard'), fn: () => {}, showInMore: true }
];
/*
const pieChart3 = [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.seventh, id: 'none' }),
showInMore: false,
hide: hideChart7Alert
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart7Alert
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.seventh),
showInMore: false
}
// { title: t('analyzeTab.chartMenus.addToDashboard'), fn: () => {}, showInMore: true }
];
*/
return (
<Row>
<Col md="8">
<ChartWrapper
title={t('analyzeTab.charts.mentionsOverTime')}
menus={barchart1Menus}
>
<ECharts
xLabel={barData.labels}
loading={barData.loading}
options={barData.data}
/>
</ChartWrapper>
</Col>
<Col md="4">
<ChartWrapper title={t('analyzeTab.charts.mentions')} menus={pieChart1}>
<ECharts
xLabel={pieMentions.labels}
loading={pieMentions.loading}
options={pieMentions.data}
/>
</ChartWrapper>
</Col>
<Col md="8">
<ChartWrapper
title={t('analyzeTab.charts.engagementOverTime')}
menus={barchart2Menus}
>
<ECharts
xLabel={engBarData.labels}
loading={engBarData.loading}
options={engBarData.data}
/>
</ChartWrapper>
</Col>
<Col md="4">
<ChartWrapper
title={t('analyzeTab.charts.engagement')}
menus={pieChart2}
>
<ECharts
xLabel={pieEng.labels}
loading={pieEng.loading}
options={pieEng.data}
/>
</ChartWrapper>
</Col>
{/* <Col md="8">
<ChartWrapper title={t('analyzeTab.charts.potentialReachOverTime')} menus={barchart3Menus}>
<ECharts
xLabel={potentialBarData.labels}
loading={potentialBarData.loading}
options={potentialBarData.data}
/>
</ChartWrapper>
</Col>
<Col md="4">
<ChartWrapper title={t('analyzeTab.charts.potentialReach')} menus={pieChart3}>
<ECharts
xLabel={pieReach.labels}
loading={pieReach.loading}
options={pieReach.data}
/>
</ChartWrapper>
</Col> */}
{feedData.feeds.map((feed) => (
<Col md="12" key={feed.id}>
<ChartWrapper
title={`${t('analyzeTab.charts.proportionofSentiment')} (${
feed.feed
})`}
menus={barchart4Menus(feed.id)}
>
<ECharts
xLabel={sentimentBar.labels}
loading={sentimentBar.loading}
options={sentimentBar.data[feed.feed]}
/>
</ChartWrapper>
</Col>
))}
</Row>
);
}
const cn = {
first: 'Mentions over time',
second: 'Engagement over time',
third: 'Potential reach over time',
fourth: 'Proportion of sentiment',
fifth: 'Mentions',
sixth: 'Engagement',
seventh: 'Potential Reach'
};
Performance.propTypes = {
chartData: PropTypes.object,
actions: PropTypes.object,
feedData: PropTypes.object,
id: PropTypes.string,
analyze: PropTypes.object,
t: PropTypes.func
};
const applyDecorators = compose(
reduxConnect('analyze', ['appState', 'analyze']),
translate(['tabsContent'], { wait: true })
);
export default applyDecorators(React.memo(Performance));
@@ -0,0 +1,403 @@
import React, { Fragment, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Button, ButtonGroup, Col, Row } from 'reactstrap';
import ECharts from '../../../../../common/charts/ECharts';
import ChartWrapper from '../ChartWrapper';
import {
getBarOptions,
getPieOptions
} from '../../../../../common/charts/ChartsOptions';
import { IoIosAdd, IoIosRefresh, IoIosCheckmark } from 'react-icons/io';
import reduxConnect from '../../../../../../redux/utils/connect';
import translate from 'react-i18next/dist/commonjs/translate';
import { compose } from 'redux';
import {
getOverviewBarAPI,
getOverviewPieAPI
} from '../../../../../../api/analytics/createAnalytics';
import useIsMounted from '../../../../../common/hooks/useIsMounted';
const initialBar = {
data: [],
error: undefined,
loading: true,
vertical: false
};
const initialPie = { data: [], error: undefined, loading: true };
function ResultsTab(props) {
const { actions, analyze, feedData, id, t } = props;
const isMounted = useIsMounted();
const [barData, setBarData] = useState(initialBar);
const [barTimeData, setBarTimeData] = useState(initialBar);
const [pieData, setPieData] = useState(initialPie);
const [pieTimeData, setPieTimeData] = useState(initialPie);
const [filter, setFilter] = useState(filtersNames[0].id);
useEffect(() => {
if (!id) {
return;
}
if (filter === filtersNames[0].id) {
getBarChart();
getPieChart();
} else {
getBarChartFeeds();
getPieChartFeeds();
}
}, [filter]);
useEffect(() => {
if (barData.data) {
setBarData((prev) => ({
...prev,
data: {
...prev.data,
xAxis: prev.data.yAxis,
yAxis: prev.data.xAxis
}
}));
}
}, [barData.vertical]);
function updateResult(foo, id) {
switch (id) {
case cn.first:
filter === filtersNames[0].id ? getBarChart() : getBarChartFeeds();
return;
case cn.second:
filter === filtersNames[0].id ? getPieChart() : getPieChartFeeds();
return;
}
}
function getBarChart() {
setBarData((prev) => ({ ...prev, loading: true }));
getOverviewBarAPI(filter, id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// on error
setBarData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const labels = data[0] ? Object.keys(data[0].data) : [];
const datasets = data.map((item) => ({
name: item.name,
type: barData.vertical ? 'bar' : 'line',
smooth: true,
data: Object.values(item.data)
}));
const barOptions = getBarOptions(datasets, labels);
setBarData({
data: barOptions,
error: false,
loading: false,
vertical: false
});
});
}
function getBarChartFeeds() {
setBarTimeData((prev) => ({ ...prev, loading: true }));
getOverviewBarAPI(filter, id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// on error
setBarTimeData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const barOptions = {};
const errors = {};
data.map((feed) => {
const { name, data } = feed;
if (!data || (Array.isArray(data) && data.length < 1)) {
errors[name] = t('analyzeTab.noData');
return;
}
const labels = Object.keys(data[0].data).sort();
const datasets = data.map((item) => ({
name: item.name,
type: barTimeData.vertical ? 'bar' : 'line',
smooth: true,
data: labels.map((v) => item.data[v])
}));
barOptions[name] = getBarOptions(datasets, labels);
});
setBarTimeData({
data: barOptions,
error: errors,
loading: false,
vertical: false
});
});
}
function getPieChart() {
setPieData((prev) => ({ ...prev, loading: true }));
getOverviewPieAPI(filter, id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// alert on error
setPieData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const pieOptions = getPieOptions(
Object.entries(data).map((v) => ({ name: v[0], value: v[1] }))
);
setPieData({
data: pieOptions,
error: false,
loading: false
});
});
}
function getPieChartFeeds() {
setPieTimeData((prev) => ({ ...prev, loading: true }));
getOverviewPieAPI(filter, id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// alert on error
setPieTimeData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const pieOptions = {};
const errors = {};
Object.entries(data).forEach((feed) => {
const [name, value] = feed;
if (!value || (Array.isArray(value) && value.length < 1)) {
errors[name] = t('analyzeTab.noData');
}
pieOptions[name] = getPieOptions(
Object.entries(value).map((v) => ({
name: v[0],
value: v[1]
}))
);
});
setPieTimeData({
data: pieOptions,
error: errors,
loading: false
});
});
}
function changeVertical() {
setBarData((prev) => ({ ...prev, vertical: !prev.vertical }));
}
const hideChart1Alert = (id) =>
analyze.alertCharts.find((v) => v.name === cn.first && v.id === id);
const hideChart2Alert = (id) =>
analyze.alertCharts.find((v) => v.name === cn.second && v.id === id);
const barchartMenus = (id) => [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.first, id }),
showInMore: false,
hide: hideChart1Alert(id)
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart1Alert(id)
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.first),
showInMore: false
},
/* {
title: t('analyzeTab.chartMenus.addToDashboard'),
fn: () => {},
showInMore: true
}, */
{
title: 'Toggle Horizontal/Vertical',
fn: changeVertical,
showInMore: true
}
];
const piechartMenus = (id) => [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.second, id }),
showInMore: false,
hide: hideChart2Alert(id)
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart2Alert(id)
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.second),
showInMore: false
}
// { title: t('analyzeTab.chartMenus.addToDashboard'), fn: () => {}, showInMore: true }
];
return (
<Fragment>
<div className="mask-line overflow-auto white-space-nowrap pl-3 mb-3">
<ButtonGroup size="sm">
{filtersNames.map((item) => (
<Button
outline
key={item.id}
title={item.name}
color="secondary"
onClick={function () {
setFilter(item.id);
}}
active={filter === item.id}
>
{t(`analyzeTab.overviewCharts.${item.transKey}`)}
</Button>
))}
</ButtonGroup>
</div>
{filter === filtersNames[0].id ? ( // feeds in single graph
<Row>
<Col md="8">
<ChartWrapper
title={t('analyzeTab.charts.mentionsOverTime')}
menus={barchartMenus('none')}
>
<ECharts
xLabel={barData.labels}
loading={barData.loading}
options={barData.data}
/>
</ChartWrapper>
</Col>
<Col md="4">
<ChartWrapper
title={t('analyzeTab.charts.mentions')}
menus={piechartMenus('none')}
>
<ECharts loading={pieData.loading} options={pieData.data} />
</ChartWrapper>
</Col>
</Row>
) : (
feedData.feeds.map((feed) => (
<Row key={feed.id}>
<Col md="8">
<ChartWrapper
title={`${t('analyzeTab.charts.mentionsOverTime')} (${
feed.feed
})`}
menus={barchartMenus(feed.id)}
>
<ECharts
xLabel={barTimeData.labels}
loading={barTimeData.loading}
options={barTimeData.data && barTimeData.data[feed.feed]}
message={barTimeData.error && barTimeData.error[feed.feed]}
/>
</ChartWrapper>
</Col>
<Col md="4">
<ChartWrapper
title={`${t('analyzeTab.charts.mentions')} (${feed.feed})`}
menus={piechartMenus(feed.id)}
>
<ECharts
loading={pieTimeData.loading}
options={pieTimeData.data[feed.feed]}
message={pieTimeData.error && pieTimeData.error[feed.feed]}
/>
</ChartWrapper>
</Col>
</Row>
))
)}
</Fragment>
);
}
const cn = {
first: 'Mentions Over Time',
second: 'Share of Mentions'
};
const filtersNames = [
{ name: 'None', transKey: 'none', id: 'none' },
{ name: 'Media Types', transKey: 'mediaTypes', id: 'media' },
{ name: 'Sentiments', transKey: 'sentiments', id: 'sentiment' },
// { name: 'Countries', transKey:'countries', id: 'country' },
{ name: 'Languages', transKey: 'languages', id: 'language' }
];
ResultsTab.propTypes = {
actions: PropTypes.object,
id: PropTypes.string,
t: PropTypes.func,
feedData: PropTypes.object,
analyze: PropTypes.object
};
const applyDecorators = compose(
reduxConnect('analyze', ['appState', 'analyze']),
translate(['tabsContent'], { wait: true })
);
export default applyDecorators(React.memo(ResultsTab));
@@ -0,0 +1,255 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Col, Row } from 'reactstrap';
import ECharts from '../../../../../common/charts/ECharts';
import ChartWrapper from '../ChartWrapper';
import {
getBarOptions,
getPieOptions
} from '../../../../../common/charts/ChartsOptions';
import { IoIosAdd, IoIosRefresh, IoIosCheckmark } from 'react-icons/io';
import reduxConnect from '../../../../../../redux/utils/connect';
import translate from 'react-i18next/dist/commonjs/translate';
import { compose } from 'redux';
import {
getOverviewBarAPI,
getOverviewPieAPI
} from '../../../../../../api/analytics/createAnalytics';
import useIsMounted from '../../../../../common/hooks/useIsMounted';
const initialBar = {
data: [],
error: undefined,
loading: true,
vertical: false
};
const initialPie = { data: [], error: undefined, loading: true };
function Sentiment(props) {
const { actions, analyze, feedData, id, t } = props;
const isMounted = useIsMounted();
const [barData, setBarData] = useState(initialBar);
const [pieData, setPieData] = useState(initialPie);
useEffect(() => {
if (!id) {
return;
}
getBarChart();
getPieChart();
}, []);
useEffect(() => {
if (barData.data) {
setBarData((prev) => ({
...prev,
data: {
...prev.data,
xAxis: prev.data.yAxis,
yAxis: prev.data.xAxis
}
}));
}
}, [barData.vertical]);
function updateResult(foo, id) {
switch (id) {
case cn.first:
getBarChart();
return;
case cn.second:
getPieChart();
return;
}
}
function getBarChart() {
setBarData((prev) => ({ ...prev, loading: true }));
getOverviewBarAPI('sentiment', id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// on error
setBarData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const barOptions = {};
data.forEach((feed) => {
const { name, data } = feed;
const labels = Object.keys(data[0].data).sort();
const datasets = data.map((item) => ({
name: item.name,
type: barData.vertical ? 'bar' : 'line',
smooth: true,
data: labels.map((v) => item.data[v])
}));
barOptions[name] = getBarOptions(datasets, labels);
});
setBarData({
data: barOptions,
error: false,
loading: false,
vertical: false
});
});
}
function getPieChart() {
setPieData((prev) => ({ ...prev, loading: true }));
getOverviewPieAPI('sentiment', id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// alert on error
setPieData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const pieOptions = {};
Object.entries(data).forEach((feed) => {
const [name, value] = feed;
pieOptions[name] = getPieOptions(
Object.entries(value).map((v) => ({
name: v[0],
value: v[1]
}))
);
});
setPieData({
data: pieOptions,
error: false,
loading: false
});
});
}
function changeVertical() {
setBarData((prev) => ({ ...prev, vertical: !prev.vertical }));
}
const hideChart1Alert = (id) =>
analyze.alertCharts.find((v) => v.name === cn.first && v.id === id);
const hideChart2Alert = (id) =>
analyze.alertCharts.find((v) => v.name === cn.second && v.id === id);
const barchartMenus = (id) => [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.first, id }),
showInMore: false,
hide: hideChart1Alert(id)
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart1Alert(id)
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.first),
showInMore: false
},
/* {
title: t('analyzeTab.chartMenus.addToDashboard'),
fn: () => {},
showInMore: true
}, */
{
title: 'Toggle Horizontal/Vertical',
fn: changeVertical,
showInMore: true
}
];
const piechartMenus = (id) => [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.second, id }),
showInMore: false,
hide: hideChart2Alert(id)
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart2Alert(id)
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.second),
showInMore: false
}
// { title: t('analyzeTab.chartMenus.addToDashboard'), fn: () => {}, showInMore: true }
];
return feedData.feeds.map((feed) => (
<Row key={feed.id}>
<Col md="8">
<ChartWrapper
title={`${t('analyzeTab.charts.sentimentOverTime')} (${feed.feed})`}
menus={barchartMenus(feed.id)}
>
<ECharts
xLabel={barData.labels}
loading={barData.loading}
options={barData.data[feed.feed]}
/>
</ChartWrapper>
</Col>
<Col md="4">
<ChartWrapper
title={`${t('analyzeTab.charts.shareofSentiment')} (${feed.feed})`}
menus={piechartMenus(feed.id)}
>
<ECharts
loading={pieData.loading}
options={pieData.data[feed.feed]}
/>
</ChartWrapper>
</Col>
</Row>
));
}
const cn = {
first: 'Sentiment Over Time',
second: 'Share of Sentiment'
};
Sentiment.propTypes = {
actions: PropTypes.object,
feedData: PropTypes.object,
analyze: PropTypes.object,
t: PropTypes.func
};
const applyDecorators = compose(
reduxConnect('analyze', ['appState', 'analyze']),
translate(['tabsContent'], { wait: true })
);
export default applyDecorators(React.memo(Sentiment));
@@ -0,0 +1,284 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Col, Row } from 'reactstrap';
import ECharts from '../../../../../common/charts/ECharts';
import 'echarts-wordcloud';
import { capitalize } from 'lodash';
import ChartWrapper from '../ChartWrapper';
import {
getBarOptions,
PieToolbox,
WordCloudOptions
} from '../../../../../common/charts/ChartsOptions';
import { IoIosAdd, IoIosRefresh, IoIosCheckmark } from 'react-icons/io';
import reduxConnect from '../../../../../../redux/utils/connect';
import translate from 'react-i18next/dist/commonjs/translate';
import { compose } from 'redux';
import {
getThemesCloudAPI,
getThemesTimeAPI
} from '../../../../../../api/analytics/createAnalytics';
import useIsMounted from '../../../../../common/hooks/useIsMounted';
import { capFirstLetter } from '../../../../../../common/helper';
const initialBar = {
data: [],
error: undefined,
loading: true,
vertical: false
};
const initialPie = { data: [], error: undefined, loading: true };
function Themes(props) {
const { actions, analyze, feedData, id, t } = props;
const isMounted = useIsMounted();
const [barData, setBarData] = useState(initialBar);
const [wordData, setWordData] = useState(initialPie);
useEffect(() => {
if (!id) {
return;
}
getBarChart();
getWordCloud();
}, []);
useEffect(() => {
if (barData.data) {
setBarData((prev) => ({
...prev,
data: {
...prev.data,
xAxis: prev.data.yAxis,
yAxis: prev.data.xAxis
}
}));
}
}, [barData.vertical]);
function updateResult(foo, id) {
switch (id) {
case cn.first:
getBarChart();
return;
case cn.second:
getWordCloud();
return;
}
}
function getBarChart() {
setBarData((prev) => ({ ...prev, loading: true }));
getThemesTimeAPI(id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// on error
setBarData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
let labels = null;
const barOptions = {};
const errors = {};
data.forEach((feedData) => {
const { name, data } = feedData;
const datasets = data.map((item) => ({
name: capitalize(item.name),
type: barData.vertical ? 'bar' : 'line',
smooth: true,
data: Object.values(item.data)
}));
if (!labels && data && data[0] && data[0].data) {
labels = Object.keys(data[0].data);
}
barOptions[name] = getBarOptions(datasets, labels);
if (!datasets || (Array.isArray(datasets) && datasets.length < 1)) {
errors[name] = t('analyzeTab.noData');
}
});
setBarData({
data: barOptions,
error: errors,
loading: false,
vertical: false
});
});
}
function getWordCloud() {
setWordData((prev) => ({ ...prev, loading: true }));
getThemesCloudAPI(id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// alert on error
setWordData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const cloudOptions = {};
const errors = {};
data.forEach((feed) => {
const { name, data } = feed;
if (!data || (Array.isArray(data) && data.length < 1)) {
errors[name] = t('analyzeTab.noData');
}
cloudOptions[name] = {
tooltip: {
show: true
},
toolbox: PieToolbox,
series: [
{
...WordCloudOptions,
data: Object.entries(data).map((v) => ({
name: capFirstLetter(v[0]),
value: v[1]
}))
}
]
};
});
setWordData({
data: cloudOptions,
error: false,
loading: false
});
});
}
function changeVertical() {
setBarData((prev) => ({ ...prev, vertical: !prev.vertical }));
}
const hideChart1Alert = (id) =>
analyze.alertCharts.find((v) => v.name === cn.first && v.id === id);
const hideChart2Alert = (id) =>
analyze.alertCharts.find((v) => v.name === cn.second && v.id === id);
const barchartMenus = (id) => [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.first, id }),
showInMore: false,
hide: hideChart1Alert(id)
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart1Alert(id)
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.first),
showInMore: false
},
/* {
title: t('analyzeTab.chartMenus.addToDashboard'),
fn: () => {},
showInMore: true
}, */
{
title: 'Toggle Horizontal/Vertical',
fn: changeVertical,
showInMore: true
}
];
const wordCloudMenus = (id) => [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.second, id }),
showInMore: false,
hide: hideChart2Alert(id)
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart2Alert(id)
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.second),
showInMore: false
}
// { title: t('analyzeTab.chartMenus.addToDashboard'), fn: () => {}, showInMore: true }
];
return feedData.feeds.map((feed) => (
<Row key={feed.id}>
<Col md="8">
<ChartWrapper
title={`${t('analyzeTab.charts.themesOverTime')} (${feed.feed})`}
menus={barchartMenus(feed.id)}
>
<ECharts
xLabel={barData.labels}
loading={barData.loading}
options={barData.data[feed.feed]}
message={barData.error && barData.error[feed.feed]}
/>
</ChartWrapper>
</Col>
<Col md="4">
<ChartWrapper
title={`${t('analyzeTab.charts.topThemes')} (${feed.feed})`}
menus={wordCloudMenus(feed.id)}
>
<ECharts
loading={wordData.loading}
options={wordData.data[feed.feed]}
message={barData.error && barData.error[feed.feed]}
/>
</ChartWrapper>
</Col>
</Row>
));
}
const cn = {
first: 'Themes over time',
second: 'Top Themes'
};
Themes.propTypes = {
chartData: PropTypes.object,
actions: PropTypes.object,
feedData: PropTypes.object,
t: PropTypes.func,
analyze: PropTypes.object
};
const applyDecorators = compose(
reduxConnect('analyze', ['appState', 'analyze']),
translate(['tabsContent'], { wait: true })
);
export default applyDecorators(React.memo(Themes));
@@ -0,0 +1,208 @@
import React, { useEffect, useRef, useState, Fragment } from 'react';
import PropTypes from 'prop-types';
import { Row, Col, ButtonGroup, Button } from 'reactstrap';
import 'leaflet/dist/leaflet.css';
import L from 'leaflet';
import 'leaflet-dvf/dist/leaflet-dvf';
// keep above 3 in sequence
import ChartWrapper from '../ChartWrapper';
import { getWorldMapAPI } from '../../../../../../api/analytics/createAnalytics';
import useIsMounted from '../../../../../common/hooks/useIsMounted';
import { translate } from 'react-i18next';
const initialPie = {
data: [],
error: undefined,
loading: true,
selected: undefined
};
function WorldMap(props) {
const { id, t } = props;
const mapRef = useRef();
const isMounted = useIsMounted();
const [pieData, setPieData] = useState(initialPie);
const [markers, setMarkers] = useState([]);
const feedNames = (pieData.data && Object.keys(pieData.data)) || [];
useEffect(() => {
mapRef.current = L.map('leaflet-map', {
center: [0, 0],
zoom: 2,
layers: [
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
noWrap: true,
attribution:
'&copy; <a target="_blank" noreferrer noopener href="http://osm.org/copyright">OpenStreetMap</a> contributors'
})
]
});
mapRef.current.whenReady(getMapSentiments);
}, []);
useEffect(() => {
const { data, selected, error } = pieData;
const selectedData = data[feedNames[selected]];
const hasErr = error && error[feedNames[selected]];
clearMap();
if (selectedData && !hasErr) {
// loop to add marker
const markersList = [];
selectedData.forEach((data) => {
const [lat, lng] = getLatLong(data.LatLng);
if (!lat || !lng) {
return;
}
let pieChartMarker = new L.PieChartMarker(new L.LatLng(lat, lng), {
...options,
data: {
positive: data.POSITIVE,
negative: data.NEGATIVE,
neutral: data.NEUTRAL
}
});
pieChartMarker.addTo(mapRef.current);
markersList.push(pieChartMarker);
});
// eslint-disable-next-line new-cap
const group = new L.featureGroup(markersList);
mapRef.current.fitBounds(group.getBounds());
setMarkers(markersList);
}
}, [pieData.data, pieData.selected]);
function getLatLong(str) {
const [lat, lng] = str.split(', ');
return [lat && parseFloat(lat), lng && parseFloat(lng)];
}
function clearMap() {
if (mapRef.current) {
markers.forEach((v) => {
mapRef.current.removeLayer(v);
});
}
}
function getMapSentiments() {
setPieData((prev) => ({ ...prev, loading: true }));
getWorldMapAPI(id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// alert on error
setPieData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const dataValues = {};
const errors = {};
data.map((feed) => {
const { name, data } = feed;
if (!data || (Array.isArray(data) && data.length < 1)) {
errors[name] = t('analyzeTab.noData');
}
dataValues[name] = data;
});
setPieData({
data: dataValues,
error: errors,
loading: false,
selected: 0
});
});
}
const style = {
height: 'max(300px, calc(100vh - 200px))'
};
return (
<Row>
<Col md="12">
<ChartWrapper title="Distribution by Sentiments">
<Fragment>
<ButtonGroup size="sm" className="d-block mb-2 text-right">
{feedNames.map((name, i) => (
<Button
outline
key={name}
title={name}
color="secondary"
onClick={function () {
setPieData((prev) => ({
...prev,
selected: i
}));
}}
active={pieData.selected === i}
>
{name}
</Button>
))}
</ButtonGroup>
<div className="position-relative">
<div id="leaflet-map" style={style} />
{pieData.error && pieData.error[feedNames[pieData.selected]] ? (
<div className="no-data" style={{ zIndex: 1000 }}>
{pieData.error[feedNames[pieData.selected]]}
</div>
) : null}
</div>
</Fragment>
</ChartWrapper>
</Col>
</Row>
);
}
const options = {
stroke: false,
fillOpacity: 0.7,
radius: 20,
gradient: false,
chartOptions: {
positive: {
fillColor: '#00FF00',
displayText: function (value) {
return value.toFixed(0);
}
},
negative: {
fillColor: '#FF0000',
displayText: function (value) {
return value.toFixed(0);
}
},
neutral: {
fillColor: '#000000',
displayText: function (value) {
return value.toFixed(0);
}
}
}
// Other L.Path style options
};
WorldMap.propTypes = {
actions: PropTypes.object,
feedData: PropTypes.object,
id: PropTypes.string,
t: PropTypes.func.isRequired,
analyze: PropTypes.object
};
export default translate(['tabsContent'], { wait: true })(WorldMap);
@@ -0,0 +1,17 @@
import Results from './Results'
import Performance from './Performance'
import Influencers from './Influencers'
import Sentiment from './Sentiment'
import Themes from './Themes'
import Demographics from './Demographics'
import WorldMap from './WorldMap'
export {
Results,
Performance,
Influencers,
Sentiment,
Themes,
Demographics,
WorldMap
}
@@ -0,0 +1,58 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
import { deleteAnalytics } from '../../../../../api/analytics/savedAnalytics';
import { translate } from 'react-i18next';
function DeleteDialog(props) {
const [loading, setLoading] = useState(false);
const { actions, data, toggle, fetchData, t } = props;
function handleSubmit() {
setLoading(true);
deleteAnalytics(data.value).then((res) => {
if (res.error) {
res.data
? actions.addAlert(res.data)
: actions.addAlert({ type: 'error', transKey: 'somethingWrong' });
setLoading(false);
return;
}
actions.addAlert({ type: 'notice', transKey: 'analyticsDeleted' });
setLoading(false);
toggle();
fetchData();
});
}
return (
<Modal isOpen={!!data} toggle={toggle} backdrop="static">
<ModalHeader toggle={toggle}>
{t('tabsContent:analyzeTab.deleteAnalysis')}
</ModalHeader>
<ModalBody>
<div>
<p>{t('messages.deleteMessage')}</p>
</div>
</ModalBody>
<ModalFooter>
<Button color="link" onClick={toggle}>
{t('commonWords.Cancel')}
</Button>
<Button color="danger" disabled={loading} onClick={handleSubmit}>
{loading ? t('commonWords.loading') : t('commonWords.Delete')}
</Button>
</ModalFooter>
</Modal>
);
}
DeleteDialog.propTypes = {
toggle: PropTypes.func,
t: PropTypes.func.isRequired,
data: PropTypes.object.isRequired,
fetchData: PropTypes.func,
actions: PropTypes.object
};
export default React.memo(translate(['common'], { wait: true })(DeleteDialog));
@@ -0,0 +1,170 @@
/* eslint-disable react/prop-types */
import React, {
useState,
useCallback,
useMemo,
Fragment,
useEffect
} from 'react';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { compose } from 'redux';
import { Table } from '../../../../common/Table/Table';
import { savedAnalytics } from '../../../../../api/analytics/savedAnalytics';
import reduxConnect from '../../../../../redux/utils/connect';
import {
getDate,
getQueryParams,
setDocumentData
} from '../../../../../common/helper';
import { Button } from 'reactstrap';
import DeleteDialog from './DeleteDialog';
import i18n from '../../../../../i18n';
function SavedAnalysisSubTab(props) {
const [dataSource, setDataSource] = useState({ data: [] });
const [loading, setLoading] = useState(true);
const [deleteValues, setDeleteValues] = useState(false);
const { t, actions } = props;
useEffect(() => {
setDocumentData('title', 'Saved Analysis | Analyze');
return () => {
setDocumentData('title');
};
}, []);
const columns = useMemo(() => {
const columnsList = [
{
id: 'feeds',
Header: t('analyzeTab.savedAnalytics.feeds'),
accessor: (d) => d.context.feeds,
Cell: (props) =>
props.value ? props.value.map((v) => v.name).join(', ') : ''
},
{
id: 'date',
Header: t('analyzeTab.savedAnalytics.dateRange'),
accessor: (d) => d.context.rawFilters.date,
Cell: (props) =>
props.value
? `${getDate(props.value.start, 'MM/DD/YYYY')} to ${getDate(
props.value.end,
'MM/DD/YYYY'
)}`
: '-'
},
{
Header: t('analyzeTab.savedAnalytics.createdAt'),
accessor: 'createdAt',
Cell: (props) => getDate(props.value, 'MM/DD/YYYY')
},
{
Header: t('analyzeTab.savedAnalytics.actions'),
accessor: 'id',
Cell: (props) => getActions(props)
}
];
return columnsList;
}, [getActions, i18n.language]);
const getActions = useCallback((props) => {
return (
<div>
<Button
outline
className="border-0 btn-transition"
color="primary"
size="sm"
tag={Link}
to={`/app/analyze/${props.value}/overview`}
>
{t('analyzeTab.savedAnalytics.view')}
</Button>
<Button
outline
className="border-0 btn-transition"
color="secondary"
tag={Link}
to={`/app/analyze/edit/${props.value}`}
>
{t('analyzeTab.savedAnalytics.edit')}
</Button>
<Button
outline
className="border-0 btn-transition"
color="secondary"
onClick={function () {
setDeleteValues(props);
}}
>
{t('analyzeTab.savedAnalytics.delete')}
</Button>
</div>
);
}, []);
const getSavedList = useCallback(
(page, pageSize) => {
setLoading(true);
const params = getQueryParams({ page, pageSize });
savedAnalytics(params).then((res) => {
if (res.error || res.data === null || !res.data) {
setLoading(false);
return actions.addAlert({
type: 'error',
transKey: 'somethingWrong'
});
}
res.data.length > 0 && setDataSource(res.data[0]);
setLoading(false);
});
},
[savedAnalytics]
);
const { data = [], totalCount = 0, limit = 10, page = 1 } = dataSource;
return (
<Fragment>
<Table
t={t}
cardTitle={t('analyzeTab.savedAnalysis')}
columns={columns}
data={data}
totalCount={totalCount}
showTotalCount
limit={limit}
page={page}
isLoading={loading}
onFetchData={getSavedList}
/>
{deleteValues && (
<DeleteDialog
data={deleteValues}
actions={actions}
toggle={function () {
setDeleteValues(false);
}}
fetchData={function () {
getSavedList(dataSource.page - 1, dataSource.limit);
}}
/>
)}
</Fragment>
);
}
SavedAnalysisSubTab.propTypes = {
t: PropTypes.func.isRequired,
actions: PropTypes.object
};
const applyDecorators = compose(
translate(['tabsContent'], { wait: true }),
reduxConnect()
);
export default applyDecorators(SavedAnalysisSubTab);
@@ -0,0 +1,61 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import { Link } from 'react-router-dom'
import { compose } from 'redux'
import { Card, Col, Row } from 'reactstrap'
class WelcomeSubTab extends React.Component {
render () {
const { t } = this.props
return (
<Card className="py-md-5 mb-3">
<Row className="justify-content-center no-gutters">
<Col sm="6" md="4" xl="4" className="m-4">
<div className="border b-radius-5 text-center p-4">
<div className="icon-wrapper mb-4 rounded-circle">
<div className="icon-wrapper-bg bg-primary" />
<i className="lnr-plus-circle text-primary" />
</div>
<div>
<h5 className="mb-5">{t('analyzeTab.createNewAnalysis')}</h5>
<Link
to="/app/analyze/create"
className="btn btn-primary btn-block fsize-1 btn-lg mr-1"
>
{t('analyzeTab.go')}
</Link>
</div>
</div>
</Col>
<Col sm="6" md="4" xl="4" className="m-4">
<div className="border b-radius-5 text-center p-4">
<div className="icon-wrapper mb-4 rounded-circle">
<div className="icon-wrapper-bg bg-primary" />
<i className="lnr-list text-primary" />
</div>
<div>
<h5 className="mb-5">{t('analyzeTab.viewSavedAnalysis')}</h5>
<Link
to="/app/analyze/saved"
className="btn btn-primary btn-block fsize-1 btn-lg mr-1"
>
{t('analyzeTab.view')}
</Link>
</div>
</div>
</Col>
</Row>
</Card>
)
}
}
WelcomeSubTab.propTypes = {
t: PropTypes.func.isRequired
}
const applyDecorators = compose(translate(['tabsContent'], { wait: true }))
export default applyDecorators(WelcomeSubTab)
@@ -0,0 +1,548 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import TimeAgo from 'timeago-react';
import ArticleComment from './ArticleComment';
import {
UncontrolledDropdown,
DropdownToggle,
DropdownMenu,
DropdownItem,
CustomInput,
Button
} from 'reactstrap';
import ShareMenu from './ShareMenu';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faFacebook,
faInstagram,
faPinterest,
faReddit,
faTumblr,
faTwitter,
faYoutube
} from '@fortawesome/free-brands-svg-icons';
import {
faComments,
faEye,
faFrown,
faMeh,
faQuoteLeft,
faShareAlt,
faSmile,
faThumbsDown,
faThumbsUp
} from '@fortawesome/free-solid-svg-icons';
import {
capOnlyFirstLetter,
convertUTCtoLocal,
abbreviateNumber,
notNullAndUnd
} from '../../../../../common/helper';
import SourceIndexInfoPopup from '../SourceIndexSubTab/SourceIndexInfoPopup';
const icons = {
twitter: faTwitter,
facebook: faFacebook,
instagram: faInstagram,
tumblr: faTumblr,
pinterest: faPinterest,
reddit: faReddit,
youtube: faYoutube,
POSITIVE: faSmile,
NEGATIVE: faFrown,
NEUTRAL: faMeh
};
const colors = {
POSITIVE: '#3ac47d',
NEGATIVE: '#FC3939',
NEUTRAL: '#868e96',
twitter: '#1DA1F2',
facebook: '#4267B2',
reddit: '#FF5700',
instagram: '#8a3ab9',
tumblr: '#34526F',
pinterest: '#E60023',
youtube: '#FF0000'
};
export class Article extends React.Component {
constructor() {
super();
this.state = {
shareMenu: false,
imgErr: false,
sourceModal: false
};
this.elemDesc = React.createRef();
}
selectArticle = () => {
this.props.selectArticle(this.props.article);
};
showEmailPopup = () => {
this.props.showEmailPopup([this.props.article]);
};
showCommentPopup = () => {
this.props.showCommentPopup(this.props.article);
};
showDeletePopup = () => {
this.props.showDeletePopup([this.props.article]);
};
showClipPopup = () => {
this.props.showClipPopup([this.props.article]);
};
toggleShareMenu = () => {
this.setState((prev) => ({ shareMenu: !prev.shareMenu }));
};
loadMoreComments = () => {
const {
loadMoreComments,
article: {
id: articleId,
comments: { count: offset }
}
} = this.props;
loadMoreComments(articleId, offset);
};
readLater = () => {
this.props.readArticleLater(this.props.article);
};
onImgError = () => {
this.setState({ imgErr: true });
};
toggleSourceModal = () => {
this.setState((prev) => ({ sourceModal: !prev.sourceModal }));
};
render() {
const { article, t, i18n, showCommentPopup, deleteComment } = this.props;
let {
comments,
id,
source,
sentiment,
permalink,
publisher,
title,
image,
author,
content,
published,
mentions,
tags,
likes,
dislikes,
views,
shares,
categories
} = article;
const { imgErr } = this.state;
const {
data: commentsData,
count: commentsCount, // should get real post comment count
totalCount: commentsTotalCount
} = comments;
const isArticleChosen = !!this.props.selectedArticles.find(
(item) => item.id === id
);
const offsetWidth =
this.elemDesc &&
this.elemDesc.current &&
this.elemDesc.current.offsetWidth;
const hasRightCounters =
notNullAndUnd(likes) ||
notNullAndUnd(dislikes) ||
commentsCount || // add not null and undefined when counter shows
notNullAndUnd(views) ||
notNullAndUnd(shares) ||
notNullAndUnd(mentions);
const isTwitter = source.siteType === 'twitter';
const isInstagram = source.siteType === 'instagram';
let username;
if (isTwitter) {
username =
author.link &&
author.link.match(
/^https?:\/\/(www\.)?twitter\.com\/(#!\/)?([^\/]+)(\/\w+)*$/
);
username = username && username[3];
}
if (isInstagram) {
username =
author.link &&
author.link.match(
/(?:(?:http|https):\/\/)?(?:www\.)?(?:instagram\.com|instagr\.am)\/([A-Za-z0-9-_\.]+)/
);
username = username && username[1];
}
const isRTL = document.documentElement.dir === 'rtl';
return (
<div className="post border b-radius-5 mb-4">
<UncontrolledDropdown className="post__menu">
<DropdownToggle
outline
color="primary"
className="btn-icon btn-icon-only p-1 m-2"
>
<i className="lnr lnr-menu btn-icon-wrapper" />
</DropdownToggle>
<DropdownMenu className={isRTL ? ' dropdown-menu-left' : ''}>
<DropdownItem
className="text-muted"
onClick={this.showCommentPopup}
>
<i className="mr-2 fa fa-comments"> </i>
<span>{t('searchTab.commentBtn')}</span>
</DropdownItem>
<DropdownItem className="text-muted" onClick={this.showClipPopup}>
<i className="mr-2 fa fa-cut"> </i>
<span>{t('searchTab.clipBtn')}</span>
</DropdownItem>
<DropdownItem className="text-muted" onClick={this.readLater}>
<i className="mr-2 fa fa-bookmark"> </i>
<span>{t('searchTab.readLaterBtn')}</span>
</DropdownItem>
<DropdownItem className="text-muted" onClick={this.readLater}>
<i className="mr-2 fa fa-archive"> </i>
<span>{t('searchTab.archiveBtn')}</span>
</DropdownItem>
<DropdownItem className="text-muted" onClick={this.showEmailPopup}>
<i className="mr-2 fa fa-envelope"> </i>
<span>{t('searchTab.emailBtn')}</span>
</DropdownItem>
<DropdownItem className="text-muted" onClick={this.toggleShareMenu}>
<i className="mr-2 fa fa-share-alt"> </i>
<span>{t('searchTab.shareBtn')}</span>
</DropdownItem>
<DropdownItem className="text-muted" onClick={this.showDeletePopup}>
<i className="mr-2 fa fa-trash"> </i>
<span>{t('searchTab.deleteBtn')}</span>
</DropdownItem>
</DropdownMenu>
</UncontrolledDropdown>
<div className="d-flex flex-row">
<div className="post__icons">
<CustomInput
id={'article-check-' + id}
type="checkbox"
className="mb-3"
onChange={this.selectArticle}
checked={isArticleChosen}
/>
{source.siteType && (
<FontAwesomeIcon
title={capOnlyFirstLetter(source.siteType)}
icon={icons[source.siteType]}
size="lg"
className="fa-w-16 mb-3"
color={colors[source.siteType]}
/>
)}
{sentiment && (
<FontAwesomeIcon
title={capOnlyFirstLetter(sentiment)}
icon={icons[sentiment]}
className="mb-3"
size="lg"
color={colors[sentiment]}
/>
)}
</div>
<div className="post_middlepart">
<h2 className="post__title">
{title && (
<a href={permalink} target="_blank" rel="noopener noreferrer">
{title}
</a>
)}
</h2>
<div
ref={this.elemDesc}
className={`post__content${
offsetWidth && offsetWidth < 430 ? ' flex-column' : ''
}`}
>
{image &&
!imgErr &&
(!title && permalink ? (
<a href={permalink} target="_blank" rel="noopener noreferrer">
<img
id={id}
width="180px"
className="post__img mb-2 mb-lg-0 mr-3"
src={image}
onError={this.onImgError}
/>
</a>
) : (
<img
id={id}
width="180px"
className="post__img mb-2 mb-lg-0 mr-3"
src={image}
onError={this.onImgError}
/>
))}
<div>
{author.name ? (
author.link ? (
<a
className="d-inline-block hover-link text-muted mb-2"
href={author.link}
target="_blank"
>
{username ? `@${username}` : author.name}
</a>
) : (
<p className="text-muted mb-2">{author.name}</p>
)
) : null}
{!title && permalink ? (
<a
href={permalink}
target="_blank"
rel="noopener noreferrer"
className="post__desc-link"
>
<p
className="post__desc"
dangerouslySetInnerHTML={{ __html: content }}
></p>
</a>
) : (
<p
className="post__desc"
dangerouslySetInnerHTML={{ __html: content }}
></p>
)}
</div>
</div>
{tags && tags.length && tags.length > 0 && (
<div className="post__tags mt-2">
<strong>{t('searchTab.tags')}</strong>: {tags.join(', ')}
</div>
)}
{categories && categories.length > 0 && (
<p className="post__tags my-2">
<strong>{t('searchTab.categories')}</strong>:{' '}
{categories.join(', ')}
</p>
)}
<div className="post__about-info text-muted mt-3">
{published && (
<Fragment>
<span
className="d-inline-block"
title={convertUTCtoLocal(published, 'MM/DD/YYYY HH:mm:ss')}
>
<TimeAgo
datetime={published}
locale={i18n.language}
opts={{ minInterval: 60 }}
/>
</span>
<span className="mx-2">|</span>
</Fragment>
)}
{source.type && (
<Fragment>
<span>{capOnlyFirstLetter(source.type)}</span>
<span className="mx-2">|</span>
</Fragment>
)}
{source.country && (
<Fragment>
<span>{source.country}</span>
<span className="mx-2">|</span>
</Fragment>
)}
{publisher && (
<Fragment>
<Button
color="link"
className="btn-anchor"
title="Click to see details"
onClick={this.toggleSourceModal}
>
{publisher}
</Button>
<span className="mx-2">|</span>
</Fragment>
)}
{source.title && (
<Fragment>
{publisher ? (
<a
href={source.link}
style={{ overflowWrap: 'anywhere' }}
rel="noopener noreferrer"
target="_blank"
>
{source.title}
</a>
) : (
<Button
color="link"
className="btn-anchor"
title="Click to see details"
onClick={this.toggleSourceModal}
>
{(isTwitter || isInstagram) && author.name
? author.name
: source.title}
</Button>
)}
</Fragment>
)}
</div>
</div>
{hasRightCounters && (
<div className="post__extras p-3">
<div className="post__icons-wrapper">
{notNullAndUnd(likes) && (
<div className="post__icon-metrics mb-1">
<FontAwesomeIcon
title="Likes"
icon={faThumbsUp}
className="text-success"
/>
<p className="ml-2" title={likes}>
{abbreviateNumber(likes)}
</p>
</div>
)}
{notNullAndUnd(dislikes) && (
<div className="post__icon-metrics mb-1">
<FontAwesomeIcon title="Dislikes" icon={faThumbsDown} />
<p className="ml-2" title={dislikes}>
{abbreviateNumber(dislikes)}
</p>
</div>
)}
{/* {notNullAndUnd(commentsCount) && (
Add above line when real comment counts are visible
*/}
{commentsCount ? (
<div className="post__icon-metrics mb-1">
<FontAwesomeIcon title="Comments" icon={faComments} />
<p className="ml-2" title={commentsCount}>
{abbreviateNumber(commentsCount)}
</p>
</div>
) : (
''
)}
{notNullAndUnd(views) && (
<div className="post__icon-metrics mb-1">
<FontAwesomeIcon title="Viwes" icon={faEye} />
<p className="ml-2 text-center" title={views}>
{abbreviateNumber(views)}
</p>
</div>
)}
{notNullAndUnd(shares) && (
<div className="post__icon-metrics mb-1">
<FontAwesomeIcon title="Shares" icon={faShareAlt} />
<p className="ml-2 text-center" title={shares}>
{abbreviateNumber(shares)}
</p>
</div>
)}
{notNullAndUnd(mentions) && (
<div className="post__icon-metrics mb-1">
<FontAwesomeIcon title="Mentions" icon={faQuoteLeft} />
<p className="ml-2 text-center" title={mentions}>
{abbreviateNumber(mentions)}
</p>
</div>
)}
</div>
</div>
)}
</div>
{commentsData && commentsData.length > 0 && (
<div className="post__comments border-top px-3 pb-3">
{commentsData.map((comment) => {
return (
<ArticleComment
article={article}
comment={comment}
showCommentPopup={showCommentPopup}
deleteComment={deleteComment}
key={comment.id}
/>
);
})}
{commentsCount < commentsTotalCount && (
<Button
outline
size="sm"
color="light"
className="mt-2 d-block ml-auto btn-icon"
onClick={this.loadMoreComments}
>
<i className="lnr lnr-chevron-down btn-icon-wrapper" />{' '}
{t('searchTab.moreComments')}
</Button>
)}
</div>
)}
{this.state.shareMenu && (
<ShareMenu article={article} hideMenu={this.toggleShareMenu} />
)}
{this.state.sourceModal && (
<SourceIndexInfoPopup
source={article.source}
hideSourceInfoPopup={this.toggleSourceModal}
/>
)}
</div>
);
}
}
Article.propTypes = {
article: PropTypes.object.isRequired,
selectedArticles: PropTypes.array.isRequired,
selectArticle: PropTypes.func.isRequired,
showEmailPopup: PropTypes.func.isRequired,
showDeletePopup: PropTypes.func.isRequired,
showCommentPopup: PropTypes.func.isRequired,
showClipPopup: PropTypes.func.isRequired,
deleteComment: PropTypes.func.isRequired,
readArticleLater: PropTypes.func.isRequired,
loadMoreComments: PropTypes.func.isRequired,
showShareMenu: PropTypes.func.isRequired,
i18n: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
};
export default translate(['tabsContent'], { wait: true })(Article);
@@ -0,0 +1,66 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate, Interpolate } from 'react-i18next'
import TimeAgo from 'timeago-react'
import { Button } from 'reactstrap'
export class ArticleComment extends React.Component {
static propTypes = {
article: PropTypes.object.isRequired,
comment: PropTypes.func.isRequired,
deleteComment: PropTypes.func.isRequired,
showCommentPopup: PropTypes.func.isRequired,
i18n: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
}
onEdit = () => {
const { showCommentPopup, article, comment } = this.props
showCommentPopup(article, comment)
}
onDelete = () => {
const { deleteComment, article, comment } = this.props
deleteComment(comment.id, article.id)
}
render() {
const { comment, i18n } = this.props
return (
<div className="post__comment mt-2">
<div className="d-flex justify-content-between">
<div>
<cite className="post__commentor mr-3">
<Interpolate
i18nKey="searchTab.commentMetadata"
author={`${comment.author.firstName} ${comment.author.lastName}`}
/>
</cite>
<span className="post__cmttime mr-3 text-muted">
<TimeAgo
datetime={comment.createdAt}
locale={i18n.language}
opts={{ minInterval: 30 }}
/>
</span>
</div>
<div>
<Button color="link" className="p-0" onClick={this.onEdit}>
<i className="lnr lnr-pencil"></i>
</Button>
<Button color="link" className="ml-2 p-0" onClick={this.onDelete}>
<i className="lnr lnr-trash"></i>
</Button>
</div>
</div>
<p className="post__cmt-content">
<strong className="d-block mb-1">{comment.title}</strong>
{comment.content}
</p>
</div>
)
}
}
export default translate(['tabsContent'], { wait: true })(ArticleComment)
@@ -0,0 +1,90 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import ClipDragSource from './ClipDragSource'
import RecentFeed from './RecentFeed'
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'
export class ClipArticlesPopup extends React.Component {
static propTypes = {
hidePopup: PropTypes.func.isRequired,
clipArticles: PropTypes.func.isRequired,
articles: PropTypes.array.isRequired,
recentClipFeeds: PropTypes.array.isRequired,
getRecentClipFeeds: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
}
hidePopupFromOutside = (e) => {
if (e.target === e.currentTarget) this.hidePopup()
}
hidePopup = () => {
this.props.hidePopup()
}
onSubmit = () => {
this.hidePopup()
}
componentWillMount = () => {
this.props.getRecentClipFeeds()
}
onRecentFeedClick = (feed) => {
this.props.clipArticles(feed.id)
this.props.hidePopup()
}
render() {
const { t, articles, recentClipFeeds } = this.props
return (
<Modal
isOpen
toggle={this.hidePopup}
backdrop={false}
modalClassName="pointer-events-none"
>
<ModalHeader toggle={this.hidePopup}>
{t('searchTab.clipPopup.header')}
</ModalHeader>
<ModalBody>
<div className="text-center">
<p>{t('searchTab.clipPopup.hint1')}</p>
<div className="draggable-container">
<ClipDragSource articles={articles} />
</div>
{recentClipFeeds && recentClipFeeds.length > 0 && (
<div className="mt-2">
<p className="mb-2">{t('searchTab.clipPopup.hint2')}</p>
<div className="d-flex justify-content-center flex-wrap">
{recentClipFeeds.map((feed) => {
return (
<RecentFeed
onRecentFeedClick={this.onRecentFeedClick}
key={feed.id}
feed={feed}
/>
)
})}
</div>
</div>
)}
</div>
</ModalBody>
<ModalFooter>
<Button color="light" onClick={this.hidePopup}>
{t('common:commonWords.Cancel')}
</Button>
</ModalFooter>
</Modal>
)
}
}
export default translate(['tabsContent', 'common'], { wait: true })(
ClipArticlesPopup
)
@@ -0,0 +1,69 @@
import React from 'react'
import PropTypes from 'prop-types'
import { TYPES } from '../../../../../../redux/modules/appState/sidebar'
import { Interpolate } from 'react-i18next'
import { DragSource } from 'react-dnd'
const source = {
beginDrag (props, monitor, component) {
setTimeout(() => {
component.setState({
isDragging: true
})
}, 0)
return {
type: TYPES.CLIP_ARTICLE
}
},
endDrag (props, monitor, component) {
component.setState({
isDragging: false
})
}
}
/**
* Specifies which props to inject into component from Drag n Drop.
*/
function collect (connect) {
return {
// Call this function inside render()
// to let React DnD handle the drag events:
connectDragSource: connect.dragSource()
}
}
export class ClipDragSource extends React.Component {
static propTypes = {
articles: PropTypes.array.isRequired,
connectDragSource: PropTypes.func.isRequired
};
constructor (props) {
super(props)
this.state = {
isDragging: false
}
}
render () {
const style = {
visibility: this.state.isDragging ? 'hidden' : 'visible'
}
return this.props.connectDragSource(
<div className="draggable-item" style={style}>
<span className="drag-handle" />
<Interpolate
i18nKey='searchTab.clipPopup.clippedArticles'
count={this.props.articles.length}
/>
</div>
)
}
}
export default DragSource(TYPES.CLIP_ARTICLE, source, collect)(ClipDragSource)
@@ -0,0 +1,26 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Button } from 'reactstrap'
export default class RecentFeed extends React.Component {
static propTypes = {
feed: PropTypes.object.isRequired,
onRecentFeedClick: PropTypes.func.isRequired
};
onClick = () => {
this.props.onRecentFeedClick(this.props.feed)
}
render () {
const { feed } = this.props
return (
<Button color="light" className={'mr-2 mb-2 feed-icon ' + feed.class} onClick={this.onClick}>
{feed.name}
</Button>
)
}
}
@@ -0,0 +1,139 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate, Interpolate } from 'react-i18next';
import TimeAgo from 'timeago-react';
import {
Button,
Input,
Modal,
ModalBody,
ModalFooter,
ModalHeader
} from 'reactstrap';
const initCharactersCount = 5000;
export class CommentArticlePopup extends React.Component {
static propTypes = {
article: PropTypes.object.isRequired,
comment: PropTypes.object,
commentArticle: PropTypes.func.isRequired,
updateComment: PropTypes.func.isRequired,
hidePopup: PropTypes.func.isRequired,
i18n: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
};
constructor(props) {
super(props);
const content = props.comment ? props.comment.content : '';
this.state = {
charactersCount: initCharactersCount - content.length,
title: props.comment ? props.comment.title : '',
comment: content
};
}
handleTitleChange = (e) => {
const { value } = e.target;
this.setState({ title: value });
};
hidePopup = () => {
this.props.hidePopup();
};
onSubmit = () => {
const newComment = {
title: this.state.title,
content: this.state.comment
};
if (this.props.comment) {
//edit exisitng
this.props.updateComment(newComment, this.props.article.id);
} else {
//create new comment
this.props.commentArticle(newComment, this.props.article.id);
}
this.hidePopup();
};
onChangeComment = (e) => {
const charactersCount = initCharactersCount - e.target.value.length;
if (charactersCount >= 0) {
this.setState({
charactersCount: charactersCount,
comment: e.target.value
});
}
};
render() {
const { t, i18n, article, comment } = this.props;
const popupTitle = comment
? t('searchTab.commentPopup.editUserComment')
: t('searchTab.commentPopup.addUserComment');
return (
<Modal isOpen toggle={this.hidePopup} backdrop="static">
<ModalHeader toggle={this.hidePopup}>{popupTitle}</ModalHeader>
<ModalBody>
<div className="mb-3">
<a
className="font-size-lg"
href={article.permalink}
target="_blank"
rel="noopener noreferrer"
>
{article.title}
</a>
<p>{article.author.name}</p>
<p className="font-size-xs text-muted">
<TimeAgo
datetime={article.published}
locale={i18n.language}
opts={{ minInterval: 30 }}
/>
</p>
</div>
<Input
value={this.state.title}
type="text"
className="mb-2"
onChange={this.handleTitleChange}
placeholder={t('searchTab.commentPopup.inputTitlePlaceholder')}
/>
<Input
rows="3"
type="textarea"
value={this.state.comment}
onChange={this.onChangeComment}
placeholder={t('searchTab.commentPopup.commentPlanceholder')}
/>
<p className="font-size-xs text-muted text-right mt-1">
<Interpolate
i18nKey="searchTab.commentPopup.charactersLeft"
count={this.state.charactersCount}
/>
</p>
</ModalBody>
<ModalFooter>
<Button color="light" onClick={this.hidePopup}>
{t('common:commonWords.Cancel')}
</Button>
<Button color="primary" onClick={this.onSubmit}>
{t('common:commonWords.submit')}
</Button>
</ModalFooter>
</Modal>
);
}
}
export default translate(['tabsContent', 'common'], { wait: true })(
CommentArticlePopup
);
@@ -0,0 +1,65 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Interpolate, translate } from 'react-i18next';
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
export class DeleteArticlesPopup extends React.Component {
static propTypes = {
articles: PropTypes.array.isRequired,
activeFeed: PropTypes.object,
hidePopup: PropTypes.func.isRequired,
deleteArticles: PropTypes.func.isRequired,
deleteArticlesFromFeed: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
onSubmit = () => {
const {
articles,
activeFeed,
deleteArticles,
deleteArticlesFromFeed,
hidePopup
} = this.props;
const ids = articles.map((a) => a.id);
if (activeFeed) {
deleteArticlesFromFeed(ids, activeFeed.id);
} else {
deleteArticles(ids);
}
hidePopup();
};
render() {
const { t, articles, hidePopup } = this.props;
return (
<Modal isOpen toggle={hidePopup} backdrop="static">
<ModalHeader toggle={hidePopup}>{t('commonWords.Confirm')}</ModalHeader>
<ModalBody>
<p>
{articles.length > 1 ? (
<Interpolate
t={t}
i18nKey="tabsContent:searchTab.deleteArticlePopupText_plural"
articlesLength={articles.length}
/>
) : (
t('tabsContent:searchTab.deleteArticlePopupText')
)}
</p>
</ModalBody>
<ModalFooter>
<Button color="light" onClick={hidePopup}>
{t('commonWords.Cancel')}
</Button>
<Button color="danger" onClick={this.onSubmit}>
{t('commonWords.Delete')}
</Button>
</ModalFooter>
</Modal>
);
}
}
export default translate(['common'], { wait: true })(DeleteArticlesPopup);
@@ -0,0 +1,209 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import moment from 'moment'
import Select from 'react-select'
import {
Button,
Modal,
ModalHeader,
ModalBody,
Label,
Input,
ModalFooter,
FormGroup,
Col,
Container
} from 'reactstrap'
import QuillEditor from '../../../../common/QuillEditor'
const replyToEmail = 'support@socialhose.io'
export class EmailArticlesPopup extends React.Component {
static propTypes = {
articlesToEmail: PropTypes.array.isRequired,
emailArticles: PropTypes.func.isRequired,
hidePopup: PropTypes.func.isRequired,
recipients: PropTypes.object.isRequired,
loadRecipients: PropTypes.func.isRequired,
children: PropTypes.any,
t: PropTypes.func.isRequired
}
constructor(props) {
super(props)
this.state = {
selectedRecipients: ''
}
this.editorRef = React.createRef()
}
componentWillMount = () => {
!this.props.recipients.all.length && this.props.loadRecipients()
}
componentDidMount = () => {
this.props.loadRecipients()
}
hidePopup = () => {
this.props.hidePopup()
}
collectParams = () => { // need to change with states
const recipients = this.state.selectedRecipients
if (!recipients) return false
return {
emailTo: recipients.map((r) => r.value),
emailReplyTo: document.getElementById('email-reply-to').value,
subject: document.getElementById('email-subject').value,
content: this.editorRef.current && this.editorRef.current.root.innerHTML
}
}
onSubmit = () => {
const params = this.collectParams()
if (params) {
this.props.emailArticles(params)
}
}
changeRecipient = (value) => {
this.setState({
selectedRecipients: value
})
}
validEmails = (str) => {
const re = /\S+@\S+\.\S+/
const arr = str.split(',')
for (let s of arr) {
if (!re.test(s)) {
return false
}
}
return true
}
emailRe = /\S+@\S+\.\S+/
isValidNewOption = ({ label }) => {
return this.emailRe.test(label)
}
promptTextCreator = (label) => {
return label
}
render() {
const { t, articlesToEmail, recipients } = this.props
const { selectedRecipients } = this.state
const recipientsAll = recipients.all.map((recipient) => ({
value: recipient,
label: recipient
}))
return (
<Modal
isOpen
size="lg"
toggle={this.hidePopup}
backdrop="static"
>
<ModalHeader toggle={this.hidePopup}>
{t('searchTab.emailPopup.header')}
</ModalHeader>
<ModalBody>
<Container>
<FormGroup row>
<Label htmlFor="email-to" sm={2}>
{t('searchTab.emailPopup.labelTo')}
</Label>
<Col sm={10}>
{recipients.pending && <i className="fa fa-spinner fa-pulse m-2" />}
{!recipients.pending && (
<Select.Creatable
multi
value={selectedRecipients}
options={recipientsAll}
onChange={this.changeRecipient}
isValidNewOption={this.isValidNewOption}
promptTextCreator={this.promptTextCreator}
noResultsText="Email not valid"
/>
)}
</Col>
</FormGroup>
<FormGroup row>
<Label htmlFor="email-reply-to" sm={2}>
{t('searchTab.emailPopup.labelReplyTo')}
</Label>
<Col sm={10}>
<Input
type="email"
id="email-reply-to"
defaultValue={replyToEmail}
/>
</Col>
</FormGroup>
<FormGroup row>
<Label htmlFor="email-subject" sm={2}>
{t('searchTab.emailPopup.labelSubject')}
</Label>
<Col sm={10}>
<Input type="text" id="email-subject" />
</Col>
</FormGroup>
<div className="email-popup">
<QuillEditor
className="email-popup__articles email-editor"
reference={this.editorRef}
id="email-editor"
>
{articlesToEmail.map((article) => {
return (
<div className="email-popup__article" key={article.id}>
<h2 className="article__title">
<a href={article.source.link}>{article.title}</a>
</h2>
<div className="article__about-info">
<a href={article.source.link} target="blank">
{article.source.title}
</a>{' '}
<span> | </span>
<a href={article.author.link} target="blank">
{article.author.name}
</a>{' '}
<span> | </span>
{moment(article.published).format('LLL')}
</div>
<p className="article__desc">{article.content}</p>
</div>
)
})}
</QuillEditor>
</div>
</Container>
{this.props.children}
</ModalBody>
<ModalFooter>
<Button color="light" onClick={this.hidePopup}>
{t('common:commonWords.Cancel')}
</Button>
<Button color="primary" onClick={this.onSubmit}>
{t('searchTab.emailPopup.submitBtn')}
</Button>
</ModalFooter>
</Modal>
)
}
}
export default translate(['tabsContent', 'common'], { wait: true })(
EmailArticlesPopup
)
@@ -0,0 +1,50 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'
class EmailConfirmPopup extends React.Component {
static propTypes = {
hidePopup: PropTypes.func.isRequired,
hideEmailPopup: PropTypes.func.isRequired,
sendDocumentsByEmail: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
}
hidePopup = () => {
this.props.hidePopup()
}
onSubmit = () => {
this.props.sendDocumentsByEmail()
this.hidePopup()
this.props.hideEmailPopup()
}
render() {
const { t } = this.props
return (
<Modal isOpen toggle={this.hidePopup} backdrop="static">
<ModalHeader toggle={this.hidePopup}>
{t('common:commonWords.Confirm')}
</ModalHeader>
<ModalBody>
<p>{t('searchTab.emailPopup.sendConfirmWithoutSubject')}</p>
</ModalBody>
<ModalFooter>
<Button color="light" onClick={this.hidePopup}>
{t('searchTab.emailPopup.dontSend')}
</Button>
<Button color="warning" onClick={this.onSubmit}>
{t('searchTab.emailPopup.sendAnyway')}
</Button>
</ModalFooter>
</Modal>
)
}
}
export default translate(['tabsContent', 'common'], { wait: true })(
EmailConfirmPopup
)
@@ -0,0 +1,175 @@
/* eslint-disable react/jsx-no-bind */
import React, { Fragment, useState } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import { translate } from 'react-i18next';
import SearchDatesPopup from './SearchDatesPopup';
import { Modal, Button, ModalHeader, ModalBody } from 'reactstrap';
import { IoIosCalendar } from 'react-icons/io';
// previous commented code
// componentWillMount = () => {
// const { actions, userSubscription } = this.props;
// actions.setSearchLastDate(userSubscription);
// };
export function MediaTypes(props) {
const [modal, setModal] = useState(false);
const {
t,
mediaTypes,
actions,
chosenMediaTypes,
toggleMediaType,
toggleAllMediaTypes,
restrictions
} = props;
const allSelected = mediaTypes.length === chosenMediaTypes.length;
function toggle() {
setModal((modal) => !modal);
}
// set only the allowed media types from restrictions initially
function allowPermissions(mediaType) {
if (!restrictions || !restrictions.plans) {
return false;
}
// for selecting all
if (!mediaType) {
return mediaTypes.every((mt) => restrictions.plans[mt]);
}
return restrictions.plans[mediaType];
}
function toggleSingleType(mediaType, value) {
/* const isFree = restrictions.plans.price === 0;
// TODO: remove following restrictions when duplication fixes
const restrictedTemporary =
isFree && ['news', 'blogs'].includes(mediaType) && value;
if (!allowPermissions(mediaType) || restrictedTemporary) { */
if (!allowPermissions(mediaType)) {
return actions.toggleUpgradeModal();
}
toggleMediaType(mediaType, value); // restrict condition
}
function toggleAllTypes() {
// TODO: remove following restrictions when duplication fixes
/* const isFree = restrictions.plans.price === 0;
if (!allowPermissions() || isFree) { */
if (!allowPermissions()) {
return actions.toggleUpgradeModal();
}
toggleAllMediaTypes(!allSelected);
}
/*
const {
chosenSearchDate,
chosenSearchInterval
chosenStartDate,
chosenEndDate
} = props.searchByFiltersState
const isIntervalBetween = chosenSearchInterval === 'between';
const searchDateBtnText = isIntervalBetween &&
chosenStartDate !== '' ||
isIntervalBetween &&
chosenEndDate !== ''
? chosenSearchDate : t('searchTab.userSubscription.' + chosenSearchDate);
*/
return (
<Fragment>
<div className="d-flex justify-content-between align-items-start">
<div data-tour="select-media-types">
<Button
outline
size="sm"
title={allSelected ? 'Click to deselect' : 'Click to select'}
className="btn-pill mb-2 mr-2 px-3"
color={cx('light', { active: allSelected })}
onClick={toggleAllTypes}
>
{t('searchTab.sourceTypes.all')}
</Button>
{mediaTypes.map((mediaType, i) => {
const isMediaTypeChosen =
chosenMediaTypes.indexOf(mediaType) !== -1;
return (
<Button
key={mediaType}
outline
size="sm"
title={
isMediaTypeChosen ? 'Click to deselect' : 'Click to select'
}
className="btn-pill mb-2 mr-2 px-3"
color={cx('light', {
active: isMediaTypeChosen
})}
onClick={() => toggleSingleType(mediaType, !isMediaTypeChosen)}
>
{t('searchTab.sourceTypes.' + mediaType)}
</Button>
);
})}
</div>
<Button
color="link"
className="ml-2"
onClick={toggle}
data-tour="select-date-range"
>
<IoIosCalendar fontSize="24px" />
{/* {t('searchTab.datesRange')} */}
</Button>
</div>
<Modal isOpen={modal} toggle={toggle} data-tour="date-range-modal">
<ModalHeader toggle={toggle}>Select dates</ModalHeader>
<ModalBody>
<SearchDatesPopup
outsideClickIgnoreClass="react-datepicker"
userSubscription={props.userSubscription}
userSubscriptionDate={props.userSubscriptionDate}
searchIntervals={props.searchByFiltersState.searchIntervals}
searchLastDates={props.searchByFiltersState.searchLastDates}
chosenSearchInterval={
props.searchByFiltersState.chosenSearchInterval
}
chosenSearchLastDate={
props.searchByFiltersState.chosenSearchLastDate
}
chosenStartDate={props.searchByFiltersState.chosenStartDate}
chosenEndDate={props.searchByFiltersState.chosenEndDate}
hideSearchDatesPopup={toggle}
setSearchInterval={actions.setSearchInterval}
setSearchLastDate={actions.setSearchLastDate}
setSearchDate={actions.setSearchDate}
setStartDate={actions.setStartDate}
setEndDate={actions.setEndDate}
/>
</ModalBody>
</Modal>
</Fragment>
);
}
MediaTypes.propTypes = {
t: PropTypes.func.isRequired,
mediaTypes: PropTypes.array.isRequired,
chosenMediaTypes: PropTypes.array.isRequired,
toggleMediaType: PropTypes.func.isRequired,
toggleAllMediaTypes: PropTypes.func.isRequired,
restrictions: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,
userSubscriptionDate: PropTypes.string.isRequired,
userSubscription: PropTypes.string.isRequired,
searchByFiltersState: PropTypes.object.isRequired
};
export default translate(['tabsContent'], { wait: true })(MediaTypes);
@@ -0,0 +1,94 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import FiltersTable from '../../../../common/FiltersTable/FiltersTable'
import { Button } from 'reactstrap'
export class RefinePanel extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
advancedFilters: PropTypes.object.isRequired,
selectedFilters: PropTypes.object.isRequired,
clearPending: PropTypes.object.isRequired,
filterPages: PropTypes.object.isRequired,
onRefine: PropTypes.func.isRequired,
actions: PropTypes.object.isRequired
};
onHiderClick = (e) => {
e.preventDefault()
this.props.actions.toggleRefinePanel()
};
onSelectFilter = (groupName, filterValue) => {
this.props.actions.selectRefineFilter(groupName, filterValue)
};
onClearFilters = (groupName) => {
this.props.actions.clearRefineFilters(groupName)
};
onClearAllFilters = () => {
this.props.actions.clearAllRefineFilters()
};
onMoreFilters = (groupName) => {
this.props.actions.loadMoreRefineFilters(groupName)
};
onLessFilters = (groupName) => {
this.props.actions.loadLessRefineFilters(groupName)
};
/* onPressEnter = (e) => {
if (e.keyCode === 13) {
const keyword = document.getElementById('refine-keyword').value
this.props.actions.selectRefineFilter('keyword', keyword)
setTimeout(() => {
this.props.onRefine()
})
}
}; */
render () {
return (
<div className="refine-panel px-4">
<Button
color="light"
title="Hide refine panel"
className="d-block ml-auto mb-3 btn-icon"
onClick={this.onHiderClick}
>
{this.props.t('searchTab.hide')}
</Button>
{/* <Input
type="text"
className="mb-2"
id="refine-keyword"
placeholder={this.props.t('common:advancedFilters.keywordRefine')}
onKeyUp={this.onPressEnter}
/> */}
<FiltersTable
filters={this.props.advancedFilters}
selectedFilters={this.props.selectedFilters}
clearPending={this.props.clearPending}
pages={this.props.filterPages}
callbacks={{
'selectFilter': this.onSelectFilter,
'clearFilters': this.onClearFilters,
'clearAllFilters': this.onClearAllFilters,
'moreFilters': this.onMoreFilters,
'lessFilters': this.onLessFilters,
'refine': this.props.onRefine
}}
/>
</div>
)
}
}
export default translate(['tabsContent'], { wait: true })(RefinePanel)
@@ -0,0 +1,154 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import {
Button,
Modal,
ModalHeader,
ModalBody,
Label,
Input,
ModalFooter,
FormGroup
} from 'reactstrap'
export class SaveFeedPopup extends React.Component {
static propTypes = {
feedCategories: PropTypes.array.isRequired,
saveType: PropTypes.string.isRequired,
toggleSaveFeedPopup: PropTypes.func.isRequired,
addAlert: PropTypes.func.isRequired,
onSaveAsFeed: PropTypes.func.isRequired,
getSidebarCategories: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
}
constructor(props) {
super(props)
this.state = {
isFeedNameError: false,
feedCategoriesKeys: [],
feedName: '',
selectCategory: ''
}
}
componentWillMount = () => {
let nestingCount = -1
this.getCategoriesKeys(this.props.feedCategories, nestingCount)
}
//function that generates new array of categories without nesting
getCategoriesKeys = (categories, nestingCount) => {
nestingCount += 1
categories.forEach((category) => {
if (category.subType === 'deleted_content') return false
const categoryName = '-'.repeat(nestingCount) + ' ' + category.name
const feedCategoriesKeys = this.state.feedCategoriesKeys
feedCategoriesKeys.push({ id: category.id, name: categoryName })
this.setState({
feedCategoriesKeys: feedCategoriesKeys,
selectCategory: feedCategoriesKeys[0].id.toString()
})
if (category.childes.length) {
this.getCategoriesKeys(category.childes, nestingCount)
}
})
}
changeHandler = (e) => {
const { name, value } = e.target
this.setState({ [name]: value })
}
hidePopupFromOutside = (e) => {
if (e.target === e.currentTarget) this.hidePopup()
}
hidePopup = () => {
this.props.toggleSaveFeedPopup()
}
onSubmit = () => {
const { feedName: name, selectCategory: category } = this.state
if (!name || !name.trim()) {
this.setState({ isFeedNameError: true })
return false
}
this.props.onSaveAsFeed(name, category)
this.hidePopup()
}
render() {
const { t } = this.props
const {
feedCategoriesKeys,
isFeedNameError,
feedName,
selectCategory
} = this.state
return (
<Modal isOpen toggle={this.hidePopup} backdrop="static" data-tour="feed-save-modal">
<ModalHeader toggle={this.hidePopup}>
{t('searchTab.saveFeedPopup.' + this.props.saveType)}
</ModalHeader>
<ModalBody>
<FormGroup>
<Label>
{t('searchTab.saveFeedPopup.nameLabel')}<span className="text-danger">*</span>
</Label>
<Input
name="feedName"
type="text"
value={feedName}
onChange={this.changeHandler}
/>
{isFeedNameError && (
<p className="text-danger">
{t('searchTab.saveFeedPopup.feedNameErrorMsg')}
</p>
)}
</FormGroup>
<FormGroup>
<Label>
{t('searchTab.saveFeedPopup.folderLabel')}<span className="text-danger">*</span>
</Label>
<Input
name="selectCategory"
type="select"
value={selectCategory}
onChange={this.changeHandler}
>
{feedCategoriesKeys.map((category) => {
return (
<option key={category.id} value={category.id}>
{category.name}
</option>
)
})}
</Input>
</FormGroup>
</ModalBody>
<ModalFooter>
<Button color="light" onClick={this.hidePopup}>
{t('common:commonWords.Cancel')}
</Button>
<Button color="primary" onClick={this.onSubmit}>
{t('searchTab.saveBtn')}
</Button>
</ModalFooter>
</Modal>
)
}
}
export default translate(['tabsContent', 'common'], { wait: true })(
SaveFeedPopup
)

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