src/visits/VisitsStats.js   A
last analyzed

Complexity

Total Complexity 8
Complexity/F 0

Size

Lines of Code 247
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Test Coverage

Coverage 67.24%

Importance

Changes 0
Metric Value
wmc 8
eloc 214
mnd 8
bc 8
fnc 0
dl 0
loc 247
ccs 39
cts 58
cp 0.6724
rs 10
bpm 0
cpm 0
noi 0
c 0
b 0
f 0
1
import { isEmpty, propEq, values } from 'ramda';
2
import React, { useState, useEffect, useMemo } from 'react';
3
import { Button, Card, Collapse, Progress } from 'reactstrap';
4
import PropTypes from 'prop-types';
5
import classNames from 'classnames';
6
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
7
import { faChevronDown as chevronDown } from '@fortawesome/free-solid-svg-icons';
8
import DateRangeRow from '../utils/DateRangeRow';
9
import Message from '../utils/Message';
10
import { formatDate } from '../utils/helpers/date';
11
import { useToggle } from '../utils/helpers/hooks';
12
import SortableBarGraph from './helpers/SortableBarGraph';
13
import GraphCard from './helpers/GraphCard';
14
import LineChartCard from './helpers/LineChartCard';
15
import VisitsTable from './VisitsTable';
16
import { VisitsInfoType } from './types';
17
18 1
const propTypes = {
19
  children: PropTypes.node,
20
  getVisits: PropTypes.func,
21
  visitsInfo: VisitsInfoType,
22
  cancelGetVisits: PropTypes.func,
23
  matchMedia: PropTypes.func,
24
};
25
26 18
const highlightedVisitsToStats = (highlightedVisits, prop) => highlightedVisits.reduce((acc, highlightedVisit) => {
27 2
  if (!acc[highlightedVisit[prop]]) {
28
    acc[highlightedVisit[prop]] = 0;
29
  }
30
31
  acc[highlightedVisit[prop]] += 1;
32
33
  return acc;
34
}, {});
35 1
const format = formatDate();
36
let selectedBar;
37
38 1
const VisitsStats = ({ processStatsFromVisits, normalizeVisits }, OpenMapModalBtn) => {
39 7
  const VisitsStatsComp = ({ children, visitsInfo, getVisits, cancelGetVisits, matchMedia = window.matchMedia }) => {
40 10
    const [ startDate, setStartDate ] = useState(undefined);
41 10
    const [ endDate, setEndDate ] = useState(undefined);
42 10
    const [ showTable, toggleTable ] = useToggle();
43 10
    const [ tableIsSticky, , setSticky, unsetSticky ] = useToggle();
44 10
    const [ highlightedVisits, setHighlightedVisits ] = useState([]);
45 10
    const [ highlightedLabel, setHighlightedLabel ] = useState();
46 10
    const [ isMobileDevice, setIsMobileDevice ] = useState(false);
47 10
    const determineIsMobileDevice = () => setIsMobileDevice(matchMedia('(max-width: 991px)').matches);
48 10
    const setSelectedVisits = (selectedVisits) => {
49
      selectedBar = undefined;
50
      setHighlightedVisits(selectedVisits);
51
    };
52 18
    const highlightVisitsForProp = (prop) => (value) => {
53
      const newSelectedBar = `${prop}_${value}`;
54
55 2
      if (selectedBar === newSelectedBar) {
56
        setHighlightedVisits([]);
57
        setHighlightedLabel(undefined);
58
        selectedBar = undefined;
59
      } else {
60
        setHighlightedVisits(normalizedVisits.filter(propEq(prop, value)));
61
        setHighlightedLabel(value);
62
        selectedBar = newSelectedBar;
63
      }
64
    };
65
66 10
    const { visits, loading, loadingLarge, error, progress } = visitsInfo;
67 10
    const showTableControls = !loading && visits.length > 0;
68 10
    const normalizedVisits = useMemo(() => normalizeVisits(visits), [ visits ]);
69 10
    const { os, browsers, referrers, countries, cities, citiesForMap } = useMemo(
70 7
      () => processStatsFromVisits(normalizedVisits),
71
      [ normalizedVisits ]
72
    );
73 10
    const mapLocations = values(citiesForMap);
74
75 10
    useEffect(() => {
76
      determineIsMobileDevice();
77
      window.addEventListener('resize', determineIsMobileDevice);
78
79
      return () => {
80
        cancelGetVisits();
81
        window.removeEventListener('resize', determineIsMobileDevice);
82
      };
83
    }, []);
84 10
    useEffect(() => {
85
      getVisits({ startDate: format(startDate), endDate: format(endDate) });
86
    }, [ startDate, endDate ]);
87
88 10
    const renderVisitsContent = () => {
89 10
      if (loadingLarge) {
90 1
        return (
91
          <Message loading>
92
            This is going to take a while... :S
93
            <Progress value={progress} striped={progress === 100} className="mt-3" />
94
          </Message>
95
        );
96
      }
97
98 9
      if (loading) {
99 1
        return <Message loading />;
100
      }
101
102 8
      if (error) {
103 1
        return (
104
          <Card className="mt-4" body inverse color="danger">
105
            An error occurred while loading visits :(
106
          </Card>
107
        );
108
      }
109
110 7
      if (isEmpty(visits)) {
111 1
        return <Message>There are no visits matching current filter  :(</Message>;
112
      }
113
114 6
      return (
115
        <div className="row">
116
          <div className="col-12 mt-4">
117
            <LineChartCard
118
              title="Visits during time"
119
              visits={visits}
120
              highlightedVisits={highlightedVisits}
121
              highlightedLabel={highlightedLabel}
122
            />
123
          </div>
124
          <div className="col-xl-4 col-lg-6 mt-4">
125
            <GraphCard title="Operating systems" stats={os} />
126
          </div>
127
          <div className="col-xl-4 col-lg-6 mt-4">
128
            <GraphCard title="Browsers" stats={browsers} />
129
          </div>
130
          <div className="col-xl-4 mt-4">
131
            <SortableBarGraph
132
              title="Referrers"
133
              stats={referrers}
134
              withPagination={false}
135
              highlightedStats={highlightedVisitsToStats(highlightedVisits, 'referer')}
136
              highlightedLabel={highlightedLabel}
137
              sortingItems={{
138
                name: 'Referrer name',
139
                amount: 'Visits amount',
140
              }}
141
              onClick={highlightVisitsForProp('referer')}
142
            />
143
          </div>
144
          <div className="col-lg-6 mt-4">
145
            <SortableBarGraph
146
              title="Countries"
147
              stats={countries}
148
              highlightedStats={highlightedVisitsToStats(highlightedVisits, 'country')}
149
              highlightedLabel={highlightedLabel}
150
              sortingItems={{
151
                name: 'Country name',
152
                amount: 'Visits amount',
153
              }}
154
              onClick={highlightVisitsForProp('country')}
155
            />
156
          </div>
157
          <div className="col-lg-6 mt-4">
158
            <SortableBarGraph
159
              title="Cities"
160
              stats={cities}
161
              highlightedStats={highlightedVisitsToStats(highlightedVisits, 'city')}
162
              highlightedLabel={highlightedLabel}
163
              extraHeaderContent={(activeCities) =>
164 2
                mapLocations.length > 0 &&
165
                <OpenMapModalBtn modalTitle="Cities" locations={mapLocations} activeCities={activeCities} />
166
              }
167
              sortingItems={{
168
                name: 'City name',
169
                amount: 'Visits amount',
170
              }}
171
              onClick={highlightVisitsForProp('city')}
172
            />
173
          </div>
174
        </div>
175
      );
176
    };
177
178 10
    return (
179
      <React.Fragment>
180
        {children}
181
182
        <section className="mt-4">
183
          <div className="row flex-md-row-reverse">
184
            <div className="col-lg-7 col-xl-6">
185
              <DateRangeRow
186
                disabled={loading}
187
                startDate={startDate}
188
                endDate={endDate}
189
                onStartDateChange={setStartDate}
190
                onEndDateChange={setEndDate}
191
              />
192
            </div>
193
            <div className="col-lg-5 col-xl-6 mt-4 mt-lg-0">
194
              {showTableControls && (
195
                <span className={classNames({ row: isMobileDevice })}>
196
                  <span className={classNames({ 'col-6': isMobileDevice })}>
197
                    <Button outline color="primary" block={isMobileDevice} onClick={toggleTable}>
198
                      {showTable ? 'Hide' : 'Show'} table
199
                      <FontAwesomeIcon icon={chevronDown} rotation={showTable ? 180 : undefined} className="ml-2" />
200
                    </Button>
201
                  </span>
202
                  <span className={classNames({ 'col-6': isMobileDevice, 'ml-2': !isMobileDevice })}>
203
                    <Button
204
                      outline
205
                      disabled={highlightedVisits.length === 0}
206
                      block={isMobileDevice}
207
                      onClick={() => setSelectedVisits([])}
208
                    >
209
                      Reset selection
210
                    </Button>
211
                  </span>
212
                </span>
213
              )}
214
            </div>
215
          </div>
216
        </section>
217
218
        {showTableControls && (
219
          <Collapse
220
            isOpen={showTable}
221
            // Enable stickiness only when there's no CSS animation, to avoid weird rendering effects
222
            onEntered={setSticky}
223
            onExiting={unsetSticky}
224
          >
225
            <VisitsTable
226
              visits={normalizedVisits}
227
              selectedVisits={highlightedVisits}
228
              setSelectedVisits={setSelectedVisits}
229
              isSticky={tableIsSticky}
230
            />
231
          </Collapse>
232
        )}
233
234
        <section>
235
          {renderVisitsContent()}
236
        </section>
237
      </React.Fragment>
238
    );
239
  };
240
241 7
  VisitsStatsComp.propTypes = propTypes;
242
243 7
  return VisitsStatsComp;
244
};
245
246
export default VisitsStats;
247