Completed
Push — master ( 6eead7...dcc5b9 )
by Alejandro
20s queued 10s
created

src/visits/VisitsStats.js   A

Complexity

Total Complexity 8
Complexity/F 0

Size

Lines of Code 232
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Test Coverage

Coverage 69.09%

Importance

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