Skip to content

Commit 9b1a75f

Browse files
authored
Merge pull request #762 from mikecao/dev
v1.22.0
2 parents ae7186c + 98bd599 commit 9b1a75f

File tree

16 files changed

+915
-724
lines changed

16 files changed

+915
-724
lines changed

components/forms/ResetForm.js

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import React, { useState } from 'react';
2+
import { FormattedMessage } from 'react-intl';
3+
import { Formik, Form, Field } from 'formik';
4+
import Button from 'components/common/Button';
5+
import FormLayout, {
6+
FormButtons,
7+
FormError,
8+
FormMessage,
9+
FormRow,
10+
} from 'components/layout/FormLayout';
11+
import usePost from 'hooks/usePost';
12+
13+
const CONFIRMATION_WORD = 'RESET';
14+
15+
const validate = ({ confirmation }) => {
16+
const errors = {};
17+
18+
if (confirmation !== CONFIRMATION_WORD) {
19+
errors.confirmation = !confirmation ? (
20+
<FormattedMessage id="label.required" defaultMessage="Required" />
21+
) : (
22+
<FormattedMessage id="label.invalid" defaultMessage="Invalid" />
23+
);
24+
}
25+
26+
return errors;
27+
};
28+
29+
export default function ResetForm({ values, onSave, onClose }) {
30+
const post = usePost();
31+
const [message, setMessage] = useState();
32+
33+
const handleSubmit = async ({ type, id }) => {
34+
const { ok, data } = await post(`/api/${type}/${id}/reset`);
35+
36+
if (ok) {
37+
onSave();
38+
} else {
39+
setMessage(
40+
data || <FormattedMessage id="message.failure" defaultMessage="Something went wrong." />,
41+
);
42+
}
43+
};
44+
45+
return (
46+
<FormLayout>
47+
<Formik
48+
initialValues={{ confirmation: '', ...values }}
49+
validate={validate}
50+
onSubmit={handleSubmit}
51+
>
52+
{props => (
53+
<Form>
54+
<div>
55+
<FormattedMessage
56+
id="message.confirm-reset"
57+
defaultMessage="Are your sure you want to reset {target}'s statistics?"
58+
values={{ target: <b>{values.name}</b> }}
59+
/>
60+
</div>
61+
<div>
62+
<FormattedMessage
63+
id="message.reset-warning"
64+
defaultMessage="All statistics for this website will be deleted, but your tracking code will remain intact."
65+
/>
66+
</div>
67+
<p>
68+
<FormattedMessage
69+
id="message.type-reset"
70+
defaultMessage="Type {reset} in the box below to confirm."
71+
values={{ reset: <b>{CONFIRMATION_WORD}</b> }}
72+
/>
73+
</p>
74+
<FormRow>
75+
<div>
76+
<Field name="confirmation" type="text" />
77+
<FormError name="confirmation" />
78+
</div>
79+
</FormRow>
80+
<FormButtons>
81+
<Button
82+
type="submit"
83+
variant="danger"
84+
disabled={props.values.confirmation !== CONFIRMATION_WORD}
85+
>
86+
<FormattedMessage id="label.reset" defaultMessage="Reset" />
87+
</Button>
88+
<Button onClick={onClose}>
89+
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
90+
</Button>
91+
</FormButtons>
92+
<FormMessage>{message}</FormMessage>
93+
</Form>
94+
)}
95+
</Formik>
96+
</FormLayout>
97+
);
98+
}

components/metrics/MetricCard.js

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,38 @@ import { useSpring, animated } from 'react-spring';
33
import { formatNumber } from '../../lib/format';
44
import styles from './MetricCard.module.css';
55

6-
const MetricCard = ({ value = 0, label, format = formatNumber }) => {
6+
const MetricCard = ({
7+
value = 0,
8+
change = 0,
9+
label,
10+
reverseColors = false,
11+
format = formatNumber,
12+
}) => {
713
const props = useSpring({ x: Number(value) || 0, from: { x: 0 } });
14+
const changeProps = useSpring({ x: Number(change) || 0, from: { x: 0 } });
815

916
return (
1017
<div className={styles.card}>
1118
<animated.div className={styles.value}>{props.x.interpolate(x => format(x))}</animated.div>
12-
<div className={styles.label}>{label}</div>
19+
<div className={styles.label}>
20+
{label}
21+
{~~change === 0 && <span className={styles.change}>{format(0)}</span>}
22+
{~~change !== 0 && (
23+
<animated.span
24+
className={`${styles.change} ${
25+
change >= 0
26+
? !reverseColors
27+
? styles.positive
28+
: styles.negative
29+
: !reverseColors
30+
? styles.negative
31+
: styles.positive
32+
}`}
33+
>
34+
{changeProps.x.interpolate(x => `${change >= 0 ? '+' : ''}${format(x)}`)}
35+
</animated.span>
36+
)}
37+
</div>
1338
</div>
1439
);
1540
};

components/metrics/MetricCard.module.css

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,24 @@
1616
.label {
1717
font-size: var(--font-size-normal);
1818
white-space: nowrap;
19+
display: flex;
20+
align-items: center;
21+
gap: 5px;
22+
}
23+
24+
.change {
25+
font-size: 12px;
26+
padding: 0 5px;
27+
border-radius: 5px;
28+
margin-left: 4px;
29+
border: 1px solid var(--gray200);
30+
color: var(--gray500);
31+
}
32+
33+
.change.positive {
34+
color: var(--green500);
35+
}
36+
37+
.change.negative {
38+
color: var(--red500);
1939
}

components/metrics/MetricsBar.js

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,22 @@ export default function MetricsBar({ websiteId, className }) {
3434
[url, modified],
3535
);
3636

37-
const formatFunc = format ? formatLongNumber : formatNumber;
37+
const formatFunc = format
38+
? n => (n >= 0 ? formatLongNumber(n) : `-${formatLongNumber(Math.abs(n))}`)
39+
: formatNumber;
3840

3941
function handleSetFormat() {
4042
setFormat(state => !state);
4143
}
4244

4345
const { pageviews, uniques, bounces, totaltime } = data || {};
44-
const num = Math.min(uniques, bounces);
46+
const num = Math.min(data && uniques.value, data && bounces.value);
47+
const diffs = data && {
48+
pageviews: pageviews.value - pageviews.change,
49+
uniques: uniques.value - uniques.change,
50+
bounces: bounces.value - bounces.change,
51+
totaltime: totaltime.value - totaltime.change,
52+
};
4553

4654
return (
4755
<div className={classNames(styles.bar, className)} onClick={handleSetFormat}>
@@ -51,18 +59,27 @@ export default function MetricsBar({ websiteId, className }) {
5159
<>
5260
<MetricCard
5361
label={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
54-
value={pageviews}
62+
value={pageviews.value}
63+
change={pageviews.change}
5564
format={formatFunc}
5665
/>
5766
<MetricCard
5867
label={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
59-
value={uniques}
68+
value={uniques.value}
69+
change={uniques.change}
6070
format={formatFunc}
6171
/>
6272
<MetricCard
6373
label={<FormattedMessage id="metrics.bounce-rate" defaultMessage="Bounce rate" />}
64-
value={uniques ? (num / uniques) * 100 : 0}
74+
value={uniques.value ? (num / uniques.value) * 100 : 0}
75+
change={
76+
uniques.value && uniques.change
77+
? (num / uniques.value) * 100 -
78+
(Math.min(diffs.uniques, diffs.bounces) / diffs.uniques) * 100 || 0
79+
: 0
80+
}
6581
format={n => Number(n).toFixed(0) + '%'}
82+
reverseColors
6683
/>
6784
<MetricCard
6885
label={
@@ -71,8 +88,19 @@ export default function MetricsBar({ websiteId, className }) {
7188
defaultMessage="Average visit time"
7289
/>
7390
}
74-
value={totaltime && pageviews ? totaltime / (pageviews - bounces) : 0}
75-
format={n => formatShortTime(n, ['m', 's'], ' ')}
91+
value={
92+
totaltime.value && pageviews.value
93+
? totaltime.value / (pageviews.value - bounces.value)
94+
: 0
95+
}
96+
change={
97+
totaltime.value && pageviews.value
98+
? (diffs.totaltime / (diffs.pageviews - diffs.bounces) -
99+
totaltime.value / (pageviews.value - bounces.value)) *
100+
-1 || 0
101+
: 0
102+
}
103+
format={n => `${n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`}
76104
/>
77105
</>
78106
)}

components/settings/WebsiteSettings.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import Button from 'components/common/Button';
77
import PageHeader from 'components/layout/PageHeader';
88
import Modal from 'components/common/Modal';
99
import WebsiteEditForm from 'components/forms/WebsiteEditForm';
10+
import ResetForm from 'components/forms/ResetForm';
1011
import DeleteForm from 'components/forms/DeleteForm';
1112
import TrackingCodeForm from 'components/forms/TrackingCodeForm';
1213
import ShareUrlForm from 'components/forms/ShareUrlForm';
@@ -16,6 +17,7 @@ import Toast from 'components/common/Toast';
1617
import Favicon from 'components/common/Favicon';
1718
import Pen from 'assets/pen.svg';
1819
import Trash from 'assets/trash.svg';
20+
import Reset from 'assets/redo.svg';
1921
import Plus from 'assets/plus.svg';
2022
import Code from 'assets/code.svg';
2123
import LinkIcon from 'assets/link.svg';
@@ -24,6 +26,7 @@ import styles from './WebsiteSettings.module.css';
2426

2527
export default function WebsiteSettings() {
2628
const [editWebsite, setEditWebsite] = useState();
29+
const [resetWebsite, setResetWebsite] = useState();
2730
const [deleteWebsite, setDeleteWebsite] = useState();
2831
const [addWebsite, setAddWebsite] = useState();
2932
const [showCode, setShowCode] = useState();
@@ -55,6 +58,9 @@ export default function WebsiteSettings() {
5558
<Button icon={<Pen />} size="small" onClick={() => setEditWebsite(row)}>
5659
<FormattedMessage id="label.edit" defaultMessage="Edit" />
5760
</Button>
61+
<Button icon={<Reset />} size="small" onClick={() => setResetWebsite(row)}>
62+
<FormattedMessage id="label.reset" defaultMessage="Reset" />
63+
</Button>
5864
<Button icon={<Trash />} size="small" onClick={() => setDeleteWebsite(row)}>
5965
<FormattedMessage id="label.delete" defaultMessage="Delete" />
6066
</Button>
@@ -96,6 +102,7 @@ export default function WebsiteSettings() {
96102
function handleClose() {
97103
setAddWebsite(null);
98104
setEditWebsite(null);
105+
setResetWebsite(null);
99106
setDeleteWebsite(null);
100107
setShowCode(null);
101108
setShowUrl(null);
@@ -141,6 +148,17 @@ export default function WebsiteSettings() {
141148
<WebsiteEditForm onSave={handleSave} onClose={handleClose} />
142149
</Modal>
143150
)}
151+
{resetWebsite && (
152+
<Modal
153+
title={<FormattedMessage id="label.reset-website" defaultMessage="Reset statistics" />}
154+
>
155+
<ResetForm
156+
values={{ type: 'website', id: resetWebsite.website_id, name: resetWebsite.name }}
157+
onSave={handleSave}
158+
onClose={handleClose}
159+
/>
160+
</Modal>
161+
)}
144162
{deleteWebsite && (
145163
<Modal
146164
title={<FormattedMessage id="label.delete-website" defaultMessage="Delete website" />}

lang/en-US.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"label.delete": "Delete",
2020
"label.delete-account": "Delete account",
2121
"label.delete-website": "Delete website",
22+
"label.reset-website": "Reset statistics",
2223
"label.dismiss": "Dismiss",
2324
"label.domain": "Domain",
2425
"label.edit": "Edit",
@@ -58,8 +59,10 @@
5859
"label.view-details": "View details",
5960
"label.websites": "Websites",
6061
"message.active-users": "{x} current {x, plural, one {visitor} other {visitors}}",
62+
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?",
6163
"message.confirm-delete": "Are your sure you want to delete {target}?",
6264
"message.copied": "Copied!",
65+
"message.reset-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.",
6366
"message.delete-warning": "All associated data will be deleted as well.",
6467
"message.failure": "Something went wrong.",
6568
"message.get-share-url": "Get share URL",

lang/pl-PL.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"label.administrator": "Administrator",
66
"label.all": "Wszystkie",
77
"label.all-websites": "Wszystkie witryny",
8+
"label.all-events": "Wszystkie wydarzenia",
89
"label.back": "Powrót",
910
"label.cancel": "Anuluj",
1011
"label.change-password": "Zmień hasło",

0 commit comments

Comments
 (0)