mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-11 02:37:22 +03:00
Created VisitsTable
This commit is contained in:
parent
c8ba6764c2
commit
e6034dfb14
11 changed files with 325 additions and 70 deletions
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { Pagination, PaginationItem, PaginationLink } from 'reactstrap';
|
||||
import { isPageDisabled, keyForPage, progressivePagination } from '../utils/helpers/pagination';
|
||||
import './SimplePaginator.scss';
|
||||
|
@ -8,9 +9,10 @@ const propTypes = {
|
|||
pagesCount: PropTypes.number.isRequired,
|
||||
currentPage: PropTypes.number.isRequired,
|
||||
setCurrentPage: PropTypes.func.isRequired,
|
||||
centered: PropTypes.bool,
|
||||
};
|
||||
|
||||
const SimplePaginator = ({ pagesCount, currentPage, setCurrentPage }) => {
|
||||
const SimplePaginator = ({ pagesCount, currentPage, setCurrentPage, centered = true }) => {
|
||||
if (pagesCount < 2) {
|
||||
return null;
|
||||
}
|
||||
|
@ -18,7 +20,7 @@ const SimplePaginator = ({ pagesCount, currentPage, setCurrentPage }) => {
|
|||
const onClick = (page) => () => setCurrentPage(page);
|
||||
|
||||
return (
|
||||
<Pagination listClassName="flex-wrap justify-content-center mb-0 simple-paginator">
|
||||
<Pagination listClassName={classNames('flex-wrap mb-0 simple-paginator', { 'justify-content-center': centered })}>
|
||||
<PaginationItem disabled={currentPage <= 1}>
|
||||
<PaginationLink previous tag="span" onClick={onClick(currentPage - 1)} />
|
||||
</PaginationItem>
|
||||
|
|
|
@ -36,12 +36,16 @@ const SearchBar = (colorGenerator, ForServerVersion) => {
|
|||
|
||||
<ForServerVersion minVersion="1.21.0">
|
||||
<div className="mt-3">
|
||||
<DateRangeRow
|
||||
startDate={dateOrUndefined(shortUrlsListParams.startDate)}
|
||||
endDate={dateOrUndefined(shortUrlsListParams.endDate)}
|
||||
onStartDateChange={setDate('startDate')}
|
||||
onEndDateChange={setDate('endDate')}
|
||||
/>
|
||||
<div className="row">
|
||||
<div className="col-lg-8 offset-lg-4 col-xl-6 offset-xl-6">
|
||||
<DateRangeRow
|
||||
startDate={dateOrUndefined(shortUrlsListParams.startDate)}
|
||||
endDate={dateOrUndefined(shortUrlsListParams.endDate)}
|
||||
onStartDateChange={setDate('startDate')}
|
||||
onEndDateChange={setDate('endDate')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ForServerVersion>
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ const propTypes = {
|
|||
|
||||
const DateRangeRow = ({ startDate, endDate, onStartDateChange, onEndDateChange }) => (
|
||||
<div className="row">
|
||||
<div className="col-xl-3 col-lg-4 col-md-6 offset-xl-6 offset-lg-4">
|
||||
<div className="col-md-6">
|
||||
<DateInput
|
||||
selected={startDate}
|
||||
placeholderText="Since"
|
||||
|
@ -22,7 +22,7 @@ const DateRangeRow = ({ startDate, endDate, onStartDateChange, onEndDateChange }
|
|||
onChange={onStartDateChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-xl-3 col-lg-4 col-md-6">
|
||||
<div className="col-md-6">
|
||||
<DateInput
|
||||
className="date-range-row__date-input"
|
||||
selected={endDate}
|
||||
|
|
|
@ -12,10 +12,14 @@ export default class SearchField extends React.Component {
|
|||
onChange: PropTypes.func.isRequired,
|
||||
className: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
large: PropTypes.bool,
|
||||
noBorder: PropTypes.bool,
|
||||
};
|
||||
static defaultProps = {
|
||||
className: '',
|
||||
placeholder: 'Search...',
|
||||
large: true,
|
||||
noBorder: false,
|
||||
};
|
||||
|
||||
state = { showClearBtn: false, searchTerm: '' };
|
||||
|
@ -41,13 +45,16 @@ export default class SearchField extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { className, placeholder } = this.props;
|
||||
const { className, placeholder, large, noBorder } = this.props;
|
||||
|
||||
return (
|
||||
<div className={classNames('search-field', className)}>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-lg search-field__input"
|
||||
className={classNames('form-control search-field__input', {
|
||||
'form-control-lg': large,
|
||||
'search-field__input--no-border': noBorder,
|
||||
})}
|
||||
placeholder={placeholder}
|
||||
value={this.state.searchTerm}
|
||||
onChange={(e) => this.searchTermChanged(e.target.value)}
|
||||
|
|
|
@ -9,6 +9,11 @@
|
|||
padding-right: 40px;
|
||||
}
|
||||
|
||||
.search-field__input--no-border.search-field__input--no-border {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.search-field__icon {
|
||||
@include vertical-align();
|
||||
|
||||
|
|
59
src/utils/helpers/visits.js
Normal file
59
src/utils/helpers/visits.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
import { hasValue } from '../utils';
|
||||
|
||||
const DEFAULT = 'Others';
|
||||
|
||||
export const osFromUserAgent = (userAgent) => {
|
||||
if (!hasValue(userAgent)) {
|
||||
return DEFAULT;
|
||||
}
|
||||
|
||||
const lowerUserAgent = userAgent.toLowerCase();
|
||||
|
||||
switch (true) {
|
||||
case lowerUserAgent.includes('linux'):
|
||||
return 'Linux';
|
||||
case lowerUserAgent.includes('windows'):
|
||||
return 'Windows';
|
||||
case lowerUserAgent.includes('mac'):
|
||||
return 'MacOS';
|
||||
case lowerUserAgent.includes('mobi'):
|
||||
return 'Mobile';
|
||||
default:
|
||||
return DEFAULT;
|
||||
}
|
||||
};
|
||||
|
||||
export const browserFromUserAgent = (userAgent) => {
|
||||
if (!hasValue(userAgent)) {
|
||||
return DEFAULT;
|
||||
}
|
||||
|
||||
const lowerUserAgent = userAgent.toLowerCase();
|
||||
|
||||
switch (true) {
|
||||
case lowerUserAgent.includes('opera') || lowerUserAgent.includes('opr'):
|
||||
return 'Opera';
|
||||
case lowerUserAgent.includes('firefox'):
|
||||
return 'Firefox';
|
||||
case lowerUserAgent.includes('chrome'):
|
||||
return 'Chrome';
|
||||
case lowerUserAgent.includes('safari'):
|
||||
return 'Safari';
|
||||
case lowerUserAgent.includes('edg'):
|
||||
return 'Microsoft Edge';
|
||||
case lowerUserAgent.includes('msie'):
|
||||
return 'Internet Explorer';
|
||||
default:
|
||||
return DEFAULT;
|
||||
}
|
||||
};
|
||||
|
||||
export const extractDomain = (url) => {
|
||||
if (!hasValue(url)) {
|
||||
return 'Direct';
|
||||
}
|
||||
|
||||
const domain = url.includes('://') ? url.split('/')[2] : url.split('/')[0];
|
||||
|
||||
return domain.split(':')[0];
|
||||
};
|
|
@ -1,8 +1,10 @@
|
|||
import { isEmpty, mapObjIndexed, values } from 'ramda';
|
||||
import React from 'react';
|
||||
import { Card } from 'reactstrap';
|
||||
import { Button, Card, Collapse } from 'reactstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
import qs from 'qs';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faChevronDown as chevronDown } from '@fortawesome/free-solid-svg-icons';
|
||||
import DateRangeRow from '../utils/DateRangeRow';
|
||||
import Message from '../utils/Message';
|
||||
import { formatDate } from '../utils/helpers/date';
|
||||
|
@ -11,6 +13,7 @@ import { shortUrlVisitsType } from './reducers/shortUrlVisits';
|
|||
import VisitsHeader from './VisitsHeader';
|
||||
import GraphCard from './GraphCard';
|
||||
import { shortUrlDetailType } from './reducers/shortUrlDetail';
|
||||
import VisitsTable from './VisitsTable';
|
||||
|
||||
const ShortUrlVisits = (
|
||||
{ processStatsFromVisits },
|
||||
|
@ -30,7 +33,12 @@ const ShortUrlVisits = (
|
|||
cancelGetShortUrlVisits: PropTypes.func,
|
||||
};
|
||||
|
||||
state = { startDate: undefined, endDate: undefined };
|
||||
state = {
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
showTable: false,
|
||||
};
|
||||
|
||||
loadVisits = (loadDetail = false) => {
|
||||
const { match: { params }, location: { search }, getShortUrlVisits, getShortUrlDetail } = this.props;
|
||||
const { shortCode } = params;
|
||||
|
@ -57,10 +65,9 @@ const ShortUrlVisits = (
|
|||
|
||||
render() {
|
||||
const { shortUrlVisits, shortUrlDetail } = this.props;
|
||||
const { visits, loading, loadingLarge, error } = shortUrlVisits;
|
||||
|
||||
const renderVisitsContent = () => {
|
||||
const { visits, loading, loadingLarge, error } = shortUrlVisits;
|
||||
|
||||
if (loading) {
|
||||
const message = loadingLarge ? 'This is going to take a while... :S' : 'Loading...';
|
||||
|
||||
|
@ -137,14 +144,31 @@ const ShortUrlVisits = (
|
|||
<VisitsHeader shortUrlDetail={shortUrlDetail} shortUrlVisits={shortUrlVisits} />
|
||||
|
||||
<section className="mt-4">
|
||||
<DateRangeRow
|
||||
startDate={this.state.startDate}
|
||||
endDate={this.state.endDate}
|
||||
onStartDateChange={setDate('startDate')}
|
||||
onEndDateChange={setDate('endDate')}
|
||||
/>
|
||||
<div className="row flex-md-row-reverse">
|
||||
<div className="col-lg-8 col-xl-6">
|
||||
<DateRangeRow
|
||||
startDate={this.state.startDate}
|
||||
endDate={this.state.endDate}
|
||||
onStartDateChange={setDate('startDate')}
|
||||
onEndDateChange={setDate('endDate')}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-lg-4 col-xl-6 mt-4 mt-lg-0">
|
||||
{visits.length > 0 && (
|
||||
<Button outline onClick={() => this.setState(({ showTable }) => ({ showTable: !showTable }))}>
|
||||
Show table <FontAwesomeIcon icon={chevronDown} rotation={this.state.showTable ? 180 : undefined} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{!loading && visits.length > 0 && (
|
||||
<Collapse isOpen={this.state.showTable}>
|
||||
<VisitsTable visits={visits} />
|
||||
</Collapse>
|
||||
)}
|
||||
|
||||
<section>
|
||||
{renderVisitsContent()}
|
||||
</section>
|
||||
|
|
176
src/visits/VisitsTable.js
Normal file
176
src/visits/VisitsTable.js
Normal file
|
@ -0,0 +1,176 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Moment from 'react-moment';
|
||||
import classNames from 'classnames';
|
||||
import { map } from 'ramda';
|
||||
import {
|
||||
faCaretDown as caretDownIcon,
|
||||
faCaretUp as caretUpIcon,
|
||||
faCheck as checkIcon,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import SimplePaginator from '../common/SimplePaginator';
|
||||
import SearchField from '../utils/SearchField';
|
||||
import { browserFromUserAgent, extractDomain, osFromUserAgent } from '../utils/helpers/visits';
|
||||
import { determineOrderDir } from '../utils/utils';
|
||||
import { visitType } from './reducers/shortUrlVisits';
|
||||
import './VisitsTable.scss';
|
||||
|
||||
const propTypes = {
|
||||
visits: PropTypes.arrayOf(visitType).isRequired,
|
||||
onVisitSelected: PropTypes.func,
|
||||
};
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
const visitMatchesSearch = ({ browser, os, referer, location }, searchTerm) =>
|
||||
`${browser} ${os} ${referer} ${location}`.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const calculateVisits = (allVisits, page, searchTerm, { field, dir }) => {
|
||||
const end = page * PAGE_SIZE;
|
||||
const start = end - PAGE_SIZE;
|
||||
const filteredVisits = searchTerm ? allVisits.filter((visit) => visitMatchesSearch(visit, searchTerm)) : allVisits;
|
||||
const total = filteredVisits.length;
|
||||
const visits = filteredVisits
|
||||
.sort((a, b) => {
|
||||
if (!dir) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const greaterThan = dir === 'ASC' ? 1 : -1;
|
||||
const smallerThan = dir === 'ASC' ? -1 : 1;
|
||||
|
||||
return a[field] > b[field] ? greaterThan : smallerThan;
|
||||
})
|
||||
.slice(start, end);
|
||||
|
||||
return { visits, start, end, total };
|
||||
};
|
||||
const normalizeVisits = map(({ userAgent, date, referer, visitLocation }) => ({
|
||||
date,
|
||||
browser: browserFromUserAgent(userAgent),
|
||||
os: osFromUserAgent(userAgent),
|
||||
referer: extractDomain(referer),
|
||||
location: visitLocation ? `${visitLocation.countryName} - ${visitLocation.cityName}` : '',
|
||||
}));
|
||||
|
||||
const VisitsTable = ({ visits, onVisitSelected }) => {
|
||||
const allVisits = normalizeVisits(visits);
|
||||
|
||||
const [ selectedVisit, setSelectedVisit ] = useState(undefined);
|
||||
const [ page, setPage ] = useState(1);
|
||||
const [ searchTerm, setSearchTerm ] = useState(undefined);
|
||||
const [ order, setOrder ] = useState({ field: undefined, dir: undefined });
|
||||
const [ currentPage, setCurrentPageVisits ] = useState(calculateVisits(allVisits, page, searchTerm, order));
|
||||
|
||||
const orderByColumn = (field) => () => setOrder({ field, dir: determineOrderDir(field, order.field, order.dir) });
|
||||
const renderOrderIcon = (field) => {
|
||||
if (!order.dir || order.field !== field) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<FontAwesomeIcon
|
||||
icon={order.dir === 'ASC' ? caretUpIcon : caretDownIcon}
|
||||
className="visits-table__header-icon"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
onVisitSelected && onVisitSelected(selectedVisit);
|
||||
}, [ selectedVisit ]);
|
||||
useEffect(() => {
|
||||
setCurrentPageVisits(calculateVisits(allVisits, page, searchTerm, order));
|
||||
}, [ page, searchTerm, order ]);
|
||||
|
||||
return (
|
||||
<table className="table table-striped table-bordered table-hover table-sm table-responsive-sm mt-4 mb-0">
|
||||
<thead className="short-urls-list__header">
|
||||
<tr>
|
||||
<th className="text-center">
|
||||
<FontAwesomeIcon icon={checkIcon} />
|
||||
</th>
|
||||
<th className="short-urls-list__header-cell--with-action" onClick={orderByColumn('date')}>
|
||||
{renderOrderIcon('date')}
|
||||
Date
|
||||
</th>
|
||||
<th className="short-urls-list__header-cell--with-action" onClick={orderByColumn('location')}>
|
||||
{renderOrderIcon('location')}
|
||||
Location
|
||||
</th>
|
||||
<th className="short-urls-list__header-cell--with-action" onClick={orderByColumn('browser')}>
|
||||
{renderOrderIcon('browser')}
|
||||
Browser
|
||||
</th>
|
||||
<th className="short-urls-list__header-cell--with-action" onClick={orderByColumn('os')}>
|
||||
{renderOrderIcon('os')}
|
||||
OS
|
||||
</th>
|
||||
<th className="short-urls-list__header-cell--with-action" onClick={orderByColumn('referer')}>
|
||||
{renderOrderIcon('referer')}
|
||||
Referrer
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan={6} className="p-0">
|
||||
<SearchField noBorder large={false} onChange={setSearchTerm} />
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{currentPage.visits.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={6} className="text-center">
|
||||
No visits found with current filtering
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{currentPage.visits.map((visit, index) => (
|
||||
<tr
|
||||
key={index}
|
||||
style={{ cursor: 'pointer' }}
|
||||
className={classNames({ 'table-primary': selectedVisit === visit })}
|
||||
onClick={() => setSelectedVisit(selectedVisit === visit ? undefined : visit)}
|
||||
>
|
||||
<td className="text-center">
|
||||
{selectedVisit === visit && <FontAwesomeIcon icon={checkIcon} className="text-primary" />}
|
||||
</td>
|
||||
<td>
|
||||
<Moment format="YYYY-MM-DD HH:mm">{visit.date}</Moment>
|
||||
</td>
|
||||
<td>{visit.location}</td>
|
||||
<td>{visit.browser}</td>
|
||||
<td>{visit.os}</td>
|
||||
<td>{visit.referer}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
{currentPage.total >= PAGE_SIZE && (
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colSpan={6} className="p-2">
|
||||
<div className="row">
|
||||
<div className="col-6">
|
||||
<SimplePaginator
|
||||
pagesCount={Math.ceil(currentPage.total / PAGE_SIZE)}
|
||||
currentPage={page}
|
||||
setCurrentPage={setPage}
|
||||
centered={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-6 d-flex align-items-center flex-row-reverse">
|
||||
<div>
|
||||
Visits <b>{currentPage.start + 1}</b> to <b>{currentPage.end}</b> of <b>{currentPage.total}</b>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
)}
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
VisitsTable.propTypes = propTypes;
|
||||
|
||||
export default VisitsTable;
|
4
src/visits/VisitsTable.scss
Normal file
4
src/visits/VisitsTable.scss
Normal file
|
@ -0,0 +1,4 @@
|
|||
.visits-table__header-icon {
|
||||
float: right;
|
||||
margin-top: 3px;
|
||||
}
|
|
@ -10,8 +10,24 @@ export const GET_SHORT_URL_VISITS_LARGE = 'shlink/shortUrlVisits/GET_SHORT_URL_V
|
|||
export const GET_SHORT_URL_VISITS_CANCEL = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_CANCEL';
|
||||
/* eslint-enable padding-line-between-statements */
|
||||
|
||||
export const visitType = PropTypes.shape({
|
||||
referer: PropTypes.string,
|
||||
date: PropTypes.string,
|
||||
userAgent: PropTypes.string,
|
||||
visitLocations: PropTypes.shape({
|
||||
countryCode: PropTypes.string,
|
||||
countryName: PropTypes.string,
|
||||
regionName: PropTypes.string,
|
||||
cityName: PropTypes.string,
|
||||
latitude: PropTypes.number,
|
||||
longitude: PropTypes.number,
|
||||
timezone: PropTypes.string,
|
||||
isEmpty: PropTypes.bool,
|
||||
}),
|
||||
});
|
||||
|
||||
export const shortUrlVisitsType = PropTypes.shape({
|
||||
visits: PropTypes.array,
|
||||
visits: PropTypes.arrayOf(visitType),
|
||||
loading: PropTypes.bool,
|
||||
error: PropTypes.bool,
|
||||
});
|
||||
|
|
|
@ -1,46 +1,5 @@
|
|||
import { isNil, isEmpty, memoizeWith, prop } from 'ramda';
|
||||
|
||||
const osFromUserAgent = (userAgent) => {
|
||||
const lowerUserAgent = userAgent.toLowerCase();
|
||||
|
||||
switch (true) {
|
||||
case lowerUserAgent.indexOf('linux') >= 0:
|
||||
return 'Linux';
|
||||
case lowerUserAgent.indexOf('windows') >= 0:
|
||||
return 'Windows';
|
||||
case lowerUserAgent.indexOf('mac') >= 0:
|
||||
return 'MacOS';
|
||||
case lowerUserAgent.indexOf('mobi') >= 0:
|
||||
return 'Mobile';
|
||||
default:
|
||||
return 'Others';
|
||||
}
|
||||
};
|
||||
|
||||
const browserFromUserAgent = (userAgent) => {
|
||||
const lowerUserAgent = userAgent.toLowerCase();
|
||||
|
||||
switch (true) {
|
||||
case lowerUserAgent.indexOf('opera') >= 0 || lowerUserAgent.indexOf('opr') >= 0:
|
||||
return 'Opera';
|
||||
case lowerUserAgent.indexOf('firefox') >= 0:
|
||||
return 'Firefox';
|
||||
case lowerUserAgent.indexOf('chrome') >= 0:
|
||||
return 'Chrome';
|
||||
case lowerUserAgent.indexOf('safari') >= 0:
|
||||
return 'Safari';
|
||||
case lowerUserAgent.indexOf('msie') >= 0:
|
||||
return 'Internet Explorer';
|
||||
default:
|
||||
return 'Others';
|
||||
}
|
||||
};
|
||||
|
||||
const extractDomain = (url) => {
|
||||
const domain = url.indexOf('://') > -1 ? url.split('/')[2] : url.split('/')[0];
|
||||
|
||||
return domain.split(':')[0];
|
||||
};
|
||||
import { isEmpty, isNil, memoizeWith, prop } from 'ramda';
|
||||
import { browserFromUserAgent, extractDomain, osFromUserAgent } from '../../utils/helpers/visits';
|
||||
|
||||
const visitLocationHasProperty = (visitLocation, propertyName) =>
|
||||
!isNil(visitLocation)
|
||||
|
@ -48,20 +7,19 @@ const visitLocationHasProperty = (visitLocation, propertyName) =>
|
|||
&& !isEmpty(visitLocation[propertyName]);
|
||||
|
||||
const updateOsStatsForVisit = (osStats, { userAgent }) => {
|
||||
const os = isNil(userAgent) ? 'Others' : osFromUserAgent(userAgent);
|
||||
const os = osFromUserAgent(userAgent);
|
||||
|
||||
osStats[os] = (osStats[os] || 0) + 1;
|
||||
};
|
||||
|
||||
const updateBrowsersStatsForVisit = (browsersStats, { userAgent }) => {
|
||||
const browser = isNil(userAgent) ? 'Others' : browserFromUserAgent(userAgent);
|
||||
const browser = browserFromUserAgent(userAgent);
|
||||
|
||||
browsersStats[browser] = (browsersStats[browser] || 0) + 1;
|
||||
};
|
||||
|
||||
const updateReferrersStatsForVisit = (referrersStats, { referer }) => {
|
||||
const notHasDomain = isNil(referer) || isEmpty(referer);
|
||||
const domain = notHasDomain ? 'Direct' : extractDomain(referer);
|
||||
const domain = extractDomain(referer);
|
||||
|
||||
referrersStats[domain] = (referrersStats[domain] || 0) + 1;
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue