Compare commits
572 Commits
Author | SHA1 | Date |
---|---|---|
![]() |
732fc73fe6 | |
![]() |
586529a5ca | |
![]() |
22e2f79bff | |
![]() |
6f4cc9e84c | |
![]() |
e77e030f2c | |
![]() |
a43bbc51f6 | |
![]() |
bf26874965 | |
![]() |
6cdf9a68d1 | |
![]() |
402acf563e | |
![]() |
2d6077fe16 | |
![]() |
c9ec055996 | |
![]() |
3f0815855c | |
![]() |
46615fe7eb | |
![]() |
9345c9ac9e | |
![]() |
938047ce45 | |
![]() |
34757e7b2a | |
![]() |
01fe8b44a3 | |
![]() |
ae85bf696b | |
![]() |
f450fc35fb | |
![]() |
4c544361fd | |
![]() |
4ea3eec98c | |
![]() |
5864aee043 | |
![]() |
c49e2c6974 | |
![]() |
02f031bde1 | |
![]() |
3f1ed750f0 | |
![]() |
d73def80c7 | |
![]() |
4b72918a91 | |
![]() |
44a1c7fa10 | |
![]() |
c47d246a79 | |
![]() |
89d37e0045 | |
![]() |
4045cfd039 | |
![]() |
c3beb10e3b | |
![]() |
b6f3f57455 | |
![]() |
4876684461 | |
![]() |
9e11866ddd | |
![]() |
1e9a4ad08f | |
![]() |
92ab391ef8 | |
![]() |
c23373d164 | |
![]() |
1b8c2c4dde | |
![]() |
26a9cb67d8 | |
![]() |
4a51a5db3f | |
![]() |
8bc0fb92ba | |
![]() |
ff9a6335d1 | |
![]() |
c5bffb97cc | |
![]() |
2e3d2924e7 | |
![]() |
8666965930 | |
![]() |
5027d9d945 | |
![]() |
6cb2429ee1 | |
![]() |
ef59e93adc | |
![]() |
47f63d80c4 | |
![]() |
36b767af75 | |
![]() |
00d0421623 | |
![]() |
987e3aabbd | |
![]() |
a2bce7ae2c | |
![]() |
6469dd296d | |
![]() |
71fcad26a5 | |
![]() |
80438bf84c | |
![]() |
eaaf3a0a5c | |
![]() |
a7e6c32460 | |
![]() |
39f630400f | |
![]() |
3ba75a6737 | |
![]() |
71628bec90 | |
![]() |
6f80908160 | |
![]() |
21c4ec068d | |
![]() |
115a2560a7 | |
![]() |
f246183094 | |
![]() |
edce62eede | |
![]() |
223f0051ed | |
![]() |
6c09dd1e23 | |
![]() |
5b4bd67455 | |
![]() |
e6c5a9b17d | |
![]() |
69b9458975 | |
![]() |
65c07f5034 | |
![]() |
b9a0f0442e | |
![]() |
a0894866b9 | |
![]() |
39cafafd76 | |
![]() |
31df0f7f07 | |
![]() |
8bddc666b4 | |
![]() |
b2d8423dcb | |
![]() |
5d8fb2f791 | |
![]() |
3039ad9315 | |
![]() |
1baf4d5571 | |
![]() |
322990b03b | |
![]() |
7aac95a410 | |
![]() |
cce966c48c | |
![]() |
5bfbc71e7f | |
![]() |
1d639c77a2 | |
![]() |
80aad4ef55 | |
![]() |
c4d3d6fb09 | |
![]() |
288f77d6ff | |
![]() |
4bd8b44420 | |
![]() |
5f30cbd789 | |
![]() |
5cd4e9fd8d | |
![]() |
fa3659fcf6 | |
![]() |
a84d2181e7 | |
![]() |
981abd5966 | |
![]() |
136f962218 | |
![]() |
aba523cbee | |
![]() |
d2c9c45461 | |
![]() |
dbc19e4ffd | |
![]() |
f0d13efc7a | |
![]() |
03ba225587 | |
![]() |
e63591e35d | |
![]() |
98028f1756 | |
![]() |
3b6c6846b1 | |
![]() |
ae49317c93 | |
![]() |
e33a8e054b | |
![]() |
1cfebc8c78 | |
![]() |
06d19e8594 | |
![]() |
eb7a83b404 | |
![]() |
afde1377dc | |
![]() |
b1856c7eb3 | |
![]() |
f2c0b4253a | |
![]() |
1520f21b82 | |
![]() |
1d9cb0d7c7 | |
![]() |
dbafc98142 | |
![]() |
b39ef68372 | |
![]() |
b95208afcd | |
![]() |
374555aff3 | |
![]() |
af14ccb60e | |
![]() |
8bd289ad01 | |
![]() |
9c2242012e | |
![]() |
cc20f898b1 | |
![]() |
2cd1f5541b | |
![]() |
083a9ffc2c | |
![]() |
ac8d8bbd1e | |
![]() |
ab48a0882a | |
![]() |
aca612c833 | |
![]() |
a52bfaa629 | |
![]() |
0889542f3b | |
![]() |
16ece9ad64 | |
![]() |
e8ca23c390 | |
![]() |
3eabe9b958 | |
![]() |
78030eb4a0 | |
![]() |
db31810e68 | |
![]() |
81bc5d730e | |
![]() |
64d2cf3805 | |
![]() |
3d64985f1a | |
![]() |
03f7f910f2 | |
![]() |
ab0066517d | |
![]() |
939f4752bb | |
![]() |
56ad1c1602 | |
![]() |
c548ac9ca3 | |
![]() |
b3c1eb1437 | |
![]() |
e19d612db2 | |
![]() |
94df42a306 | |
![]() |
904f9408d8 | |
![]() |
10a4921942 | |
![]() |
a7daae2d20 | |
![]() |
bac8edd113 | |
![]() |
dfae0c150d | |
![]() |
0e4c071b16 | |
![]() |
d406564af3 | |
![]() |
60984e031c | |
![]() |
d2edb0d0d7 | |
![]() |
2bd970ba2b | |
![]() |
23b98314d6 | |
![]() |
01559c4c63 | |
![]() |
43499eb4fd | |
![]() |
672608b2f5 | |
![]() |
b1074c893a | |
![]() |
561770a469 | |
![]() |
7e79711900 | |
![]() |
061d8cd174 | |
![]() |
b7d64a91bd | |
![]() |
54b018f7ba | |
![]() |
43a4357729 | |
![]() |
fa52e6c83f | |
![]() |
b183fc1600 | |
![]() |
06649c2e31 | |
![]() |
7e096336ce | |
![]() |
3461342601 | |
![]() |
2890cb4419 | |
![]() |
35fbb7321e | |
![]() |
42df7695e8 | |
![]() |
9dd9ab1325 | |
![]() |
e286994397 | |
![]() |
f2edd0d604 | |
![]() |
44e37ece2d | |
![]() |
ad3ee5e953 | |
![]() |
0335fd3529 | |
![]() |
081a126c01 | |
![]() |
baa6d8486b | |
![]() |
aa418e0bed | |
![]() |
887b3abb29 | |
![]() |
cc9b1d21cb | |
![]() |
ef324fdf73 | |
![]() |
ab44150532 | |
![]() |
5fc96cf5e1 | |
![]() |
5997efc276 | |
![]() |
a30147547b | |
![]() |
4e36149e32 | |
![]() |
b32ced5501 | |
![]() |
4a1c6f40a6 | |
![]() |
e2fcd40c2b | |
![]() |
0926a4c310 | |
![]() |
21e48c194e | |
![]() |
f5f6dc052e | |
![]() |
da79e4c19e | |
![]() |
f84e41e198 | |
![]() |
94ed67e339 | |
![]() |
3e1c1a85e4 | |
![]() |
339085a946 | |
![]() |
f7119d494b | |
![]() |
9eff565e7a | |
![]() |
53b23420a4 | |
![]() |
07c92ff9b5 | |
![]() |
0dc6529cf1 | |
![]() |
7b97cb3348 | |
![]() |
2798de0c4c | |
![]() |
b2bebaea3e | |
![]() |
e850fbd0bd | |
![]() |
2dbc6ce381 | |
![]() |
2656de6682 | |
![]() |
63e535eea4 | |
![]() |
45d3fa4016 | |
![]() |
f45995e8b5 | |
![]() |
7b07de7a98 | |
![]() |
f0cddc2743 | |
![]() |
ee8d1ee9e3 | |
![]() |
6ee846d102 | |
![]() |
7a1ac22dde | |
![]() |
0f6cfca522 | |
![]() |
24ac7d6be0 | |
![]() |
a33706585b | |
![]() |
6424100223 | |
![]() |
f4f657b666 | |
![]() |
6b0d6c0d1c | |
![]() |
459fbc03ce | |
![]() |
f38f0c6420 | |
![]() |
db845d6c04 | |
![]() |
b1fbb20967 | |
![]() |
594660f298 | |
![]() |
25d4a9cec0 | |
![]() |
05ece6a4d0 | |
![]() |
767b25a9af | |
![]() |
5325996a56 | |
![]() |
e71200a53f | |
![]() |
f56374b259 | |
![]() |
0217f19d99 | |
![]() |
6dd90ce9ca | |
![]() |
dea4b4766c | |
![]() |
2631f00598 | |
![]() |
84894a5b80 | |
![]() |
d851a9e88b | |
![]() |
c0e79f5e9d | |
![]() |
dee58f8a55 | |
![]() |
4c92da6fc6 | |
![]() |
76296bc2ee | |
![]() |
329df22a5d | |
![]() |
b4f6dfedda | |
![]() |
c30aa4c0b6 | |
![]() |
8276a769e6 | |
![]() |
005b14b629 | |
![]() |
58197116dd | |
![]() |
93707606df | |
![]() |
d47724858a | |
![]() |
5103da331a | |
![]() |
31f28125dc | |
![]() |
ae772f048c | |
![]() |
ba71f5cafc | |
![]() |
7464bd2521 | |
![]() |
9001361e4c | |
![]() |
bbdb672063 | |
![]() |
b77657faa6 | |
![]() |
4e9d661d82 | |
![]() |
8b966a04b0 | |
![]() |
6805a8978a | |
![]() |
2713e3316a | |
![]() |
8da72a693d | |
![]() |
2411aeae8d | |
![]() |
8397102bac | |
![]() |
728e4cff5b | |
![]() |
74192cd695 | |
![]() |
2b395f252d | |
![]() |
479dc06d36 | |
![]() |
e31ee89fee | |
![]() |
57c3d03cc8 | |
![]() |
17601c4dea | |
![]() |
14802afe8b | |
![]() |
91100393eb | |
![]() |
d2687b6580 | |
![]() |
e2b95f2235 | |
![]() |
9dd35b3766 | |
![]() |
b7c3b58072 | |
![]() |
ead831bb3c | |
![]() |
2172dddd1c | |
![]() |
e4c801e823 | |
![]() |
c175120aa2 | |
![]() |
64b040c865 | |
![]() |
358519dddd | |
![]() |
a026fc97fc | |
![]() |
14a8ae2b81 | |
![]() |
d471d05217 | |
![]() |
055e7f04fc | |
![]() |
44e6bff6b1 | |
![]() |
964ef1c92e | |
![]() |
6d5aeb3bd1 | |
![]() |
96d25721da | |
![]() |
f984d80c42 | |
![]() |
a22d50a597 | |
![]() |
3b7000a436 | |
![]() |
95ed8a09aa | |
![]() |
9e865ecdf3 | |
![]() |
05933ff1d2 | |
![]() |
23d02e5aac | |
![]() |
33bf2ccb5e | |
![]() |
f3e146d0fb | |
![]() |
f2d86f9307 | |
![]() |
4d6abc4529 | |
![]() |
17c8cc07e4 | |
![]() |
077fad20ea | |
![]() |
c38da5bfd7 | |
![]() |
fbe35f4c97 | |
![]() |
cb79d806e8 | |
![]() |
14e4a090bb | |
![]() |
cdacb640c6 | |
![]() |
43ef6884df | |
![]() |
9260e5bc33 | |
![]() |
be2fc0de8d | |
![]() |
9979672de5 | |
![]() |
c2789d70bc | |
![]() |
1666e5e3af | |
![]() |
87545a5648 | |
![]() |
ea39f5b431 | |
![]() |
9ddb8b5d25 | |
![]() |
f3e1f18e1b | |
![]() |
8532c673fe | |
![]() |
fc2a8f3d9f | |
![]() |
a8cd0932d8 | |
![]() |
611169c65f | |
![]() |
7b0b97afb9 | |
![]() |
ffb2771819 | |
![]() |
b0c5899569 | |
![]() |
0b20885772 | |
![]() |
c62e2e9cc7 | |
![]() |
434ef3eb9e | |
![]() |
9c8cb8247e | |
![]() |
afec08b355 | |
![]() |
790e71cc14 | |
![]() |
f569f6ac91 | |
![]() |
54051d7204 | |
![]() |
275e7d3df6 | |
![]() |
be8eb61f7f | |
![]() |
c1d3e9ec67 | |
![]() |
7596f42545 | |
![]() |
cfa36d1802 | |
![]() |
9321401297 | |
![]() |
3823705fc6 | |
![]() |
3a18e79d45 | |
![]() |
403144eb30 | |
![]() |
61732f391e | |
![]() |
0ec20d42cf | |
![]() |
e31f882333 | |
![]() |
c07a561eda | |
![]() |
7df58a921f | |
![]() |
4362be44e0 | |
![]() |
91bcbec2af | |
![]() |
9a3e8921a7 | |
![]() |
6ba00152c2 | |
![]() |
2470fb4c7c | |
![]() |
72af76a417 | |
![]() |
701bde53b7 | |
![]() |
fd83b63a30 | |
![]() |
7a3443cd06 | |
![]() |
23ea2e9e93 | |
![]() |
c3426a67ee | |
![]() |
e2c801439d | |
![]() |
49402924aa | |
![]() |
0d6da743b9 | |
![]() |
8a9532f213 | |
![]() |
f908476e71 | |
![]() |
0607e9f442 | |
![]() |
9b90036401 | |
![]() |
178d780ab7 | |
![]() |
cb55cec342 | |
![]() |
fcf51d9727 | |
![]() |
696d9c978c | |
![]() |
bb2cd93ad4 | |
![]() |
7b9abd72f8 | |
![]() |
3fc5f5151b | |
![]() |
05f01cd09e | |
![]() |
b41a2a4e56 | |
![]() |
19662abcd8 | |
![]() |
ab8296235f | |
![]() |
0119f7bcde | |
![]() |
fd1006a299 | |
![]() |
bb99b3eba5 | |
![]() |
448d2b2241 | |
![]() |
3325df0d84 | |
![]() |
65691f5912 | |
![]() |
8684781624 | |
![]() |
c9cacbb0e2 | |
![]() |
5262d19c8b | |
![]() |
82f0bc3d2b | |
![]() |
94165ca5ad | |
![]() |
e5645cfb00 | |
![]() |
ad28d69d27 | |
![]() |
37e98c9751 | |
![]() |
a777b2916f | |
![]() |
93650a2f66 | |
![]() |
4cb5a14de9 | |
![]() |
3ac560dc0f | |
![]() |
6c302a7325 | |
![]() |
84430e38eb | |
![]() |
5657a64c77 | |
![]() |
25f44b84df | |
![]() |
b853255c23 | |
![]() |
7d3334ccce | |
![]() |
02272d3909 | |
![]() |
982d47c7e9 | |
![]() |
b8f72660d9 | |
![]() |
55a586fe27 | |
![]() |
8ecc6400ef | |
![]() |
802c262cd9 | |
![]() |
6bacfa5892 | |
![]() |
7e091c5d40 | |
![]() |
074fa2c5fc | |
![]() |
b6cc6cb655 | |
![]() |
93b77672f3 | |
![]() |
28921a7cd5 | |
![]() |
4351ef37f8 | |
![]() |
eea01b21cf | |
![]() |
90db2239a2 | |
![]() |
1b44c5f826 | |
![]() |
638a674e99 | |
![]() |
2c36eaad5c | |
![]() |
4a689bf294 | |
![]() |
d4437427c4 | |
![]() |
c815e7cd51 | |
![]() |
9f8ea060b6 | |
![]() |
f062cdbed2 | |
![]() |
45c13da262 | |
![]() |
87bbaa7f1d | |
![]() |
a7cad0190c | |
![]() |
ad5e34a894 | |
![]() |
94726239aa | |
![]() |
423f26c7fb | |
![]() |
54d5af5cbb | |
![]() |
30274a07fd | |
![]() |
6b3eb7617d | |
![]() |
e2e6c2cb8d | |
![]() |
0ce2d1fbfc | |
![]() |
6457ddfec1 | |
![]() |
9078444ca5 | |
![]() |
0b16d06d8c | |
![]() |
d6e17fe982 | |
![]() |
835289a1f8 | |
![]() |
22aeaf7166 | |
![]() |
e5b57f63cd | |
![]() |
656df4f846 | |
![]() |
9e4a5159ba | |
![]() |
5fd946cfde | |
![]() |
89f2fd601e | |
![]() |
689bb94898 | |
![]() |
318dd7de74 | |
![]() |
02a1438cfe | |
![]() |
5f15ad0807 | |
![]() |
cc2be9f47a | |
![]() |
b3c41786ba | |
![]() |
219641a633 | |
![]() |
765833b357 | |
![]() |
54eb8a19d4 | |
![]() |
a430eae12b | |
![]() |
3acff56a59 | |
![]() |
4b1013c8c6 | |
![]() |
58a380d6e7 | |
![]() |
7725b5c129 | |
![]() |
51e79be23d | |
![]() |
fbccf4d3af | |
![]() |
cdd54df8f6 | |
![]() |
11ffd1cf4c | |
![]() |
5bd8608918 | |
![]() |
1b6653c35c | |
![]() |
91af593ff5 | |
![]() |
7c0db0633f | |
![]() |
451fbc84a4 | |
![]() |
d9ba4097ee | |
![]() |
5c683f9103 | |
![]() |
86aaa5519e | |
![]() |
cd400bcef2 | |
![]() |
922c3acab3 | |
![]() |
1d9c3133f0 | |
![]() |
558ce268a0 | |
![]() |
529b7a35c3 | |
![]() |
1bb33f7aff | |
![]() |
aba5b9362e | |
![]() |
5c134f3a2e | |
![]() |
05c7196bc6 | |
![]() |
1758ae782d | |
![]() |
92398c04f7 | |
![]() |
48286ec1ce | |
![]() |
65fe29c385 | |
![]() |
fad38dc180 | |
![]() |
1eb9e10d94 | |
![]() |
fabe90d99d | |
![]() |
2492b0b9ad | |
![]() |
17d37fb15b | |
![]() |
38ebf49827 | |
![]() |
434b86cf19 | |
![]() |
4419a25f97 | |
![]() |
c9c384871f | |
![]() |
613bf86014 | |
![]() |
96de650a67 | |
![]() |
53ec66e820 | |
![]() |
4f15c4f146 | |
![]() |
b4bd988e4e | |
![]() |
f654e6728c | |
![]() |
9d967fb0fe | |
![]() |
4b5b4db108 | |
![]() |
ac2b79c5cf | |
![]() |
b05b18e736 | |
![]() |
9cc0588175 | |
![]() |
70ef857dc7 | |
![]() |
7450b76e6d | |
![]() |
d199065d3a | |
![]() |
0cb33a9c73 | |
![]() |
c40c2e15c3 | |
![]() |
4c79c7054e | |
![]() |
d827b79c72 | |
![]() |
fcb1767eb1 | |
![]() |
d7920543c3 | |
![]() |
ecc4e0b889 | |
![]() |
19166e580e | |
![]() |
f10694d07e | |
![]() |
796c65fa29 | |
![]() |
5f159ed5ce | |
![]() |
b845ae547d | |
![]() |
1cd195d895 | |
![]() |
3cf0617366 | |
![]() |
c24f8cb985 | |
![]() |
0a731e3408 | |
![]() |
170b30b391 | |
![]() |
2a5927e345 | |
![]() |
8f686db116 | |
![]() |
82e6728e1e | |
![]() |
660c810ec1 | |
![]() |
b46a0c179f | |
![]() |
f3879c92e1 | |
![]() |
f42cab8d83 | |
![]() |
eb2f07c105 | |
![]() |
db6a6d6055 | |
![]() |
c56f02c475 | |
![]() |
b243f0a319 | |
![]() |
74d2cd8d03 | |
![]() |
7bbed0e12b | |
![]() |
cad0b73e42 | |
![]() |
1472e205a3 | |
![]() |
8ee7172ea0 | |
![]() |
561cde6e7e | |
![]() |
c90bd941b5 | |
![]() |
aef7326f4c | |
![]() |
ef9d915592 | |
![]() |
dff105c747 | |
![]() |
c05d116875 | |
![]() |
ad15466028 | |
![]() |
677566b5f3 | |
![]() |
a91386434d | |
![]() |
e1f99a7d01 | |
![]() |
f4db04c3c6 | |
![]() |
1d36a8a46b | |
![]() |
1fcaa1c9fc | |
![]() |
9ee6fb994c | |
![]() |
545f1f901d | |
![]() |
6145b31ce5 | |
![]() |
2555ee09d1 | |
![]() |
c443d83654 | |
![]() |
7b0b44b18d | |
![]() |
d1bc42f1b5 | |
![]() |
6a2ea9f7cc | |
![]() |
268c036632 | |
![]() |
e241008cd5 |
|
@ -4,3 +4,4 @@ Dockerfile
|
|||
.gitignore
|
||||
.DS_Store
|
||||
node_modules
|
||||
.idea
|
|
@ -4,12 +4,7 @@
|
|||
"es2020": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:prettier/recommended",
|
||||
"plugin:import/recommended",
|
||||
"next"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
|
@ -17,6 +12,15 @@
|
|||
"ecmaVersion": 11,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:prettier/recommended",
|
||||
"plugin:import/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"next"
|
||||
],
|
||||
|
||||
"plugins": ["@typescript-eslint", "prettier"],
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"alias": {
|
||||
|
@ -32,7 +36,7 @@
|
|||
["store", "./store"],
|
||||
["styles", "./styles"]
|
||||
],
|
||||
"extensions": [".ts", ".js", ".jsx", ".json"]
|
||||
"extensions": [".ts", ".tsx", ".js", ".jsx", ".json"]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -42,7 +46,11 @@
|
|||
"react/react-in-jsx-scope": "off",
|
||||
"react/prop-types": "off",
|
||||
"import/no-anonymous-default-export": "off",
|
||||
"@next/next/no-img-element": "off"
|
||||
"import/no-named-as-default": "off",
|
||||
"@next/next/no-img-element": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-var-requires": "off"
|
||||
},
|
||||
"globals": {
|
||||
"React": "writable"
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
name: "🐛 Bug Report"
|
||||
description: Create a bug report for Umami.
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the Bug
|
||||
description: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Database
|
||||
description: What database are you using?
|
||||
options:
|
||||
- PostgreSQL
|
||||
- MySQL
|
||||
- Umami Cloud
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
- type: input
|
||||
attributes:
|
||||
label: Which browser are you using? (if relevant)
|
||||
description: 'For example: Chrome, Edge, Firefox, etc'
|
||||
- type: input
|
||||
attributes:
|
||||
label: How are you deploying your application? (if relevant)
|
||||
description: 'For example: Vercel, Railway, Docker, etc'
|
|
@ -0,0 +1,10 @@
|
|||
name: "✨ Feature Request"
|
||||
description: Create a feature or enhancement request for Umami.
|
||||
labels: ['enhancement']
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the feature or enhancement
|
||||
description: A clear and concise description of what the feature or enhancement is.
|
||||
validations:
|
||||
required: true
|
|
@ -0,0 +1,6 @@
|
|||
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: "🤔 Ask a question"
|
||||
url: https://github.com/umami-software/umami/discussions
|
||||
about: Ask questions and discuss with other community members.
|
|
@ -18,14 +18,16 @@ jobs:
|
|||
db-type: [postgresql, mysql]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: mr-smithers-excellent/docker-build-push@v5
|
||||
- uses: mr-smithers-excellent/docker-build-push@v6
|
||||
name: Build & push Docker image for ${{ matrix.db-type }}
|
||||
with:
|
||||
image: umami
|
||||
tags: ${{ matrix.db-type }}-${{ inputs.version }}, ${{ matrix.db-type }}-latest
|
||||
buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
|
||||
registry: ghcr.io
|
||||
multiPlatform: true
|
||||
platform: linux/amd64,linux/arm64
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
@ -3,7 +3,6 @@ name: Create docker images
|
|||
on: [create]
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
name: Build, push, and deploy
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
|
@ -14,17 +13,19 @@ jobs:
|
|||
db-type: [postgresql, mysql]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set env
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
|
||||
- uses: mr-smithers-excellent/docker-build-push@v5
|
||||
- uses: mr-smithers-excellent/docker-build-push@v6
|
||||
name: Build & push Docker image for ${{ matrix.db-type }}
|
||||
with:
|
||||
image: umami
|
||||
tags: ${{ matrix.db-type }}-${{ env.RELEASE_VERSION }}, ${{ matrix.db-type }}-latest
|
||||
buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
|
||||
registry: ghcr.io
|
||||
multiPlatform: true
|
||||
platform: linux/amd64,linux/arm64
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
|
@ -15,15 +15,16 @@
|
|||
|
||||
# production
|
||||
/build
|
||||
/public/umami.js
|
||||
/public/geo
|
||||
/public/script.js
|
||||
/geo
|
||||
/dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.idea
|
||||
*.iml
|
||||
*.log
|
||||
/.vscode/
|
||||
.vscode
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
|
|
|
@ -1 +1 @@
|
|||
/public/
|
||||
/public/script.js
|
|
@ -1,15 +1,18 @@
|
|||
# Install dependencies only when needed
|
||||
FROM node:16-alpine AS deps
|
||||
FROM node:18-alpine AS deps
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
COPY package.json yarn.lock ./
|
||||
# Add yarn timeout to handle slow CPU when Github Actions
|
||||
RUN yarn config set network-timeout 300000
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM node:16-alpine AS builder
|
||||
FROM node:18-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY docker/middleware.js .
|
||||
COPY . .
|
||||
|
||||
ARG DATABASE_TYPE
|
||||
|
@ -23,7 +26,7 @@ ENV NEXT_TELEMETRY_DISABLED 1
|
|||
RUN yarn build-docker
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM node:16-alpine AS runner
|
||||
FROM node:18-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
|
|
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020 Mike Cao <mike@mikecao.com>
|
||||
Copyright (c) 2022 Umami Software, Inc. <hello@umami.is>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# umami
|
||||
|
||||
Umami is a simple, fast, privacy-focused alternative to Google Analytics.
|
||||
Umami MAMI is a simple, fast, privacy-focused alternative to Google Analytics.
|
||||
|
||||
## Getting started
|
||||
|
||||
|
@ -66,7 +66,7 @@ or change the [port](https://nextjs.org/docs/api-reference/cli#production) to se
|
|||
To build the umami container and start up a Postgres database, run:
|
||||
|
||||
```bash
|
||||
docker compose up
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Alternatively, to pull just the Umami Docker image with PostgreSQL support:
|
||||
|
|
2
app.json
|
@ -6,7 +6,7 @@
|
|||
"repository": "https://github.com/umami-software/umami",
|
||||
"addons": ["heroku-postgresql"],
|
||||
"env": {
|
||||
"HASH_SALT": {
|
||||
"APP_SECRET": {
|
||||
"description": "Used to generate unique values for your installation",
|
||||
"required": true,
|
||||
"generator": "secret"
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<svg id="Layer_2" height="512" viewBox="0 0 30 30" width="512" xmlns="http://www.w3.org/2000/svg" data-name="Layer 2"><g fill="rgb(0,0,0)"><path d="m15 14a5.5 5.5 0 1 1 5.5-5.5 5.51 5.51 0 0 1 -5.5 5.5zm0-9a3.5 3.5 0 1 0 3.5 3.5 3.5 3.5 0 0 0 -3.5-3.5z"/><path d="m7.5 24.5a1 1 0 0 1 -1-1 8.5 8.5 0 0 1 13.6-6.8 1 1 0 1 1 -1.2 1.6 6.44 6.44 0 0 0 -3.9-1.3 6.51 6.51 0 0 0 -6.5 6.5 1 1 0 0 1 -1 1z"/><path d="m23 27a1 1 0 0 1 -1-1v-6a1 1 0 0 1 2 0v6a1 1 0 0 1 -1 1z"/><path d="m26 24h-6a1 1 0 0 1 0-2h6a1 1 0 0 1 0 2z"/></g></svg>
|
After Width: | Height: | Size: 529 B |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="m216.464 36.465-7.071 7.07c-4.686 4.686-4.686 12.284 0 16.971L387.887 239H12c-6.627 0-12 5.373-12 12v10c0 6.627 5.373 12 12 12h375.887L209.393 451.494c-4.686 4.686-4.686 12.284 0 16.971l7.071 7.07c4.686 4.686 12.284 4.686 16.97 0l211.051-211.05c4.686-4.686 4.686-12.284 0-16.971L233.434 36.465c-4.686-4.687-12.284-4.687-16.97 0z"/></svg>
|
Before Width: | Height: | Size: 408 B |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M392 320c-13.25 0-24 10.75-24 24v112c0 4.406-3.594 8-8 8H56c-4.406 0-8-3.594-8-8V152c0-4.406 3.594-8 8-8h112c13.25 0 24-10.75 24-24s-10.75-24-24-24H56c-30.875 0-56 25.125-56 56v304c0 30.875 25.125 56 56 56h304c30.875 0 56-25.125 56-56V344c0-13.25-10.75-24-24-24ZM488 0H320c-13.25 0-24 10.75-24 24s10.75 24 24 24h110.062L183.031 295.031c-9.375 9.375-9.375 24.563 0 33.938A23.9 23.9 0 0 0 200 336a23.9 23.9 0 0 0 16.969-7.031L464 81.938V192c0 13.25 10.75 24 24 24s24-10.75 24-24V24c0-13.25-10.75-24-24-24Z"/></svg>
|
Before Width: | Height: | Size: 583 B |
Before Width: | Height: | Size: 1002 B After Width: | Height: | Size: 1002 B |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M396.8 352h22.4c6.4 0 12.8-6.4 12.8-12.8V108.8c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v230.4c0 6.4 6.4 12.8 12.8 12.8zm-192 0h22.4c6.4 0 12.8-6.4 12.8-12.8V140.8c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v198.4c0 6.4 6.4 12.8 12.8 12.8zm96 0h22.4c6.4 0 12.8-6.4 12.8-12.8V204.8c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v134.4c0 6.4 6.4 12.8 12.8 12.8zM496 400H48V80c0-8.84-7.16-16-16-16H16C7.16 64 0 71.16 0 80v336c0 17.67 14.33 32 32 32h464c8.84 0 16-7.16 16-16v-16c0-8.84-7.16-16-16-16zm-387.2-48h22.4c6.4 0 12.8-6.4 12.8-12.8v-70.4c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v70.4c0 6.4 6.4 12.8 12.8 12.8z"/></svg>
|
Before Width: | Height: | Size: 748 B |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M435.848 83.466 172.804 346.51l-96.652-96.652c-4.686-4.686-12.284-4.686-16.971 0l-28.284 28.284c-4.686 4.686-4.686 12.284 0 16.971l133.421 133.421c4.686 4.686 12.284 4.686 16.971 0l299.813-299.813c4.686-4.686 4.686-12.284 0-16.971l-28.284-28.284c-4.686-4.686-12.284-4.686-16.97 0z"/></svg>
|
Before Width: | Height: | Size: 360 B |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="m441.9 167.3-19.8-19.8c-4.7-4.7-12.3-4.7-17 0L224 328.2 42.9 147.5c-4.7-4.7-12.3-4.7-17 0L6.1 167.3c-4.7 4.7-4.7 12.3 0 17l209.4 209.4c4.7 4.7 12.3 4.7 17 0l209.4-209.4c4.7-4.7 4.7-12.3 0-17z"/></svg>
|
Before Width: | Height: | Size: 271 B |
|
@ -0,0 +1 @@
|
|||
<svg height="512" viewBox="0 0 24 24" width="512" xmlns="http://www.w3.org/2000/svg"><g clip-rule="evenodd"><path d="M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16zM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12z"/><path d="M11.168 11.445a1 1 0 0 1 1.387-.277l3 2a1 1 0 0 1-1.11 1.664l-3-2a1 1 0 0 1-.277-1.387z"/><path d="M12 6a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V7a1 1 0 0 1 1-1z"/></g></svg>
|
After Width: | Height: | Size: 400 B |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M234.8 511.7 196 500.4c-4.2-1.2-6.7-5.7-5.5-9.9L331.3 5.8c1.2-4.2 5.7-6.7 9.9-5.5L380 11.6c4.2 1.2 6.7 5.7 5.5 9.9L244.7 506.2c-1.2 4.3-5.6 6.7-9.9 5.5zm-83.2-121.1 27.2-29c3.1-3.3 2.8-8.5-.5-11.5L72.2 256l106.1-94.1c3.4-3 3.6-8.2.5-11.5l-27.2-29c-3-3.2-8.1-3.4-11.3-.4L2.5 250.2c-3.4 3.2-3.4 8.5 0 11.7L140.3 391c3.2 3 8.2 2.8 11.3-.4zm284.1.4 137.7-129.1c3.4-3.2 3.4-8.5 0-11.7L435.7 121c-3.2-3-8.3-2.9-11.3.4l-27.2 29c-3.1 3.3-2.8 8.5.5 11.5L503.8 256l-106.1 94.1c-3.4 3-3.6 8.2-.5 11.5l27.2 29c3.1 3.2 8.1 3.4 11.3.4z"/></svg>
|
Before Width: | Height: | Size: 601 B |
|
@ -0,0 +1 @@
|
|||
<svg height="512pt" viewBox="0 0 512 512" width="512pt" xmlns="http://www.w3.org/2000/svg"><path d="M197.332 170.668h-160C16.746 170.668 0 153.922 0 133.332v-96C0 16.746 16.746 0 37.332 0h160c20.59 0 37.336 16.746 37.336 37.332v96c0 20.59-16.746 37.336-37.336 37.336zM37.332 32A5.336 5.336 0 0 0 32 37.332v96a5.337 5.337 0 0 0 5.332 5.336h160a5.338 5.338 0 0 0 5.336-5.336v-96A5.337 5.337 0 0 0 197.332 32zM197.332 512h-160C16.746 512 0 495.254 0 474.668v-224c0-20.59 16.746-37.336 37.332-37.336h160c20.59 0 37.336 16.746 37.336 37.336v224c0 20.586-16.746 37.332-37.336 37.332zm-160-266.668A5.337 5.337 0 0 0 32 250.668v224A5.336 5.336 0 0 0 37.332 480h160a5.337 5.337 0 0 0 5.336-5.332v-224a5.338 5.338 0 0 0-5.336-5.336zM474.668 512h-160c-20.59 0-37.336-16.746-37.336-37.332v-96c0-20.59 16.746-37.336 37.336-37.336h160c20.586 0 37.332 16.746 37.332 37.336v96C512 495.254 495.254 512 474.668 512zm-160-138.668a5.338 5.338 0 0 0-5.336 5.336v96a5.337 5.337 0 0 0 5.336 5.332h160a5.336 5.336 0 0 0 5.332-5.332v-96a5.337 5.337 0 0 0-5.332-5.336zM474.668 298.668h-160c-20.59 0-37.336-16.746-37.336-37.336v-224C277.332 16.746 294.078 0 314.668 0h160C495.254 0 512 16.746 512 37.332v224c0 20.59-16.746 37.336-37.332 37.336zM314.668 32a5.337 5.337 0 0 0-5.336 5.332v224a5.338 5.338 0 0 0 5.336 5.336h160a5.337 5.337 0 0 0 5.332-5.336v-224A5.336 5.336 0 0 0 474.668 32zm0 0"/></svg>
|
After Width: | Height: | Size: 1.3 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M304 256c0 26.5-21.5 48-48 48s-48-21.5-48-48 21.5-48 48-48 48 21.5 48 48zm120-48c-26.5 0-48 21.5-48 48s21.5 48 48 48 48-21.5 48-48-21.5-48-48-48zm-336 0c-26.5 0-48 21.5-48 48s21.5 48 48 48 48-21.5 48-48-21.5-48-48-48z"/></svg>
|
Before Width: | Height: | Size: 297 B |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M270.2 160h35.5c3.4 0 6.1 2.8 6 6.2l-7.5 196c-.1 3.2-2.8 5.8-6 5.8h-20.5c-3.2 0-5.9-2.5-6-5.8l-7.5-196c-.1-3.4 2.6-6.2 6-6.2zM288 388c-15.5 0-28 12.5-28 28s12.5 28 28 28 28-12.5 28-28-12.5-28-28-28zm281.5 52L329.6 24c-18.4-32-64.7-32-83.2 0L6.5 440c-18.4 31.9 4.6 72 41.6 72H528c36.8 0 60-40 41.5-72zM528 480H48c-12.3 0-20-13.3-13.9-24l240-416c6.1-10.6 21.6-10.7 27.7 0l240 416c6.2 10.6-1.5 24-13.8 24z"/></svg>
|
Before Width: | Height: | Size: 482 B |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M497.6 0 334.4.17a14.4 14.4 0 0 0-14.4 14.4v33.31a14.4 14.4 0 0 0 14.69 14.4l73.63-2.72 2.06 2.06-278.86 278.87a12 12 0 0 0 0 17l23 23a12 12 0 0 0 17 0l278.86-278.87 2.06 2.06-2.72 73.63a14.4 14.4 0 0 0 14.4 14.69h33.31a14.4 14.4 0 0 0 14.4-14.4L512 14.4A14.4 14.4 0 0 0 497.6 0ZM432 288h-16a16 16 0 0 0-16 16v154a6 6 0 0 1-6 6H54a6 6 0 0 1-6-6V118a6 6 0 0 1 6-6h154a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16H48a48 48 0 0 0-48 48v352a48 48 0 0 0 48 48h352a48 48 0 0 0 48-48V304a16 16 0 0 0-16-16Z"/></svg>
|
Before Width: | Height: | Size: 573 B |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M48 368a48 48 0 1 0 48 48 48 48 0 0 0-48-48zm0-160a48 48 0 1 0 48 48 48 48 0 0 0-48-48zm0-160a48 48 0 1 0 48 48 48 48 0 0 0-48-48zm448 24H176a16 16 0 0 0-16 16v16a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16V88a16 16 0 0 0-16-16zm0 160H176a16 16 0 0 0-16 16v16a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-16a16 16 0 0 0-16-16zm0 160H176a16 16 0 0 0-16 16v16a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-16a16 16 0 0 0-16-16z"/></svg>
|
Before Width: | Height: | Size: 492 B |
|
@ -0,0 +1 @@
|
|||
<svg height="512" viewBox="0 0 24 24" width="512" xmlns="http://www.w3.org/2000/svg"><path d="M18.75 9H18V6c0-3.309-2.691-6-6-6S6 2.691 6 6v3h-.75A2.253 2.253 0 0 0 3 11.25v10.5C3 22.991 4.01 24 5.25 24h13.5c1.24 0 2.25-1.009 2.25-2.25v-10.5C21 10.009 19.99 9 18.75 9zM8 6c0-2.206 1.794-4 4-4s4 1.794 4 4v3H8zm5 10.722V19a1 1 0 1 1-2 0v-2.278c-.595-.347-1-.985-1-1.722 0-1.103.897-2 2-2s2 .897 2 2c0 .737-.405 1.375-1 1.722z"/></svg>
|
After Width: | Height: | Size: 433 B |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="m493.26 56.26-37.51-37.51C443.25 6.25 426.87 0 410.49 0s-32.76 6.25-45.25 18.74l-74.49 74.49L256 127.98 12.85 371.12.15 485.34C-1.45 499.72 9.88 512 23.95 512c.89 0 1.79-.05 2.69-.15l114.14-12.61L384.02 256l34.74-34.74 74.49-74.49c25-25 25-65.52.01-90.51zM118.75 453.39l-67.58 7.46 7.53-67.69 231.24-231.24 31.02-31.02 60.14 60.14-31.02 31.02-231.33 231.33zm340.56-340.57-44.28 44.28-60.13-60.14 44.28-44.28c4.08-4.08 8.84-4.69 11.31-4.69s7.24.61 11.31 4.69l37.51 37.51c6.24 6.25 6.24 16.4 0 22.63z"/></svg>
|
Before Width: | Height: | Size: 578 B |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M368 224H224V80c0-8.84-7.16-16-16-16h-32c-8.84 0-16 7.16-16 16v144H16c-8.84 0-16 7.16-16 16v32c0 8.84 7.16 16 16 16h144v144c0 8.84 7.16 16 16 16h32c8.84 0 16-7.16 16-16V288h144c8.84 0 16-7.16 16-16v-32c0-8.84-7.16-16-16-16z"/></svg>
|
Before Width: | Height: | Size: 303 B |
|
@ -0,0 +1 @@
|
|||
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M437.02 74.98C388.668 26.63 324.379 0 256 0S123.332 26.629 74.98 74.98C26.63 123.332 0 187.621 0 256s26.629 132.668 74.98 181.02C123.332 485.37 187.621 512 256 512s132.668-26.629 181.02-74.98C485.37 388.668 512 324.379 512 256s-26.629-132.668-74.98-181.02zM111.105 429.297c8.454-72.735 70.989-128.89 144.895-128.89 38.96 0 75.598 15.179 103.156 42.734 23.281 23.285 37.965 53.687 41.742 86.152C361.641 462.172 311.094 482 256 482s-105.637-19.824-144.895-52.703zM256 269.507c-42.871 0-77.754-34.882-77.754-77.753C178.246 148.879 213.13 114 256 114s77.754 34.879 77.754 77.754c0 42.871-34.883 77.754-77.754 77.754zm170.719 134.427a175.9 175.9 0 0 0-46.352-82.004c-18.437-18.438-40.25-32.27-64.039-40.938 28.598-19.394 47.426-52.16 47.426-89.238C363.754 132.34 315.414 84 256 84s-107.754 48.34-107.754 107.754c0 37.098 18.844 69.875 47.465 89.266-21.887 7.976-42.14 20.308-59.566 36.542-25.235 23.5-42.758 53.465-50.883 86.348C50.852 364.242 30 312.512 30 256 30 131.383 131.383 30 256 30s226 101.383 226 226c0 56.523-20.86 108.266-55.281 147.934zm0 0"/></svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 1400"><path d="M367.43 422.13a54.44 54.44 0 0 1-38.66-16L205 282.35A54.69 54.69 0 0 1 282.37 205l123.74 123.79a54.68 54.68 0 0 1-38.68 93.34ZM1156.3 1211a54.51 54.51 0 0 1-38.67-16l-123.74-123.79a54.68 54.68 0 1 1 77.34-77.33L1195 1117.65a54.7 54.7 0 0 1-38.7 93.35ZM243.7 1211a54.7 54.7 0 0 1-38.7-93.35l123.74-123.76a54.69 54.69 0 0 1 77.36 77.32L282.37 1195a54.51 54.51 0 0 1-38.67 16ZM1032.57 422.13a54.68 54.68 0 0 1-38.68-93.34L1117.61 205a54.69 54.69 0 0 1 77.39 77.35l-123.77 123.76a54.44 54.44 0 0 1-38.66 16.02ZM229.69 754.69h-175a54.69 54.69 0 0 1 0-109.38h175a54.69 54.69 0 0 1 0 109.38ZM1345.31 754.69h-175a54.69 54.69 0 0 1 0-109.38h175a54.69 54.69 0 0 1 0 109.38ZM700 1400a54.68 54.68 0 0 1-54.69-54.69v-175a54.69 54.69 0 0 1 109.38 0v175A54.68 54.68 0 0 1 700 1400ZM700 284.38a54.7 54.7 0 0 1-54.69-54.69v-175a54.69 54.69 0 0 1 109.38 0v175A54.7 54.7 0 0 1 700 284.38Z"/><circle cx="700" cy="700" r="306.25"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 1400"><path d="M367.43 422.13a54.44 54.44 0 0 1-38.66-16L205 282.35A54.69 54.69 0 0 1 282.37 205l123.74 123.79a54.68 54.68 0 0 1-38.68 93.34ZM1156.3 1211a54.51 54.51 0 0 1-38.67-16l-123.74-123.79a54.68 54.68 0 1 1 77.34-77.33L1195 1117.65a54.7 54.7 0 0 1-38.7 93.35Zm-912.6 0a54.7 54.7 0 0 1-38.7-93.35l123.74-123.76a54.69 54.69 0 0 1 77.36 77.32L282.37 1195a54.51 54.51 0 0 1-38.67 16Zm788.87-788.87a54.68 54.68 0 0 1-38.68-93.34L1117.61 205a54.69 54.69 0 0 1 77.39 77.35l-123.77 123.76a54.44 54.44 0 0 1-38.66 16.02ZM229.69 754.69h-175a54.69 54.69 0 0 1 0-109.38h175a54.69 54.69 0 0 1 0 109.38Zm1115.62 0h-175a54.69 54.69 0 0 1 0-109.38h175a54.69 54.69 0 0 1 0 109.38ZM700 1400a54.68 54.68 0 0 1-54.69-54.69v-175a54.69 54.69 0 0 1 109.38 0v175A54.68 54.68 0 0 1 700 1400Zm0-1115.62a54.7 54.7 0 0 1-54.69-54.69v-175a54.69 54.69 0 0 1 109.38 0v175A54.7 54.7 0 0 1 700 284.38Z"/><circle cx="700" cy="700" r="306.25"/></svg>
|
Before Width: | Height: | Size: 989 B After Width: | Height: | Size: 980 B |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="m207.6 256 107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"/></svg>
|
Before Width: | Height: | Size: 468 B |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M432 80h-82.4l-34-56.7A48 48 0 0 0 274.4 0H173.6a48 48 0 0 0-41.2 23.3L98.4 80H16A16 16 0 0 0 0 96v16a16 16 0 0 0 16 16h16l21.2 339a48 48 0 0 0 47.9 45h245.8a48 48 0 0 0 47.9-45L416 128h16a16 16 0 0 0 16-16V96a16 16 0 0 0-16-16zM173.6 48h100.8l19.2 32H154.4zm173.3 416H101.11l-21-336h287.8z"/></svg>
|
Before Width: | Height: | Size: 370 B |
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1652 1652"><g data-name="Layer 2"><path d="M1587.07 504.47A828.56 828.56 0 1 0 1652 826a823.13 823.13 0 0 0-64.93-321.53ZM826 1577a747.29 747.29 0 0 1-464.48-161.26 39.94 39.94 0 0 0 2.8-11.35 458.82 458.82 0 0 1 34.29-135.74 464.15 464.15 0 0 1 854.78 0 458.82 458.82 0 0 1 34.29 135.74 39.94 39.94 0 0 0 2.8 11.35A747.29 747.29 0 0 1 826 1577ZM719.81 866.57A274 274 0 1 1 826 888a272.1 272.1 0 0 1-106.19-21.43Zm641.28 485.87c-36.11-201.1-182.78-363.82-374.86-423 114.28-58.37 192.53-177.22 192.53-314.35 0-194.83-157.94-352.76-352.76-352.76S473.24 420.29 473.24 615.12c0 137.13 78.25 256 192.53 314.35-192.08 59.15-338.75 221.87-374.86 423C157.46 1216.81 75 1030.86 75 826 75 411.9 411.9 75 826 75s751 336.9 751 751c0 204.86-82.46 390.81-215.91 526.44Z" data-name="Layer 1"/></g></svg>
|
||||
<svg height="512pt" viewBox="-56 0 512 512" width="512pt" xmlns="http://www.w3.org/2000/svg"><path d="M267 236.375c36.254-22.582 60.434-62.797 60.434-108.563C327.434 57.337 270.098 0 199.62 0 129.145 0 71.81 57.336 71.81 127.813c0 45.765 24.18 85.976 60.43 108.558C55.222 264.071 0 337.84 0 424.273v72.243C0 505.066 6.934 512 15.484 512H383.75c8.55 0 15.48-6.934 15.48-15.484v-72.243c0-86.43-55.218-160.195-132.23-187.898zm101.266 244.656H30.969v-56.758c0-92.992 75.652-168.644 168.648-168.644 92.992 0 168.649 75.652 168.649 168.644zm-71.801-353.219c0 53.403-43.442 96.848-96.844 96.848s-96.844-43.445-96.844-96.847c0-53.399 43.442-96.844 96.844-96.844s96.844 43.445 96.844 96.844zm0 0"/></svg>
|
Before Width: | Height: | Size: 841 B After Width: | Height: | Size: 695 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 477.869 477.869" style="enable-background:new 0 0 477.869 477.869" xml:space="preserve"><path d="M387.415 233.496c48.976-44.029 52.987-119.424 8.958-168.4C355.991 20.177 288.4 12.546 239.02 47.332c-53.83-37.99-128.264-25.149-166.254 28.68-34.859 49.393-27.259 117.054 17.689 157.483C34.606 262.935-.251 320.976.002 384.108v51.2c0 9.426 7.641 17.067 17.067 17.067h443.733c9.426 0 17.067-7.641 17.067-17.067v-51.2c.252-63.132-34.605-121.173-90.454-150.612zM307.201 59.842c47.062-.052 85.256 38.057 85.309 85.119.037 33.564-19.631 64.023-50.237 77.799-1.314.597-2.628 1.143-3.959 1.707a83.66 83.66 0 0 1-12.988 4.045c-.853.188-1.707.29-2.577.461a85.366 85.366 0 0 1-15.019 1.519c-2.27 0-4.557-.171-6.827-.375-.853 0-1.707 0-2.56-.171a86.219 86.219 0 0 1-27.904-8.226c-.324-.154-.7-.137-1.024-.273-1.707-.819-3.413-1.536-4.932-2.458.137-.171.222-.358.358-.529a119.721 119.721 0 0 0 18.278-33.297l.529-1.434a120.381 120.381 0 0 0 4.523-17.562c.154-.87.273-1.707.41-2.645.987-6.067 1.506-12.2 1.553-18.347a120.041 120.041 0 0 0-1.553-18.313c-.137-.887-.256-1.707-.41-2.645a120.414 120.414 0 0 0-4.523-17.562l-.529-1.434a119.747 119.747 0 0 0-18.278-33.297c-.137-.171-.222-.358-.358-.529a84.787 84.787 0 0 1 42.718-11.553zM85.335 145.176c-.121-47.006 37.886-85.21 84.892-85.331a85.112 85.112 0 0 1 59.134 23.686c.99.956 1.963 1.911 2.918 2.901a87.748 87.748 0 0 1 8.09 9.813c.751 1.058 1.434 2.185 2.133 3.277a83.951 83.951 0 0 1 6.263 11.52c.427.973.751 1.963 1.126 2.935a83.422 83.422 0 0 1 4.233 13.653c.12.512.154 1.024.256 1.553a80.338 80.338 0 0 1 0 32.119c-.102.529-.137 1.041-.256 1.553a83.228 83.228 0 0 1-4.233 13.653c-.375.973-.7 1.963-1.126 2.935a84.251 84.251 0 0 1-6.263 11.503c-.7 1.092-1.382 2.219-2.133 3.277a87.549 87.549 0 0 1-8.09 9.813c-.956.99-1.929 1.946-2.918 2.901a85.187 85.187 0 0 1-23.569 15.906 49.35 49.35 0 0 1-4.198 1.707 85.839 85.839 0 0 1-12.663 3.925c-1.075.239-2.185.375-3.277.563a84.67 84.67 0 0 1-14.046 1.417h-1.877a84.563 84.563 0 0 1-14.046-1.417c-1.092-.188-2.202-.324-3.277-.563a85.802 85.802 0 0 1-12.663-3.925c-1.417-.563-2.816-1.143-4.198-1.707-30.534-13.786-50.173-44.166-50.212-77.667zm221.866 273.066H34.135v-34.133c-.25-57.833 36.188-109.468 90.76-128.614a119.092 119.092 0 0 0 91.546 0 137.138 137.138 0 0 1 16.623 7.356c3.55 1.826 6.827 3.908 10.24 6.007 2.219 1.382 4.471 2.731 6.605 4.25 3.294 2.338 6.4 4.881 9.455 7.492 1.963 1.707 3.908 3.413 5.751 5.12 2.816 2.662 5.461 5.478 8.004 8.363a134.465 134.465 0 0 1 5.291 6.383 132.594 132.594 0 0 1 6.349 8.823c1.707 2.56 3.226 5.222 4.727 7.885 1.707 2.935 3.277 5.871 4.71 8.926 1.434 3.055 2.697 6.4 3.925 9.66 1.075 2.833 2.219 5.649 3.106 8.533 1.195 3.959 2.031 8.055 2.867 12.151.512 2.423 1.178 4.796 1.553 7.253a141.153 141.153 0 0 1 1.553 20.412v34.133zm136.534 0h-102.4v-34.133c0-5.342-.307-10.633-.785-15.872-.137-1.536-.375-3.055-.546-4.591-.461-3.772-.99-7.509-1.707-11.213a246.936 246.936 0 0 0-.973-4.762c-.819-3.8-1.769-7.566-2.85-11.298-.358-1.229-.683-2.475-1.058-3.686a169.105 169.105 0 0 0-20.565-43.127l-.666-.973a168.958 168.958 0 0 0-9.404-12.646l-.119-.154a154.895 154.895 0 0 0-11.008-12.237h.7a120.8 120.8 0 0 0 14.524 1.024h.939c4.496-.039 8.985-.33 13.449-.87 1.399-.171 2.782-.427 4.181-.649a117.43 117.43 0 0 0 10.752-2.167c1.007-.256 2.031-.495 3.055-.785a116.211 116.211 0 0 0 13.653-4.642c54.612 19.127 91.083 70.785 90.829 128.649v34.132z"/></svg>
|
After Width: | Height: | Size: 3.4 KiB |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 511.999 511.999" style="enable-background:new 0 0 511.999 511.999" xml:space="preserve"><path d="M437.019 74.981C388.667 26.628 324.38 0 256 0 187.62 0 123.332 26.628 74.981 74.98 26.628 123.332 0 187.62 0 256s26.628 132.667 74.981 181.019c48.351 48.352 112.639 74.98 181.019 74.98 68.381 0 132.667-26.628 181.02-74.981C485.371 388.667 512 324.379 512 255.999s-26.629-132.667-74.981-181.018zM96.216 96.216c22.511-22.511 48.938-39.681 77.742-50.888-7.672 9.578-14.851 20.587-21.43 32.969-7.641 14.38-14.234 30.173-19.725 47.042-19.022-3.157-36.647-7.039-52.393-11.595a230.423 230.423 0 0 1 15.806-17.528zm-33.987 43.369c18.417 5.897 39.479 10.87 62.461 14.809-6.4 27.166-10.167 56.399-11.066 86.591H30.536c2.36-36.233 13.242-70.813 31.693-101.4zm-1.635 230.053c-17.455-29.899-27.769-63.481-30.059-98.623h83.146c.982 29.329 4.674 57.731 10.858 84.186-23.454 3.802-45.045 8.649-63.945 14.437zm35.622 46.146a229.917 229.917 0 0 1-17.831-20.055c16.323-4.526 34.571-8.359 54.214-11.433 5.53 17.103 12.194 33.105 19.928 47.662 7.17 13.493 15.053 25.349 23.51 35.505-29.61-11.183-56.769-28.629-79.821-51.679zm144.768 62.331c-22.808-6.389-44.384-27.217-61.936-60.249-6.139-11.552-11.531-24.155-16.15-37.587 24.73-2.722 51.045-4.331 78.086-4.709v102.545zm0-132.578c-29.988.409-59.217 2.292-86.59 5.507-6.038-24.961-9.671-51.978-10.668-80.028h97.259v74.521zm0-104.553h-97.315c.911-28.834 4.602-56.605 10.828-82.201 27.198 3.4 56.366 5.468 86.487 6.06v76.141zm0-106.176c-27.146-.547-53.403-2.317-77.958-5.205 4.591-13.292 9.941-25.768 16.022-37.215 17.551-33.032 39.128-53.86 61.936-60.249v102.669zm209.733 6.372c17.874 30.193 28.427 64.199 30.749 99.804h-83.088c-.889-29.844-4.584-58.749-10.85-85.647 23.133-3.736 44.456-8.489 63.189-14.157zm-34.934-44.964a230.122 230.122 0 0 1 16.914 18.91c-16.073 4.389-33.972 8.114-53.204 11.112-5.548-17.208-12.243-33.305-20.02-47.941-6.579-12.382-13.758-23.391-21.43-32.969 28.802 11.207 55.23 28.377 77.74 50.888zm-144.767 174.8h97.259c-1.004 28.268-4.686 55.49-10.81 80.612-27.194-3.381-56.349-5.43-86.449-6.006v-74.606zm0-30.032v-76.041c30.005-.394 59.257-2.261 86.656-5.464 6.125 25.403 9.756 52.932 10.659 81.505h-97.315zm-.002-208.845h.001c22.808 6.389 44.384 27.217 61.936 60.249 6.178 11.627 11.601 24.318 16.24 37.848-24.763 2.712-51.108 4.309-78.177 4.674V32.139zm.002 445.976V375.657c27.12.532 53.357 2.286 77.903 5.156-4.579 13.232-9.911 25.654-15.967 37.053-17.552 33.032-39.128 53.86-61.936 60.249zm144.767-62.331c-23.051 23.051-50.21 40.496-79.821 51.678 8.457-10.156 16.34-22.011 23.51-35.504 7.62-14.341 14.198-30.088 19.68-46.906 19.465 3.213 37.473 7.186 53.515 11.859a230.268 230.268 0 0 1-16.884 18.873zm34.823-44.775c-18.635-5.991-40-11.032-63.326-15.01 6.296-26.68 10.048-55.36 11.041-84.983h83.146c-2.328 35.678-12.918 69.753-30.861 99.993z"/></svg>
|
After Width: | Height: | Size: 2.8 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M312.973 375.032c9.369 9.369 9.369 24.572 0 33.941s-24.572 9.369-33.941 0L160 289.941 40.968 408.973c-9.369 9.369-24.572 9.369-33.941 0s-9.369-24.572 0-33.941L126.059 256 7.027 136.968c-9.369-9.369-9.369-24.572 0-33.941s24.572-9.369 33.941 0L160 222.059l119.032-119.032c9.369-9.369 24.572-9.369 33.941 0s9.369 24.572 0 33.941L193.941 256l119.032 119.032Z"/></svg>
|
Before Width: | Height: | Size: 434 B |
|
@ -1,63 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import classNames from 'classnames';
|
||||
import Icon from './Icon';
|
||||
import styles from './Button.module.css';
|
||||
|
||||
function Button({
|
||||
type = 'button',
|
||||
icon,
|
||||
size,
|
||||
variant,
|
||||
children,
|
||||
className,
|
||||
tooltip,
|
||||
tooltipId,
|
||||
disabled,
|
||||
iconRight,
|
||||
onClick = () => {},
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
data-tip={tooltip}
|
||||
data-effect="solid"
|
||||
data-for={tooltipId}
|
||||
data-offset={JSON.stringify({ left: 10 })}
|
||||
type={type}
|
||||
className={classNames(styles.button, className, {
|
||||
[styles.large]: size === 'large',
|
||||
[styles.small]: size === 'small',
|
||||
[styles.xsmall]: size === 'xsmall',
|
||||
[styles.action]: variant === 'action',
|
||||
[styles.danger]: variant === 'danger',
|
||||
[styles.light]: variant === 'light',
|
||||
[styles.iconRight]: iconRight,
|
||||
})}
|
||||
disabled={disabled}
|
||||
onClick={!disabled ? onClick : null}
|
||||
{...props}
|
||||
>
|
||||
{icon && <Icon className={styles.icon} icon={icon} size={size} />}
|
||||
{children && <div className={styles.label}>{children}</div>}
|
||||
{tooltip && <ReactTooltip id={tooltipId}>{tooltip}</ReactTooltip>}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
Button.propTypes = {
|
||||
type: PropTypes.oneOf(['button', 'submit', 'reset']),
|
||||
icon: PropTypes.node,
|
||||
size: PropTypes.oneOf(['xlarge', 'large', 'medium', 'small', 'xsmall']),
|
||||
variant: PropTypes.oneOf(['action', 'danger', 'light']),
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
tooltip: PropTypes.node,
|
||||
tooltipId: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
iconRight: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Button;
|
|
@ -1,102 +0,0 @@
|
|||
.button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-md);
|
||||
color: var(--base900);
|
||||
background: var(--base100);
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
border: 0;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: var(--base200);
|
||||
}
|
||||
|
||||
.button:active {
|
||||
color: var(--base900);
|
||||
}
|
||||
|
||||
.label {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.large {
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.xsmall {
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.action,
|
||||
.action:active {
|
||||
color: var(--base50);
|
||||
background: var(--base900);
|
||||
}
|
||||
|
||||
.action:hover {
|
||||
background: var(--base800);
|
||||
}
|
||||
|
||||
.danger,
|
||||
.danger:active {
|
||||
color: var(--base50);
|
||||
background: var(--red500);
|
||||
}
|
||||
|
||||
.danger:hover {
|
||||
background: var(--red400);
|
||||
}
|
||||
|
||||
.light,
|
||||
.light:active {
|
||||
color: var(--base900);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.light:hover {
|
||||
background: inherit;
|
||||
}
|
||||
|
||||
.button .icon + * {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.button.iconRight .icon {
|
||||
order: 1;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.button.iconRight .icon + * {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
cursor: default;
|
||||
color: var(--base500);
|
||||
background: var(--base75);
|
||||
}
|
||||
|
||||
.button:disabled:active {
|
||||
color: var(--base500);
|
||||
}
|
||||
|
||||
.button:disabled:hover {
|
||||
background: var(--base75);
|
||||
}
|
||||
|
||||
.button.light:disabled {
|
||||
background: var(--base50);
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import Button from './Button';
|
||||
import styles from './ButtonGroup.module.css';
|
||||
|
||||
function ButtonGroup({ items = [], selectedItem, className, size, icon, onClick = () => {} }) {
|
||||
return (
|
||||
<div className={classNames(styles.group, className)}>
|
||||
{items.map(item => {
|
||||
const { label, value } = item;
|
||||
return (
|
||||
<Button
|
||||
key={value}
|
||||
className={classNames(styles.button, { [styles.selected]: selectedItem === value })}
|
||||
size={size}
|
||||
icon={icon}
|
||||
onClick={() => onClick(value)}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ButtonGroup.propTypes = {
|
||||
items: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
label: PropTypes.node,
|
||||
value: PropTypes.any.isRequired,
|
||||
}),
|
||||
),
|
||||
selectedItem: PropTypes.any,
|
||||
className: PropTypes.string,
|
||||
size: PropTypes.oneOf(['xlarge', 'large', 'medium', 'small', 'xsmall']),
|
||||
icon: PropTypes.node,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ButtonGroup;
|
|
@ -1,31 +0,0 @@
|
|||
.group {
|
||||
display: inline-flex;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--base500);
|
||||
}
|
||||
|
||||
.group .button {
|
||||
border-radius: 0;
|
||||
color: var(--base800);
|
||||
background: var(--base50);
|
||||
border-left: 1px solid var(--base500);
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.group .button:first-child {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.group .button:hover {
|
||||
background: var(--base100);
|
||||
}
|
||||
|
||||
.group .button + .button {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.group .button.selected {
|
||||
color: var(--base900);
|
||||
font-weight: 600;
|
||||
}
|
|
@ -1,273 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
startOfWeek,
|
||||
startOfMonth,
|
||||
startOfYear,
|
||||
endOfMonth,
|
||||
addDays,
|
||||
subDays,
|
||||
addYears,
|
||||
subYears,
|
||||
addMonths,
|
||||
setMonth,
|
||||
setYear,
|
||||
isSameDay,
|
||||
isBefore,
|
||||
isAfter,
|
||||
} from 'date-fns';
|
||||
import Button from './Button';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import { dateFormat } from 'lib/date';
|
||||
import { chunk } from 'lib/array';
|
||||
import { getDateLocale } from 'lib/lang';
|
||||
import Chevron from 'assets/chevron-down.svg';
|
||||
import Cross from 'assets/times.svg';
|
||||
import styles from './Calendar.module.css';
|
||||
import Icon from './Icon';
|
||||
|
||||
export default function Calendar({ date, minDate, maxDate, onChange }) {
|
||||
const { locale } = useLocale();
|
||||
const [selectMonth, setSelectMonth] = useState(false);
|
||||
const [selectYear, setSelectYear] = useState(false);
|
||||
|
||||
const month = dateFormat(date, 'MMMM', locale);
|
||||
const year = date.getFullYear();
|
||||
|
||||
function toggleMonthSelect() {
|
||||
setSelectYear(false);
|
||||
setSelectMonth(state => !state);
|
||||
}
|
||||
|
||||
function toggleYearSelect() {
|
||||
setSelectMonth(false);
|
||||
setSelectYear(state => !state);
|
||||
}
|
||||
|
||||
function handleChange(value) {
|
||||
setSelectMonth(false);
|
||||
setSelectYear(false);
|
||||
if (value) {
|
||||
onChange(value);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.calendar}>
|
||||
<div className={styles.header}>
|
||||
<div>{date.getDate()}</div>
|
||||
<div
|
||||
className={classNames(styles.selector, { [styles.open]: selectMonth })}
|
||||
onClick={toggleMonthSelect}
|
||||
>
|
||||
{month}
|
||||
<Icon className={styles.icon} icon={selectMonth ? <Cross /> : <Chevron />} size="small" />
|
||||
</div>
|
||||
<div
|
||||
className={classNames(styles.selector, { [styles.open]: selectYear })}
|
||||
onClick={toggleYearSelect}
|
||||
>
|
||||
{year}
|
||||
<Icon className={styles.icon} icon={selectYear ? <Cross /> : <Chevron />} size="small" />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
{!selectMonth && !selectYear && (
|
||||
<DaySelector
|
||||
date={date}
|
||||
minDate={minDate}
|
||||
maxDate={maxDate}
|
||||
locale={locale}
|
||||
onSelect={handleChange}
|
||||
/>
|
||||
)}
|
||||
{selectMonth && (
|
||||
<MonthSelector
|
||||
date={date}
|
||||
minDate={minDate}
|
||||
maxDate={maxDate}
|
||||
locale={locale}
|
||||
onSelect={handleChange}
|
||||
onClose={toggleMonthSelect}
|
||||
/>
|
||||
)}
|
||||
{selectYear && (
|
||||
<YearSelector
|
||||
date={date}
|
||||
minDate={minDate}
|
||||
maxDate={maxDate}
|
||||
onSelect={handleChange}
|
||||
onClose={toggleYearSelect}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const DaySelector = ({ date, minDate, maxDate, locale, onSelect }) => {
|
||||
const dateLocale = getDateLocale(locale);
|
||||
const weekStartsOn = dateLocale?.options?.weekStartsOn || 0;
|
||||
const startWeek = startOfWeek(date, {
|
||||
locale: dateLocale,
|
||||
weekStartsOn,
|
||||
});
|
||||
const startMonth = startOfMonth(date);
|
||||
const startDay = subDays(startMonth, startMonth.getDay() - weekStartsOn);
|
||||
const month = date.getMonth();
|
||||
const year = date.getFullYear();
|
||||
|
||||
const daysOfWeek = [];
|
||||
for (let i = 0; i < 7; i++) {
|
||||
daysOfWeek.push(addDays(startWeek, i));
|
||||
}
|
||||
|
||||
const days = [];
|
||||
for (let i = 0; i < 35; i++) {
|
||||
days.push(addDays(startDay, i));
|
||||
}
|
||||
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{daysOfWeek.map((day, i) => (
|
||||
<th key={i} className={locale}>
|
||||
{dateFormat(day, 'EEE', locale)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{chunk(days, 7).map((week, i) => (
|
||||
<tr key={i}>
|
||||
{week.map((day, j) => {
|
||||
const disabled = isBefore(day, minDate) || isAfter(day, maxDate);
|
||||
return (
|
||||
<td
|
||||
key={j}
|
||||
className={classNames({
|
||||
[styles.selected]: isSameDay(date, day),
|
||||
[styles.faded]: day.getMonth() !== month || day.getFullYear() !== year,
|
||||
[styles.disabled]: disabled,
|
||||
})}
|
||||
onClick={!disabled ? () => onSelect(day) : null}
|
||||
>
|
||||
{day.getDate()}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
const MonthSelector = ({ date, minDate, maxDate, locale, onSelect }) => {
|
||||
const start = startOfYear(date);
|
||||
const months = [];
|
||||
for (let i = 0; i < 12; i++) {
|
||||
months.push(addMonths(start, i));
|
||||
}
|
||||
|
||||
function handleSelect(value) {
|
||||
onSelect(setMonth(date, value));
|
||||
}
|
||||
|
||||
return (
|
||||
<table>
|
||||
<tbody>
|
||||
{chunk(months, 3).map((row, i) => (
|
||||
<tr key={i}>
|
||||
{row.map((month, j) => {
|
||||
const disabled =
|
||||
isBefore(endOfMonth(month), minDate) || isAfter(startOfMonth(month), maxDate);
|
||||
return (
|
||||
<td
|
||||
key={j}
|
||||
className={classNames(locale, {
|
||||
[styles.selected]: month.getMonth() === date.getMonth(),
|
||||
[styles.disabled]: disabled,
|
||||
})}
|
||||
onClick={!disabled ? () => handleSelect(month.getMonth()) : null}
|
||||
>
|
||||
{dateFormat(month, 'MMMM', locale)}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
const YearSelector = ({ date, minDate, maxDate, onSelect }) => {
|
||||
const [currentDate, setCurrentDate] = useState(date);
|
||||
const year = date.getFullYear();
|
||||
const currentYear = currentDate.getFullYear();
|
||||
const minYear = minDate.getFullYear();
|
||||
const maxYear = maxDate.getFullYear();
|
||||
const years = [];
|
||||
for (let i = 0; i < 15; i++) {
|
||||
years.push(currentYear - 7 + i);
|
||||
}
|
||||
|
||||
function handleSelect(value) {
|
||||
onSelect(setYear(date, value));
|
||||
}
|
||||
|
||||
function handlePrevClick() {
|
||||
setCurrentDate(state => subYears(state, 15));
|
||||
}
|
||||
|
||||
function handleNextClick() {
|
||||
setCurrentDate(state => addYears(state, 15));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.pager}>
|
||||
<div className={styles.left}>
|
||||
<Button
|
||||
icon={<Chevron />}
|
||||
size="small"
|
||||
onClick={handlePrevClick}
|
||||
disabled={years[0] <= minYear}
|
||||
variant="light"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.middle}>
|
||||
<table>
|
||||
<tbody>
|
||||
{chunk(years, 5).map((row, i) => (
|
||||
<tr key={i}>
|
||||
{row.map((n, j) => (
|
||||
<td
|
||||
key={j}
|
||||
className={classNames({
|
||||
[styles.selected]: n === year,
|
||||
[styles.disabled]: n < minYear || n > maxYear,
|
||||
})}
|
||||
onClick={() => (n < minYear || n > maxYear ? null : handleSelect(n))}
|
||||
>
|
||||
{n}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className={styles.right}>
|
||||
<Button
|
||||
icon={<Chevron />}
|
||||
size="small"
|
||||
onClick={handleNextClick}
|
||||
disabled={years[years.length - 1] > maxYear}
|
||||
variant="light"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,111 +0,0 @@
|
|||
.calendar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: var(--font-size-sm);
|
||||
flex: 1;
|
||||
min-height: 306px;
|
||||
}
|
||||
|
||||
.calendar table {
|
||||
width: 100%;
|
||||
border-spacing: 5px;
|
||||
}
|
||||
|
||||
.calendar td {
|
||||
color: var(--base800);
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
vertical-align: center;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.calendar td:hover {
|
||||
border: 1px solid var(--base300);
|
||||
background: var(--base75);
|
||||
}
|
||||
|
||||
.calendar td.faded {
|
||||
color: var(--base500);
|
||||
}
|
||||
|
||||
.calendar td.selected {
|
||||
font-weight: 600;
|
||||
border: 1px solid var(--base600);
|
||||
}
|
||||
|
||||
.calendar td.selected:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.calendar td.disabled {
|
||||
color: var(--base400);
|
||||
background: var(--base75);
|
||||
}
|
||||
|
||||
.calendar td.disabled:hover {
|
||||
cursor: default;
|
||||
background: var(--base75);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.calendar td.faded.disabled {
|
||||
background: var(--base100);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
font-weight: 700;
|
||||
line-height: 40px;
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.selector {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pager {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.pager button {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.middle {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.left,
|
||||
.right {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.left svg {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.right svg {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
.calendar table {
|
||||
max-width: calc(100vw - 30px);
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
import React, { useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Icon from 'components/common/Icon';
|
||||
import Check from 'assets/check.svg';
|
||||
import styles from './Checkbox.module.css';
|
||||
|
||||
function Checkbox({ name, value, label, onChange }) {
|
||||
const ref = useRef();
|
||||
|
||||
const onClick = () => ref.current.click();
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.checkbox} onClick={onClick}>
|
||||
{value && <Icon icon={<Check />} size="small" />}
|
||||
</div>
|
||||
<label className={styles.label} htmlFor={name} onClick={onClick}>
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
ref={ref}
|
||||
className={styles.input}
|
||||
type="checkbox"
|
||||
name={name}
|
||||
defaultChecked={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Checkbox.propTypes = {
|
||||
name: PropTypes.string,
|
||||
value: PropTypes.any,
|
||||
label: PropTypes.node,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Checkbox;
|
|
@ -1,30 +0,0 @@
|
|||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 1px solid var(--base500);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-left: 10px;
|
||||
user-select: none; /* disable text selection when clicking to toggle the checkbox */
|
||||
}
|
||||
|
||||
.input {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
height: 0;
|
||||
width: 0;
|
||||
bottom: 100%;
|
||||
right: 100%;
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Button from './Button';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
const defaultText = (
|
||||
<FormattedMessage id="label.copy-to-clipboard" defaultMessage="Copy to clipboard" />
|
||||
);
|
||||
|
||||
function CopyButton({ element, ...props }) {
|
||||
const [text, setText] = useState(defaultText);
|
||||
|
||||
function handleClick() {
|
||||
if (element?.current) {
|
||||
element.current.select();
|
||||
document.execCommand('copy');
|
||||
setText(<FormattedMessage id="message.copied" defaultMessage="Copied!" />);
|
||||
window.getSelection().removeAllRanges();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Button {...props} onClick={handleClick}>
|
||||
{text}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
CopyButton.propTypes = {
|
||||
element: PropTypes.shape({
|
||||
current: PropTypes.shape({
|
||||
select: PropTypes.func.isRequired,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
export default CopyButton;
|
|
@ -1,138 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { endOfYear, isSameDay } from 'date-fns';
|
||||
import Modal from './Modal';
|
||||
import DropDown from './DropDown';
|
||||
import DatePickerForm from 'components/forms/DatePickerForm';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import { dateFormat } from 'lib/date';
|
||||
import Calendar from 'assets/calendar-alt.svg';
|
||||
import Icon from './Icon';
|
||||
|
||||
export const filterOptions = [
|
||||
{ label: <FormattedMessage id="label.today" defaultMessage="Today" />, value: '1day' },
|
||||
{
|
||||
label: (
|
||||
<FormattedMessage id="label.last-hours" defaultMessage="Last {x} hours" values={{ x: 24 }} />
|
||||
),
|
||||
value: '24hour',
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="label.yesterday" defaultMessage="Yesterday" />,
|
||||
value: '-1day',
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="label.this-week" defaultMessage="This week" />,
|
||||
value: '1week',
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<FormattedMessage id="label.last-days" defaultMessage="Last {x} days" values={{ x: 7 }} />
|
||||
),
|
||||
value: '7day',
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="label.this-month" defaultMessage="This month" />,
|
||||
value: '1month',
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<FormattedMessage id="label.last-days" defaultMessage="Last {x} days" values={{ x: 30 }} />
|
||||
),
|
||||
value: '30day',
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<FormattedMessage id="label.last-days" defaultMessage="Last {x} days" values={{ x: 90 }} />
|
||||
),
|
||||
value: '90day',
|
||||
},
|
||||
{ label: <FormattedMessage id="label.this-year" defaultMessage="This year" />, value: '1year' },
|
||||
{
|
||||
label: <FormattedMessage id="label.all-time" defaultMessage="All time" />,
|
||||
value: 'all',
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="label.custom-range" defaultMessage="Custom range" />,
|
||||
value: 'custom',
|
||||
divider: true,
|
||||
},
|
||||
];
|
||||
|
||||
function DateFilter({ value, startDate, endDate, onChange, className, options }) {
|
||||
const [showPicker, setShowPicker] = useState(false);
|
||||
const displayValue =
|
||||
value === 'custom' ? (
|
||||
<CustomRange startDate={startDate} endDate={endDate} onClick={() => handleChange('custom')} />
|
||||
) : (
|
||||
value
|
||||
);
|
||||
|
||||
async function handleChange(value) {
|
||||
if (value === 'custom') {
|
||||
setShowPicker(true);
|
||||
return;
|
||||
}
|
||||
onChange(value);
|
||||
}
|
||||
|
||||
function handlePickerChange(value) {
|
||||
setShowPicker(false);
|
||||
onChange(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropDown
|
||||
className={className}
|
||||
value={displayValue}
|
||||
options={options || filterOptions}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{showPicker && (
|
||||
<Modal>
|
||||
<DatePickerForm
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
minDate={new Date(2000, 0, 1)}
|
||||
maxDate={endOfYear(new Date())}
|
||||
onChange={handlePickerChange}
|
||||
onClose={() => setShowPicker(false)}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const CustomRange = ({ startDate, endDate, onClick }) => {
|
||||
const { locale } = useLocale();
|
||||
|
||||
function handleClick(e) {
|
||||
e.stopPropagation();
|
||||
|
||||
onClick();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Icon icon={<Calendar />} className="mr-2" onClick={handleClick} />
|
||||
{dateFormat(startDate, 'd LLL y', locale)}
|
||||
{!isSameDay(startDate, endDate) && ` — ${dateFormat(endDate, 'd LLL y', locale)}`}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
DateFilter.propTypes = {
|
||||
value: PropTypes.string,
|
||||
startDate: PropTypes.instanceOf(Date),
|
||||
endDate: PropTypes.instanceOf(Date),
|
||||
onChange: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default DateFilter;
|
|
@ -1,26 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import styles from './Dot.module.css';
|
||||
|
||||
function Dot({ color, size, className }) {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div
|
||||
style={{ background: color }}
|
||||
className={classNames(styles.dot, className, {
|
||||
[styles.small]: size === 'small',
|
||||
[styles.large]: size === 'large',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Dot.propTypes = {
|
||||
color: PropTypes.string,
|
||||
size: PropTypes.oneOf(['small', 'large']),
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Dot;
|
|
@ -1,22 +0,0 @@
|
|||
.wrapper {
|
||||
background: var(--base50);
|
||||
margin-right: 10px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.dot {
|
||||
background: var(--green400);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.dot.small {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.dot.large {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
import React, { useState, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import Menu from './Menu';
|
||||
import useDocumentClick from 'hooks/useDocumentClick';
|
||||
import Chevron from 'assets/chevron-down.svg';
|
||||
import styles from './Dropdown.module.css';
|
||||
import Icon from './Icon';
|
||||
|
||||
function DropDown({ value, className, menuClassName, options = [], onChange = () => {} }) {
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const ref = useRef();
|
||||
const selectedOption = options.find(e => e.value === value);
|
||||
|
||||
function handleShowMenu() {
|
||||
setShowMenu(state => !state);
|
||||
}
|
||||
|
||||
function handleSelect(selected, e) {
|
||||
e.stopPropagation();
|
||||
setShowMenu(false);
|
||||
|
||||
onChange(selected);
|
||||
}
|
||||
|
||||
useDocumentClick(e => {
|
||||
if (!ref.current?.contains(e.target)) {
|
||||
setShowMenu(false);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={ref} className={classNames(styles.dropdown, className)} onClick={handleShowMenu}>
|
||||
<div className={styles.value}>
|
||||
<div className={styles.text}>{options.find(e => e.value === value)?.label || value}</div>
|
||||
<Icon icon={<Chevron />} className={styles.icon} size="small" />
|
||||
</div>
|
||||
{showMenu && (
|
||||
<Menu
|
||||
className={menuClassName}
|
||||
options={options}
|
||||
selectedOption={selectedOption}
|
||||
onSelect={handleSelect}
|
||||
float="bottom"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
DropDown.propTypes = {
|
||||
value: PropTypes.any,
|
||||
className: PropTypes.string,
|
||||
menuClassName: PropTypes.string,
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
value: PropTypes.any.isRequired,
|
||||
label: PropTypes.node,
|
||||
}),
|
||||
),
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
export default DropDown;
|
|
@ -1,28 +0,0 @@
|
|||
.dropdown {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border: 1px solid var(--base500);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.value {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: var(--font-size-sm);
|
||||
flex-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
padding: 4px 16px;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.icon {
|
||||
padding-left: 20px;
|
||||
}
|
|
@ -1,22 +1,16 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Icon from 'components/common/Icon';
|
||||
import { Icon, Text, Flexbox } from 'react-basics';
|
||||
import Logo from 'assets/logo.svg';
|
||||
import styles from './EmptyPlaceholder.module.css';
|
||||
|
||||
function EmptyPlaceholder({ msg, children }) {
|
||||
export function EmptyPlaceholder({ message, children }) {
|
||||
return (
|
||||
<div className={styles.placeholder}>
|
||||
<Icon className={styles.icon} icon={<Logo />} size="xlarge" />
|
||||
<h2 className={styles.msg}>{msg}</h2>
|
||||
{children}
|
||||
</div>
|
||||
<Flexbox direction="column" alignItems="center" justifyContent="center" gap={60} height={600}>
|
||||
<Icon size="xl">
|
||||
<Logo />
|
||||
</Icon>
|
||||
<Text size="lg">{message}</Text>
|
||||
<div>{children}</div>
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
|
||||
EmptyPlaceholder.propTypes = {
|
||||
msg: PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default EmptyPlaceholder;
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
.placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.msg {
|
||||
margin-bottom: 15px;
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/* eslint-disable no-console */
|
||||
import { ErrorBoundary as Boundary } from 'react-error-boundary';
|
||||
import { Button } from 'react-basics';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import styles from './ErrorBoundry.module.css';
|
||||
|
||||
const logError = (error, info) => {
|
||||
console.error(error, info.componentStack);
|
||||
};
|
||||
|
||||
export function ErrorBoundary({ children }) {
|
||||
const { formatMessage, messages } = useMessages();
|
||||
|
||||
const fallbackRender = ({ error, resetErrorBoundary }) => {
|
||||
console.log({ error });
|
||||
return (
|
||||
<div className={styles.error} role="alert">
|
||||
<h1>{formatMessage(messages.error)}</h1>
|
||||
<h3>{error.message}</h3>
|
||||
<pre>{error.stack}</pre>
|
||||
<Button onClick={resetErrorBoundary}>OK</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Boundary fallbackRender={fallbackRender} onError={logError}>
|
||||
{children}
|
||||
</Boundary>
|
||||
);
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
|
@ -0,0 +1,19 @@
|
|||
.error {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
z-index: var(--z-index-overlay);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 600px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.error button {
|
||||
align-self: center;
|
||||
}
|
|
@ -1,14 +1,18 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import Icon from './Icon';
|
||||
import Exclamation from 'assets/exclamation-triangle.svg';
|
||||
import { Icon, Icons, Text } from 'react-basics';
|
||||
import styles from './ErrorMessage.module.css';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
|
||||
export function ErrorMessage() {
|
||||
const { formatMessage, messages } = useMessages();
|
||||
|
||||
export default function ErrorMessage() {
|
||||
return (
|
||||
<div className={styles.error}>
|
||||
<Icon icon={<Exclamation />} className={styles.icon} size="large" />
|
||||
<FormattedMessage id="message.failure" defaultMessage="Something went wrong." />
|
||||
<Icon className={styles.icon} size="large">
|
||||
<Icons.Alert />
|
||||
</Icon>
|
||||
<Text>{formatMessage(messages.error)}</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ErrorMessage;
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
transform: translate(-50%, -50%);
|
||||
margin: auto;
|
||||
display: flex;
|
||||
z-index: 1;
|
||||
background-color: var(--base50);
|
||||
padding: 10px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
import List from 'assets/list-ul.svg';
|
||||
import Modal from 'components/common/Modal';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import Button from './Button';
|
||||
import EventDataForm from 'components/forms/EventDataForm';
|
||||
import styles from './EventDataButton.module.css';
|
||||
|
||||
function EventDataButton({ websiteId }) {
|
||||
const [showEventData, setShowEventData] = useState(false);
|
||||
|
||||
function handleClick() {
|
||||
if (!showEventData) {
|
||||
setShowEventData(true);
|
||||
}
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
setShowEventData(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
icon={<List />}
|
||||
tooltip={<FormattedMessage id="label.event-data" defaultMessage="Event" />}
|
||||
tooltipId="button-event"
|
||||
size="small"
|
||||
onClick={handleClick}
|
||||
className={styles.button}
|
||||
>
|
||||
Event Data
|
||||
</Button>
|
||||
{showEventData && (
|
||||
<Modal title={<FormattedMessage id="label.event-data" defaultMessage="Query Event Data" />}>
|
||||
<EventDataForm websiteId={websiteId} onClose={handleClose} />
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
EventDataButton.propTypes = {
|
||||
websiteId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
};
|
||||
|
||||
export default EventDataButton;
|
|
@ -1,3 +0,0 @@
|
|||
.button {
|
||||
width: fit-content;
|
||||
}
|
|
@ -1,5 +1,3 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Favicon.module.css';
|
||||
|
||||
function getHostName(url) {
|
||||
|
@ -7,7 +5,7 @@ function getHostName(url) {
|
|||
return match && match.length > 1 ? match[1] : null;
|
||||
}
|
||||
|
||||
function Favicon({ domain, ...props }) {
|
||||
export function Favicon({ domain, ...props }) {
|
||||
const hostName = domain ? getHostName(domain) : null;
|
||||
|
||||
return hostName ? (
|
||||
|
@ -21,8 +19,4 @@ function Favicon({ domain, ...props }) {
|
|||
) : null;
|
||||
}
|
||||
|
||||
Favicon.propTypes = {
|
||||
domain: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Favicon;
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
.favicon {
|
||||
margin-right: 8px;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
|
|
|
@ -1,25 +1,13 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ButtonLayout from 'components/layout/ButtonLayout';
|
||||
import ButtonGroup from './ButtonGroup';
|
||||
import { ButtonGroup, Button, Flexbox } from 'react-basics';
|
||||
|
||||
function FilterButtons({ buttons, selected, onClick }) {
|
||||
export function FilterButtons({ items, selectedKey, onSelect }) {
|
||||
return (
|
||||
<ButtonLayout>
|
||||
<ButtonGroup size="xsmall" items={buttons} selectedItem={selected} onClick={onClick} />
|
||||
</ButtonLayout>
|
||||
<Flexbox justifyContent="center">
|
||||
<ButtonGroup items={items} selectedKey={selectedKey} onSelect={onSelect}>
|
||||
{({ key, label }) => <Button key={key}>{label}</Button>}
|
||||
</ButtonGroup>
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
|
||||
FilterButtons.propTypes = {
|
||||
buttons: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
label: PropTypes.node,
|
||||
value: PropTypes.any.isRequired,
|
||||
}),
|
||||
),
|
||||
selected: PropTypes.any,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default FilterButtons;
|
||||
|
|
|
@ -1,34 +1,40 @@
|
|||
import React from 'react';
|
||||
import { Icon, Icons } from 'react-basics';
|
||||
import classNames from 'classnames';
|
||||
import Link from 'next/link';
|
||||
import { safeDecodeURI } from 'next-basics';
|
||||
import usePageQuery from 'hooks/usePageQuery';
|
||||
import External from 'assets/arrow-up-right-from-square.svg';
|
||||
import Icon from './Icon';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import styles from './FilterLink.module.css';
|
||||
|
||||
export default function FilterLink({ id, value, label, externalUrl }) {
|
||||
const { resolve, query } = usePageQuery();
|
||||
export function FilterLink({ id, value, label, externalUrl, children, className }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { resolveUrl, query } = usePageQuery();
|
||||
const active = query[id] !== undefined;
|
||||
const selected = query[id] === value;
|
||||
|
||||
return (
|
||||
<div className={styles.row}>
|
||||
<Link href={resolve({ [id]: value })} replace>
|
||||
<a
|
||||
className={classNames(styles.label, {
|
||||
[styles.inactive]: active && !selected,
|
||||
[styles.active]: active && selected,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={classNames(styles.row, className, {
|
||||
[styles.inactive]: active && !selected,
|
||||
[styles.active]: active && selected,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
{!value && `(${label || formatMessage(labels.unknown)})`}
|
||||
{value && (
|
||||
<Link href={resolveUrl({ [id]: value })} className={styles.label} replace>
|
||||
{safeDecodeURI(label || value)}
|
||||
</a>
|
||||
</Link>
|
||||
</Link>
|
||||
)}
|
||||
{externalUrl && (
|
||||
<a className={styles.link} href={externalUrl} target="_blank" rel="noreferrer noopener">
|
||||
<Icon icon={<External />} className={styles.icon} />
|
||||
<Icon className={styles.icon}>
|
||||
<Icons.External />
|
||||
</Icon>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FilterLink;
|
||||
|
|
|
@ -1,20 +1,25 @@
|
|||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.row .inactive {
|
||||
.row.inactive {
|
||||
color: var(--base500);
|
||||
}
|
||||
|
||||
.row .active {
|
||||
.row.inactive img {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.row.active {
|
||||
color: var(--base900);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.row .link {
|
||||
display: none;
|
||||
margin-left: 20px;
|
||||
margin-inline-start: 20px;
|
||||
}
|
||||
|
||||
.row .label {
|
||||
|
|
|
@ -1,44 +1,61 @@
|
|||
import Button from 'components/common/Button';
|
||||
import XMark from 'assets/xmark.svg';
|
||||
import Bars from 'assets/bars.svg';
|
||||
import { Button, Icon } from 'react-basics';
|
||||
import { useState } from 'react';
|
||||
import styles from './HamburgerButton.module.css';
|
||||
import MobileMenu from './MobileMenu';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import Icons from 'components/icons';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import useConfig from 'hooks/useConfig';
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
label: <FormattedMessage id="label.dashboard" defaultMessage="Dashboard" />,
|
||||
value: '/dashboard',
|
||||
},
|
||||
{ label: <FormattedMessage id="label.realtime" defaultMessage="Realtime" />, value: '/realtime' },
|
||||
{ label: <FormattedMessage id="label.settings" defaultMessage="Settings" />, value: '/settings' },
|
||||
{
|
||||
label: <FormattedMessage id="label.profile" defaultMessage="Profile" />,
|
||||
value: '/settings/profile',
|
||||
},
|
||||
{ label: <FormattedMessage id="label.logout" defaultMessage="Logout" />, value: '/logout' },
|
||||
];
|
||||
|
||||
export default function HamburgerButton() {
|
||||
export function HamburgerButton() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [active, setActive] = useState(false);
|
||||
const { cloudMode } = useConfig();
|
||||
|
||||
function handleClick() {
|
||||
setActive(state => !state);
|
||||
}
|
||||
const menuItems = [
|
||||
{
|
||||
label: formatMessage(labels.dashboard),
|
||||
url: '/dashboard',
|
||||
},
|
||||
{ label: formatMessage(labels.realtime), url: '/realtime' },
|
||||
!cloudMode && {
|
||||
label: formatMessage(labels.settings),
|
||||
url: '/settings',
|
||||
children: [
|
||||
{
|
||||
label: formatMessage(labels.websites),
|
||||
url: '/settings/websites',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.teams),
|
||||
url: '/settings/teams',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.users),
|
||||
url: '/settings/users',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.profile),
|
||||
url: '/settings/profile',
|
||||
},
|
||||
],
|
||||
},
|
||||
cloudMode && {
|
||||
label: formatMessage(labels.profile),
|
||||
url: '/settings/profile',
|
||||
},
|
||||
!cloudMode && { label: formatMessage(labels.logout), url: '/logout' },
|
||||
].filter(n => n);
|
||||
|
||||
function handleClose() {
|
||||
setActive(false);
|
||||
}
|
||||
const handleClick = () => setActive(state => !state);
|
||||
const handleClose = () => setActive(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
className={styles.button}
|
||||
icon={active ? <XMark /> : <Bars />}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
<Button variant="quiet" onClick={handleClick}>
|
||||
<Icon>{active ? <Icons.Close /> : <Icons.Menu />}</Icon>
|
||||
</Button>
|
||||
{active && <MobileMenu items={menuItems} onClose={handleClose} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default HamburgerButton;
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { Tooltip } from 'react-basics';
|
||||
import styles from './HoverTooltip.module.css';
|
||||
|
||||
export function HoverTooltip({ tooltip }) {
|
||||
const [position, setPosition] = useState({ x: -1000, y: -1000 });
|
||||
|
||||
useEffect(() => {
|
||||
const handler = e => {
|
||||
setPosition({ x: e.clientX, y: e.clientY });
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handler);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.tooltip} style={{ left: position.x, top: position.y }}>
|
||||
<Tooltip position="top" action="none" label={tooltip} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HoverTooltip;
|
|
@ -3,9 +3,9 @@
|
|||
}
|
||||
|
||||
.tooltip {
|
||||
color: var(--msgColor);
|
||||
position: fixed;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
z-index: var(--z-index-popup);
|
||||
}
|
||||
|
||||
.content {
|
|
@ -1,29 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import styles from './Icon.module.css';
|
||||
|
||||
function Icon({ icon, className, size = 'medium', ...props }) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.icon, className, {
|
||||
[styles.xlarge]: size === 'xlarge',
|
||||
[styles.large]: size === 'large',
|
||||
[styles.medium]: size === 'medium',
|
||||
[styles.small]: size === 'small',
|
||||
[styles.xsmall]: size === 'xsmall',
|
||||
})}
|
||||
{...props}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Icon.propTypes = {
|
||||
className: PropTypes.string,
|
||||
icon: PropTypes.node.isRequired,
|
||||
size: PropTypes.oneOf(['xlarge', 'large', 'medium', 'small', 'xsmall']),
|
||||
};
|
||||
|
||||
export default Icon;
|
|
@ -1,35 +0,0 @@
|
|||
.icon {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.icon svg {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.xlarge > svg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.large > svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.medium > svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.small > svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.xsmall > svg {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import NextLink from 'next/link';
|
||||
import Icon from './Icon';
|
||||
import styles from './Link.module.css';
|
||||
|
||||
function Link({ className, icon, children, size, iconRight, onClick, ...props }) {
|
||||
return (
|
||||
<NextLink {...props}>
|
||||
<a
|
||||
className={classNames(styles.link, className, {
|
||||
[styles.large]: size === 'large',
|
||||
[styles.small]: size === 'small',
|
||||
[styles.xsmall]: size === 'xsmall',
|
||||
[styles.iconRight]: iconRight,
|
||||
})}
|
||||
onClick={onClick}
|
||||
>
|
||||
{icon && <Icon className={styles.icon} icon={icon} size={size} />}
|
||||
{children}
|
||||
</a>
|
||||
</NextLink>
|
||||
);
|
||||
}
|
||||
|
||||
Link.propTypes = {
|
||||
className: PropTypes.string,
|
||||
icon: PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
size: PropTypes.oneOf(['large', 'small', 'xsmall']),
|
||||
iconRight: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Link;
|
|
@ -1,42 +0,0 @@
|
|||
a.link,
|
||||
a.link:active,
|
||||
a.link:visited {
|
||||
position: relative;
|
||||
color: var(--base900);
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
a.link span {
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
a.link:hover span {
|
||||
border-bottom: 2px solid var(--primary400);
|
||||
}
|
||||
|
||||
a.link.large {
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
a.link.small {
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
a.link.xsmall {
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
a.link .icon + * {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
a.link.iconRight .icon {
|
||||
order: 1;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
a.link.iconRight .icon + * {
|
||||
margin: 0;
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import styles from './Loading.module.css';
|
||||
|
||||
function Loading({ className, overlay = false }) {
|
||||
return (
|
||||
<div className={classNames(styles.loading, { [styles.overlay]: overlay }, className)}>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Loading.propTypes = {
|
||||
className: PropTypes.string,
|
||||
overlay: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Loading;
|
|
@ -1,55 +0,0 @@
|
|||
@keyframes blink {
|
||||
0% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
20% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.loading.overlay {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
background: var(--base400);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.loading div {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 100%;
|
||||
background: var(--base400);
|
||||
animation: blink 1.4s infinite;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
.loading.overlay div {
|
||||
background: var(--base900);
|
||||
}
|
||||
|
||||
.loading div + div {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.loading div:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.loading div:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import styles from './Menu.module.css';
|
||||
|
||||
function Menu({
|
||||
options = [],
|
||||
selectedOption,
|
||||
className,
|
||||
float,
|
||||
align = 'left',
|
||||
optionClassName,
|
||||
selectedClassName,
|
||||
onSelect = () => {},
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.menu, className, {
|
||||
[styles.float]: float,
|
||||
[styles.top]: float === 'top',
|
||||
[styles.bottom]: float === 'bottom',
|
||||
[styles.left]: align === 'left',
|
||||
[styles.right]: align === 'right',
|
||||
})}
|
||||
>
|
||||
{options
|
||||
.filter(({ hidden }) => !hidden)
|
||||
.map(option => {
|
||||
const { label, value, className: customClassName, render, divider } = option;
|
||||
|
||||
return render ? (
|
||||
render(option)
|
||||
) : (
|
||||
<div
|
||||
key={value}
|
||||
className={classNames(styles.option, optionClassName, customClassName, {
|
||||
[selectedClassName]: selectedOption === option,
|
||||
[styles.selected]: selectedOption === option,
|
||||
[styles.divider]: divider,
|
||||
})}
|
||||
onClick={e => onSelect(value, e)}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Menu.propTypes = {
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
label: PropTypes.node,
|
||||
value: PropTypes.any,
|
||||
className: PropTypes.string,
|
||||
render: PropTypes.func,
|
||||
divider: PropTypes.bool,
|
||||
}),
|
||||
),
|
||||
selectedOption: PropTypes.any,
|
||||
className: PropTypes.string,
|
||||
float: PropTypes.oneOf(['top', 'bottom']),
|
||||
align: PropTypes.oneOf(['left', 'right']),
|
||||
optionClassName: PropTypes.string,
|
||||
selectedClassName: PropTypes.string,
|
||||
onSelect: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Menu;
|
|
@ -1,51 +0,0 @@
|
|||
.menu {
|
||||
background: var(--base50);
|
||||
border: 1px solid var(--base500);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.option {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: normal;
|
||||
background: var(--base50);
|
||||
padding: 4px 16px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background: var(--base100);
|
||||
}
|
||||
|
||||
.float {
|
||||
position: absolute;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.top {
|
||||
bottom: 100%;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
top: 100%;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.right {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.divider {
|
||||
border-top: 1px solid var(--base300);
|
||||
}
|
||||
|
||||
.selected {
|
||||
font-weight: 600;
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
import React, { useState, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import Menu from 'components/common/Menu';
|
||||
import Button from 'components/common/Button';
|
||||
import useDocumentClick from 'hooks/useDocumentClick';
|
||||
import styles from './MenuButton.module.css';
|
||||
|
||||
function MenuButton({
|
||||
icon,
|
||||
value,
|
||||
options,
|
||||
buttonClassName,
|
||||
buttonVariant,
|
||||
menuClassName,
|
||||
menuPosition = 'bottom',
|
||||
menuAlign = 'right',
|
||||
onSelect,
|
||||
renderValue,
|
||||
hideLabel,
|
||||
}) {
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const ref = useRef();
|
||||
const selectedOption = options.find(e => e.value === value);
|
||||
|
||||
function handleSelect(value) {
|
||||
onSelect(value);
|
||||
setShowMenu(false);
|
||||
}
|
||||
|
||||
function toggleMenu() {
|
||||
setShowMenu(state => !state);
|
||||
}
|
||||
|
||||
useDocumentClick(e => {
|
||||
if (!ref.current?.contains(e.target)) {
|
||||
setShowMenu(false);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.container} ref={ref}>
|
||||
<Button
|
||||
icon={icon}
|
||||
className={classNames(styles.button, buttonClassName, { [styles.open]: showMenu })}
|
||||
onClick={toggleMenu}
|
||||
variant={buttonVariant}
|
||||
>
|
||||
{!hideLabel && (
|
||||
<div className={styles.text}>{renderValue ? renderValue(selectedOption) : value}</div>
|
||||
)}
|
||||
</Button>
|
||||
{showMenu && (
|
||||
<Menu
|
||||
className={menuClassName}
|
||||
options={options}
|
||||
selectedOption={selectedOption}
|
||||
onSelect={handleSelect}
|
||||
float={menuPosition}
|
||||
align={menuAlign}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
MenuButton.propTypes = {
|
||||
icon: PropTypes.node,
|
||||
value: PropTypes.any,
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
label: PropTypes.node,
|
||||
value: PropTypes.any,
|
||||
className: PropTypes.string,
|
||||
render: PropTypes.func,
|
||||
divider: PropTypes.bool,
|
||||
}),
|
||||
),
|
||||
buttonClassName: PropTypes.string,
|
||||
menuClassName: PropTypes.string,
|
||||
menuPosition: PropTypes.oneOf(['top', 'bottom']),
|
||||
menuAlign: PropTypes.oneOf(['left', 'right']),
|
||||
onSelect: PropTypes.func,
|
||||
renderValue: PropTypes.func,
|
||||
};
|
||||
|
||||
export default MenuButton;
|
|
@ -1,20 +0,0 @@
|
|||
.container {
|
||||
display: flex;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button {
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.open,
|
||||
.open:hover {
|
||||
background: var(--base50);
|
||||
border: 1px solid var(--base500);
|
||||
}
|
|
@ -1,22 +1,38 @@
|
|||
import classNames from 'classnames';
|
||||
import Link from './Link';
|
||||
import Button from './Button';
|
||||
import XMark from 'assets/xmark.svg';
|
||||
import { useRouter } from 'next/router';
|
||||
import Link from 'next/link';
|
||||
import styles from './MobileMenu.module.css';
|
||||
|
||||
export default function MobileMenu({ items = [], onClose }) {
|
||||
export function MobileMenu({ items = [], onClose }) {
|
||||
const { pathname } = useRouter();
|
||||
|
||||
const Items = ({ items, className }) => (
|
||||
<div className={classNames(styles.items, className)}>
|
||||
{items.map(({ label, url, children }) => {
|
||||
const selected = pathname.startsWith(url);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link
|
||||
key={url}
|
||||
href={url}
|
||||
className={classNames(styles.item, { [styles.selected]: selected })}
|
||||
onClick={onClose}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
{children && <Items items={children} className={styles.submenu} />}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.menu, 'container')}>
|
||||
<div className={styles.header}>
|
||||
<Button icon={<XMark />} onClick={onClose} />
|
||||
</div>
|
||||
<div className={styles.items}>
|
||||
{items.map(({ label, value }) => (
|
||||
<Link key={value} href={value} className={styles.item} onClick={onClose}>
|
||||
{label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className={classNames(styles.menu)}>
|
||||
<Items items={items} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MobileMenu;
|
||||
|
|
|
@ -1,41 +1,39 @@
|
|||
.menu {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
top: 60px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--base50);
|
||||
z-index: var(--z-index-popup);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.items {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.item {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 700;
|
||||
line-height: 80px;
|
||||
padding: 0 40px;
|
||||
}
|
||||
|
||||
.item + .item {
|
||||
margin-top: 20px;
|
||||
a.item {
|
||||
color: var(--base600);
|
||||
}
|
||||
|
||||
.item:last-child {
|
||||
margin-top: 60px;
|
||||
a.item.selected,
|
||||
.submenu a.item.selected {
|
||||
color: var(--base900);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
height: 100px;
|
||||
padding: 0 30px;
|
||||
.submenu a.item {
|
||||
color: var(--base600);
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { useSpring, animated } from 'react-spring';
|
||||
import styles from './Modal.module.css';
|
||||
|
||||
function Modal({ title, children }) {
|
||||
const props = useSpring({ opacity: 1, from: { opacity: 0 } });
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<animated.div className={styles.modal} style={props}>
|
||||
<div className={styles.content}>
|
||||
{title && <div className={styles.header}>{title}</div>}
|
||||
<div className={styles.body}>{children}</div>
|
||||
</div>
|
||||
</animated.div>,
|
||||
document.getElementById('__modals'),
|
||||
);
|
||||
}
|
||||
|
||||
Modal.propTypes = {
|
||||
title: PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default Modal;
|
|
@ -1,46 +0,0 @@
|
|||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.modal:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
background: #000;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: var(--base50);
|
||||
min-width: 400px;
|
||||
min-height: 100px;
|
||||
max-width: 100vw;
|
||||
z-index: 1;
|
||||
border: 1px solid var(--base300);
|
||||
padding: 30px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.header {
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useRouter } from 'next/router';
|
||||
import classNames from 'classnames';
|
||||
import styles from './NavMenu.module.css';
|
||||
|
||||
function NavMenu({ options = [], className, onSelect = () => {} }) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.menu, className)}>
|
||||
{options
|
||||
.filter(({ hidden }) => !hidden)
|
||||
.map(option => {
|
||||
const { label, value, className: customClassName, render } = option;
|
||||
|
||||
return render ? (
|
||||
render(option)
|
||||
) : (
|
||||
<div
|
||||
key={value}
|
||||
className={classNames(styles.option, customClassName, {
|
||||
[styles.selected]: router.asPath === value,
|
||||
})}
|
||||
onClick={e => onSelect(value, e)}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
NavMenu.propTypes = {
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
label: PropTypes.node,
|
||||
value: PropTypes.any,
|
||||
className: PropTypes.string,
|
||||
render: PropTypes.func,
|
||||
}),
|
||||
),
|
||||
className: PropTypes.string,
|
||||
onSelect: PropTypes.func,
|
||||
};
|
||||
export default NavMenu;
|
|
@ -1,22 +0,0 @@
|
|||
.menu {
|
||||
color: var(--base800);
|
||||
border: 1px solid var(--base500);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.option {
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background: var(--base75);
|
||||
}
|
||||
|
||||
.selected {
|
||||
color: var(--base900);
|
||||
font-weight: 600;
|
||||
}
|
|
@ -1,19 +1,15 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import styles from './NoData.module.css';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
|
||||
export function NoData({ className }) {
|
||||
const { formatMessage, messages } = useMessages();
|
||||
|
||||
function NoData({ className }) {
|
||||
return (
|
||||
<div className={classNames(styles.container, className)}>
|
||||
<FormattedMessage id="message.no-data-available" defaultMessage="No data available." />
|
||||
{formatMessage(messages.noDataAvailable)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
NoData.propTypes = {
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default NoData;
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
|
||||
import styles from './OverflowText.module.css';
|
||||
|
||||
const OverflowText = ({ children, tooltipId }) => {
|
||||
const measureEl = useRef();
|
||||
const [isOverflown, setIsOverflown] = useState(false);
|
||||
|
||||
const measure = useCallback(
|
||||
el => {
|
||||
if (!el) return;
|
||||
setIsOverflown(el.scrollWidth > el.clientWidth);
|
||||
},
|
||||
[setIsOverflown],
|
||||
);
|
||||
|
||||
// Do one measure on mount
|
||||
useEffect(() => {
|
||||
measure(measureEl.current);
|
||||
}, [measure]);
|
||||
|
||||
// Set up resize listener for subsequent measures
|
||||
useEffect(() => {
|
||||
if (!measureEl.current) return;
|
||||
|
||||
// Destructure ref in case it changes out from under us
|
||||
const el = measureEl.current;
|
||||
|
||||
if ('ResizeObserver' in global) {
|
||||
// Ideally, we have access to ResizeObservers
|
||||
const observer = new ResizeObserver(() => {
|
||||
measure(el);
|
||||
});
|
||||
observer.observe(el);
|
||||
return () => observer.unobserve(el);
|
||||
} else {
|
||||
// Otherwise, fall back to measuring on window resizes
|
||||
const handler = () => measure(el);
|
||||
|
||||
window.addEventListener('resize', handler, { passive: true });
|
||||
return () => window.removeEventListener('resize', handler, { passive: true });
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<span
|
||||
ref={measureEl}
|
||||
data-tip={children.toString()}
|
||||
data-effect="solid"
|
||||
data-for={tooltipId}
|
||||
className={styles.root}
|
||||
>
|
||||
{children}
|
||||
{isOverflown && <ReactTooltip id={tooltipId}>{children}</ReactTooltip>}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
OverflowText.propTypes = {
|
||||
children: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||
tooltipId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default OverflowText;
|
|
@ -1,6 +0,0 @@
|
|||
.root {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import useStore from 'store/queries';
|
||||
import { setDateRange } from 'store/websites';
|
||||
import Button from './Button';
|
||||
import Refresh from 'assets/redo.svg';
|
||||
import Dots from 'assets/ellipsis-h.svg';
|
||||
import useDateRange from 'hooks/useDateRange';
|
||||
|
||||
function RefreshButton({ websiteId }) {
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const selector = useCallback(state => state[`/websites/${websiteId}/stats`], [websiteId]);
|
||||
const completed = useStore(selector);
|
||||
|
||||
function handleClick() {
|
||||
if (!loading && dateRange) {
|
||||
setLoading(true);
|
||||
if (/^[\d]+/.test(dateRange.value)) {
|
||||
setDateRange(websiteId, dateRange.value);
|
||||
} else {
|
||||
setDateRange(websiteId, dateRange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(false);
|
||||
}, [completed]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon={loading ? <Dots /> : <Refresh />}
|
||||
tooltip={<FormattedMessage id="label.refresh" defaultMessage="Refresh" />}
|
||||
tooltipId="button-refresh"
|
||||
size="small"
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
RefreshButton.propTypes = {
|
||||
websiteId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
};
|
||||
|
||||
export default RefreshButton;
|
|
@ -0,0 +1,38 @@
|
|||
import { Table, TableHeader, TableBody, TableRow, TableCell, TableColumn } from 'react-basics';
|
||||
import styles from './SettingsTable.module.css';
|
||||
|
||||
export function SettingsTable({ columns = [], data = [], children, cellRender }) {
|
||||
return (
|
||||
<Table columns={columns} rows={data}>
|
||||
<TableHeader className={styles.header}>
|
||||
{(column, index) => {
|
||||
return (
|
||||
<TableColumn key={index} className={styles.cell} style={columns[index].style}>
|
||||
{column.label}
|
||||
</TableColumn>
|
||||
);
|
||||
}}
|
||||
</TableHeader>
|
||||
<TableBody className={styles.body}>
|
||||
{(row, keys, rowIndex) => {
|
||||
row.action = children(row, keys, rowIndex);
|
||||
|
||||
return (
|
||||
<TableRow key={rowIndex} data={row} keys={keys} className={styles.row}>
|
||||
{(data, key, colIndex) => {
|
||||
return (
|
||||
<TableCell key={colIndex} className={styles.cell} style={columns[colIndex].style}>
|
||||
<label className={styles.label}>{columns[colIndex].label}</label>
|
||||
{cellRender ? cellRender(row, data, key, colIndex) : data[key]}
|
||||
</TableCell>
|
||||
);
|
||||
}}
|
||||
</TableRow>
|
||||
);
|
||||
}}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export default SettingsTable;
|
|
@ -0,0 +1,44 @@
|
|||
.cell {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.row .cell:last-child {
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
.header .cell {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.row .cell {
|
||||
padding-left: 0;
|
||||
flex-basis: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
.row {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.header .cell:last-child {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.row .cell:last-child {
|
||||
padding-left: 0;
|
||||
flex-basis: 100%;
|
||||
}
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import NoData from 'components/common/NoData';
|
||||
import styles from './Table.module.css';
|
||||
|
||||
function Table({
|
||||
columns,
|
||||
rows,
|
||||
empty,
|
||||
className,
|
||||
bodyClassName,
|
||||
rowKey,
|
||||
showHeader = true,
|
||||
children,
|
||||
}) {
|
||||
if (empty && rows.length === 0) {
|
||||
return empty;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.table, className)}>
|
||||
{showHeader && (
|
||||
<div className={classNames(styles.header, 'row')}>
|
||||
{columns.map(({ key, label, className, style, header }) => (
|
||||
<div
|
||||
key={key}
|
||||
className={classNames(styles.head, className, header?.className)}
|
||||
style={{ ...style, ...header?.style }}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className={classNames(styles.body, bodyClassName)}>
|
||||
{rows.length === 0 && <NoData />}
|
||||
{!children &&
|
||||
rows.map((row, index) => {
|
||||
const id = rowKey ? rowKey(row) : index;
|
||||
return <TableRow key={id} columns={columns} row={row} />;
|
||||
})}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styledObject = PropTypes.shape({
|
||||
className: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
});
|
||||
|
||||
Table.propTypes = {
|
||||
columns: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
cell: styledObject,
|
||||
className: PropTypes.string,
|
||||
header: styledObject,
|
||||
key: PropTypes.string,
|
||||
label: PropTypes.node,
|
||||
render: PropTypes.func,
|
||||
style: PropTypes.object,
|
||||
}),
|
||||
),
|
||||
rows: PropTypes.arrayOf(PropTypes.object),
|
||||
empty: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
bodyClassName: PropTypes.string,
|
||||
rowKey: PropTypes.func,
|
||||
showHeader: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default Table;
|
||||
|
||||
export const TableRow = ({ columns, row }) => (
|
||||
<div className={classNames(styles.row, 'row')}>
|
||||
{columns.map(({ key, label, render, className, style, cell }, index) => (
|
||||
<div
|
||||
key={`${key}-${index}`}
|
||||
className={classNames(styles.cell, className, cell?.className)}
|
||||
style={{ ...style, ...cell?.style }}
|
||||
>
|
||||
{label && <label>{label}</label>}
|
||||
{render ? render(row) : row[key]}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
|
@ -1,55 +0,0 @@
|
|||
.table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.table label {
|
||||
display: none;
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.header {
|
||||
border-bottom: 1px solid var(--base300);
|
||||
}
|
||||
|
||||
.head {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.body {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.row {
|
||||
border-bottom: 1px solid var(--base300);
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
.table label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cell {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import styles from './Tag.module.css';
|
||||
|
||||
function Tag({ className, children }) {
|
||||
return <span className={classNames(styles.tag, className)}>{children}</span>;
|
||||
}
|
||||
|
||||
Tag.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default Tag;
|
|
@ -1,6 +0,0 @@
|
|||
.tag {
|
||||
padding: 2px 4px;
|
||||
border: 1px solid var(--base300);
|
||||
border-radius: 4px;
|
||||
margin-right: 10px;
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { useSpring, animated } from 'react-spring';
|
||||
import Icon from 'components/common/Icon';
|
||||
import Close from 'assets/times.svg';
|
||||
import styles from './Toast.module.css';
|
||||
|
||||
function Toast({ message, timeout = 3000, onClose }) {
|
||||
const props = useSpring({
|
||||
opacity: 1,
|
||||
transform: 'translate3d(0,0px,0)',
|
||||
from: { opacity: 0, transform: 'translate3d(0,-40px,0)' },
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(onClose, timeout);
|
||||
}, []);
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<animated.div className={styles.toast} style={props} onClick={onClose}>
|
||||
<div className={styles.message}>{message}</div>
|
||||
<Icon className={styles.close} icon={<Close />} size="small" />
|
||||
</animated.div>,
|
||||
document.getElementById('__modals'),
|
||||
);
|
||||
}
|
||||
|
||||
Toast.propTypes = {
|
||||
message: PropTypes.node,
|
||||
timeout: PropTypes.number,
|
||||
onClose: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Toast;
|
|
@ -1,25 +0,0 @@
|
|||
.toast {
|
||||
position: fixed;
|
||||
top: 30px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 300px;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
color: var(--msgColor);
|
||||
background: var(--green400);
|
||||
margin: auto;
|
||||
z-index: 2;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.message {
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
|
||||
.close {
|
||||
margin-left: 20px;
|
||||
}
|
|
@ -1,13 +1,13 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Button, Row, Column } from 'react-basics';
|
||||
import { setItem } from 'next-basics';
|
||||
import ButtonLayout from 'components/layout/ButtonLayout';
|
||||
import useStore, { checkVersion } from 'store/version';
|
||||
import { REPO_URL, VERSION_CHECK } from 'lib/constants';
|
||||
import Button from './Button';
|
||||
import styles from './UpdateNotice.module.css';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
|
||||
export default function UpdateNotice() {
|
||||
export function UpdateNotice() {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { latest, checked, hasUpdate, releaseUrl } = useStore();
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
|
||||
|
@ -37,22 +37,18 @@ export default function UpdateNotice() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={styles.notice}>
|
||||
<div className={styles.message}>
|
||||
<FormattedMessage
|
||||
id="message.new-version-available"
|
||||
defaultMessage="A new version of umami {version} is available!"
|
||||
values={{ version: `v${latest}` }}
|
||||
/>
|
||||
</div>
|
||||
<ButtonLayout className={styles.buttons}>
|
||||
<Button size="xsmall" variant="action" onClick={handleViewClick}>
|
||||
<FormattedMessage id="label.view-details" defaultMessage="View details" />
|
||||
<Row className={styles.notice}>
|
||||
<Column variant="two" className={styles.message}>
|
||||
{formatMessage(messages.newVersionAvailable, { version: `v${latest}` })}
|
||||
</Column>
|
||||
<Column className={styles.buttons}>
|
||||
<Button variant="primary" onClick={handleViewClick}>
|
||||
{formatMessage(labels.viewDetails)}
|
||||
</Button>
|
||||
<Button size="xsmall" onClick={handleDismissClick}>
|
||||
<FormattedMessage id="label.dismiss" defaultMessage="Dismiss" />
|
||||
</Button>
|
||||
</ButtonLayout>
|
||||
</div>
|
||||
<Button onClick={handleDismissClick}>{formatMessage(labels.dismiss)}</Button>
|
||||
</Column>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
export default UpdateNotice;
|
||||
|
|
|
@ -1,18 +1,33 @@
|
|||
.notice {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding-top: 20px;
|
||||
position: absolute;
|
||||
max-width: 800px;
|
||||
gap: 20px;
|
||||
margin: 20px auto;
|
||||
justify-self: center;
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border: 1px solid var(--base300);
|
||||
border-radius: var(--border-radius);
|
||||
z-index: var(--z-index-popup);
|
||||
}
|
||||
|
||||
.message {
|
||||
font-size: var(--font-size-sm);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
.message {
|
||||
height: 80px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
import React, { useState, useMemo } from 'react';
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
|
||||
import classNames from 'classnames';
|
||||
import { colord } from 'colord';
|
||||
import useTheme from 'hooks/useTheme';
|
||||
import HoverTooltip from 'components/common/HoverTooltip';
|
||||
import { ISO_COUNTRIES, THEME_COLORS, MAP_FILE } from 'lib/constants';
|
||||
import styles from './WorldMap.module.css';
|
||||
import useTheme from 'hooks/useTheme';
|
||||
import useCountryNames from 'hooks/useCountryNames';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import { formatLongNumber } from 'lib/format';
|
||||
import { percentFilter } from 'lib/filters';
|
||||
import styles from './WorldMap.module.css';
|
||||
|
||||
function WorldMap({ data, className }) {
|
||||
export function WorldMap({ data, className }) {
|
||||
const { basePath } = useRouter();
|
||||
const [tooltip, setTooltip] = useState();
|
||||
const [theme] = useTheme();
|
||||
|
@ -26,10 +27,11 @@ function WorldMap({ data, className }) {
|
|||
);
|
||||
const { locale } = useLocale();
|
||||
const countryNames = useCountryNames(locale);
|
||||
const metrics = useMemo(() => (data ? percentFilter(data) : []), [data]);
|
||||
|
||||
function getFillColor(code) {
|
||||
if (code === 'AQ') return;
|
||||
const country = data?.find(({ x }) => x === code);
|
||||
const country = metrics?.find(({ x }) => x === code);
|
||||
|
||||
if (!country) {
|
||||
return colors.fillColor;
|
||||
|
@ -46,8 +48,8 @@ function WorldMap({ data, className }) {
|
|||
|
||||
function handleHover(code) {
|
||||
if (code === 'AQ') return;
|
||||
const country = data?.find(({ x }) => x === code);
|
||||
setTooltip(`${countryNames[code]}: ${country?.y || 0} visitors`);
|
||||
const country = metrics?.find(({ x }) => x === code);
|
||||
setTooltip(`${countryNames[code]}: ${formatLongNumber(country?.y || 0)} visitors`);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -84,20 +86,9 @@ function WorldMap({ data, className }) {
|
|||
</Geographies>
|
||||
</ZoomableGroup>
|
||||
</ComposableMap>
|
||||
<ReactTooltip id="world-map-tooltip">{tooltip}</ReactTooltip>
|
||||
{tooltip && <HoverTooltip tooltip={tooltip} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
WorldMap.propTypes = {
|
||||
data: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
x: PropTypes.string,
|
||||
y: PropTypes.number,
|
||||
z: PropTypes.number,
|
||||
}),
|
||||
),
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default WorldMap;
|
||||
|
|
|
@ -1,107 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Formik, Form, Field } from 'formik';
|
||||
import Button from 'components/common/Button';
|
||||
import FormLayout, {
|
||||
FormButtons,
|
||||
FormError,
|
||||
FormMessage,
|
||||
FormRow,
|
||||
} from 'components/layout/FormLayout';
|
||||
import useApi from 'hooks/useApi';
|
||||
import useUser from 'hooks/useUser';
|
||||
|
||||
const initialValues = {
|
||||
current_password: '',
|
||||
new_password: '',
|
||||
confirm_password: '',
|
||||
};
|
||||
|
||||
const validate = ({ current_password, new_password, confirm_password }) => {
|
||||
const errors = {};
|
||||
|
||||
if (!current_password) {
|
||||
errors.current_password = <FormattedMessage id="label.required" defaultMessage="Required" />;
|
||||
}
|
||||
if (!new_password) {
|
||||
errors.new_password = <FormattedMessage id="label.required" defaultMessage="Required" />;
|
||||
}
|
||||
if (!confirm_password) {
|
||||
errors.confirm_password = <FormattedMessage id="label.required" defaultMessage="Required" />;
|
||||
} else if (new_password !== confirm_password) {
|
||||
errors.confirm_password = (
|
||||
<FormattedMessage id="label.passwords-dont-match" defaultMessage="Passwords don't match" />
|
||||
);
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
export default function ChangePasswordForm({ values, onSave, onClose }) {
|
||||
const { post } = useApi();
|
||||
const [message, setMessage] = useState();
|
||||
const { user } = useUser();
|
||||
|
||||
const handleSubmit = async values => {
|
||||
const { ok, error } = await post(`/users/${user.id}/password`, values);
|
||||
|
||||
if (ok) {
|
||||
onSave();
|
||||
} else {
|
||||
setMessage(
|
||||
error || <FormattedMessage id="message.failure" defaultMessage="Something went wrong." />,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FormLayout>
|
||||
<Formik
|
||||
initialValues={{ ...initialValues, ...values }}
|
||||
validate={validate}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{() => (
|
||||
<Form>
|
||||
<FormRow>
|
||||
<label htmlFor="current_password">
|
||||
<FormattedMessage id="label.current-password" defaultMessage="Current password" />
|
||||
</label>
|
||||
<div>
|
||||
<Field name="current_password" type="password" />
|
||||
<FormError name="current_password" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<label htmlFor="new_password">
|
||||
<FormattedMessage id="label.new-password" defaultMessage="New password" />
|
||||
</label>
|
||||
<div>
|
||||
<Field name="new_password" type="password" />
|
||||
<FormError name="new_password" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<label htmlFor="confirm_password">
|
||||
<FormattedMessage id="label.confirm-password" defaultMessage="Confirm password" />
|
||||
</label>
|
||||
<div>
|
||||
<Field name="confirm_password" type="password" />
|
||||
<FormError name="confirm_password" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<Button type="submit" variant="action">
|
||||
<FormattedMessage id="label.save" defaultMessage="Save" />
|
||||
</Button>
|
||||
<Button onClick={onClose}>
|
||||
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
</FormButtons>
|
||||
<FormMessage>{message}</FormMessage>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</FormLayout>
|
||||
);
|
||||
}
|