Completed
Pull Request — master (#244)
by Alejandro
12:02
created

src/visits/ShortUrlVisits.js   A

Complexity

Total Complexity 8
Complexity/F 0

Size

Lines of Code 253
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Test Coverage

Coverage 68.85%

Importance

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