Completed
Push — master ( d231ed...aa59a9 )
by Alejandro
59s queued 56s
created

src/visits/ShortUrlVisits.js   A

Complexity

Total Complexity 8
Complexity/F 0

Size

Lines of Code 265
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Test Coverage

Coverage 69.35%

Importance

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