at the end of the day, it was inevitable
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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')
|
||||
@@ -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}`)
|
||||
@@ -0,0 +1,4 @@
|
||||
// import {createApi} from '../common/Common'
|
||||
|
||||
// const baseUrl = '/api/v1/receivers'
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
})
|
||||
@@ -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})
|
||||
})
|
||||
@@ -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));
|
||||
};
|
||||
@@ -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
|
||||
})
|
||||
@@ -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));
|
||||
};
|
||||
@@ -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')
|
||||
@@ -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));
|
||||
};
|
||||
@@ -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})
|
||||
})
|
||||
@@ -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
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
import {createApi} from '../common/Common'
|
||||
|
||||
const root = '/api/v1/users'
|
||||
|
||||
export const changePassword = createApi('POST', `${root}/change-password`)
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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}`;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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> </Fragment>
|
||||
) : subStartDate && subEndDate ? (
|
||||
`${convertUTCtoLocal(
|
||||
subStartDate,
|
||||
'MMM D, YYYY'
|
||||
)} - ${convertUTCtoLocal(subEndDate, 'MMM D, YYYY')}`
|
||||
) : (
|
||||
t('plans.currentPlan.perMonth')
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col sm="6">
|
||||
<button
|
||||
className="card mb-3 widget-chart bg-success text-white text-left"
|
||||
onClick={changePlan}
|
||||
>
|
||||
<div className="widget-chart-content">
|
||||
<div className="widget-subheading">
|
||||
{t('plans.currentPlan.changePlan')}
|
||||
</div>
|
||||
<div className="widget-numbers font-size-xlg">
|
||||
{t('plans.currentPlan.upgradeYourPlan')}
|
||||
</div>
|
||||
<div className="widget-description">
|
||||
<span>{t('plans.currentPlan.upgradeText')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</Col>
|
||||
<Col xs="12">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>{t('plans.currentPlan.currentPlanDetails')}</CardTitle>
|
||||
<div className="mb-3">
|
||||
<p className="text-muted">
|
||||
{t('plans.currentPlan.selectedMediaTypes')}
|
||||
</p>
|
||||
<p className="font-size-xlg">
|
||||
{selectedMedias.length > 0
|
||||
? selectedMedias.join(', ')
|
||||
: t('plans.currentPlan.none')}
|
||||
{notSelectedMedias.length > 0 ? (
|
||||
<span className="font-size-md opacity-6 ml-2">
|
||||
({t('plans.currentPlan.upgradeToGet')}:{' '}
|
||||
{notSelectedMedias.join(', ')})
|
||||
</span>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="divider" />
|
||||
<div className="mb-3">
|
||||
<p className="text-muted mb-2">
|
||||
{t('plans.currentPlan.selectedLicenses')}
|
||||
</p>
|
||||
<Row>
|
||||
<Col xs="12" sm="6" md="3">
|
||||
<div className="mb-3 card widget-chart">
|
||||
{!isRTL ? (
|
||||
<div className="widget-numbers">
|
||||
{limits.savedFeeds.current}/{limits.savedFeeds.limit}
|
||||
</div>
|
||||
) : (
|
||||
<div className="widget-numbers">
|
||||
{limits.savedFeeds.limit}/{limits.savedFeeds.current}
|
||||
</div>
|
||||
)}
|
||||
<div className="widget-subheading mb-3">
|
||||
{t('plans.currentPlan.feedsLicenses')}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs="12" sm="6" md="3">
|
||||
<div className="mb-3 card widget-chart">
|
||||
{!isRTL ? (
|
||||
<div className="widget-numbers">
|
||||
{limits.searchesPerDay.current}/
|
||||
{limits.searchesPerDay.limit}
|
||||
</div>
|
||||
) : (
|
||||
<div className="widget-numbers">
|
||||
{limits.searchesPerDay.limit}/
|
||||
{limits.searchesPerDay.current}
|
||||
</div>
|
||||
)}
|
||||
<div className="widget-subheading mb-3">
|
||||
{t('plans.currentPlan.searchLicenses')}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs="12" sm="6" md="3">
|
||||
<div className="mb-3 card widget-chart">
|
||||
{!isRTL ? (
|
||||
<div className="widget-numbers">
|
||||
{limits.webFeeds.current}/{limits.webFeeds.limit}
|
||||
</div>
|
||||
) : (
|
||||
<div className="widget-numbers">
|
||||
{limits.webFeeds.limit}/{limits.webFeeds.current}
|
||||
</div>
|
||||
)}
|
||||
<div className="widget-subheading mb-3">
|
||||
{t('plans.currentPlan.webfeedLicenses')}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs="12" sm="6" md="3">
|
||||
<div className="mb-3 card widget-chart">
|
||||
{!isRTL ? (
|
||||
<div className="widget-numbers">
|
||||
{limits.alerts.current}/{limits.alerts.limit}
|
||||
</div>
|
||||
) : (
|
||||
<div className="widget-numbers">
|
||||
{limits.alerts.limit}/{limits.alerts.current}
|
||||
</div>
|
||||
)}
|
||||
<div className="widget-subheading mb-3">
|
||||
{t('plans.currentPlan.alertLicenses')}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs="12" sm="6" md="3">
|
||||
<div className="mb-3 card widget-chart">
|
||||
{!isRTL ? (
|
||||
<div className="widget-numbers">
|
||||
{limits.subscriberAccounts.current}/
|
||||
{limits.subscriberAccounts.limit}
|
||||
</div>
|
||||
) : (
|
||||
<div className="widget-numbers">
|
||||
{limits.subscriberAccounts.limit}/
|
||||
{limits.subscriberAccounts.current}
|
||||
</div>
|
||||
)}
|
||||
<div className="widget-subheading mb-3">
|
||||
{t('plans.currentPlan.userAccounts')}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
<div className="divider" />
|
||||
<div className="mb-3">
|
||||
<p className="text-muted">{t('plans.currentPlan.features')}</p>
|
||||
<p className="font-size-xlg">
|
||||
{plans.analytics ? (
|
||||
t('plans.currentPlan.analytics')
|
||||
) : (
|
||||
<Fragment>
|
||||
{t('plans.currentPlan.none')}
|
||||
<span className="font-size-md opacity-6 ml-2">
|
||||
({t('plans.currentPlan.upgradeToGet')}:{' '}
|
||||
{t('plans.currentPlan.analytics')})
|
||||
</span>
|
||||
</Fragment>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{plans.price > 0 && (
|
||||
<Fragment>
|
||||
<div className="divider" />
|
||||
<div className="mb-3">
|
||||
{!isPlanCancelled ? (
|
||||
<div className="text-muted">
|
||||
<Button
|
||||
color="danger"
|
||||
outline
|
||||
onClick={toggleCancelModal}
|
||||
>
|
||||
{t('plans.currentPlan.cancelSubscriptionBtn')}
|
||||
</Button>
|
||||
<p className="text-muted mt-2">
|
||||
{t('plans.currentPlan.cancelWarning')}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-muted">
|
||||
<Button color="secondary" outline disabled>
|
||||
{t('plans.currentPlan.cancelSubscriptionBtn')}
|
||||
</Button>
|
||||
<p className="d-block d-md-inline-block ml-md-3 mt-md-0 mt-2 ml-0 text-muted">
|
||||
{t('plans.currentPlan.alreadyCancelled')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
<CancellationFeedback
|
||||
isOpen={cancelModal}
|
||||
toggle={toggleCancelModal}
|
||||
actions={actions}
|
||||
user={user}
|
||||
t={t}
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
CurrentPlan.propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
actions: PropTypes.object,
|
||||
user: PropTypes.object
|
||||
};
|
||||
|
||||
export default reduxConnect('user', ['common', 'auth', 'user'])(
|
||||
translate(['tabsContent'], { wait: true })(CurrentPlan)
|
||||
);
|
||||
@@ -0,0 +1,168 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
Row,
|
||||
Table
|
||||
} from 'reactstrap';
|
||||
import { convertUTCtoLocal } from '../../../../common/helper';
|
||||
import moment from 'moment';
|
||||
import { capitalize } from 'lodash';
|
||||
import { translate } from 'react-i18next';
|
||||
|
||||
function ShowTransactionDetails(props) {
|
||||
const { data, closeModal, t } = props;
|
||||
|
||||
const plan = data && data.lines && data.lines.data && data.lines.data[0];
|
||||
|
||||
useEffect(() => {
|
||||
return () => closeModal();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Modal isOpen={!!data && !!plan} toggle={closeModal} size="lg">
|
||||
<ModalHeader toggle={closeModal}>
|
||||
{t('plans.transactions.modal.heading')}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
{data && (
|
||||
<Row>
|
||||
<Col xs="12" lg="6" className="mb-3">
|
||||
<h6 className="mb-3">
|
||||
{t('plans.transactions.modal.transactionDetails')}
|
||||
</h6>
|
||||
<Table striped>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{t('plans.transactions.modal.transactionDate')}</th>
|
||||
<td>
|
||||
{convertUTCtoLocal(
|
||||
moment.unix(
|
||||
data.status_transitions &&
|
||||
data.status_transitions.paid_at
|
||||
),
|
||||
'MM/DD/YYYY hh:mm:ss a'
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{t('plans.transactions.modal.activationDate')}</th>
|
||||
<td>
|
||||
{convertUTCtoLocal(
|
||||
moment.unix(plan && plan.period.start),
|
||||
'MM/DD/YYYY'
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{t('plans.transactions.modal.expirationDate')}</th>
|
||||
<td>
|
||||
{convertUTCtoLocal(
|
||||
moment.unix(plan && plan.period.end),
|
||||
'MM/DD/YYYY'
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{t('plans.transactions.modal.amount')}</th>
|
||||
<td>${data.amount_paid / 100}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{t('plans.transactions.modal.status')}</th>
|
||||
<td>{capitalize(data.status)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
</Col>
|
||||
<Col xs="12" lg="6" className="mb-3">
|
||||
<h6 className="mb-3">
|
||||
{t('plans.transactions.modal.billingDetails')}
|
||||
</h6>
|
||||
<Table striped>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{t('plans.transactions.modal.name')}</th>
|
||||
<td>{data.customer_name || '-'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{t('plans.transactions.modal.email')}</th>
|
||||
<td>{data.customer_email || '-'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{t('plans.transactions.modal.phone')}</th>
|
||||
<td>{data.customer_phone || '-'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{t('plans.transactions.modal.address')}</th>
|
||||
<td>{data.customer_address || '-'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{t('plans.transactions.modal.invoiceNo')}</th>
|
||||
<td>
|
||||
{data.number} (
|
||||
<a
|
||||
href={data.hosted_invoice_url}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{t('plans.transactions.modal.showInvoiceLink')}
|
||||
</a>
|
||||
)
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
</Col>
|
||||
{/* <Col xs="12" lg="6" className="mb-3">
|
||||
<h6 className="mb-3">Plan Details</h6>
|
||||
<Table striped>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Feeds Licenses</th>
|
||||
<td>0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Webfeed Licenses</th>
|
||||
<td>0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Newsletter Licenses</th>
|
||||
<td>0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>User Accounts</th>
|
||||
<td>0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Analytics</th>
|
||||
<td>No</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
</Col> */}
|
||||
</Row>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="link" onClick={closeModal}>
|
||||
{t('plans.transactions.modal.cancelBtn')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
ShowTransactionDetails.propTypes = {
|
||||
t: PropTypes.func,
|
||||
closeModal: PropTypes.func,
|
||||
data: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]).isRequired
|
||||
};
|
||||
|
||||
export default React.memo(
|
||||
translate(['tabsContent'], { wait: true })(ShowTransactionDetails)
|
||||
);
|
||||
@@ -0,0 +1,749 @@
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
import React, { Fragment, useCallback, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Slider from 'rc-slider';
|
||||
import Tooltip from 'rc-tooltip';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
CardTitle,
|
||||
Col,
|
||||
Form,
|
||||
FormGroup,
|
||||
Label,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
Row
|
||||
} from 'reactstrap';
|
||||
|
||||
import {
|
||||
licenses,
|
||||
mediaTypes,
|
||||
features,
|
||||
addonFeatures
|
||||
} from '../../../LoginRegister/Registration/PlanConstants';
|
||||
import useForm from '../../../common/hooks/useForm';
|
||||
import { debounce } from 'lodash';
|
||||
import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
|
||||
|
||||
import useIsMounted from '../../../common/hooks/useIsMounted';
|
||||
import reduxConnect from '../../../../redux/utils/connect';
|
||||
import {
|
||||
getPlans,
|
||||
updatePrice
|
||||
} from '../../../../api/registration/registration';
|
||||
import {
|
||||
updatePlanHubspot,
|
||||
updatePlanPayment
|
||||
} from '../../../../api/plans/userPlans';
|
||||
import { planRoutes } from './UserPlans';
|
||||
import BillingDetailsForm from './BillingDetailsForm';
|
||||
|
||||
import simpleNumberLocalizer from 'react-widgets-simple-number';
|
||||
import NumberPicker from 'react-widgets/lib/NumberPicker';
|
||||
import LoadersAdvanced from '../../../common/Loader/Loader';
|
||||
import { IoIosWarning } from 'react-icons/io';
|
||||
import { convertUTCtoLocal, setDocumentData } from '../../../../common/helper';
|
||||
import { translate } from 'react-i18next';
|
||||
|
||||
simpleNumberLocalizer();
|
||||
|
||||
const Handle = Slider.Handle;
|
||||
|
||||
const handle = (props) => {
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const { value, dragging, index, ...restProps } = props;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
key={index}
|
||||
prefixCls="rc-slider-tooltip"
|
||||
overlay={value}
|
||||
visible={dragging}
|
||||
placement="top"
|
||||
>
|
||||
<Handle value={value} {...restProps} />
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const initialForm = {
|
||||
savedFeeds: 0,
|
||||
searchesPerDay: 0,
|
||||
webFeeds: 0,
|
||||
alerts: 0,
|
||||
news: 0,
|
||||
blog: 0,
|
||||
reddit: 0,
|
||||
instagram: 0,
|
||||
twitter: 0,
|
||||
analytics: 0,
|
||||
subscriberAccounts: 0,
|
||||
masterAccounts: 0
|
||||
};
|
||||
|
||||
const initialPaymentForm = {
|
||||
name: '',
|
||||
line1: '',
|
||||
line2: '',
|
||||
city: '',
|
||||
state: '',
|
||||
postal_code: '',
|
||||
country: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
errors: {
|
||||
name: null,
|
||||
line1: null,
|
||||
city: null,
|
||||
state: null,
|
||||
postal_code: null,
|
||||
country: null,
|
||||
email: null,
|
||||
phone: null
|
||||
}
|
||||
};
|
||||
|
||||
function UpdatePlan({ actions, restrictions, t }) {
|
||||
const stripe = useStripe();
|
||||
const elements = useElements();
|
||||
const isMounted = useIsMounted();
|
||||
|
||||
// first step
|
||||
const { form, handleChange, resetForm } = useForm(initialForm);
|
||||
const [updatingPrice, setUpdatingPrice] = useState(true);
|
||||
const [totalCost, setTotalCost] = useState(' - ');
|
||||
const [modal, setModal] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [planLoading, setPlanLoading] = useState(true);
|
||||
const [planError, setPlanError] = useState(false);
|
||||
const [planList, setPlanList] = useState([]);
|
||||
const [disableUpdate, setDisableUpdate] = useState(true);
|
||||
|
||||
// second step
|
||||
const [nextStep, setNextStep] = useState(false);
|
||||
const {
|
||||
form: paymentForm,
|
||||
handleChange: handlePaymentForm,
|
||||
errors: paymentFormErrors,
|
||||
handleValidation: handlePaymentValidation,
|
||||
validateSubmit
|
||||
} = useForm(initialPaymentForm);
|
||||
const [paymentError, setPaymentError] = useState(false);
|
||||
const [paymentLoading, setPaymentLoading] = useState(false);
|
||||
|
||||
// to update price when input changes
|
||||
useEffect(() => {
|
||||
if (planList.length > 0) {
|
||||
debouncePrice(form);
|
||||
}
|
||||
}, [...Object.values(form)]);
|
||||
|
||||
const debouncePrice = useCallback(
|
||||
debounce((form) => {
|
||||
setUpdatingPrice(true);
|
||||
updatePrice(form).then((res) => {
|
||||
if (!isMounted.current) {
|
||||
return false;
|
||||
}
|
||||
if (res.error || isNaN(res.data.totalPrice)) {
|
||||
actions.addAlert(res.data);
|
||||
setUpdatingPrice(false);
|
||||
setTotalCost('Error');
|
||||
return;
|
||||
}
|
||||
setTotalCost(res.data.totalPrice);
|
||||
setUpdatingPrice(false);
|
||||
});
|
||||
}, 1000),
|
||||
[isMounted.current]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!restrictions.isPlanCancelled && !restrictions.isPlanDowngrade) {
|
||||
setDisableUpdate(false);
|
||||
} else {
|
||||
setDisableUpdate(true);
|
||||
}
|
||||
}, [restrictions.isPlanCancelled, restrictions.isPlanDowngrade]);
|
||||
|
||||
useEffect(() => {
|
||||
getBillingPlans();
|
||||
|
||||
setDocumentData('title', 'Update Plan');
|
||||
return () => setDocumentData('title'); // default
|
||||
}, []);
|
||||
|
||||
function getBillingPlans() {
|
||||
setPlanLoading(true);
|
||||
setPlanError(false);
|
||||
getPlans().then((res) => {
|
||||
if (!isMounted.current) {
|
||||
return false;
|
||||
}
|
||||
if (res.error || !res.data || !res.data.length) {
|
||||
setPlanError(true);
|
||||
setPlanLoading(false);
|
||||
res.data && res.data.length > 0 && actions.addAlert(res.data);
|
||||
return;
|
||||
}
|
||||
setPlanLoading(false);
|
||||
setPlanList(res.data);
|
||||
|
||||
const modified = { ...initialForm };
|
||||
let selectedPlan = {};
|
||||
if (restrictions.plans.price > 0) {
|
||||
selectedPlan = { ...restrictions.plans };
|
||||
Object.entries(restrictions.limits).map(([key, value]) => {
|
||||
selectedPlan[key] = value.limit;
|
||||
});
|
||||
selectedPlan.blog = selectedPlan.blogs;
|
||||
delete selectedPlan.blogs;
|
||||
} else {
|
||||
selectedPlan = res.data[0];
|
||||
}
|
||||
|
||||
Object.keys(initialForm).map((key) => {
|
||||
modified[key] =
|
||||
selectedPlan[key] === undefined
|
||||
? modified[key]
|
||||
: selectedPlan[key] === true
|
||||
? 1
|
||||
: selectedPlan[key] === false
|
||||
? 0
|
||||
: selectedPlan[key];
|
||||
});
|
||||
resetForm(modified);
|
||||
});
|
||||
}
|
||||
|
||||
function changePlan(id) {
|
||||
const selectedPlan = planList.find((plan) => plan.id === id);
|
||||
const modified = { ...initialForm };
|
||||
Object.keys(initialForm).map((key) => {
|
||||
modified[key] =
|
||||
selectedPlan[key] === undefined
|
||||
? modified[key]
|
||||
: selectedPlan[key] === true
|
||||
? 1
|
||||
: selectedPlan[key] === false
|
||||
? 0
|
||||
: selectedPlan[key];
|
||||
});
|
||||
resetForm(modified);
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
if (restrictions.isPlanCancelled || restrictions.isPlanDowngrade) {
|
||||
return;
|
||||
}
|
||||
// move to payment page if new basic user
|
||||
// instruct according to upgrade and downgrade
|
||||
// if card already stored then only update the plan by showing modal or providing option to change card
|
||||
setLoading(true);
|
||||
if (restrictions.isPaymentId) {
|
||||
setModal(true); // show details of card
|
||||
} else {
|
||||
setNextStep(true);
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
setModal((prev) => !prev);
|
||||
}
|
||||
|
||||
function proceedToDetails() {
|
||||
toggle();
|
||||
setNextStep(true);
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
const submitPayment = async () => {
|
||||
if (!stripe || !elements) {
|
||||
// Stripe.js has not loaded yet.
|
||||
return;
|
||||
}
|
||||
|
||||
if (restrictions.isPlanCancelled || restrictions.isPlanDowngrade) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPaymentError(false);
|
||||
setPaymentLoading(true);
|
||||
|
||||
const obj = validateSubmit();
|
||||
if (!obj) {
|
||||
setPaymentLoading(false);
|
||||
return actions.addAlert({
|
||||
type: 'error',
|
||||
transKey: 'requiredInfo'
|
||||
});
|
||||
}
|
||||
|
||||
const cardElement = elements.getElement(CardElement);
|
||||
const {
|
||||
name,
|
||||
line1,
|
||||
line2,
|
||||
city,
|
||||
state,
|
||||
postal_code,
|
||||
country,
|
||||
email,
|
||||
phone
|
||||
} = obj;
|
||||
const { error, paymentMethod } = await stripe.createPaymentMethod({
|
||||
type: 'card',
|
||||
card: cardElement,
|
||||
billing_details: {
|
||||
name,
|
||||
email,
|
||||
phone,
|
||||
address: {
|
||||
line1: line1,
|
||||
line2: line2,
|
||||
city: city,
|
||||
state: state,
|
||||
postal_code: postal_code,
|
||||
country: country
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (error) {
|
||||
setPaymentError(error);
|
||||
setPaymentLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const newObj = { ...form };
|
||||
newObj.masterAccounts = '1';
|
||||
newObj.paymentID = paymentMethod.id; //stripe card element ID
|
||||
const res = await updatePlanPayment(newObj);
|
||||
|
||||
if (res.error) {
|
||||
res.data
|
||||
? actions.addAlert(res.data)
|
||||
: actions.addAlert({ type: 'error', transKey: 'somethingWrong' });
|
||||
setPaymentLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
window.gtag &&
|
||||
window.gtag('event', 'purchase', {
|
||||
currency: 'USD',
|
||||
value: totalCost
|
||||
});
|
||||
|
||||
await updatePlanHubspot({ ...obj, ...form, totalCost });
|
||||
|
||||
actions.addAlert({ type: 'notice', transKey: 'planUpdated' });
|
||||
|
||||
// refresh page on success and move to active plan details
|
||||
setTimeout(() => {
|
||||
window.location.pathname = `/app/plans/${planRoutes.current}`;
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const proceedPayment = async () => {
|
||||
// payment with old card
|
||||
setLoading(true);
|
||||
|
||||
const newObj = { ...form };
|
||||
newObj.masterAccounts = '1';
|
||||
const res = await updatePlanPayment(newObj);
|
||||
|
||||
if (res.error) {
|
||||
res.data
|
||||
? actions.addAlert(res.data)
|
||||
: actions.addAlert({ type: 'error', transKey: 'somethingWrong' });
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
window.gtag &&
|
||||
window.gtag('event', 'purchase', {
|
||||
currency: 'USD',
|
||||
value: totalCost
|
||||
});
|
||||
|
||||
await updatePlanHubspot({ ...form, totalCost });
|
||||
|
||||
actions.addAlert({ type: 'notice', transKey: 'planUpdated' });
|
||||
|
||||
// refresh page on success and move to active plan details
|
||||
setTimeout(() => {
|
||||
window.location.pathname = `/app/plans/${planRoutes.current}`;
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
function moveBack() {
|
||||
window.scrollTo(0, 0);
|
||||
setNextStep(false);
|
||||
}
|
||||
|
||||
if (planError || planLoading) {
|
||||
return (
|
||||
<Col xs="12" lg="8" xl="9">
|
||||
<Card className="h-75 mb-3">
|
||||
<CardBody>
|
||||
<CardTitle>{t('plans.updatePlan.heading')}</CardTitle>
|
||||
{planError && (
|
||||
<div className="text-danger text-center p-4">
|
||||
<IoIosWarning
|
||||
className="d-block mx-auto mb-2"
|
||||
fontSize="32px"
|
||||
/>
|
||||
{t('plans.updatePlan.planLoadingFailed')}{' '}
|
||||
<Button color="link" onClick={getBillingPlans} className="p-0">
|
||||
{t('plans.updatePlan.tryAgainBtn')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardBody>
|
||||
{planLoading && <LoadersAdvanced />}
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
const isRTL = document.documentElement.dir === 'rtl';
|
||||
|
||||
return (
|
||||
<Col xs="12" lg="8" xl="9">
|
||||
<Card className="mb-3">
|
||||
{!nextStep ? (
|
||||
<CardBody>
|
||||
<CardTitle>{t('plans.updatePlan.heading')}</CardTitle>
|
||||
<p className="text-muted">
|
||||
{t('plans.updatePlan.subText')}{' '}
|
||||
<a
|
||||
href="https://www.socialhose.io/en/pricing"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{t('plans.updatePlan.learnMoreBtn')}
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<hr />
|
||||
<Form>
|
||||
<Row>
|
||||
<Col md={12}>
|
||||
<div className="mb-3">
|
||||
<h6 className="font-weight-bold mb-3">
|
||||
{t('plans.updatePlan.prePlans')}
|
||||
</h6>
|
||||
<div className="d-flex flex-wrap justify-content-center justify-content-md-start">
|
||||
{planList.map((plan) => (
|
||||
<Button
|
||||
outline
|
||||
key={plan.id}
|
||||
color="primary"
|
||||
type="button"
|
||||
className="btn-wide btn-lg p-sm-3 mb-2 mr-2"
|
||||
onClick={() => changePlan(plan.id)}
|
||||
>
|
||||
{plan.name}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div className="mb-3">
|
||||
<h6 className="font-weight-bold mb-3">
|
||||
{t('plans.updatePlan.mediaTypes')}
|
||||
</h6>
|
||||
<div>
|
||||
{mediaTypes.map((type) => (
|
||||
<Button
|
||||
key={type.name}
|
||||
size="lg"
|
||||
type="button"
|
||||
title={
|
||||
form[type.name]
|
||||
? 'Click to deselect'
|
||||
: 'Click to select'
|
||||
}
|
||||
outline={!form[type.name]}
|
||||
className="btn-pill mb-2 mr-2"
|
||||
color={form[type.name] ? 'success' : 'light'}
|
||||
onClick={() =>
|
||||
handleChange(type.name, !form[type.name])
|
||||
}
|
||||
>
|
||||
{t(`searchTab.sourceTypes.${type.transKey}`)} (
|
||||
{type.price})
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div className="mb-3">
|
||||
<h6 className="font-weight-bold mb-3">
|
||||
{t('plans.updatePlan.licenses')}
|
||||
</h6>
|
||||
<Row noGutters className="justify-content-center">
|
||||
{licenses.map((license) => (
|
||||
<Col sm={6} key={license.name}>
|
||||
<div className="p-4 m-2 border b-radius-5 shadow-sm">
|
||||
<FormGroup>
|
||||
<div className="d-flex justify-content-between">
|
||||
<Label title={license.title}>
|
||||
{t(`plans.currentPlan.${license.transKey}`)}
|
||||
</Label>
|
||||
<span className="font-size-lg font-weight-bold text-primary">
|
||||
{form[license.name]}
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
{...license.props}
|
||||
reverse={isRTL}
|
||||
handle={handle}
|
||||
value={form[license.name]}
|
||||
onChange={(val) =>
|
||||
handleChange(license.name, val)
|
||||
}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</div>
|
||||
<hr />
|
||||
<Row>
|
||||
<Col md="6">
|
||||
<div className="mb-3">
|
||||
<h6 className="font-weight-bold mb-3">
|
||||
{t('plans.updatePlan.features')}
|
||||
</h6>
|
||||
<div>
|
||||
{features.map((type) => (
|
||||
<Button
|
||||
key={type.name}
|
||||
size="lg"
|
||||
type="button"
|
||||
title={
|
||||
form[type.name]
|
||||
? t('plans.updatePlan.deselectTooltip')
|
||||
: t('plans.updatePlan.selectTooltip')
|
||||
}
|
||||
outline={!form[type.name]}
|
||||
className="btn-pill mb-2 mr-2"
|
||||
color={form[type.name] ? 'success' : 'light'}
|
||||
onClick={() =>
|
||||
handleChange(type.name, !form[type.name])
|
||||
}
|
||||
>
|
||||
{t(`plans.currentPlan.${type.transKey}`)} (
|
||||
{type.price})
|
||||
</Button>
|
||||
))}
|
||||
<div className="pl-2">
|
||||
{features.map((type) =>
|
||||
form[type.name] ? (
|
||||
<p
|
||||
key={type.name}
|
||||
className="font-size-sm text-muted mb-1"
|
||||
>
|
||||
{type.desc}
|
||||
</p>
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col md="6">
|
||||
<div className="mb-3">
|
||||
<h6 className="font-weight-bold mb-3">
|
||||
{t('plans.updatePlan.addOns')}
|
||||
</h6>
|
||||
<Row className="px-3">
|
||||
{addonFeatures.map((type) => (
|
||||
<Col xs="12" key={type.name}>
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{t(`plans.currentPlan.${type.transKey}`)}
|
||||
</Label>
|
||||
<NumberPicker
|
||||
{...type.props}
|
||||
value={form[type.name]}
|
||||
onChange={(val) =>
|
||||
handleChange(type.name, val)
|
||||
}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<div className="widget-content total-price">
|
||||
<div className="widget-content-wrapper justify-content-start justify-content-md-end mr-5">
|
||||
<div className="widget-content-left">
|
||||
<div className="widget-heading">
|
||||
{t('plans.updatePlan.totalCost')}
|
||||
</div>
|
||||
<div className="widget-subheading">
|
||||
{t('plans.updatePlan.monthly')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="widget-content-right position-relative ml-0 ml-5">
|
||||
{/* {updatingPrice && (
|
||||
<div className="widget-numbers position-absolute text-secondary px-3">
|
||||
<FontAwesomeIcon icon={faSpinner} pulse />
|
||||
</div>
|
||||
)} */}
|
||||
<div
|
||||
className={`widget-numbers text-warning ${
|
||||
updatingPrice ? 'opacity-3' : ''
|
||||
}`}
|
||||
>
|
||||
${totalCost}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<hr />
|
||||
{restrictions.isPlanCancelled || restrictions.isPlanDowngrade ? (
|
||||
<p className="text-danger mb-3">
|
||||
{t('plans.updatePlan.cancelledWarning', {
|
||||
text: restrictions.isPlanCancelled
|
||||
? 'cancelled'
|
||||
: 'downgraded'
|
||||
})}{' '}
|
||||
{restrictions.subStartDate && restrictions.subEndDate
|
||||
? `(${convertUTCtoLocal(
|
||||
restrictions.subStartDate,
|
||||
'MMM D, YYYY'
|
||||
)} - ${convertUTCtoLocal(
|
||||
restrictions.subEndDate,
|
||||
'MMM D, YYYY'
|
||||
)})`
|
||||
: ''}
|
||||
</p>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<div className="text-right">
|
||||
<Button
|
||||
type="button"
|
||||
disabled={updatingPrice || loading || disableUpdate}
|
||||
onClick={handleSubmit}
|
||||
className="btn-wide"
|
||||
color="primary"
|
||||
size="lg"
|
||||
>
|
||||
{loading
|
||||
? t('plans.updatePlan.continueBtnLoading')
|
||||
: t('plans.updatePlan.continueBtn')}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</CardBody>
|
||||
) : (
|
||||
<CardBody>
|
||||
<CardTitle>{t('plans.updatePlan.billingHeading')}</CardTitle>
|
||||
<BillingDetailsForm
|
||||
form={paymentForm}
|
||||
errors={paymentFormErrors}
|
||||
handleChange={handlePaymentForm}
|
||||
handleValidation={handlePaymentValidation}
|
||||
/>
|
||||
<Row className="divider" />
|
||||
{paymentError && (
|
||||
<Alert color="danger">
|
||||
<Fragment>
|
||||
<p className="font-size-xs font-weight-bold text-uppercase">
|
||||
{t('plans.updatePlan.error')}
|
||||
</p>
|
||||
{paymentError.message}
|
||||
</Fragment>
|
||||
</Alert>
|
||||
)}
|
||||
<div className="d-flex justify-content-between flex-column-reverse flex-sm-row">
|
||||
<Button
|
||||
type="button"
|
||||
color="secondary"
|
||||
size="lg"
|
||||
disabled={paymentLoading}
|
||||
onClick={moveBack}
|
||||
>
|
||||
{t('plans.updatePlan.back')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="primary"
|
||||
onClick={submitPayment}
|
||||
disabled={!stripe || !elements || paymentLoading}
|
||||
className="btn-wide btn-hover-shine mb-2 mb-sm-0"
|
||||
size="lg"
|
||||
>
|
||||
{paymentLoading
|
||||
? t('plans.updatePlan.payLoading')
|
||||
: t('plans.updatePlan.payBtn', { totalCost })}
|
||||
</Button>
|
||||
</div>
|
||||
</CardBody>
|
||||
)}
|
||||
</Card>
|
||||
<Modal isOpen={modal} toggle={toggle} backdrop="static">
|
||||
<ModalHeader toggle={toggle}>
|
||||
{t('plans.updatePlan.confirmationHeading')}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<div>
|
||||
{restrictions.plans && restrictions.plans.price > 0 ? (
|
||||
restrictions.plans.price === totalCost ? null : restrictions.plans
|
||||
.price < totalCost ? (
|
||||
<p className="text-muted mb-3">
|
||||
{t('plans.updatePlan.upgradeNotice')}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-muted mb-3">
|
||||
{t('plans.updatePlan.downgradeNotice')}
|
||||
</p>
|
||||
)
|
||||
) : null}
|
||||
<p>{t('plans.updatePlan.alreadyStoredCard')}</p>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="link" onClick={proceedToDetails} disabled={loading}>
|
||||
{t('plans.updatePlan.payWithOtherCardBtn')}
|
||||
</Button>
|
||||
<Button color="primary" disabled={loading} onClick={proceedPayment}>
|
||||
{loading
|
||||
? t('plans.updatePlan.payLoading')
|
||||
: t('plans.updatePlan.payWithStoredCardBtn')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
UpdatePlan.propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
actions: PropTypes.object,
|
||||
restrictions: PropTypes.object
|
||||
};
|
||||
|
||||
export default reduxConnect('restrictions', [
|
||||
'common',
|
||||
'auth',
|
||||
'user',
|
||||
'restrictions'
|
||||
])(translate(['tabsContent'], { wait: true })(UpdatePlan));
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button, Modal, ModalBody, ModalHeader } from 'reactstrap';
|
||||
import { planRoutes } from './UserPlans';
|
||||
import { Trans, translate } from 'react-i18next';
|
||||
|
||||
function UpgradePlanModal({ isModalOpen = false, toggle, t }) {
|
||||
function toggleModal() {
|
||||
return toggle();
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isModalOpen}
|
||||
toggle={toggleModal}
|
||||
modalClassName="zoom-modal"
|
||||
backdrop="static"
|
||||
>
|
||||
<ModalHeader toggle={toggleModal} />
|
||||
<ModalBody className="px-4 px-sm-5 pb-5">
|
||||
<div className="text-center">
|
||||
<div className="display-4 mb-2">
|
||||
<i className="lnr-rocket text-primary"></i>
|
||||
</div>
|
||||
<h3 className="mb-3">{t('plans.upgradeModal.heading')}</h3>
|
||||
<div className="mb-4">
|
||||
<p className="text-muted">
|
||||
<Trans i18nKey="plans.upgradeModal.text">
|
||||
You have to upgrade your plan to get access of these features.
|
||||
Take a look at our bite-sized
|
||||
<strong>à la carte menu options</strong> with monthly billing.
|
||||
</Trans>{' '}
|
||||
<a
|
||||
href="https://www.socialhose.io/en/pricing"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{t('plans.upgradeModal.learnMore')}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
tag={Link}
|
||||
to={`/app/plans/${planRoutes.update}`}
|
||||
onClick={toggleModal}
|
||||
className="btn-pill btn-wide d-block mx-auto"
|
||||
color="success"
|
||||
size="lg"
|
||||
>
|
||||
{t('plans.upgradeModal.upgradeNowBtn')}
|
||||
</Button>
|
||||
<Button color="link" size="sm" onClick={toggleModal}>
|
||||
{t('plans.upgradeModal.maybeLaterBtn')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
UpgradePlanModal.propTypes = {
|
||||
isModalOpen: PropTypes.bool,
|
||||
t: PropTypes.func.isRequired,
|
||||
toggle: PropTypes.func
|
||||
};
|
||||
|
||||
export default React.memo(
|
||||
translate(['tabsContent'], { wait: true })(UpgradePlanModal)
|
||||
);
|
||||
@@ -0,0 +1,128 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
NavLink,
|
||||
Redirect,
|
||||
Route,
|
||||
Switch,
|
||||
useRouteMatch
|
||||
} from 'react-router-dom';
|
||||
import reduxConnect from '../../../../redux/utils/connect';
|
||||
import ChangeCard from './ChangeCard';
|
||||
import CurrentPlan from './CurrentPlan';
|
||||
import UpdatePlan from './UpdatePlan';
|
||||
import UserTransactions from './UserTransactions';
|
||||
import { Card, CardBody, Col, Row } from 'reactstrap';
|
||||
import { translate } from 'react-i18next';
|
||||
|
||||
export const planRoutes = {
|
||||
current: 'current',
|
||||
changeCard: 'change-card',
|
||||
txn: 'transactions',
|
||||
update: 'update'
|
||||
};
|
||||
|
||||
function UserPlans({ actions, restrictions, t }) {
|
||||
const match = useRouteMatch();
|
||||
|
||||
useEffect(() => {
|
||||
const { setEnableClosedSidebar } = actions;
|
||||
actions.getRestrictions();
|
||||
setEnableClosedSidebar(true);
|
||||
|
||||
return () => setEnableClosedSidebar(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Col xs={12} lg={4} xl={3}>
|
||||
<Card className="mb-3">
|
||||
<CardBody className="navigation-vertical">
|
||||
<ul className="navigation-ul">
|
||||
<li className="navigation-item">
|
||||
<NavLink
|
||||
className="navigation-link"
|
||||
activeClassName="active"
|
||||
to={`${match.url}/${planRoutes.current}`}
|
||||
>
|
||||
<em>
|
||||
<i className="font-size-lg lnr-file-empty"> </i>
|
||||
</em>
|
||||
<span>{t('plans.sidebar.activePlanDetails')}</span>
|
||||
</NavLink>
|
||||
</li>
|
||||
{restrictions.isPaymentId && (
|
||||
<li>
|
||||
<NavLink
|
||||
className="navigation-link"
|
||||
activeClassName="active"
|
||||
to={`${match.url}/${planRoutes.changeCard}`}
|
||||
>
|
||||
<em>
|
||||
<i className="font-size-lg lnr-license"> </i>
|
||||
</em>
|
||||
<span>{t('plans.sidebar.changeCard')}</span>
|
||||
</NavLink>
|
||||
</li>
|
||||
)}
|
||||
<li>
|
||||
<NavLink
|
||||
className="navigation-link"
|
||||
activeClassName="active"
|
||||
to={`${match.url}/${planRoutes.update}`}
|
||||
>
|
||||
<em>
|
||||
<i className="font-size-lg lnr-arrow-up-circle"> </i>
|
||||
</em>
|
||||
<span>{t('plans.sidebar.updatePlan')}</span>
|
||||
</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink
|
||||
className="navigation-link"
|
||||
activeClassName="active"
|
||||
to={`${match.url}/${planRoutes.txn}`}
|
||||
>
|
||||
<em>
|
||||
<i className="font-size-lg lnr-list"> </i>
|
||||
</em>
|
||||
<span>{t('plans.sidebar.yourTransactions')}</span>
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Col>
|
||||
<Switch>
|
||||
<Route path={`${match.url}/${planRoutes.current}`}>
|
||||
<CurrentPlan />
|
||||
</Route>
|
||||
{restrictions.isPaymentId && (
|
||||
<Route path={`${match.url}/${planRoutes.changeCard}`}>
|
||||
<ChangeCard />
|
||||
</Route>
|
||||
)}
|
||||
<Route path={`${match.url}/${planRoutes.txn}`}>
|
||||
<UserTransactions />
|
||||
</Route>
|
||||
<Route path={`${match.url}/${planRoutes.update}`}>
|
||||
<UpdatePlan />
|
||||
</Route>
|
||||
<Redirect to={`${match.url}/current`} />
|
||||
</Switch>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
UserPlans.propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
actions: PropTypes.object.isRequired,
|
||||
restrictions: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default reduxConnect('restrictions', [
|
||||
'common',
|
||||
'auth',
|
||||
'user',
|
||||
'restrictions'
|
||||
])(translate(['tabsContent'], { wait: true })(UserPlans));
|
||||
@@ -0,0 +1,132 @@
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
/* eslint-disable react/prop-types */
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { reduxActions } from '../../../../redux/utils/connect';
|
||||
import {
|
||||
convertUTCtoLocal,
|
||||
getQueryParams,
|
||||
setDocumentData
|
||||
} from '../../../../common/helper';
|
||||
import { getTransactions } from '../../../../api/plans/userPlans';
|
||||
import Table from '../../../common/Table/Table';
|
||||
import { Button, Col } from 'reactstrap';
|
||||
import ShowTransactionDetails from './ShowTransactionDetails';
|
||||
import moment from 'moment';
|
||||
import { capitalize } from 'lodash';
|
||||
import { translate } from 'react-i18next';
|
||||
|
||||
function UserTransactions(props) {
|
||||
const [dataSource, setDataSource] = useState({ data: [] });
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedData, setSelectedData] = useState(false);
|
||||
const { actions, t } = props;
|
||||
|
||||
useEffect(() => {
|
||||
setDocumentData('title', 'User Transactions');
|
||||
|
||||
return () => setDocumentData('title'); // default
|
||||
}, []);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
id: 'activeDate',
|
||||
Header: t('plans.transactions.activationDate'),
|
||||
accessor: (d) => d.lines.data[0] && d.lines.data[0].period.start,
|
||||
Cell: (props) => convertUTCtoLocal(moment.unix(props.value), 'MM/DD/YYYY')
|
||||
},
|
||||
{
|
||||
id: 'expireDate',
|
||||
Header: t('plans.transactions.expirationDate'),
|
||||
accessor: (d) => d.lines.data[0] && d.lines.data[0].period.end,
|
||||
Cell: (props) => convertUTCtoLocal(moment.unix(props.value), 'MM/DD/YYYY')
|
||||
},
|
||||
{
|
||||
id: 'paid_at',
|
||||
Header: t('plans.transactions.transactionDate'),
|
||||
accessor: (d) => d.status_transitions.paid_at,
|
||||
Cell: (props) =>
|
||||
convertUTCtoLocal(moment.unix(props.value), 'MM/DD/YYYY HH:mm:ss')
|
||||
},
|
||||
{
|
||||
Header: t('plans.transactions.amount'),
|
||||
accessor: 'amount_paid',
|
||||
Cell: (props) => (props.value ? `$${props.value / 100}` : '-')
|
||||
},
|
||||
{
|
||||
Header: t('plans.transactions.status'),
|
||||
accessor: 'status',
|
||||
Cell: (props) => capitalize(props.value)
|
||||
},
|
||||
{
|
||||
Header: t('plans.transactions.actions'),
|
||||
accessor: 'id',
|
||||
Cell: (props) => (
|
||||
<Button
|
||||
outline
|
||||
className="border-0 btn-transition"
|
||||
color="primary"
|
||||
size="sm"
|
||||
onClick={() => setSelectedData(props.original)}
|
||||
>
|
||||
{t('plans.transactions.more')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
function closeModal() {
|
||||
setSelectedData(false);
|
||||
}
|
||||
|
||||
const getTransactionList = useCallback((page, pageSize) => {
|
||||
setLoading(true);
|
||||
const params = getQueryParams({ page, pageSize });
|
||||
getTransactions(params).then((res) => {
|
||||
if (res.error || !res.data || !res.data.success || !res.data.data) {
|
||||
setDataSource({ data: [] }); // comment this line when API is ready
|
||||
setLoading(false);
|
||||
return actions.addAlert({
|
||||
type: 'error',
|
||||
transKey: 'somethingWrong'
|
||||
});
|
||||
}
|
||||
|
||||
// setDataSource(sampleData); // comment this line when API is ready
|
||||
setDataSource({
|
||||
data:
|
||||
res.data.data.data && res.data.data.data.length > 0
|
||||
? res.data.data.data
|
||||
: []
|
||||
});
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const { data = [], totalCount = 0, limit = 100, page = 1 } = dataSource;
|
||||
return (
|
||||
<Col xs="12" lg="8" xl="9">
|
||||
<Table
|
||||
cardTitle={t('plans.transactions.heading')}
|
||||
columns={columns}
|
||||
data={data}
|
||||
totalCount={totalCount}
|
||||
showTotalCount
|
||||
limit={limit}
|
||||
page={page}
|
||||
isLoading={loading}
|
||||
onFetchData={getTransactionList}
|
||||
/>
|
||||
<ShowTransactionDetails data={selectedData} closeModal={closeModal} />
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
UserTransactions.propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
actions: PropTypes.object
|
||||
};
|
||||
|
||||
export default reduxActions()(
|
||||
translate(['tabsContent'], { wait: true })(UserTransactions)
|
||||
);
|
||||
@@ -0,0 +1,226 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { compose } from 'redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { isMobile } from 'react-device-detect';
|
||||
import { TouchBackend } from 'react-dnd-touch-backend';
|
||||
import cx from 'classnames';
|
||||
import echarts from 'echarts';
|
||||
import ResizeDetector from 'react-resize-detector';
|
||||
|
||||
import AppHeader from './AppHeader/AppHeader';
|
||||
import WebTour from './AppHeader/WebTour';
|
||||
import Sidebar from './Sidebar/Sidebar';
|
||||
import reduxConnect from '../../redux/utils/connect';
|
||||
// import { NOTIFICATION_SUBSCREENS } from '../../redux/modules/appState/share/tabs';
|
||||
import LoadersAdvanced from '../common/Loader/Loader';
|
||||
import WesteronTheme from '../common/charts/WesterosTheme.json';
|
||||
import 'react-dates/initialize';
|
||||
import 'react-dates/lib/css/_datepicker.css';
|
||||
import Footer from '../common/Footer';
|
||||
import { Button, UncontrolledTooltip } from 'reactstrap';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faQuestion } from '@fortawesome/free-solid-svg-icons';
|
||||
import { find, map } from 'lodash';
|
||||
import tourPages from './AppHeader/WebTourSteps';
|
||||
import { allMediaTypes } from '../../redux/modules/appState/searchByFilters';
|
||||
import { translate } from 'react-i18next';
|
||||
import i18n from '../../i18n';
|
||||
import * as timeago from 'timeago.js';
|
||||
|
||||
import ar from 'timeago.js/lib/lang/ar';
|
||||
import fr from 'timeago.js/lib/lang/fr';
|
||||
|
||||
// register it languages for time-ago.
|
||||
timeago.register('ar', ar);
|
||||
timeago.register('fr', fr);
|
||||
|
||||
const DnDBackend = isMobile ? TouchBackend : HTML5Backend;
|
||||
|
||||
class App extends React.Component {
|
||||
static propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
actions: PropTypes.object.isRequired,
|
||||
children: PropTypes.element,
|
||||
history: PropTypes.object.isRequired,
|
||||
location: PropTypes.object.isRequired,
|
||||
store: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
state = {
|
||||
showSidebar: true,
|
||||
sidebarAnimationDisabled: true,
|
||||
closedSmallerSidebar: false,
|
||||
showTourIcon: false
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
echarts.registerTheme('westeros', WesteronTheme);
|
||||
this.checkIfTourGuide();
|
||||
|
||||
const {
|
||||
common: { auth }
|
||||
} = this.props.store;
|
||||
|
||||
const activeLang = i18n.language.slice(0, 2);
|
||||
this.props.actions.chooseLanguage(activeLang);
|
||||
|
||||
if (
|
||||
auth &&
|
||||
auth.user &&
|
||||
auth.user.restrictions &&
|
||||
auth.user.restrictions.plans
|
||||
) {
|
||||
const planDetails = auth.user.restrictions.plans;
|
||||
let allowedMediaTypes = allMediaTypes.filter((v) => planDetails[v]);
|
||||
/*if (auth.user.restrictions.plans.price === 0) {
|
||||
// TODO: remove following restrictions when duplication fixes
|
||||
const restrictedTemporary = ['news', 'blogs'];
|
||||
allowedMediaTypes = allowedMediaTypes.filter(
|
||||
(v) => !restrictedTemporary.includes(v)
|
||||
);
|
||||
} */
|
||||
this.props.actions.toggleMediaType(allowedMediaTypes, true);
|
||||
} else {
|
||||
this.props.actions.toggleMediaType([], true);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.location.pathname !== this.props.location.pathname) {
|
||||
this.checkIfTourGuide();
|
||||
}
|
||||
}
|
||||
|
||||
checkIfTourGuide = () => {
|
||||
const tourCurrentPaths = map(tourPages, 'showOn');
|
||||
const hasTour = tourCurrentPaths.some((path) =>
|
||||
this.props.location.pathname.startsWith(path)
|
||||
);
|
||||
|
||||
if (hasTour) {
|
||||
!this.state.showTourIcon && this.setState({ showTourIcon: true });
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.showTourIcon && this.setState({ showTourIcon: false });
|
||||
};
|
||||
|
||||
showWebTour = () => {
|
||||
const tourSendPaths = find(tourPages, (o) =>
|
||||
this.props.location.pathname.startsWith(o.showOn)
|
||||
);
|
||||
|
||||
if (tourSendPaths) {
|
||||
// Open in a new tab to reset every redux state
|
||||
const win = window.open(`${tourSendPaths.to}?webtour=true`, '_blank');
|
||||
win.focus();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { store, actions, children, t } = this.props;
|
||||
const { common: commonState, appState } = store;
|
||||
const { sidebar, themeOptions } = appState;
|
||||
const { base, auth } = commonState;
|
||||
|
||||
const {
|
||||
colorScheme,
|
||||
enableFixedHeader,
|
||||
enableFixedSidebar,
|
||||
enableFixedFooter,
|
||||
enableClosedSidebar,
|
||||
closedSmallerSidebar,
|
||||
enableMobileMenu,
|
||||
enablePageTabsAlt
|
||||
} = themeOptions;
|
||||
|
||||
if (!auth.token) {
|
||||
<LoadersAdvanced />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ResizeDetector
|
||||
handleWidth
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
render={({ width }) => {
|
||||
return (
|
||||
<DndProvider backend={DnDBackend}>
|
||||
<div
|
||||
className={cx(
|
||||
'app-container app-theme-' + colorScheme,
|
||||
{ 'fixed-header': enableFixedHeader },
|
||||
{ 'fixed-sidebar': enableFixedSidebar || width < 1250 },
|
||||
{ 'fixed-footer': enableFixedFooter },
|
||||
{ 'closed-sidebar': enableClosedSidebar || width < 1250 },
|
||||
{
|
||||
'closed-sidebar-mobile':
|
||||
closedSmallerSidebar || width < 1250
|
||||
},
|
||||
{ 'sidebar-mobile-open': enableMobileMenu },
|
||||
{ 'body-tabs-shadow-btn': enablePageTabsAlt }
|
||||
)}
|
||||
>
|
||||
{this.state.showTourIcon && (
|
||||
<div>
|
||||
<Button
|
||||
id="GuidedTour"
|
||||
className="floating-icon"
|
||||
color="warning"
|
||||
onClick={this.showWebTour}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faQuestion}
|
||||
color="#573a04"
|
||||
fixedWidth={false}
|
||||
size="2x"
|
||||
/>
|
||||
</Button>
|
||||
<UncontrolledTooltip placement="left" target={'GuidedTour'}>
|
||||
{t('userSettings.guidedTourTooltip')}
|
||||
</UncontrolledTooltip>
|
||||
</div>
|
||||
)}
|
||||
<AppHeader
|
||||
appCommonState={base}
|
||||
userFirstName={auth.user.firstName}
|
||||
userLastName={auth.user.lastName}
|
||||
restrictions={auth.user.restrictions}
|
||||
userRole={auth.user.role}
|
||||
actions={actions}
|
||||
themeOptions={themeOptions}
|
||||
/>
|
||||
|
||||
<div className="app-main">
|
||||
<Sidebar
|
||||
t={t}
|
||||
sidebarState={sidebar}
|
||||
themeOptions={themeOptions}
|
||||
actions={actions}
|
||||
/>
|
||||
<div className="app-main__outer">
|
||||
<div className="app-main__inner">
|
||||
{children}
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<WebTour />
|
||||
</div>
|
||||
</DndProvider>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const applyDecorators = compose(
|
||||
translate(['common'], { wait: true }),
|
||||
withRouter,
|
||||
reduxConnect()
|
||||
);
|
||||
|
||||
export default applyDecorators(App);
|
||||
@@ -0,0 +1,110 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import HeaderSettings from './HeaderSettings';
|
||||
import SettingsPopup from './SettingsPopup';
|
||||
import cx from 'classnames';
|
||||
import MainTabsLinks from './MainTabsLinks';
|
||||
import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup';
|
||||
import HeaderLogo from './HeaderLogo';
|
||||
import HeaderDots from './HeaderDots';
|
||||
|
||||
export class AppHeader extends React.Component {
|
||||
static propTypes = {
|
||||
appCommonState: PropTypes.object.isRequired,
|
||||
actions: PropTypes.object.isRequired,
|
||||
userFirstName: PropTypes.string,
|
||||
userLastName: PropTypes.string,
|
||||
userRole: PropTypes.string.isRequired,
|
||||
restrictions: PropTypes.object.isRequired,
|
||||
themeOptions: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
state = {
|
||||
active: false,
|
||||
mobile: false,
|
||||
activeSecondaryMenuMobile: false
|
||||
};
|
||||
|
||||
toggleResponsiveMenu = () => {
|
||||
this.props.actions.toggleSidebar();
|
||||
};
|
||||
|
||||
activeSearchFunc = () => {
|
||||
this.setState({ active: !this.state.active });
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
appCommonState,
|
||||
restrictions,
|
||||
actions,
|
||||
userFirstName,
|
||||
userLastName,
|
||||
themeOptions
|
||||
} = this.props;
|
||||
const mainTabs = Object.keys(appCommonState.tabs);
|
||||
|
||||
const {
|
||||
headerBackgroundColor,
|
||||
enableHeaderShadow,
|
||||
enableMobileMenuSmall
|
||||
} = themeOptions;
|
||||
|
||||
const settingsPopupVisible = appCommonState.isSettingsPopupVisible;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<CSSTransitionGroup
|
||||
component="div"
|
||||
className={cx('app-header', headerBackgroundColor, {
|
||||
'header-shadow': enableHeaderShadow
|
||||
})}
|
||||
transitionName="HeaderAnimation"
|
||||
transitionAppear
|
||||
transitionAppearTimeout={1500}
|
||||
transitionEnter={false}
|
||||
transitionLeave={false}
|
||||
>
|
||||
<HeaderLogo />
|
||||
<div
|
||||
className={cx('app-header__content', {
|
||||
'header-mobile-open': enableMobileMenuSmall
|
||||
})}
|
||||
>
|
||||
<div className="app-header-left" data-tour="app-header-left">
|
||||
<MainTabsLinks
|
||||
tabs={appCommonState.tabs}
|
||||
restrictions={restrictions}
|
||||
actions={actions}
|
||||
/>
|
||||
</div>
|
||||
<div className="app-header-right">
|
||||
<HeaderDots
|
||||
mainTabs={mainTabs}
|
||||
restrictions={restrictions}
|
||||
planDetails={restrictions.plans}
|
||||
/>
|
||||
<HeaderSettings
|
||||
isThereSomethingNew={appCommonState.isThereSomethingNew}
|
||||
langs={appCommonState.langs}
|
||||
userFirstName={userFirstName}
|
||||
userLastName={userLastName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{settingsPopupVisible && (
|
||||
<SettingsPopup
|
||||
hidePopup={actions.hideUserSettingsPopup}
|
||||
setErrorMsg={actions.setSettingsPopupError}
|
||||
changePassword={actions.changeUserPassword}
|
||||
errorMsg={appCommonState.settingsPopupError}
|
||||
/>
|
||||
)}
|
||||
</CSSTransitionGroup>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AppHeader;
|
||||
@@ -0,0 +1,99 @@
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Fragment } from 'react';
|
||||
import { Slider } from 'react-burgers';
|
||||
import cx from 'classnames';
|
||||
import { faEllipsisV } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Button } from 'reactstrap';
|
||||
import reduxConnect from '../../../redux/utils/connect';
|
||||
import translate from 'react-i18next/dist/commonjs/translate';
|
||||
import { compose } from 'redux';
|
||||
|
||||
class AppMobileMenu extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
active: false,
|
||||
mobile: false,
|
||||
activeSecondaryMenuMobile: false
|
||||
};
|
||||
}
|
||||
|
||||
toggleMobileSidebar = () => {
|
||||
const { setEnableMobileMenu } = this.props.actions;
|
||||
const { enableMobileMenu } = this.props.appState.themeOptions;
|
||||
setEnableMobileMenu(!enableMobileMenu);
|
||||
};
|
||||
|
||||
toggleMobileSmall = () => {
|
||||
const { setEnableMobileMenuSmall } = this.props.actions;
|
||||
const { enableMobileMenuSmall } = this.props.appState.themeOptions;
|
||||
setEnableMobileMenuSmall(!enableMobileMenuSmall);
|
||||
};
|
||||
|
||||
state = {
|
||||
openLeft: false,
|
||||
openRight: false,
|
||||
relativeWidth: false,
|
||||
width: 280,
|
||||
noTouchOpen: false,
|
||||
noTouchClose: false
|
||||
};
|
||||
|
||||
changeActive = () => {
|
||||
this.setState({ active: !this.state.active });
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="app-header__mobile-menu" data-tour="mobile-left-menu">
|
||||
<div onClick={this.toggleMobileSidebar}>
|
||||
<Slider
|
||||
width={26}
|
||||
lineHeight={2}
|
||||
lineSpacing={5}
|
||||
color="#6c757d"
|
||||
active={this.state.active}
|
||||
onClick={this.changeActive}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="app-header__menu">
|
||||
<span onClick={this.toggleMobileSmall}>
|
||||
<Button
|
||||
size="sm"
|
||||
className={cx('btn-icon btn-icon-only', {
|
||||
active: this.state.activeSecondaryMenuMobile
|
||||
})}
|
||||
color="primary"
|
||||
onClick={() =>
|
||||
this.setState({
|
||||
activeSecondaryMenuMobile: !this.state
|
||||
.activeSecondaryMenuMobile
|
||||
})
|
||||
}
|
||||
>
|
||||
<div className="btn-icon-wrapper">
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</div>
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AppMobileMenu.propTypes = {
|
||||
actions: PropTypes.object,
|
||||
appState: PropTypes.object
|
||||
};
|
||||
|
||||
const applyDecorators = compose(
|
||||
reduxConnect('appState', ['appState']),
|
||||
translate(['tabsContent'], { wait: true })
|
||||
);
|
||||
|
||||
export default applyDecorators(AppMobileMenu);
|
||||
@@ -0,0 +1,168 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
UncontrolledDropdown,
|
||||
DropdownToggle,
|
||||
DropdownMenu,
|
||||
Col,
|
||||
Row,
|
||||
Button,
|
||||
DropdownItem
|
||||
} from 'reactstrap';
|
||||
|
||||
import { IoIosGrid } from 'react-icons/io';
|
||||
import Notifications from './Notifications';
|
||||
import LangSettingsMenu from './LangSettingsMenu';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faAngleDown } from '@fortawesome/free-solid-svg-icons';
|
||||
import { planRoutes } from '../Account/Plans/UserPlans';
|
||||
import { convertUTCtoLocal } from '../../../common/helper';
|
||||
|
||||
class HeaderDots extends React.Component {
|
||||
static propTypes = {
|
||||
mainTabs: PropTypes.array.isRequired,
|
||||
restrictions: PropTypes.object.isRequired,
|
||||
planDetails: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
validateTab(tab) {
|
||||
if (tab === 'analyze') {
|
||||
if (!this.props.restrictions) {
|
||||
return false;
|
||||
}
|
||||
const permissions = this.props.restrictions.permissions;
|
||||
return permissions.analytics;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { t, mainTabs, planDetails, restrictions } = this.props;
|
||||
const isFreeAccount = planDetails.price === 0;
|
||||
const isRTL = document.documentElement.dir === 'rtl';
|
||||
|
||||
return (
|
||||
<div className="header-dots">
|
||||
<UncontrolledDropdown nav inNavbar>
|
||||
<DropdownToggle nav>
|
||||
{t('plans.currentPlan')}
|
||||
<FontAwesomeIcon className="ml-2 opacity-5" icon={faAngleDown} />
|
||||
</DropdownToggle>
|
||||
<DropdownMenu
|
||||
className={`dropdown-menu-rounded rm-pointers${
|
||||
isRTL ? ' dropdown-menu-left' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="dropdown-menu-header">
|
||||
<div className="dropdown-menu-header-inner bg-success">
|
||||
<div className="menu-header-image opacity-1"></div>
|
||||
<div className="menu-header-content text-left">
|
||||
<h5 className="menu-header-title font-weight-bold">
|
||||
{isFreeAccount
|
||||
? t('plans.freeBasicAccount')
|
||||
: `$${planDetails.price}`}
|
||||
</h5>
|
||||
{!isFreeAccount && (
|
||||
<p>
|
||||
{restrictions.subStartDate && restrictions.subEndDate
|
||||
? `${convertUTCtoLocal(
|
||||
isRTL
|
||||
? restrictions.subEndDate
|
||||
: restrictions.subStartDate,
|
||||
'MMM D, YYYY'
|
||||
)} - ${convertUTCtoLocal(
|
||||
isRTL
|
||||
? restrictions.subStartDate
|
||||
: restrictions.subEndDate,
|
||||
'MMM D, YYYY'
|
||||
)}`
|
||||
: t('plans.perMonth')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DropdownItem
|
||||
tag={Link}
|
||||
to={`/app/plans/${planRoutes.update}`}
|
||||
className="font-weight-bold"
|
||||
>
|
||||
<i className="dropdown-icon lnr-rocket opacity-8"> </i>
|
||||
{t('plans.upgradePlan')}
|
||||
</DropdownItem>
|
||||
<DropdownItem tag={Link} to={`/app/plans/${planRoutes.txn}`}>
|
||||
<i className="dropdown-icon lnr-list"> </i>
|
||||
{t('plans.yourTransactions')}
|
||||
</DropdownItem>
|
||||
{!isFreeAccount && (
|
||||
<DropdownItem
|
||||
tag={Link}
|
||||
to={`/app/plans/${planRoutes.changeCard}`}
|
||||
>
|
||||
<i className="dropdown-icon lnr-license"> </i>
|
||||
{t('plans.changeCard')}
|
||||
</DropdownItem>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
</UncontrolledDropdown>
|
||||
<Button
|
||||
tag={Link}
|
||||
to={`/app/plans/${planRoutes.update}`}
|
||||
size="sm"
|
||||
outline
|
||||
color="success"
|
||||
className="align-self-center mr-3 d-none d-lg-block"
|
||||
>
|
||||
{t('plans.upgradePlan')}
|
||||
</Button>
|
||||
<UncontrolledDropdown className="d-block d-lg-none">
|
||||
<DropdownToggle className="p-0 mr-2" color="link">
|
||||
<div className="icon-wrapper icon-wrapper-alt rounded-circle">
|
||||
<div className="icon-wrapper-bg bg-primary" />
|
||||
<IoIosGrid color="#3f6ad8" fontSize="23px" />
|
||||
</div>
|
||||
</DropdownToggle>
|
||||
<DropdownMenu
|
||||
className={`rm-pointers${isRTL ? ' dropdown-menu-left' : ''}`}
|
||||
>
|
||||
<div className="grid-menu grid-menu-xl grid-menu-3col">
|
||||
{mainTabs.map((tab, i) => {
|
||||
if (!this.validateTab(tab)) return null;
|
||||
return (
|
||||
<Col md="12" key={`main-tab-link-${i}`}>
|
||||
<Button
|
||||
className="btn-icon-vertical btn-square btn-transition"
|
||||
outline
|
||||
color="link"
|
||||
>
|
||||
<Link to={'/app/' + tab} className="nav-link">
|
||||
<Row>
|
||||
<i
|
||||
className={
|
||||
i
|
||||
? 'lnr lnr-exit-up btn-icon-wrapper mr-1'
|
||||
: 'lnr-magnifier btn-icon-wrapper mr-1'
|
||||
}
|
||||
></i>
|
||||
<p>{t('tabs.' + tab)}</p>
|
||||
</Row>
|
||||
</Link>
|
||||
</Button>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</DropdownMenu>
|
||||
</UncontrolledDropdown>
|
||||
<LangSettingsMenu />
|
||||
<Notifications />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['common'], { wait: true })(HeaderDots);
|
||||
@@ -0,0 +1,74 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import { Slider } from 'react-burgers'
|
||||
|
||||
import { compose } from 'redux'
|
||||
import reduxConnect from '../../../redux/utils/connect'
|
||||
import translate from 'react-i18next/dist/commonjs/translate'
|
||||
import AppMobileMenu from './AppMobileMenu'
|
||||
|
||||
class HeaderLogo extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
active: false,
|
||||
mobile: false,
|
||||
activeSecondaryMenuMobile: false
|
||||
}
|
||||
}
|
||||
|
||||
toggleEnableClosedSidebar = () => {
|
||||
const { setEnableClosedSidebar } = this.props.actions
|
||||
const { enableClosedSidebar } = this.props.appState.themeOptions
|
||||
setEnableClosedSidebar(!enableClosedSidebar)
|
||||
}
|
||||
|
||||
state = {
|
||||
openLeft: false,
|
||||
openRight: false,
|
||||
relativeWidth: false,
|
||||
width: 280,
|
||||
noTouchOpen: false,
|
||||
noTouchClose: false
|
||||
}
|
||||
|
||||
changeActive = () => {
|
||||
this.setState({ active: !this.state.active })
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="app-header__logo">
|
||||
<div className="logo-src ml-0 ml-lg-2" />
|
||||
<div className="header__pane ml-auto">
|
||||
<div onClick={this.toggleEnableClosedSidebar}>
|
||||
<Slider
|
||||
width={26}
|
||||
lineHeight={2}
|
||||
lineSpacing={5}
|
||||
color="#6c757d"
|
||||
active={this.state.active}
|
||||
onClick={this.changeActive}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AppMobileMenu />
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HeaderLogo.propTypes = {
|
||||
actions: PropTypes.object,
|
||||
appState: PropTypes.object
|
||||
}
|
||||
|
||||
const applyDecorators = compose(
|
||||
reduxConnect('appState', ['appState']),
|
||||
translate(['tabsContent'], { wait: true })
|
||||
)
|
||||
|
||||
export default applyDecorators(HeaderLogo)
|
||||
@@ -0,0 +1,84 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import UserSettingsMenu from './UserSettingsMenu';
|
||||
import { DropdownToggle, DropdownMenu, Dropdown } from 'reactstrap';
|
||||
import { faAngleDown, faUser } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
export class HeaderSettings extends React.Component {
|
||||
static propTypes = {
|
||||
userFirstName: PropTypes.string.isRequired,
|
||||
userLastName: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
state = {
|
||||
isOpen: false
|
||||
};
|
||||
|
||||
toggleUserSettingsDrop = () => {
|
||||
this.setState((prev) => ({ isOpen: !prev.isOpen }));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { userFirstName, userLastName } = this.props;
|
||||
const isRTL = document.documentElement.dir === 'rtl';
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="header-btn-lg pr-0">
|
||||
<div className="widget-content p-0">
|
||||
<div className="widget-content-wrapper">
|
||||
<div className="widget-content-left">
|
||||
<Dropdown
|
||||
isOpen={this.state.isOpen}
|
||||
toggle={this.toggleUserSettingsDrop}
|
||||
>
|
||||
<DropdownToggle
|
||||
color="link"
|
||||
title="User Profile"
|
||||
className="d-flex align-items-center p-0"
|
||||
data-tour="app-header-user-settings"
|
||||
>
|
||||
<div className="user-profile">
|
||||
<FontAwesomeIcon
|
||||
className="user-profile-icon"
|
||||
icon={faUser}
|
||||
/>
|
||||
</div>
|
||||
{window.outerWidth >= 768 && (
|
||||
<FontAwesomeIcon
|
||||
className="ml-2 opacity-8"
|
||||
icon={faAngleDown}
|
||||
/>
|
||||
)}
|
||||
</DropdownToggle>
|
||||
<DropdownMenu
|
||||
className={`rm-pointers dropdown-menu-lg${
|
||||
isRTL ? ' dropdown-menu-left' : ''
|
||||
}`}
|
||||
>
|
||||
<UserSettingsMenu
|
||||
toggleMenu={this.toggleUserSettingsDrop}
|
||||
userFirstName={userFirstName}
|
||||
userLastName={userLastName}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div className="widget-content-left ml-3 header-user-info">
|
||||
<div className="widget-heading">
|
||||
{userFirstName + ' ' + userLastName}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['common'], { wait: true })(
|
||||
React.memo(HeaderSettings)
|
||||
);
|
||||
@@ -0,0 +1,100 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { compose } from 'redux';
|
||||
import { translate } from 'react-i18next';
|
||||
import i18n from '../../../i18n';
|
||||
import {
|
||||
UncontrolledDropdown,
|
||||
DropdownToggle,
|
||||
DropdownMenu,
|
||||
DropdownItem
|
||||
} from 'reactstrap';
|
||||
import reduxConnect from '../../../redux/utils/connect';
|
||||
|
||||
import Flag from 'react-flagkit';
|
||||
|
||||
const langCountry = {
|
||||
en: 'US',
|
||||
ar: 'SA',
|
||||
fr: 'FR'
|
||||
};
|
||||
|
||||
function LangSettingsMenu(props) {
|
||||
const {
|
||||
t,
|
||||
base: { langs, activeLang },
|
||||
actions,
|
||||
direction = ''
|
||||
} = props;
|
||||
|
||||
const chooseLang = (e) => {
|
||||
const newLang = e.target.dataset.lang;
|
||||
actions.chooseLanguage(newLang);
|
||||
i18n.changeLanguage(newLang);
|
||||
};
|
||||
const isRTL = document.documentElement.dir === 'rtl';
|
||||
|
||||
const dropDownProps = {};
|
||||
if (direction) {
|
||||
dropDownProps.direction = direction;
|
||||
}
|
||||
|
||||
return (
|
||||
<UncontrolledDropdown {...dropDownProps}>
|
||||
<DropdownToggle className="p-0 mr-2" color="link">
|
||||
<div className="icon-wrapper icon-wrapper-alt rounded-circle">
|
||||
<div className="icon-wrapper-bg bg-focus" />
|
||||
<div className="language-icon">
|
||||
<Flag
|
||||
className="mr-3 opacity-8"
|
||||
country={langCountry[activeLang]}
|
||||
size="40"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownToggle>
|
||||
<DropdownMenu
|
||||
className={`rm-pointers${isRTL ? ' dropdown-menu-left' : ''}`}
|
||||
>
|
||||
<div className="dropdown-menu-header">
|
||||
<div className="dropdown-menu-header-inner pt-4 pb-4 bg-focus">
|
||||
<div className="menu-header-content text-center text-white">
|
||||
<h6 className="menu-header-subtitle mt-0">
|
||||
{t('langs.chooseLanguage')}
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{langs.map((lang, i) => {
|
||||
const translateTarget = 'langs.' + lang;
|
||||
return (
|
||||
<DropdownItem
|
||||
key={lang}
|
||||
active={activeLang === lang}
|
||||
data-lang={lang}
|
||||
onClick={chooseLang}
|
||||
>
|
||||
<Flag className="mr-3 opacity-8" country={langCountry[lang]} />
|
||||
{t(translateTarget)}
|
||||
</DropdownItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenu>
|
||||
</UncontrolledDropdown>
|
||||
);
|
||||
}
|
||||
|
||||
LangSettingsMenu.propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
base: PropTypes.object.isRequired,
|
||||
actions: PropTypes.object.isRequired,
|
||||
direction: PropTypes.string
|
||||
};
|
||||
|
||||
const applyDecorators = compose(
|
||||
reduxConnect('base', ['common', 'base']),
|
||||
translate(['common'], { wait: true })
|
||||
);
|
||||
|
||||
export default applyDecorators(LangSettingsMenu);
|
||||
@@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
import { Nav, NavItem } from 'reactstrap';
|
||||
import cl from 'classnames';
|
||||
|
||||
export class MainTabsLinks extends React.Component {
|
||||
static propTypes = {
|
||||
tabs: PropTypes.object.isRequired,
|
||||
restrictions: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
actions: PropTypes.object.isRequired,
|
||||
location: PropTypes.object
|
||||
};
|
||||
|
||||
validateTab = (tab) => {
|
||||
if (tab === 'analyze') {
|
||||
if (!this.props.restrictions) {
|
||||
// to prevent: permissions of `undefined`
|
||||
return false;
|
||||
}
|
||||
const permissions = this.props.restrictions.permissions;
|
||||
return permissions.analytics;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
showUpgradeModal = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.actions.toggleUpgradeModal();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, tabs, location } = this.props;
|
||||
|
||||
return (
|
||||
<Nav className="header-megamenu">
|
||||
{Object.keys(tabs).map((tab, i) => {
|
||||
const firstSubTab =
|
||||
tabs[tab].items && tabs[tab].items[0] ? tabs[tab].items[0].url : '';
|
||||
|
||||
if (!this.validateTab(tab)) {
|
||||
return (
|
||||
<NavItem key={tab}>
|
||||
<a
|
||||
href="#"
|
||||
onClick={this.showUpgradeModal}
|
||||
className={cl('nav-link', {
|
||||
active: location.pathname.startsWith(`/app/${tab}`)
|
||||
})}
|
||||
>
|
||||
<i className={`nav-link-icon ${tabs[tab].icon}`}> </i>
|
||||
<p>{t('tabs.' + tab)}</p>
|
||||
</a>
|
||||
</NavItem>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<NavItem key={tab}>
|
||||
<Link
|
||||
to={`/app/${tab}/${firstSubTab}`}
|
||||
className={cl('nav-link', {
|
||||
active: location.pathname.startsWith(`/app/${tab}`)
|
||||
})}
|
||||
>
|
||||
<i className={`nav-link-icon ${tabs[tab].icon}`}> </i>
|
||||
<p>{t('tabs.' + tab)}</p>
|
||||
</Link>
|
||||
</NavItem>
|
||||
);
|
||||
})}
|
||||
</Nav>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['common'], { wait: true })(
|
||||
withRouter(React.memo(MainTabsLinks))
|
||||
);
|
||||
@@ -0,0 +1,160 @@
|
||||
import React, { Fragment, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import city3 from '../../../styles/utils/images/dropdown-header/city3.jpg';
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar';
|
||||
import reduxConnect from '../../../redux/utils/connect';
|
||||
import cl from 'classnames';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownToggle,
|
||||
Nav,
|
||||
NavItem,
|
||||
UncontrolledDropdown
|
||||
} from 'reactstrap';
|
||||
import { Interpolate, translate } from 'react-i18next';
|
||||
import { compose } from 'redux';
|
||||
import { IoIosNotificationsOutline } from 'react-icons/io';
|
||||
|
||||
function Notifications({ alerts, t, actions }) {
|
||||
const [alertsList, setAlertsList] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
// Empty list when mounts
|
||||
actions.removeAllAlerts();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const newAlerts = alerts
|
||||
.reverse()
|
||||
.map((alert) => {
|
||||
return typeof alert === 'string' ? { message: alert } : alert;
|
||||
})
|
||||
.map((alert) => {
|
||||
const interpolateParameters = alert ? alert.parameters : {};
|
||||
const i18nKey = alert && `alerts.${alert.type}.${alert.transKey}`;
|
||||
let type, msg;
|
||||
|
||||
type = alert.type ? oldValueMapping[alert.type] : 'warning';
|
||||
|
||||
msg = t(i18nKey, {
|
||||
...interpolateParameters,
|
||||
defaultValue: alert.message || t('error.unknown')
|
||||
});
|
||||
|
||||
return { type, msg };
|
||||
});
|
||||
|
||||
setAlertsList(newAlerts);
|
||||
}, [alerts.length]);
|
||||
|
||||
const isRTL = document.documentElement.dir === 'rtl';
|
||||
|
||||
return (
|
||||
<UncontrolledDropdown>
|
||||
<DropdownToggle className="p-0 mr-2" color="link">
|
||||
<div className="icon-wrapper icon-wrapper-alt rounded-circle">
|
||||
<div className="icon-wrapper-bg bg-danger" />
|
||||
<IoIosNotificationsOutline color="#d92550" fontSize="23px" />
|
||||
<div className="badge badge-dot badge-dot-sm badge-danger">
|
||||
{alertsList.length > 0 ? t('userSettings.notifications') : ''}
|
||||
</div>
|
||||
</div>
|
||||
</DropdownToggle>
|
||||
<DropdownMenu
|
||||
className={cl('dropdown-menu-xl rm-pointers', {
|
||||
'py-0': alertsList.length < 1,
|
||||
'dropdown-menu-left': isRTL
|
||||
})}
|
||||
>
|
||||
<div className="dropdown-menu-header mb-0">
|
||||
<div className="dropdown-menu-header-inner bg-deep-blue">
|
||||
<div
|
||||
className="menu-header-image opacity-1"
|
||||
style={{
|
||||
backgroundImage: 'url(' + city3 + ')'
|
||||
}}
|
||||
/>
|
||||
<div className="menu-header-content text-dark">
|
||||
<h5 className="menu-header-title">
|
||||
{t('userSettings.notifications')}
|
||||
</h5>
|
||||
<h6 className="menu-header-subtitle">
|
||||
<Interpolate
|
||||
i18nKey={
|
||||
alertsList.length > 1
|
||||
? 'userSettings.notificationsSub_plural'
|
||||
: 'userSettings.notificationsSub'
|
||||
}
|
||||
alertLength={alertsList.length}
|
||||
/>
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{alertsList.length > 0 && (
|
||||
<Fragment>
|
||||
<div className="scroll-area-md">
|
||||
<PerfectScrollbar>
|
||||
<div className="p-2">
|
||||
{alertsList.map((item, i) => (
|
||||
<Alert
|
||||
key={i}
|
||||
className="mb-2"
|
||||
style={{ wordBreak: 'break-word' }}
|
||||
color={colorsMapping[item.type]}
|
||||
>
|
||||
<p className="font-size-xs font-weight-bold text-uppercase">
|
||||
{item.type}
|
||||
</p>
|
||||
{item.msg}
|
||||
</Alert>
|
||||
))}
|
||||
</div>
|
||||
</PerfectScrollbar>
|
||||
</div>
|
||||
<Nav vertical>
|
||||
<NavItem className="nav-item-divider" />
|
||||
<NavItem className="nav-item-btn text-center">
|
||||
<Button
|
||||
size="sm"
|
||||
className="btn-shadow btn-wide btn-pill"
|
||||
color="focus"
|
||||
onClick={actions.removeAllAlerts}
|
||||
>
|
||||
{t('userSettings.clearAll')}
|
||||
</Button>
|
||||
</NavItem>
|
||||
</Nav>
|
||||
</Fragment>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
</UncontrolledDropdown>
|
||||
);
|
||||
}
|
||||
|
||||
const oldValueMapping = {
|
||||
notice: 'success',
|
||||
warning: 'warning',
|
||||
error: 'error'
|
||||
};
|
||||
|
||||
const colorsMapping = {
|
||||
success: 'success',
|
||||
warning: 'warning',
|
||||
error: 'danger'
|
||||
};
|
||||
|
||||
Notifications.propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
alerts: PropTypes.array.isRequired,
|
||||
actions: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const applyDecorators = compose(
|
||||
reduxConnect('alerts', ['common', 'alerts']),
|
||||
translate(['common'], { wait: true })
|
||||
);
|
||||
|
||||
export default applyDecorators(Notifications);
|
||||
@@ -0,0 +1,39 @@
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import cx from 'classnames'
|
||||
|
||||
class SearchBox extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
activeSearch: false
|
||||
}
|
||||
}
|
||||
|
||||
activeSearchFunc = () => {
|
||||
this.setState({ activeSearch: !this.state.activeSearch })
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={cx('search-wrapper', {
|
||||
active: this.state.activeSearch
|
||||
})}>
|
||||
<div className="input-holder">
|
||||
<input type="text" className="search-input" placeholder="Type to search" />
|
||||
<button onClick={this.activeSearchFunc}
|
||||
className="search-icon">
|
||||
<span />
|
||||
</button>
|
||||
</div>
|
||||
<button onClick={this.activeSearchFunc}
|
||||
className="close" />
|
||||
</div>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchBox
|
||||
@@ -0,0 +1,122 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import {
|
||||
Button,
|
||||
Label,
|
||||
Input,
|
||||
FormGroup,
|
||||
Modal,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter
|
||||
} from 'reactstrap';
|
||||
|
||||
export class SettingsPopup extends React.Component {
|
||||
static propTypes = {
|
||||
hidePopup: PropTypes.func.isRequired,
|
||||
setErrorMsg: PropTypes.func.isRequired,
|
||||
changePassword: PropTypes.func.isRequired,
|
||||
errorMsg: PropTypes.string,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
};
|
||||
}
|
||||
|
||||
hidePopup = () => {
|
||||
this.props.hidePopup();
|
||||
this.props.setErrorMsg(null);
|
||||
};
|
||||
|
||||
onSubmit = () => {
|
||||
const { t } = this.props;
|
||||
const { oldPassword, newPassword, confirmPassword } = this.state;
|
||||
|
||||
// need more validations
|
||||
if (!oldPassword || !newPassword || !confirmPassword) {
|
||||
return this.props.setErrorMsg(t('userSettings.enterRequiredFields'));
|
||||
}
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
return this.props.setErrorMsg(t('userSettings.passwordsNotMatched'));
|
||||
}
|
||||
|
||||
if (oldPassword && newPassword) {
|
||||
this.props.changePassword(newPassword, oldPassword);
|
||||
}
|
||||
};
|
||||
|
||||
handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
this.setState({ [name]: value });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, errorMsg } = this.props;
|
||||
|
||||
return (
|
||||
<Modal isOpen toggle={this.hidePopup} backdrop="static">
|
||||
<ModalHeader toggle={this.hidePopup}>
|
||||
{t('userSettings.changePassword')}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{t('userSettings.enterOldPassword')}
|
||||
<span className="text-danger">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="password"
|
||||
name="oldPassword"
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{t('userSettings.enterNewPassword')}
|
||||
<span className="text-danger">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="password"
|
||||
name="newPassword"
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{t('userSettings.retypeNewPassword')}
|
||||
<span className="text-danger">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="password"
|
||||
name="confirmPassword"
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<p className="text-danger">{errorMsg}</p>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button color="light" onClick={this.hidePopup}>
|
||||
{t('common:commonWords.Cancel')}
|
||||
</Button>
|
||||
<Button color="primary" onClick={this.onSubmit}>
|
||||
{t('userSettings.changePassword')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent', 'common'], { wait: true })(
|
||||
SettingsPopup
|
||||
);
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { translate } from 'react-i18next';
|
||||
|
||||
class SubTabWrapper extends React.Component {
|
||||
static propTypes = {
|
||||
activeTabName: PropTypes.string.isRequired,
|
||||
subTabs: PropTypes.array.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
children: PropTypes.object
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, activeTabName, subTabs, children } = this.props;
|
||||
|
||||
return (
|
||||
<div className="rc-tabs-top position-relative" key="sub-tab-wrapper">
|
||||
<div role="tablist" className="rc-tabs-bar" tabIndex="0">
|
||||
<div className="rc-tabs-nav-container">
|
||||
<div className="rc-tabs-nav-wrap mask-line pt-0">
|
||||
<div className="rc-tabs-nav-scroll">
|
||||
<div className="rc-tabs-nav rc-tabs-nav-animated">
|
||||
{subTabs &&
|
||||
subTabs.map((subTab) => {
|
||||
const tabText =
|
||||
activeTabName === 'dashboard'
|
||||
? subTab.title
|
||||
: t('tabs.' + subTab.title);
|
||||
const fullUrl =
|
||||
'/app/' + activeTabName + '/' + subTab.url;
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
to={fullUrl}
|
||||
key={subTab.url}
|
||||
activeClassName="rc-tabs-tab-active rc-tabs-ink-bar rc-tabs-ink-bar-animated"
|
||||
className="rc-tabs-tab"
|
||||
>
|
||||
{tabText}
|
||||
</NavLink>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['common'], { wait: true })(SubTabWrapper);
|
||||
@@ -0,0 +1,165 @@
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { compose } from 'redux';
|
||||
import { translate } from 'react-i18next';
|
||||
import { Nav, Button, NavItem, NavLink } from 'reactstrap';
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar';
|
||||
import city from '../../../images/city3.jpg';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faUser } from '@fortawesome/free-solid-svg-icons';
|
||||
import { reduxActions } from '../../../redux/utils/connect';
|
||||
import tourPages from './WebTourSteps';
|
||||
|
||||
import { useHistory } from 'react-router';
|
||||
import { planRoutes } from '../Account/Plans/UserPlans';
|
||||
|
||||
function UserSettingsMenu(props) {
|
||||
const { push } = useHistory();
|
||||
|
||||
function hideMenu() {
|
||||
props.toggleMenu();
|
||||
props.actions.setEnableMobileMenuSmall(false);
|
||||
}
|
||||
|
||||
function showUserSettings() {
|
||||
hideMenu();
|
||||
props.actions.showUserSettingsPopup();
|
||||
}
|
||||
|
||||
function onLogout() {
|
||||
hideMenu();
|
||||
props.actions.logout();
|
||||
}
|
||||
|
||||
function tourGuide(path) {
|
||||
const win = window.open(`${path}?webtour=true`, '_blank');
|
||||
win.focus();
|
||||
|
||||
// props.actions.toggleWebTour(); for dev
|
||||
}
|
||||
|
||||
function gotToActivePlan() {
|
||||
hideMenu();
|
||||
push(`/app/plans/${planRoutes.current}`);
|
||||
}
|
||||
|
||||
const { t } = props;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="dropdown-menu-header">
|
||||
<div className="dropdown-menu-header-inner bg-info">
|
||||
<div
|
||||
className="menu-header-image opacity-2"
|
||||
style={{
|
||||
backgroundImage: 'url(' + city + ')'
|
||||
}}
|
||||
/>
|
||||
<div className="menu-header-content text-left">
|
||||
<div className="widget-content p-0">
|
||||
<div className="widget-content-wrapper">
|
||||
<div className="widget-content-left mr-3">
|
||||
<div className="user-profile">
|
||||
<FontAwesomeIcon
|
||||
className="user-profile-icon"
|
||||
icon={faUser}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="widget-content-left">
|
||||
<div className="widget-heading">
|
||||
{props.userFirstName + ' ' + props.userLastName}{' '}
|
||||
</div>
|
||||
</div>
|
||||
<div className="widget-content-right ml-auto mr-2">
|
||||
<Button
|
||||
className="btn-pill btn-shadow btn-shine"
|
||||
color="focus"
|
||||
onClick={onLogout}
|
||||
>
|
||||
{t('userSettings.signOut')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <div className="scroll-area-xs"> */}
|
||||
<div>
|
||||
<PerfectScrollbar>
|
||||
<Nav vertical>
|
||||
<NavItem>
|
||||
<NavLink
|
||||
tag={Button}
|
||||
type="button"
|
||||
color="link"
|
||||
className="font-size-md w-100"
|
||||
onClick={gotToActivePlan}
|
||||
>
|
||||
{t('plans.activePlanDetails')}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<NavLink
|
||||
tag={Button}
|
||||
type="button"
|
||||
color="link"
|
||||
className="font-size-md w-100"
|
||||
onClick={showUserSettings}
|
||||
>
|
||||
{t('userSettings.changePassword')}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<NavLink
|
||||
className="font-size-md w-100"
|
||||
href="https://www.socialhose.io/en/user-guide"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{t('userSettings.userGuide')}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
<NavItem style={{ textAlign: 'start' }}>
|
||||
<div className="mt-2 mb-3 mx-3 px-1">
|
||||
<p className="text-muted font-size-md mb-2">{t('userSettings.guidedTourTooltip')}</p>
|
||||
<div className="d-flex flex-row flex-wrap pl-3">
|
||||
{tourPages.map((tour) => (
|
||||
<Button
|
||||
key={tour.name}
|
||||
className="btn-icon-vertical btn-transition btn-transition-alt pt-2 pb-2 mr-2"
|
||||
outline
|
||||
color="primary"
|
||||
onClick={() => tourGuide(tour.to)}
|
||||
>
|
||||
<i className={`${tour.icon} btn-icon-wrapper mb-2`} />
|
||||
{t(`userSettings.${tour.translateKey}`)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</NavItem>
|
||||
</Nav>
|
||||
</PerfectScrollbar>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
UserSettingsMenu.propTypes = {
|
||||
toggleMenu: PropTypes.func.isRequired,
|
||||
userFirstName: PropTypes.string.isRequired,
|
||||
userLastName: PropTypes.string.isRequired,
|
||||
actions: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const applyDecorators = compose(
|
||||
reduxActions(),
|
||||
translate(['common'], { wait: true })
|
||||
);
|
||||
|
||||
export default React.memo(applyDecorators(UserSettingsMenu));
|
||||
@@ -0,0 +1,120 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { find } from 'lodash';
|
||||
import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock';
|
||||
import Tour from 'reactour';
|
||||
import { useHistory, useLocation } from 'react-router';
|
||||
|
||||
import reduxConnect from '../../../redux/utils/connect';
|
||||
import tourPages from './WebTourSteps';
|
||||
|
||||
function WebTour({
|
||||
actions,
|
||||
store: {
|
||||
common: { base },
|
||||
appState: { themeOptions }
|
||||
}
|
||||
}) {
|
||||
const [hasSidebar, setHasSidebar] = useState(window.innerWidth > 991);
|
||||
const [tourData, setTourData] = useState({ content: [] });
|
||||
const location = useLocation();
|
||||
const { replace } = useHistory();
|
||||
|
||||
const { isTourOpen = false } = base;
|
||||
const params = new URLSearchParams(location.search);
|
||||
const webtour = params.get('webtour');
|
||||
|
||||
useEffect(() => {
|
||||
if (webtour) {
|
||||
const tour = find(tourPages, {
|
||||
to: location.pathname
|
||||
});
|
||||
|
||||
if (tour) {
|
||||
setTourData(tour);
|
||||
window.gtag && window.gtag('event', 'tutorial_begin', {
|
||||
name: tour.name
|
||||
});
|
||||
actions.toggleWebTour(); // open tour if param is available
|
||||
}
|
||||
} else {
|
||||
actions.toggleWebTour(); // close if param is removed
|
||||
}
|
||||
}, [webtour]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isTourOpen) {
|
||||
if (window.innerWidth > 991) {
|
||||
!hasSidebar && setHasSidebar(true);
|
||||
} else {
|
||||
hasSidebar && setHasSidebar(false);
|
||||
}
|
||||
}
|
||||
}, [window.innerWidth]);
|
||||
|
||||
const accentColor = '#0094bd';
|
||||
|
||||
function closeWebTour() {
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
|
||||
if (queryParams.has('webtour')) {
|
||||
queryParams.delete('webtour');
|
||||
replace({
|
||||
search: queryParams.toString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentStep(step) {
|
||||
const stepState = tourData.content;
|
||||
const stepDetails = stepState.find((v, i) => i === step);
|
||||
|
||||
if (step === stepState.length - 1) {
|
||||
window.gtag && window.gtag('event', 'tutorial_complete', {
|
||||
name: tourData.name
|
||||
});
|
||||
}
|
||||
|
||||
if (!hasSidebar) {
|
||||
if (stepDetails.needSidebar) {
|
||||
!themeOptions.enableMobileMenu && actions.setEnableMobileMenu(true);
|
||||
} else {
|
||||
themeOptions.enableMobileMenu && actions.setEnableMobileMenu(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function disableBody(target) {
|
||||
disableBodyScroll(target);
|
||||
}
|
||||
|
||||
function enableBody(target) {
|
||||
enableBodyScroll(target);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tour
|
||||
onRequestClose={closeWebTour}
|
||||
steps={tourData.content}
|
||||
getCurrentStep={getCurrentStep}
|
||||
isOpen={isTourOpen && tourData.content && tourData.content.length > 0}
|
||||
maskClassName="mask"
|
||||
className="helper"
|
||||
rounded={5}
|
||||
startAt={0}
|
||||
closeWithMask={false}
|
||||
accentColor={accentColor}
|
||||
onAfterOpen={disableBody}
|
||||
onBeforeClose={enableBody}
|
||||
disableFocusLock
|
||||
lastStepNextButton={<div className="btn btn-primary">Finish</div>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
WebTour.propTypes = {
|
||||
actions: PropTypes.object,
|
||||
store: PropTypes.object
|
||||
};
|
||||
|
||||
export default reduxConnect()(WebTour);
|
||||
@@ -0,0 +1,172 @@
|
||||
import React from 'react';
|
||||
import i18n from '../../../i18n';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
const baseKey = 'tabsContent:webtour';
|
||||
|
||||
const steps = [
|
||||
{
|
||||
selector: '',
|
||||
content: i18n.t(`${baseKey}.search.start`)
|
||||
},
|
||||
{
|
||||
selector: '[data-tour="left-panel"]',
|
||||
content: i18n.t(`${baseKey}.search.feedsView`),
|
||||
resizeObservables: ['[data-tour="left-panel"]'],
|
||||
needSidebar: true,
|
||||
stepInteraction: false
|
||||
},
|
||||
{
|
||||
selector: '[data-tour="app-header-left"]',
|
||||
content: () => (
|
||||
<Trans i18nKey={`${baseKey}.search.mainTabs`}>
|
||||
There are 3 main pages: <strong>Search</strong> to find content,
|
||||
<strong>Analyze</strong> to generate reports, and <strong>Share</strong>
|
||||
to distribute findings via alerts or webfeeds.
|
||||
</Trans>
|
||||
),
|
||||
onlyWeb: true,
|
||||
stepInteraction: false
|
||||
},
|
||||
{
|
||||
selector: '[data-tour="app-header-user-settings"]',
|
||||
content: i18n.t(`${baseKey}.search.userSettings`),
|
||||
stepInteraction: false
|
||||
},
|
||||
{
|
||||
selector: '[data-tour="search-licenses"]',
|
||||
content: i18n.t(`${baseKey}.search.license`),
|
||||
stepInteraction: false
|
||||
},
|
||||
{
|
||||
selector: '[data-tour="input-field-search"]',
|
||||
content: () => (
|
||||
<p>
|
||||
<Trans i18nKey={`${baseKey}.search.searchField`}>
|
||||
A simple boolean search looks like this:
|
||||
<strong>BMW AND Texas</strong>. Which will find all mentions of “bmw”
|
||||
and "texas”.
|
||||
</Trans>
|
||||
</p>
|
||||
)
|
||||
},
|
||||
{
|
||||
selector: '[data-tour="select-date-range"]',
|
||||
content: i18n.t(`${baseKey}.search.dateRange`),
|
||||
stepInteraction: false
|
||||
},
|
||||
{
|
||||
selector: '[data-tour="select-media-types"]',
|
||||
content: i18n.t(`${baseKey}.search.mediaChannels`)
|
||||
},
|
||||
{
|
||||
selector: '[data-tour="advanced-search"]',
|
||||
content: () => (
|
||||
<Trans i18nKey={`${baseKey}.search.advancedSearch`}>
|
||||
Click on <strong>Advanced Search</strong> to uncover the different
|
||||
options for your search.
|
||||
</Trans>
|
||||
),
|
||||
resizeObservables: ['[data-tour="advanced-search"]']
|
||||
},
|
||||
{
|
||||
selector: '[data-tour="advanced-search"]',
|
||||
content: () => (
|
||||
<Trans i18nKey={`${baseKey}.search.emphasis`}>
|
||||
<strong>Emphasis:</strong> Include or exclude specific words or phrases
|
||||
in the headline of a news article or a blog post.
|
||||
</Trans>
|
||||
),
|
||||
resizeObservables: ['[data-tour="advanced-search-content"]']
|
||||
},
|
||||
{
|
||||
selector: '[data-tour="advanced-search"]',
|
||||
content: () => (
|
||||
<Trans i18nKey={`${baseKey}.search.languages`}>
|
||||
<strong>Languages:</strong> Capture the content that is tagged with the
|
||||
following language(s).
|
||||
</Trans>
|
||||
),
|
||||
resizeObservables: ['[data-tour="advanced-search-content"]']
|
||||
},
|
||||
{
|
||||
selector: '[data-tour="advanced-search"]',
|
||||
content: () => (
|
||||
<Trans i18nKey={`${baseKey}.search.locations`}>
|
||||
<strong>Locations:</strong> Include or exclude content that is geotagged
|
||||
with the following countries or US States.
|
||||
</Trans>
|
||||
),
|
||||
resizeObservables: ['[data-tour="advanced-search-content"]']
|
||||
},
|
||||
{
|
||||
selector: '[data-tour="advanced-search"]',
|
||||
content: () => (
|
||||
<Trans i18nKey={`${baseKey}.search.extras`}>
|
||||
<strong>Extras:</strong> Only show posts with images.
|
||||
</Trans>
|
||||
),
|
||||
resizeObservables: ['[data-tour="advanced-search-content"]']
|
||||
},
|
||||
/* {
|
||||
selector: '[data-tour="search-button"]',
|
||||
content: () => (
|
||||
<Fragment>
|
||||
Click <strong>Search icon</strong>.
|
||||
</Fragment>
|
||||
)
|
||||
}, */
|
||||
{
|
||||
selector: '[data-tour="search-buttons"]',
|
||||
content: i18n.t(`${baseKey}.search.saveSearch`),
|
||||
stepInteraction: false
|
||||
}
|
||||
];
|
||||
|
||||
const analyticsSteps = [
|
||||
{
|
||||
selector: '',
|
||||
content: i18n.t(`${baseKey}.analytics.start`)
|
||||
},
|
||||
{
|
||||
selector: '[data-tour="left-panel"]',
|
||||
content: i18n.t(`${baseKey}.analytics.dragFeed`),
|
||||
resizeObservables: ['[data-tour="left-panel"]'],
|
||||
needSidebar: true
|
||||
},
|
||||
{
|
||||
selector: '[data-tour="drop-feeds-box"]',
|
||||
highlightedSelectors: ['[data-tour="left-panel"]'],
|
||||
content: i18n.t(`${baseKey}.analytics.drop`)
|
||||
},
|
||||
{
|
||||
selector: '[data-tour="analytics-data-range"]',
|
||||
content: i18n.t(`${baseKey}.analytics.dateRange`),
|
||||
observe: '.DateRangePickerInput'
|
||||
},
|
||||
{
|
||||
selector: '[data-tour="create-analytics-button"]',
|
||||
content: i18n.t(`${baseKey}.analytics.create`)
|
||||
}
|
||||
];
|
||||
|
||||
const tourPages = [
|
||||
{
|
||||
translateKey: 'HowToSearch',
|
||||
name: 'How to Search',
|
||||
icon: 'pe-7s-search',
|
||||
to: '/app/search/search',
|
||||
showOn: '/app/search/search',
|
||||
content: steps
|
||||
},
|
||||
{
|
||||
translateKey: 'HowToAnalyze',
|
||||
name: 'How to Analyze',
|
||||
icon: 'pe-7s-graph1',
|
||||
to: '/app/analyze/create',
|
||||
showOn: '/app/analyze',
|
||||
content: analyticsSteps
|
||||
}
|
||||
];
|
||||
|
||||
export default tourPages;
|
||||
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
Label,
|
||||
Input,
|
||||
ModalFooter
|
||||
} from 'reactstrap';
|
||||
|
||||
export class AddCategoryPopup extends React.Component {
|
||||
state = {
|
||||
folderName: ''
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
parentId: PropTypes.number.isRequired,
|
||||
hideAddCategoryPopup: PropTypes.func.isRequired,
|
||||
addCategory: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
onChangeName = (e) => {
|
||||
const { value } = e.target; // need validation
|
||||
this.setState({ folderName: value });
|
||||
};
|
||||
|
||||
hidePopup = () => {
|
||||
this.props.hideAddCategoryPopup();
|
||||
};
|
||||
|
||||
onSubmit = () => {
|
||||
const { folderName } = this.state;
|
||||
this.props.addCategory(folderName, this.props.parentId);
|
||||
this.props.hideAddCategoryPopup();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
const { folderName } = this.state;
|
||||
|
||||
return (
|
||||
<Modal isOpen toggle={this.hidePopup} backdrop="static">
|
||||
<ModalHeader toggle={this.hidePopup}>
|
||||
{t('sidebarPopup.addFolderBtn')}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<Label>{t('sidebarPopup.enterFolderName')}</Label>
|
||||
<Input type="text" value={folderName} onChange={this.onChangeName} />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="light" onClick={this.hidePopup}>
|
||||
{t('commonWords.Cancel')}
|
||||
</Button>
|
||||
<Button color="primary" onClick={this.onSubmit}>
|
||||
{t('sidebarPopup.addFolderBtn')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['common'], { wait: true })(AddCategoryPopup);
|
||||
@@ -0,0 +1,121 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import Select from 'react-select';
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
FormGroup,
|
||||
Label,
|
||||
Input,
|
||||
ModalFooter
|
||||
} from 'reactstrap';
|
||||
|
||||
export class AddClippingsFeedPopup extends React.Component {
|
||||
static propTypes = {
|
||||
parentId: PropTypes.number.isRequired,
|
||||
hidePopup: PropTypes.func.isRequired,
|
||||
addClippingsFeed: PropTypes.func.isRequired,
|
||||
addAlert: PropTypes.func.isRequired,
|
||||
categories: PropTypes.array.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
parentId: props.parentId,
|
||||
feedName: ''
|
||||
};
|
||||
}
|
||||
|
||||
onChangeName = (e) => {
|
||||
const { value } = e.target;
|
||||
this.setState({ feedName: value });
|
||||
};
|
||||
|
||||
hidePopup = () => {
|
||||
this.props.hidePopup();
|
||||
};
|
||||
|
||||
onSubmit = () => {
|
||||
const { parentId } = this.state;
|
||||
const { addAlert, addClippingsFeed, hidePopup } = this.props;
|
||||
const { feedName } = this.state;
|
||||
if (feedName) {
|
||||
addClippingsFeed(feedName, parentId);
|
||||
hidePopup();
|
||||
} else {
|
||||
addAlert({
|
||||
type: 'error',
|
||||
transKey: 'feedNameEmpty'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
flattenCategories = (categories, level = '') => {
|
||||
return categories.reduce((result, category) => {
|
||||
result.push({
|
||||
label:
|
||||
level +
|
||||
this.props.t(`sidebar.${category.name}`, {
|
||||
defaultValue: category.name
|
||||
}),
|
||||
value: category.id
|
||||
});
|
||||
if (category.childes && category.childes.length) {
|
||||
return result.concat(
|
||||
this.flattenCategories(category.childes, '- ' + level)
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
};
|
||||
|
||||
onParentCategorySelect = (value) => {
|
||||
this.setState({ parentId: value });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, categories } = this.props;
|
||||
const { parentId, feedName } = this.state;
|
||||
const options = this.flattenCategories(categories);
|
||||
|
||||
return (
|
||||
<Modal isOpen toggle={this.hidePopup} backdrop="static">
|
||||
<ModalHeader toggle={this.hidePopup}>
|
||||
{t('sidebarPopup.addClippingsFeed')}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<FormGroup>
|
||||
<Label>{t('sidebarPopup.feedName')}</Label>
|
||||
<Input type="text" value={feedName} onChange={this.onChangeName} />
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Label>{t('sidebarPopup.folder')}</Label>
|
||||
<Select
|
||||
onChange={this.onParentCategorySelect}
|
||||
options={options}
|
||||
value={parentId}
|
||||
editable={false}
|
||||
clearable={false}
|
||||
simpleValue
|
||||
/>
|
||||
</FormGroup>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="light" onClick={this.hidePopup}>
|
||||
{t('commonWords.Cancel')}
|
||||
</Button>
|
||||
<Button color="primary" onClick={this.onSubmit}>
|
||||
{t('sidebarPopup.addClippingsFeed')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['common'], { wait: true })(AddClippingsFeedPopup);
|
||||
@@ -0,0 +1,58 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Category from './Category'
|
||||
|
||||
export class Categories extends React.Component {
|
||||
static propTypes = {
|
||||
actions: PropTypes.object.isRequired,
|
||||
areCategoriesLoaded: PropTypes.bool.isRequired,
|
||||
areFeedsFiltered: PropTypes.bool.isRequired,
|
||||
categories: PropTypes.array.isRequired,
|
||||
filteredCategories: PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
hideParentCategoryDrop = () => {}; //empty func for first level categories
|
||||
|
||||
render () {
|
||||
const { areCategoriesLoaded, areFeedsFiltered, actions } = this.props
|
||||
const {
|
||||
showDeletePopup, showRenamePopup, showAddCategoryPopup,
|
||||
showAddClippingsFeedPopup, getFeedResults,
|
||||
moveCategory, moveFeed, toggleExportFeed,
|
||||
toggleExportCategory, clipArticles
|
||||
} = actions
|
||||
|
||||
const categories = areFeedsFiltered ? this.props.filteredCategories : this.props.categories
|
||||
|
||||
return (
|
||||
<div className='sidebar-categories'>
|
||||
|
||||
{areCategoriesLoaded &&
|
||||
categories.map((category, i) => {
|
||||
return (
|
||||
<Category
|
||||
hideParentCategoryDrop={this.hideParentCategoryDrop} //set empty func
|
||||
parentId={-1} //set empty parent category for first level categories
|
||||
category={category}
|
||||
categories={categories}
|
||||
showDeletePopup={showDeletePopup}
|
||||
showRenamePopup={showRenamePopup}
|
||||
showAddCategoryPopup={showAddCategoryPopup}
|
||||
showAddClippingsFeedPopup={showAddClippingsFeedPopup}
|
||||
getFeedResults={getFeedResults}
|
||||
moveCategory={moveCategory}
|
||||
moveFeed={moveFeed}
|
||||
clipArticles={clipArticles}
|
||||
key={'main-category' + i}
|
||||
toggleExportFeed={toggleExportFeed}
|
||||
toggleExportCategory={toggleExportCategory}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Categories
|
||||
@@ -0,0 +1,219 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { DropTarget, DragSource } from 'react-dnd';
|
||||
import { compose } from 'redux';
|
||||
import onClickOutside from 'react-onclickoutside';
|
||||
import Feed from './Feed';
|
||||
import CategoryHead from './CategoryHead';
|
||||
import { TYPES } from '../../../redux/modules/appState/sidebar';
|
||||
import cx from 'classnames';
|
||||
|
||||
const folderSource = {
|
||||
beginDrag(props) {
|
||||
return {
|
||||
type: TYPES.FOLDER,
|
||||
id: props.category.id,
|
||||
category: props.category
|
||||
};
|
||||
},
|
||||
|
||||
canDrag(props) {
|
||||
return props.category.type === 'directory';
|
||||
}
|
||||
};
|
||||
|
||||
const targetTypes = [TYPES.FEED, TYPES.FOLDER];
|
||||
const categoryTarget = {
|
||||
drop(props, monitor) {
|
||||
if (monitor.didDrop()) return;
|
||||
const { category, moveCategory, moveFeed } = props;
|
||||
|
||||
const item = monitor.getItem();
|
||||
const draggedCategoryId = item.id;
|
||||
const newCategoryId = category.id;
|
||||
|
||||
if (item.type === TYPES.FOLDER) {
|
||||
moveCategory(item.category, newCategoryId);
|
||||
} else if (item.type === TYPES.FEED) {
|
||||
moveFeed(draggedCategoryId, newCategoryId);
|
||||
}
|
||||
},
|
||||
|
||||
canDrop(props, monitor) {
|
||||
const categoryType = props.category.type;
|
||||
return (
|
||||
categoryType !== 'deleted_content' && categoryType !== 'shared_content'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export class CategoryClass extends React.Component {
|
||||
static propTypes = {
|
||||
parentId: PropTypes.number.isRequired,
|
||||
category: PropTypes.object.isRequired,
|
||||
showDeletePopup: PropTypes.func.isRequired,
|
||||
showRenamePopup: PropTypes.func.isRequired,
|
||||
showAddCategoryPopup: PropTypes.func.isRequired,
|
||||
showAddClippingsFeedPopup: PropTypes.func.isRequired,
|
||||
hideParentCategoryDrop: PropTypes.func.isRequired,
|
||||
categories: PropTypes.array.isRequired,
|
||||
connectDropTarget: PropTypes.func.isRequired,
|
||||
connectDragSource: PropTypes.func.isRequired,
|
||||
getFeedResults: PropTypes.func.isRequired,
|
||||
moveFeed: PropTypes.func.isRequired,
|
||||
moveCategory: PropTypes.func.isRequired,
|
||||
clipArticles: PropTypes.func.isRequired,
|
||||
toggleExportFeed: PropTypes.func.isRequired,
|
||||
toggleExportCategory: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isCategoryActive: true, // sub menus
|
||||
isCategoryDropActive: false // more options
|
||||
};
|
||||
}
|
||||
|
||||
// hide category dropdown if there was click outside
|
||||
handleClickOutside = () => {
|
||||
this.state.isCategoryDropActive && this.hideCategoryDropdown();
|
||||
};
|
||||
|
||||
toggleCollapse = (e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
this.setState((prev) => ({
|
||||
isCategoryActive: !prev.isCategoryActive
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
toggleCategoryDropdown = (e) => {
|
||||
e.preventDefault();
|
||||
// this.props.hideParentCategoryDrop();
|
||||
this.setState((prev) => ({
|
||||
isCategoryDropActive: !prev.isCategoryDropActive
|
||||
}));
|
||||
};
|
||||
|
||||
hideCategoryDropdown = () => {
|
||||
this.setState({
|
||||
isCategoryDropActive: false
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
category,
|
||||
categories,
|
||||
connectDropTarget,
|
||||
connectDragSource,
|
||||
hideParentCategoryDrop,
|
||||
parentId,
|
||||
showDeletePopup,
|
||||
getFeedResults,
|
||||
showRenamePopup,
|
||||
showAddCategoryPopup,
|
||||
moveCategory,
|
||||
moveFeed,
|
||||
showAddClippingsFeedPopup,
|
||||
clipArticles,
|
||||
toggleExportFeed,
|
||||
toggleExportCategory
|
||||
} = this.props;
|
||||
|
||||
const isFeeds = category.feeds.length > 0;
|
||||
const isChildes = category.childes.length > 0;
|
||||
const categoryType = category.type;
|
||||
|
||||
let categoryActiveClass = this.state.isCategoryActive
|
||||
? ' active-category'
|
||||
: '';
|
||||
|
||||
return connectDragSource(
|
||||
connectDropTarget(
|
||||
<li
|
||||
className={'metismenu-item ' + categoryType + categoryActiveClass}
|
||||
onClick={hideParentCategoryDrop}
|
||||
>
|
||||
<CategoryHead
|
||||
toggleCollapse={this.toggleCollapse}
|
||||
toggleCategoryDropdown={this.toggleCategoryDropdown}
|
||||
isCategoryDropActive={this.state.isCategoryDropActive}
|
||||
isCategoryActive={this.state.isCategoryActive}
|
||||
hideDropDown={this.hideCategoryDropdown}
|
||||
parentId={parentId}
|
||||
category={category}
|
||||
showDeletePopup={showDeletePopup}
|
||||
showRenamePopup={showRenamePopup}
|
||||
showAddCategoryPopup={showAddCategoryPopup}
|
||||
toggleExportCategory={toggleExportCategory}
|
||||
showAddClippingsFeedPopup={showAddClippingsFeedPopup}
|
||||
categories={categories}
|
||||
/>
|
||||
|
||||
<ul
|
||||
className={cx('metismenu-container', {
|
||||
visible: this.state.isCategoryActive
|
||||
})}
|
||||
>
|
||||
{isFeeds &&
|
||||
category.feeds.map((feed, i) => {
|
||||
return (
|
||||
<Feed
|
||||
key={'feed' + i}
|
||||
feed={feed}
|
||||
showDeletePopup={showDeletePopup}
|
||||
showRenamePopup={showRenamePopup}
|
||||
categories={categories}
|
||||
categoryId={category.id}
|
||||
hideParentCategoryDrop={this.hideCategoryDropdown}
|
||||
getFeedResults={getFeedResults}
|
||||
clipArticles={clipArticles}
|
||||
toggleExportFeed={toggleExportFeed}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{isChildes &&
|
||||
category.childes.map((_category, i) => {
|
||||
return (
|
||||
<Category
|
||||
key={'category' + i}
|
||||
showDeletePopup={showDeletePopup}
|
||||
showRenamePopup={showRenamePopup}
|
||||
showAddCategoryPopup={showAddCategoryPopup}
|
||||
showAddClippingsFeedPopup={showAddClippingsFeedPopup}
|
||||
parentId={category.id}
|
||||
category={_category}
|
||||
categories={categories}
|
||||
hideParentCategoryDrop={this.hideCategoryDropdown}
|
||||
getFeedResults={getFeedResults}
|
||||
connectDropTarget={connectDropTarget}
|
||||
connectDragSource={connectDragSource}
|
||||
moveCategory={moveCategory}
|
||||
moveFeed={moveFeed}
|
||||
clipArticles={clipArticles}
|
||||
toggleExportFeed={toggleExportFeed}
|
||||
toggleExportCategory={toggleExportCategory}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</li>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const Category = compose(
|
||||
DropTarget(targetTypes, categoryTarget, (connect, monitor) => ({
|
||||
connectDropTarget: connect.dropTarget(),
|
||||
itemType: monitor.getItemType()
|
||||
})),
|
||||
DragSource(TYPES.FOLDER, folderSource, (connect) => ({
|
||||
connectDragSource: connect.dragSource()
|
||||
}))
|
||||
)(onClickOutside(CategoryClass));
|
||||
|
||||
export default Category;
|
||||
@@ -0,0 +1,105 @@
|
||||
import React from 'react';
|
||||
import cx from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import SidebarDropdown from './SidebarDropdown';
|
||||
import { translate } from 'react-i18next';
|
||||
|
||||
class CategoryHead extends React.Component {
|
||||
static propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
showDeletePopup: PropTypes.func.isRequired,
|
||||
showRenamePopup: PropTypes.func.isRequired,
|
||||
showAddCategoryPopup: PropTypes.func.isRequired,
|
||||
showAddClippingsFeedPopup: PropTypes.func.isRequired,
|
||||
toggleCollapse: PropTypes.func.isRequired,
|
||||
toggleCategoryDropdown: PropTypes.func.isRequired,
|
||||
toggleExportCategory: PropTypes.func.isRequired,
|
||||
isCategoryDropActive: PropTypes.bool.isRequired,
|
||||
isCategoryActive: PropTypes.bool.isRequired,
|
||||
hideDropDown: PropTypes.func.isRequired,
|
||||
parentId: PropTypes.number.isRequired,
|
||||
category: PropTypes.object.isRequired,
|
||||
categories: PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
getSidebarName(name) {
|
||||
const catName = this.props.t(`sidebar.${name}`);
|
||||
if (catName === `sidebar.${name}`) {
|
||||
return name;
|
||||
}
|
||||
return catName;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
isCategoryActive,
|
||||
isCategoryDropActive,
|
||||
category,
|
||||
categories,
|
||||
showDeletePopup,
|
||||
showRenamePopup,
|
||||
showAddCategoryPopup,
|
||||
showAddClippingsFeedPopup,
|
||||
toggleExportCategory,
|
||||
hideDropDown
|
||||
} = this.props;
|
||||
|
||||
const isCategoryDeletedType = category.subType === 'deleted_content';
|
||||
const categoryAttrId = 'sidebar-category' + category.id;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="metismenu-link"
|
||||
id={categoryAttrId}
|
||||
onClick={this.props.toggleCollapse}
|
||||
>
|
||||
{/* <i className="sidebar-category__closed-icon" onClick={this.props.toggleCollapse}> </i>
|
||||
<i className="sidebar-category__open-icon" onClick={this.props.toggleCollapse}> </i> */}
|
||||
|
||||
{isCategoryDeletedType ? (
|
||||
<i className="metismenu-icon pe-7s-trash"></i>
|
||||
) : (
|
||||
<i className="metismenu-icon pe-7s-folder"></i>
|
||||
)}
|
||||
|
||||
{this.getSidebarName(category.name)}
|
||||
|
||||
{!isCategoryDeletedType && (
|
||||
<i
|
||||
tabIndex="0"
|
||||
className="metismenu-state-icon font-size-lg opacity-10 pe-7s-more mr-4"
|
||||
onClick={this.props.toggleCategoryDropdown}
|
||||
/>
|
||||
)}
|
||||
<i
|
||||
className={cx(
|
||||
'metismenu-state-icon pe-7s-angle-down pointer-events-none opacity-10',
|
||||
{
|
||||
'rotate-minus-90': isCategoryActive
|
||||
}
|
||||
)}
|
||||
/>
|
||||
|
||||
{isCategoryDropActive && (
|
||||
<SidebarDropdown
|
||||
parentAttrId={categoryAttrId}
|
||||
categories={categories}
|
||||
itemId={category.id}
|
||||
itemSubType={category.subType}
|
||||
itemType={category.type}
|
||||
itemName={category.name}
|
||||
parentId={this.props.parentId}
|
||||
showDeletePopup={showDeletePopup}
|
||||
showRenamePopup={showRenamePopup}
|
||||
showAddCategoryPopup={showAddCategoryPopup}
|
||||
showAddClippingsPopup={showAddClippingsFeedPopup}
|
||||
hideDropDown={hideDropDown}
|
||||
toggleExportCategory={toggleExportCategory}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['common'], { wait: true })(CategoryHead);
|
||||
@@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
|
||||
|
||||
export class DeletePopup extends React.Component {
|
||||
static propTypes = {
|
||||
itemToDelete: PropTypes.object.isRequired,
|
||||
hideDeletePopup: PropTypes.func.isRequired,
|
||||
deleteFeed: PropTypes.func.isRequired,
|
||||
deleteCategory: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
hidePopup = () => {
|
||||
this.props.hideDeletePopup();
|
||||
};
|
||||
|
||||
onSubmit = () => {
|
||||
const {
|
||||
itemToDelete,
|
||||
deleteCategory,
|
||||
deleteFeed,
|
||||
hideDeletePopup
|
||||
} = this.props;
|
||||
switch (this.props.itemToDelete.itemType) {
|
||||
case 'feed':
|
||||
deleteFeed(itemToDelete.itemId, itemToDelete.parentId);
|
||||
break;
|
||||
case 'directory':
|
||||
deleteCategory(itemToDelete.itemId);
|
||||
break;
|
||||
}
|
||||
hideDeletePopup();
|
||||
};
|
||||
|
||||
render() {
|
||||
const itemName = this.props.itemToDelete.itemName;
|
||||
const itemType = this.props.itemToDelete.itemType;
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<Modal isOpen toggle={this.hidePopup} backdrop="static">
|
||||
<ModalHeader toggle={this.hidePopup}>
|
||||
{t('commonWords.Confirm')}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<p>
|
||||
{t('messages.deleteMessage')} {itemType + ' "' + itemName + '"'}
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="light" onClick={this.hidePopup}>
|
||||
{t('commonWords.Cancel')}
|
||||
</Button>
|
||||
<Button color="danger" onClick={this.onSubmit}>
|
||||
{t('commonWords.Delete')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['common'], { wait: true })(DeletePopup);
|
||||
@@ -0,0 +1,162 @@
|
||||
/** DRAG SOURCE **/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { compose } from 'redux';
|
||||
import SidebarDropdown from './SidebarDropdown';
|
||||
import { DragSource, DropTarget } from 'react-dnd';
|
||||
import onClickOutside from 'react-onclickoutside';
|
||||
import { TYPES } from '../../../redux/modules/appState/sidebar';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
const feedSource = {
|
||||
beginDrag(props) {
|
||||
return {
|
||||
type: TYPES.FEED,
|
||||
id: props.feed.id,
|
||||
feed: props.feed,
|
||||
currentCategoryId: props.categoryId
|
||||
};
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Specifies which props to inject into component from Drag n Drop.
|
||||
*/
|
||||
function dragCollect(connect) {
|
||||
return {
|
||||
// Call this function inside render()
|
||||
// to let React DnD handle the drag events:
|
||||
connectDragSource: connect.dragSource()
|
||||
};
|
||||
}
|
||||
|
||||
/** DROP TARGET **/
|
||||
const feedTarget = {
|
||||
drop(props, monitor) {
|
||||
if (monitor.didDrop()) return;
|
||||
const { feed, clipArticles } = props;
|
||||
clipArticles(feed.id);
|
||||
},
|
||||
|
||||
canDrop(props, monitor) {
|
||||
return props.feed.subType === 'clip_feed';
|
||||
}
|
||||
};
|
||||
|
||||
function dropCollect(connect, monitor) {
|
||||
return {
|
||||
connectDropTarget: connect.dropTarget()
|
||||
};
|
||||
}
|
||||
|
||||
export class Feed extends React.Component {
|
||||
static propTypes = {
|
||||
feed: PropTypes.object.isRequired,
|
||||
categoryId: PropTypes.number.isRequired,
|
||||
categories: PropTypes.array.isRequired,
|
||||
showDeletePopup: PropTypes.func.isRequired,
|
||||
showRenamePopup: PropTypes.func.isRequired,
|
||||
hideParentCategoryDrop: PropTypes.func.isRequired,
|
||||
connectDragSource: PropTypes.func.isRequired,
|
||||
connectDropTarget: PropTypes.func.isRequired,
|
||||
getFeedResults: PropTypes.func.isRequired,
|
||||
clipArticles: PropTypes.func.isRequired,
|
||||
toggleExportFeed: PropTypes.func.isRequired,
|
||||
history: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isItemDropActive: false
|
||||
};
|
||||
}
|
||||
|
||||
//hide feed dropdown if there was click outside
|
||||
handleClickOutside = () => {
|
||||
this.state.isItemDropActive && this.hideDropDown();
|
||||
};
|
||||
|
||||
hideDropDown = () => {
|
||||
this.setState({
|
||||
isItemDropActive: false
|
||||
});
|
||||
};
|
||||
|
||||
toggleItemDropdown = (e) => {
|
||||
e.preventDefault();
|
||||
this.setState({
|
||||
isItemDropActive: !this.state.isItemDropActive
|
||||
});
|
||||
};
|
||||
|
||||
onFeedClick = (e) => {
|
||||
const { history, getFeedResults, feed } = this.props;
|
||||
e.preventDefault();
|
||||
history.push('/app/search/search');
|
||||
getFeedResults({ page: 1 }, feed.id);
|
||||
window.scrollTo(0, 0);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
feed,
|
||||
categoryId,
|
||||
categories,
|
||||
connectDragSource,
|
||||
connectDropTarget,
|
||||
showDeletePopup,
|
||||
showRenamePopup,
|
||||
toggleExportFeed
|
||||
} = this.props;
|
||||
const feedAttrId = 'sidebar-feed' + feed.id;
|
||||
const dragAndDrop = compose(connectDragSource, connectDropTarget);
|
||||
|
||||
return dragAndDrop(
|
||||
<li
|
||||
id={feedAttrId}
|
||||
onClick={this.props.hideParentCategoryDrop}
|
||||
className="metismenu-item"
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
className={`metismenu-link feed-icon ${feed.class}`}
|
||||
onClick={this.onFeedClick}
|
||||
>
|
||||
{feed.name}
|
||||
</a>
|
||||
|
||||
<i
|
||||
tabIndex="0"
|
||||
className="metismenu-state-icon font-size-lg opacity-10 pe-7s-more"
|
||||
onClick={this.toggleItemDropdown}
|
||||
></i>
|
||||
|
||||
{this.state.isItemDropActive && (
|
||||
<SidebarDropdown
|
||||
parentAttrId={feedAttrId}
|
||||
categories={categories}
|
||||
itemId={feed.id}
|
||||
itemType={feed.type}
|
||||
itemSubType={feed.subType}
|
||||
itemName={feed.name}
|
||||
itemExported={feed.exported}
|
||||
parentId={categoryId}
|
||||
showDeletePopup={showDeletePopup}
|
||||
showRenamePopup={showRenamePopup}
|
||||
toggleExportFeed={toggleExportFeed}
|
||||
hideDropDown={this.hideDropDown}
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const applyDecorators = compose(
|
||||
withRouter,
|
||||
DragSource(TYPES.FEED, feedSource, dragCollect),
|
||||
DropTarget([TYPES.CLIP_ARTICLE], feedTarget, dropCollect),
|
||||
onClickOutside
|
||||
);
|
||||
|
||||
export default applyDecorators(Feed);
|
||||
@@ -0,0 +1,106 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
|
||||
export class Filter extends React.Component {
|
||||
static propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
areFeedsFiltered: PropTypes.bool.isRequired,
|
||||
categories: PropTypes.array.isRequired,
|
||||
setFilteredCategories: PropTypes.func.isRequired,
|
||||
clearFilteredCategories: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
sidebarAnimationDisabled: true,
|
||||
activeSearch: false
|
||||
}
|
||||
}
|
||||
|
||||
activeSearchFunc = () => {
|
||||
this.setState({ activeSearch: !this.state.activeSearch })
|
||||
this.clearFilter()
|
||||
}
|
||||
|
||||
filterCategoriesList = (
|
||||
categories,
|
||||
searchQuery,
|
||||
setParentBranchMatchFromParent
|
||||
) => {
|
||||
// show category if there is feed
|
||||
return categories.filter((category) => {
|
||||
category.branchMatch = false
|
||||
|
||||
//function that sets parent branchMatch prop
|
||||
function setParentBranchMatch (flag) {
|
||||
category.branchMatch = flag
|
||||
}
|
||||
|
||||
if (category.childes.length > 0) {
|
||||
category.childes = this.filterCategoriesList(
|
||||
category.childes,
|
||||
searchQuery,
|
||||
setParentBranchMatch
|
||||
)
|
||||
}
|
||||
|
||||
// filter feeds in category
|
||||
category.feeds = category.feeds.filter((feed) => {
|
||||
return feed.name.toLowerCase().indexOf(searchQuery) !== -1
|
||||
})
|
||||
// if this category is a child and it has matched feeds or its child have, then we set branchMatch prop of parent
|
||||
if (
|
||||
(category.feeds.length > 0 && setParentBranchMatchFromParent) ||
|
||||
(category.branchMatch && setParentBranchMatchFromParent)
|
||||
) {
|
||||
setParentBranchMatchFromParent(true)
|
||||
}
|
||||
|
||||
return category.branchMatch || category.feeds.length > 0
|
||||
})
|
||||
}
|
||||
|
||||
filterSidebarItems = (e) => {
|
||||
const searchQuery = e.target.value.toLowerCase()
|
||||
const categoriesCopy = this.props.categories.slice(0)
|
||||
|
||||
if (searchQuery.length) {
|
||||
const filteredCat = this.filterCategoriesList(categoriesCopy, searchQuery)
|
||||
this.props.setFilteredCategories(filteredCat)
|
||||
} else {
|
||||
this.props.clearFilteredCategories()
|
||||
}
|
||||
}
|
||||
|
||||
clearFilter = () => {
|
||||
this.props.clearFilteredCategories()
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div
|
||||
className={classnames('search-wrapper mb-1', {
|
||||
active: this.state.activeSearch
|
||||
})}
|
||||
>
|
||||
<div className="input-holder">
|
||||
<input
|
||||
type="text"
|
||||
className="search-input"
|
||||
placeholder={this.props.t('common:sidebar.typeToSearch')}
|
||||
onKeyUp={this.filterSidebarItems}
|
||||
id="sidebar-search"
|
||||
/>
|
||||
<button onClick={this.activeSearchFunc} className="search-icon">
|
||||
<span />
|
||||
</button>
|
||||
</div>
|
||||
<button onClick={this.activeSearchFunc} className="close"></button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Filter
|
||||
@@ -0,0 +1,90 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate } from 'react-i18next'
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalHeader
|
||||
} from 'reactstrap'
|
||||
|
||||
export class RenamePopup extends React.Component {
|
||||
static propTypes = {
|
||||
itemToRename: PropTypes.object.isRequired,
|
||||
hideRenamePopup: PropTypes.func.isRequired,
|
||||
renameFeed: PropTypes.func.isRequired,
|
||||
renameCategory: PropTypes.func.isRequired,
|
||||
addAlert: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
itemName: props.itemToRename.itemName
|
||||
}
|
||||
}
|
||||
|
||||
hidePopup = () => {
|
||||
this.props.hideRenamePopup()
|
||||
}
|
||||
|
||||
onSubmit = () => {
|
||||
const newName = this.state.itemName
|
||||
const {
|
||||
itemToRename,
|
||||
renameFeed,
|
||||
renameCategory,
|
||||
hideRenamePopup
|
||||
} = this.props
|
||||
|
||||
switch (this.props.itemToRename.itemType) {
|
||||
case 'feed':
|
||||
renameFeed(itemToRename.itemId, newName, itemToRename.parentId)
|
||||
break
|
||||
case 'directory':
|
||||
renameCategory(itemToRename.itemId, newName, itemToRename.parentId)
|
||||
break
|
||||
}
|
||||
|
||||
hideRenamePopup()
|
||||
}
|
||||
|
||||
onChangeName = (e) => {
|
||||
const { value } = e.target // validation needed
|
||||
|
||||
this.setState({
|
||||
itemName: value
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const itemName = this.state.itemName
|
||||
const { t } = this.props
|
||||
|
||||
return (
|
||||
<Modal isOpen toggle={this.hidePopup} backdrop="static">
|
||||
<ModalHeader toggle={this.hidePopup}>
|
||||
{t('commonWords.Rename')}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<Label>{t('sidebarPopup.enterNamelabel')}</Label>
|
||||
<Input type="text" value={itemName} onChange={this.onChangeName} />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="light" onClick={this.hidePopup}>
|
||||
{t('commonWords.Cancel')}
|
||||
</Button>
|
||||
<Button color="primary" onClick={this.onSubmit}>
|
||||
{t('commonWords.Rename')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['common'], { wait: true })(RenamePopup)
|
||||
@@ -0,0 +1,155 @@
|
||||
import React, { Fragment } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import cx from 'classnames'
|
||||
import Categories from './Categories'
|
||||
import Filter from './Filter'
|
||||
import DeletePopup from './DeletePopup'
|
||||
import RenamePopup from './RenamePopup'
|
||||
import AddCategoryPopup from './AddCategoryPopup'
|
||||
import AddClippingsFeedPopup from './AddClippingsFeedPopup'
|
||||
import LoadersAdvanced from '../../common/Loader/Loader'
|
||||
import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup'
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
import HeaderLogo from '../AppHeader/HeaderLogo'
|
||||
|
||||
export class Sidebar extends React.Component {
|
||||
static propTypes = {
|
||||
actions: PropTypes.object.isRequired,
|
||||
themeOptions: PropTypes.object.isRequired,
|
||||
backgroundColor: PropTypes.string,
|
||||
backgroundImage: PropTypes.any,
|
||||
backgroundImageOpacity: PropTypes.any,
|
||||
enableBackgroundImage: PropTypes.any,
|
||||
enableMobileMenu: PropTypes.any,
|
||||
enableSidebarShadow: PropTypes.any,
|
||||
setEnableMobileMenu: PropTypes.func,
|
||||
t: PropTypes.func,
|
||||
sidebarState: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
sidebarAnimationDisabled: true,
|
||||
activeSearch: false
|
||||
}
|
||||
}
|
||||
|
||||
toggleMobileSidebar = () => {
|
||||
let { enableMobileMenu, setEnableMobileMenu } = this.props
|
||||
setEnableMobileMenu(!enableMobileMenu)
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
this.props.actions.getSidebarCategories()
|
||||
}
|
||||
|
||||
activeSearchFunc = () => {
|
||||
this.setState({ activeSearch: !this.state.activeSearch })
|
||||
}
|
||||
|
||||
render () {
|
||||
let {
|
||||
backgroundColor,
|
||||
enableBackgroundImage,
|
||||
enableSidebarShadow,
|
||||
backgroundImage,
|
||||
backgroundImageOpacity
|
||||
} = this.props.themeOptions
|
||||
|
||||
const { sidebarState, actions } = this.props
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div
|
||||
className="sidebar-mobile-overlay"
|
||||
onClick={this.toggleMobileSidebar}
|
||||
/>
|
||||
<CSSTransitionGroup
|
||||
component="div"
|
||||
className={cx('app-sidebar', backgroundColor, {
|
||||
'sidebar-shadow': enableSidebarShadow
|
||||
})}
|
||||
transitionName="SidebarAnimation"
|
||||
transitionAppear
|
||||
transitionAppearTimeout={1500}
|
||||
transitionEnter={false}
|
||||
transitionLeave={false}
|
||||
>
|
||||
<HeaderLogo />
|
||||
{!sidebarState.areCategoriesLoaded && <LoadersAdvanced />}
|
||||
<PerfectScrollbar>
|
||||
<div className="app-sidebar__inner mt-3">
|
||||
<div className="vertical-nav-menu" data-tour="left-panel">
|
||||
<div className="metismenu-container">
|
||||
<Filter
|
||||
t={this.props.t}
|
||||
categories={sidebarState.categories}
|
||||
areFeedsFiltered={sidebarState.areFeedsFiltered}
|
||||
setFilteredCategories={actions.setFilteredCategories}
|
||||
clearFilteredCategories={actions.clearFilteredCategories}
|
||||
/>
|
||||
|
||||
<Categories
|
||||
actions={actions}
|
||||
areCategoriesLoaded={sidebarState.areCategoriesLoaded}
|
||||
areFeedsFiltered={sidebarState.areFeedsFiltered}
|
||||
categories={sidebarState.categories}
|
||||
filteredCategories={sidebarState.filteredCategories}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{sidebarState.popupVisible.delete && (
|
||||
<DeletePopup
|
||||
hideDeletePopup={actions.hideDeletePopup}
|
||||
deleteFeed={actions.deleteFeed}
|
||||
deleteCategory={actions.deleteCategory}
|
||||
itemToDelete={sidebarState.popupItems.delete}
|
||||
/>
|
||||
)}
|
||||
|
||||
{sidebarState.popupVisible.rename && (
|
||||
<RenamePopup
|
||||
addAlert={actions.addAlert}
|
||||
hideRenamePopup={actions.hideRenamePopup}
|
||||
renameFeed={actions.renameFeed}
|
||||
renameCategory={actions.renameCategory}
|
||||
itemToRename={sidebarState.popupItems.rename}
|
||||
/>
|
||||
)}
|
||||
|
||||
{sidebarState.popupVisible.addCategory && (
|
||||
<AddCategoryPopup
|
||||
hideAddCategoryPopup={actions.hideAddCategoryPopup}
|
||||
addCategory={actions.addCategory}
|
||||
parentId={sidebarState.popupItems.addCategory.parentId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{sidebarState.popupVisible.addClippingsFeed && (
|
||||
<AddClippingsFeedPopup
|
||||
parentId={sidebarState.popupItems.addClippingsFeed.parentId}
|
||||
hidePopup={actions.hideAddClippingsFeedPopup}
|
||||
addClippingsFeed={actions.addClippingsFeed}
|
||||
addAlert={actions.addAlert}
|
||||
categories={sidebarState.categories}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PerfectScrollbar>
|
||||
<div
|
||||
className={cx('app-sidebar-bg', backgroundImageOpacity)}
|
||||
style={{
|
||||
backgroundImage: enableBackgroundImage
|
||||
? 'url(' + backgroundImage + ')'
|
||||
: null
|
||||
}}
|
||||
></div>
|
||||
</CSSTransitionGroup>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default React.memo(Sidebar)
|
||||
@@ -0,0 +1,154 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import $ from 'jquery'
|
||||
import { translate } from 'react-i18next'
|
||||
|
||||
export class SidebarDropdown extends React.Component {
|
||||
static propTypes = {
|
||||
itemName: PropTypes.string.isRequired,
|
||||
itemSubType: PropTypes.string.isRequired,
|
||||
itemType: PropTypes.string.isRequired,
|
||||
itemId: PropTypes.number.isRequired,
|
||||
itemExported: PropTypes.bool,
|
||||
parentId: PropTypes.number.isRequired,
|
||||
parentAttrId: PropTypes.string.isRequired,
|
||||
showDeletePopup: PropTypes.func.isRequired,
|
||||
showRenamePopup: PropTypes.func.isRequired,
|
||||
showAddCategoryPopup: PropTypes.func,
|
||||
showAddClippingsPopup: PropTypes.func,
|
||||
toggleExportFeed: PropTypes.func,
|
||||
toggleExportCategory: PropTypes.func,
|
||||
t: PropTypes.func.isRequired,
|
||||
hideDropDown: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
dropdownTopPos: 'auto',
|
||||
dropdownBottomPos: 'auto',
|
||||
dropdownOpacity: 0
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
const topPos = $('#' + this.props.parentAttrId).offset().top - $(document).scrollTop()
|
||||
const dropdownHeight = $('#sidebar-category-dropdown').height()
|
||||
|
||||
if ($(window).height() - topPos >= dropdownHeight) {
|
||||
this.setState({
|
||||
dropdownTopPos: topPos,
|
||||
dropdownOpacity: 1
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
dropdownBottomPos: 5,
|
||||
dropdownOpacity: 1
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
onExportToggle = () => {
|
||||
const {itemId, toggleExportFeed, itemExported, hideDropDown} = this.props
|
||||
toggleExportFeed(itemId, !itemExported)
|
||||
hideDropDown()
|
||||
};
|
||||
|
||||
onExportCategoryToggle = () => {
|
||||
const {itemId, toggleExportCategory, itemExported, hideDropDown} = this.props
|
||||
toggleExportCategory(itemId, !itemExported)
|
||||
hideDropDown()
|
||||
};
|
||||
|
||||
onDelete = () => {
|
||||
this.props.showDeletePopup(this.props.itemId, this.props.itemType, this.props.itemName, this.props.parentId)
|
||||
};
|
||||
|
||||
onRename = () => {
|
||||
this.props.showRenamePopup(this.props.itemId, this.props.itemType, this.props.itemName, this.props.parentId)
|
||||
};
|
||||
|
||||
onAddCategory = () => {
|
||||
// set this item id as parent of new category
|
||||
this.props.showAddCategoryPopup(this.props.itemId)
|
||||
};
|
||||
|
||||
onAddClippingsFeedPopup = () => {
|
||||
this.props.showAddClippingsPopup(this.props.itemId)
|
||||
};
|
||||
|
||||
render () {
|
||||
const { itemSubType, t, itemExported } = this.props
|
||||
let dropdown
|
||||
|
||||
switch (itemSubType) {
|
||||
case 'my_content':
|
||||
dropdown = <ul id="sidebar-category-dropdown" className="sidebar-category__dropdown" style={{top: this.state.dropdownTopPos, bottom: this.state.dropdownBottomPos, opacity: this.state.dropdownOpacity}}>
|
||||
<li><a href="#" onClick={this.onAddClippingsFeedPopup}>{t('sidebarDropdown.AddClippingsFeed')}</a></li>
|
||||
<li><a href="#" onClick={this.onAddCategory}>{t('sidebarDropdown.AddFolder')}</a></li>
|
||||
{/*<li><a href="#">{t('sidebarDropdown.DownloadSearchCriteria')}</a></li>
|
||||
<li><a href="#">{t('sidebarDropdown.EditSearchTemplate')}</a></li>*/}
|
||||
<li><a href="#" onClick={this.onExportCategoryToggle}>{t(itemExported ? 'sidebarDropdown.UnexportFeeds' : 'sidebarDropdown.ExportFeeds')}</a></li>
|
||||
{/*<li><a href="#">{t('sidebarDropdown.ViewUserComments')}</a></li>*/}
|
||||
</ul>
|
||||
break
|
||||
|
||||
case 'shared_content':
|
||||
dropdown = <ul id="sidebar-category-dropdown" className="sidebar-category__dropdown" style={{top: this.state.dropdownTopPos, bottom: this.state.dropdownBottomPos, opacity: this.state.dropdownOpacity}}>
|
||||
<li><a href="#" onClick={this.onAddClippingsFeedPopup}>{t('sidebarDropdown.AddClippingsFeed')}</a></li>
|
||||
<li><a href="#" onClick={this.onAddCategory}>{t('sidebarDropdown.AddFolder')}</a></li>
|
||||
{/*<li><a href="#">{t('sidebarDropdown.DownloadSearchCriteria')}</a></li>*/}
|
||||
<li><a href="#" onClick={this.onExportCategoryToggle}>{t(itemExported ? 'sidebarDropdown.UnexportFeeds' : 'sidebarDropdown.ExportFeeds')}</a></li>
|
||||
{/*<li><a href="#">{t('sidebarDropdown.ViewUserComments')}</a></li>*/}
|
||||
</ul>
|
||||
break
|
||||
|
||||
case 'custom':
|
||||
dropdown = <ul id="sidebar-category-dropdown" className="sidebar-category__dropdown" style={{top: this.state.dropdownTopPos, bottom: this.state.dropdownBottomPos, opacity: this.state.dropdownOpacity}}>
|
||||
<li><a href="#" onClick={this.onAddClippingsFeedPopup}>{t('sidebarDropdown.AddClippingsFeed')}</a></li>
|
||||
<li><a href="#" onClick={this.onAddCategory}>{t('sidebarDropdown.AddFolder')}</a></li>
|
||||
{/*<li><a href="#">{t('sidebarDropdown.DownloadSearchCriteria')}</a></li>*/}
|
||||
<li><a href="#" onClick={this.onExportCategoryToggle}>{t(itemExported ? 'sidebarDropdown.UnexportFeeds' : 'sidebarDropdown.ExportFeeds')}</a></li>
|
||||
<li><a href="#" onClick={this.onRename}>{t('sidebarDropdown.RenameFolder')}</a></li>
|
||||
{/*<li><a href="#">{t('sidebarDropdown.ViewUserComments')}</a></li>*/}
|
||||
<li><a href="#" onClick={this.onDelete}>{t('sidebarDropdown.DeleteFolder')}</a></li>
|
||||
</ul>
|
||||
break
|
||||
|
||||
case 'query_feed':
|
||||
dropdown = <ul id="sidebar-category-dropdown" className="sidebar-category__dropdown" style={{top: this.state.dropdownTopPos, bottom: this.state.dropdownBottomPos, opacity: this.state.dropdownOpacity}}>
|
||||
{/*<li><a href="#">{t('sidebarDropdown.AddArticle')}</a></li>
|
||||
<li><a href="#">{t('sidebarDropdown.AddToDashboard')}</a></li>
|
||||
<li><a href="#">{t('sidebarDropdown.AnalyzeFeed')}</a></li>
|
||||
<li><a href="#">{t('sidebarDropdown.DownloadArticleData')}</a></li>
|
||||
<li><a href="#">{t('sidebarDropdown.DownloadFeedStatistics')}</a></li>
|
||||
<li><a href="#">{t('sidebarDropdown.DownloadSearchCriteria')}</a></li>*/}
|
||||
<li><a href="#" onClick={this.onExportToggle}>{t(itemExported ? 'sidebarDropdown.UnexportFeed' : 'sidebarDropdown.ExportFeed')}</a></li>
|
||||
<li><a href="#" onClick={this.onRename}>{t('sidebarDropdown.RenameFeed')}</a></li>
|
||||
<li><a href="#" onClick={this.onDelete}>{t('sidebarDropdown.DeleteFeed')}</a></li>
|
||||
</ul>
|
||||
break
|
||||
|
||||
case 'clip_feed':
|
||||
dropdown = <ul id="sidebar-category-dropdown" className="sidebar-category__dropdown" style={{top: this.state.dropdownTopPos, bottom: this.state.dropdownBottomPos, opacity: this.state.dropdownOpacity}}>
|
||||
{/*<li><a href="#">{t('sidebarDropdown.AddArticle')}</a></li>
|
||||
<li><a href="#">{t('sidebarDropdown.AddToDashboard')}</a></li>
|
||||
<li><a href="#">{t('sidebarDropdown.AnalyzeFeed')}</a></li>
|
||||
<li><a href="#">{t('sidebarDropdown.DownloadArticleData')}</a></li>
|
||||
<li><a href="#">{t('sidebarDropdown.DownloadFeedStatistics')}</a></li>
|
||||
<li><a href="#">{t('sidebarDropdown.DownloadSearchCriteria')}</a></li>*/}
|
||||
<li><a href="#" onClick={this.onExportToggle}>{t(itemExported ? 'sidebarDropdown.UnexportFeed' : 'sidebarDropdown.ExportFeed')}</a></li>
|
||||
<li><a href="#" onClick={this.onRename}>{t('sidebarDropdown.RenameFeed')}</a></li>
|
||||
<li><a href="#" onClick={this.onDelete}>{t('sidebarDropdown.DeleteFeed')}</a></li>
|
||||
</ul>
|
||||
break
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
dropdown
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['common'], { wait: true })(SidebarDropdown)
|
||||
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup';
|
||||
import SubTabWrapper from '../../AppHeader/SubTabWrapper';
|
||||
import { Redirect, Route, Switch, withRouter } from 'react-router-dom';
|
||||
import ShowCharts from './CreateAnalysisSubTab/ShowCharts';
|
||||
import SavedAnalysisSubTab from './SavedAnalysisSubTab/SavedAnalysisSubTab';
|
||||
import CreateAnalysisSubTab from './CreateAnalysisSubTab/CreateAnalysisSubTab';
|
||||
|
||||
function AnalyzeTab(props) {
|
||||
const { subTabs, allowAnalytics, history, activeTabName, match } = props;
|
||||
|
||||
if (!allowAnalytics) {
|
||||
history.push('/app/search/search');
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<CSSTransitionGroup
|
||||
component="div"
|
||||
transitionName="TabsAnimation"
|
||||
transitionAppear
|
||||
transitionAppearTimeout={0}
|
||||
transitionEnter={false}
|
||||
transitionLeave={false}
|
||||
>
|
||||
<SubTabWrapper activeTabName={activeTabName} subTabs={subTabs}>
|
||||
<Switch>
|
||||
{/* <Route path={`${match.url}/welcome`} component={WelcomeSubTab} /> */}
|
||||
<Route path={`${match.url}/saved`} component={SavedAnalysisSubTab} />
|
||||
<Route
|
||||
path={`${match.url}/create`}
|
||||
component={CreateAnalysisSubTab}
|
||||
/>
|
||||
<Route
|
||||
path={`${match.url}/edit/:id`}
|
||||
component={CreateAnalysisSubTab}
|
||||
/>
|
||||
<Route path={`${match.url}/:id`} component={ShowCharts} />
|
||||
<Redirect to={`${match.url}/saved`} />
|
||||
</Switch>
|
||||
</SubTabWrapper>
|
||||
</CSSTransitionGroup>
|
||||
);
|
||||
}
|
||||
|
||||
AnalyzeTab.propTypes = {
|
||||
activeTabName: PropTypes.string,
|
||||
children: PropTypes.any,
|
||||
history: PropTypes.object,
|
||||
match: PropTypes.object,
|
||||
allowAnalytics: PropTypes.bool,
|
||||
subTabs: PropTypes.array
|
||||
};
|
||||
|
||||
export default withRouter(AnalyzeTab);
|
||||
+293
@@ -0,0 +1,293 @@
|
||||
import React, { Fragment, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
FormGroup,
|
||||
Label,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalHeader
|
||||
} from 'reactstrap';
|
||||
import Select from 'react-select';
|
||||
import { Input, Checkbox, RadioButton } from '../../../../common/FormControls';
|
||||
import useForm from '../../../../common/hooks/useForm.js';
|
||||
import { EXTRAS } from '../../../../../redux/modules/appState/share/forms/alertForm';
|
||||
import { createAlertAPI } from '../../../../../api/analytics/createAnalytics';
|
||||
import { getCurrentTimezone, timezones } from '../../../../../common/Timezones';
|
||||
import { compose } from 'redux';
|
||||
import reduxConnect from '../../../../../redux/utils/connect';
|
||||
import translate from 'react-i18next/dist/commonjs/translate';
|
||||
import { THEME_TYPES } from '../../../../../redux/modules/appState/share/forms/notificationForm';
|
||||
|
||||
const initialForm = {
|
||||
name: '',
|
||||
recipients: [],
|
||||
subject: '',
|
||||
automatedSubject: false,
|
||||
unsubscribeNotification: false,
|
||||
published: false,
|
||||
allowUnsubscribe: false,
|
||||
articleExtracts: EXTRAS.CONTEXTUAL,
|
||||
highlight: false,
|
||||
showSourceCountry: false,
|
||||
showUserComments: false,
|
||||
themeType: THEME_TYPES.PLAIN,
|
||||
sendWhenEmpty: false,
|
||||
timezone: getCurrentTimezone(),
|
||||
notificationType: 'alert',
|
||||
// automatic: [], // auto schedule
|
||||
// sentUntil: '',
|
||||
errors: {
|
||||
name: null
|
||||
}
|
||||
};
|
||||
|
||||
function AlertDialog(props) {
|
||||
const { toggle, isOpen, alertCharts, actions, resetAlertChart, user } = props;
|
||||
const [loading, setLoading] = useState(false);
|
||||
const {
|
||||
form,
|
||||
handleChange,
|
||||
handleValidation,
|
||||
errors,
|
||||
validateSubmit,
|
||||
resetForm
|
||||
} = useForm(initialForm);
|
||||
|
||||
function handleSubmit() {
|
||||
const obj = validateSubmit();
|
||||
if (!obj) {
|
||||
return actions.addAlert({ type: 'error', transKey: 'requiredInfo' });
|
||||
}
|
||||
setLoading(true);
|
||||
if (obj.automatedSubject) {
|
||||
delete obj.subject;
|
||||
}
|
||||
|
||||
obj.sources = alertCharts.map((chart) => ({
|
||||
id: chart.id,
|
||||
type: 'chart'
|
||||
}));
|
||||
|
||||
createAlertAPI(obj).then((res) => {
|
||||
if (res.error) {
|
||||
res.data
|
||||
? actions.addAlert(res.data)
|
||||
: actions.addAlert({ type: 'error', transKey: 'somethingWrong' });
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
actions.addAlert({ type: 'notice', transKey: 'alertSaved' });
|
||||
setLoading(false);
|
||||
toggle();
|
||||
resetForm();
|
||||
resetAlertChart();
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (form.recipients && user.recipient && user.recipient.id) {
|
||||
handleChange('recipients', [user.recipient.id]);
|
||||
}
|
||||
|
||||
return () => resetForm();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} toggle={toggle} backdrop="static" size="lg">
|
||||
<ModalHeader toggle={toggle}>Create Alert</ModalHeader>
|
||||
<ModalBody>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<Label>Selected Charts</Label>
|
||||
<div className="b-radius-5 bg-light p-2">
|
||||
{alertCharts.map((chart, i, arr) => (
|
||||
<Fragment key={chart.name}>
|
||||
<span className="d-inline-block mr-1">
|
||||
{chart.name}
|
||||
{arr.length - 1 !== i ? ', ' : ''}
|
||||
</span>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</FormGroup>
|
||||
<Input
|
||||
name="name"
|
||||
title="Name"
|
||||
required
|
||||
value={form.name}
|
||||
error={errors.name}
|
||||
handleChange={handleChange}
|
||||
handleValidation={handleValidation}
|
||||
/>
|
||||
<Checkbox
|
||||
name="automatedSubject"
|
||||
title="Automated Subject"
|
||||
description="Use automated email subject based on the feeds"
|
||||
value={form.automatedSubject}
|
||||
error={errors.automatedSubject}
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
{!form.automatedSubject && (
|
||||
<Input
|
||||
name="subject"
|
||||
title="Email Subject"
|
||||
value={form.subject}
|
||||
error={errors.subject}
|
||||
handleChange={handleChange}
|
||||
handleValidation={handleValidation}
|
||||
/>
|
||||
)}
|
||||
<Checkbox
|
||||
name="published"
|
||||
title="Publish"
|
||||
description="Alerts and Newsletters that are Published are available for other users to subscribe"
|
||||
value={form.published}
|
||||
error={errors.publish}
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
<Checkbox
|
||||
name="allowUnsubscribe"
|
||||
title="Unsubscribe Link"
|
||||
description="Allow recipients to unsubscribe from Alert"
|
||||
value={form.allowUnsubscribe}
|
||||
error={errors.allowUnsubscribe}
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
<Checkbox
|
||||
name="unsubscribeNotification"
|
||||
title="Notifications"
|
||||
description="Notify creator when recipients unsubscribe"
|
||||
value={form.unsubscribeNotification}
|
||||
error={errors.unsubscribeNotification}
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
<FormGroup className="radio-options">
|
||||
<Label>Options</Label>
|
||||
<RadioButton
|
||||
name="articleExtracts"
|
||||
title="Article Extracts"
|
||||
formClass="mb-0"
|
||||
options={[
|
||||
{ label: 'Contextual extract', value: EXTRAS.CONTEXTUAL },
|
||||
{ label: 'Start of text extract', value: EXTRAS.START },
|
||||
{ label: 'No article extract', value: EXTRAS.NO }
|
||||
]}
|
||||
inline
|
||||
value={form.articleExtracts}
|
||||
error={errors.articleExtracts}
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
<RadioButton
|
||||
name="highlight"
|
||||
title="Highlight Keywords"
|
||||
formClass="mb-0"
|
||||
options={[
|
||||
{ label: 'Yes', value: true },
|
||||
{ label: 'No', value: false }
|
||||
]}
|
||||
inline
|
||||
value={form.highlight}
|
||||
error={errors.highlight}
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
<RadioButton
|
||||
name="showSourceCountry"
|
||||
title="Show Source Country"
|
||||
formClass="mb-0"
|
||||
options={[
|
||||
{ label: 'Yes', value: true },
|
||||
{ label: 'No', value: false }
|
||||
]}
|
||||
inline
|
||||
value={form.showSourceCountry}
|
||||
error={errors.showSourceCountry}
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
<RadioButton
|
||||
name="showUserComments"
|
||||
title="Show User Comments"
|
||||
formClass="mb-0"
|
||||
options={[
|
||||
{ label: 'Yes', value: true },
|
||||
{ label: 'No', value: false }
|
||||
]}
|
||||
inline
|
||||
value={form.showUserComments}
|
||||
error={errors.showUserComments}
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
<RadioButton
|
||||
name="themeType"
|
||||
title="Layout"
|
||||
formClass="mb-0"
|
||||
options={[
|
||||
{ label: 'Enhanced HTML', value: THEME_TYPES.ENHANCED },
|
||||
{ label: 'Plain HTML', value: THEME_TYPES.PLAIN }
|
||||
]}
|
||||
inline
|
||||
value={form.themeType}
|
||||
error={errors.themeType}
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
<RadioButton
|
||||
name="sendWhenEmpty"
|
||||
title="Send When Empty"
|
||||
formClass="mb-0"
|
||||
options={[
|
||||
{ label: 'Yes', value: true },
|
||||
{ label: 'No', value: false }
|
||||
]}
|
||||
inline
|
||||
value={form.sendWhenEmpty}
|
||||
error={errors.sendWhenEmpty}
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Label>Timezone</Label>
|
||||
<Select
|
||||
className="timezone-select"
|
||||
value={form.timezone}
|
||||
options={timezones}
|
||||
clearable={false}
|
||||
onChange={function (v) {
|
||||
handleChange('timezone', v.value);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
{/* <FormGroup>
|
||||
<Label>Automatic</Label>
|
||||
<Scheduling state={state.scheduling} actions={actions} />
|
||||
</FormGroup> */}
|
||||
</Form>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="link" onClick={toggle}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button color="primary" disabled={loading} onClick={handleSubmit}>
|
||||
{loading ? 'Loading...' : 'Submit'}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
AlertDialog.propTypes = {
|
||||
toggle: PropTypes.func,
|
||||
resetAlertChart: PropTypes.func,
|
||||
isOpen: PropTypes.bool,
|
||||
alertCharts: PropTypes.array,
|
||||
user: PropTypes.object,
|
||||
actions: PropTypes.object
|
||||
};
|
||||
|
||||
const applyDecorators = compose(
|
||||
reduxConnect('user', ['common', 'auth', 'user']),
|
||||
translate(['tabsContent'], { wait: true })
|
||||
);
|
||||
|
||||
export default applyDecorators(AlertDialog);
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
DropdownToggle,
|
||||
UncontrolledButtonDropdown
|
||||
} from 'reactstrap';
|
||||
import cx from 'classnames';
|
||||
import { IoIosMenu } from 'react-icons/io';
|
||||
|
||||
function ChartWrapper(props) {
|
||||
let { title, children, menus } = props;
|
||||
|
||||
const hasShowMore = menus.find((menu) => !menu.hide && menu.showInMore);
|
||||
|
||||
// TODO: hide alert until API is ready
|
||||
menus = menus.filter((menu) => menu.title);
|
||||
|
||||
const isRTL = document.documentElement.dir === 'rtl';
|
||||
return (
|
||||
<Card className="mb-3">
|
||||
<CardHeader>
|
||||
{title && <div>{title}</div>}
|
||||
<div className="btn-actions-pane-right actions-icon-btn">
|
||||
<div className="align-content-center d-flex d-inline-flex">
|
||||
{menus &&
|
||||
menus.map((menu) =>
|
||||
!menu.hide && !menu.showInMore && menu.icon ? (
|
||||
<button
|
||||
key={menu.title}
|
||||
title={menu.title}
|
||||
className="btn btn-icon-only mr-2 p-0"
|
||||
onClick={menu.fn}
|
||||
disabled={!menu.fn}
|
||||
>
|
||||
<menu.icon size={menu.size || 16} />
|
||||
</button>
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
{menus && hasShowMore && (
|
||||
<UncontrolledButtonDropdown>
|
||||
<DropdownToggle className="btn-icon btn-icon-only" color="link">
|
||||
<div className="btn-icon-wrapper">
|
||||
<IoIosMenu size={24} />
|
||||
</div>
|
||||
</DropdownToggle>
|
||||
<DropdownMenu
|
||||
className={`dropdown-menu-shadow dropdown-menu-hover-link${
|
||||
isRTL ? ' dropdown-menu-left' : ''
|
||||
}`}
|
||||
>
|
||||
{menus.map((menu) =>
|
||||
!menu.hide && menu.showInMore ? (
|
||||
<DropdownItem onClick={menu.fn} key={menu.title}>
|
||||
{menu.icon && (
|
||||
<i className={cx('dropdown-icon', menu.icon)}></i>
|
||||
)}
|
||||
<span>{menu.title}</span>
|
||||
</DropdownItem>
|
||||
) : null
|
||||
)}
|
||||
</DropdownMenu>
|
||||
</UncontrolledButtonDropdown>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardBody>{children}</CardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
ChartWrapper.propTypes = {
|
||||
title: PropTypes.string,
|
||||
children: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
|
||||
menus: PropTypes.array
|
||||
};
|
||||
|
||||
export default ChartWrapper;
|
||||
+272
@@ -0,0 +1,272 @@
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { DateRangePicker } from 'react-dates';
|
||||
import { translate } from 'react-i18next';
|
||||
import { compose } from 'redux';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
CardTitle,
|
||||
Col,
|
||||
FormGroup,
|
||||
InputGroup,
|
||||
Label,
|
||||
Row
|
||||
} from 'reactstrap';
|
||||
import Loader from 'react-loader-advanced';
|
||||
import { Loader as LoaderAnim } from 'react-loaders';
|
||||
import { useDrop } from 'react-dnd';
|
||||
import { IoIosCloseCircleOutline } from 'react-icons/io';
|
||||
|
||||
import {
|
||||
addEditAnalyticsAPI,
|
||||
getAnalyticDetailsAPI
|
||||
} from '../../../../../api/analytics/createAnalytics';
|
||||
|
||||
import { TYPES } from '../../../../../redux/modules/appState/sidebar';
|
||||
import reduxConnect from '../../../../../redux/utils/connect';
|
||||
import useIsMounted from '../../../../common/hooks/useIsMounted';
|
||||
import { subChartCategories } from './ShowCharts';
|
||||
import { getMomentObject, setDocumentData } from '../../../../../common/helper';
|
||||
|
||||
const initialState = {
|
||||
feeds: [],
|
||||
startDate: null,
|
||||
endDate: null
|
||||
};
|
||||
|
||||
const spinner = <LoaderAnim color="#ffffff" type="ball-pulse" />;
|
||||
|
||||
function CreateAnalysisSubTab({ t, actions }) {
|
||||
const isMounted = useIsMounted();
|
||||
const history = useHistory();
|
||||
const { id } = useParams();
|
||||
const [form, setForm] = useState(initialState);
|
||||
const [error, setError] = useState();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [fetching, setFetching] = useState(!!id);
|
||||
const [focusedInput, setFocusedInput] = useState();
|
||||
const [{ canDrop, isOver }, drop] = useDrop({
|
||||
accept: [TYPES.FEED, TYPES.CLIP_ARTICLE],
|
||||
drop: droppedFeeds,
|
||||
canDrop: canDroppable,
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver(),
|
||||
canDrop: monitor.canDrop()
|
||||
})
|
||||
});
|
||||
|
||||
function getAnalyticData() {
|
||||
setFetching(true);
|
||||
getAnalyticDetailsAPI(id).then((res) => {
|
||||
if (!isMounted.current) {
|
||||
return;
|
||||
}
|
||||
if (res.error || !res.data || !res.data.context) {
|
||||
setFetching(false);
|
||||
res.data
|
||||
? actions.addAlert(res.data)
|
||||
: actions.addAlert({ type: 'error', transKey: 'somethingWrong' });
|
||||
history.push('/app/analyze/saved');
|
||||
return;
|
||||
}
|
||||
|
||||
const { context } = res.data;
|
||||
const date = context && context.rawFilters && context.rawFilters.date;
|
||||
setForm({
|
||||
feeds: context.feeds.map((item) => ({
|
||||
feed: { name: item.name },
|
||||
id: item.id
|
||||
})),
|
||||
startDate: getMomentObject(date && date.start),
|
||||
endDate: getMomentObject(date && date.end)
|
||||
});
|
||||
setFetching(false);
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setDocumentData('title', `${id ? 'Update' : 'Create'} Analysis | Analyze`);
|
||||
return () => {
|
||||
setDocumentData('title');
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
getAnalyticData();
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
function canDroppable(item) {
|
||||
if (form.feeds.find((val) => val.id === item.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function droppedFeeds(item) {
|
||||
if (form.feeds.find((val) => val.id === item.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setForm((prev) => ({ ...prev, feeds: [...prev.feeds, item] }));
|
||||
}
|
||||
|
||||
function removeFeeds(id) {
|
||||
setForm((prev) => {
|
||||
const modifiedFeeds = form.feeds.filter((val) => val.id !== id);
|
||||
return { ...prev, feeds: modifiedFeeds };
|
||||
});
|
||||
}
|
||||
|
||||
const isActive = canDrop && isOver;
|
||||
function handleSubmit() {
|
||||
const isValid = Object.values(form).every((value) =>
|
||||
value ? (Array.isArray(value) ? value.length > 0 : true) : false
|
||||
);
|
||||
if (!isValid) {
|
||||
return setError(t('common:alerts.error.requiredInfo'));
|
||||
}
|
||||
|
||||
setError(false);
|
||||
setLoading(true);
|
||||
addEditAnalyticsAPI(form, id).then((res) => {
|
||||
if (!isMounted.current) {
|
||||
return false;
|
||||
}
|
||||
if (res.error || !res.data.id) {
|
||||
// on error
|
||||
setLoading(false);
|
||||
setError(res.errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
actions.resetAlertChart();
|
||||
setLoading(false);
|
||||
|
||||
history.push(`/app/analyze/${res.data.id}/${subChartCategories[0].path}`);
|
||||
});
|
||||
}
|
||||
|
||||
function handleDateChange({ startDate, endDate }) {
|
||||
setForm((prev) => ({ ...prev, startDate, endDate }));
|
||||
}
|
||||
|
||||
function onFocusChange(focus) {
|
||||
setFocusedInput(focus);
|
||||
}
|
||||
|
||||
function isOutsideRange() {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isRTL = document.documentElement.dir === 'rtl';
|
||||
return (
|
||||
<Card className="mb-3">
|
||||
<Loader message={spinner} show={fetching}>
|
||||
<CardBody>
|
||||
<CardTitle>
|
||||
{id ? t('analyzeTab.updateDetails') : t('analyzeTab.enterDetails')}
|
||||
</CardTitle>
|
||||
<Row>
|
||||
<Col sm="12">
|
||||
<FormGroup data-tour="drop-feeds-box">
|
||||
<div>
|
||||
{form.feeds.length > 0 && (
|
||||
<div className="mb-3">
|
||||
<Label>{t('analyzeTab.selectedFeeds')}</Label>
|
||||
<div>
|
||||
{form.feeds.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="bg-light d-inline d-inline-flex align-items-center mr-2 p-2 text-dark"
|
||||
>
|
||||
<p>{item.feed.name}</p>
|
||||
<button
|
||||
className="btn p-0"
|
||||
onClick={function () {
|
||||
removeFeeds(item.id);
|
||||
}}
|
||||
>
|
||||
<IoIosCloseCircleOutline
|
||||
size={22}
|
||||
className="text-danger ml-2"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Label>{t('analyzeTab.selectFeeds')}</Label>
|
||||
<div ref={drop} className="dropzone-wrapper">
|
||||
<div>
|
||||
<div className="dropzone-content">
|
||||
<p>
|
||||
{isActive
|
||||
? t('analyzeTab.releaseDesc')
|
||||
: t('analyzeTab.dropDesc')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormGroup>
|
||||
<FormGroup data-tour="analytics-data-range">
|
||||
<Label className="mr-sm-2">{t('analyzeTab.dateRange')}</Label>
|
||||
<InputGroup>
|
||||
<DateRangePicker
|
||||
startDateId="startDate"
|
||||
endDateId="endDate"
|
||||
startDate={form.startDate}
|
||||
endDate={form.endDate}
|
||||
onDatesChange={handleDateChange}
|
||||
focusedInput={focusedInput}
|
||||
onFocusChange={onFocusChange}
|
||||
displayFormat="MM/DD/YYYY"
|
||||
startDatePlaceholderText={t('analyzeTab.startDatePlaceholder')}
|
||||
endDatePlaceholderText={t('analyzeTab.endDatePlaceholder')}
|
||||
numberOfMonths={1}
|
||||
isOutsideRange={isOutsideRange}
|
||||
isRTL={isRTL}
|
||||
/>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
{error && <div className="text-danger mb-2">{error}</div>}
|
||||
<Button
|
||||
className="mb-2 mr-2 btn-icon"
|
||||
color="primary"
|
||||
disabled={loading}
|
||||
data-tour="create-analytics-button"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{loading
|
||||
? 'Loading...'
|
||||
: id
|
||||
? t('analyzeTab.updateBtn')
|
||||
: t('analyzeTab.createBtn')}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</CardBody>
|
||||
</Loader>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
CreateAnalysisSubTab.propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
actions: PropTypes.object
|
||||
};
|
||||
|
||||
const applyDecorators = compose(
|
||||
reduxConnect(),
|
||||
translate(['tabsContent'], { wait: true })
|
||||
);
|
||||
|
||||
export default applyDecorators(CreateAnalysisSubTab);
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
import React, {
|
||||
useState,
|
||||
useCallback,
|
||||
Fragment,
|
||||
useEffect,
|
||||
useMemo
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
NavLink,
|
||||
Redirect,
|
||||
Route,
|
||||
Switch,
|
||||
useHistory,
|
||||
useParams
|
||||
} from 'react-router-dom';
|
||||
import {
|
||||
Button,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
DropdownToggle,
|
||||
UncontrolledDropdown
|
||||
} from 'reactstrap';
|
||||
import { IoIosTrash } from 'react-icons/io';
|
||||
|
||||
import {
|
||||
Results,
|
||||
Performance,
|
||||
Influencers,
|
||||
Sentiment,
|
||||
Themes,
|
||||
Demographics
|
||||
// WorldMap
|
||||
} from './Tabs';
|
||||
import AlertDialog from './AlertDialog';
|
||||
import reduxConnect from '../../../../../redux/utils/connect';
|
||||
import translate from 'react-i18next/dist/commonjs/translate';
|
||||
import { compose } from 'redux';
|
||||
import { getAnalyticDetailsAPI } from '../../../../../api/analytics/createAnalytics';
|
||||
import useIsMounted from '../../../../common/hooks/useIsMounted';
|
||||
import { setDocumentData } from '../../../../../common/helper';
|
||||
import { Interpolate } from 'react-i18next';
|
||||
|
||||
// exported for routing
|
||||
export const subChartCategories = [
|
||||
{
|
||||
title: 'Overview',
|
||||
transKey: 'overview',
|
||||
path: 'overview',
|
||||
component: Results
|
||||
},
|
||||
{
|
||||
title: 'Performance',
|
||||
transKey: 'performance',
|
||||
path: 'performance',
|
||||
component: Performance
|
||||
},
|
||||
{
|
||||
title: 'Influencers',
|
||||
transKey: 'influencers',
|
||||
path: 'influencers',
|
||||
component: Influencers
|
||||
},
|
||||
{
|
||||
title: 'Sentiment',
|
||||
transKey: 'sentiment',
|
||||
path: 'sentiment',
|
||||
component: Sentiment
|
||||
},
|
||||
{ title: 'Themes', transKey: 'themes', path: 'themes', component: Themes },
|
||||
{
|
||||
title: 'Demographics',
|
||||
transKey: 'demographics',
|
||||
path: 'demographics',
|
||||
component: Demographics
|
||||
}
|
||||
// { title: 'World Map', transKey: 'worldMap', path: 'worldmap', component: WorldMap }
|
||||
];
|
||||
|
||||
function ShowCharts({ analyze, actions, t }) {
|
||||
const isMounted = useIsMounted();
|
||||
const history = useHistory();
|
||||
const params = useParams();
|
||||
const [chartData, setChartData] = useState({});
|
||||
const [alertModal, setAlertModal] = useState(false);
|
||||
const [fetching, setFetching] = useState(true);
|
||||
const [feedData, setFeedData] = useState(null);
|
||||
|
||||
const { removeAlertChart, resetAlertChart } = actions;
|
||||
const { alertCharts } = analyze;
|
||||
|
||||
useEffect(() => {
|
||||
setDocumentData('title', 'View Analysis | Analyze');
|
||||
return () => {
|
||||
setDocumentData('title');
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!params.id || isNaN(params.id)) {
|
||||
history.push('/app/analyze/saved');
|
||||
} else {
|
||||
getAnalyticData();
|
||||
}
|
||||
|
||||
return () => resetAlertChart(); // reset store
|
||||
}, [params.id]);
|
||||
|
||||
const updateResult = useCallback((data, chartName) => {
|
||||
setChartData((prev) => ({ ...prev, [chartName]: data }));
|
||||
}, []);
|
||||
|
||||
const subChartRoutes = useMemo(() => {
|
||||
return subChartCategories.map(({ path, component: SubChart }) => (
|
||||
<Route exact key={path} path={`/app/analyze/${params.id}/${path}`}>
|
||||
<SubChart
|
||||
id={params.id}
|
||||
feedData={feedData}
|
||||
chartData={chartData}
|
||||
updateResult={updateResult}
|
||||
/>
|
||||
</Route>
|
||||
));
|
||||
}, [updateResult, chartData, feedData, params.id]);
|
||||
|
||||
function getAnalyticData() {
|
||||
setFetching(true);
|
||||
getAnalyticDetailsAPI(params.id).then((res) => {
|
||||
if (!isMounted.current) {
|
||||
return;
|
||||
}
|
||||
if (res.error || !res.data || !res.data.context) {
|
||||
setFetching(false);
|
||||
res.data
|
||||
? actions.addAlert(res.data)
|
||||
: actions.addAlert({ type: 'error', transKey: 'somethingWrong' });
|
||||
history.push('/app/analyze/saved');
|
||||
return;
|
||||
}
|
||||
|
||||
const { context } = res.data;
|
||||
const date = context && context.rawFilters && context.rawFilters.date;
|
||||
setFeedData({
|
||||
feeds: context.feeds.map((item) => ({
|
||||
feed: item.name,
|
||||
id: item.id
|
||||
})),
|
||||
startDate: date && date.start,
|
||||
endDate: date && date.end
|
||||
});
|
||||
setFetching(false);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleModal() {
|
||||
setAlertModal((prev) => !prev);
|
||||
}
|
||||
|
||||
if (fetching) {
|
||||
return 'Loading...';
|
||||
}
|
||||
|
||||
const isRTL = document.documentElement.dir === 'rtl';
|
||||
return (
|
||||
<Fragment>
|
||||
<div
|
||||
className="d-flex"
|
||||
style={{ position: 'absolute', top: 0, right: 0 }}
|
||||
>
|
||||
{alertCharts && alertCharts.length > 0 && (
|
||||
<UncontrolledDropdown className="d-inline-block">
|
||||
<DropdownToggle color="info" className="btn-shadow" caret>
|
||||
<Interpolate
|
||||
t={t}
|
||||
i18nKey="analyzeTab.createAlert"
|
||||
alertsLength={alertCharts.length}
|
||||
/>
|
||||
</DropdownToggle>
|
||||
<DropdownMenu
|
||||
className={`dropdown-menu-right rm-pointers dropdown-menu-shadow dropdown-menu-hover-link${
|
||||
isRTL ? ' dropdown-menu-left' : ''
|
||||
}`}
|
||||
>
|
||||
<DropdownItem header>
|
||||
{t('analyzeTab.selectedCharts')}
|
||||
</DropdownItem>
|
||||
{alertCharts.map((chart, i) => (
|
||||
<div className="dropdown-item" key={`${chart.name}_${i}}`}>
|
||||
<span>
|
||||
{chart.name}
|
||||
{isNaN(chart.id) ? '' : ` (#${chart.id})`}
|
||||
</span>
|
||||
<Button
|
||||
className="btn-icon btn-icon-only ml-auto mr-2 p-1"
|
||||
color="danger"
|
||||
onClick={function () {
|
||||
removeAlertChart({ name: chart.name, id: chart.id });
|
||||
}}
|
||||
>
|
||||
<IoIosTrash fontSize="1rem" className="ml-auto" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<DropdownItem divider />
|
||||
<div className="p-2 pr-3 text-right">
|
||||
<Button
|
||||
className="btn-shadow btn-sm"
|
||||
color="primary"
|
||||
onClick={toggleModal}
|
||||
>
|
||||
{t('analyzeTab.createAlertBtn')}
|
||||
</Button>
|
||||
</div>
|
||||
</DropdownMenu>
|
||||
</UncontrolledDropdown>
|
||||
)}
|
||||
{/*
|
||||
<Button
|
||||
className="btn-icon ml-2"
|
||||
color="info"
|
||||
// change style for mobile view
|
||||
>
|
||||
<IoIosSave className="btn-icon-wrapper" />
|
||||
Save
|
||||
</Button> */}
|
||||
</div>
|
||||
<div className="btn-actions-pane-right mask-line overflow-auto mb-3 pl-3">
|
||||
{subChartCategories.map((cat, i, arr) => (
|
||||
<Button
|
||||
key={cat.title}
|
||||
title={cat.title}
|
||||
tag={NavLink}
|
||||
to={`/app/analyze/${params.id}/${cat.path}`}
|
||||
size="sm"
|
||||
outline
|
||||
color="primary"
|
||||
className={cx('btn-pill btn-wide', {
|
||||
'mr-1 ml-1': i !== 0 && i !== arr.length - 1
|
||||
})}
|
||||
activeClassName="active"
|
||||
>
|
||||
{t(`analyzeTab.overviewCharts.${cat.transKey}`)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<AlertDialog
|
||||
isOpen={alertModal}
|
||||
toggle={toggleModal}
|
||||
alertCharts={alertCharts}
|
||||
resetAlertChart={resetAlertChart}
|
||||
/>
|
||||
|
||||
<Switch>
|
||||
{subChartRoutes}
|
||||
<Redirect
|
||||
to={`/app/analyze/${params.id}/${subChartCategories[0].path}`}
|
||||
/>
|
||||
</Switch>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
ShowCharts.propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
analyze: PropTypes.object,
|
||||
actions: PropTypes.object
|
||||
};
|
||||
|
||||
const applyDecorators = compose(
|
||||
reduxConnect('analyze', ['appState', 'analyze']),
|
||||
translate(['tabsContent'], { wait: true })
|
||||
);
|
||||
|
||||
export default applyDecorators(ShowCharts);
|
||||
+357
@@ -0,0 +1,357 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Col, Row } from 'reactstrap';
|
||||
import ECharts from '../../../../../common/charts/ECharts';
|
||||
import ChartWrapper from '../ChartWrapper';
|
||||
import {
|
||||
getBarOptions,
|
||||
getPieOptions
|
||||
} from '../../../../../common/charts/ChartsOptions';
|
||||
import { IoIosAdd, IoIosRefresh, IoIosCheckmark } from 'react-icons/io';
|
||||
import reduxConnect from '../../../../../../redux/utils/connect';
|
||||
import translate from 'react-i18next/dist/commonjs/translate';
|
||||
import { compose } from 'redux';
|
||||
import { getOverviewPieAPI } from '../../../../../../api/analytics/createAnalytics';
|
||||
import useIsMounted from '../../../../../common/hooks/useIsMounted';
|
||||
|
||||
const initialBar = {
|
||||
data: [],
|
||||
error: undefined,
|
||||
loading: true,
|
||||
vertical: false
|
||||
};
|
||||
const initialPie = { data: [], error: undefined, loading: true };
|
||||
|
||||
function Demographics(props) {
|
||||
const { actions, analyze, feedData, id, t } = props;
|
||||
const isMounted = useIsMounted();
|
||||
const [barCountriesData, setBarCountriesData] = useState(initialBar);
|
||||
const [barLanguagesData, setBarLanguagesData] = useState(initialBar);
|
||||
const [genderData, setGenderData] = useState(initialPie);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
// getCountriesData()
|
||||
getLanguagesData();
|
||||
getGenderData();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (barCountriesData.data) {
|
||||
setBarCountriesData((prev) => ({
|
||||
...prev,
|
||||
data: {
|
||||
...prev.data,
|
||||
xAxis: prev.data.yAxis,
|
||||
yAxis: prev.data.xAxis
|
||||
}
|
||||
}));
|
||||
}
|
||||
}, [barCountriesData.vertical]);
|
||||
|
||||
useEffect(() => {
|
||||
if (barLanguagesData.data) {
|
||||
setBarLanguagesData((prev) => ({
|
||||
...prev,
|
||||
data: {
|
||||
...prev.data,
|
||||
xAxis: prev.data.yAxis,
|
||||
yAxis: prev.data.xAxis
|
||||
}
|
||||
}));
|
||||
}
|
||||
}, [barLanguagesData.vertical]);
|
||||
|
||||
function updateResult(foo, id) {
|
||||
switch (id) {
|
||||
case cn.first:
|
||||
// getCountriesData()
|
||||
return;
|
||||
case cn.second:
|
||||
getLanguagesData();
|
||||
return;
|
||||
case cn.third:
|
||||
getGenderData();
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* Uncomment when country chart shows up
|
||||
function getCountriesData() {
|
||||
setBarCountriesData((prev) => ({ ...prev, loading: true }))
|
||||
getOverviewPieAPI('country', id).then((res) => {
|
||||
if (!isMounted.current) {
|
||||
return false
|
||||
}
|
||||
if (res.error || !res.data.data) {
|
||||
// on error
|
||||
setBarCountriesData((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: res.errorMessage
|
||||
}))
|
||||
return
|
||||
}
|
||||
const { data } = res.data
|
||||
const barOptions = {}
|
||||
const errors = {}
|
||||
Object.entries(data).forEach((feed) => {
|
||||
const [name, value] = feed
|
||||
const labels = ['Results']
|
||||
const datasets = Object.keys(value).map((item) => ({
|
||||
name: item,
|
||||
type: 'bar',
|
||||
data: [value[item]]
|
||||
}))
|
||||
|
||||
if (!datasets || (Array.isArray(datasets) && datasets.length < 1)) {
|
||||
errors[name] = t('analyzeTab.noData');
|
||||
}
|
||||
|
||||
barOptions[name] = getBarOptions(datasets, labels)
|
||||
})
|
||||
|
||||
setBarCountriesData({
|
||||
data: barOptions,
|
||||
error: errors,
|
||||
loading: false,
|
||||
vertical: false
|
||||
})
|
||||
})
|
||||
} */
|
||||
|
||||
function getLanguagesData() {
|
||||
setBarLanguagesData((prev) => ({ ...prev, loading: true }));
|
||||
getOverviewPieAPI('language', id).then((res) => {
|
||||
if (!isMounted.current) {
|
||||
return false;
|
||||
}
|
||||
if (res.error || !res.data.data) {
|
||||
// on error
|
||||
setBarLanguagesData((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: res.errorMessage
|
||||
}));
|
||||
return;
|
||||
}
|
||||
const { data } = res.data;
|
||||
const barOptions = {};
|
||||
const errors = {};
|
||||
Object.entries(data).forEach((feed) => {
|
||||
const [name, value] = feed;
|
||||
const labels = ['Results'];
|
||||
const datasets = Object.keys(value).map((item) => ({
|
||||
name: item,
|
||||
type: 'bar',
|
||||
data: [value[item]]
|
||||
}));
|
||||
|
||||
if (!datasets || (Array.isArray(datasets) && datasets.length < 1)) {
|
||||
errors[name] = t('analyzeTab.noData');
|
||||
}
|
||||
|
||||
barOptions[name] = getBarOptions(datasets, labels);
|
||||
});
|
||||
|
||||
setBarLanguagesData({
|
||||
data: barOptions,
|
||||
error: errors,
|
||||
loading: false,
|
||||
vertical: false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getGenderData() {
|
||||
setGenderData((prev) => ({ ...prev, loading: true }));
|
||||
getOverviewPieAPI('gender', id).then((res) => {
|
||||
if (!isMounted.current) {
|
||||
return false;
|
||||
}
|
||||
if (res.error || !res.data.data) {
|
||||
// on error
|
||||
setGenderData((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: res.errorMessage
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = res.data;
|
||||
const pieOptions = {};
|
||||
const errors = {};
|
||||
|
||||
Object.entries(data).forEach((feed) => {
|
||||
const [name, value] = feed;
|
||||
|
||||
if (!value || (Array.isArray(value) && value.length < 1)) {
|
||||
errors[name] = t('analyzeTab.noData');
|
||||
}
|
||||
|
||||
pieOptions[name] = getPieOptions(
|
||||
Object.entries(value).map((v) => ({
|
||||
name: v[0],
|
||||
value: v[1]
|
||||
}))
|
||||
);
|
||||
});
|
||||
|
||||
setGenderData({
|
||||
data: pieOptions,
|
||||
error: errors,
|
||||
loading: false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function changeVertical(name, id) {
|
||||
name === cn.first
|
||||
? setBarCountriesData((prev) => ({ ...prev, vertical: !prev.vertical }))
|
||||
: setBarLanguagesData((prev) => ({ ...prev, vertical: !prev.vertical }));
|
||||
}
|
||||
|
||||
const hideChartAlert = (name, id) =>
|
||||
analyze.alertCharts.find((v) => v.name === name && v.id === id);
|
||||
const hideChartPieAlert = (id) =>
|
||||
analyze.alertCharts.find((v) => v.name === cn.third && v.id === id);
|
||||
|
||||
const barchartMenus = (name, id) => [
|
||||
{
|
||||
title: '', // t('analyzeTab.chartMenus.addToAlert'),
|
||||
icon: IoIosAdd,
|
||||
size: 24,
|
||||
fn: () => actions.addAlertChart({ name: name, id }),
|
||||
showInMore: false,
|
||||
hide: hideChartAlert(name, id)
|
||||
},
|
||||
{
|
||||
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
|
||||
icon: IoIosCheckmark,
|
||||
size: 24,
|
||||
showInMore: false,
|
||||
hide: !hideChartAlert(name, id)
|
||||
},
|
||||
{
|
||||
title: t('analyzeTab.chartMenus.refresh'),
|
||||
icon: IoIosRefresh,
|
||||
fn: () => updateResult(null, name),
|
||||
showInMore: false
|
||||
},
|
||||
/* {
|
||||
title: t('analyzeTab.chartMenus.addToDashboard'),
|
||||
fn: () => {},
|
||||
showInMore: true
|
||||
}, */
|
||||
{
|
||||
title: t('analyzeTab.chartMenus.toggleHV'),
|
||||
fn: () => changeVertical(name, id),
|
||||
showInMore: true
|
||||
}
|
||||
];
|
||||
|
||||
const piechartMenus = (id) => [
|
||||
{
|
||||
title: '', // t('analyzeTab.chartMenus.addToAlert'),
|
||||
icon: IoIosAdd,
|
||||
size: 24,
|
||||
fn: () => actions.addAlertChart({ name: cn.third, id }),
|
||||
showInMore: false,
|
||||
hide: hideChartPieAlert(id)
|
||||
},
|
||||
{
|
||||
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
|
||||
icon: IoIosCheckmark,
|
||||
size: 24,
|
||||
showInMore: false,
|
||||
hide: !hideChartPieAlert(id)
|
||||
},
|
||||
{
|
||||
title: t('analyzeTab.chartMenus.refresh'),
|
||||
icon: IoIosRefresh,
|
||||
fn: () => updateResult(null, cn.third),
|
||||
showInMore: false
|
||||
}
|
||||
// { title: t('analyzeTab.chartMenus.addToDashboard'), fn: () => {}, showInMore: true }
|
||||
];
|
||||
|
||||
return (
|
||||
<Row>
|
||||
{/* {feedData.feeds.map((feed) => (
|
||||
<Col key={feed.id} md="6">
|
||||
<ChartWrapper
|
||||
title={`${t('analyzeTab.charts.topLanguages')} (${feed.feed})`}
|
||||
menus={barchartMenus(cn.first, feed.id)}
|
||||
>
|
||||
<ECharts
|
||||
xLabel={barCountriesData.labels}
|
||||
loading={barCountriesData.loading}
|
||||
options={barCountriesData.data[feed.feed]}
|
||||
message={
|
||||
barCountriesData.error && barCountriesData.error[feed.feed]
|
||||
}
|
||||
/>
|
||||
</ChartWrapper>
|
||||
</Col>
|
||||
))} */}
|
||||
{feedData.feeds.map((feed) => (
|
||||
<Col key={feed.id} md="6">
|
||||
<ChartWrapper
|
||||
title={`${t('analyzeTab.charts.topLanguages')} (${feed.feed})`}
|
||||
menus={barchartMenus(cn.second, feed.id)}
|
||||
>
|
||||
<ECharts
|
||||
xLabel={barLanguagesData.labels}
|
||||
loading={barLanguagesData.loading}
|
||||
options={barLanguagesData.data[feed.feed]}
|
||||
message={
|
||||
barLanguagesData.error && barLanguagesData.error[feed.feed]
|
||||
}
|
||||
/>
|
||||
</ChartWrapper>
|
||||
</Col>
|
||||
))}
|
||||
{feedData.feeds.map((feed) => (
|
||||
<Col key={feed.id} md="6">
|
||||
<ChartWrapper
|
||||
title={`${t('analyzeTab.charts.gender')} (${feed.feed})`}
|
||||
menus={piechartMenus(feed.id)}
|
||||
>
|
||||
<ECharts
|
||||
loading={genderData.loading}
|
||||
options={genderData.data[feed.feed]}
|
||||
message={genderData.error && genderData.error[feed.feed]}
|
||||
/>
|
||||
</ChartWrapper>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
const cn = {
|
||||
first: 'Top Countries',
|
||||
second: 'Top Languages',
|
||||
third: 'Gender'
|
||||
};
|
||||
|
||||
Demographics.propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
chartData: PropTypes.object,
|
||||
actions: PropTypes.object,
|
||||
id: PropTypes.string,
|
||||
feedData: PropTypes.object,
|
||||
analyze: PropTypes.object
|
||||
};
|
||||
|
||||
const applyDecorators = compose(
|
||||
reduxConnect('analyze', ['appState', 'analyze']),
|
||||
translate(['tabsContent'], { wait: true })
|
||||
);
|
||||
|
||||
export default applyDecorators(React.memo(Demographics));
|
||||
+290
@@ -0,0 +1,290 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import React, {
|
||||
useState,
|
||||
useCallback,
|
||||
Fragment,
|
||||
useEffect,
|
||||
useMemo
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import { compose } from 'redux';
|
||||
import { Table } from '../../../../../common/Table/Table';
|
||||
import { getInfluencersAPI } from '../../../../../../api/analytics/createAnalytics';
|
||||
import { reduxActions } from '../../../../../../redux/utils/connect';
|
||||
import {
|
||||
getQueryParams,
|
||||
removeHttpsUrl,
|
||||
capOnlyFirstLetter,
|
||||
getValidHttpUrl
|
||||
} from '../../../../../../common/helper';
|
||||
import i18n from '../../../../../../i18n';
|
||||
|
||||
function Influencers(props) {
|
||||
const [dataSource, setDataSource] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filter] = useState(filtersNames[1].id);
|
||||
const { t, actions, id, feedData } = props;
|
||||
|
||||
useEffect(() => {
|
||||
if (!id || !dataSource) {
|
||||
return;
|
||||
}
|
||||
getInfluencers(); //called from table
|
||||
}, [filter]);
|
||||
|
||||
const getDetailsColumns = (id) => {
|
||||
return id === filtersNames[0].id ? sourceDetails : authorDetails;
|
||||
};
|
||||
|
||||
const authorDetails = useMemo(
|
||||
() => [
|
||||
{
|
||||
Header: i18n.t('tabsContent:analyzeTab.influencerCols.rank'),
|
||||
accessor: 'source_hashcode',
|
||||
Cell: (row) => (
|
||||
<div style={{ textAlign: 'center' }}>{row.index + 1}</div>
|
||||
),
|
||||
minWidth: 52
|
||||
},
|
||||
{
|
||||
Header: i18n.t('tabsContent:analyzeTab.influencerCols.influencers'),
|
||||
accessor: 'influence',
|
||||
Cell: (row) =>
|
||||
getValidHttpUrl(row.value) ? (
|
||||
<a
|
||||
target="_blank"
|
||||
rel="nofollow noopener"
|
||||
href={getValidHttpUrl(row.value)}
|
||||
>
|
||||
{row.original && row.original.author_name}
|
||||
</a>
|
||||
) : (
|
||||
removeHttpsUrl(row.value)
|
||||
),
|
||||
minWidth: 130
|
||||
},
|
||||
{
|
||||
Header: i18n.t('tabsContent:analyzeTab.influencerCols.sourceType'),
|
||||
accessor: 'source_type',
|
||||
Cell: (row) => capOnlyFirstLetter(row.value),
|
||||
minWidth: 102
|
||||
}
|
||||
],
|
||||
[i18n.language]
|
||||
);
|
||||
|
||||
const sourceDetails = useMemo(
|
||||
() => [
|
||||
{
|
||||
Header: i18n.t('tabsContent:analyzeTab.influencerCols.rank'),
|
||||
accessor: 'source_hashcode',
|
||||
Cell: (row) => (
|
||||
<div style={{ textAlign: 'center' }}>{row.index + 1}</div>
|
||||
),
|
||||
minWidth: 52
|
||||
},
|
||||
{
|
||||
Header: i18n.t('tabsContent:analyzeTab.influencerCols.influencers'),
|
||||
accessor: 'influence',
|
||||
Cell: (row) =>
|
||||
getValidHttpUrl(row.value) ? (
|
||||
<a
|
||||
target="_blank"
|
||||
rel="nofollow noopener"
|
||||
href={getValidHttpUrl(row.value)}
|
||||
>
|
||||
{removeHttpsUrl(row.value)}
|
||||
</a>
|
||||
) : (
|
||||
removeHttpsUrl(row.value)
|
||||
),
|
||||
minWidth: 130
|
||||
},
|
||||
{
|
||||
Header: i18n.t('tabsContent:analyzeTab.influencerCols.sourceType'),
|
||||
accessor: 'source_type',
|
||||
Cell: (row) => capOnlyFirstLetter(row.value),
|
||||
minWidth: 102
|
||||
}
|
||||
],
|
||||
[i18n.language]
|
||||
);
|
||||
|
||||
const sentimentColumns = useMemo(
|
||||
() => [
|
||||
{
|
||||
Header: i18n.t('tabsContent:analyzeTab.influencerCols.total'),
|
||||
// accessor: d => d.nop.total
|
||||
accessor: 'totalSentiment',
|
||||
minWidth: 52,
|
||||
Cell: (row) => (
|
||||
<div style={{ textAlign: 'center' }}>{row.value || 0}</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
Header: i18n.t('tabsContent:analyzeTab.influencerCols.positive'),
|
||||
accessor: 'POSITIVE',
|
||||
minWidth: 78,
|
||||
Cell: (row) => (
|
||||
<div style={{ textAlign: 'center' }}>{row.value || 0}</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
Header: i18n.t('tabsContent:analyzeTab.influencerCols.neutral'),
|
||||
accessor: 'NEUTRAL',
|
||||
minWidth: 78,
|
||||
Cell: (row) => (
|
||||
<div style={{ textAlign: 'center' }}>{row.value || 0}</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
Header: i18n.t('tabsContent:analyzeTab.influencerCols.negative'),
|
||||
accessor: 'NEGATIVE',
|
||||
minWidth: 78,
|
||||
Cell: (row) => (
|
||||
<div style={{ textAlign: 'center' }}>{row.value || 0}</div>
|
||||
)
|
||||
}
|
||||
],
|
||||
[i18n.language]
|
||||
);
|
||||
|
||||
const reachColumns = useMemo(
|
||||
() => [
|
||||
/* {
|
||||
Header: i18n.t('tabsContent:analyzeTab.influencerCols.reach'),
|
||||
accessor: 'reach',
|
||||
minWidth: 65,
|
||||
Cell: (row) => <div style={{ textAlign: 'center' }}>{row.value || 0}</div>
|
||||
}, */
|
||||
{
|
||||
Header: i18n.t('tabsContent:analyzeTab.influencerCols.engagement'),
|
||||
accessor: 'engagement',
|
||||
minWidth: 105,
|
||||
Cell: (row) => (
|
||||
<div style={{ textAlign: 'center' }}>{row.value || 0}</div>
|
||||
)
|
||||
}
|
||||
/* {
|
||||
Header: i18n.t('tabsContent:analyzeTab.influencerCols.engagementPerMention'),
|
||||
accessor: 'engagement_per_mention',
|
||||
Cell: (row) => <div style={{ textAlign: 'center' }}>{row.value || 0}</div>
|
||||
} */
|
||||
],
|
||||
[i18n.language]
|
||||
);
|
||||
|
||||
const columnsList = useMemo(
|
||||
() => [
|
||||
{
|
||||
Header: i18n.t('tabsContent:analyzeTab.influencerCols.details'),
|
||||
headerClassName: 'text-center',
|
||||
columns: getDetailsColumns(filter)
|
||||
},
|
||||
{
|
||||
Header: i18n.t('tabsContent:analyzeTab.influencerCols.sentiments'),
|
||||
headerClassName: 'text-center',
|
||||
columns: sentimentColumns
|
||||
},
|
||||
{
|
||||
Header: i18n.t('tabsContent:analyzeTab.influencerCols.reach'),
|
||||
headerClassName: 'text-center',
|
||||
columns: reachColumns
|
||||
}
|
||||
],
|
||||
[filter, i18n.language]
|
||||
);
|
||||
|
||||
const getInfluencers = useCallback(
|
||||
(page = 0, pageSize = 10) => {
|
||||
setLoading(true);
|
||||
const filterParams = getQueryParams({ page, pageSize });
|
||||
getInfluencersAPI(id, filter, filterParams).then((res) => {
|
||||
// if (false) {
|
||||
if (res.error || res.data === null || !res.data.data) {
|
||||
setLoading(false);
|
||||
return actions.addAlert({
|
||||
type: 'error',
|
||||
transKey: 'somethingWrong'
|
||||
});
|
||||
}
|
||||
|
||||
const tableData = {};
|
||||
res.data.data.forEach((v) => {
|
||||
tableData[v.name] = v.data;
|
||||
});
|
||||
setDataSource(tableData);
|
||||
setLoading(false);
|
||||
});
|
||||
},
|
||||
[id, filter]
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{/* <ButtonGroup size="sm" className="mb-3 d-block text-right">
|
||||
{filtersNames.map((item) => (
|
||||
<Button
|
||||
outline
|
||||
key={item.id}
|
||||
title={item.name}
|
||||
color="secondary"
|
||||
onClick={function () {
|
||||
setFilter(item.id)
|
||||
}}
|
||||
active={filter === item.id}
|
||||
>
|
||||
{item.name}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup> */}
|
||||
{feedData.feeds.map((feed) => {
|
||||
let tableData = dataSource;
|
||||
if (!tableData || !tableData[feed.feed]) {
|
||||
tableData = { [feed.feed]: [] };
|
||||
// uncomment for pagination
|
||||
// tableData[feed.feed] = { data: [], totalCount: 0, limit: 0, page: 0 }
|
||||
}
|
||||
|
||||
const { totalCount = 0, limit = 0, page = 0 } = tableData[feed.feed];
|
||||
return (
|
||||
<Table
|
||||
key={feed.id}
|
||||
t={t}
|
||||
cardTitle={`${t('analyzeTab.charts.topInfluencers')} (${
|
||||
feed.feed
|
||||
})`}
|
||||
columns={columnsList}
|
||||
data={tableData[feed.feed]}
|
||||
totalCount={totalCount}
|
||||
showTotalCount
|
||||
limit={limit}
|
||||
page={page}
|
||||
isLoading={loading}
|
||||
onFetchData={getInfluencers}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const filtersNames = [
|
||||
{ name: 'Source', id: 0 },
|
||||
{ name: 'Author', id: 1 }
|
||||
];
|
||||
|
||||
Influencers.propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
feedData: PropTypes.object,
|
||||
id: PropTypes.string,
|
||||
actions: PropTypes.object
|
||||
};
|
||||
|
||||
const applyDecorators = compose(
|
||||
translate(['tabsContent'], { wait: true }),
|
||||
reduxActions()
|
||||
);
|
||||
|
||||
export default applyDecorators(Influencers);
|
||||
+722
@@ -0,0 +1,722 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Col, Row } from 'reactstrap';
|
||||
import ECharts from '../../../../../common/charts/ECharts';
|
||||
import ChartWrapper from '../ChartWrapper';
|
||||
import {
|
||||
getBarOptions,
|
||||
getPieOptions
|
||||
} from '../../../../../common/charts/ChartsOptions';
|
||||
import { IoIosAdd, IoIosRefresh, IoIosCheckmark } from 'react-icons/io';
|
||||
import reduxConnect from '../../../../../../redux/utils/connect';
|
||||
import translate from 'react-i18next/dist/commonjs/translate';
|
||||
import { compose } from 'redux';
|
||||
import {
|
||||
getEngagementsAPI,
|
||||
getEngagementsTimeAPI,
|
||||
getOverviewBarAPI,
|
||||
getOverviewPieAPI
|
||||
} from '../../../../../../api/analytics/createAnalytics';
|
||||
import useIsMounted from '../../../../../common/hooks/useIsMounted';
|
||||
|
||||
function Performance(props) {
|
||||
const { actions, analyze, feedData, id, t } = props;
|
||||
const isMounted = useIsMounted();
|
||||
const [barData, setBarData] = useState({
|
||||
data: [],
|
||||
error: undefined,
|
||||
loading: true,
|
||||
vertical: false
|
||||
});
|
||||
const [engBarData, setEngBarData] = useState({
|
||||
data: [],
|
||||
error: undefined,
|
||||
loading: true,
|
||||
vertical: false
|
||||
});
|
||||
const [potentialBarData, setPotentialBarData] = useState({
|
||||
data: [],
|
||||
error: undefined,
|
||||
loading: true,
|
||||
vertical: false
|
||||
});
|
||||
const [sentimentBar, setSentimentBar] = useState({
|
||||
data: [],
|
||||
error: undefined,
|
||||
loading: true
|
||||
});
|
||||
const [pieMentions, setpieMentions] = useState({
|
||||
data: [],
|
||||
error: undefined,
|
||||
loading: true
|
||||
});
|
||||
const [pieEng, setpieEng] = useState({
|
||||
data: [],
|
||||
error: undefined,
|
||||
loading: true
|
||||
});
|
||||
/* const [pieReach, setpieReach] = useState({
|
||||
data: [],
|
||||
error: undefined,
|
||||
loading: true
|
||||
}); */
|
||||
|
||||
useEffect(() => {
|
||||
// pass filter
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
getBarChart();
|
||||
getEngBarChart();
|
||||
// getPotentialChart()
|
||||
getSentimentChart();
|
||||
getpieMentions();
|
||||
getpieEngg();
|
||||
// getpieReach()
|
||||
}, []);
|
||||
|
||||
function updateResult(foo, id) {
|
||||
switch (id) {
|
||||
case cn.first:
|
||||
getBarChart();
|
||||
return;
|
||||
case cn.second:
|
||||
getEngBarChart();
|
||||
return;
|
||||
case cn.third:
|
||||
// getPotentialChart() // Uncomment when API has data
|
||||
return;
|
||||
case cn.fourth:
|
||||
getSentimentChart();
|
||||
return;
|
||||
case cn.fifth:
|
||||
getpieMentions();
|
||||
return;
|
||||
case cn.sixth:
|
||||
getpieEngg();
|
||||
return;
|
||||
case cn.seventh:
|
||||
// getpieReach() // Uncomment when API has data
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (barData.data) {
|
||||
setBarData((prev) => ({
|
||||
...prev,
|
||||
data: {
|
||||
...prev.data,
|
||||
xAxis: prev.data.yAxis,
|
||||
yAxis: prev.data.xAxis
|
||||
}
|
||||
}));
|
||||
}
|
||||
}, [barData.vertical]);
|
||||
|
||||
useEffect(() => {
|
||||
if (engBarData.data) {
|
||||
setEngBarData((prev) => ({
|
||||
...prev,
|
||||
data: {
|
||||
...prev.data,
|
||||
xAxis: prev.data.yAxis,
|
||||
yAxis: prev.data.xAxis
|
||||
}
|
||||
}));
|
||||
}
|
||||
}, [engBarData.vertical]);
|
||||
|
||||
useEffect(() => {
|
||||
if (potentialBarData.data) {
|
||||
setPotentialBarData((prev) => ({
|
||||
...prev,
|
||||
data: {
|
||||
...prev.data,
|
||||
xAxis: prev.data.yAxis,
|
||||
yAxis: prev.data.xAxis
|
||||
}
|
||||
}));
|
||||
}
|
||||
}, [potentialBarData.vertical]);
|
||||
|
||||
function getBarChart() {
|
||||
setBarData((prev) => ({ ...prev, loading: true }));
|
||||
getOverviewBarAPI('none', id).then((res) => {
|
||||
if (!isMounted.current) {
|
||||
return false;
|
||||
}
|
||||
if (res.error || !res.data.data) {
|
||||
// on error
|
||||
setBarData((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: res.errorMessage
|
||||
}));
|
||||
return;
|
||||
}
|
||||
const { data } = res.data;
|
||||
const labels = Object.keys(data[0].data);
|
||||
|
||||
const datasets = data.map((item) => ({
|
||||
name: item.name,
|
||||
type: barData.vertical ? 'bar' : 'line',
|
||||
smooth: true,
|
||||
data: Object.values(item.data)
|
||||
}));
|
||||
|
||||
const barOptions = getBarOptions(datasets, labels);
|
||||
|
||||
setBarData({
|
||||
data: barOptions,
|
||||
error: false,
|
||||
loading: false,
|
||||
vertical: false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getEngBarChart() {
|
||||
setEngBarData((prev) => ({ ...prev, loading: true }));
|
||||
getEngagementsTimeAPI(id).then((res) => {
|
||||
if (!isMounted.current) {
|
||||
return false;
|
||||
}
|
||||
if (res.error || !res.data.data) {
|
||||
// on error
|
||||
setEngBarData((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: res.errorMessage
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = res.data;
|
||||
const labels = Object.keys(data[0].data);
|
||||
const datasets = data.map((item) => ({
|
||||
name: item.name,
|
||||
type: barData.vertical ? 'bar' : 'line',
|
||||
smooth: true,
|
||||
data: Object.values(item.data)
|
||||
}));
|
||||
|
||||
const barOptions = getBarOptions(datasets, labels);
|
||||
|
||||
setEngBarData({
|
||||
data: barOptions,
|
||||
error: false,
|
||||
loading: false,
|
||||
vertical: false
|
||||
});
|
||||
});
|
||||
}
|
||||
/*
|
||||
function getPotentialChart() {
|
||||
setPotentialBarData((prev) => ({ ...prev, loading: true }));
|
||||
getOverviewBarAPI('none', id).then((res) => {
|
||||
if (!isMounted.current) {
|
||||
return false;
|
||||
}
|
||||
if (res.error || !res.data.data) {
|
||||
// on error
|
||||
setPotentialBarData((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: res.errorMessage
|
||||
}));
|
||||
return;
|
||||
}
|
||||
const { data } = res.data;
|
||||
const labels = Object.keys(data);
|
||||
|
||||
const datasets = {
|
||||
name: 'Potential reach over time',
|
||||
type: potentialBarData.vertical ? 'bar' : 'line',
|
||||
smooth: true,
|
||||
data: Object.values(data)
|
||||
};
|
||||
|
||||
const barOptions = getBarOptions(datasets, labels);
|
||||
|
||||
setPotentialBarData({
|
||||
data: barOptions,
|
||||
error: false,
|
||||
loading: false,
|
||||
vertical: false
|
||||
});
|
||||
});
|
||||
} */
|
||||
|
||||
function getSentimentChart() {
|
||||
setSentimentBar((prev) => ({ ...prev, loading: true }));
|
||||
getOverviewPieAPI('sentiment', id).then((res) => {
|
||||
if (!isMounted.current) {
|
||||
return false;
|
||||
}
|
||||
if (res.error || !res.data.data) {
|
||||
// on error
|
||||
setSentimentBar((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: res.errorMessage
|
||||
}));
|
||||
return;
|
||||
}
|
||||
const { data } = res.data;
|
||||
const barOptions = {};
|
||||
Object.keys(data).forEach((feed) => {
|
||||
const labels = ['Results'];
|
||||
const datasets = ['POSITIVE', 'NEGATIVE', 'NEUTRAL'].map((item) => ({
|
||||
name: item,
|
||||
type: 'bar',
|
||||
data: [data[feed][item]]
|
||||
}));
|
||||
|
||||
barOptions[feed] = getBarOptions(datasets, labels);
|
||||
});
|
||||
|
||||
setSentimentBar({
|
||||
data: barOptions,
|
||||
error: false,
|
||||
loading: false,
|
||||
vertical: false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getpieMentions() {
|
||||
setpieMentions((prev) => ({ ...prev, loading: true }));
|
||||
getOverviewPieAPI('none', id).then((res) => {
|
||||
if (!isMounted.current) {
|
||||
return false;
|
||||
}
|
||||
if (res.error || !res.data.data) {
|
||||
// alert on error
|
||||
setpieMentions((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: res.errorMessage
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = res.data;
|
||||
const pieOptions = getPieOptions(
|
||||
Object.entries(data).map((v) => ({ name: v[0], value: v[1] }))
|
||||
);
|
||||
|
||||
setpieMentions({
|
||||
data: pieOptions,
|
||||
error: false,
|
||||
loading: false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getpieEngg() {
|
||||
setpieEng((prev) => ({ ...prev, loading: true }));
|
||||
getEngagementsAPI(id).then((res) => {
|
||||
if (!isMounted.current) {
|
||||
return false;
|
||||
}
|
||||
if (res.error || !res.data.data) {
|
||||
// alert on error
|
||||
setpieEng((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: res.errorMessage
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// condition for other filter than 0
|
||||
const { data } = res.data;
|
||||
const pieOptions = getPieOptions(
|
||||
Object.entries(data).map((v) => ({ name: v[0], value: v[1] }))
|
||||
);
|
||||
|
||||
setpieEng({
|
||||
data: pieOptions,
|
||||
error: false,
|
||||
loading: false
|
||||
});
|
||||
});
|
||||
}
|
||||
/*
|
||||
function getpieReach() {
|
||||
setpieReach((prev) => ({ ...prev, loading: true }));
|
||||
getOverviewPieAPI('none', id).then((res) => {
|
||||
if (!isMounted.current) {
|
||||
return false;
|
||||
}
|
||||
if (res.error || !res.data.data) {
|
||||
// alert on error
|
||||
setpieReach((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: res.errorMessage
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = res.data;
|
||||
const pieOptions = getPieOptions(
|
||||
Object.entries(data).map((v) => ({ name: v[0], value: v[1] }))
|
||||
);
|
||||
|
||||
setpieReach({
|
||||
data: pieOptions,
|
||||
error: false,
|
||||
loading: false
|
||||
});
|
||||
});
|
||||
} */
|
||||
|
||||
function changeVertical(chart) {
|
||||
switch (chart) {
|
||||
case cn.first:
|
||||
setBarData((prev) => ({ ...prev, vertical: !prev.vertical }));
|
||||
return;
|
||||
case cn.second:
|
||||
setEngBarData((prev) => ({ ...prev, vertical: !prev.vertical }));
|
||||
return;
|
||||
case cn.third:
|
||||
setPotentialBarData((prev) => ({ ...prev, vertical: !prev.vertical }));
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const hideChart1Alert = analyze.alertCharts.find((v) => v.name === cn.first);
|
||||
const hideChart2Alert = analyze.alertCharts.find((v) => v.name === cn.second);
|
||||
// const hideChart3Alert = analyze.alertCharts.find((v) => v.name === cn.third);
|
||||
const hideChart4Alert = (id) =>
|
||||
analyze.alertCharts.find((v) => v.name === cn.fourth && v.id === id);
|
||||
const hideChart5Alert = analyze.alertCharts.find((v) => v.name === cn.fifth);
|
||||
const hideChart6Alert = analyze.alertCharts.find((v) => v.name === cn.sixth);
|
||||
/* const hideChart7Alert = analyze.alertCharts.find(
|
||||
(v) => v.name === cn.seventh
|
||||
); */
|
||||
|
||||
const barchart1Menus = [
|
||||
{
|
||||
title: '', // t('analyzeTab.chartMenus.addToAlert'),
|
||||
icon: IoIosAdd,
|
||||
size: 24,
|
||||
fn: () => actions.addAlertChart({ name: cn.first, id: 'none' }),
|
||||
showInMore: false,
|
||||
hide: hideChart1Alert
|
||||
},
|
||||
{
|
||||
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
|
||||
icon: IoIosCheckmark,
|
||||
size: 24,
|
||||
showInMore: false,
|
||||
hide: !hideChart1Alert
|
||||
},
|
||||
{
|
||||
title: t('analyzeTab.chartMenus.refresh'),
|
||||
icon: IoIosRefresh,
|
||||
fn: () => updateResult(null, cn.first),
|
||||
showInMore: false
|
||||
},
|
||||
/* {
|
||||
title: t('analyzeTab.chartMenus.addToDashboard'),
|
||||
fn: () => {},
|
||||
showInMore: true
|
||||
}, */
|
||||
{
|
||||
title: t('analyzeTab.chartMenus.toggleHV'),
|
||||
fn: () => changeVertical(cn.first),
|
||||
showInMore: true
|
||||
}
|
||||
];
|
||||
|
||||
const barchart2Menus = [
|
||||
{
|
||||
title: '', // t('analyzeTab.chartMenus.addToAlert'),
|
||||
icon: IoIosAdd,
|
||||
size: 24,
|
||||
fn: () => actions.addAlertChart({ name: cn.second, id: 'none' }),
|
||||
showInMore: false,
|
||||
hide: hideChart2Alert
|
||||
},
|
||||
{
|
||||
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
|
||||
icon: IoIosCheckmark,
|
||||
size: 24,
|
||||
showInMore: false,
|
||||
hide: !hideChart2Alert
|
||||
},
|
||||
{
|
||||
title: t('analyzeTab.chartMenus.refresh'),
|
||||
icon: IoIosRefresh,
|
||||
fn: () => updateResult(null, cn.second),
|
||||
showInMore: false
|
||||
},
|
||||
/* {
|
||||
title: t('analyzeTab.chartMenus.addToDashboard'),
|
||||
fn: () => {},
|
||||
showInMore: true
|
||||
}, */
|
||||
{
|
||||
title: t('analyzeTab.chartMenus.toggleHV'),
|
||||
fn: () => changeVertical(cn.second),
|
||||
showInMore: true
|
||||
}
|
||||
];
|
||||
/*
|
||||
const barchart3Menus = [
|
||||
{
|
||||
title: '', // t('analyzeTab.chartMenus.addToAlert'),
|
||||
icon: IoIosAdd,
|
||||
size: 24,
|
||||
fn: () => actions.addAlertChart({ name: cn.third, id: 'none' }),
|
||||
showInMore: false,
|
||||
hide: hideChart3Alert
|
||||
},
|
||||
{
|
||||
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
|
||||
icon: IoIosCheckmark,
|
||||
size: 24,
|
||||
showInMore: false,
|
||||
hide: !hideChart3Alert
|
||||
},
|
||||
{
|
||||
title: t('analyzeTab.chartMenus.refresh'),
|
||||
icon: IoIosRefresh,
|
||||
fn: () => updateResult(null, cn.third),
|
||||
showInMore: false
|
||||
},
|
||||
{
|
||||
title: t('analyzeTab.chartMenus.addToDashboard'),
|
||||
fn: () => {},
|
||||
showInMore: true
|
||||
},
|
||||
{
|
||||
title: t('analyzeTab.chartMenus.toggleHV'),
|
||||
fn: () => changeVertical(cn.third),
|
||||
showInMore: true
|
||||
}
|
||||
];
|
||||
*/
|
||||
function barchart4Menus(id) {
|
||||
return [
|
||||
{
|
||||
title: '', // t('analyzeTab.chartMenus.addToAlert'),
|
||||
icon: IoIosAdd,
|
||||
size: 24,
|
||||
fn: () => actions.addAlertChart({ name: cn.fourth, id }),
|
||||
showInMore: false,
|
||||
hide: hideChart4Alert(id)
|
||||
},
|
||||
{
|
||||
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
|
||||
icon: IoIosCheckmark,
|
||||
size: 24,
|
||||
showInMore: false,
|
||||
hide: !hideChart4Alert(id)
|
||||
},
|
||||
{
|
||||
title: t('analyzeTab.chartMenus.refresh'),
|
||||
icon: IoIosRefresh,
|
||||
fn: () => updateResult(null, cn.fourth, id),
|
||||
showInMore: false
|
||||
}
|
||||
/* {
|
||||
title: t('analyzeTab.chartMenus.addToDashboard'),
|
||||
fn: () => {},
|
||||
showInMore: true
|
||||
} */
|
||||
];
|
||||
}
|
||||
|
||||
const pieChart1 = [
|
||||
{
|
||||
title: '', // t('analyzeTab.chartMenus.addToAlert'),
|
||||
icon: IoIosAdd,
|
||||
size: 24,
|
||||
fn: () => actions.addAlertChart({ name: cn.fifth, id: 'none' }),
|
||||
showInMore: false,
|
||||
hide: hideChart5Alert
|
||||
},
|
||||
{
|
||||
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
|
||||
icon: IoIosCheckmark,
|
||||
size: 24,
|
||||
showInMore: false,
|
||||
hide: !hideChart5Alert
|
||||
},
|
||||
{
|
||||
title: t('analyzeTab.chartMenus.refresh'),
|
||||
icon: IoIosRefresh,
|
||||
fn: () => updateResult(null, cn.fifth),
|
||||
showInMore: false
|
||||
}
|
||||
// { title: t('analyzeTab.chartMenus.addToDashboard'), fn: () => {}, showInMore: true }
|
||||
];
|
||||
|
||||
const pieChart2 = [
|
||||
{
|
||||
title: '', // t('analyzeTab.chartMenus.addToAlert'),
|
||||
icon: IoIosAdd,
|
||||
size: 24,
|
||||
fn: () => actions.addAlertChart({ name: cn.sixth, id: 'none' }),
|
||||
showInMore: false,
|
||||
hide: hideChart6Alert
|
||||
},
|
||||
{
|
||||
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
|
||||
icon: IoIosCheckmark,
|
||||
size: 24,
|
||||
showInMore: false,
|
||||
hide: !hideChart6Alert
|
||||
},
|
||||
{
|
||||
title: t('analyzeTab.chartMenus.refresh'),
|
||||
icon: IoIosRefresh,
|
||||
fn: () => updateResult(null, cn.sixth),
|
||||
showInMore: false
|
||||
}
|
||||
// { title: t('analyzeTab.chartMenus.addToDashboard'), fn: () => {}, showInMore: true }
|
||||
];
|
||||
/*
|
||||
const pieChart3 = [
|
||||
{
|
||||
title: '', // t('analyzeTab.chartMenus.addToAlert'),
|
||||
icon: IoIosAdd,
|
||||
size: 24,
|
||||
fn: () => actions.addAlertChart({ name: cn.seventh, id: 'none' }),
|
||||
showInMore: false,
|
||||
hide: hideChart7Alert
|
||||
},
|
||||
{
|
||||
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
|
||||
icon: IoIosCheckmark,
|
||||
size: 24,
|
||||
showInMore: false,
|
||||
hide: !hideChart7Alert
|
||||
},
|
||||
{
|
||||
title: t('analyzeTab.chartMenus.refresh'),
|
||||
icon: IoIosRefresh,
|
||||
fn: () => updateResult(null, cn.seventh),
|
||||
showInMore: false
|
||||
}
|
||||
// { title: t('analyzeTab.chartMenus.addToDashboard'), fn: () => {}, showInMore: true }
|
||||
];
|
||||
*/
|
||||
return (
|
||||
<Row>
|
||||
<Col md="8">
|
||||
<ChartWrapper
|
||||
title={t('analyzeTab.charts.mentionsOverTime')}
|
||||
menus={barchart1Menus}
|
||||
>
|
||||
<ECharts
|
||||
xLabel={barData.labels}
|
||||
loading={barData.loading}
|
||||
options={barData.data}
|
||||
/>
|
||||
</ChartWrapper>
|
||||
</Col>
|
||||
<Col md="4">
|
||||
<ChartWrapper title={t('analyzeTab.charts.mentions')} menus={pieChart1}>
|
||||
<ECharts
|
||||
xLabel={pieMentions.labels}
|
||||
loading={pieMentions.loading}
|
||||
options={pieMentions.data}
|
||||
/>
|
||||
</ChartWrapper>
|
||||
</Col>
|
||||
<Col md="8">
|
||||
<ChartWrapper
|
||||
title={t('analyzeTab.charts.engagementOverTime')}
|
||||
menus={barchart2Menus}
|
||||
>
|
||||
<ECharts
|
||||
xLabel={engBarData.labels}
|
||||
loading={engBarData.loading}
|
||||
options={engBarData.data}
|
||||
/>
|
||||
</ChartWrapper>
|
||||
</Col>
|
||||
<Col md="4">
|
||||
<ChartWrapper
|
||||
title={t('analyzeTab.charts.engagement')}
|
||||
menus={pieChart2}
|
||||
>
|
||||
<ECharts
|
||||
xLabel={pieEng.labels}
|
||||
loading={pieEng.loading}
|
||||
options={pieEng.data}
|
||||
/>
|
||||
</ChartWrapper>
|
||||
</Col>
|
||||
{/* <Col md="8">
|
||||
<ChartWrapper title={t('analyzeTab.charts.potentialReachOverTime')} menus={barchart3Menus}>
|
||||
<ECharts
|
||||
xLabel={potentialBarData.labels}
|
||||
loading={potentialBarData.loading}
|
||||
options={potentialBarData.data}
|
||||
/>
|
||||
</ChartWrapper>
|
||||
</Col>
|
||||
<Col md="4">
|
||||
<ChartWrapper title={t('analyzeTab.charts.potentialReach')} menus={pieChart3}>
|
||||
<ECharts
|
||||
xLabel={pieReach.labels}
|
||||
loading={pieReach.loading}
|
||||
options={pieReach.data}
|
||||
/>
|
||||
</ChartWrapper>
|
||||
</Col> */}
|
||||
{feedData.feeds.map((feed) => (
|
||||
<Col md="12" key={feed.id}>
|
||||
<ChartWrapper
|
||||
title={`${t('analyzeTab.charts.proportionofSentiment')} (${
|
||||
feed.feed
|
||||
})`}
|
||||
menus={barchart4Menus(feed.id)}
|
||||
>
|
||||
<ECharts
|
||||
xLabel={sentimentBar.labels}
|
||||
loading={sentimentBar.loading}
|
||||
options={sentimentBar.data[feed.feed]}
|
||||
/>
|
||||
</ChartWrapper>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
const cn = {
|
||||
first: 'Mentions over time',
|
||||
second: 'Engagement over time',
|
||||
third: 'Potential reach over time',
|
||||
fourth: 'Proportion of sentiment',
|
||||
fifth: 'Mentions',
|
||||
sixth: 'Engagement',
|
||||
seventh: 'Potential Reach'
|
||||
};
|
||||
|
||||
Performance.propTypes = {
|
||||
chartData: PropTypes.object,
|
||||
actions: PropTypes.object,
|
||||
feedData: PropTypes.object,
|
||||
id: PropTypes.string,
|
||||
analyze: PropTypes.object,
|
||||
t: PropTypes.func
|
||||
};
|
||||
|
||||
const applyDecorators = compose(
|
||||
reduxConnect('analyze', ['appState', 'analyze']),
|
||||
translate(['tabsContent'], { wait: true })
|
||||
);
|
||||
|
||||
export default applyDecorators(React.memo(Performance));
|
||||
+403
@@ -0,0 +1,403 @@
|
||||
import React, { Fragment, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, ButtonGroup, Col, Row } from 'reactstrap';
|
||||
import ECharts from '../../../../../common/charts/ECharts';
|
||||
import ChartWrapper from '../ChartWrapper';
|
||||
import {
|
||||
getBarOptions,
|
||||
getPieOptions
|
||||
} from '../../../../../common/charts/ChartsOptions';
|
||||
import { IoIosAdd, IoIosRefresh, IoIosCheckmark } from 'react-icons/io';
|
||||
import reduxConnect from '../../../../../../redux/utils/connect';
|
||||
import translate from 'react-i18next/dist/commonjs/translate';
|
||||
import { compose } from 'redux';
|
||||
import {
|
||||
getOverviewBarAPI,
|
||||
getOverviewPieAPI
|
||||
} from '../../../../../../api/analytics/createAnalytics';
|
||||
import useIsMounted from '../../../../../common/hooks/useIsMounted';
|
||||
|
||||
const initialBar = {
|
||||
data: [],
|
||||
error: undefined,
|
||||
loading: true,
|
||||
vertical: false
|
||||
};
|
||||
|
||||
const initialPie = { data: [], error: undefined, loading: true };
|
||||
|
||||
function ResultsTab(props) {
|
||||
const { actions, analyze, feedData, id, t } = props;
|
||||
const isMounted = useIsMounted();
|
||||
const [barData, setBarData] = useState(initialBar);
|
||||
const [barTimeData, setBarTimeData] = useState(initialBar);
|
||||
const [pieData, setPieData] = useState(initialPie);
|
||||
const [pieTimeData, setPieTimeData] = useState(initialPie);
|
||||
const [filter, setFilter] = useState(filtersNames[0].id);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
if (filter === filtersNames[0].id) {
|
||||
getBarChart();
|
||||
getPieChart();
|
||||
} else {
|
||||
getBarChartFeeds();
|
||||
getPieChartFeeds();
|
||||
}
|
||||
}, [filter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (barData.data) {
|
||||
setBarData((prev) => ({
|
||||
...prev,
|
||||
data: {
|
||||
...prev.data,
|
||||
xAxis: prev.data.yAxis,
|
||||
yAxis: prev.data.xAxis
|
||||
}
|
||||
}));
|
||||
}
|
||||
}, [barData.vertical]);
|
||||
|
||||
function updateResult(foo, id) {
|
||||
switch (id) {
|
||||
case cn.first:
|
||||
filter === filtersNames[0].id ? getBarChart() : getBarChartFeeds();
|
||||
return;
|
||||
case cn.second:
|
||||
filter === filtersNames[0].id ? getPieChart() : getPieChartFeeds();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function getBarChart() {
|
||||
setBarData((prev) => ({ ...prev, loading: true }));
|
||||
getOverviewBarAPI(filter, id).then((res) => {
|
||||
if (!isMounted.current) {
|
||||
return false;
|
||||
}
|
||||
if (res.error || !res.data.data) {
|
||||
// on error
|
||||
setBarData((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: res.errorMessage
|
||||
}));
|
||||
return;
|
||||
}
|
||||
const { data } = res.data;
|
||||
const labels = data[0] ? Object.keys(data[0].data) : [];
|
||||
const datasets = data.map((item) => ({
|
||||
name: item.name,
|
||||
type: barData.vertical ? 'bar' : 'line',
|
||||
smooth: true,
|
||||
data: Object.values(item.data)
|
||||
}));
|
||||
|
||||
const barOptions = getBarOptions(datasets, labels);
|
||||
|
||||
setBarData({
|
||||
data: barOptions,
|
||||
error: false,
|
||||
loading: false,
|
||||
vertical: false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getBarChartFeeds() {
|
||||
setBarTimeData((prev) => ({ ...prev, loading: true }));
|
||||
getOverviewBarAPI(filter, id).then((res) => {
|
||||
if (!isMounted.current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (res.error || !res.data.data) {
|
||||
// on error
|
||||
setBarTimeData((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: res.errorMessage
|
||||
}));
|
||||
return;
|
||||
}
|
||||
const { data } = res.data;
|
||||
const barOptions = {};
|
||||
const errors = {};
|
||||
|
||||
data.map((feed) => {
|
||||
const { name, data } = feed;
|
||||
|
||||
if (!data || (Array.isArray(data) && data.length < 1)) {
|
||||
errors[name] = t('analyzeTab.noData');
|
||||
return;
|
||||
}
|
||||
|
||||
const labels = Object.keys(data[0].data).sort();
|
||||
const datasets = data.map((item) => ({
|
||||
name: item.name,
|
||||
type: barTimeData.vertical ? 'bar' : 'line',
|
||||
smooth: true,
|
||||
data: labels.map((v) => item.data[v])
|
||||
}));
|
||||
|
||||
barOptions[name] = getBarOptions(datasets, labels);
|
||||
});
|
||||
|
||||
setBarTimeData({
|
||||
data: barOptions,
|
||||
error: errors,
|
||||
loading: false,
|
||||
vertical: false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getPieChart() {
|
||||
setPieData((prev) => ({ ...prev, loading: true }));
|
||||
getOverviewPieAPI(filter, id).then((res) => {
|
||||
if (!isMounted.current) {
|
||||
return false;
|
||||
}
|
||||
if (res.error || !res.data.data) {
|
||||
// alert on error
|
||||
setPieData((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: res.errorMessage
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = res.data;
|
||||
const pieOptions = getPieOptions(
|
||||
Object.entries(data).map((v) => ({ name: v[0], value: v[1] }))
|
||||
);
|
||||
|
||||
setPieData({
|
||||
data: pieOptions,
|
||||
error: false,
|
||||
loading: false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getPieChartFeeds() {
|
||||
setPieTimeData((prev) => ({ ...prev, loading: true }));
|
||||
getOverviewPieAPI(filter, id).then((res) => {
|
||||
if (!isMounted.current) {
|
||||
return false;
|
||||
}
|
||||
if (res.error || !res.data.data) {
|
||||
// alert on error
|
||||
setPieTimeData((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: res.errorMessage
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = res.data;
|
||||
const pieOptions = {};
|
||||
const errors = {};
|
||||
|
||||
Object.entries(data).forEach((feed) => {
|
||||
const [name, value] = feed;
|
||||
|
||||
if (!value || (Array.isArray(value) && value.length < 1)) {
|
||||
errors[name] = t('analyzeTab.noData');
|
||||
}
|
||||
|
||||
pieOptions[name] = getPieOptions(
|
||||
Object.entries(value).map((v) => ({
|
||||
name: v[0],
|
||||
value: v[1]
|
||||
}))
|
||||
);
|
||||
});
|
||||
|
||||
setPieTimeData({
|
||||
data: pieOptions,
|
||||
error: errors,
|
||||
loading: false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function changeVertical() {
|
||||
setBarData((prev) => ({ ...prev, vertical: !prev.vertical }));
|
||||
}
|
||||
|
||||
const hideChart1Alert = (id) =>
|
||||
analyze.alertCharts.find((v) => v.name === cn.first && v.id === id);
|
||||
const hideChart2Alert = (id) =>
|
||||
analyze.alertCharts.find((v) => v.name === cn.second && v.id === id);
|
||||
|
||||
const barchartMenus = (id) => [
|
||||
{
|
||||
title: '', // t('analyzeTab.chartMenus.addToAlert'),
|
||||
icon: IoIosAdd,
|
||||
size: 24,
|
||||
fn: () => actions.addAlertChart({ name: cn.first, id }),
|
||||
showInMore: false,
|
||||
hide: hideChart1Alert(id)
|
||||
},
|
||||
{
|
||||
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
|
||||
icon: IoIosCheckmark,
|
||||
size: 24,
|
||||
showInMore: false,
|
||||
hide: !hideChart1Alert(id)
|
||||
},
|
||||
{
|
||||
title: t('analyzeTab.chartMenus.refresh'),
|
||||
icon: IoIosRefresh,
|
||||
fn: () => updateResult(null, cn.first),
|
||||
showInMore: false
|
||||
},
|
||||
/* {
|
||||
title: t('analyzeTab.chartMenus.addToDashboard'),
|
||||
fn: () => {},
|
||||
showInMore: true
|
||||
}, */
|
||||
{
|
||||
title: 'Toggle Horizontal/Vertical',
|
||||
fn: changeVertical,
|
||||
showInMore: true
|
||||
}
|
||||
];
|
||||
const piechartMenus = (id) => [
|
||||
{
|
||||
title: '', // t('analyzeTab.chartMenus.addToAlert'),
|
||||
icon: IoIosAdd,
|
||||
size: 24,
|
||||
fn: () => actions.addAlertChart({ name: cn.second, id }),
|
||||
showInMore: false,
|
||||
hide: hideChart2Alert(id)
|
||||
},
|
||||
{
|
||||
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
|
||||
icon: IoIosCheckmark,
|
||||
size: 24,
|
||||
showInMore: false,
|
||||
hide: !hideChart2Alert(id)
|
||||
},
|
||||
{
|
||||
title: t('analyzeTab.chartMenus.refresh'),
|
||||
icon: IoIosRefresh,
|
||||
fn: () => updateResult(null, cn.second),
|
||||
showInMore: false
|
||||
}
|
||||
// { title: t('analyzeTab.chartMenus.addToDashboard'), fn: () => {}, showInMore: true }
|
||||
];
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="mask-line overflow-auto white-space-nowrap pl-3 mb-3">
|
||||
<ButtonGroup size="sm">
|
||||
{filtersNames.map((item) => (
|
||||
<Button
|
||||
outline
|
||||
key={item.id}
|
||||
title={item.name}
|
||||
color="secondary"
|
||||
onClick={function () {
|
||||
setFilter(item.id);
|
||||
}}
|
||||
active={filter === item.id}
|
||||
>
|
||||
{t(`analyzeTab.overviewCharts.${item.transKey}`)}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
{filter === filtersNames[0].id ? ( // feeds in single graph
|
||||
<Row>
|
||||
<Col md="8">
|
||||
<ChartWrapper
|
||||
title={t('analyzeTab.charts.mentionsOverTime')}
|
||||
menus={barchartMenus('none')}
|
||||
>
|
||||
<ECharts
|
||||
xLabel={barData.labels}
|
||||
loading={barData.loading}
|
||||
options={barData.data}
|
||||
/>
|
||||
</ChartWrapper>
|
||||
</Col>
|
||||
<Col md="4">
|
||||
<ChartWrapper
|
||||
title={t('analyzeTab.charts.mentions')}
|
||||
menus={piechartMenus('none')}
|
||||
>
|
||||
<ECharts loading={pieData.loading} options={pieData.data} />
|
||||
</ChartWrapper>
|
||||
</Col>
|
||||
</Row>
|
||||
) : (
|
||||
feedData.feeds.map((feed) => (
|
||||
<Row key={feed.id}>
|
||||
<Col md="8">
|
||||
<ChartWrapper
|
||||
title={`${t('analyzeTab.charts.mentionsOverTime')} (${
|
||||
feed.feed
|
||||
})`}
|
||||
menus={barchartMenus(feed.id)}
|
||||
>
|
||||
<ECharts
|
||||
xLabel={barTimeData.labels}
|
||||
loading={barTimeData.loading}
|
||||
options={barTimeData.data && barTimeData.data[feed.feed]}
|
||||
message={barTimeData.error && barTimeData.error[feed.feed]}
|
||||
/>
|
||||
</ChartWrapper>
|
||||
</Col>
|
||||
<Col md="4">
|
||||
<ChartWrapper
|
||||
title={`${t('analyzeTab.charts.mentions')} (${feed.feed})`}
|
||||
menus={piechartMenus(feed.id)}
|
||||
>
|
||||
<ECharts
|
||||
loading={pieTimeData.loading}
|
||||
options={pieTimeData.data[feed.feed]}
|
||||
message={pieTimeData.error && pieTimeData.error[feed.feed]}
|
||||
/>
|
||||
</ChartWrapper>
|
||||
</Col>
|
||||
</Row>
|
||||
))
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const cn = {
|
||||
first: 'Mentions Over Time',
|
||||
second: 'Share of Mentions'
|
||||
};
|
||||
|
||||
const filtersNames = [
|
||||
{ name: 'None', transKey: 'none', id: 'none' },
|
||||
{ name: 'Media Types', transKey: 'mediaTypes', id: 'media' },
|
||||
{ name: 'Sentiments', transKey: 'sentiments', id: 'sentiment' },
|
||||
// { name: 'Countries', transKey:'countries', id: 'country' },
|
||||
{ name: 'Languages', transKey: 'languages', id: 'language' }
|
||||
];
|
||||
|
||||
ResultsTab.propTypes = {
|
||||
actions: PropTypes.object,
|
||||
id: PropTypes.string,
|
||||
t: PropTypes.func,
|
||||
feedData: PropTypes.object,
|
||||
analyze: PropTypes.object
|
||||
};
|
||||
|
||||
const applyDecorators = compose(
|
||||
reduxConnect('analyze', ['appState', 'analyze']),
|
||||
translate(['tabsContent'], { wait: true })
|
||||
);
|
||||
|
||||
export default applyDecorators(React.memo(ResultsTab));
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Col, Row } from 'reactstrap';
|
||||
import ECharts from '../../../../../common/charts/ECharts';
|
||||
import ChartWrapper from '../ChartWrapper';
|
||||
import {
|
||||
getBarOptions,
|
||||
getPieOptions
|
||||
} from '../../../../../common/charts/ChartsOptions';
|
||||
import { IoIosAdd, IoIosRefresh, IoIosCheckmark } from 'react-icons/io';
|
||||
import reduxConnect from '../../../../../../redux/utils/connect';
|
||||
import translate from 'react-i18next/dist/commonjs/translate';
|
||||
import { compose } from 'redux';
|
||||
import {
|
||||
getOverviewBarAPI,
|
||||
getOverviewPieAPI
|
||||
} from '../../../../../../api/analytics/createAnalytics';
|
||||
import useIsMounted from '../../../../../common/hooks/useIsMounted';
|
||||
|
||||
const initialBar = {
|
||||
data: [],
|
||||
error: undefined,
|
||||
loading: true,
|
||||
vertical: false
|
||||
};
|
||||
|
||||
const initialPie = { data: [], error: undefined, loading: true };
|
||||
|
||||
function Sentiment(props) {
|
||||
const { actions, analyze, feedData, id, t } = props;
|
||||
const isMounted = useIsMounted();
|
||||
const [barData, setBarData] = useState(initialBar);
|
||||
const [pieData, setPieData] = useState(initialPie);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
getBarChart();
|
||||
getPieChart();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (barData.data) {
|
||||
setBarData((prev) => ({
|
||||
...prev,
|
||||
data: {
|
||||
...prev.data,
|
||||
xAxis: prev.data.yAxis,
|
||||
yAxis: prev.data.xAxis
|
||||
}
|
||||
}));
|
||||
}
|
||||
}, [barData.vertical]);
|
||||
|
||||
function updateResult(foo, id) {
|
||||
switch (id) {
|
||||
case cn.first:
|
||||
getBarChart();
|
||||
return;
|
||||
case cn.second:
|
||||
getPieChart();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function getBarChart() {
|
||||
setBarData((prev) => ({ ...prev, loading: true }));
|
||||
getOverviewBarAPI('sentiment', id).then((res) => {
|
||||
if (!isMounted.current) {
|
||||
return false;
|
||||
}
|
||||
if (res.error || !res.data.data) {
|
||||
// on error
|
||||
setBarData((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: res.errorMessage
|
||||
}));
|
||||
return;
|
||||
}
|
||||
const { data } = res.data;
|
||||
const barOptions = {};
|
||||
data.forEach((feed) => {
|
||||
const { name, data } = feed;
|
||||
const labels = Object.keys(data[0].data).sort();
|
||||
const datasets = data.map((item) => ({
|
||||
name: item.name,
|
||||
type: barData.vertical ? 'bar' : 'line',
|
||||
smooth: true,
|
||||
data: labels.map((v) => item.data[v])
|
||||
}));
|
||||
|
||||
barOptions[name] = getBarOptions(datasets, labels);
|
||||
});
|
||||
|
||||
setBarData({
|
||||
data: barOptions,
|
||||
error: false,
|
||||
loading: false,
|
||||
vertical: false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getPieChart() {
|
||||
setPieData((prev) => ({ ...prev, loading: true }));
|
||||
getOverviewPieAPI('sentiment', id).then((res) => {
|
||||
if (!isMounted.current) {
|
||||
return false;
|
||||
}
|
||||
if (res.error || !res.data.data) {
|
||||
// alert on error
|
||||
setPieData((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: res.errorMessage
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = res.data;
|
||||
const pieOptions = {};
|
||||
Object.entries(data).forEach((feed) => {
|
||||
const [name, value] = feed;
|
||||
pieOptions[name] = getPieOptions(
|
||||
Object.entries(value).map((v) => ({
|
||||
name: v[0],
|
||||
value: v[1]
|
||||
}))
|
||||
);
|
||||
});
|
||||
|
||||
setPieData({
|
||||
data: pieOptions,
|
||||
error: false,
|
||||
loading: false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function changeVertical() {
|
||||
setBarData((prev) => ({ ...prev, vertical: !prev.vertical }));
|
||||
}
|
||||
|
||||
const hideChart1Alert = (id) =>
|
||||
analyze.alertCharts.find((v) => v.name === cn.first && v.id === id);
|
||||
const hideChart2Alert = (id) =>
|
||||
analyze.alertCharts.find((v) => v.name === cn.second && v.id === id);
|
||||
|
||||
const barchartMenus = (id) => [
|
||||
{
|
||||
title: '', // t('analyzeTab.chartMenus.addToAlert'),
|
||||
icon: IoIosAdd,
|
||||
size: 24,
|
||||
fn: () => actions.addAlertChart({ name: cn.first, id }),
|
||||
showInMore: false,
|
||||
hide: hideChart1Alert(id)
|
||||
},
|
||||
{
|
||||
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
|
||||
icon: IoIosCheckmark,
|
||||
size: 24,
|
||||
showInMore: false,
|
||||
hide: !hideChart1Alert(id)
|
||||
},
|
||||
{
|
||||
title: t('analyzeTab.chartMenus.refresh'),
|
||||
icon: IoIosRefresh,
|
||||
fn: () => updateResult(null, cn.first),
|
||||
showInMore: false
|
||||
},
|
||||
/* {
|
||||
title: t('analyzeTab.chartMenus.addToDashboard'),
|
||||
fn: () => {},
|
||||
showInMore: true
|
||||
}, */
|
||||
{
|
||||
title: 'Toggle Horizontal/Vertical',
|
||||
fn: changeVertical,
|
||||
showInMore: true
|
||||
}
|
||||
];
|
||||
const piechartMenus = (id) => [
|
||||
{
|
||||
title: '', // t('analyzeTab.chartMenus.addToAlert'),
|
||||
icon: IoIosAdd,
|
||||
size: 24,
|
||||
fn: () => actions.addAlertChart({ name: cn.second, id }),
|
||||
showInMore: false,
|
||||
hide: hideChart2Alert(id)
|
||||
},
|
||||
{
|
||||
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
|
||||
icon: IoIosCheckmark,
|
||||
size: 24,
|
||||
showInMore: false,
|
||||
hide: !hideChart2Alert(id)
|
||||
},
|
||||
{
|
||||
title: t('analyzeTab.chartMenus.refresh'),
|
||||
icon: IoIosRefresh,
|
||||
fn: () => updateResult(null, cn.second),
|
||||
showInMore: false
|
||||
}
|
||||
// { title: t('analyzeTab.chartMenus.addToDashboard'), fn: () => {}, showInMore: true }
|
||||
];
|
||||
|
||||
return feedData.feeds.map((feed) => (
|
||||
<Row key={feed.id}>
|
||||
<Col md="8">
|
||||
<ChartWrapper
|
||||
title={`${t('analyzeTab.charts.sentimentOverTime')} (${feed.feed})`}
|
||||
menus={barchartMenus(feed.id)}
|
||||
>
|
||||
<ECharts
|
||||
xLabel={barData.labels}
|
||||
loading={barData.loading}
|
||||
options={barData.data[feed.feed]}
|
||||
/>
|
||||
</ChartWrapper>
|
||||
</Col>
|
||||
<Col md="4">
|
||||
<ChartWrapper
|
||||
title={`${t('analyzeTab.charts.shareofSentiment')} (${feed.feed})`}
|
||||
menus={piechartMenus(feed.id)}
|
||||
>
|
||||
<ECharts
|
||||
loading={pieData.loading}
|
||||
options={pieData.data[feed.feed]}
|
||||
/>
|
||||
</ChartWrapper>
|
||||
</Col>
|
||||
</Row>
|
||||
));
|
||||
}
|
||||
|
||||
const cn = {
|
||||
first: 'Sentiment Over Time',
|
||||
second: 'Share of Sentiment'
|
||||
};
|
||||
|
||||
Sentiment.propTypes = {
|
||||
actions: PropTypes.object,
|
||||
feedData: PropTypes.object,
|
||||
analyze: PropTypes.object,
|
||||
t: PropTypes.func
|
||||
};
|
||||
|
||||
const applyDecorators = compose(
|
||||
reduxConnect('analyze', ['appState', 'analyze']),
|
||||
translate(['tabsContent'], { wait: true })
|
||||
);
|
||||
|
||||
export default applyDecorators(React.memo(Sentiment));
|
||||
+284
@@ -0,0 +1,284 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Col, Row } from 'reactstrap';
|
||||
import ECharts from '../../../../../common/charts/ECharts';
|
||||
import 'echarts-wordcloud';
|
||||
import { capitalize } from 'lodash';
|
||||
import ChartWrapper from '../ChartWrapper';
|
||||
import {
|
||||
getBarOptions,
|
||||
PieToolbox,
|
||||
WordCloudOptions
|
||||
} from '../../../../../common/charts/ChartsOptions';
|
||||
import { IoIosAdd, IoIosRefresh, IoIosCheckmark } from 'react-icons/io';
|
||||
import reduxConnect from '../../../../../../redux/utils/connect';
|
||||
import translate from 'react-i18next/dist/commonjs/translate';
|
||||
import { compose } from 'redux';
|
||||
import {
|
||||
getThemesCloudAPI,
|
||||
getThemesTimeAPI
|
||||
} from '../../../../../../api/analytics/createAnalytics';
|
||||
import useIsMounted from '../../../../../common/hooks/useIsMounted';
|
||||
import { capFirstLetter } from '../../../../../../common/helper';
|
||||
|
||||
const initialBar = {
|
||||
data: [],
|
||||
error: undefined,
|
||||
loading: true,
|
||||
vertical: false
|
||||
};
|
||||
|
||||
const initialPie = { data: [], error: undefined, loading: true };
|
||||
|
||||
function Themes(props) {
|
||||
const { actions, analyze, feedData, id, t } = props;
|
||||
const isMounted = useIsMounted();
|
||||
const [barData, setBarData] = useState(initialBar);
|
||||
const [wordData, setWordData] = useState(initialPie);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
getBarChart();
|
||||
getWordCloud();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (barData.data) {
|
||||
setBarData((prev) => ({
|
||||
...prev,
|
||||
data: {
|
||||
...prev.data,
|
||||
xAxis: prev.data.yAxis,
|
||||
yAxis: prev.data.xAxis
|
||||
}
|
||||
}));
|
||||
}
|
||||
}, [barData.vertical]);
|
||||
|
||||
function updateResult(foo, id) {
|
||||
switch (id) {
|
||||
case cn.first:
|
||||
getBarChart();
|
||||
return;
|
||||
case cn.second:
|
||||
getWordCloud();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function getBarChart() {
|
||||
setBarData((prev) => ({ ...prev, loading: true }));
|
||||
getThemesTimeAPI(id).then((res) => {
|
||||
if (!isMounted.current) {
|
||||
return false;
|
||||
}
|
||||
if (res.error || !res.data.data) {
|
||||
// on error
|
||||
setBarData((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: res.errorMessage
|
||||
}));
|
||||
return;
|
||||
}
|
||||
const { data } = res.data;
|
||||
let labels = null;
|
||||
const barOptions = {};
|
||||
const errors = {};
|
||||
data.forEach((feedData) => {
|
||||
const { name, data } = feedData;
|
||||
const datasets = data.map((item) => ({
|
||||
name: capitalize(item.name),
|
||||
type: barData.vertical ? 'bar' : 'line',
|
||||
smooth: true,
|
||||
data: Object.values(item.data)
|
||||
}));
|
||||
|
||||
if (!labels && data && data[0] && data[0].data) {
|
||||
labels = Object.keys(data[0].data);
|
||||
}
|
||||
|
||||
barOptions[name] = getBarOptions(datasets, labels);
|
||||
|
||||
if (!datasets || (Array.isArray(datasets) && datasets.length < 1)) {
|
||||
errors[name] = t('analyzeTab.noData');
|
||||
}
|
||||
});
|
||||
|
||||
setBarData({
|
||||
data: barOptions,
|
||||
error: errors,
|
||||
loading: false,
|
||||
vertical: false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getWordCloud() {
|
||||
setWordData((prev) => ({ ...prev, loading: true }));
|
||||
getThemesCloudAPI(id).then((res) => {
|
||||
if (!isMounted.current) {
|
||||
return false;
|
||||
}
|
||||
if (res.error || !res.data.data) {
|
||||
// alert on error
|
||||
setWordData((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: res.errorMessage
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = res.data;
|
||||
const cloudOptions = {};
|
||||
const errors = {};
|
||||
data.forEach((feed) => {
|
||||
const { name, data } = feed;
|
||||
if (!data || (Array.isArray(data) && data.length < 1)) {
|
||||
errors[name] = t('analyzeTab.noData');
|
||||
}
|
||||
|
||||
cloudOptions[name] = {
|
||||
tooltip: {
|
||||
show: true
|
||||
},
|
||||
toolbox: PieToolbox,
|
||||
series: [
|
||||
{
|
||||
...WordCloudOptions,
|
||||
data: Object.entries(data).map((v) => ({
|
||||
name: capFirstLetter(v[0]),
|
||||
value: v[1]
|
||||
}))
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
setWordData({
|
||||
data: cloudOptions,
|
||||
error: false,
|
||||
loading: false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function changeVertical() {
|
||||
setBarData((prev) => ({ ...prev, vertical: !prev.vertical }));
|
||||
}
|
||||
|
||||
const hideChart1Alert = (id) =>
|
||||
analyze.alertCharts.find((v) => v.name === cn.first && v.id === id);
|
||||
const hideChart2Alert = (id) =>
|
||||
analyze.alertCharts.find((v) => v.name === cn.second && v.id === id);
|
||||
|
||||
const barchartMenus = (id) => [
|
||||
{
|
||||
title: '', // t('analyzeTab.chartMenus.addToAlert'),
|
||||
icon: IoIosAdd,
|
||||
size: 24,
|
||||
fn: () => actions.addAlertChart({ name: cn.first, id }),
|
||||
showInMore: false,
|
||||
hide: hideChart1Alert(id)
|
||||
},
|
||||
{
|
||||
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
|
||||
icon: IoIosCheckmark,
|
||||
size: 24,
|
||||
showInMore: false,
|
||||
hide: !hideChart1Alert(id)
|
||||
},
|
||||
{
|
||||
title: t('analyzeTab.chartMenus.refresh'),
|
||||
icon: IoIosRefresh,
|
||||
fn: () => updateResult(null, cn.first),
|
||||
showInMore: false
|
||||
},
|
||||
/* {
|
||||
title: t('analyzeTab.chartMenus.addToDashboard'),
|
||||
fn: () => {},
|
||||
showInMore: true
|
||||
}, */
|
||||
{
|
||||
title: 'Toggle Horizontal/Vertical',
|
||||
fn: changeVertical,
|
||||
showInMore: true
|
||||
}
|
||||
];
|
||||
const wordCloudMenus = (id) => [
|
||||
{
|
||||
title: '', // t('analyzeTab.chartMenus.addToAlert'),
|
||||
icon: IoIosAdd,
|
||||
size: 24,
|
||||
fn: () => actions.addAlertChart({ name: cn.second, id }),
|
||||
showInMore: false,
|
||||
hide: hideChart2Alert(id)
|
||||
},
|
||||
{
|
||||
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
|
||||
icon: IoIosCheckmark,
|
||||
size: 24,
|
||||
showInMore: false,
|
||||
hide: !hideChart2Alert(id)
|
||||
},
|
||||
{
|
||||
title: t('analyzeTab.chartMenus.refresh'),
|
||||
icon: IoIosRefresh,
|
||||
fn: () => updateResult(null, cn.second),
|
||||
showInMore: false
|
||||
}
|
||||
// { title: t('analyzeTab.chartMenus.addToDashboard'), fn: () => {}, showInMore: true }
|
||||
];
|
||||
|
||||
return feedData.feeds.map((feed) => (
|
||||
<Row key={feed.id}>
|
||||
<Col md="8">
|
||||
<ChartWrapper
|
||||
title={`${t('analyzeTab.charts.themesOverTime')} (${feed.feed})`}
|
||||
menus={barchartMenus(feed.id)}
|
||||
>
|
||||
<ECharts
|
||||
xLabel={barData.labels}
|
||||
loading={barData.loading}
|
||||
options={barData.data[feed.feed]}
|
||||
message={barData.error && barData.error[feed.feed]}
|
||||
/>
|
||||
</ChartWrapper>
|
||||
</Col>
|
||||
<Col md="4">
|
||||
<ChartWrapper
|
||||
title={`${t('analyzeTab.charts.topThemes')} (${feed.feed})`}
|
||||
menus={wordCloudMenus(feed.id)}
|
||||
>
|
||||
<ECharts
|
||||
loading={wordData.loading}
|
||||
options={wordData.data[feed.feed]}
|
||||
message={barData.error && barData.error[feed.feed]}
|
||||
/>
|
||||
</ChartWrapper>
|
||||
</Col>
|
||||
</Row>
|
||||
));
|
||||
}
|
||||
|
||||
const cn = {
|
||||
first: 'Themes over time',
|
||||
second: 'Top Themes'
|
||||
};
|
||||
|
||||
Themes.propTypes = {
|
||||
chartData: PropTypes.object,
|
||||
actions: PropTypes.object,
|
||||
feedData: PropTypes.object,
|
||||
t: PropTypes.func,
|
||||
analyze: PropTypes.object
|
||||
};
|
||||
|
||||
const applyDecorators = compose(
|
||||
reduxConnect('analyze', ['appState', 'analyze']),
|
||||
translate(['tabsContent'], { wait: true })
|
||||
);
|
||||
|
||||
export default applyDecorators(React.memo(Themes));
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
import React, { useEffect, useRef, useState, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Row, Col, ButtonGroup, Button } from 'reactstrap';
|
||||
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import L from 'leaflet';
|
||||
import 'leaflet-dvf/dist/leaflet-dvf';
|
||||
// keep above 3 in sequence
|
||||
import ChartWrapper from '../ChartWrapper';
|
||||
import { getWorldMapAPI } from '../../../../../../api/analytics/createAnalytics';
|
||||
import useIsMounted from '../../../../../common/hooks/useIsMounted';
|
||||
import { translate } from 'react-i18next';
|
||||
|
||||
const initialPie = {
|
||||
data: [],
|
||||
error: undefined,
|
||||
loading: true,
|
||||
selected: undefined
|
||||
};
|
||||
|
||||
function WorldMap(props) {
|
||||
const { id, t } = props;
|
||||
const mapRef = useRef();
|
||||
const isMounted = useIsMounted();
|
||||
const [pieData, setPieData] = useState(initialPie);
|
||||
const [markers, setMarkers] = useState([]);
|
||||
|
||||
const feedNames = (pieData.data && Object.keys(pieData.data)) || [];
|
||||
|
||||
useEffect(() => {
|
||||
mapRef.current = L.map('leaflet-map', {
|
||||
center: [0, 0],
|
||||
zoom: 2,
|
||||
layers: [
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
noWrap: true,
|
||||
attribution:
|
||||
'© <a target="_blank" noreferrer noopener href="http://osm.org/copyright">OpenStreetMap</a> contributors'
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
mapRef.current.whenReady(getMapSentiments);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const { data, selected, error } = pieData;
|
||||
const selectedData = data[feedNames[selected]];
|
||||
const hasErr = error && error[feedNames[selected]];
|
||||
clearMap();
|
||||
|
||||
if (selectedData && !hasErr) {
|
||||
// loop to add marker
|
||||
const markersList = [];
|
||||
selectedData.forEach((data) => {
|
||||
const [lat, lng] = getLatLong(data.LatLng);
|
||||
if (!lat || !lng) {
|
||||
return;
|
||||
}
|
||||
|
||||
let pieChartMarker = new L.PieChartMarker(new L.LatLng(lat, lng), {
|
||||
...options,
|
||||
data: {
|
||||
positive: data.POSITIVE,
|
||||
negative: data.NEGATIVE,
|
||||
neutral: data.NEUTRAL
|
||||
}
|
||||
});
|
||||
pieChartMarker.addTo(mapRef.current);
|
||||
markersList.push(pieChartMarker);
|
||||
});
|
||||
// eslint-disable-next-line new-cap
|
||||
const group = new L.featureGroup(markersList);
|
||||
mapRef.current.fitBounds(group.getBounds());
|
||||
setMarkers(markersList);
|
||||
}
|
||||
}, [pieData.data, pieData.selected]);
|
||||
|
||||
function getLatLong(str) {
|
||||
const [lat, lng] = str.split(', ');
|
||||
return [lat && parseFloat(lat), lng && parseFloat(lng)];
|
||||
}
|
||||
|
||||
function clearMap() {
|
||||
if (mapRef.current) {
|
||||
markers.forEach((v) => {
|
||||
mapRef.current.removeLayer(v);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getMapSentiments() {
|
||||
setPieData((prev) => ({ ...prev, loading: true }));
|
||||
getWorldMapAPI(id).then((res) => {
|
||||
if (!isMounted.current) {
|
||||
return false;
|
||||
}
|
||||
if (res.error || !res.data.data) {
|
||||
// alert on error
|
||||
setPieData((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: res.errorMessage
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = res.data;
|
||||
const dataValues = {};
|
||||
const errors = {};
|
||||
|
||||
data.map((feed) => {
|
||||
const { name, data } = feed;
|
||||
if (!data || (Array.isArray(data) && data.length < 1)) {
|
||||
errors[name] = t('analyzeTab.noData');
|
||||
}
|
||||
dataValues[name] = data;
|
||||
});
|
||||
|
||||
setPieData({
|
||||
data: dataValues,
|
||||
error: errors,
|
||||
loading: false,
|
||||
selected: 0
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const style = {
|
||||
height: 'max(300px, calc(100vh - 200px))'
|
||||
};
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Col md="12">
|
||||
<ChartWrapper title="Distribution by Sentiments">
|
||||
<Fragment>
|
||||
<ButtonGroup size="sm" className="d-block mb-2 text-right">
|
||||
{feedNames.map((name, i) => (
|
||||
<Button
|
||||
outline
|
||||
key={name}
|
||||
title={name}
|
||||
color="secondary"
|
||||
onClick={function () {
|
||||
setPieData((prev) => ({
|
||||
...prev,
|
||||
selected: i
|
||||
}));
|
||||
}}
|
||||
active={pieData.selected === i}
|
||||
>
|
||||
{name}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
<div className="position-relative">
|
||||
<div id="leaflet-map" style={style} />
|
||||
{pieData.error && pieData.error[feedNames[pieData.selected]] ? (
|
||||
<div className="no-data" style={{ zIndex: 1000 }}>
|
||||
{pieData.error[feedNames[pieData.selected]]}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Fragment>
|
||||
</ChartWrapper>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
const options = {
|
||||
stroke: false,
|
||||
fillOpacity: 0.7,
|
||||
radius: 20,
|
||||
gradient: false,
|
||||
chartOptions: {
|
||||
positive: {
|
||||
fillColor: '#00FF00',
|
||||
displayText: function (value) {
|
||||
return value.toFixed(0);
|
||||
}
|
||||
},
|
||||
negative: {
|
||||
fillColor: '#FF0000',
|
||||
displayText: function (value) {
|
||||
return value.toFixed(0);
|
||||
}
|
||||
},
|
||||
neutral: {
|
||||
fillColor: '#000000',
|
||||
displayText: function (value) {
|
||||
return value.toFixed(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Other L.Path style options
|
||||
};
|
||||
|
||||
WorldMap.propTypes = {
|
||||
actions: PropTypes.object,
|
||||
feedData: PropTypes.object,
|
||||
id: PropTypes.string,
|
||||
t: PropTypes.func.isRequired,
|
||||
analyze: PropTypes.object
|
||||
};
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(WorldMap);
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import Results from './Results'
|
||||
import Performance from './Performance'
|
||||
import Influencers from './Influencers'
|
||||
import Sentiment from './Sentiment'
|
||||
import Themes from './Themes'
|
||||
import Demographics from './Demographics'
|
||||
import WorldMap from './WorldMap'
|
||||
|
||||
export {
|
||||
Results,
|
||||
Performance,
|
||||
Influencers,
|
||||
Sentiment,
|
||||
Themes,
|
||||
Demographics,
|
||||
WorldMap
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
||||
import { deleteAnalytics } from '../../../../../api/analytics/savedAnalytics';
|
||||
import { translate } from 'react-i18next';
|
||||
|
||||
function DeleteDialog(props) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { actions, data, toggle, fetchData, t } = props;
|
||||
|
||||
function handleSubmit() {
|
||||
setLoading(true);
|
||||
deleteAnalytics(data.value).then((res) => {
|
||||
if (res.error) {
|
||||
res.data
|
||||
? actions.addAlert(res.data)
|
||||
: actions.addAlert({ type: 'error', transKey: 'somethingWrong' });
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
actions.addAlert({ type: 'notice', transKey: 'analyticsDeleted' });
|
||||
setLoading(false);
|
||||
toggle();
|
||||
fetchData();
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={!!data} toggle={toggle} backdrop="static">
|
||||
<ModalHeader toggle={toggle}>
|
||||
{t('tabsContent:analyzeTab.deleteAnalysis')}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<div>
|
||||
<p>{t('messages.deleteMessage')}</p>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="link" onClick={toggle}>
|
||||
{t('commonWords.Cancel')}
|
||||
</Button>
|
||||
<Button color="danger" disabled={loading} onClick={handleSubmit}>
|
||||
{loading ? t('commonWords.loading') : t('commonWords.Delete')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
DeleteDialog.propTypes = {
|
||||
toggle: PropTypes.func,
|
||||
t: PropTypes.func.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
fetchData: PropTypes.func,
|
||||
actions: PropTypes.object
|
||||
};
|
||||
|
||||
export default React.memo(translate(['common'], { wait: true })(DeleteDialog));
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import React, {
|
||||
useState,
|
||||
useCallback,
|
||||
useMemo,
|
||||
Fragment,
|
||||
useEffect
|
||||
} from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import { compose } from 'redux';
|
||||
import { Table } from '../../../../common/Table/Table';
|
||||
import { savedAnalytics } from '../../../../../api/analytics/savedAnalytics';
|
||||
import reduxConnect from '../../../../../redux/utils/connect';
|
||||
import {
|
||||
getDate,
|
||||
getQueryParams,
|
||||
setDocumentData
|
||||
} from '../../../../../common/helper';
|
||||
import { Button } from 'reactstrap';
|
||||
import DeleteDialog from './DeleteDialog';
|
||||
import i18n from '../../../../../i18n';
|
||||
|
||||
function SavedAnalysisSubTab(props) {
|
||||
const [dataSource, setDataSource] = useState({ data: [] });
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [deleteValues, setDeleteValues] = useState(false);
|
||||
const { t, actions } = props;
|
||||
|
||||
useEffect(() => {
|
||||
setDocumentData('title', 'Saved Analysis | Analyze');
|
||||
return () => {
|
||||
setDocumentData('title');
|
||||
};
|
||||
}, []);
|
||||
|
||||
const columns = useMemo(() => {
|
||||
const columnsList = [
|
||||
{
|
||||
id: 'feeds',
|
||||
Header: t('analyzeTab.savedAnalytics.feeds'),
|
||||
accessor: (d) => d.context.feeds,
|
||||
Cell: (props) =>
|
||||
props.value ? props.value.map((v) => v.name).join(', ') : ''
|
||||
},
|
||||
{
|
||||
id: 'date',
|
||||
Header: t('analyzeTab.savedAnalytics.dateRange'),
|
||||
accessor: (d) => d.context.rawFilters.date,
|
||||
Cell: (props) =>
|
||||
props.value
|
||||
? `${getDate(props.value.start, 'MM/DD/YYYY')} to ${getDate(
|
||||
props.value.end,
|
||||
'MM/DD/YYYY'
|
||||
)}`
|
||||
: '-'
|
||||
},
|
||||
{
|
||||
Header: t('analyzeTab.savedAnalytics.createdAt'),
|
||||
accessor: 'createdAt',
|
||||
Cell: (props) => getDate(props.value, 'MM/DD/YYYY')
|
||||
},
|
||||
{
|
||||
Header: t('analyzeTab.savedAnalytics.actions'),
|
||||
accessor: 'id',
|
||||
Cell: (props) => getActions(props)
|
||||
}
|
||||
];
|
||||
|
||||
return columnsList;
|
||||
}, [getActions, i18n.language]);
|
||||
|
||||
const getActions = useCallback((props) => {
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
outline
|
||||
className="border-0 btn-transition"
|
||||
color="primary"
|
||||
size="sm"
|
||||
tag={Link}
|
||||
to={`/app/analyze/${props.value}/overview`}
|
||||
>
|
||||
{t('analyzeTab.savedAnalytics.view')}
|
||||
</Button>
|
||||
<Button
|
||||
outline
|
||||
className="border-0 btn-transition"
|
||||
color="secondary"
|
||||
tag={Link}
|
||||
to={`/app/analyze/edit/${props.value}`}
|
||||
>
|
||||
{t('analyzeTab.savedAnalytics.edit')}
|
||||
</Button>
|
||||
<Button
|
||||
outline
|
||||
className="border-0 btn-transition"
|
||||
color="secondary"
|
||||
onClick={function () {
|
||||
setDeleteValues(props);
|
||||
}}
|
||||
>
|
||||
{t('analyzeTab.savedAnalytics.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}, []);
|
||||
|
||||
const getSavedList = useCallback(
|
||||
(page, pageSize) => {
|
||||
setLoading(true);
|
||||
const params = getQueryParams({ page, pageSize });
|
||||
savedAnalytics(params).then((res) => {
|
||||
if (res.error || res.data === null || !res.data) {
|
||||
setLoading(false);
|
||||
return actions.addAlert({
|
||||
type: 'error',
|
||||
transKey: 'somethingWrong'
|
||||
});
|
||||
}
|
||||
res.data.length > 0 && setDataSource(res.data[0]);
|
||||
setLoading(false);
|
||||
});
|
||||
},
|
||||
[savedAnalytics]
|
||||
);
|
||||
|
||||
const { data = [], totalCount = 0, limit = 10, page = 1 } = dataSource;
|
||||
return (
|
||||
<Fragment>
|
||||
<Table
|
||||
t={t}
|
||||
cardTitle={t('analyzeTab.savedAnalysis')}
|
||||
columns={columns}
|
||||
data={data}
|
||||
totalCount={totalCount}
|
||||
showTotalCount
|
||||
limit={limit}
|
||||
page={page}
|
||||
isLoading={loading}
|
||||
onFetchData={getSavedList}
|
||||
/>
|
||||
{deleteValues && (
|
||||
<DeleteDialog
|
||||
data={deleteValues}
|
||||
actions={actions}
|
||||
toggle={function () {
|
||||
setDeleteValues(false);
|
||||
}}
|
||||
fetchData={function () {
|
||||
getSavedList(dataSource.page - 1, dataSource.limit);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
SavedAnalysisSubTab.propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
actions: PropTypes.object
|
||||
};
|
||||
|
||||
const applyDecorators = compose(
|
||||
translate(['tabsContent'], { wait: true }),
|
||||
reduxConnect()
|
||||
);
|
||||
|
||||
export default applyDecorators(SavedAnalysisSubTab);
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate } from 'react-i18next'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { compose } from 'redux'
|
||||
import { Card, Col, Row } from 'reactstrap'
|
||||
|
||||
class WelcomeSubTab extends React.Component {
|
||||
render () {
|
||||
const { t } = this.props
|
||||
|
||||
return (
|
||||
<Card className="py-md-5 mb-3">
|
||||
<Row className="justify-content-center no-gutters">
|
||||
<Col sm="6" md="4" xl="4" className="m-4">
|
||||
<div className="border b-radius-5 text-center p-4">
|
||||
<div className="icon-wrapper mb-4 rounded-circle">
|
||||
<div className="icon-wrapper-bg bg-primary" />
|
||||
<i className="lnr-plus-circle text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="mb-5">{t('analyzeTab.createNewAnalysis')}</h5>
|
||||
<Link
|
||||
to="/app/analyze/create"
|
||||
className="btn btn-primary btn-block fsize-1 btn-lg mr-1"
|
||||
>
|
||||
{t('analyzeTab.go')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col sm="6" md="4" xl="4" className="m-4">
|
||||
<div className="border b-radius-5 text-center p-4">
|
||||
<div className="icon-wrapper mb-4 rounded-circle">
|
||||
<div className="icon-wrapper-bg bg-primary" />
|
||||
<i className="lnr-list text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="mb-5">{t('analyzeTab.viewSavedAnalysis')}</h5>
|
||||
<Link
|
||||
to="/app/analyze/saved"
|
||||
className="btn btn-primary btn-block fsize-1 btn-lg mr-1"
|
||||
>
|
||||
{t('analyzeTab.view')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
WelcomeSubTab.propTypes = {
|
||||
t: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
const applyDecorators = compose(translate(['tabsContent'], { wait: true }))
|
||||
|
||||
export default applyDecorators(WelcomeSubTab)
|
||||
@@ -0,0 +1,548 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import TimeAgo from 'timeago-react';
|
||||
import ArticleComment from './ArticleComment';
|
||||
import {
|
||||
UncontrolledDropdown,
|
||||
DropdownToggle,
|
||||
DropdownMenu,
|
||||
DropdownItem,
|
||||
CustomInput,
|
||||
Button
|
||||
} from 'reactstrap';
|
||||
import ShareMenu from './ShareMenu';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faFacebook,
|
||||
faInstagram,
|
||||
faPinterest,
|
||||
faReddit,
|
||||
faTumblr,
|
||||
faTwitter,
|
||||
faYoutube
|
||||
} from '@fortawesome/free-brands-svg-icons';
|
||||
import {
|
||||
faComments,
|
||||
faEye,
|
||||
faFrown,
|
||||
faMeh,
|
||||
faQuoteLeft,
|
||||
faShareAlt,
|
||||
faSmile,
|
||||
faThumbsDown,
|
||||
faThumbsUp
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import {
|
||||
capOnlyFirstLetter,
|
||||
convertUTCtoLocal,
|
||||
abbreviateNumber,
|
||||
notNullAndUnd
|
||||
} from '../../../../../common/helper';
|
||||
import SourceIndexInfoPopup from '../SourceIndexSubTab/SourceIndexInfoPopup';
|
||||
|
||||
const icons = {
|
||||
twitter: faTwitter,
|
||||
facebook: faFacebook,
|
||||
instagram: faInstagram,
|
||||
tumblr: faTumblr,
|
||||
pinterest: faPinterest,
|
||||
reddit: faReddit,
|
||||
youtube: faYoutube,
|
||||
POSITIVE: faSmile,
|
||||
NEGATIVE: faFrown,
|
||||
NEUTRAL: faMeh
|
||||
};
|
||||
|
||||
const colors = {
|
||||
POSITIVE: '#3ac47d',
|
||||
NEGATIVE: '#FC3939',
|
||||
NEUTRAL: '#868e96',
|
||||
twitter: '#1DA1F2',
|
||||
facebook: '#4267B2',
|
||||
reddit: '#FF5700',
|
||||
instagram: '#8a3ab9',
|
||||
tumblr: '#34526F',
|
||||
pinterest: '#E60023',
|
||||
youtube: '#FF0000'
|
||||
};
|
||||
|
||||
export class Article extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
shareMenu: false,
|
||||
imgErr: false,
|
||||
sourceModal: false
|
||||
};
|
||||
|
||||
this.elemDesc = React.createRef();
|
||||
}
|
||||
|
||||
selectArticle = () => {
|
||||
this.props.selectArticle(this.props.article);
|
||||
};
|
||||
|
||||
showEmailPopup = () => {
|
||||
this.props.showEmailPopup([this.props.article]);
|
||||
};
|
||||
|
||||
showCommentPopup = () => {
|
||||
this.props.showCommentPopup(this.props.article);
|
||||
};
|
||||
|
||||
showDeletePopup = () => {
|
||||
this.props.showDeletePopup([this.props.article]);
|
||||
};
|
||||
|
||||
showClipPopup = () => {
|
||||
this.props.showClipPopup([this.props.article]);
|
||||
};
|
||||
|
||||
toggleShareMenu = () => {
|
||||
this.setState((prev) => ({ shareMenu: !prev.shareMenu }));
|
||||
};
|
||||
|
||||
loadMoreComments = () => {
|
||||
const {
|
||||
loadMoreComments,
|
||||
article: {
|
||||
id: articleId,
|
||||
comments: { count: offset }
|
||||
}
|
||||
} = this.props;
|
||||
loadMoreComments(articleId, offset);
|
||||
};
|
||||
|
||||
readLater = () => {
|
||||
this.props.readArticleLater(this.props.article);
|
||||
};
|
||||
|
||||
onImgError = () => {
|
||||
this.setState({ imgErr: true });
|
||||
};
|
||||
|
||||
toggleSourceModal = () => {
|
||||
this.setState((prev) => ({ sourceModal: !prev.sourceModal }));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { article, t, i18n, showCommentPopup, deleteComment } = this.props;
|
||||
let {
|
||||
comments,
|
||||
id,
|
||||
source,
|
||||
sentiment,
|
||||
permalink,
|
||||
publisher,
|
||||
title,
|
||||
image,
|
||||
author,
|
||||
content,
|
||||
published,
|
||||
mentions,
|
||||
tags,
|
||||
likes,
|
||||
dislikes,
|
||||
views,
|
||||
shares,
|
||||
categories
|
||||
} = article;
|
||||
const { imgErr } = this.state;
|
||||
const {
|
||||
data: commentsData,
|
||||
count: commentsCount, // should get real post comment count
|
||||
totalCount: commentsTotalCount
|
||||
} = comments;
|
||||
|
||||
const isArticleChosen = !!this.props.selectedArticles.find(
|
||||
(item) => item.id === id
|
||||
);
|
||||
|
||||
const offsetWidth =
|
||||
this.elemDesc &&
|
||||
this.elemDesc.current &&
|
||||
this.elemDesc.current.offsetWidth;
|
||||
|
||||
const hasRightCounters =
|
||||
notNullAndUnd(likes) ||
|
||||
notNullAndUnd(dislikes) ||
|
||||
commentsCount || // add not null and undefined when counter shows
|
||||
notNullAndUnd(views) ||
|
||||
notNullAndUnd(shares) ||
|
||||
notNullAndUnd(mentions);
|
||||
|
||||
const isTwitter = source.siteType === 'twitter';
|
||||
const isInstagram = source.siteType === 'instagram';
|
||||
let username;
|
||||
if (isTwitter) {
|
||||
username =
|
||||
author.link &&
|
||||
author.link.match(
|
||||
/^https?:\/\/(www\.)?twitter\.com\/(#!\/)?([^\/]+)(\/\w+)*$/
|
||||
);
|
||||
username = username && username[3];
|
||||
}
|
||||
if (isInstagram) {
|
||||
username =
|
||||
author.link &&
|
||||
author.link.match(
|
||||
/(?:(?:http|https):\/\/)?(?:www\.)?(?:instagram\.com|instagr\.am)\/([A-Za-z0-9-_\.]+)/
|
||||
);
|
||||
username = username && username[1];
|
||||
}
|
||||
|
||||
const isRTL = document.documentElement.dir === 'rtl';
|
||||
return (
|
||||
<div className="post border b-radius-5 mb-4">
|
||||
<UncontrolledDropdown className="post__menu">
|
||||
<DropdownToggle
|
||||
outline
|
||||
color="primary"
|
||||
className="btn-icon btn-icon-only p-1 m-2"
|
||||
>
|
||||
<i className="lnr lnr-menu btn-icon-wrapper" />
|
||||
</DropdownToggle>
|
||||
<DropdownMenu className={isRTL ? ' dropdown-menu-left' : ''}>
|
||||
<DropdownItem
|
||||
className="text-muted"
|
||||
onClick={this.showCommentPopup}
|
||||
>
|
||||
<i className="mr-2 fa fa-comments"> </i>
|
||||
<span>{t('searchTab.commentBtn')}</span>
|
||||
</DropdownItem>
|
||||
<DropdownItem className="text-muted" onClick={this.showClipPopup}>
|
||||
<i className="mr-2 fa fa-cut"> </i>
|
||||
<span>{t('searchTab.clipBtn')}</span>
|
||||
</DropdownItem>
|
||||
<DropdownItem className="text-muted" onClick={this.readLater}>
|
||||
<i className="mr-2 fa fa-bookmark"> </i>
|
||||
<span>{t('searchTab.readLaterBtn')}</span>
|
||||
</DropdownItem>
|
||||
<DropdownItem className="text-muted" onClick={this.readLater}>
|
||||
<i className="mr-2 fa fa-archive"> </i>
|
||||
<span>{t('searchTab.archiveBtn')}</span>
|
||||
</DropdownItem>
|
||||
<DropdownItem className="text-muted" onClick={this.showEmailPopup}>
|
||||
<i className="mr-2 fa fa-envelope"> </i>
|
||||
<span>{t('searchTab.emailBtn')}</span>
|
||||
</DropdownItem>
|
||||
<DropdownItem className="text-muted" onClick={this.toggleShareMenu}>
|
||||
<i className="mr-2 fa fa-share-alt"> </i>
|
||||
<span>{t('searchTab.shareBtn')}</span>
|
||||
</DropdownItem>
|
||||
<DropdownItem className="text-muted" onClick={this.showDeletePopup}>
|
||||
<i className="mr-2 fa fa-trash"> </i>
|
||||
<span>{t('searchTab.deleteBtn')}</span>
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</UncontrolledDropdown>
|
||||
<div className="d-flex flex-row">
|
||||
<div className="post__icons">
|
||||
<CustomInput
|
||||
id={'article-check-' + id}
|
||||
type="checkbox"
|
||||
className="mb-3"
|
||||
onChange={this.selectArticle}
|
||||
checked={isArticleChosen}
|
||||
/>
|
||||
{source.siteType && (
|
||||
<FontAwesomeIcon
|
||||
title={capOnlyFirstLetter(source.siteType)}
|
||||
icon={icons[source.siteType]}
|
||||
size="lg"
|
||||
className="fa-w-16 mb-3"
|
||||
color={colors[source.siteType]}
|
||||
/>
|
||||
)}
|
||||
{sentiment && (
|
||||
<FontAwesomeIcon
|
||||
title={capOnlyFirstLetter(sentiment)}
|
||||
icon={icons[sentiment]}
|
||||
className="mb-3"
|
||||
size="lg"
|
||||
color={colors[sentiment]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="post_middlepart">
|
||||
<h2 className="post__title">
|
||||
{title && (
|
||||
<a href={permalink} target="_blank" rel="noopener noreferrer">
|
||||
{title}
|
||||
</a>
|
||||
)}
|
||||
</h2>
|
||||
<div
|
||||
ref={this.elemDesc}
|
||||
className={`post__content${
|
||||
offsetWidth && offsetWidth < 430 ? ' flex-column' : ''
|
||||
}`}
|
||||
>
|
||||
{image &&
|
||||
!imgErr &&
|
||||
(!title && permalink ? (
|
||||
<a href={permalink} target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
id={id}
|
||||
width="180px"
|
||||
className="post__img mb-2 mb-lg-0 mr-3"
|
||||
src={image}
|
||||
onError={this.onImgError}
|
||||
/>
|
||||
</a>
|
||||
) : (
|
||||
<img
|
||||
id={id}
|
||||
width="180px"
|
||||
className="post__img mb-2 mb-lg-0 mr-3"
|
||||
src={image}
|
||||
onError={this.onImgError}
|
||||
/>
|
||||
))}
|
||||
|
||||
<div>
|
||||
{author.name ? (
|
||||
author.link ? (
|
||||
<a
|
||||
className="d-inline-block hover-link text-muted mb-2"
|
||||
href={author.link}
|
||||
target="_blank"
|
||||
>
|
||||
{username ? `@${username}` : author.name}
|
||||
</a>
|
||||
) : (
|
||||
<p className="text-muted mb-2">{author.name}</p>
|
||||
)
|
||||
) : null}
|
||||
{!title && permalink ? (
|
||||
<a
|
||||
href={permalink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="post__desc-link"
|
||||
>
|
||||
<p
|
||||
className="post__desc"
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
></p>
|
||||
</a>
|
||||
) : (
|
||||
<p
|
||||
className="post__desc"
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
></p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tags && tags.length && tags.length > 0 && (
|
||||
<div className="post__tags mt-2">
|
||||
<strong>{t('searchTab.tags')}</strong>: {tags.join(', ')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{categories && categories.length > 0 && (
|
||||
<p className="post__tags my-2">
|
||||
<strong>{t('searchTab.categories')}</strong>:{' '}
|
||||
{categories.join(', ')}
|
||||
</p>
|
||||
)}
|
||||
<div className="post__about-info text-muted mt-3">
|
||||
{published && (
|
||||
<Fragment>
|
||||
<span
|
||||
className="d-inline-block"
|
||||
title={convertUTCtoLocal(published, 'MM/DD/YYYY HH:mm:ss')}
|
||||
>
|
||||
<TimeAgo
|
||||
datetime={published}
|
||||
locale={i18n.language}
|
||||
opts={{ minInterval: 60 }}
|
||||
/>
|
||||
</span>
|
||||
<span className="mx-2">|</span>
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
{source.type && (
|
||||
<Fragment>
|
||||
<span>{capOnlyFirstLetter(source.type)}</span>
|
||||
<span className="mx-2">|</span>
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
{source.country && (
|
||||
<Fragment>
|
||||
<span>{source.country}</span>
|
||||
<span className="mx-2">|</span>
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
{publisher && (
|
||||
<Fragment>
|
||||
<Button
|
||||
color="link"
|
||||
className="btn-anchor"
|
||||
title="Click to see details"
|
||||
onClick={this.toggleSourceModal}
|
||||
>
|
||||
{publisher}
|
||||
</Button>
|
||||
<span className="mx-2">|</span>
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
{source.title && (
|
||||
<Fragment>
|
||||
{publisher ? (
|
||||
<a
|
||||
href={source.link}
|
||||
style={{ overflowWrap: 'anywhere' }}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{source.title}
|
||||
</a>
|
||||
) : (
|
||||
<Button
|
||||
color="link"
|
||||
className="btn-anchor"
|
||||
title="Click to see details"
|
||||
onClick={this.toggleSourceModal}
|
||||
>
|
||||
{(isTwitter || isInstagram) && author.name
|
||||
? author.name
|
||||
: source.title}
|
||||
</Button>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{hasRightCounters && (
|
||||
<div className="post__extras p-3">
|
||||
<div className="post__icons-wrapper">
|
||||
{notNullAndUnd(likes) && (
|
||||
<div className="post__icon-metrics mb-1">
|
||||
<FontAwesomeIcon
|
||||
title="Likes"
|
||||
icon={faThumbsUp}
|
||||
className="text-success"
|
||||
/>
|
||||
<p className="ml-2" title={likes}>
|
||||
{abbreviateNumber(likes)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{notNullAndUnd(dislikes) && (
|
||||
<div className="post__icon-metrics mb-1">
|
||||
<FontAwesomeIcon title="Dislikes" icon={faThumbsDown} />
|
||||
<p className="ml-2" title={dislikes}>
|
||||
{abbreviateNumber(dislikes)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{/* {notNullAndUnd(commentsCount) && (
|
||||
Add above line when real comment counts are visible
|
||||
*/}
|
||||
{commentsCount ? (
|
||||
<div className="post__icon-metrics mb-1">
|
||||
<FontAwesomeIcon title="Comments" icon={faComments} />
|
||||
<p className="ml-2" title={commentsCount}>
|
||||
{abbreviateNumber(commentsCount)}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{notNullAndUnd(views) && (
|
||||
<div className="post__icon-metrics mb-1">
|
||||
<FontAwesomeIcon title="Viwes" icon={faEye} />
|
||||
<p className="ml-2 text-center" title={views}>
|
||||
{abbreviateNumber(views)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{notNullAndUnd(shares) && (
|
||||
<div className="post__icon-metrics mb-1">
|
||||
<FontAwesomeIcon title="Shares" icon={faShareAlt} />
|
||||
<p className="ml-2 text-center" title={shares}>
|
||||
{abbreviateNumber(shares)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{notNullAndUnd(mentions) && (
|
||||
<div className="post__icon-metrics mb-1">
|
||||
<FontAwesomeIcon title="Mentions" icon={faQuoteLeft} />
|
||||
<p className="ml-2 text-center" title={mentions}>
|
||||
{abbreviateNumber(mentions)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{commentsData && commentsData.length > 0 && (
|
||||
<div className="post__comments border-top px-3 pb-3">
|
||||
{commentsData.map((comment) => {
|
||||
return (
|
||||
<ArticleComment
|
||||
article={article}
|
||||
comment={comment}
|
||||
showCommentPopup={showCommentPopup}
|
||||
deleteComment={deleteComment}
|
||||
key={comment.id}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{commentsCount < commentsTotalCount && (
|
||||
<Button
|
||||
outline
|
||||
size="sm"
|
||||
color="light"
|
||||
className="mt-2 d-block ml-auto btn-icon"
|
||||
onClick={this.loadMoreComments}
|
||||
>
|
||||
<i className="lnr lnr-chevron-down btn-icon-wrapper" />{' '}
|
||||
{t('searchTab.moreComments')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{this.state.shareMenu && (
|
||||
<ShareMenu article={article} hideMenu={this.toggleShareMenu} />
|
||||
)}
|
||||
|
||||
{this.state.sourceModal && (
|
||||
<SourceIndexInfoPopup
|
||||
source={article.source}
|
||||
hideSourceInfoPopup={this.toggleSourceModal}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Article.propTypes = {
|
||||
article: PropTypes.object.isRequired,
|
||||
selectedArticles: PropTypes.array.isRequired,
|
||||
selectArticle: PropTypes.func.isRequired,
|
||||
showEmailPopup: PropTypes.func.isRequired,
|
||||
showDeletePopup: PropTypes.func.isRequired,
|
||||
showCommentPopup: PropTypes.func.isRequired,
|
||||
showClipPopup: PropTypes.func.isRequired,
|
||||
deleteComment: PropTypes.func.isRequired,
|
||||
readArticleLater: PropTypes.func.isRequired,
|
||||
loadMoreComments: PropTypes.func.isRequired,
|
||||
showShareMenu: PropTypes.func.isRequired,
|
||||
i18n: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(Article);
|
||||
@@ -0,0 +1,66 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate, Interpolate } from 'react-i18next'
|
||||
import TimeAgo from 'timeago-react'
|
||||
import { Button } from 'reactstrap'
|
||||
|
||||
export class ArticleComment extends React.Component {
|
||||
static propTypes = {
|
||||
article: PropTypes.object.isRequired,
|
||||
comment: PropTypes.func.isRequired,
|
||||
deleteComment: PropTypes.func.isRequired,
|
||||
showCommentPopup: PropTypes.func.isRequired,
|
||||
i18n: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
onEdit = () => {
|
||||
const { showCommentPopup, article, comment } = this.props
|
||||
showCommentPopup(article, comment)
|
||||
}
|
||||
|
||||
onDelete = () => {
|
||||
const { deleteComment, article, comment } = this.props
|
||||
deleteComment(comment.id, article.id)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { comment, i18n } = this.props
|
||||
|
||||
return (
|
||||
<div className="post__comment mt-2">
|
||||
<div className="d-flex justify-content-between">
|
||||
<div>
|
||||
<cite className="post__commentor mr-3">
|
||||
<Interpolate
|
||||
i18nKey="searchTab.commentMetadata"
|
||||
author={`${comment.author.firstName} ${comment.author.lastName}`}
|
||||
/>
|
||||
</cite>
|
||||
<span className="post__cmttime mr-3 text-muted">
|
||||
<TimeAgo
|
||||
datetime={comment.createdAt}
|
||||
locale={i18n.language}
|
||||
opts={{ minInterval: 30 }}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<Button color="link" className="p-0" onClick={this.onEdit}>
|
||||
<i className="lnr lnr-pencil"></i>
|
||||
</Button>
|
||||
<Button color="link" className="ml-2 p-0" onClick={this.onDelete}>
|
||||
<i className="lnr lnr-trash"></i>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="post__cmt-content">
|
||||
<strong className="d-block mb-1">{comment.title}</strong>
|
||||
{comment.content}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(ArticleComment)
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate } from 'react-i18next'
|
||||
import ClipDragSource from './ClipDragSource'
|
||||
import RecentFeed from './RecentFeed'
|
||||
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'
|
||||
|
||||
export class ClipArticlesPopup extends React.Component {
|
||||
static propTypes = {
|
||||
hidePopup: PropTypes.func.isRequired,
|
||||
clipArticles: PropTypes.func.isRequired,
|
||||
articles: PropTypes.array.isRequired,
|
||||
recentClipFeeds: PropTypes.array.isRequired,
|
||||
getRecentClipFeeds: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
hidePopupFromOutside = (e) => {
|
||||
if (e.target === e.currentTarget) this.hidePopup()
|
||||
}
|
||||
|
||||
hidePopup = () => {
|
||||
this.props.hidePopup()
|
||||
}
|
||||
|
||||
onSubmit = () => {
|
||||
this.hidePopup()
|
||||
}
|
||||
|
||||
componentWillMount = () => {
|
||||
this.props.getRecentClipFeeds()
|
||||
}
|
||||
|
||||
onRecentFeedClick = (feed) => {
|
||||
this.props.clipArticles(feed.id)
|
||||
this.props.hidePopup()
|
||||
}
|
||||
|
||||
render() {
|
||||
const { t, articles, recentClipFeeds } = this.props
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen
|
||||
toggle={this.hidePopup}
|
||||
backdrop={false}
|
||||
modalClassName="pointer-events-none"
|
||||
>
|
||||
<ModalHeader toggle={this.hidePopup}>
|
||||
{t('searchTab.clipPopup.header')}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="text-center">
|
||||
<p>{t('searchTab.clipPopup.hint1')}</p>
|
||||
|
||||
<div className="draggable-container">
|
||||
<ClipDragSource articles={articles} />
|
||||
</div>
|
||||
|
||||
{recentClipFeeds && recentClipFeeds.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<p className="mb-2">{t('searchTab.clipPopup.hint2')}</p>
|
||||
<div className="d-flex justify-content-center flex-wrap">
|
||||
{recentClipFeeds.map((feed) => {
|
||||
return (
|
||||
<RecentFeed
|
||||
onRecentFeedClick={this.onRecentFeedClick}
|
||||
key={feed.id}
|
||||
feed={feed}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="light" onClick={this.hidePopup}>
|
||||
{t('common:commonWords.Cancel')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent', 'common'], { wait: true })(
|
||||
ClipArticlesPopup
|
||||
)
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { TYPES } from '../../../../../../redux/modules/appState/sidebar'
|
||||
import { Interpolate } from 'react-i18next'
|
||||
import { DragSource } from 'react-dnd'
|
||||
|
||||
const source = {
|
||||
beginDrag (props, monitor, component) {
|
||||
setTimeout(() => {
|
||||
component.setState({
|
||||
isDragging: true
|
||||
})
|
||||
}, 0)
|
||||
return {
|
||||
type: TYPES.CLIP_ARTICLE
|
||||
}
|
||||
},
|
||||
|
||||
endDrag (props, monitor, component) {
|
||||
component.setState({
|
||||
isDragging: false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies which props to inject into component from Drag n Drop.
|
||||
*/
|
||||
function collect (connect) {
|
||||
return {
|
||||
// Call this function inside render()
|
||||
// to let React DnD handle the drag events:
|
||||
connectDragSource: connect.dragSource()
|
||||
}
|
||||
}
|
||||
|
||||
export class ClipDragSource extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
articles: PropTypes.array.isRequired,
|
||||
connectDragSource: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
isDragging: false
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
|
||||
const style = {
|
||||
visibility: this.state.isDragging ? 'hidden' : 'visible'
|
||||
}
|
||||
|
||||
return this.props.connectDragSource(
|
||||
<div className="draggable-item" style={style}>
|
||||
<span className="drag-handle" />
|
||||
<Interpolate
|
||||
i18nKey='searchTab.clipPopup.clippedArticles'
|
||||
count={this.props.articles.length}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default DragSource(TYPES.CLIP_ARTICLE, source, collect)(ClipDragSource)
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Button } from 'reactstrap'
|
||||
|
||||
export default class RecentFeed extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
feed: PropTypes.object.isRequired,
|
||||
onRecentFeedClick: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
onClick = () => {
|
||||
this.props.onRecentFeedClick(this.props.feed)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { feed } = this.props
|
||||
|
||||
return (
|
||||
<Button color="light" className={'mr-2 mb-2 feed-icon ' + feed.class} onClick={this.onClick}>
|
||||
{feed.name}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate, Interpolate } from 'react-i18next';
|
||||
import TimeAgo from 'timeago-react';
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalHeader
|
||||
} from 'reactstrap';
|
||||
|
||||
const initCharactersCount = 5000;
|
||||
|
||||
export class CommentArticlePopup extends React.Component {
|
||||
static propTypes = {
|
||||
article: PropTypes.object.isRequired,
|
||||
comment: PropTypes.object,
|
||||
commentArticle: PropTypes.func.isRequired,
|
||||
updateComment: PropTypes.func.isRequired,
|
||||
hidePopup: PropTypes.func.isRequired,
|
||||
i18n: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const content = props.comment ? props.comment.content : '';
|
||||
this.state = {
|
||||
charactersCount: initCharactersCount - content.length,
|
||||
title: props.comment ? props.comment.title : '',
|
||||
comment: content
|
||||
};
|
||||
}
|
||||
|
||||
handleTitleChange = (e) => {
|
||||
const { value } = e.target;
|
||||
this.setState({ title: value });
|
||||
};
|
||||
|
||||
hidePopup = () => {
|
||||
this.props.hidePopup();
|
||||
};
|
||||
|
||||
onSubmit = () => {
|
||||
const newComment = {
|
||||
title: this.state.title,
|
||||
content: this.state.comment
|
||||
};
|
||||
if (this.props.comment) {
|
||||
//edit exisitng
|
||||
this.props.updateComment(newComment, this.props.article.id);
|
||||
} else {
|
||||
//create new comment
|
||||
this.props.commentArticle(newComment, this.props.article.id);
|
||||
}
|
||||
this.hidePopup();
|
||||
};
|
||||
|
||||
onChangeComment = (e) => {
|
||||
const charactersCount = initCharactersCount - e.target.value.length;
|
||||
|
||||
if (charactersCount >= 0) {
|
||||
this.setState({
|
||||
charactersCount: charactersCount,
|
||||
comment: e.target.value
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, i18n, article, comment } = this.props;
|
||||
const popupTitle = comment
|
||||
? t('searchTab.commentPopup.editUserComment')
|
||||
: t('searchTab.commentPopup.addUserComment');
|
||||
|
||||
return (
|
||||
<Modal isOpen toggle={this.hidePopup} backdrop="static">
|
||||
<ModalHeader toggle={this.hidePopup}>{popupTitle}</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="mb-3">
|
||||
<a
|
||||
className="font-size-lg"
|
||||
href={article.permalink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{article.title}
|
||||
</a>
|
||||
<p>{article.author.name}</p>
|
||||
<p className="font-size-xs text-muted">
|
||||
<TimeAgo
|
||||
datetime={article.published}
|
||||
locale={i18n.language}
|
||||
opts={{ minInterval: 30 }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
value={this.state.title}
|
||||
type="text"
|
||||
className="mb-2"
|
||||
onChange={this.handleTitleChange}
|
||||
placeholder={t('searchTab.commentPopup.inputTitlePlaceholder')}
|
||||
/>
|
||||
|
||||
<Input
|
||||
rows="3"
|
||||
type="textarea"
|
||||
value={this.state.comment}
|
||||
onChange={this.onChangeComment}
|
||||
placeholder={t('searchTab.commentPopup.commentPlanceholder')}
|
||||
/>
|
||||
|
||||
<p className="font-size-xs text-muted text-right mt-1">
|
||||
<Interpolate
|
||||
i18nKey="searchTab.commentPopup.charactersLeft"
|
||||
count={this.state.charactersCount}
|
||||
/>
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="light" onClick={this.hidePopup}>
|
||||
{t('common:commonWords.Cancel')}
|
||||
</Button>
|
||||
<Button color="primary" onClick={this.onSubmit}>
|
||||
{t('common:commonWords.submit')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent', 'common'], { wait: true })(
|
||||
CommentArticlePopup
|
||||
);
|
||||
@@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Interpolate, translate } from 'react-i18next';
|
||||
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
||||
|
||||
export class DeleteArticlesPopup extends React.Component {
|
||||
static propTypes = {
|
||||
articles: PropTypes.array.isRequired,
|
||||
activeFeed: PropTypes.object,
|
||||
hidePopup: PropTypes.func.isRequired,
|
||||
deleteArticles: PropTypes.func.isRequired,
|
||||
deleteArticlesFromFeed: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
onSubmit = () => {
|
||||
const {
|
||||
articles,
|
||||
activeFeed,
|
||||
deleteArticles,
|
||||
deleteArticlesFromFeed,
|
||||
hidePopup
|
||||
} = this.props;
|
||||
const ids = articles.map((a) => a.id);
|
||||
if (activeFeed) {
|
||||
deleteArticlesFromFeed(ids, activeFeed.id);
|
||||
} else {
|
||||
deleteArticles(ids);
|
||||
}
|
||||
hidePopup();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, articles, hidePopup } = this.props;
|
||||
|
||||
return (
|
||||
<Modal isOpen toggle={hidePopup} backdrop="static">
|
||||
<ModalHeader toggle={hidePopup}>{t('commonWords.Confirm')}</ModalHeader>
|
||||
<ModalBody>
|
||||
<p>
|
||||
{articles.length > 1 ? (
|
||||
<Interpolate
|
||||
t={t}
|
||||
i18nKey="tabsContent:searchTab.deleteArticlePopupText_plural"
|
||||
articlesLength={articles.length}
|
||||
/>
|
||||
) : (
|
||||
t('tabsContent:searchTab.deleteArticlePopupText')
|
||||
)}
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="light" onClick={hidePopup}>
|
||||
{t('commonWords.Cancel')}
|
||||
</Button>
|
||||
<Button color="danger" onClick={this.onSubmit}>
|
||||
{t('commonWords.Delete')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['common'], { wait: true })(DeleteArticlesPopup);
|
||||
@@ -0,0 +1,209 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate } from 'react-i18next'
|
||||
import moment from 'moment'
|
||||
import Select from 'react-select'
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
Label,
|
||||
Input,
|
||||
ModalFooter,
|
||||
FormGroup,
|
||||
Col,
|
||||
Container
|
||||
} from 'reactstrap'
|
||||
import QuillEditor from '../../../../common/QuillEditor'
|
||||
|
||||
const replyToEmail = 'support@socialhose.io'
|
||||
|
||||
export class EmailArticlesPopup extends React.Component {
|
||||
static propTypes = {
|
||||
articlesToEmail: PropTypes.array.isRequired,
|
||||
emailArticles: PropTypes.func.isRequired,
|
||||
hidePopup: PropTypes.func.isRequired,
|
||||
recipients: PropTypes.object.isRequired,
|
||||
loadRecipients: PropTypes.func.isRequired,
|
||||
children: PropTypes.any,
|
||||
t: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
selectedRecipients: ''
|
||||
}
|
||||
this.editorRef = React.createRef()
|
||||
}
|
||||
|
||||
componentWillMount = () => {
|
||||
!this.props.recipients.all.length && this.props.loadRecipients()
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
this.props.loadRecipients()
|
||||
}
|
||||
|
||||
hidePopup = () => {
|
||||
this.props.hidePopup()
|
||||
}
|
||||
|
||||
collectParams = () => { // need to change with states
|
||||
const recipients = this.state.selectedRecipients
|
||||
if (!recipients) return false
|
||||
return {
|
||||
emailTo: recipients.map((r) => r.value),
|
||||
emailReplyTo: document.getElementById('email-reply-to').value,
|
||||
subject: document.getElementById('email-subject').value,
|
||||
content: this.editorRef.current && this.editorRef.current.root.innerHTML
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit = () => {
|
||||
const params = this.collectParams()
|
||||
if (params) {
|
||||
this.props.emailArticles(params)
|
||||
}
|
||||
}
|
||||
|
||||
changeRecipient = (value) => {
|
||||
this.setState({
|
||||
selectedRecipients: value
|
||||
})
|
||||
}
|
||||
|
||||
validEmails = (str) => {
|
||||
const re = /\S+@\S+\.\S+/
|
||||
const arr = str.split(',')
|
||||
for (let s of arr) {
|
||||
if (!re.test(s)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
emailRe = /\S+@\S+\.\S+/
|
||||
|
||||
isValidNewOption = ({ label }) => {
|
||||
return this.emailRe.test(label)
|
||||
}
|
||||
|
||||
promptTextCreator = (label) => {
|
||||
return label
|
||||
}
|
||||
|
||||
render() {
|
||||
const { t, articlesToEmail, recipients } = this.props
|
||||
const { selectedRecipients } = this.state
|
||||
|
||||
const recipientsAll = recipients.all.map((recipient) => ({
|
||||
value: recipient,
|
||||
label: recipient
|
||||
}))
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen
|
||||
size="lg"
|
||||
toggle={this.hidePopup}
|
||||
backdrop="static"
|
||||
>
|
||||
<ModalHeader toggle={this.hidePopup}>
|
||||
{t('searchTab.emailPopup.header')}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<Container>
|
||||
<FormGroup row>
|
||||
<Label htmlFor="email-to" sm={2}>
|
||||
{t('searchTab.emailPopup.labelTo')}
|
||||
</Label>
|
||||
<Col sm={10}>
|
||||
{recipients.pending && <i className="fa fa-spinner fa-pulse m-2" />}
|
||||
{!recipients.pending && (
|
||||
<Select.Creatable
|
||||
multi
|
||||
value={selectedRecipients}
|
||||
options={recipientsAll}
|
||||
onChange={this.changeRecipient}
|
||||
isValidNewOption={this.isValidNewOption}
|
||||
promptTextCreator={this.promptTextCreator}
|
||||
noResultsText="Email not valid"
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
</FormGroup>
|
||||
<FormGroup row>
|
||||
<Label htmlFor="email-reply-to" sm={2}>
|
||||
{t('searchTab.emailPopup.labelReplyTo')}
|
||||
</Label>
|
||||
<Col sm={10}>
|
||||
<Input
|
||||
type="email"
|
||||
id="email-reply-to"
|
||||
defaultValue={replyToEmail}
|
||||
/>
|
||||
</Col>
|
||||
</FormGroup>
|
||||
<FormGroup row>
|
||||
<Label htmlFor="email-subject" sm={2}>
|
||||
{t('searchTab.emailPopup.labelSubject')}
|
||||
</Label>
|
||||
<Col sm={10}>
|
||||
<Input type="text" id="email-subject" />
|
||||
</Col>
|
||||
</FormGroup>
|
||||
|
||||
<div className="email-popup">
|
||||
<QuillEditor
|
||||
className="email-popup__articles email-editor"
|
||||
reference={this.editorRef}
|
||||
id="email-editor"
|
||||
>
|
||||
{articlesToEmail.map((article) => {
|
||||
return (
|
||||
<div className="email-popup__article" key={article.id}>
|
||||
<h2 className="article__title">
|
||||
<a href={article.source.link}>{article.title}</a>
|
||||
</h2>
|
||||
|
||||
<div className="article__about-info">
|
||||
<a href={article.source.link} target="blank">
|
||||
{article.source.title}
|
||||
</a>{' '}
|
||||
<span> | </span>
|
||||
<a href={article.author.link} target="blank">
|
||||
{article.author.name}
|
||||
</a>{' '}
|
||||
<span> | </span>
|
||||
{moment(article.published).format('LLL')}
|
||||
</div>
|
||||
|
||||
<p className="article__desc">{article.content}</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</QuillEditor>
|
||||
</div>
|
||||
</Container>
|
||||
{this.props.children}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button color="light" onClick={this.hidePopup}>
|
||||
{t('common:commonWords.Cancel')}
|
||||
</Button>
|
||||
<Button color="primary" onClick={this.onSubmit}>
|
||||
{t('searchTab.emailPopup.submitBtn')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent', 'common'], { wait: true })(
|
||||
EmailArticlesPopup
|
||||
)
|
||||
@@ -0,0 +1,50 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate } from 'react-i18next'
|
||||
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'
|
||||
|
||||
class EmailConfirmPopup extends React.Component {
|
||||
static propTypes = {
|
||||
hidePopup: PropTypes.func.isRequired,
|
||||
hideEmailPopup: PropTypes.func.isRequired,
|
||||
sendDocumentsByEmail: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
hidePopup = () => {
|
||||
this.props.hidePopup()
|
||||
}
|
||||
|
||||
onSubmit = () => {
|
||||
this.props.sendDocumentsByEmail()
|
||||
this.hidePopup()
|
||||
this.props.hideEmailPopup()
|
||||
}
|
||||
|
||||
render() {
|
||||
const { t } = this.props
|
||||
|
||||
return (
|
||||
<Modal isOpen toggle={this.hidePopup} backdrop="static">
|
||||
<ModalHeader toggle={this.hidePopup}>
|
||||
{t('common:commonWords.Confirm')}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<p>{t('searchTab.emailPopup.sendConfirmWithoutSubject')}</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="light" onClick={this.hidePopup}>
|
||||
{t('searchTab.emailPopup.dontSend')}
|
||||
</Button>
|
||||
<Button color="warning" onClick={this.onSubmit}>
|
||||
{t('searchTab.emailPopup.sendAnyway')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent', 'common'], { wait: true })(
|
||||
EmailConfirmPopup
|
||||
)
|
||||
@@ -0,0 +1,175 @@
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
import React, { Fragment, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
import { translate } from 'react-i18next';
|
||||
import SearchDatesPopup from './SearchDatesPopup';
|
||||
import { Modal, Button, ModalHeader, ModalBody } from 'reactstrap';
|
||||
import { IoIosCalendar } from 'react-icons/io';
|
||||
|
||||
// previous commented code
|
||||
// componentWillMount = () => {
|
||||
// const { actions, userSubscription } = this.props;
|
||||
// actions.setSearchLastDate(userSubscription);
|
||||
// };
|
||||
export function MediaTypes(props) {
|
||||
const [modal, setModal] = useState(false);
|
||||
|
||||
const {
|
||||
t,
|
||||
mediaTypes,
|
||||
actions,
|
||||
chosenMediaTypes,
|
||||
toggleMediaType,
|
||||
toggleAllMediaTypes,
|
||||
restrictions
|
||||
} = props;
|
||||
|
||||
const allSelected = mediaTypes.length === chosenMediaTypes.length;
|
||||
|
||||
function toggle() {
|
||||
setModal((modal) => !modal);
|
||||
}
|
||||
|
||||
// set only the allowed media types from restrictions initially
|
||||
function allowPermissions(mediaType) {
|
||||
if (!restrictions || !restrictions.plans) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// for selecting all
|
||||
if (!mediaType) {
|
||||
return mediaTypes.every((mt) => restrictions.plans[mt]);
|
||||
}
|
||||
|
||||
return restrictions.plans[mediaType];
|
||||
}
|
||||
|
||||
function toggleSingleType(mediaType, value) {
|
||||
/* const isFree = restrictions.plans.price === 0;
|
||||
// TODO: remove following restrictions when duplication fixes
|
||||
const restrictedTemporary =
|
||||
isFree && ['news', 'blogs'].includes(mediaType) && value;
|
||||
|
||||
if (!allowPermissions(mediaType) || restrictedTemporary) { */
|
||||
if (!allowPermissions(mediaType)) {
|
||||
return actions.toggleUpgradeModal();
|
||||
}
|
||||
toggleMediaType(mediaType, value); // restrict condition
|
||||
}
|
||||
|
||||
function toggleAllTypes() {
|
||||
// TODO: remove following restrictions when duplication fixes
|
||||
/* const isFree = restrictions.plans.price === 0;
|
||||
if (!allowPermissions() || isFree) { */
|
||||
if (!allowPermissions()) {
|
||||
return actions.toggleUpgradeModal();
|
||||
}
|
||||
toggleAllMediaTypes(!allSelected);
|
||||
}
|
||||
|
||||
/*
|
||||
const {
|
||||
chosenSearchDate,
|
||||
chosenSearchInterval
|
||||
chosenStartDate,
|
||||
chosenEndDate
|
||||
} = props.searchByFiltersState
|
||||
const isIntervalBetween = chosenSearchInterval === 'between';
|
||||
const searchDateBtnText = isIntervalBetween &&
|
||||
chosenStartDate !== '' ||
|
||||
isIntervalBetween &&
|
||||
chosenEndDate !== ''
|
||||
? chosenSearchDate : t('searchTab.userSubscription.' + chosenSearchDate);
|
||||
*/
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="d-flex justify-content-between align-items-start">
|
||||
<div data-tour="select-media-types">
|
||||
<Button
|
||||
outline
|
||||
size="sm"
|
||||
title={allSelected ? 'Click to deselect' : 'Click to select'}
|
||||
className="btn-pill mb-2 mr-2 px-3"
|
||||
color={cx('light', { active: allSelected })}
|
||||
onClick={toggleAllTypes}
|
||||
>
|
||||
{t('searchTab.sourceTypes.all')}
|
||||
</Button>
|
||||
{mediaTypes.map((mediaType, i) => {
|
||||
const isMediaTypeChosen =
|
||||
chosenMediaTypes.indexOf(mediaType) !== -1;
|
||||
return (
|
||||
<Button
|
||||
key={mediaType}
|
||||
outline
|
||||
size="sm"
|
||||
title={
|
||||
isMediaTypeChosen ? 'Click to deselect' : 'Click to select'
|
||||
}
|
||||
className="btn-pill mb-2 mr-2 px-3"
|
||||
color={cx('light', {
|
||||
active: isMediaTypeChosen
|
||||
})}
|
||||
onClick={() => toggleSingleType(mediaType, !isMediaTypeChosen)}
|
||||
>
|
||||
{t('searchTab.sourceTypes.' + mediaType)}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Button
|
||||
color="link"
|
||||
className="ml-2"
|
||||
onClick={toggle}
|
||||
data-tour="select-date-range"
|
||||
>
|
||||
<IoIosCalendar fontSize="24px" />
|
||||
{/* {t('searchTab.datesRange')} */}
|
||||
</Button>
|
||||
</div>
|
||||
<Modal isOpen={modal} toggle={toggle} data-tour="date-range-modal">
|
||||
<ModalHeader toggle={toggle}>Select dates</ModalHeader>
|
||||
<ModalBody>
|
||||
<SearchDatesPopup
|
||||
outsideClickIgnoreClass="react-datepicker"
|
||||
userSubscription={props.userSubscription}
|
||||
userSubscriptionDate={props.userSubscriptionDate}
|
||||
searchIntervals={props.searchByFiltersState.searchIntervals}
|
||||
searchLastDates={props.searchByFiltersState.searchLastDates}
|
||||
chosenSearchInterval={
|
||||
props.searchByFiltersState.chosenSearchInterval
|
||||
}
|
||||
chosenSearchLastDate={
|
||||
props.searchByFiltersState.chosenSearchLastDate
|
||||
}
|
||||
chosenStartDate={props.searchByFiltersState.chosenStartDate}
|
||||
chosenEndDate={props.searchByFiltersState.chosenEndDate}
|
||||
hideSearchDatesPopup={toggle}
|
||||
setSearchInterval={actions.setSearchInterval}
|
||||
setSearchLastDate={actions.setSearchLastDate}
|
||||
setSearchDate={actions.setSearchDate}
|
||||
setStartDate={actions.setStartDate}
|
||||
setEndDate={actions.setEndDate}
|
||||
/>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
MediaTypes.propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
mediaTypes: PropTypes.array.isRequired,
|
||||
chosenMediaTypes: PropTypes.array.isRequired,
|
||||
toggleMediaType: PropTypes.func.isRequired,
|
||||
toggleAllMediaTypes: PropTypes.func.isRequired,
|
||||
restrictions: PropTypes.object.isRequired,
|
||||
actions: PropTypes.object.isRequired,
|
||||
userSubscriptionDate: PropTypes.string.isRequired,
|
||||
userSubscription: PropTypes.string.isRequired,
|
||||
searchByFiltersState: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(MediaTypes);
|
||||
@@ -0,0 +1,94 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate } from 'react-i18next'
|
||||
import FiltersTable from '../../../../common/FiltersTable/FiltersTable'
|
||||
import { Button } from 'reactstrap'
|
||||
|
||||
export class RefinePanel extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
advancedFilters: PropTypes.object.isRequired,
|
||||
selectedFilters: PropTypes.object.isRequired,
|
||||
clearPending: PropTypes.object.isRequired,
|
||||
filterPages: PropTypes.object.isRequired,
|
||||
onRefine: PropTypes.func.isRequired,
|
||||
actions: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
onHiderClick = (e) => {
|
||||
e.preventDefault()
|
||||
this.props.actions.toggleRefinePanel()
|
||||
};
|
||||
|
||||
onSelectFilter = (groupName, filterValue) => {
|
||||
this.props.actions.selectRefineFilter(groupName, filterValue)
|
||||
};
|
||||
|
||||
onClearFilters = (groupName) => {
|
||||
this.props.actions.clearRefineFilters(groupName)
|
||||
};
|
||||
|
||||
onClearAllFilters = () => {
|
||||
this.props.actions.clearAllRefineFilters()
|
||||
};
|
||||
|
||||
onMoreFilters = (groupName) => {
|
||||
this.props.actions.loadMoreRefineFilters(groupName)
|
||||
};
|
||||
|
||||
onLessFilters = (groupName) => {
|
||||
this.props.actions.loadLessRefineFilters(groupName)
|
||||
};
|
||||
|
||||
/* onPressEnter = (e) => {
|
||||
if (e.keyCode === 13) {
|
||||
const keyword = document.getElementById('refine-keyword').value
|
||||
this.props.actions.selectRefineFilter('keyword', keyword)
|
||||
setTimeout(() => {
|
||||
this.props.onRefine()
|
||||
})
|
||||
}
|
||||
}; */
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className="refine-panel px-4">
|
||||
<Button
|
||||
color="light"
|
||||
title="Hide refine panel"
|
||||
className="d-block ml-auto mb-3 btn-icon"
|
||||
onClick={this.onHiderClick}
|
||||
>
|
||||
{this.props.t('searchTab.hide')}
|
||||
</Button>
|
||||
{/* <Input
|
||||
type="text"
|
||||
className="mb-2"
|
||||
id="refine-keyword"
|
||||
placeholder={this.props.t('common:advancedFilters.keywordRefine')}
|
||||
onKeyUp={this.onPressEnter}
|
||||
/> */}
|
||||
<FiltersTable
|
||||
filters={this.props.advancedFilters}
|
||||
selectedFilters={this.props.selectedFilters}
|
||||
clearPending={this.props.clearPending}
|
||||
pages={this.props.filterPages}
|
||||
callbacks={{
|
||||
'selectFilter': this.onSelectFilter,
|
||||
'clearFilters': this.onClearFilters,
|
||||
'clearAllFilters': this.onClearAllFilters,
|
||||
'moreFilters': this.onMoreFilters,
|
||||
'lessFilters': this.onLessFilters,
|
||||
'refine': this.props.onRefine
|
||||
}}
|
||||
/>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(RefinePanel)
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate } from 'react-i18next'
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
Label,
|
||||
Input,
|
||||
ModalFooter,
|
||||
FormGroup
|
||||
} from 'reactstrap'
|
||||
|
||||
export class SaveFeedPopup extends React.Component {
|
||||
static propTypes = {
|
||||
feedCategories: PropTypes.array.isRequired,
|
||||
saveType: PropTypes.string.isRequired,
|
||||
toggleSaveFeedPopup: PropTypes.func.isRequired,
|
||||
addAlert: PropTypes.func.isRequired,
|
||||
onSaveAsFeed: PropTypes.func.isRequired,
|
||||
getSidebarCategories: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
isFeedNameError: false,
|
||||
feedCategoriesKeys: [],
|
||||
feedName: '',
|
||||
selectCategory: ''
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount = () => {
|
||||
let nestingCount = -1
|
||||
this.getCategoriesKeys(this.props.feedCategories, nestingCount)
|
||||
}
|
||||
|
||||
//function that generates new array of categories without nesting
|
||||
getCategoriesKeys = (categories, nestingCount) => {
|
||||
nestingCount += 1
|
||||
categories.forEach((category) => {
|
||||
if (category.subType === 'deleted_content') return false
|
||||
|
||||
const categoryName = '-'.repeat(nestingCount) + ' ' + category.name
|
||||
|
||||
const feedCategoriesKeys = this.state.feedCategoriesKeys
|
||||
feedCategoriesKeys.push({ id: category.id, name: categoryName })
|
||||
this.setState({
|
||||
feedCategoriesKeys: feedCategoriesKeys,
|
||||
selectCategory: feedCategoriesKeys[0].id.toString()
|
||||
})
|
||||
|
||||
if (category.childes.length) {
|
||||
this.getCategoriesKeys(category.childes, nestingCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
changeHandler = (e) => {
|
||||
const { name, value } = e.target
|
||||
this.setState({ [name]: value })
|
||||
}
|
||||
|
||||
hidePopupFromOutside = (e) => {
|
||||
if (e.target === e.currentTarget) this.hidePopup()
|
||||
}
|
||||
|
||||
hidePopup = () => {
|
||||
this.props.toggleSaveFeedPopup()
|
||||
}
|
||||
|
||||
onSubmit = () => {
|
||||
const { feedName: name, selectCategory: category } = this.state
|
||||
|
||||
if (!name || !name.trim()) {
|
||||
this.setState({ isFeedNameError: true })
|
||||
return false
|
||||
}
|
||||
|
||||
this.props.onSaveAsFeed(name, category)
|
||||
this.hidePopup()
|
||||
}
|
||||
|
||||
render() {
|
||||
const { t } = this.props
|
||||
|
||||
const {
|
||||
feedCategoriesKeys,
|
||||
isFeedNameError,
|
||||
feedName,
|
||||
selectCategory
|
||||
} = this.state
|
||||
|
||||
return (
|
||||
<Modal isOpen toggle={this.hidePopup} backdrop="static" data-tour="feed-save-modal">
|
||||
<ModalHeader toggle={this.hidePopup}>
|
||||
{t('searchTab.saveFeedPopup.' + this.props.saveType)}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{t('searchTab.saveFeedPopup.nameLabel')}<span className="text-danger">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
name="feedName"
|
||||
type="text"
|
||||
value={feedName}
|
||||
onChange={this.changeHandler}
|
||||
/>
|
||||
{isFeedNameError && (
|
||||
<p className="text-danger">
|
||||
{t('searchTab.saveFeedPopup.feedNameErrorMsg')}
|
||||
</p>
|
||||
)}
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{t('searchTab.saveFeedPopup.folderLabel')}<span className="text-danger">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
name="selectCategory"
|
||||
type="select"
|
||||
value={selectCategory}
|
||||
onChange={this.changeHandler}
|
||||
>
|
||||
{feedCategoriesKeys.map((category) => {
|
||||
return (
|
||||
<option key={category.id} value={category.id}>
|
||||
{category.name}
|
||||
</option>
|
||||
)
|
||||
})}
|
||||
</Input>
|
||||
</FormGroup>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="light" onClick={this.hidePopup}>
|
||||
{t('common:commonWords.Cancel')}
|
||||
</Button>
|
||||
<Button color="primary" onClick={this.onSubmit}>
|
||||
{t('searchTab.saveBtn')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent', 'common'], { wait: true })(
|
||||
SaveFeedPopup
|
||||
)
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { DateRangePicker } from 'react-dates'
|
||||
import moment from 'moment'
|
||||
import { getMomentObject } from '../../../../../../common/helper'
|
||||
|
||||
export class BetweenDatepickers extends React.Component {
|
||||
state = {}
|
||||
|
||||
static propTypes = {
|
||||
chosenSearchInterval: PropTypes.string.isRequired,
|
||||
chosenStartDate: PropTypes.string.isRequired,
|
||||
chosenEndDate: PropTypes.string.isRequired,
|
||||
setSearchInterval: PropTypes.func.isRequired,
|
||||
setSearchDate: PropTypes.func.isRequired,
|
||||
setStartDate: PropTypes.func.isRequired,
|
||||
minDate: PropTypes.object,
|
||||
setEndDate: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
swapDate = (startDate, endDate) => {
|
||||
if (startDate.isAfter(endDate)) {
|
||||
const temp = startDate
|
||||
startDate = endDate
|
||||
endDate = temp
|
||||
}
|
||||
return { startDate, endDate }
|
||||
}
|
||||
/*
|
||||
setDates = (date, isStartDate) => {
|
||||
const {
|
||||
chosenStartDate,
|
||||
chosenEndDate,
|
||||
setStartDate,
|
||||
setEndDate,
|
||||
setSearchDate
|
||||
} = this.props
|
||||
|
||||
const hasStartDate = !!chosenStartDate
|
||||
const hasEndDate = !!chosenEndDate
|
||||
let startDate = hasStartDate ? moment(chosenStartDate) : moment()
|
||||
let endDate = hasEndDate ? moment(chosenEndDate) : moment()
|
||||
|
||||
startDate = isStartDate ? date : startDate
|
||||
endDate = !isStartDate ? date : endDate
|
||||
|
||||
const swappedDate = this.swapDate(startDate, endDate)
|
||||
startDate = swappedDate.startDate.format('YYYY-MM-DD')
|
||||
endDate = swappedDate.endDate.format('YYYY-MM-DD')
|
||||
|
||||
setStartDate(startDate.format('YYYY-MM-DD'))
|
||||
setEndDate(endDate.format('YYYY-MM-DD'))
|
||||
|
||||
const endDateLabel = hasEndDate ? endDate : 'now'
|
||||
const startDateLabel = hasStartDate ? startDate : 'until'
|
||||
let label = isStartDate
|
||||
? `${startDate} - ${endDateLabel}`
|
||||
: `${startDateLabel} - ${endDate}`
|
||||
setSearchDate(label)
|
||||
} */
|
||||
|
||||
setBetweenInterval = () => {
|
||||
const { chosenSearchInterval, setSearchInterval } = this.props
|
||||
if (chosenSearchInterval === 'between') return false
|
||||
|
||||
setSearchInterval('between')
|
||||
}
|
||||
|
||||
handleDateChange = ({ startDate, endDate }) => {
|
||||
const { setStartDate, setEndDate } = this.props
|
||||
|
||||
setStartDate(startDate ? startDate.format('YYYY-MM-DD') : null)
|
||||
setEndDate(endDate ? endDate.format('YYYY-MM-DD') : null)
|
||||
|
||||
if (startDate && endDate) {
|
||||
this.setBetweenInterval()
|
||||
}
|
||||
}
|
||||
|
||||
onFocusChange = (focus) => {
|
||||
this.setState({ focusedInput: focus })
|
||||
}
|
||||
|
||||
isOutsideRange = (date) => {
|
||||
const today = moment()
|
||||
return date.isAfter(today) || date.isBefore(this.props.minDate)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { chosenStartDate, chosenEndDate } = this.props
|
||||
const today = moment()
|
||||
const startDate = getMomentObject(chosenStartDate)
|
||||
const endDate = getMomentObject(chosenEndDate)
|
||||
|
||||
return (
|
||||
<div className="ml-3">
|
||||
<DateRangePicker
|
||||
startDateId="startDate"
|
||||
endDateId="endDate"
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onDatesChange={this.handleDateChange}
|
||||
focusedInput={this.state.focusedInput}
|
||||
onFocusChange={this.onFocusChange}
|
||||
displayFormat="MM/DD/YYYY"
|
||||
startDatePlaceholderText="Start Date"
|
||||
endDatePlaceholderText="End Date"
|
||||
numberOfMonths={1}
|
||||
maxDate={today}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
isOutsideRange={this.isOutsideRange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default BetweenDatepickers
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import { Col, CustomInput, FormGroup } from 'reactstrap';
|
||||
|
||||
export class DuplicatesTab extends React.Component {
|
||||
static propTypes = {
|
||||
includeDuplicates: PropTypes.bool.isRequired,
|
||||
toggleIncludeDuplicates: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<Col sm={12}>
|
||||
<FormGroup>
|
||||
<CustomInput
|
||||
className="checkbox-input-hidden"
|
||||
type="checkbox"
|
||||
id="duplicates-check"
|
||||
checked={this.props.includeDuplicates}
|
||||
onChange={this.props.toggleIncludeDuplicates}
|
||||
label={t('searchTab.searchBySection.duplicates.includeDuplicates')}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(DuplicatesTab);
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import { Col, FormGroup, Input, Label } from 'reactstrap';
|
||||
|
||||
export class EmphasisTab extends React.Component {
|
||||
static propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
include: PropTypes.string.isRequired,
|
||||
exclude: PropTypes.string.isRequired,
|
||||
setHeadlineIncluded: PropTypes.func.isRequired,
|
||||
setHeadlineExcluded: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
setHeadInclude = (e) => {
|
||||
const headline = e.target.value;
|
||||
this.props.setHeadlineIncluded(headline);
|
||||
};
|
||||
|
||||
setHeadExclude = (e) => {
|
||||
const headline = e.target.value;
|
||||
this.props.setHeadlineExcluded(headline);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, include, exclude } = this.props;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Col sm="6">
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{t('searchTab.searchBySection.emphasis.headlineLabel')}{' '}
|
||||
{t('searchTab.searchBySection.emphasis.include')}
|
||||
</Label>
|
||||
<Input type="text" value={include} onChange={this.setHeadInclude} />
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col sm="6">
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{t('searchTab.searchBySection.emphasis.headlineLabel')}{' '}
|
||||
{t('searchTab.searchBySection.emphasis.exclude')}
|
||||
</Label>
|
||||
<Input type="text" value={exclude} onChange={this.setHeadExclude} />
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(EmphasisTab);
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import { Col, CustomInput, FormGroup } from 'reactstrap';
|
||||
|
||||
function ExtrasTab({ t, hasImages, toggleHasImages }) {
|
||||
return (
|
||||
<Col sm={12}>
|
||||
<FormGroup>
|
||||
<CustomInput
|
||||
id="has-images-check"
|
||||
type="checkbox"
|
||||
className="d-flex"
|
||||
checked={hasImages}
|
||||
label={t('searchTab.searchBySection.extras.hasImages')}
|
||||
onChange={toggleHasImages}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
ExtrasTab.propTypes = {
|
||||
hasImages: PropTypes.bool.isRequired,
|
||||
toggleHasImages: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(ExtrasTab);
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import { Col, CustomInput } from 'reactstrap';
|
||||
|
||||
export class LangsTab extends React.Component {
|
||||
static propTypes = {
|
||||
chosenLanguages: PropTypes.array.isRequired,
|
||||
searchLanguages: PropTypes.array.isRequired,
|
||||
toggleLang: PropTypes.func.isRequired,
|
||||
toggleAllLangs: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
toggleLangs = ({ target: { id, checked } }) => {
|
||||
this.props.toggleLang(id, checked);
|
||||
};
|
||||
|
||||
toggleAllLangs = (e) => {
|
||||
this.props.toggleAllLangs(e.target.checked);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
const { searchLanguages, chosenLanguages } = this.props;
|
||||
return (
|
||||
<Col sm={12} className="search-by-lang">
|
||||
<CustomInput
|
||||
id="article-check-all"
|
||||
type="checkbox"
|
||||
label={t('common:language.all')}
|
||||
checked={searchLanguages.length === chosenLanguages.length}
|
||||
onChange={this.toggleAllLangs}
|
||||
/>
|
||||
|
||||
{searchLanguages.map((lang) => (
|
||||
<CustomInput
|
||||
key={lang}
|
||||
id={lang}
|
||||
type="checkbox"
|
||||
checked={chosenLanguages.indexOf(lang) !== -1}
|
||||
label={t('common:language.' + lang)}
|
||||
onChange={this.toggleLangs}
|
||||
/>
|
||||
))}
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(LangsTab);
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { DragSource } from 'react-dnd'
|
||||
|
||||
const Types = {
|
||||
LOC: 'location'
|
||||
}
|
||||
|
||||
const locationSource = {
|
||||
beginDrag (props) {
|
||||
// Return the data describing the dragged item
|
||||
return { oldDropTargetType: props.dropTargetType }
|
||||
},
|
||||
|
||||
endDrag (props, monitor, component) {
|
||||
// When dropped on a compatible target, do something
|
||||
if (monitor.getDropResult() !== null) {
|
||||
const locFrom = props.dropTargetType
|
||||
const locTo = monitor.getDropResult().newDropTargetType
|
||||
|
||||
const locationType = props.locationType
|
||||
const location = props.location
|
||||
|
||||
props.moveLocation(locFrom, locTo, locationType, location)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies which props to inject into your component.
|
||||
*/
|
||||
function collectDragSource (connect) {
|
||||
return {
|
||||
// Call this function inside render()
|
||||
// to let React DnD handle the drag events:
|
||||
connectDragSource: connect.dragSource()
|
||||
}
|
||||
}
|
||||
|
||||
export class LocationsTabList extends React.Component {
|
||||
static propTypes = {
|
||||
location: PropTypes.object.isRequired,
|
||||
dropTargetType: PropTypes.string.isRequired,
|
||||
moveLocation: PropTypes.func.isRequired,
|
||||
connectDragSource: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
render () {
|
||||
const { connectDragSource } = this.props
|
||||
const { location } = this.props
|
||||
|
||||
return connectDragSource(
|
||||
<li className="list-group-item cursor-move p-2">
|
||||
<span className="drag-handle" />
|
||||
{location.name}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default DragSource(Types.LOC, locationSource, collectDragSource)(LocationsTabList)
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import LocationsTabList from './LocationsTabList';
|
||||
import { Button, Col, Row } from 'reactstrap';
|
||||
|
||||
export class LocationsTab extends React.Component {
|
||||
static propTypes = {
|
||||
locations: PropTypes.array.isRequired,
|
||||
locationsToInclude: PropTypes.array.isRequired,
|
||||
locationsToExclude: PropTypes.array.isRequired,
|
||||
chosenLocationsType: PropTypes.string.isRequired,
|
||||
changeLocationsType: PropTypes.func.isRequired,
|
||||
moveLocation: PropTypes.func.isRequired,
|
||||
clearLocations: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
dropdownOpen: false,
|
||||
dropDownValue: 'country'
|
||||
};
|
||||
}
|
||||
|
||||
onClearLocations = () => {
|
||||
this.props.clearLocations();
|
||||
this.props.changeLocationsType('country');
|
||||
this.setState({ dropDownValue: 'country' });
|
||||
};
|
||||
|
||||
selectLocation = (value) => {
|
||||
this.props.changeLocationsType(value);
|
||||
this.setState({ dropDownValue: value });
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
locations,
|
||||
chosenLocationsType,
|
||||
locationsToInclude,
|
||||
locationsToExclude
|
||||
} = this.props;
|
||||
const { t } = this.props;
|
||||
const locationsMainList = locations.filter((loc) => {
|
||||
return loc.type === chosenLocationsType;
|
||||
});
|
||||
const includeList = locationsToInclude.filter((loc) => {
|
||||
return loc.type === chosenLocationsType;
|
||||
});
|
||||
const excludeList = locationsToExclude.filter((loc) => {
|
||||
return loc.type === chosenLocationsType;
|
||||
});
|
||||
|
||||
const { dropDownValue } = this.state;
|
||||
return (
|
||||
<Col sm={12}>
|
||||
<Button
|
||||
outline
|
||||
active={dropDownValue === 'country'}
|
||||
color="secondary"
|
||||
className="mr-2 mb-3"
|
||||
onClick={() => this.selectLocation('country')}
|
||||
>
|
||||
{t('searchTab.searchBySection.locations.countriesSelect')}
|
||||
</Button>
|
||||
<Button
|
||||
outline
|
||||
active={dropDownValue === 'state'}
|
||||
color="secondary"
|
||||
className="mb-3"
|
||||
onClick={() => this.selectLocation('state')}
|
||||
>
|
||||
{t('searchTab.searchBySection.locations.statesSelect')}
|
||||
</Button>
|
||||
|
||||
<Row className="draggable">
|
||||
<Col md={4}>
|
||||
<LocationsTabList
|
||||
locations={locationsMainList}
|
||||
chosenLocationsType={chosenLocationsType}
|
||||
dropTargetType="locations"
|
||||
moveLocation={this.props.moveLocation}
|
||||
/>
|
||||
</Col>
|
||||
<Col md={4}>
|
||||
<LocationsTabList
|
||||
locations={includeList}
|
||||
chosenLocationsType={chosenLocationsType}
|
||||
dropTargetType="locationsToInclude"
|
||||
moveLocation={this.props.moveLocation}
|
||||
/>
|
||||
</Col>
|
||||
<Col md={4}>
|
||||
<LocationsTabList
|
||||
locations={excludeList}
|
||||
chosenLocationsType={chosenLocationsType}
|
||||
dropTargetType="locationsToExclude"
|
||||
moveLocation={this.props.moveLocation}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(LocationsTab);
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import { DropTarget } from 'react-dnd';
|
||||
import flow from 'lodash/flow';
|
||||
import LocationItem from './LocationItem';
|
||||
import {
|
||||
ListGroup
|
||||
} from 'reactstrap';
|
||||
|
||||
const targetTypes = ['location'];
|
||||
const locationListTarget = {
|
||||
drop(props, monitor, component) {
|
||||
if (monitor.didDrop()) {
|
||||
//check whether some nested
|
||||
// target already handled drop
|
||||
return;
|
||||
}
|
||||
|
||||
return { newDropTargetType: props.dropTargetType };
|
||||
},
|
||||
|
||||
canDrop(props, monitor) {
|
||||
return props.dropTargetType !== monitor.getItem().oldDropTargetType;
|
||||
}
|
||||
};
|
||||
|
||||
function collectDropTarget(connect, monitor) {
|
||||
return {
|
||||
// Call this function inside render()
|
||||
// to let React DnD handle the drag events:
|
||||
connectDropTarget: connect.dropTarget(),
|
||||
// You can ask the monitor about the current drag state:
|
||||
itemType: monitor.getItemType()
|
||||
};
|
||||
}
|
||||
|
||||
export class LocationsTabList extends React.Component {
|
||||
static propTypes = {
|
||||
locations: PropTypes.array.isRequired,
|
||||
chosenLocationsType: PropTypes.string.isRequired,
|
||||
dropTargetType: PropTypes.string.isRequired,
|
||||
moveLocation: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
connectDropTarget: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
const { locations, chosenLocationsType, dropTargetType } = this.props;
|
||||
const { t } = this.props;
|
||||
const { connectDropTarget } = this.props;
|
||||
|
||||
locations.forEach((location) => {
|
||||
location.name = t('common:' + location.type + '.' + location.code);
|
||||
});
|
||||
|
||||
const sortedLocations = locations.sort((a, b) => {
|
||||
const nameA = a.name.toLowerCase();
|
||||
const nameB = b.name.toLowerCase();
|
||||
if (nameA < nameB) {
|
||||
//sort string ascending
|
||||
return -1;
|
||||
}
|
||||
if (nameA > nameB) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return connectDropTarget(
|
||||
<div className="scroll-area-md border b-radius-5">
|
||||
<p className="text-muted border-bottom p-2">{t('searchTab.searchBySection.locations.' + dropTargetType)}</p>
|
||||
<ListGroup className="p-2">
|
||||
{sortedLocations.map((location, i) => {
|
||||
return (
|
||||
<LocationItem
|
||||
key={'location-' + i}
|
||||
location={location}
|
||||
dropTargetType={dropTargetType}
|
||||
locationType={chosenLocationsType}
|
||||
moveLocation={this.props.moveLocation}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ListGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default flow(
|
||||
DropTarget(targetTypes, locationListTarget, collectDropTarget),
|
||||
translate(['tabsContent'], { wait: true })
|
||||
)(LocationsTabList);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user