at the end of the day, it was inevitable
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"presets": ["es2015", "react", "stage-0"],
|
||||
"plugins": ["transform-runtime", "add-module-exports"]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
# http://editorconfig.org
|
||||
|
||||
# A special property that should be specified at the top of the file outside of
|
||||
# any sections. Set to true to stop .editor config file search on current file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
# Indentation style
|
||||
# Possible values - tab, space
|
||||
indent_style = space
|
||||
|
||||
# Indentation size in single-spaced characters
|
||||
# Possible values - an integer, tab
|
||||
indent_size = 2
|
||||
|
||||
# Line ending file format
|
||||
# Possible values - lf, crlf, cr
|
||||
end_of_line = lf
|
||||
|
||||
# File character encoding
|
||||
# Possible values - latin1, utf-8, utf-16be, utf-16le
|
||||
charset = utf-8
|
||||
|
||||
# Denotes whether to trim whitespace at the end of lines
|
||||
# Possible values - true, false
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# Denotes whether file should end with a newline
|
||||
# Possible values - true, false
|
||||
insert_final_newline = true
|
||||
@@ -0,0 +1,3 @@
|
||||
node_modules/**
|
||||
app/index.html
|
||||
config/**
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"parser": "babel-eslint",
|
||||
"extends": ["standard", "standard-react"],
|
||||
"env": {
|
||||
"browser": true
|
||||
},
|
||||
"globals": {
|
||||
"__DEV__": false,
|
||||
"__PROD__": false,
|
||||
"__PLAYER_DEBUG__": false,
|
||||
"__BASENAME__": false
|
||||
},
|
||||
"rules": {
|
||||
"semi": 0,
|
||||
"spaced-comment": 0,
|
||||
"brace-style": 0,
|
||||
"no-trailing-spaces": 0,
|
||||
"padded-blocks": 0,
|
||||
"quotes": [2, "single"],
|
||||
"space-before-function-paren": "off" // ignore
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
app
|
||||
config
|
||||
server
|
||||
webpack
|
||||
.babelrc
|
||||
.editorconfig
|
||||
.eslintignore
|
||||
.eslintrc
|
||||
.gitignore
|
||||
.npmignore
|
||||
@@ -0,0 +1,11 @@
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
/dist
|
||||
/environments
|
||||
/config
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"endOfLine": "lf",
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "none",
|
||||
"bracketSpacing": true
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
# SOCIALHOSE - Front-end
|
||||
|
||||
This folder is the front-end codebase for SOCIALHOSE.
|
||||
|
||||
## Requirements
|
||||
|
||||
- node `>=5.0.0`
|
||||
- npm `^3.0.0`
|
||||
|
||||
## Overview
|
||||
|
||||
The project is using React 16.13.1 which supports hooks but due to early versions of ESLint and Webpack, it may not support few things.
|
||||
|
||||
The frontend design follows the [ArchitectUI](https://dashboardpack.com/theme-details/architectui-dashboard-react-pro). Here are the [downloadable ZIP files](<(https://github.com/melzubeir/socialhose/issues/59#issuecomment-702164269)>) which also contain the design for RTL language.
|
||||
|
||||
There is also Admin portal to manage users but it is not the part of this folder or React.
|
||||
|
||||
## Getting Started
|
||||
|
||||
This short guide will help you get started with setting this project up on your development machine.
|
||||
|
||||
| Command | Description |
|
||||
| --------------- | --------------------------------------------------- |
|
||||
| `npm start` | Start development server on `http://localhost:5085` |
|
||||
| `npm run build` | Create a build for the production at `/web/dist` |
|
||||
|
||||
If we want to make a build locally for the first time, then follow the instructions given in `README.md` located at project's root directory (under Docker heading) which will generate a build in `/web/dist` and it will be served on `http://localhost:8081/`.
|
||||
|
||||
Whereas in development, one has to follow the above steps for very first time and then use `npm run start` to start development server on `http://localhost:5085`.
|
||||
|
||||
---
|
||||
|
||||
**Note:**
|
||||
Resolve or disable ESLint errors before creating the build. If there are any remaining ESlint errors are there then it will fail to generate the build. (if there are any errors/warnings, then `npm start` will show while server is running)
|
||||
|
||||
---
|
||||
@@ -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
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user