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
+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
)
@@ -0,0 +1,118 @@
import React from 'react'
import PropTypes from 'prop-types'
import { DateRangePicker } from 'react-dates'
import moment from 'moment'
import { getMomentObject } from '../../../../../../common/helper'
export class BetweenDatepickers extends React.Component {
state = {}
static propTypes = {
chosenSearchInterval: PropTypes.string.isRequired,
chosenStartDate: PropTypes.string.isRequired,
chosenEndDate: PropTypes.string.isRequired,
setSearchInterval: PropTypes.func.isRequired,
setSearchDate: PropTypes.func.isRequired,
setStartDate: PropTypes.func.isRequired,
minDate: PropTypes.object,
setEndDate: PropTypes.func.isRequired
}
swapDate = (startDate, endDate) => {
if (startDate.isAfter(endDate)) {
const temp = startDate
startDate = endDate
endDate = temp
}
return { startDate, endDate }
}
/*
setDates = (date, isStartDate) => {
const {
chosenStartDate,
chosenEndDate,
setStartDate,
setEndDate,
setSearchDate
} = this.props
const hasStartDate = !!chosenStartDate
const hasEndDate = !!chosenEndDate
let startDate = hasStartDate ? moment(chosenStartDate) : moment()
let endDate = hasEndDate ? moment(chosenEndDate) : moment()
startDate = isStartDate ? date : startDate
endDate = !isStartDate ? date : endDate
const swappedDate = this.swapDate(startDate, endDate)
startDate = swappedDate.startDate.format('YYYY-MM-DD')
endDate = swappedDate.endDate.format('YYYY-MM-DD')
setStartDate(startDate.format('YYYY-MM-DD'))
setEndDate(endDate.format('YYYY-MM-DD'))
const endDateLabel = hasEndDate ? endDate : 'now'
const startDateLabel = hasStartDate ? startDate : 'until'
let label = isStartDate
? `${startDate} - ${endDateLabel}`
: `${startDateLabel} - ${endDate}`
setSearchDate(label)
} */
setBetweenInterval = () => {
const { chosenSearchInterval, setSearchInterval } = this.props
if (chosenSearchInterval === 'between') return false
setSearchInterval('between')
}
handleDateChange = ({ startDate, endDate }) => {
const { setStartDate, setEndDate } = this.props
setStartDate(startDate ? startDate.format('YYYY-MM-DD') : null)
setEndDate(endDate ? endDate.format('YYYY-MM-DD') : null)
if (startDate && endDate) {
this.setBetweenInterval()
}
}
onFocusChange = (focus) => {
this.setState({ focusedInput: focus })
}
isOutsideRange = (date) => {
const today = moment()
return date.isAfter(today) || date.isBefore(this.props.minDate)
}
render() {
const { chosenStartDate, chosenEndDate } = this.props
const today = moment()
const startDate = getMomentObject(chosenStartDate)
const endDate = getMomentObject(chosenEndDate)
return (
<div className="ml-3">
<DateRangePicker
startDateId="startDate"
endDateId="endDate"
startDate={startDate}
endDate={endDate}
onDatesChange={this.handleDateChange}
focusedInput={this.state.focusedInput}
onFocusChange={this.onFocusChange}
displayFormat="MM/DD/YYYY"
startDatePlaceholderText="Start Date"
endDatePlaceholderText="End Date"
numberOfMonths={1}
maxDate={today}
// eslint-disable-next-line react/jsx-no-bind
isOutsideRange={this.isOutsideRange}
/>
</div>
)
}
}
export default BetweenDatepickers
@@ -0,0 +1,33 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { Col, CustomInput, FormGroup } from 'reactstrap';
export class DuplicatesTab extends React.Component {
static propTypes = {
includeDuplicates: PropTypes.bool.isRequired,
toggleIncludeDuplicates: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
render() {
const { t } = this.props;
return (
<Col sm={12}>
<FormGroup>
<CustomInput
className="checkbox-input-hidden"
type="checkbox"
id="duplicates-check"
checked={this.props.includeDuplicates}
onChange={this.props.toggleIncludeDuplicates}
label={t('searchTab.searchBySection.duplicates.includeDuplicates')}
/>
</FormGroup>
</Col>
);
}
}
export default translate(['tabsContent'], { wait: true })(DuplicatesTab);
@@ -0,0 +1,53 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { Col, FormGroup, Input, Label } from 'reactstrap';
export class EmphasisTab extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
include: PropTypes.string.isRequired,
exclude: PropTypes.string.isRequired,
setHeadlineIncluded: PropTypes.func.isRequired,
setHeadlineExcluded: PropTypes.func.isRequired
};
setHeadInclude = (e) => {
const headline = e.target.value;
this.props.setHeadlineIncluded(headline);
};
setHeadExclude = (e) => {
const headline = e.target.value;
this.props.setHeadlineExcluded(headline);
};
render() {
const { t, include, exclude } = this.props;
return (
<Fragment>
<Col sm="6">
<FormGroup>
<Label>
{t('searchTab.searchBySection.emphasis.headlineLabel')}{' '}
{t('searchTab.searchBySection.emphasis.include')}
</Label>
<Input type="text" value={include} onChange={this.setHeadInclude} />
</FormGroup>
</Col>
<Col sm="6">
<FormGroup>
<Label>
{t('searchTab.searchBySection.emphasis.headlineLabel')}{' '}
{t('searchTab.searchBySection.emphasis.exclude')}
</Label>
<Input type="text" value={exclude} onChange={this.setHeadExclude} />
</FormGroup>
</Col>
</Fragment>
);
}
}
export default translate(['tabsContent'], { wait: true })(EmphasisTab);
@@ -0,0 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { Col, CustomInput, FormGroup } from 'reactstrap';
function ExtrasTab({ t, hasImages, toggleHasImages }) {
return (
<Col sm={12}>
<FormGroup>
<CustomInput
id="has-images-check"
type="checkbox"
className="d-flex"
checked={hasImages}
label={t('searchTab.searchBySection.extras.hasImages')}
onChange={toggleHasImages}
/>
</FormGroup>
</Col>
);
}
ExtrasTab.propTypes = {
hasImages: PropTypes.bool.isRequired,
toggleHasImages: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
export default translate(['tabsContent'], { wait: true })(ExtrasTab);
@@ -0,0 +1,51 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { Col, CustomInput } from 'reactstrap';
export class LangsTab extends React.Component {
static propTypes = {
chosenLanguages: PropTypes.array.isRequired,
searchLanguages: PropTypes.array.isRequired,
toggleLang: PropTypes.func.isRequired,
toggleAllLangs: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
toggleLangs = ({ target: { id, checked } }) => {
this.props.toggleLang(id, checked);
};
toggleAllLangs = (e) => {
this.props.toggleAllLangs(e.target.checked);
};
render() {
const { t } = this.props;
const { searchLanguages, chosenLanguages } = this.props;
return (
<Col sm={12} className="search-by-lang">
<CustomInput
id="article-check-all"
type="checkbox"
label={t('common:language.all')}
checked={searchLanguages.length === chosenLanguages.length}
onChange={this.toggleAllLangs}
/>
{searchLanguages.map((lang) => (
<CustomInput
key={lang}
id={lang}
type="checkbox"
checked={chosenLanguages.indexOf(lang) !== -1}
label={t('common:language.' + lang)}
onChange={this.toggleLangs}
/>
))}
</Col>
);
}
}
export default translate(['tabsContent'], { wait: true })(LangsTab);
@@ -0,0 +1,61 @@
import React from 'react'
import PropTypes from 'prop-types'
import { DragSource } from 'react-dnd'
const Types = {
LOC: 'location'
}
const locationSource = {
beginDrag (props) {
// Return the data describing the dragged item
return { oldDropTargetType: props.dropTargetType }
},
endDrag (props, monitor, component) {
// When dropped on a compatible target, do something
if (monitor.getDropResult() !== null) {
const locFrom = props.dropTargetType
const locTo = monitor.getDropResult().newDropTargetType
const locationType = props.locationType
const location = props.location
props.moveLocation(locFrom, locTo, locationType, location)
}
}
}
/**
* Specifies which props to inject into your component.
*/
function collectDragSource (connect) {
return {
// Call this function inside render()
// to let React DnD handle the drag events:
connectDragSource: connect.dragSource()
}
}
export class LocationsTabList extends React.Component {
static propTypes = {
location: PropTypes.object.isRequired,
dropTargetType: PropTypes.string.isRequired,
moveLocation: PropTypes.func.isRequired,
connectDragSource: PropTypes.func.isRequired
};
render () {
const { connectDragSource } = this.props
const { location } = this.props
return connectDragSource(
<li className="list-group-item cursor-move p-2">
<span className="drag-handle" />
{location.name}
</li>
)
}
}
export default DragSource(Types.LOC, locationSource, collectDragSource)(LocationsTabList)
@@ -0,0 +1,111 @@
/* eslint-disable react/jsx-no-bind */
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import LocationsTabList from './LocationsTabList';
import { Button, Col, Row } from 'reactstrap';
export class LocationsTab extends React.Component {
static propTypes = {
locations: PropTypes.array.isRequired,
locationsToInclude: PropTypes.array.isRequired,
locationsToExclude: PropTypes.array.isRequired,
chosenLocationsType: PropTypes.string.isRequired,
changeLocationsType: PropTypes.func.isRequired,
moveLocation: PropTypes.func.isRequired,
clearLocations: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
constructor(props) {
super(props);
this.state = {
dropdownOpen: false,
dropDownValue: 'country'
};
}
onClearLocations = () => {
this.props.clearLocations();
this.props.changeLocationsType('country');
this.setState({ dropDownValue: 'country' });
};
selectLocation = (value) => {
this.props.changeLocationsType(value);
this.setState({ dropDownValue: value });
};
render() {
const {
locations,
chosenLocationsType,
locationsToInclude,
locationsToExclude
} = this.props;
const { t } = this.props;
const locationsMainList = locations.filter((loc) => {
return loc.type === chosenLocationsType;
});
const includeList = locationsToInclude.filter((loc) => {
return loc.type === chosenLocationsType;
});
const excludeList = locationsToExclude.filter((loc) => {
return loc.type === chosenLocationsType;
});
const { dropDownValue } = this.state;
return (
<Col sm={12}>
<Button
outline
active={dropDownValue === 'country'}
color="secondary"
className="mr-2 mb-3"
onClick={() => this.selectLocation('country')}
>
{t('searchTab.searchBySection.locations.countriesSelect')}
</Button>
<Button
outline
active={dropDownValue === 'state'}
color="secondary"
className="mb-3"
onClick={() => this.selectLocation('state')}
>
{t('searchTab.searchBySection.locations.statesSelect')}
</Button>
<Row className="draggable">
<Col md={4}>
<LocationsTabList
locations={locationsMainList}
chosenLocationsType={chosenLocationsType}
dropTargetType="locations"
moveLocation={this.props.moveLocation}
/>
</Col>
<Col md={4}>
<LocationsTabList
locations={includeList}
chosenLocationsType={chosenLocationsType}
dropTargetType="locationsToInclude"
moveLocation={this.props.moveLocation}
/>
</Col>
<Col md={4}>
<LocationsTabList
locations={excludeList}
chosenLocationsType={chosenLocationsType}
dropTargetType="locationsToExclude"
moveLocation={this.props.moveLocation}
/>
</Col>
</Row>
</Col>
);
}
}
export default translate(['tabsContent'], { wait: true })(LocationsTab);
@@ -0,0 +1,94 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { DropTarget } from 'react-dnd';
import flow from 'lodash/flow';
import LocationItem from './LocationItem';
import {
ListGroup
} from 'reactstrap';
const targetTypes = ['location'];
const locationListTarget = {
drop(props, monitor, component) {
if (monitor.didDrop()) {
//check whether some nested
// target already handled drop
return;
}
return { newDropTargetType: props.dropTargetType };
},
canDrop(props, monitor) {
return props.dropTargetType !== monitor.getItem().oldDropTargetType;
}
};
function collectDropTarget(connect, monitor) {
return {
// Call this function inside render()
// to let React DnD handle the drag events:
connectDropTarget: connect.dropTarget(),
// You can ask the monitor about the current drag state:
itemType: monitor.getItemType()
};
}
export class LocationsTabList extends React.Component {
static propTypes = {
locations: PropTypes.array.isRequired,
chosenLocationsType: PropTypes.string.isRequired,
dropTargetType: PropTypes.string.isRequired,
moveLocation: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
connectDropTarget: PropTypes.func.isRequired
};
render() {
const { locations, chosenLocationsType, dropTargetType } = this.props;
const { t } = this.props;
const { connectDropTarget } = this.props;
locations.forEach((location) => {
location.name = t('common:' + location.type + '.' + location.code);
});
const sortedLocations = locations.sort((a, b) => {
const nameA = a.name.toLowerCase();
const nameB = b.name.toLowerCase();
if (nameA < nameB) {
//sort string ascending
return -1;
}
if (nameA > nameB) {
return 1;
}
return 0;
});
return connectDropTarget(
<div className="scroll-area-md border b-radius-5">
<p className="text-muted border-bottom p-2">{t('searchTab.searchBySection.locations.' + dropTargetType)}</p>
<ListGroup className="p-2">
{sortedLocations.map((location, i) => {
return (
<LocationItem
key={'location-' + i}
location={location}
dropTargetType={dropTargetType}
locationType={chosenLocationsType}
moveLocation={this.props.moveLocation}
/>
);
})}
</ListGroup>
</div>
);
}
}
export default flow(
DropTarget(targetTypes, locationListTarget, collectDropTarget),
translate(['tabsContent'], { wait: true })
)(LocationsTabList);

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