Compare commits
516 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e26cdc11c3 | ||
|
|
fa54aa3128 | ||
|
|
e31e70039d | ||
|
|
cb761dea8f | ||
|
|
949e0da105 | ||
|
|
770cc59448 | ||
|
|
72dd2bd0a7 | ||
|
|
54733eaa18 | ||
|
|
52c56f7918 | ||
|
|
c46d5187c1 | ||
|
|
05e3e87653 | ||
|
|
8b9289ff08 | ||
|
|
16ffbcfbc0 | ||
|
|
d825b6e174 | ||
|
|
73e55cc742 | ||
|
|
32cc1cc580 | ||
|
|
e00574553f | ||
|
|
984c1ea716 | ||
|
|
df38cf6ca9 | ||
|
|
1b60b0e2a8 | ||
|
|
11f9c7c507 | ||
|
|
ebe649aaac | ||
|
|
656b68d422 | ||
|
|
cd1f186e28 | ||
|
|
d0b3edaa2f | ||
|
|
2268b85ade | ||
|
|
d7e3b7b912 | ||
|
|
4bd83eecfb | ||
|
|
b7fd2308ad | ||
|
|
a6958941ad | ||
|
|
c98b28ff0f | ||
|
|
6a372badfa | ||
|
|
b6ab9a1bdd | ||
|
|
daf9e7cf64 | ||
|
|
ef42dcd666 | ||
|
|
1b6028ae6d | ||
|
|
9340512980 | ||
|
|
9d0b4cc065 | ||
|
|
c5cb0dcb26 | ||
|
|
a42f5ab13e | ||
|
|
68b0577526 | ||
|
|
61867366e7 | ||
|
|
c670d86955 | ||
|
|
4565a64cd8 | ||
|
|
f36e42d9c1 | ||
|
|
0a3a97242b | ||
|
|
68253c3bc4 | ||
|
|
544384d85e | ||
|
|
91daec852f | ||
|
|
dcc5b9cc8c | ||
|
|
1d26cd93fb | ||
|
|
e47dfaf36f | ||
|
|
09e2c69e46 | ||
|
|
07d3567244 | ||
|
|
9bdbe90716 | ||
|
|
02a4380f7c | ||
|
|
4e483dc5d4 | ||
|
|
52631e629e | ||
|
|
3a53298417 | ||
|
|
fb0f14fc16 | ||
|
|
7a94b1730d | ||
|
|
f856bc218a | ||
|
|
bfbb21e1cc | ||
|
|
18e18f533b | ||
|
|
6eead70511 | ||
|
|
6fd30ed51a | ||
|
|
67c674f073 | ||
|
|
289d8784c0 | ||
|
|
18e026e4ca | ||
|
|
8741f42fe8 | ||
|
|
665d6209d9 | ||
|
|
59fda29894 | ||
|
|
61c027f9a1 | ||
|
|
241c9b73b0 | ||
|
|
85dc1d0825 | ||
|
|
e38887aa26 | ||
|
|
54fec79945 | ||
|
|
fad0bf1c9d | ||
|
|
be2f86050f | ||
|
|
a7f941e8e4 | ||
|
|
b08c6748c7 | ||
|
|
bdd7932e07 | ||
|
|
bcf5dcf180 | ||
|
|
8b2cbf7aea | ||
|
|
277b5e43f8 | ||
|
|
7dd6a31609 | ||
|
|
86bf1515d4 | ||
|
|
bbc47b387e | ||
|
|
3953e98a77 | ||
|
|
09b8bd501d | ||
|
|
6bddaaa055 | ||
|
|
dd728d4d13 | ||
|
|
9ba8bc8f3d | ||
|
|
16dee3664b | ||
|
|
6fcf588bfd | ||
|
|
6a6c427b0e | ||
|
|
41f885d8ec | ||
|
|
7516ca8dd9 | ||
|
|
aa59a95f91 | ||
|
|
8a5161c0e8 | ||
|
|
d8ae69e861 | ||
|
|
a485d0b507 | ||
|
|
ed40b79c8d | ||
|
|
91488ae294 | ||
|
|
a22a1938c1 | ||
|
|
0f73cb9f8c | ||
|
|
f3129399de | ||
|
|
37e6c27461 | ||
|
|
d231ed3ede | ||
|
|
cf6f9028f2 | ||
|
|
7cf49d2c1a | ||
|
|
e37fb1b4bd | ||
|
|
faf5d0bf7b | ||
|
|
6fede88072 | ||
|
|
87ffbefa61 | ||
|
|
f33ae17781 | ||
|
|
2a2bae6d1a | ||
|
|
eb65e99024 | ||
|
|
52dbeb6201 | ||
|
|
fafe920b7b | ||
|
|
9d1e48ee90 | ||
|
|
3851342e1b | ||
|
|
b863c2e19d | ||
|
|
ed584d19e5 | ||
|
|
73256dcf5b | ||
|
|
c67a23c988 | ||
|
|
8f42e65ccd | ||
|
|
05deb1aff0 | ||
|
|
a74b7cdfad | ||
|
|
1c3119ee76 | ||
|
|
ca52911e42 | ||
|
|
9177bc7cef | ||
|
|
310831a26a | ||
|
|
8a486d991b | ||
|
|
b79333393b | ||
|
|
cb7062bb95 | ||
|
|
94c5b2c471 | ||
|
|
66bf26f1dc | ||
|
|
f5cc1abe75 | ||
|
|
bd4255108d | ||
|
|
06b63d1af2 | ||
|
|
2bd70fb9e6 | ||
|
|
e6034dfb14 | ||
|
|
c8ba6764c2 | ||
|
|
19337d6c05 | ||
|
|
a6ad3c2d4d | ||
|
|
b0dd885c09 | ||
|
|
2235592308 | ||
|
|
1219a16261 | ||
|
|
7949e224e0 | ||
|
|
ab2f311bb7 | ||
|
|
a5aab43666 | ||
|
|
74ebd4e572 | ||
|
|
bd29670108 | ||
|
|
9a20b4428d | ||
|
|
d7da8521ce | ||
|
|
bab3b252c1 | ||
|
|
7f05c5c2da | ||
|
|
2d5c2779c3 | ||
|
|
06db4f6556 | ||
|
|
ea5ec63a22 | ||
|
|
f46e737e77 | ||
|
|
6e63bdaafa | ||
|
|
79ccef9f7e | ||
|
|
a9653b3674 | ||
|
|
b5a188e802 | ||
|
|
38fc402b16 | ||
|
|
584d1ec1ce | ||
|
|
2ca7faa457 | ||
|
|
03806abda0 | ||
|
|
18d125430d | ||
|
|
f57f6b7745 | ||
|
|
75ff2b8f40 | ||
|
|
2ec04c0121 | ||
|
|
5145a41dac | ||
|
|
25c67f1c3e | ||
|
|
77b9181150 | ||
|
|
e4f7ded8e2 | ||
|
|
35a62f1fb1 | ||
|
|
24f2deda46 | ||
|
|
5d8af1a0e5 | ||
|
|
6d44ac1e0c | ||
|
|
fb0ebddf28 | ||
|
|
0aebaa4da1 | ||
|
|
f6baedc655 | ||
|
|
7db222664d | ||
|
|
8223f0fd64 | ||
|
|
f44ec42f51 | ||
|
|
dab75ab6a9 | ||
|
|
01672b88e1 | ||
|
|
78dc297022 | ||
|
|
c8cf75fa28 | ||
|
|
b011b4e1d8 | ||
|
|
9804a2d18d | ||
|
|
d1a5ee43e9 | ||
|
|
febecab33c | ||
|
|
99042c0979 | ||
|
|
6395e4e00b | ||
|
|
4a69907ca3 | ||
|
|
c8d682cc98 | ||
|
|
f4cc8d3a0c | ||
|
|
6ac89334fd | ||
|
|
f55d3a66aa | ||
|
|
972eafab34 | ||
|
|
fba156b271 | ||
|
|
96d538db15 | ||
|
|
b89bfa3c1c | ||
|
|
73e3f42614 | ||
|
|
e761f5e1bd | ||
|
|
4a6dd66ecd | ||
|
|
8e1c6908c6 | ||
|
|
f59e569e22 | ||
|
|
be50b24504 | ||
|
|
c181831a37 | ||
|
|
dbee62ac8c | ||
|
|
1e949b3a22 | ||
|
|
b02dcf6c53 | ||
|
|
ab7718e335 | ||
|
|
451c77d47f | ||
|
|
fa0d3d4047 | ||
|
|
397a183f65 | ||
|
|
bc8905ee7f | ||
|
|
853032ac7f | ||
|
|
3b0e282a52 | ||
|
|
bb28cb3862 | ||
|
|
d0f458bece | ||
|
|
da54a72b3e | ||
|
|
86c155d8d1 | ||
|
|
666d2d3065 | ||
|
|
01e69fb6ca | ||
|
|
30e5253acd | ||
|
|
c67ce3918b | ||
|
|
58077f2d86 | ||
|
|
098c94bccf | ||
|
|
861a3c068f | ||
|
|
3b95e8ebc0 | ||
|
|
170e427530 | ||
|
|
707c9f4ce6 | ||
|
|
dc672bf0f0 | ||
|
|
c682737505 | ||
|
|
46fa3d4345 | ||
|
|
9b7bc4b495 | ||
|
|
4385061499 | ||
|
|
e17498e68b | ||
|
|
3e298f010b | ||
|
|
30117bd121 | ||
|
|
93f33b6218 | ||
|
|
535d08a607 | ||
|
|
6ac3a49db2 | ||
|
|
c16f760d79 | ||
|
|
965c2b243f | ||
|
|
703addddb9 | ||
|
|
ab6dff5c31 | ||
|
|
2ef330c62b | ||
|
|
72e71aff40 | ||
|
|
cefd6ec752 | ||
|
|
aec3de18aa | ||
|
|
97620cb583 | ||
|
|
cf4e8190a4 | ||
|
|
8af7436f13 | ||
|
|
c53520ae56 | ||
|
|
3adcaef455 | ||
|
|
43cd9722a9 | ||
|
|
f3154e770e | ||
|
|
44aca4aeda | ||
|
|
5762342d6c | ||
|
|
2236ed467e | ||
|
|
d244b830ac | ||
|
|
e89b68fe1e | ||
|
|
1f588c5b13 | ||
|
|
38cad143a0 | ||
|
|
f52bcc5389 | ||
|
|
caa6f7bcd8 | ||
|
|
207a8cef20 | ||
|
|
d44a4b260e | ||
|
|
80a8e0b55c | ||
|
|
2d60f830f7 | ||
|
|
90751a09f7 | ||
|
|
301da4bb2a | ||
|
|
c90cd46095 | ||
|
|
7826000384 | ||
|
|
b48dcdd5e1 | ||
|
|
4f6326b139 | ||
|
|
cff96eeccc | ||
|
|
5eb4a3adec | ||
|
|
b60908a5e9 | ||
|
|
124441238b | ||
|
|
4ec0287a74 | ||
|
|
05c67a5c99 | ||
|
|
f507a3628c | ||
|
|
89e9d2b2d1 | ||
|
|
595858ac4b | ||
|
|
3f2162fe62 | ||
|
|
f2cb30409a | ||
|
|
5c4fec5a2f | ||
|
|
e96c119432 | ||
|
|
0920962d72 | ||
|
|
aaeb0fff78 | ||
|
|
de41f50945 | ||
|
|
0f51bf95e3 | ||
|
|
ba8cade6fc | ||
|
|
dbefae5a01 | ||
|
|
727b219742 | ||
|
|
fb25e44b58 | ||
|
|
fe2d394831 | ||
|
|
efd08ff1d6 | ||
|
|
4b861a5376 | ||
|
|
2076e7d5e8 | ||
|
|
37f6f1f90c | ||
|
|
81f76e0bd6 | ||
|
|
69b305cd8a | ||
|
|
45742a066e | ||
|
|
86fb8b3f7c | ||
|
|
9c0fc8e1d2 | ||
|
|
10d6302180 | ||
|
|
da7ed6992f | ||
|
|
32c9375ac8 | ||
|
|
7ed1334a51 | ||
|
|
d9097896f6 | ||
|
|
359b16e700 | ||
|
|
0237af771d | ||
|
|
86cce5b205 | ||
|
|
fc7a2e0c6d | ||
|
|
f74d135922 | ||
|
|
66124370a6 | ||
|
|
e9fc2bb73a | ||
|
|
12f6b94ece | ||
|
|
d9a8243d36 | ||
|
|
232c54885e | ||
|
|
42c43f6c78 | ||
|
|
9d2494834c | ||
|
|
a7613435ea | ||
|
|
c9df044e1a | ||
|
|
5a37787042 | ||
|
|
923cc3ba01 | ||
|
|
8fcf72f564 | ||
|
|
a7f7666ccd | ||
|
|
c181948afe | ||
|
|
ce9ecd7b93 | ||
|
|
354d19af1b | ||
|
|
6d996baf5d | ||
|
|
4120d09220 | ||
|
|
67a23bfe33 | ||
|
|
08b710930d | ||
|
|
7ec3b332ed | ||
|
|
722eb060f0 | ||
|
|
ce740aed68 | ||
|
|
09f582daa1 | ||
|
|
1b5f7b0d76 | ||
|
|
2c93e9a587 | ||
|
|
ab0976981b | ||
|
|
959ce42137 | ||
|
|
1c25db9179 | ||
|
|
810ddd7717 | ||
|
|
7bbff114a4 | ||
|
|
99475fc311 | ||
|
|
df121eb294 | ||
|
|
138194a149 | ||
|
|
ab99213d8c | ||
|
|
2fe923678e | ||
|
|
34f194c714 | ||
|
|
2bef398d4c | ||
|
|
404b5c45dd | ||
|
|
f607ade508 | ||
|
|
158ed84ec5 | ||
|
|
7c22713d7d | ||
|
|
fb94077260 | ||
|
|
d3491869bd | ||
|
|
5cefadbf37 | ||
|
|
95462b0c1d | ||
|
|
258330f985 | ||
|
|
a09b661b51 | ||
|
|
a1a0b935c7 | ||
|
|
4c11d9c6d5 | ||
|
|
78c34a342d | ||
|
|
20820c47d4 | ||
|
|
502c8a7e02 | ||
|
|
ce8a198acd | ||
|
|
32f171d861 | ||
|
|
b83c0e0aba | ||
|
|
831c0444d6 | ||
|
|
e5ef2eb5c6 | ||
|
|
7b80d78dc5 | ||
|
|
48f7103205 | ||
|
|
bc8de096be | ||
|
|
ba3189fd46 | ||
|
|
33d67cbe3d | ||
|
|
28ca54547e | ||
|
|
f8de069567 | ||
|
|
2cd6e52e9c | ||
|
|
372d3f17cc | ||
|
|
92d5b2eb3e | ||
|
|
6be55e30d9 | ||
|
|
fd517ccbe2 | ||
|
|
c2a34b4079 | ||
|
|
ce0f036bef | ||
|
|
977e143b4e | ||
|
|
d847ccf0f4 | ||
|
|
7eeed76539 | ||
|
|
2e452993ff | ||
|
|
f4dbd03c7e | ||
|
|
312c6cd550 | ||
|
|
8d9e8565f0 | ||
|
|
d1c10e4895 | ||
|
|
232c059e4f | ||
|
|
5bb9d15e27 | ||
|
|
879034c9c6 | ||
|
|
740aacbbf1 | ||
|
|
fcfab79bed | ||
|
|
468e34aa3d | ||
|
|
7ff7318089 | ||
|
|
4654bff737 | ||
|
|
3075ccb4b9 | ||
|
|
4894ab9035 | ||
|
|
4a09d99322 | ||
|
|
51b5f6264d | ||
|
|
724c804971 | ||
|
|
2ba86767fe | ||
|
|
391424d8a1 | ||
|
|
e0db6d5a57 | ||
|
|
87dc24e8a2 | ||
|
|
5233f5a07b | ||
|
|
478ee59bb0 | ||
|
|
b6f6b1ae9d | ||
|
|
1ad4290487 | ||
|
|
61480abd2e | ||
|
|
c094a27c97 | ||
|
|
83704ca4b5 | ||
|
|
60576388c5 | ||
|
|
9f172c308c | ||
|
|
d7312d26f7 | ||
|
|
4e6ef6ac53 | ||
|
|
de563f9ebf | ||
|
|
3982d77775 | ||
|
|
24bbbf6cb1 | ||
|
|
9ddd5de008 | ||
|
|
87a4598391 | ||
|
|
701c143149 | ||
|
|
43097b93e5 | ||
|
|
e303a80683 | ||
|
|
5defc20e9f | ||
|
|
d75eff62e3 | ||
|
|
ad9f0c00d0 | ||
|
|
cd908fa358 | ||
|
|
2bf79dbc80 | ||
|
|
4c729a405d | ||
|
|
28c9f9ac96 | ||
|
|
2820caf955 | ||
|
|
ba5ea7407b | ||
|
|
1bc406b0d9 | ||
|
|
7e27ceb885 | ||
|
|
252edaa2ca | ||
|
|
9a6fad4db5 | ||
|
|
127bcc14eb | ||
|
|
220d634f80 | ||
|
|
6291af2865 | ||
|
|
e9e808d339 | ||
|
|
780e4a6e9e | ||
|
|
c4bc2f24d6 | ||
|
|
d23ddd0e0b | ||
|
|
4f0ee79409 | ||
|
|
7b07445c5d | ||
|
|
dcbf4bfef8 | ||
|
|
6ec870bb08 | ||
|
|
2c6dbb42c1 | ||
|
|
98725dce04 | ||
|
|
2d3363153a | ||
|
|
9318c1c6fb | ||
|
|
11d49fb70f | ||
|
|
056286636d | ||
|
|
d020ed0b13 | ||
|
|
30b4cb4068 | ||
|
|
1aa1d29d97 | ||
|
|
4f8c7afc76 | ||
|
|
c2ee688176 | ||
|
|
f58b815ef8 | ||
|
|
daaf91b836 | ||
|
|
dee5994b1e | ||
|
|
0c1f533747 | ||
|
|
58d3a59e58 | ||
|
|
811008ee1c | ||
|
|
23af0de34a | ||
|
|
b96ea89f90 | ||
|
|
77a624a889 | ||
|
|
40892f3e91 | ||
|
|
b12dac1e35 | ||
|
|
150dcd2d5d | ||
|
|
c599d2837b | ||
|
|
bb6fb6b9ea | ||
|
|
8b8be2d7ca | ||
|
|
00d386f19f | ||
|
|
4870801f8f | ||
|
|
78745366c2 | ||
|
|
2be771cbcc | ||
|
|
6abc0e7d02 | ||
|
|
95220b913a | ||
|
|
a2c1f5ce7d | ||
|
|
5208a266e4 | ||
|
|
dc9c1712ff | ||
|
|
f9360683e9 | ||
|
|
456f50620b | ||
|
|
d76008582f | ||
|
|
6e9cbb9b28 | ||
|
|
b5f799528d | ||
|
|
f410657133 | ||
|
|
b1e0124aff | ||
|
|
b3e3bbdd9b | ||
|
|
fac3edaea7 | ||
|
|
08488e9bad | ||
|
|
835d54e90c | ||
|
|
10c3abd29a | ||
|
|
906e9ddb5c | ||
|
|
c165c8e301 | ||
|
|
7d63e05900 | ||
|
|
03f409a803 | ||
|
|
f427234373 |
@@ -1,4 +1,7 @@
|
||||
./.github
|
||||
./build
|
||||
./dist
|
||||
./coverage
|
||||
./node_modules
|
||||
./test
|
||||
./shlink-web-client.gif
|
||||
./dist
|
||||
|
||||
@@ -26,7 +26,10 @@
|
||||
"no-console": "warn",
|
||||
"template-curly-spacing": ["error", "never"],
|
||||
"no-warning-comments": "off",
|
||||
"no-magic-numbers": "off",
|
||||
"no-undefined": "off",
|
||||
"no-inline-comments": "off",
|
||||
"lines-around-comment": "off",
|
||||
"indent": ["error", 2, {
|
||||
"SwitchCase": 1
|
||||
}
|
||||
|
||||
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
github: ['acelaya']
|
||||
custom: ['https://acel.me/donate']
|
||||
6
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
<!--
|
||||
Before opening an issue, just take into account that this is a completely free of charge open source project.
|
||||
I'm always happy to help and provide support, but some understanding will be required.
|
||||
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personal if an issue gets eventually closed.
|
||||
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
|
||||
-->
|
||||
35
.github/ISSUE_TEMPLATE/Bug.md
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Something on shlink is broken or not working as documented?
|
||||
labels: bug
|
||||
---
|
||||
|
||||
<!--
|
||||
Before opening an issue, just take into account that this is a completely free of charge open source project.
|
||||
I'm always happy to help and provide support, but some understanding will be required.
|
||||
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personal if an issue gets eventually closed.
|
||||
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
|
||||
|
||||
With that said, please fill in the information requested next. More information might be requested once the issue is open.
|
||||
-->
|
||||
|
||||
#### Shlink web client version
|
||||
|
||||
* Version: x.y.z
|
||||
* How do you use shlink-web-client: app.shlink.io|Docker image|self-hosted
|
||||
|
||||
#### Summary
|
||||
|
||||
<!-- Provide a summary describing the problem you are experiencing. -->
|
||||
|
||||
#### Current behavior
|
||||
|
||||
<!-- How is it actually behaving (and it shouldn't)? -->
|
||||
|
||||
#### Expected behavior
|
||||
|
||||
<!-- How did you expected to behave? -->
|
||||
|
||||
#### How to reproduce
|
||||
|
||||
<!-- Provide steps to reproduce the bug. -->
|
||||
18
.github/ISSUE_TEMPLATE/Feature_Request.md
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Do you find shlink is missing some important feature that would make it more useful?
|
||||
labels: feature
|
||||
---
|
||||
|
||||
<!--
|
||||
Before opening an issue, just take into account that this is a completely free of charge open source project.
|
||||
I'm always happy to help and provide support, but some understanding will be required.
|
||||
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personal if an issue gets eventually closed.
|
||||
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
|
||||
|
||||
With that said, please fill in the information requested next. More information might be requested once the issue is open.
|
||||
-->
|
||||
|
||||
#### Summary
|
||||
|
||||
<!-- Describe the new feature you would like to request. -->
|
||||
23
.github/ISSUE_TEMPLATE/Question_Support.md
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: Question - Support
|
||||
about: Do you have a problem setting up or using shlink?
|
||||
labels: question
|
||||
---
|
||||
|
||||
<!--
|
||||
Before opening an issue, just take into account that this is a completely free of charge open source project.
|
||||
I'm always happy to help and provide support, but some understanding will be required.
|
||||
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personal if an issue gets eventually closed.
|
||||
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
|
||||
|
||||
With that said, please fill in the information requested next. More information might be requested once the issue is open.
|
||||
-->
|
||||
|
||||
#### Shlink web client version
|
||||
|
||||
* Version: x.y.z
|
||||
* How do you use shlink-web-client: app.shlink.io|Docker image|self-hosted
|
||||
|
||||
#### Summary
|
||||
|
||||
<!-- Describe the issue you are facing here. -->
|
||||
16
.gitignore
vendored
@@ -1,24 +1,16 @@
|
||||
# See https://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
package-lock.json
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
/.stryker-tmp
|
||||
/reports
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
docker-compose.override.yml
|
||||
home
|
||||
public/servers.json*
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
build:
|
||||
environment:
|
||||
node: v10.4.1
|
||||
node: v12.14.1
|
||||
tools:
|
||||
external_code_coverage: true
|
||||
external_code_coverage:
|
||||
timeout: 1200
|
||||
|
||||
59
.travis.yml
@@ -1,35 +1,58 @@
|
||||
dist: bionic
|
||||
|
||||
language: node_js
|
||||
|
||||
sudo: false
|
||||
|
||||
node_js:
|
||||
- "stable"
|
||||
jobs:
|
||||
fast_finish: true
|
||||
include:
|
||||
- name: "Docker publish"
|
||||
node_js: '12.16.3'
|
||||
if: NOT type = pull_request
|
||||
env:
|
||||
- DOCKER_PUBLISH="true"
|
||||
- name: "CI"
|
||||
node_js: '12.16.3'
|
||||
env:
|
||||
- DOCKER_PUBLISH="false"
|
||||
allow_failures:
|
||||
- name: "Docker publish"
|
||||
|
||||
cache:
|
||||
yarn: true
|
||||
directories:
|
||||
- node_modules
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
install:
|
||||
- yarn install
|
||||
- if [[ ${DOCKER_PUBLISH} == 'true' ]]; then sudo bash ./scripts/docker/install-docker ; fi
|
||||
- if [[ ${DOCKER_PUBLISH} == 'false' ]]; then npm ci ; fi
|
||||
|
||||
before_script:
|
||||
- if [[ ${DOCKER_PUBLISH} == 'false' ]]; then echo "Building commit range ${TRAVIS_COMMIT_RANGE}" ; fi
|
||||
- if [[ ${DOCKER_PUBLISH} == 'false' ]]; then export MUTATION_FILES=$(git diff ${TRAVIS_COMMIT_RANGE:-origin/master} --name-only | grep -E 'src\/(.*).(js|ts|jsx|tsx)$' | paste -sd ",") ; fi
|
||||
|
||||
script:
|
||||
- yarn lint
|
||||
- yarn test:ci
|
||||
- if [[ -z $TRAVIS_TAG ]]; then yarn build ; fi
|
||||
- if [[ ${DOCKER_PUBLISH} == 'true' ]]; then bash ./scripts/docker/build ; fi
|
||||
- if [[ ${DOCKER_PUBLISH} == 'false' ]]; then npm run lint ; fi
|
||||
- if [[ ${DOCKER_PUBLISH} == 'false' ]]; then npm run test:ci ; fi
|
||||
- if [[ ${DOCKER_PUBLISH} == 'false' ]]; then docker build -t shlink-web-client:test . ; fi
|
||||
- if [[ ${DOCKER_PUBLISH} == 'false' ]]; then npm run mutate:ci ; fi
|
||||
|
||||
after_success:
|
||||
- yarn ocular coverage/clover.xml
|
||||
- if [[ ${DOCKER_PUBLISH} == 'false' ]]; then node_modules/.bin/ocular coverage/clover.xml ; fi
|
||||
|
||||
# Before deploying, build dist file for current travis tag
|
||||
before_deploy:
|
||||
- yarn build ${TRAVIS_TAG#?}
|
||||
- if [[ ! -z $TRAVIS_TAG && ${DOCKER_PUBLISH} == 'false' ]]; then npm run build ${TRAVIS_TAG#?} ; fi
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key:
|
||||
secure: jBvPwC7EAbViaNR83rwMSt5XQDK0Iu9rgvEMa7GoyShbHcvUCCPd73Tu9quNpKi6NKsDY3INHgtch3vgonjGNGDGJ+yDyIBzXcvsAX2x3UcHpRbgY12uiINVmQxBI1+OVQB016Nm+cKC/i5Z36K4EmDbYfo+MrKndngM6AjcQFTwI8EwniIMaQgg4gNes//K8NhP5u0c3gwG+Q6jEGnq6uH3kcRgh6/epIZYpQyxjqWqKwF77sgcYj+X2Nf6XxtB5neuCi301UKLoLx8G0skh/Lm6KAIO4s9iIhIFa3UpoF21Ka0TxLpd2JxalLryCnFGlWWE6lxC9Htmc0TeRowJQlGdJXCskJ37xT9MljKY0fwNMu06VS/FUgykuCv+jP3zQu51pKu7Ew7+WeNPjautoOTu54VkdGyHcf2ThBNEyJQuiEwAQe4u7yAxY6R5ovEdvHBSIg4w1E5/Mxy5SMTCUlIAv6H7QQ1X9Z/zJm9HH5KeKz5tsHvQ/RIdSpgHXq/tC8o4Yup/LCFucXfrgvy/8pJoO1UpOlmvm62974NFfo0EG5YWwv6brUqz3QXpMjb8sWqgjltYMYJX3J7WZ34rIc+zt4NAmfhqgczaOC4pUGCiJ8jX3rMWIaQRn1AJ+5V337jL9fNDpTHny4phQjHrMJ1e0HZuNp0Xb5Q8wgqDPM=
|
||||
file: "./dist/shlink-web-client_${TRAVIS_TAG#?}_dist.zip"
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
- provider: releases
|
||||
api_key:
|
||||
secure: jBvPwC7EAbViaNR83rwMSt5XQDK0Iu9rgvEMa7GoyShbHcvUCCPd73Tu9quNpKi6NKsDY3INHgtch3vgonjGNGDGJ+yDyIBzXcvsAX2x3UcHpRbgY12uiINVmQxBI1+OVQB016Nm+cKC/i5Z36K4EmDbYfo+MrKndngM6AjcQFTwI8EwniIMaQgg4gNes//K8NhP5u0c3gwG+Q6jEGnq6uH3kcRgh6/epIZYpQyxjqWqKwF77sgcYj+X2Nf6XxtB5neuCi301UKLoLx8G0skh/Lm6KAIO4s9iIhIFa3UpoF21Ka0TxLpd2JxalLryCnFGlWWE6lxC9Htmc0TeRowJQlGdJXCskJ37xT9MljKY0fwNMu06VS/FUgykuCv+jP3zQu51pKu7Ew7+WeNPjautoOTu54VkdGyHcf2ThBNEyJQuiEwAQe4u7yAxY6R5ovEdvHBSIg4w1E5/Mxy5SMTCUlIAv6H7QQ1X9Z/zJm9HH5KeKz5tsHvQ/RIdSpgHXq/tC8o4Yup/LCFucXfrgvy/8pJoO1UpOlmvm62974NFfo0EG5YWwv6brUqz3QXpMjb8sWqgjltYMYJX3J7WZ34rIc+zt4NAmfhqgczaOC4pUGCiJ8jX3rMWIaQRn1AJ+5V337jL9fNDpTHny4phQjHrMJ1e0HZuNp0Xb5Q8wgqDPM=
|
||||
file: "./dist/shlink-web-client_${TRAVIS_TAG#?}_dist.zip"
|
||||
skip_cleanup: true
|
||||
on:
|
||||
all_branches: true
|
||||
condition: ${DOCKER_PUBLISH} == 'false'
|
||||
tags: true
|
||||
|
||||
381
CHANGELOG.md
@@ -4,6 +4,387 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
|
||||
|
||||
## 2.5.1 - 2020-06-06
|
||||
|
||||
#### Added
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#254](https://github.com/shlinkio/shlink-web-client/issues/254) Reduced duplication on code to handle mercure topics binding.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#276](https://github.com/shlinkio/shlink-web-client/issues/276) Fixed default grouping used for visits line chart, making it be dynamic depending on how old the short URL is.
|
||||
* [#280](https://github.com/shlinkio/shlink-web-client/issues/280) Fixed shlink-web-client version not being properly passed when building stable tags of the docker image.
|
||||
* [#269](https://github.com/shlinkio/shlink-web-client/issues/269) Fixed doughnut chart legends getting to big and hiding charts on mobile devices.
|
||||
|
||||
|
||||
## 2.5.0 - 2020-05-31
|
||||
|
||||
#### Added
|
||||
|
||||
* [#148](https://github.com/shlinkio/shlink-web-client/issues/148) Added support for real-time updates when consuming a Shlink version that is integrated with a mercure hub server.
|
||||
|
||||
The integration is transparent. When a server is opened, shlink-web-client will try to get the mercure info from it.
|
||||
|
||||
* If it works, it will setup the necessary `EventSource`s, dispatching redux actions when an event is pushed, which will in turn update the UI.
|
||||
* If it fails, it will assume it is either not configured or not supported by the Shlink version.
|
||||
|
||||
* [#265](https://github.com/shlinkio/shlink-web-client/issues/265) Updated tags section to allow displaying number of short URLs using every tag and number of visits for all short URLs using the tag.
|
||||
|
||||
This will work only when using Shlink v2.2.0 or above. For previous versions, the tags page will continue behaving the same.
|
||||
|
||||
* [#261](https://github.com/shlinkio/shlink-web-client/issues/261) Added new page to show visit stats by tag.
|
||||
|
||||
This new page will return a "not found" error when the server is lower than v2.2.0, as older versions do not support fetching stats by tag.
|
||||
|
||||
* [#253](https://github.com/shlinkio/shlink-web-client/issues/253) Created new settings page that will be used to define customizations in the app.
|
||||
|
||||
* [#149](https://github.com/shlinkio/shlink-web-client/issues/149) and [#198](https://github.com/shlinkio/shlink-web-client/issues/198) Added new line chart to visits and tags stats which displays amount of visits during selected time period, grouped by month, week, day or hour.
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#218](https://github.com/shlinkio/shlink-web-client/issues/218) Added back button to sections not displayed in left menu.
|
||||
* [#255](https://github.com/shlinkio/shlink-web-client/issues/255) Improved how servers and settings are persisted in the local storage.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#262](https://github.com/shlinkio/shlink-web-client/issues/262) Fixed charts displaying decimal numbers, when visits are absolute and that makes no sense.
|
||||
|
||||
|
||||
## 2.4.0 - 2020-04-10
|
||||
|
||||
#### Added
|
||||
|
||||
* [#199](https://github.com/shlinkio/shlink-web-client/issues/199) Added table to visits page which displays the information in a paginated, sortable and filterable list.
|
||||
|
||||
It also supports selecting multiple visits in the table which makes the corresponding data to be highlighted in the visits charts.
|
||||
|
||||
* [#241](https://github.com/shlinkio/shlink-web-client/issues/241) Added support to select charts bars in order to highlight related stats in other charts.
|
||||
|
||||
It also selects the visits in the new table, and you can even combine a selection in the chart and in the table.
|
||||
|
||||
* [#213](https://github.com/shlinkio/shlink-web-client/issues/213) The versions of both shlink-web-client and currently consumed Shlink server are now displayed in the footer.
|
||||
* [#221](https://github.com/shlinkio/shlink-web-client/issues/221) Improved how servers are handled, displaying meaningful errors when a not-found or a not-reachable server is tried to be loaded.
|
||||
* [#226](https://github.com/shlinkio/shlink-web-client/issues/226) Created servers can now be edited.
|
||||
* [#234](https://github.com/shlinkio/shlink-web-client/issues/234) Allowed short code length to be edited on any new short URL when using Shlink 2.1 or higher.
|
||||
* [#235](https://github.com/shlinkio/shlink-web-client/issues/235) Allowed editing the long URL for any existing short URL when suing Shlink 2.1 or higher.
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#205](https://github.com/shlinkio/shlink-web-client/issues/205) Replaced `jest-each` package by jet's native `test.each` function.
|
||||
* [#209](https://github.com/shlinkio/shlink-web-client/issues/209) Replaced `Unknown` by `Direct` for visits from undetermined referrers.
|
||||
* [#212](https://github.com/shlinkio/shlink-web-client/issues/212) Moved copy-to-clipboard next to short URL.
|
||||
* [#208](https://github.com/shlinkio/shlink-web-client/issues/208) Short URLs list paginator is now progressive.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#243](https://github.com/shlinkio/shlink-web-client/issues/243) Fixed loading state and resetting on short URL creation form.
|
||||
* [#239](https://github.com/shlinkio/shlink-web-client/issues/239) Fixed how user agents are parsed, reducing false results.
|
||||
|
||||
|
||||
## 2.3.1 - 2020-02-08
|
||||
|
||||
#### Added
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#191](https://github.com/shlinkio/shlink-web-client/issues/191) Created `ForServerVersion` helper component which dynamically renders children if current server conditions are met.
|
||||
* [#189](https://github.com/shlinkio/shlink-web-client/issues/189) Simplified short url tags and short url deletion components and reducers, by removing redundant actions.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#193](https://github.com/shlinkio/shlink-web-client/issues/193) Fixed `maxVisits` being set to 0 when trying to reset it from having a value to `null`.
|
||||
* [#196](https://github.com/shlinkio/shlink-web-client/issues/196) Included apache `.htaccess` file which takes care of falling back to index.html when reloading the page on a client-side handled route.
|
||||
* [#179](https://github.com/shlinkio/shlink-web-client/issues/179) Ensured domain is provided to Shlink server when editing, deleting or fetching short URLs which do not belong to default domain.
|
||||
* [#202](https://github.com/shlinkio/shlink-web-client/issues/202) Fixed domain not passed when dispatching actions that affect a single short URL (edit tags, edit meta and delete), which cased the list not to be properly updated.
|
||||
|
||||
|
||||
## 2.3.0 - 2020-01-19
|
||||
|
||||
#### Added
|
||||
|
||||
* [#174](https://github.com/shlinkio/shlink-web-client/issues/174) Added complete support for Shlink v2.x together with currently supported Shlink versions.
|
||||
* [#164](https://github.com/shlinkio/shlink-web-client/issues/164) Added max visits control on those URLs which have `maxVisits`.
|
||||
* [#178](https://github.com/shlinkio/shlink-web-client/issues/178) Short URLs list can now be filtered by date range.
|
||||
* [#46](https://github.com/shlinkio/shlink-web-client/issues/46) Allowed short URL's metadata to be edited (`maxVisits`, `validSince` and `validUntil`).
|
||||
|
||||
#### Changed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#170](https://github.com/shlinkio/shlink-web-client/issues/170) Fixed apple icon referencing to incorrect file names.
|
||||
|
||||
|
||||
## 2.2.2 - 2019-10-21
|
||||
|
||||
#### Added
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#167](https://github.com/shlinkio/shlink-web-client/issues/167) Fixed `/servers.json` path not being ignored when returning something other than an array.
|
||||
|
||||
|
||||
## 2.2.1 - 2019-10-18
|
||||
|
||||
#### Added
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#165](https://github.com/shlinkio/shlink-web-client/issues/165) Fixed error thrown when opening "create" page while using a Shlink version which does not return a valid SemVer version (like `latest` docker image, or any development instance).
|
||||
|
||||
|
||||
## 2.2.0 - 2019-10-05
|
||||
|
||||
#### Added
|
||||
|
||||
* [#144](https://github.com/shlinkio/shlink-web-client/issues/144) Added domain input to create domain page.
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#140](https://github.com/shlinkio/shlink-web-client/issues/140) Updated project dependencies.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
|
||||
## 2.1.1 - 2019-09-22
|
||||
|
||||
#### Added
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#142](https://github.com/shlinkio/shlink-web-client/issues/142) Updated to newer versions of base docker images for dev and production.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#151](https://github.com/shlinkio/shlink-web-client/issues/151) Fixed "order by" indicator (caret) still indicate ASC on column header when no order is specified.
|
||||
* [#157](https://github.com/shlinkio/shlink-web-client/issues/157) Fixed pagination control on graphs expanding too much when lots of pages need to be rendered.
|
||||
* [#155](https://github.com/shlinkio/shlink-web-client/issues/155) Fixed client-side paths resolve to 404 when served from nginx in docker image instead of falling back to `index.html`.
|
||||
|
||||
|
||||
## 2.1.0 - 2019-05-19
|
||||
|
||||
#### Added
|
||||
|
||||
* [#101](https://github.com/shlinkio/shlink-web-client/issues/101) Added checkbox to short URL creation form that allows to determine the value of the `findIfExists` flag introduced in Shlink v1.16.0.
|
||||
* [#105](https://github.com/shlinkio/shlink-web-client/issues/105) Added support to pre-configure servers. See [how to pre-configure servers](README.md#pre-configuring-servers) to get more details on how to do it.
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#125](https://github.com/shlinkio/shlink-web-client/issues/125) Refactored reducers to replace `switch` statements by `handleActions` from [redux-actions](https://github.com/redux-utilities/redux-actions).
|
||||
* [#116](https://github.com/shlinkio/shlink-web-client/issues/116) Removed sinon in favor of jest mocks.
|
||||
* [#72](https://github.com/shlinkio/shlink-web-client/issues/72) Increased code coverage up to 80%.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
|
||||
## 2.0.3 - 2019-03-16
|
||||
|
||||
#### Added
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#120](https://github.com/shlinkio/shlink-web-client/issues/120) Fixed crash when visits page is loaded and there are no visits with known cities.
|
||||
* [#113](https://github.com/shlinkio/shlink-web-client/issues/113) Ensured visits loading is cancelled when the visits page is unmounted. Requests on flight will still finish.
|
||||
* [#118](https://github.com/shlinkio/shlink-web-client/issues/118) Fixed chart crashing when trying to render lots of bars by adding pagination.
|
||||
|
||||
|
||||
## 2.0.2 - 2019-03-04
|
||||
|
||||
#### Added
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#103](https://github.com/shlinkio/shlink-web-client/issues/103) Fixed visits page getting freezed when loading large amounts of visits.
|
||||
* [#111](https://github.com/shlinkio/shlink-web-client/issues/111) Fixed crash when trying to load a map modal with only one location.
|
||||
* [#115](https://github.com/shlinkio/shlink-web-client/issues/115) Created `ErrorHandler` component which will prevent crashes in app to make it unusable.
|
||||
|
||||
|
||||
## 2.0.1 - 2019-03-03
|
||||
|
||||
#### Added
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#106](https://github.com/shlinkio/shlink-web-client/issues/106) Reduced size of docker image by using a multi-stage build Dockerfile.
|
||||
* [#95](https://github.com/shlinkio/shlink-web-client/issues/95) Tested docker image build during travis executions.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#104](https://github.com/shlinkio/shlink-web-client/issues/104) Fixed blank page being showed when not-found paths are loaded.
|
||||
* [#94](https://github.com/shlinkio/shlink-web-client/issues/94) Fixed initial zoom and center on maps.
|
||||
* [#93](https://github.com/shlinkio/shlink-web-client/issues/93) Prevented side menu to be swipeable while a modal window is displayed.
|
||||
|
||||
|
||||
## 2.0.0 - 2019-01-13
|
||||
|
||||
#### Added
|
||||
|
||||
* [#54](https://github.com/shlinkio/shlink-web-client/issues/54) Added stats by city graphic in visits page.
|
||||
* [#55](https://github.com/shlinkio/shlink-web-client/issues/55) Added map in visits page locating cities from which visits have occurred.
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#87](https://github.com/shlinkio/shlink-web-client/issues/87) and [#89](https://github.com/shlinkio/shlink-web-client/issues/89) Updated all dependencies to latest major versions.
|
||||
* [#96](https://github.com/shlinkio/shlink-web-client/issues/96) Updated visits page to load visits in multiple paginated requests of `5000` visits when used shlink server supports it. This will prevent shlink to hang when trying to load big amounts of visits.
|
||||
* [#71](https://github.com/shlinkio/shlink-web-client/issues/71) Improved tests and increased code coverage.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* [#59](https://github.com/shlinkio/shlink-web-client/issues/59) Dropped support for old browsers. Internet explorer and dead browsers are no longer supported.
|
||||
* [#97](https://github.com/shlinkio/shlink-web-client/issues/97) Dropped support for authentication via `Authorization` header with Bearer type and JWT, which will make this version no longer work with shlink earlier than v1.13.0.
|
||||
|
||||
#### Fixed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
|
||||
## 1.2.1 - 2018-12-21
|
||||
|
||||
#### Added
|
||||
|
||||
31
Dockerfile
@@ -1,21 +1,12 @@
|
||||
FROM nginx:1.15.7-alpine
|
||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||
FROM node:12.16.3-alpine as node
|
||||
COPY . /shlink-web-client
|
||||
ARG VERSION="latest"
|
||||
ENV VERSION ${VERSION}
|
||||
RUN cd /shlink-web-client && \
|
||||
npm install && npm run build -- ${VERSION} --no-dist
|
||||
|
||||
# Install node and yarn
|
||||
RUN apk add --no-cache --virtual nodejs && apk add --no-cache --virtual yarn
|
||||
|
||||
ADD . ./shlink-web-client
|
||||
|
||||
# Install dependencies and build project
|
||||
RUN cd ./shlink-web-client && \
|
||||
yarn install && \
|
||||
yarn build && \
|
||||
|
||||
# Move build contents to document root
|
||||
cd .. && \
|
||||
rm -r /usr/share/nginx/html/* && \
|
||||
mv ./shlink-web-client/build/* /usr/share/nginx/html && \
|
||||
rm -r ./shlink-web-client && \
|
||||
|
||||
# Delete and uninstall build tools
|
||||
yarn cache clean && apk del yarn && apk del nodejs
|
||||
FROM nginx:1.17.10-alpine
|
||||
LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>"
|
||||
RUN rm -r /usr/share/nginx/html && rm /etc/nginx/conf.d/default.conf
|
||||
COPY config/docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=node /shlink-web-client/build /usr/share/nginx/html
|
||||
|
||||
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 shlinkio
|
||||
Copyright (c) 2018-2020 shlinkio
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
76
README.md
@@ -4,47 +4,89 @@
|
||||
[](https://scrutinizer-ci.com/g/shlinkio/shlink-web-client/?branch=master)
|
||||
[](https://scrutinizer-ci.com/g/shlinkio/shlink-web-client/?branch=master)
|
||||
[](https://github.com/shlinkio/shlink-web-client/releases/latest)
|
||||
[](https://hub.docker.com/r/shlinkio/shlink-web-client/)
|
||||
[](https://github.com/shlinkio/shlink-web-client/blob/master/LICENSE)
|
||||
[](https://acel.me/donate)
|
||||
[](https://slnk.to/donate)
|
||||
|
||||
A ReactJS-based progressive web application for [Shlink](https://shlink.io).
|
||||
|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
There are three ways in which you can use this application.
|
||||
|
||||
* The easiest way to use shlink-web-client is by just going to https://app.shlink.io.
|
||||
### From app.shlink.io
|
||||
|
||||
The application runs 100% in the browser, so you can use that instance and access any shlink instance from it.
|
||||
The easiest way to use shlink-web-client is by just going to <https://app.shlink.io>.
|
||||
|
||||
* Self hosting the application yourself.
|
||||
The application runs 100% in the browser, so you can safely access any shlink instance from there.
|
||||
|
||||
Get the [latest release](https://github.com/shlinkio/shlink-web-client/releases/latest) and download the distributable zip file attached to it (`shlink-web-client_X.X.X_dist.zip`).
|
||||
### Docker image
|
||||
|
||||
The package contains static files only, so just put it in a folder and serve it with the web server of your choice.
|
||||
If you want to deploy shlink-web-client in a container-based cluster (kubernetes, docker swarm, etc), just pick the [shlinkio/shlink-web-client](https://hub.docker.com/r/shlinkio/shlink-web-client/) image and do it.
|
||||
|
||||
Provided dist files are configured to be served from the root of your domain. If you need to serve shlink-web-client from a subpath, you will have to build it yourself following [these simple steps](#serve-shlink-in-subpath).
|
||||
It's a lightweight [nginx:alpine](https://hub.docker.com/r/library/nginx/) image serving the static app on port 80.
|
||||
|
||||
* Use the official [docker image](https://hub.docker.com/r/shlinkio/shlink-web-client/)
|
||||
### Self-hosted
|
||||
|
||||
If you want to deploy shlink-web-client in a container-based cluster (kubernetes, docker swarm, etc), just pick the image and do it.
|
||||
If you want to self-host it yourself, get the [latest release](https://github.com/shlinkio/shlink-web-client/releases/latest) and download the distributable zip file attached to it (`shlink-web-client_X.X.X_dist.zip`).
|
||||
|
||||
It's a lightweight [nginx:alpine](https://hub.docker.com/r/library/nginx/) image serving the assets on port 80.
|
||||
The package contains static files only, so just put it in a folder and serve it with the web server of your choice.
|
||||
|
||||
**Considerations**:
|
||||
|
||||
* Provided dist files are configured to be served from the root of your domain. If you need to serve shlink-web-client from a subpath, you will have to build it yourself following [these steps](#serve-shlink-in-subpath).
|
||||
* The app has a client-side router that handles dynamic paths. Because of that, you need to configure your web server to fall-back to the `index.html` file when requested files do not exist.
|
||||
* If you use Apache, you are covered, since the project includes an `.htaccess` file which already does this.
|
||||
* If you use nginx, you can [see how it's done](config/docker/nginx.conf) for the docker image and do the same.
|
||||
|
||||
## Pre-configuring servers
|
||||
|
||||
The first time you access shlink-web-client from a browser, you will have to configure the list of shlink servers you want to manage, and they will be saved in the local storage.
|
||||
|
||||
Those servers can be exported and imported in other browsers, but if for some reason you need some servers to be there from the beginning, starting with shlink-web-client 2.1.0, you can provide a `servers.json` file in the project root folder (the same containing the `index.html`, `favicon.ico`, etc) with a structure like this:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "Main server",
|
||||
"url": "https://doma.in",
|
||||
"apiKey": "09c972b7-506b-49f1-a19a-d729e22e599c"
|
||||
},
|
||||
{
|
||||
"name": "Local",
|
||||
"url": "http://localhost:8080",
|
||||
"apiKey": "580d0b42-4dea-419a-96bf-6c876b901451"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
> The list can contain as many servers as you need.
|
||||
|
||||
If you are using the shlink-web-client docker image, you can mount the `servers.json` file in a volume inside `/usr/share/nginx/html`, which is the app's document root inside the container.
|
||||
|
||||
docker run --name shlink-web-client -p 8000:80 -v ${PWD}/servers.json:/usr/share/nginx/html/servers.json shlinkio/shlink-web-client
|
||||
|
||||
## Serve project in subpath
|
||||
|
||||
Official distributable files have been build so that they are served from the root of a domain.
|
||||
Official distributable files have been built so that they are served from the root of a domain.
|
||||
|
||||
If you need to host shlink-web-client yourself and serve it from a subpath, follow these steps:
|
||||
|
||||
* Download [node](https://nodejs.org/en/download/package-manager/) 10.4 or later (if you don't have it yet).
|
||||
* Download [yarn](https://yarnpkg.com/en/docs/install) package manager.
|
||||
* Download shlink-web-client source files for the version you want to build.
|
||||
* Download shlink-web-client source code for the version you want to build.
|
||||
* For example, if you want to build `v1.0.1`, use this link https://github.com/shlinkio/shlink-web-client/archive/v1.0.1.zip
|
||||
* Replace the `v1.0.1` part in the link with the one of the version you want to build.
|
||||
* Decompress the file and `cd` into the resulting folder.
|
||||
* Install project dependencies by running `yarn install`.
|
||||
* Open the `package.json` file in the root of the project, locate the `homepage` property and replace the value (which should be an empty string) by the path from which you want to serve shlink-web-client.
|
||||
* For example: `"homepage": "/my-projects/shlink-web-client",`.
|
||||
* Build the distributable contents by running `yarn build`.
|
||||
* Once the command finishes, you will have a `build` folder with all the static assets you need to run shlink-web-client. Just place them wherever you want them to be served from.
|
||||
* Build the project:
|
||||
* For classic hosting:
|
||||
* Download [node](https://nodejs.org/en/download/package-manager/) 10.15 or later.
|
||||
* Install project dependencies by running `npm install`.
|
||||
* Build the project by running `npm run build`.
|
||||
* Once the command finishes, you will have a `build` folder with all the static assets you need to run shlink-web-client. Just place them wherever you want them to be served from.
|
||||
* For docker image:
|
||||
* Download [docker](https://docs.docker.com/install/).
|
||||
* Build the docker image by running `docker build . -t shlink-web-client`.
|
||||
* Once the command finishes, you will have an image with the name `shlink-web-client`.
|
||||
|
||||
17
config/docker/nginx.conf
Normal file
@@ -0,0 +1,17 @@
|
||||
server {
|
||||
listen 80 default_server;
|
||||
charset utf-8;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# When requesting static paths with extension, try them, and return a 404 if not found
|
||||
location ~* .+\.(css|js|html|png|jpe?g|gif|bmp|ico|json|csv|otf|eot|svg|svgz|ttf|woff|woff2|ijmap|pdf|tif|map) {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
# When requesting a path without extension, try it, and return the index if not found
|
||||
# This allows HTML5 history paths to be handled by the client application
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html$is_args$args;
|
||||
}
|
||||
}
|
||||
@@ -43,12 +43,12 @@ dotenvFiles.forEach((dotenvFile) => {
|
||||
|
||||
// We support resolving modules according to `NODE_PATH`.
|
||||
// This lets you use absolute paths in imports inside large monorepos:
|
||||
// https://github.com/facebookincubator/create-react-app/issues/253.
|
||||
// https://github.com/facebook/create-react-app/issues/253.
|
||||
// It works similar to `NODE_PATH` in Node itself:
|
||||
// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders
|
||||
// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored.
|
||||
// Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims.
|
||||
// https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421
|
||||
// https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421
|
||||
// We also resolve them to make sure all tools using them work consistently.
|
||||
const appDirectory = fs.realpathSync(process.cwd());
|
||||
|
||||
|
||||
@@ -6,6 +6,24 @@ const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
process(src, filename) {
|
||||
return `module.exports = ${JSON.stringify(path.basename(filename))};`;
|
||||
const assetFilename = JSON.stringify(path.basename(filename));
|
||||
|
||||
if (filename.match(/\.svg$/)) {
|
||||
return `module.exports = {
|
||||
__esModule: true,
|
||||
default: ${assetFilename},
|
||||
ReactComponent: (props) => ({
|
||||
$$typeof: Symbol.for('react.element'),
|
||||
type: 'svg',
|
||||
ref: null,
|
||||
key: null,
|
||||
props: Object.assign({}, props, {
|
||||
children: ${assetFilename}
|
||||
})
|
||||
}),
|
||||
};`;
|
||||
}
|
||||
|
||||
return `module.exports = ${assetFilename};`;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,22 +4,22 @@ const fs = require('fs');
|
||||
const url = require('url');
|
||||
|
||||
// Make sure any symlinks in the project folder are resolved:
|
||||
// https://github.com/facebookincubator/create-react-app/issues/637
|
||||
// https://github.com/facebook/create-react-app/issues/637
|
||||
const appDirectory = fs.realpathSync(process.cwd());
|
||||
const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath);
|
||||
|
||||
const envPublicUrl = process.env.PUBLIC_URL;
|
||||
|
||||
function ensureSlash(path, needsSlash) {
|
||||
const hasSlash = path.endsWith('/');
|
||||
function ensureSlash(inputPath, needsSlash) {
|
||||
const hasSlash = inputPath.endsWith('/');
|
||||
|
||||
if (hasSlash && !needsSlash) {
|
||||
return path.substr(path, path.length - 1);
|
||||
return inputPath.substr(0, inputPath.length - 1);
|
||||
} else if (!hasSlash && needsSlash) {
|
||||
return `${path}/`;
|
||||
return `${inputPath}/`;
|
||||
}
|
||||
|
||||
return path;
|
||||
return inputPath;
|
||||
}
|
||||
|
||||
const getPublicUrl = (appPackageJson) =>
|
||||
@@ -33,23 +33,55 @@ const getPublicUrl = (appPackageJson) =>
|
||||
// like /todos/42/static/js/bundle.7289d.js. We have to know the root.
|
||||
function getServedPath(appPackageJson) {
|
||||
const publicUrl = getPublicUrl(appPackageJson);
|
||||
const servedUrl = envPublicUrl || (publicUrl ? url.parse(publicUrl).pathname : '/');
|
||||
const servedUrl =
|
||||
envPublicUrl || (publicUrl ? url.parse(publicUrl).pathname : '/');
|
||||
|
||||
return ensureSlash(servedUrl, true);
|
||||
}
|
||||
|
||||
const moduleFileExtensions = [
|
||||
'web.mjs',
|
||||
'mjs',
|
||||
'web.js',
|
||||
'js',
|
||||
'web.ts',
|
||||
'ts',
|
||||
'web.tsx',
|
||||
'tsx',
|
||||
'json',
|
||||
'web.jsx',
|
||||
'jsx',
|
||||
];
|
||||
|
||||
// Resolve file paths in the same order as webpack
|
||||
const resolveModule = (resolveFn, filePath) => {
|
||||
const extension = moduleFileExtensions.find((extension) =>
|
||||
fs.existsSync(resolveFn(`${filePath}.${extension}`)));
|
||||
|
||||
if (extension) {
|
||||
return resolveFn(`${filePath}.${extension}`);
|
||||
}
|
||||
|
||||
return resolveFn(`${filePath}.js`);
|
||||
};
|
||||
|
||||
// config after eject: we're in ./config/
|
||||
module.exports = {
|
||||
dotenv: resolveApp('.env'),
|
||||
appPath: resolveApp('.'),
|
||||
appBuild: resolveApp('build'),
|
||||
appPublic: resolveApp('public'),
|
||||
appHtml: resolveApp('public/index.html'),
|
||||
appIndexJs: resolveApp('src/index.js'),
|
||||
appIndexJs: resolveModule(resolveApp, 'src/index'),
|
||||
appPackageJson: resolveApp('package.json'),
|
||||
appSrc: resolveApp('src'),
|
||||
appTsConfig: resolveApp('tsconfig.json'),
|
||||
yarnLockFile: resolveApp('yarn.lock'),
|
||||
testsSetup: resolveApp('src/setupTests.js'),
|
||||
testsSetup: resolveModule(resolveApp, 'src/setupTests'),
|
||||
proxySetup: resolveApp('src/setupProxy.js'),
|
||||
appNodeModules: resolveApp('node_modules'),
|
||||
publicUrl: getPublicUrl(resolveApp('package.json')),
|
||||
servedPath: getServedPath(resolveApp('package.json')),
|
||||
};
|
||||
|
||||
module.exports.moduleFileExtensions = moduleFileExtensions;
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
|
||||
if (typeof Promise === 'undefined') {
|
||||
// Rejection tracking prevents a common issue where React gets into an
|
||||
// inconsistent state due to an error, but it gets swallowed by a Promise,
|
||||
// and the user has no idea what causes React's erratic future behavior.
|
||||
require('promise/lib/rejection-tracking').enable();
|
||||
window.Promise = require('promise/lib/es6-extensions.js');
|
||||
}
|
||||
|
||||
// fetch() polyfill for making API calls.
|
||||
require('whatwg-fetch');
|
||||
|
||||
// Object.assign() is commonly used with React.
|
||||
// It will use the native implementation if it's present and isn't buggy.
|
||||
Object.assign = require('object-assign');
|
||||
|
||||
// In tests, polyfill requestAnimationFrame since jsdom doesn't provide it yet.
|
||||
// We don't polyfill it in the browser--this is user's responsibility.
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
require('raf').polyfill(global);
|
||||
}
|
||||
@@ -1,308 +0,0 @@
|
||||
|
||||
const path = require('path');
|
||||
const autoprefixer = require('autoprefixer');
|
||||
const webpack = require('webpack');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
|
||||
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
|
||||
const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
|
||||
const eslintFormatter = require('react-dev-utils/eslintFormatter');
|
||||
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
|
||||
const ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||
const getClientEnvironment = require('./env');
|
||||
const paths = require('./paths');
|
||||
|
||||
// Webpack uses `publicPath` to determine where the app is being served from.
|
||||
// In development, we always serve from the root. This makes config easier.
|
||||
const publicPath = '/';
|
||||
|
||||
// `publicUrl` is just like `publicPath`, but we will provide it to our app
|
||||
// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
|
||||
// Omit trailing slash as %PUBLIC_PATH%/xyz looks better than %PUBLIC_PATH%xyz.
|
||||
const publicUrl = '';
|
||||
|
||||
// Get environment variables to inject into our app.
|
||||
const env = getClientEnvironment(publicUrl);
|
||||
|
||||
const postCssLoader = {
|
||||
loader: require.resolve('postcss-loader'),
|
||||
options: {
|
||||
|
||||
// Necessary for external CSS imports to work
|
||||
// https://github.com/facebookincubator/create-react-app/issues/2677
|
||||
ident: 'postcss',
|
||||
plugins: () => [
|
||||
require('postcss-flexbugs-fixes'),
|
||||
autoprefixer({
|
||||
browsers: [
|
||||
'>1%',
|
||||
'last 4 versions',
|
||||
'Firefox ESR',
|
||||
'not ie < 9',
|
||||
],
|
||||
flexbox: 'no-2009',
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// This is the development configuration.
|
||||
// It is focused on developer experience and fast rebuilds.
|
||||
// The production configuration is different and lives in a separate file.
|
||||
module.exports = {
|
||||
|
||||
// You may want 'eval' instead if you prefer to see the compiled output in DevTools.
|
||||
// See the discussion in https://github.com/facebookincubator/create-react-app/issues/343.
|
||||
devtool: 'cheap-module-source-map',
|
||||
|
||||
// These are the "entry points" to our application.
|
||||
// This means they will be the "root" imports that are included in JS bundle.
|
||||
// The first two entry points enable "hot" CSS and auto-refreshes for JS.
|
||||
entry: [
|
||||
|
||||
// We ship a few polyfills by default:
|
||||
require.resolve('./polyfills'),
|
||||
|
||||
// Include an alternative client for WebpackDevServer. A client's job is to
|
||||
// connect to WebpackDevServer by a socket and get notified about changes.
|
||||
// When you save a file, the client will either apply hot updates (in case
|
||||
// of CSS changes), or refresh the page (in case of JS changes). When you
|
||||
// make a syntax error, this client will display a syntax error overlay.
|
||||
// Note: instead of the default WebpackDevServer client, we use a custom one
|
||||
// to bring better experience for Create React App users. You can replace
|
||||
// the line below with these two lines if you prefer the stock client:
|
||||
// require.resolve('webpack-dev-server/client') + '?/',
|
||||
// require.resolve('webpack/hot/dev-server'),
|
||||
require.resolve('react-dev-utils/webpackHotDevClient'),
|
||||
|
||||
// Finally, this is your app's code:
|
||||
paths.appIndexJs,
|
||||
|
||||
// We include the app code last so that if there is a runtime error during
|
||||
// initialization, it doesn't blow up the WebpackDevServer client, and
|
||||
// changing JS code would still trigger a refresh.
|
||||
],
|
||||
output: {
|
||||
|
||||
// Add /* filename */ comments to generated require()s in the output.
|
||||
pathinfo: true,
|
||||
|
||||
// This does not produce a real file. It's just the virtual path that is
|
||||
// served by WebpackDevServer in development. This is the JS bundle
|
||||
// containing code from all our entry points, and the Webpack runtime.
|
||||
filename: 'static/js/bundle.js',
|
||||
|
||||
// There are also additional JS chunk files if you use code splitting.
|
||||
chunkFilename: 'static/js/[name].chunk.js',
|
||||
|
||||
// This is the URL that app is served from. We use "/" in development.
|
||||
publicPath,
|
||||
|
||||
// Point sourcemap entries to original disk location (format as URL on Windows)
|
||||
devtoolModuleFilenameTemplate: (info) =>
|
||||
path.resolve(info.absoluteResourcePath).replace(/\\/g, '/'),
|
||||
},
|
||||
resolve: {
|
||||
|
||||
// This allows you to set a fallback for where Webpack should look for modules.
|
||||
// We placed these paths second because we want `node_modules` to "win"
|
||||
// if there are any conflicts. This matches Node resolution mechanism.
|
||||
// https://github.com/facebookincubator/create-react-app/issues/253
|
||||
modules: [ 'node_modules', paths.appNodeModules ].concat(
|
||||
|
||||
// It is guaranteed to exist because we tweak it in `env.js`
|
||||
process.env.NODE_PATH.split(path.delimiter).filter(Boolean)
|
||||
),
|
||||
|
||||
// These are the reasonable defaults supported by the Node ecosystem.
|
||||
// We also include JSX as a common component filename extension to support
|
||||
// some tools, although we do not recommend using it, see:
|
||||
// https://github.com/facebookincubator/create-react-app/issues/290
|
||||
// `web` extension prefixes have been added for better support
|
||||
// for React Native Web.
|
||||
extensions: [ '.web.js', '.mjs', '.js', '.json', '.web.jsx', '.jsx' ],
|
||||
alias: {
|
||||
|
||||
// Support React Native Web
|
||||
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
|
||||
'react-native': 'react-native-web',
|
||||
},
|
||||
plugins: [
|
||||
|
||||
// Prevents users from importing files from outside of src/ (or node_modules/).
|
||||
// This often causes confusion because we only process files within src/ with babel.
|
||||
// To fix this, we prevent you from importing files out of src/ -- if you'd like to,
|
||||
// please link the files into your node_modules/ and let module-resolution kick in.
|
||||
// Make sure your source files are compiled, as they will not be processed in any way.
|
||||
new ModuleScopePlugin(paths.appSrc, [ paths.appPackageJson ]),
|
||||
],
|
||||
},
|
||||
module: {
|
||||
strictExportPresence: true,
|
||||
rules: [
|
||||
|
||||
// TODO: Disable require.ensure as it's not a standard language feature.
|
||||
// We are waiting for https://github.com/facebookincubator/create-react-app/issues/2176.
|
||||
// { parser: { requireEnsure: false } },
|
||||
|
||||
// First, run the linter.
|
||||
// It's important to do this before Babel processes the JS.
|
||||
{
|
||||
test: /\.(js|jsx|mjs)$/,
|
||||
enforce: 'pre',
|
||||
use: [
|
||||
{
|
||||
options: {
|
||||
formatter: eslintFormatter,
|
||||
eslintPath: require.resolve('eslint'),
|
||||
|
||||
},
|
||||
loader: require.resolve('eslint-loader'),
|
||||
},
|
||||
],
|
||||
include: paths.appSrc,
|
||||
},
|
||||
{
|
||||
|
||||
// "oneOf" will traverse all following loaders until one will
|
||||
// match the requirements. When no loader matches it will fall
|
||||
// back to the "file" loader at the end of the loader list.
|
||||
oneOf: [
|
||||
|
||||
// "url" loader works like "file" loader except that it embeds assets
|
||||
// smaller than specified limit in bytes as data URLs to avoid requests.
|
||||
// A missing `test` is equivalent to a match.
|
||||
{
|
||||
test: [ /\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/ ],
|
||||
loader: require.resolve('url-loader'),
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: 'static/media/[name].[hash:8].[ext]',
|
||||
},
|
||||
},
|
||||
|
||||
// Process JS with Babel.
|
||||
{
|
||||
test: /\.(js|jsx|mjs)$/,
|
||||
include: paths.appSrc,
|
||||
loader: require.resolve('babel-loader'),
|
||||
options: {
|
||||
|
||||
// This is a feature of `babel-loader` for webpack (not Babel itself).
|
||||
// It enables caching results in ./node_modules/.cache/babel-loader/
|
||||
// directory for faster rebuilds.
|
||||
cacheDirectory: true,
|
||||
},
|
||||
},
|
||||
|
||||
// "postcss" loader applies autoprefixer to our CSS.
|
||||
// "css" loader resolves paths in CSS and adds assets as dependencies.
|
||||
// "style" loader turns CSS into JS modules that inject <style> tags.
|
||||
// In production, we use a plugin to extract that CSS to a file, but
|
||||
// in development "style" loader enables hot editing of CSS.
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
require.resolve('style-loader'),
|
||||
{
|
||||
loader: require.resolve('css-loader'),
|
||||
options: {
|
||||
importLoaders: 1,
|
||||
},
|
||||
},
|
||||
postCssLoader,
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: ExtractTextPlugin.extract({
|
||||
fallback: 'style-loader',
|
||||
use: [ 'css-loader', 'sass-loader', postCssLoader ],
|
||||
}),
|
||||
},
|
||||
|
||||
// "file" loader makes sure those assets get served by WebpackDevServer.
|
||||
// When you `import` an asset, you get its (virtual) filename.
|
||||
// In production, they would get copied to the `build` folder.
|
||||
// This loader doesn't use a "test" so it will catch all modules
|
||||
// that fall through the other loaders.
|
||||
{
|
||||
|
||||
// Exclude `js` files to keep "css" loader working as it injects
|
||||
// its runtime that would otherwise processed through "file" loader.
|
||||
// Also exclude `html` and `json` extensions so they get processed
|
||||
// by webpacks internal loaders.
|
||||
exclude: [ /\.(js|jsx|mjs)$/, /\.html$/, /\.json$/ ],
|
||||
loader: require.resolve('file-loader'),
|
||||
options: {
|
||||
name: 'static/media/[name].[hash:8].[ext]',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ** STOP ** Are you adding a new loader?
|
||||
// Make sure to add the new loader(s) before the "file" loader.
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
|
||||
// Makes some environment variables available in index.html.
|
||||
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
|
||||
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
// In development, this will be an empty string.
|
||||
new InterpolateHtmlPlugin(env.raw),
|
||||
|
||||
// Generates an `index.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin({
|
||||
inject: true,
|
||||
template: paths.appHtml,
|
||||
}),
|
||||
|
||||
// Add module names to factory functions so they appear in browser profiler.
|
||||
new webpack.NamedModulesPlugin(),
|
||||
|
||||
// Makes some environment variables available to the JS code, for example:
|
||||
// if (process.env.NODE_ENV === 'development') { ... }. See `./env.js`.
|
||||
new webpack.DefinePlugin(env.stringified),
|
||||
|
||||
// This is necessary to emit hot updates (currently CSS only):
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
|
||||
// Watcher doesn't work well if you mistype casing in a path so we use
|
||||
// a plugin that prints an error when you attempt to do this.
|
||||
// See https://github.com/facebookincubator/create-react-app/issues/240
|
||||
new CaseSensitivePathsPlugin(),
|
||||
|
||||
// If you require a missing module and then `npm install` it, you still have
|
||||
// to restart the development server for Webpack to discover it. This plugin
|
||||
// makes the discovery automatic so you don't have to restart.
|
||||
// See https://github.com/facebookincubator/create-react-app/issues/186
|
||||
new WatchMissingNodeModulesPlugin(paths.appNodeModules),
|
||||
|
||||
// Moment.js is an extremely popular library that bundles large locale files
|
||||
// by default due to how Webpack interprets its code. This is a practical
|
||||
// solution that requires the user to opt into importing specific locales.
|
||||
// https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
|
||||
// You can remove this if you don't use Moment.js:
|
||||
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
|
||||
new ExtractTextPlugin('main.css'),
|
||||
],
|
||||
|
||||
// Some libraries import Node modules but don't use them in the browser.
|
||||
// Tell Webpack to provide empty mocks for them so importing them works.
|
||||
node: {
|
||||
dgram: 'empty',
|
||||
fs: 'empty',
|
||||
net: 'empty',
|
||||
tls: 'empty',
|
||||
child_process: 'empty',
|
||||
},
|
||||
|
||||
// Turn off performance hints during development because we don't do any
|
||||
// splitting or minification in interest of speed. These warnings become
|
||||
// cumbersome.
|
||||
performance: {
|
||||
hints: false,
|
||||
},
|
||||
};
|
||||
678
config/webpack.config.js
Normal file
@@ -0,0 +1,678 @@
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const resolve = require('resolve');
|
||||
const PnpWebpackPlugin = require('pnp-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
|
||||
const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
|
||||
const safePostCssParser = require('postcss-safe-parser');
|
||||
const ManifestPlugin = require('webpack-manifest-plugin');
|
||||
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
|
||||
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
|
||||
const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
|
||||
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
|
||||
const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');
|
||||
const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin');
|
||||
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin-alt');
|
||||
const typescriptFormatter = require('react-dev-utils/typescriptFormatter');
|
||||
const getClientEnvironment = require('./env');
|
||||
const paths = require('./paths');
|
||||
|
||||
// Source maps are resource heavy and can cause out of memory issue for large source files.
|
||||
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
|
||||
|
||||
// Some apps do not need the benefits of saving a web request, so not inlining the chunk
|
||||
// makes for a smoother build process.
|
||||
const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false';
|
||||
|
||||
// Check if TypeScript is setup
|
||||
const useTypeScript = fs.existsSync(paths.appTsConfig);
|
||||
|
||||
// style files regexes
|
||||
const cssRegex = /\.css$/;
|
||||
const cssModuleRegex = /\.module\.css$/;
|
||||
const sassRegex = /\.(scss|sass)$/;
|
||||
const sassModuleRegex = /\.module\.(scss|sass)$/;
|
||||
|
||||
// This is the production and development configuration.
|
||||
// It is focused on developer experience, fast rebuilds, and a minimal bundle.
|
||||
/* eslint-disable complexity */
|
||||
module.exports = (webpackEnv) => {
|
||||
const isEnvDevelopment = webpackEnv === 'development';
|
||||
const isEnvProduction = webpackEnv === 'production';
|
||||
|
||||
// Webpack uses `publicPath` to determine where the app is being served from.
|
||||
// It requires a trailing slash, or the file assets will get an incorrect path.
|
||||
// In development, we always serve from the root. This makes config easier.
|
||||
const publicPath = isEnvProduction
|
||||
? paths.servedPath
|
||||
: isEnvDevelopment && '/';
|
||||
|
||||
// Some apps do not use client-side routing with pushState.
|
||||
// For these, "homepage" can be set to "." to enable relative asset paths.
|
||||
const shouldUseRelativeAssetPaths = publicPath === './';
|
||||
|
||||
// `publicUrl` is just like `publicPath`, but we will provide it to our app
|
||||
// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
|
||||
// Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz.
|
||||
const publicUrl = isEnvProduction
|
||||
? publicPath.slice(0, -1)
|
||||
: isEnvDevelopment && '';
|
||||
|
||||
// Get environment variables to inject into our app.
|
||||
const env = getClientEnvironment(publicUrl);
|
||||
|
||||
// common function to get style loaders
|
||||
const getStyleLoaders = (cssOptions, preProcessor) => {
|
||||
const loaders = [
|
||||
isEnvDevelopment && require.resolve('style-loader'),
|
||||
isEnvProduction && {
|
||||
loader: MiniCssExtractPlugin.loader,
|
||||
options: Object.assign(
|
||||
{},
|
||||
shouldUseRelativeAssetPaths ? { publicPath: '../../' } : undefined
|
||||
),
|
||||
},
|
||||
{
|
||||
loader: require.resolve('css-loader'),
|
||||
options: cssOptions,
|
||||
},
|
||||
{
|
||||
|
||||
// Options for PostCSS as we reference these options twice
|
||||
// Adds vendor prefixing based on your specified browser support in
|
||||
// package.json
|
||||
loader: require.resolve('postcss-loader'),
|
||||
options: {
|
||||
|
||||
// Necessary for external CSS imports to work
|
||||
// https://github.com/facebook/create-react-app/issues/2677
|
||||
ident: 'postcss',
|
||||
plugins: () => [
|
||||
require('postcss-flexbugs-fixes'),
|
||||
require('postcss-preset-env')({
|
||||
autoprefixer: {
|
||||
flexbox: 'no-2009',
|
||||
},
|
||||
stage: 3,
|
||||
}),
|
||||
],
|
||||
sourceMap: isEnvProduction && shouldUseSourceMap,
|
||||
},
|
||||
},
|
||||
].filter(Boolean);
|
||||
|
||||
if (preProcessor) {
|
||||
loaders.push({
|
||||
loader: require.resolve(preProcessor),
|
||||
options: {
|
||||
sourceMap: isEnvProduction && shouldUseSourceMap,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return loaders;
|
||||
};
|
||||
|
||||
return {
|
||||
mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',
|
||||
|
||||
// Stop compilation early in production
|
||||
bail: isEnvProduction,
|
||||
devtool: isEnvProduction
|
||||
? shouldUseSourceMap
|
||||
? 'source-map'
|
||||
: false
|
||||
: isEnvDevelopment && 'cheap-module-source-map',
|
||||
|
||||
// These are the "entry points" to our application.
|
||||
// This means they will be the "root" imports that are included in JS bundle.
|
||||
entry: [
|
||||
|
||||
// Include an alternative client for WebpackDevServer. A client's job is to
|
||||
// connect to WebpackDevServer by a socket and get notified about changes.
|
||||
// When you save a file, the client will either apply hot updates (in case
|
||||
// of CSS changes), or refresh the page (in case of JS changes). When you
|
||||
// make a syntax error, this client will display a syntax error overlay.
|
||||
// Note: instead of the default WebpackDevServer client, we use a custom one
|
||||
// to bring better experience for Create React App users. You can replace
|
||||
// the line below with these two lines if you prefer the stock client:
|
||||
// require.resolve('webpack-dev-server/client') + '?/',
|
||||
// require.resolve('webpack/hot/dev-server'),
|
||||
isEnvDevelopment &&
|
||||
require.resolve('react-dev-utils/webpackHotDevClient'),
|
||||
|
||||
// Finally, this is your app's code:
|
||||
paths.appIndexJs,
|
||||
|
||||
// We include the app code last so that if there is a runtime error during
|
||||
// initialization, it doesn't blow up the WebpackDevServer client, and
|
||||
// changing JS code would still trigger a refresh.
|
||||
].filter(Boolean),
|
||||
output: {
|
||||
|
||||
// The build folder.
|
||||
path: isEnvProduction ? paths.appBuild : undefined,
|
||||
|
||||
// Add /* filename */ comments to generated require()s in the output.
|
||||
pathinfo: isEnvDevelopment,
|
||||
|
||||
// There will be one main bundle, and one file per asynchronous chunk.
|
||||
// In development, it does not produce real files.
|
||||
filename: isEnvProduction
|
||||
? 'static/js/[name].[chunkhash:8].js'
|
||||
: isEnvDevelopment && 'static/js/bundle.js',
|
||||
|
||||
// There are also additional JS chunk files if you use code splitting.
|
||||
chunkFilename: isEnvProduction
|
||||
? 'static/js/[name].[chunkhash:8].chunk.js'
|
||||
: isEnvDevelopment && 'static/js/[name].chunk.js',
|
||||
|
||||
// We inferred the "public path" (such as / or /my-project) from homepage.
|
||||
// We use "/" in development.
|
||||
publicPath,
|
||||
|
||||
// Point sourcemap entries to original disk location (format as URL on Windows)
|
||||
devtoolModuleFilenameTemplate: isEnvProduction
|
||||
? (info) =>
|
||||
path
|
||||
.relative(paths.appSrc, info.absoluteResourcePath)
|
||||
.replace(/\\/g, '/')
|
||||
: isEnvDevelopment &&
|
||||
((info) => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
|
||||
},
|
||||
optimization: {
|
||||
minimize: isEnvProduction,
|
||||
minimizer: [
|
||||
|
||||
// This is only used in production mode
|
||||
new TerserPlugin({
|
||||
terserOptions: {
|
||||
parse: {
|
||||
|
||||
// we want terser to parse ecma 8 code. However, we don't want it
|
||||
// to apply any minfication steps that turns valid ecma 5 code
|
||||
// into invalid ecma 5 code. This is why the 'compress' and 'output'
|
||||
// sections only apply transformations that are ecma 5 safe
|
||||
// https://github.com/facebook/create-react-app/pull/4234
|
||||
ecma: 8,
|
||||
},
|
||||
compress: {
|
||||
ecma: 5,
|
||||
warnings: false,
|
||||
|
||||
// Disabled because of an issue with Uglify breaking seemingly valid code:
|
||||
// https://github.com/facebook/create-react-app/issues/2376
|
||||
// Pending further investigation:
|
||||
// https://github.com/mishoo/UglifyJS2/issues/2011
|
||||
comparisons: false,
|
||||
|
||||
// Disabled because of an issue with Terser breaking valid code:
|
||||
// https://github.com/facebook/create-react-app/issues/5250
|
||||
// Pending futher investigation:
|
||||
// https://github.com/terser-js/terser/issues/120
|
||||
inline: 2,
|
||||
},
|
||||
mangle: {
|
||||
safari10: true,
|
||||
},
|
||||
output: {
|
||||
ecma: 5,
|
||||
comments: false,
|
||||
|
||||
// Turned on because emoji and regex is not minified properly using default
|
||||
// https://github.com/facebook/create-react-app/issues/2488
|
||||
ascii_only: true,
|
||||
},
|
||||
},
|
||||
|
||||
// Use multi-process parallel running to improve the build speed
|
||||
// Default number of concurrent runs: os.cpus().length - 1
|
||||
parallel: true,
|
||||
|
||||
// Enable file caching
|
||||
cache: true,
|
||||
sourceMap: shouldUseSourceMap,
|
||||
}),
|
||||
|
||||
// This is only used in production mode
|
||||
new OptimizeCSSAssetsPlugin({
|
||||
cssProcessorOptions: {
|
||||
parser: safePostCssParser,
|
||||
map: shouldUseSourceMap
|
||||
? {
|
||||
|
||||
// `inline: false` forces the sourcemap to be output into a
|
||||
// separate file
|
||||
inline: false,
|
||||
|
||||
// `annotation: true` appends the sourceMappingURL to the end of
|
||||
// the css file, helping the browser find the sourcemap
|
||||
annotation: true,
|
||||
}
|
||||
: false,
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
// Automatically split vendor and commons
|
||||
// https://twitter.com/wSokra/status/969633336732905474
|
||||
// https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366
|
||||
splitChunks: {
|
||||
chunks: 'all',
|
||||
name: false,
|
||||
},
|
||||
|
||||
// Keep the runtime chunk separated to enable long term caching
|
||||
// https://twitter.com/wSokra/status/969679223278505985
|
||||
runtimeChunk: true,
|
||||
},
|
||||
resolve: {
|
||||
|
||||
// This allows you to set a fallback for where Webpack should look for modules.
|
||||
// We placed these paths second because we want `node_modules` to "win"
|
||||
// if there are any conflicts. This matches Node resolution mechanism.
|
||||
// https://github.com/facebook/create-react-app/issues/253
|
||||
modules: [ 'node_modules' ].concat(
|
||||
|
||||
// It is guaranteed to exist because we tweak it in `env.js`
|
||||
process.env.NODE_PATH.split(path.delimiter).filter(Boolean)
|
||||
),
|
||||
|
||||
// These are the reasonable defaults supported by the Node ecosystem.
|
||||
// We also include JSX as a common component filename extension to support
|
||||
// some tools, although we do not recommend using it, see:
|
||||
// https://github.com/facebook/create-react-app/issues/290
|
||||
// `web` extension prefixes have been added for better support
|
||||
// for React Native Web.
|
||||
extensions: paths.moduleFileExtensions
|
||||
.map((ext) => `.${ext}`)
|
||||
.filter((ext) => useTypeScript || !ext.includes('ts')),
|
||||
alias: {
|
||||
|
||||
// Support React Native Web
|
||||
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
|
||||
'react-native': 'react-native-web',
|
||||
},
|
||||
plugins: [
|
||||
|
||||
// Adds support for installing with Plug'n'Play, leading to faster installs and adding
|
||||
// guards against forgotten dependencies and such.
|
||||
PnpWebpackPlugin,
|
||||
|
||||
// Prevents users from importing files from outside of src/ (or node_modules/).
|
||||
// This often causes confusion because we only process files within src/ with babel.
|
||||
// To fix this, we prevent you from importing files out of src/ -- if you'd like to,
|
||||
// please link the files into your node_modules/ and let module-resolution kick in.
|
||||
// Make sure your source files are compiled, as they will not be processed in any way.
|
||||
new ModuleScopePlugin(paths.appSrc, [ paths.appPackageJson ]),
|
||||
],
|
||||
},
|
||||
resolveLoader: {
|
||||
plugins: [
|
||||
|
||||
// Also related to Plug'n'Play, but this time it tells Webpack to load its loaders
|
||||
// from the current package.
|
||||
PnpWebpackPlugin.moduleLoader(module),
|
||||
],
|
||||
},
|
||||
module: {
|
||||
strictExportPresence: true,
|
||||
rules: [
|
||||
|
||||
// Disable require.ensure as it's not a standard language feature.
|
||||
{ parser: { requireEnsure: false } },
|
||||
|
||||
// First, run the linter.
|
||||
// It's important to do this before Babel processes the JS.
|
||||
{
|
||||
test: /\.(js|mjs|jsx)$/,
|
||||
enforce: 'pre',
|
||||
use: [
|
||||
{
|
||||
options: {
|
||||
formatter: require.resolve('react-dev-utils/eslintFormatter'),
|
||||
eslintPath: require.resolve('eslint'),
|
||||
|
||||
},
|
||||
loader: require.resolve('eslint-loader'),
|
||||
},
|
||||
],
|
||||
include: paths.appSrc,
|
||||
},
|
||||
{
|
||||
|
||||
// "oneOf" will traverse all following loaders until one will
|
||||
// match the requirements. When no loader matches it will fall
|
||||
// back to the "file" loader at the end of the loader list.
|
||||
oneOf: [
|
||||
|
||||
// "url" loader works like "file" loader except that it embeds assets
|
||||
// smaller than specified limit in bytes as data URLs to avoid requests.
|
||||
// A missing `test` is equivalent to a match.
|
||||
{
|
||||
test: [ /\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/ ],
|
||||
loader: require.resolve('url-loader'),
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: 'static/media/[name].[hash:8].[ext]',
|
||||
},
|
||||
},
|
||||
|
||||
// Process application JS with Babel.
|
||||
// The preset includes JSX, Flow, TypeScript, and some ESnext features.
|
||||
{
|
||||
test: /\.(js|mjs|jsx|ts|tsx)$/,
|
||||
include: paths.appSrc,
|
||||
loader: require.resolve('babel-loader'),
|
||||
options: {
|
||||
customize: require.resolve(
|
||||
'babel-preset-react-app/webpack-overrides'
|
||||
),
|
||||
|
||||
plugins: [
|
||||
[
|
||||
require.resolve('babel-plugin-named-asset-import'),
|
||||
{
|
||||
loaderMap: {
|
||||
svg: {
|
||||
ReactComponent:
|
||||
'@svgr/webpack?-prettier,-svgo![path]',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
|
||||
// This is a feature of `babel-loader` for webpack (not Babel itself).
|
||||
// It enables caching results in ./node_modules/.cache/babel-loader/
|
||||
// directory for faster rebuilds.
|
||||
cacheDirectory: true,
|
||||
cacheCompression: isEnvProduction,
|
||||
compact: isEnvProduction,
|
||||
},
|
||||
},
|
||||
|
||||
// Process any JS outside of the app with Babel.
|
||||
// Unlike the application JS, we only compile the standard ES features.
|
||||
{
|
||||
test: /\.(js|mjs)$/,
|
||||
exclude: /@babel(?:\/|\\{1,2})runtime/,
|
||||
loader: require.resolve('babel-loader'),
|
||||
options: {
|
||||
babelrc: false,
|
||||
configFile: false,
|
||||
compact: false,
|
||||
presets: [
|
||||
[
|
||||
require.resolve('babel-preset-react-app/dependencies'),
|
||||
{ helpers: true },
|
||||
],
|
||||
],
|
||||
cacheDirectory: true,
|
||||
cacheCompression: isEnvProduction,
|
||||
|
||||
// If an error happens in a package, it's possible to be
|
||||
// because it was compiled. Thus, we don't want the browser
|
||||
// debugger to show the original code. Instead, the code
|
||||
// being evaluated would be much more helpful.
|
||||
sourceMaps: false,
|
||||
},
|
||||
},
|
||||
|
||||
// "postcss" loader applies autoprefixer to our CSS.
|
||||
// "css" loader resolves paths in CSS and adds assets as dependencies.
|
||||
// "style" loader turns CSS into JS modules that inject <style> tags.
|
||||
// In production, we use MiniCSSExtractPlugin to extract that CSS
|
||||
// to a file, but in development "style" loader enables hot editing
|
||||
// of CSS.
|
||||
// By default we support CSS Modules with the extension .module.css
|
||||
{
|
||||
test: cssRegex,
|
||||
exclude: cssModuleRegex,
|
||||
use: getStyleLoaders({
|
||||
importLoaders: 1,
|
||||
sourceMap: isEnvProduction && shouldUseSourceMap,
|
||||
}),
|
||||
|
||||
// Don't consider CSS imports dead code even if the
|
||||
// containing package claims to have no side effects.
|
||||
// Remove this when webpack adds a warning or an error for this.
|
||||
// See https://github.com/webpack/webpack/issues/6571
|
||||
sideEffects: true,
|
||||
},
|
||||
|
||||
// Adds support for CSS Modules (https://github.com/css-modules/css-modules)
|
||||
// using the extension .module.css
|
||||
{
|
||||
test: cssModuleRegex,
|
||||
use: getStyleLoaders({
|
||||
importLoaders: 1,
|
||||
sourceMap: isEnvProduction && shouldUseSourceMap,
|
||||
modules: true,
|
||||
getLocalIdent: getCSSModuleLocalIdent,
|
||||
}),
|
||||
},
|
||||
|
||||
// Opt-in support for SASS (using .scss or .sass extensions).
|
||||
// By default we support SASS Modules with the
|
||||
// extensions .module.scss or .module.sass
|
||||
{
|
||||
test: sassRegex,
|
||||
exclude: sassModuleRegex,
|
||||
use: getStyleLoaders(
|
||||
{
|
||||
importLoaders: 2,
|
||||
sourceMap: isEnvProduction && shouldUseSourceMap,
|
||||
},
|
||||
'sass-loader'
|
||||
),
|
||||
|
||||
// Don't consider CSS imports dead code even if the
|
||||
// containing package claims to have no side effects.
|
||||
// Remove this when webpack adds a warning or an error for this.
|
||||
// See https://github.com/webpack/webpack/issues/6571
|
||||
sideEffects: true,
|
||||
},
|
||||
|
||||
// Adds support for CSS Modules, but using SASS
|
||||
// using the extension .module.scss or .module.sass
|
||||
{
|
||||
test: sassModuleRegex,
|
||||
use: getStyleLoaders(
|
||||
{
|
||||
importLoaders: 2,
|
||||
sourceMap: isEnvProduction && shouldUseSourceMap,
|
||||
modules: true,
|
||||
getLocalIdent: getCSSModuleLocalIdent,
|
||||
},
|
||||
'sass-loader'
|
||||
),
|
||||
},
|
||||
|
||||
// "file" loader makes sure those assets get served by WebpackDevServer.
|
||||
// When you `import` an asset, you get its (virtual) filename.
|
||||
// In production, they would get copied to the `build` folder.
|
||||
// This loader doesn't use a "test" so it will catch all modules
|
||||
// that fall through the other loaders.
|
||||
{
|
||||
loader: require.resolve('file-loader'),
|
||||
|
||||
// Exclude `js` files to keep "css" loader working as it injects
|
||||
// its runtime that would otherwise be processed through "file" loader.
|
||||
// Also exclude `html` and `json` extensions so they get processed
|
||||
// by webpacks internal loaders.
|
||||
exclude: [ /\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/ ],
|
||||
options: {
|
||||
name: 'static/media/[name].[hash:8].[ext]',
|
||||
},
|
||||
},
|
||||
|
||||
// ** STOP ** Are you adding a new loader?
|
||||
// Make sure to add the new loader(s) before the "file" loader.
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
|
||||
// Generates an `index.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin(
|
||||
Object.assign(
|
||||
{},
|
||||
{
|
||||
inject: true,
|
||||
template: paths.appHtml,
|
||||
},
|
||||
isEnvProduction
|
||||
? {
|
||||
minify: {
|
||||
removeComments: true,
|
||||
collapseWhitespace: true,
|
||||
removeRedundantAttributes: true,
|
||||
useShortDoctype: true,
|
||||
removeEmptyAttributes: true,
|
||||
removeStyleLinkTypeAttributes: true,
|
||||
keepClosingSlash: true,
|
||||
minifyJS: true,
|
||||
minifyCSS: true,
|
||||
minifyURLs: true,
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
)
|
||||
),
|
||||
|
||||
// Inlines the webpack runtime script. This script is too small to warrant
|
||||
// a network request.
|
||||
isEnvProduction &&
|
||||
shouldInlineRuntimeChunk &&
|
||||
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [ /runtime~.+[.]js/ ]),
|
||||
|
||||
// Makes some environment variables available in index.html.
|
||||
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
|
||||
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
// In production, it will be an empty string unless you specify "homepage"
|
||||
// in `package.json`, in which case it will be the pathname of that URL.
|
||||
// In development, this will be an empty string.
|
||||
new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
|
||||
|
||||
// This gives some necessary context to module not found errors, such as
|
||||
// the requesting resource.
|
||||
new ModuleNotFoundPlugin(paths.appPath),
|
||||
|
||||
// Makes some environment variables available to the JS code, for example:
|
||||
// if (process.env.NODE_ENV === 'production') { ... }. See `./env.js`.
|
||||
// It is absolutely essential that NODE_ENV is set to production
|
||||
// during a production build.
|
||||
// Otherwise React will be compiled in the very slow development mode.
|
||||
new webpack.DefinePlugin(env.stringified),
|
||||
|
||||
// This is necessary to emit hot updates (currently CSS only):
|
||||
isEnvDevelopment && new webpack.HotModuleReplacementPlugin(),
|
||||
|
||||
// Watcher doesn't work well if you mistype casing in a path so we use
|
||||
// a plugin that prints an error when you attempt to do this.
|
||||
// See https://github.com/facebook/create-react-app/issues/240
|
||||
isEnvDevelopment && new CaseSensitivePathsPlugin(),
|
||||
|
||||
// If you require a missing module and then `npm install` it, you still have
|
||||
// to restart the development server for Webpack to discover it. This plugin
|
||||
// makes the discovery automatic so you don't have to restart.
|
||||
// See https://github.com/facebook/create-react-app/issues/186
|
||||
isEnvDevelopment &&
|
||||
new WatchMissingNodeModulesPlugin(paths.appNodeModules),
|
||||
isEnvProduction &&
|
||||
new MiniCssExtractPlugin({
|
||||
|
||||
// Options similar to the same options in webpackOptions.output
|
||||
// both options are optional
|
||||
filename: 'static/css/[name].[contenthash:8].css',
|
||||
chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
|
||||
}),
|
||||
|
||||
// Generate a manifest file which contains a mapping of all asset filenames
|
||||
// to their corresponding output file so that tools can pick it up without
|
||||
// having to parse `index.html`.
|
||||
new ManifestPlugin({
|
||||
fileName: 'asset-manifest.json',
|
||||
publicPath,
|
||||
}),
|
||||
|
||||
// Moment.js is an extremely popular library that bundles large locale files
|
||||
// by default due to how Webpack interprets its code. This is a practical
|
||||
// solution that requires the user to opt into importing specific locales.
|
||||
// https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
|
||||
// You can remove this if you don't use Moment.js:
|
||||
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
|
||||
|
||||
// Generate a service worker script that will precache, and keep up to date,
|
||||
// the HTML & assets that are part of the Webpack build.
|
||||
isEnvProduction &&
|
||||
new WorkboxWebpackPlugin.GenerateSW({
|
||||
clientsClaim: true,
|
||||
exclude: [ /\.map$/, /asset-manifest\.json$/ ],
|
||||
importWorkboxFrom: 'cdn',
|
||||
navigateFallback: `${publicUrl}/index.html`,
|
||||
navigateFallbackBlacklist: [
|
||||
|
||||
// Exclude URLs starting with /_, as they're likely an API call
|
||||
new RegExp('^/_'),
|
||||
|
||||
// Exclude URLs containing a dot, as they're likely a resource in
|
||||
// public/ and not a SPA route
|
||||
new RegExp('/[^/]+\\.[^/]+$'),
|
||||
],
|
||||
}),
|
||||
|
||||
// TypeScript type checking
|
||||
useTypeScript &&
|
||||
new ForkTsCheckerWebpackPlugin({
|
||||
typescript: resolve.sync('typescript', {
|
||||
basedir: paths.appNodeModules,
|
||||
}),
|
||||
async: false,
|
||||
checkSyntacticErrors: true,
|
||||
tsconfig: paths.appTsConfig,
|
||||
compilerOptions: {
|
||||
module: 'esnext',
|
||||
moduleResolution: 'node',
|
||||
resolveJsonModule: true,
|
||||
isolatedModules: true,
|
||||
noEmit: true,
|
||||
jsx: 'preserve',
|
||||
},
|
||||
reportFiles: [
|
||||
'**',
|
||||
'!**/*.json',
|
||||
'!**/__tests__/**',
|
||||
'!**/?(*.)(spec|test).*',
|
||||
'!**/src/setupProxy.*',
|
||||
'!**/src/setupTests.*',
|
||||
],
|
||||
watch: paths.appSrc,
|
||||
silent: true,
|
||||
formatter: typescriptFormatter,
|
||||
}),
|
||||
].filter(Boolean),
|
||||
|
||||
// Some libraries import Node modules but don't use them in the browser.
|
||||
// Tell Webpack to provide empty mocks for them so importing them works.
|
||||
node: {
|
||||
dgram: 'empty',
|
||||
fs: 'empty',
|
||||
net: 'empty',
|
||||
tls: 'empty',
|
||||
child_process: 'empty',
|
||||
},
|
||||
|
||||
// Turn off performance processing because we utilize
|
||||
// our own hints via the FileSizeReporter
|
||||
performance: false,
|
||||
};
|
||||
};
|
||||
@@ -1,385 +0,0 @@
|
||||
|
||||
const path = require('path');
|
||||
const autoprefixer = require('autoprefixer');
|
||||
const webpack = require('webpack');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||
const ManifestPlugin = require('webpack-manifest-plugin');
|
||||
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
|
||||
const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin');
|
||||
const eslintFormatter = require('react-dev-utils/eslintFormatter');
|
||||
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
|
||||
const paths = require('./paths');
|
||||
const getClientEnvironment = require('./env');
|
||||
|
||||
// Webpack uses `publicPath` to determine where the app is being served from.
|
||||
// It requires a trailing slash, or the file assets will get an incorrect path.
|
||||
const publicPath = paths.servedPath;
|
||||
|
||||
// Some apps do not use client-side routing with pushState.
|
||||
// For these, "homepage" can be set to "." to enable relative asset paths.
|
||||
const shouldUseRelativeAssetPaths = publicPath === './';
|
||||
|
||||
// Source maps are resource heavy and can cause out of memory issue for large source files.
|
||||
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
|
||||
|
||||
// `publicUrl` is just like `publicPath`, but we will provide it to our app
|
||||
// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
|
||||
// Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz.
|
||||
const publicUrl = publicPath.slice(0, -1);
|
||||
|
||||
// Get environment variables to inject into our app.
|
||||
const env = getClientEnvironment(publicUrl);
|
||||
|
||||
// Assert this just to be safe.
|
||||
// Development builds of React are slow and not intended for production.
|
||||
if (env.stringified['process.env'].NODE_ENV !== '"production"') {
|
||||
throw new Error('Production builds must have NODE_ENV=production.');
|
||||
}
|
||||
|
||||
// Note: defined here because it will be used more than once.
|
||||
const cssFilename = 'static/css/[name].[contenthash:8].css';
|
||||
|
||||
// ExtractTextPlugin expects the build output to be flat.
|
||||
// (See https://github.com/webpack-contrib/extract-text-webpack-plugin/issues/27)
|
||||
// However, our output is structured with css, js and media folders.
|
||||
// To have this structure working with relative paths, we have to use custom options.
|
||||
// Making sure that the publicPath goes back to to build folder.
|
||||
const extractTextPluginOptions = shouldUseRelativeAssetPaths
|
||||
? { publicPath: Array(cssFilename.split('/').length).join('../') }
|
||||
: {};
|
||||
|
||||
// This is the production configuration.
|
||||
// It compiles slowly and is focused on producing a fast and minimal bundle.
|
||||
// The development configuration is different and lives in a separate file.
|
||||
module.exports = {
|
||||
|
||||
// Don't attempt to continue if there are any errors.
|
||||
bail: true,
|
||||
|
||||
// We generate sourcemaps in production. This is slow but gives good results.
|
||||
// You can exclude the *.map files from the build during deployment.
|
||||
devtool: shouldUseSourceMap ? 'source-map' : false,
|
||||
|
||||
// In production, we only want to load the polyfills and the app code.
|
||||
entry: [ require.resolve('./polyfills'), paths.appIndexJs ],
|
||||
output: {
|
||||
|
||||
// The build folder.
|
||||
path: paths.appBuild,
|
||||
|
||||
// Generated JS file names (with nested folders).
|
||||
// There will be one main bundle, and one file per asynchronous chunk.
|
||||
// We don't currently advertise code splitting but Webpack supports it.
|
||||
filename: 'static/js/[name].[chunkhash:8].js',
|
||||
chunkFilename: 'static/js/[name].[chunkhash:8].chunk.js',
|
||||
|
||||
// We inferred the "public path" (such as / or /my-project) from homepage.
|
||||
publicPath,
|
||||
|
||||
// Point sourcemap entries to original disk location (format as URL on Windows)
|
||||
devtoolModuleFilenameTemplate: (info) =>
|
||||
path
|
||||
.relative(paths.appSrc, info.absoluteResourcePath)
|
||||
.replace(/\\/g, '/'),
|
||||
},
|
||||
resolve: {
|
||||
|
||||
// This allows you to set a fallback for where Webpack should look for modules.
|
||||
// We placed these paths second because we want `node_modules` to "win"
|
||||
// if there are any conflicts. This matches Node resolution mechanism.
|
||||
// https://github.com/facebookincubator/create-react-app/issues/253
|
||||
modules: [ 'node_modules', paths.appNodeModules ].concat(
|
||||
|
||||
// It is guaranteed to exist because we tweak it in `env.js`
|
||||
process.env.NODE_PATH.split(path.delimiter).filter(Boolean)
|
||||
),
|
||||
|
||||
// These are the reasonable defaults supported by the Node ecosystem.
|
||||
// We also include JSX as a common component filename extension to support
|
||||
// some tools, although we do not recommend using it, see:
|
||||
// https://github.com/facebookincubator/create-react-app/issues/290
|
||||
// `web` extension prefixes have been added for better support
|
||||
// for React Native Web.
|
||||
extensions: [ '.web.js', '.mjs', '.js', '.json', '.web.jsx', '.jsx' ],
|
||||
alias: {
|
||||
|
||||
// Support React Native Web
|
||||
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
|
||||
'react-native': 'react-native-web',
|
||||
},
|
||||
plugins: [
|
||||
|
||||
// Prevents users from importing files from outside of src/ (or node_modules/).
|
||||
// This often causes confusion because we only process files within src/ with babel.
|
||||
// To fix this, we prevent you from importing files out of src/ -- if you'd like to,
|
||||
// please link the files into your node_modules/ and let module-resolution kick in.
|
||||
// Make sure your source files are compiled, as they will not be processed in any way.
|
||||
new ModuleScopePlugin(paths.appSrc, [ paths.appPackageJson ]),
|
||||
],
|
||||
},
|
||||
module: {
|
||||
strictExportPresence: true,
|
||||
rules: [
|
||||
|
||||
// TODO: Disable require.ensure as it's not a standard language feature.
|
||||
// We are waiting for https://github.com/facebookincubator/create-react-app/issues/2176.
|
||||
// { parser: { requireEnsure: false } },
|
||||
|
||||
// First, run the linter.
|
||||
// It's important to do this before Babel processes the JS.
|
||||
{
|
||||
test: /\.(js|jsx|mjs)$/,
|
||||
enforce: 'pre',
|
||||
use: [
|
||||
{
|
||||
options: {
|
||||
formatter: eslintFormatter,
|
||||
eslintPath: require.resolve('eslint'),
|
||||
|
||||
},
|
||||
loader: require.resolve('eslint-loader'),
|
||||
},
|
||||
],
|
||||
include: paths.appSrc,
|
||||
},
|
||||
{
|
||||
|
||||
// "oneOf" will traverse all following loaders until one will
|
||||
// match the requirements. When no loader matches it will fall
|
||||
// back to the "file" loader at the end of the loader list.
|
||||
oneOf: [
|
||||
|
||||
// "url" loader works just like "file" loader but it also embeds
|
||||
// assets smaller than specified size as data URLs to avoid requests.
|
||||
{
|
||||
test: [ /\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/ ],
|
||||
loader: require.resolve('url-loader'),
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: 'static/media/[name].[hash:8].[ext]',
|
||||
},
|
||||
},
|
||||
|
||||
// Process JS with Babel.
|
||||
{
|
||||
test: /\.(js|jsx|mjs)$/,
|
||||
include: paths.appSrc,
|
||||
loader: require.resolve('babel-loader'),
|
||||
options: {
|
||||
|
||||
compact: true,
|
||||
},
|
||||
},
|
||||
|
||||
// The notation here is somewhat confusing.
|
||||
// "postcss" loader applies autoprefixer to our CSS.
|
||||
// "css" loader resolves paths in CSS and adds assets as dependencies.
|
||||
// "style" loader normally turns CSS into JS modules injecting <style>,
|
||||
// but unlike in development configuration, we do something different.
|
||||
// `ExtractTextPlugin` first applies the "postcss" and "css" loaders
|
||||
// (second argument), then grabs the result CSS and puts it into a
|
||||
// separate file in our build process. This way we actually ship
|
||||
// a single CSS file in production instead of JS code injecting <style>
|
||||
// tags. If you use code splitting, however, any async bundles will still
|
||||
// use the "style" loader inside the async code so CSS from them won't be
|
||||
// in the main CSS file.
|
||||
{
|
||||
test: [ /\.css$/, /\.scss$/ ],
|
||||
loader: ExtractTextPlugin.extract(
|
||||
Object.assign(
|
||||
{
|
||||
fallback: {
|
||||
loader: require.resolve('style-loader'),
|
||||
options: {
|
||||
hmr: false,
|
||||
},
|
||||
},
|
||||
use: [
|
||||
{
|
||||
loader: require.resolve('css-loader'),
|
||||
options: {
|
||||
importLoaders: 1,
|
||||
minimize: true,
|
||||
sourceMap: shouldUseSourceMap,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: require.resolve('sass-loader'),
|
||||
},
|
||||
{
|
||||
loader: require.resolve('postcss-loader'),
|
||||
options: {
|
||||
|
||||
// Necessary for external CSS imports to work
|
||||
// https://github.com/facebookincubator/create-react-app/issues/2677
|
||||
ident: 'postcss',
|
||||
plugins: () => [
|
||||
require('postcss-flexbugs-fixes'),
|
||||
autoprefixer({
|
||||
browsers: [
|
||||
'>1%',
|
||||
'last 4 versions',
|
||||
'Firefox ESR',
|
||||
'not ie < 9',
|
||||
],
|
||||
flexbox: 'no-2009',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
extractTextPluginOptions
|
||||
)
|
||||
),
|
||||
|
||||
// Note: this won't work without `new ExtractTextPlugin()` in `plugins`.
|
||||
},
|
||||
|
||||
// "file" loader makes sure assets end up in the `build` folder.
|
||||
// When you `import` an asset, you get its filename.
|
||||
// This loader doesn't use a "test" so it will catch all modules
|
||||
// that fall through the other loaders.
|
||||
{
|
||||
loader: require.resolve('file-loader'),
|
||||
|
||||
// Exclude `js` files to keep "css" loader working as it injects
|
||||
// it's runtime that would otherwise processed through "file" loader.
|
||||
// Also exclude `html` and `json` extensions so they get processed
|
||||
// by webpacks internal loaders.
|
||||
exclude: [ /\.(js|jsx|mjs)$/, /\.html$/, /\.json$/ ],
|
||||
options: {
|
||||
name: 'static/media/[name].[hash:8].[ext]',
|
||||
},
|
||||
},
|
||||
|
||||
// ** STOP ** Are you adding a new loader?
|
||||
// Make sure to add the new loader(s) before the "file" loader.
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
|
||||
// Makes some environment variables available in index.html.
|
||||
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
|
||||
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
// In production, it will be an empty string unless you specify "homepage"
|
||||
// in `package.json`, in which case it will be the pathname of that URL.
|
||||
new InterpolateHtmlPlugin(env.raw),
|
||||
|
||||
// Generates an `index.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin({
|
||||
inject: true,
|
||||
template: paths.appHtml,
|
||||
minify: {
|
||||
removeComments: true,
|
||||
collapseWhitespace: true,
|
||||
removeRedundantAttributes: true,
|
||||
useShortDoctype: true,
|
||||
removeEmptyAttributes: true,
|
||||
removeStyleLinkTypeAttributes: true,
|
||||
keepClosingSlash: true,
|
||||
minifyJS: true,
|
||||
minifyCSS: true,
|
||||
minifyURLs: true,
|
||||
},
|
||||
}),
|
||||
|
||||
// Makes some environment variables available to the JS code, for example:
|
||||
// if (process.env.NODE_ENV === 'production') { ... }. See `./env.js`.
|
||||
// It is absolutely essential that NODE_ENV was set to production here.
|
||||
// Otherwise React will be compiled in the very slow development mode.
|
||||
new webpack.DefinePlugin(env.stringified),
|
||||
|
||||
// Minify the code.
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
compress: {
|
||||
warnings: false,
|
||||
|
||||
// Disabled because of an issue with Uglify breaking seemingly valid code:
|
||||
// https://github.com/facebookincubator/create-react-app/issues/2376
|
||||
// Pending further investigation:
|
||||
// https://github.com/mishoo/UglifyJS2/issues/2011
|
||||
comparisons: false,
|
||||
},
|
||||
mangle: {
|
||||
safari10: true,
|
||||
},
|
||||
output: {
|
||||
comments: false,
|
||||
|
||||
// Turned on because emoji and regex is not minified properly using default
|
||||
// https://github.com/facebookincubator/create-react-app/issues/2488
|
||||
ascii_only: true,
|
||||
},
|
||||
sourceMap: shouldUseSourceMap,
|
||||
}),
|
||||
|
||||
// Note: this won't work without ExtractTextPlugin.extract(..) in `loaders`.
|
||||
new ExtractTextPlugin({
|
||||
filename: cssFilename,
|
||||
}),
|
||||
|
||||
// Generate a manifest file which contains a mapping of all asset filenames
|
||||
// to their corresponding output file so that tools can pick it up without
|
||||
// having to parse `index.html`.
|
||||
new ManifestPlugin({
|
||||
fileName: 'asset-manifest.json',
|
||||
}),
|
||||
|
||||
// Generate a service worker script that will precache, and keep up to date,
|
||||
// the HTML & assets that are part of the Webpack build.
|
||||
new SWPrecacheWebpackPlugin({
|
||||
|
||||
// By default, a cache-busting query parameter is appended to requests
|
||||
// used to populate the caches, to ensure the responses are fresh.
|
||||
// If a URL is already hashed by Webpack, then there is no concern
|
||||
// about it being stale, and the cache-busting can be skipped.
|
||||
dontCacheBustUrlsMatching: /\.\w{8}\./,
|
||||
filename: 'service-worker.js',
|
||||
logger(message) {
|
||||
if (message.indexOf('Total precache size is') === 0) {
|
||||
// This message occurs for every build and is a bit too noisy.
|
||||
return;
|
||||
}
|
||||
if (message.indexOf('Skipping static resource') === 0) {
|
||||
// This message obscures real errors so we ignore it.
|
||||
// https://github.com/facebookincubator/create-react-app/issues/2612
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(message); // eslint-disable-line
|
||||
},
|
||||
minify: true,
|
||||
|
||||
// For unknown URLs, fallback to the index page
|
||||
navigateFallback: `${publicUrl}/index.html`,
|
||||
|
||||
// Ignores URLs starting from /__ (useful for Firebase):
|
||||
// https://github.com/facebookincubator/create-react-app/issues/2237#issuecomment-302693219
|
||||
navigateFallbackWhitelist: [ /^(?!\/__).*/ ],
|
||||
|
||||
// Don't precache sourcemaps (they're large) and build asset manifest:
|
||||
staticFileGlobsIgnorePatterns: [ /\.map$/, /asset-manifest\.json$/ ],
|
||||
}),
|
||||
|
||||
// Moment.js is an extremely popular library that bundles large locale files
|
||||
// by default due to how Webpack interprets its code. This is a practical
|
||||
// solution that requires the user to opt into importing specific locales.
|
||||
// https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
|
||||
// You can remove this if you don't use Moment.js:
|
||||
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
|
||||
],
|
||||
|
||||
// Some libraries import Node modules but don't use them in the browser.
|
||||
// Tell Webpack to provide empty mocks for them so importing them works.
|
||||
node: {
|
||||
dgram: 'empty',
|
||||
fs: 'empty',
|
||||
net: 'empty',
|
||||
tls: 'empty',
|
||||
child_process: 'empty',
|
||||
},
|
||||
};
|
||||
@@ -1,8 +1,9 @@
|
||||
|
||||
const fs = require('fs');
|
||||
const errorOverlayMiddleware = require('react-dev-utils/errorOverlayMiddleware');
|
||||
const evalSourceMapMiddleware = require('react-dev-utils/evalSourceMapMiddleware');
|
||||
const noopServiceWorkerMiddleware = require('react-dev-utils/noopServiceWorkerMiddleware');
|
||||
const ignoredFiles = require('react-dev-utils/ignoredFiles');
|
||||
const config = require('./webpack.config.dev');
|
||||
const paths = require('./paths');
|
||||
|
||||
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
|
||||
@@ -17,8 +18,8 @@ module.exports = function(proxy, allowedHost) {
|
||||
// https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a
|
||||
// However, it made several existing use cases such as development in cloud
|
||||
// environment or subdomains in development significantly more complicated:
|
||||
// https://github.com/facebookincubator/create-react-app/issues/2271
|
||||
// https://github.com/facebookincubator/create-react-app/issues/2233
|
||||
// https://github.com/facebook/create-react-app/issues/2271
|
||||
// https://github.com/facebook/create-react-app/issues/2233
|
||||
// While we're investigating better solutions, for now we will take a
|
||||
// compromise. Since our WDS configuration only serves files in the `public`
|
||||
// folder we won't consider accessing them a vulnerability. However, if you
|
||||
@@ -65,16 +66,16 @@ module.exports = function(proxy, allowedHost) {
|
||||
|
||||
// It is important to tell WebpackDevServer to use the same "root" path
|
||||
// as we specified in the config. In development, we always serve from /.
|
||||
publicPath: config.output.publicPath,
|
||||
publicPath: '/',
|
||||
|
||||
// WebpackDevServer is noisy by default so we emit custom message instead
|
||||
// by listening to the compiler events with `compiler.plugin` calls above.
|
||||
// by listening to the compiler events with `compiler.hooks[...].tap` calls above.
|
||||
quiet: true,
|
||||
|
||||
// Reportedly, this avoids CPU overload on some systems.
|
||||
// https://github.com/facebookincubator/create-react-app/issues/293
|
||||
// https://github.com/facebook/create-react-app/issues/293
|
||||
// src/node_modules is not ignored to support absolute imports
|
||||
// https://github.com/facebookincubator/create-react-app/issues/1065
|
||||
// https://github.com/facebook/create-react-app/issues/1065
|
||||
watchOptions: {
|
||||
ignored: ignoredFiles(paths.appSrc),
|
||||
},
|
||||
@@ -86,12 +87,20 @@ module.exports = function(proxy, allowedHost) {
|
||||
historyApiFallback: {
|
||||
|
||||
// Paths with dots should still use the history fallback.
|
||||
// See https://github.com/facebookincubator/create-react-app/issues/387.
|
||||
// See https://github.com/facebook/create-react-app/issues/387.
|
||||
disableDotRule: true,
|
||||
},
|
||||
public: allowedHost,
|
||||
proxy,
|
||||
before(app) {
|
||||
before(app, server) {
|
||||
if (fs.existsSync(paths.proxySetup)) {
|
||||
// This registers user provided middleware for proxy reasons
|
||||
require(paths.proxySetup)(app);
|
||||
}
|
||||
|
||||
// This lets us fetch source contents from webpack for the error overlay
|
||||
app.use(evalSourceMapMiddleware(server));
|
||||
|
||||
// This lets us open files from the runtime error overlay.
|
||||
app.use(errorOverlayMiddleware());
|
||||
|
||||
@@ -99,7 +108,7 @@ module.exports = function(proxy, allowedHost) {
|
||||
// previous service worker registered for the same host:port combination.
|
||||
// We do this in development to avoid hitting the production cache if
|
||||
// it used the same host and port.
|
||||
// https://github.com/facebookincubator/create-react-app/issues/2272#issuecomment-302832432
|
||||
// https://github.com/facebook/create-react-app/issues/2272#issuecomment-302832432
|
||||
app.use(noopServiceWorkerMiddleware());
|
||||
},
|
||||
};
|
||||
|
||||
@@ -6,3 +6,4 @@ services:
|
||||
volumes:
|
||||
- /etc/passwd:/etc/passwd:ro
|
||||
- /etc/group:/etc/group:ro
|
||||
- ./home:/home/alejandro
|
||||
|
||||
@@ -3,8 +3,8 @@ version: '3'
|
||||
services:
|
||||
shlink_web_client_node:
|
||||
container_name: shlink_web_client_node
|
||||
image: node:10.4.1-alpine
|
||||
command: /bin/sh -c "cd /home/shlink/www && yarn install && yarn start"
|
||||
image: node:12.16.3-alpine
|
||||
command: /bin/sh -c "cd /home/shlink/www && npm install && npm run start"
|
||||
volumes:
|
||||
- ./:/home/shlink/www
|
||||
ports:
|
||||
|
||||
@@ -8,29 +8,37 @@ module.exports = {
|
||||
'!src/**/provideServices.js',
|
||||
'!src/container/*.js',
|
||||
],
|
||||
resolver: 'jest-pnp-resolver',
|
||||
setupFiles: [
|
||||
'<rootDir>/config/polyfills.js',
|
||||
'react-app-polyfill/jsdom',
|
||||
'<rootDir>/config/setupEnzyme.js',
|
||||
],
|
||||
testMatch: [ '<rootDir>/test/**/*.test.{js,jsx,mjs}' ],
|
||||
testEnvironment: 'node',
|
||||
testMatch: [ '<rootDir>/test/**/*.test.{js,jsx,ts,tsx}' ],
|
||||
testEnvironment: 'jsdom',
|
||||
testURL: 'http://localhost',
|
||||
transform: {
|
||||
'^.+\\.(js|jsx|mjs)$': '<rootDir>/node_modules/babel-jest',
|
||||
'^.+\\.css$': '<rootDir>/config/jest/cssTransform.js',
|
||||
'^(?!.*\\.(js|jsx|mjs|css|json)$)': '<rootDir>/config/jest/fileTransform.js',
|
||||
},
|
||||
transformIgnorePatterns: [ '[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs)$' ],
|
||||
transformIgnorePatterns: [
|
||||
'[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$',
|
||||
'^.+\\.module\\.(css|sass|scss)$',
|
||||
],
|
||||
moduleNameMapper: {
|
||||
'^react-native$': 'react-native-web',
|
||||
'^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy',
|
||||
},
|
||||
moduleFileExtensions: [
|
||||
'web.js',
|
||||
'js',
|
||||
'web.ts',
|
||||
'ts',
|
||||
'web.tsx',
|
||||
'tsx',
|
||||
'json',
|
||||
'web.jsx',
|
||||
'jsx',
|
||||
'node',
|
||||
'mjs',
|
||||
],
|
||||
};
|
||||
|
||||
19580
package-lock.json
generated
Normal file
216
package.json
@@ -1,113 +1,155 @@
|
||||
{
|
||||
"name": "shlink-web-client-react",
|
||||
"name": "shlink-web-client",
|
||||
"description": "A React-based progressive web application for shlink",
|
||||
"version": "1.0.0",
|
||||
"private": false,
|
||||
"homepage": "",
|
||||
"repository": "https://github.com/shlinkio/shlink-web-client",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"lint": "yarn lint:js && yarn lint:css",
|
||||
"lint": "npm run lint:js && npm run lint:css",
|
||||
"lint:js": "eslint src test scripts config",
|
||||
"lint:js:fix": "yarn lint:js --fix",
|
||||
"lint:js:fix": "npm run lint:js -- --fix",
|
||||
"lint:css": "stylelint src/*.scss src/**/*.scss",
|
||||
"lint:css:fix": "yarn lint:css --fix",
|
||||
"lint:css:fix": "npm run lint:css -- --fix",
|
||||
"start": "node scripts/start.js",
|
||||
"serve:build": "yarn serve ./build",
|
||||
"serve:build": "serve ./build",
|
||||
"build": "node scripts/build.js",
|
||||
"test": "node scripts/test.js --env=jsdom --colors",
|
||||
"test:ci": "yarn test --coverage --coverageReporters=text --coverageReporters=text-summary --coverageReporters=clover",
|
||||
"test:pretty": "yarn test --coverage --coverageReporters=text --coverageReporters=text-summary --coverageReporters=html"
|
||||
"test:ci": "npm run test -- --coverage --coverageReporters=text --coverageReporters=text-summary --coverageReporters=clover",
|
||||
"test:pretty": "npm run test -- --coverage --coverageReporters=text --coverageReporters=text-summary --coverageReporters=html",
|
||||
"mutate": "./node_modules/.bin/stryker run",
|
||||
"mutate:ci": "npm run mutate -- --mutate=$MUTATION_FILES",
|
||||
"check": "npm run test & npm run lint & wait"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome": "^1.1.8",
|
||||
"@fortawesome/fontawesome-free-regular": "^5.0.13",
|
||||
"@fortawesome/fontawesome-free-solid": "^5.0.13",
|
||||
"@fortawesome/react-fontawesome": "0.0.19",
|
||||
"axios": "^0.18.0",
|
||||
"bootstrap": "^4.1.1",
|
||||
"bottlejs": "^1.7.1",
|
||||
"chart.js": "^2.7.2",
|
||||
"@fortawesome/fontawesome-free": "^5.11.2",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.25",
|
||||
"@fortawesome/free-regular-svg-icons": "^5.11.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.11.2",
|
||||
"@fortawesome/react-fontawesome": "^0.1.5",
|
||||
"array-filter": "^1.0.0",
|
||||
"array-map": "^0.0.0",
|
||||
"array-reduce": "^0.0.0",
|
||||
"axios": "^0.19.0",
|
||||
"bootstrap": "^4.3.1",
|
||||
"bottlejs": "^1.7.2",
|
||||
"bowser": "^2.9.0",
|
||||
"chart.js": "^2.8.0",
|
||||
"classnames": "^2.2.6",
|
||||
"compare-versions": "^3.5.1",
|
||||
"csvjson": "^5.1.0",
|
||||
"moment": "^2.22.2",
|
||||
"promise": "8.0.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"qs": "^6.5.2",
|
||||
"ramda": "^0.25.0",
|
||||
"react": "^16.6",
|
||||
"react-autosuggest": "^9.4.0",
|
||||
"react-chartjs-2": "^2.7.4",
|
||||
"react-color": "^2.14.1",
|
||||
"event-source-polyfill": "^1.0.12",
|
||||
"leaflet": "^1.5.1",
|
||||
"moment": "^2.24.0",
|
||||
"promise": "^8.0.3",
|
||||
"prop-types": "^15.7.2",
|
||||
"qs": "^6.9.0",
|
||||
"ramda": "^0.26.1",
|
||||
"react": "^16.13.1",
|
||||
"react-autosuggest": "^9.4.3",
|
||||
"react-chartjs-2": "^2.8.0",
|
||||
"react-color": "^2.17.3",
|
||||
"react-copy-to-clipboard": "^5.0.1",
|
||||
"react-datepicker": "^1.5.0",
|
||||
"react-dom": "^16.3.2",
|
||||
"react-moment": "^0.7.6",
|
||||
"react-redux": "^5.0.7",
|
||||
"react-router-dom": "^4.2.2",
|
||||
"react-swipeable": "^4.3.0",
|
||||
"react-datepicker": "~1.5.0",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-external-link": "^1.0.0",
|
||||
"react-leaflet": "^2.4.0",
|
||||
"react-moment": "^0.9.5",
|
||||
"react-redux": "^7.1.1",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-swipeable": "^5.4.0",
|
||||
"react-tagsinput": "^3.19.0",
|
||||
"reactstrap": "^6.0.1",
|
||||
"redux": "^4.0.0",
|
||||
"reactstrap": "^8.0.1",
|
||||
"redux": "^4.0.4",
|
||||
"redux-actions": "^2.6.5",
|
||||
"redux-localstorage-simple": "^2.2.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"uuid": "^3.3.2"
|
||||
"uuid": "^3.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"adm-zip": "^0.4.11",
|
||||
"autoprefixer": "7.1.6",
|
||||
"babel-core": "6.26.0",
|
||||
"babel-eslint": "7.2.3",
|
||||
"babel-jest": "20.0.3",
|
||||
"babel-loader": "^7.1.2",
|
||||
"babel-preset-react-app": "^3.1.1",
|
||||
"babel-runtime": "6.26.0",
|
||||
"case-sensitive-paths-webpack-plugin": "2.1.1",
|
||||
"chalk": "1.1.3",
|
||||
"css-loader": "0.28.7",
|
||||
"dotenv": "4.0.0",
|
||||
"dotenv-expand": "4.2.0",
|
||||
"enzyme": "^3.3.0",
|
||||
"enzyme-adapter-react-16": "^1.1.1",
|
||||
"eslint": "^4.19.0",
|
||||
"eslint-config-adidas-babel": "^1.0.1",
|
||||
"eslint-config-adidas-env": "^1.0.1",
|
||||
"eslint-config-adidas-es6": "^1.0.1",
|
||||
"eslint-config-adidas-react": "^1.0.1",
|
||||
"eslint-loader": "1.9.0",
|
||||
"eslint-plugin-import": "^2.8.0",
|
||||
"eslint-plugin-jest": "^21.22.0",
|
||||
"eslint-plugin-promise": "^3.0.0",
|
||||
"eslint-plugin-react": "^7.4.0",
|
||||
"extract-text-webpack-plugin": "^3.0.2",
|
||||
"file-loader": "1.1.5",
|
||||
"fs-extra": "3.0.1",
|
||||
"html-webpack-plugin": "2.29.0",
|
||||
"jest": "20.0.4",
|
||||
"node-sass": "^4.9.0",
|
||||
"object-assign": "4.1.1",
|
||||
"@babel/core": "^7.6.2",
|
||||
"@stryker-mutator/core": "^3.2.4",
|
||||
"@stryker-mutator/javascript-mutator": "^3.2.4",
|
||||
"@stryker-mutator/jest-runner": "^3.2.4",
|
||||
"@svgr/webpack": "^4.3.3",
|
||||
"adm-zip": "^0.4.13",
|
||||
"autoprefixer": "^9.6.3",
|
||||
"babel-core": "7.0.0-bridge.0",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"babel-jest": "^24.9.0",
|
||||
"babel-loader": "^8.0.6",
|
||||
"babel-plugin-named-asset-import": "^0.3.4",
|
||||
"babel-preset-react-app": "^9.0.2",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"bfj": "^7.0.1",
|
||||
"case-sensitive-paths-webpack-plugin": "^2.2.0",
|
||||
"chalk": "^2.4.2",
|
||||
"css-loader": "^3.2.0",
|
||||
"dotenv": "^8.1.0",
|
||||
"dotenv-expand": "^5.1.0",
|
||||
"enzyme": "^3.11.0",
|
||||
"enzyme-adapter-react-16": "^1.15.2",
|
||||
"eslint": "^5.11.1",
|
||||
"eslint-config-adidas-babel": "^1.1.0",
|
||||
"eslint-config-adidas-env": "^1.1.0",
|
||||
"eslint-config-adidas-es6": "^1.2.0",
|
||||
"eslint-config-adidas-react": "^1.1.1",
|
||||
"eslint-loader": "^3.0.2",
|
||||
"eslint-plugin-import": "^2.18.2",
|
||||
"eslint-plugin-jest": "^22.17.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.16.0",
|
||||
"file-loader": "^4.2.0",
|
||||
"fork-ts-checker-webpack-plugin-alt": "^0.4.14",
|
||||
"fs-extra": "^8.1.0",
|
||||
"html-webpack-plugin": "^4.0.0-beta.8",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^24.9.0",
|
||||
"jest-pnp-resolver": "^1.2.1",
|
||||
"jest-resolve": "^24.9.0",
|
||||
"mini-css-extract-plugin": "^0.8.0",
|
||||
"node-sass": "^4.12.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"ocular.js": "^0.1.0",
|
||||
"postcss-flexbugs-fixes": "3.2.0",
|
||||
"postcss-loader": "2.0.8",
|
||||
"raf": "3.4.0",
|
||||
"react-dev-utils": "^5.0.1",
|
||||
"resolve": "1.6.0",
|
||||
"sass-loader": "^7.0.1",
|
||||
"serve": "^10.0.0",
|
||||
"sinon": "^6.1.5",
|
||||
"style-loader": "0.19.0",
|
||||
"stylelint": "^9.5.0",
|
||||
"stylelint-config-adidas": "^1.0.1",
|
||||
"stylelint-config-adidas-bem": "^1.0.1",
|
||||
"stylelint-config-recommended-scss": "^3.2.0",
|
||||
"stylelint-scss": "^3.3.0",
|
||||
"sw-precache-webpack-plugin": "0.11.4",
|
||||
"url-loader": "0.6.2",
|
||||
"webpack": "3.8.1",
|
||||
"webpack-dev-server": "2.9.4",
|
||||
"webpack-manifest-plugin": "1.3.2",
|
||||
"whatwg-fetch": "2.0.3"
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
||||
"pnp-webpack-plugin": "^1.5.0",
|
||||
"postcss": "^7.0.18",
|
||||
"postcss-flexbugs-fixes": "^4.1.0",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"postcss-preset-env": "^6.7.0",
|
||||
"postcss-safe-parser": "^4.0.1",
|
||||
"raf": "^3.4.1",
|
||||
"react-app-polyfill": "^1.0.4",
|
||||
"react-dev-utils": "^9.1.0",
|
||||
"resolve": "^1.12.0",
|
||||
"sass-loader": "^8.0.0",
|
||||
"serve": "^11.2.0",
|
||||
"stryker-cli": "^1.0.0",
|
||||
"style-loader": "^1.0.0",
|
||||
"stylelint": "^9.10.1",
|
||||
"stylelint-config-adidas": "^1.2.1",
|
||||
"stylelint-config-adidas-bem": "^1.2.0",
|
||||
"stylelint-config-recommended-scss": "^4.0.0",
|
||||
"stylelint-scss": "^3.11.1",
|
||||
"sw-precache-webpack-plugin": "^0.11.5",
|
||||
"terser-webpack-plugin": "^2.1.2",
|
||||
"url-loader": "^2.2.0",
|
||||
"webpack": "^4.41.0",
|
||||
"webpack-dev-server": "^3.8.2",
|
||||
"webpack-manifest-plugin": "^2.2.0",
|
||||
"whatwg-fetch": "^3.0.0",
|
||||
"workbox-webpack-plugin": "^4.3.1"
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
"react-app"
|
||||
]
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not ie <= 11",
|
||||
"not op_mini all"
|
||||
]
|
||||
}
|
||||
|
||||
16
public/.htaccess
Normal file
@@ -0,0 +1,16 @@
|
||||
RewriteEngine on
|
||||
RewriteBase /
|
||||
|
||||
# do not do anything for already existing files
|
||||
RewriteCond %{REQUEST_FILENAME} -f [OR]
|
||||
RewriteCond %{REQUEST_FILENAME} -l [OR]
|
||||
RewriteCond %{REQUEST_FILENAME} -d
|
||||
RewriteRule (.*) - [L]
|
||||
|
||||
# if request is no valid file NOR directory
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
# if static asset do not do anything
|
||||
RewriteRule (.*)(css|js|html|png|jpe?g|gif|bmp|ico|json|csv|otf|eot|svg|svgz|ttf|woff|woff2|ijmap|pdf|tif|map) - [NC,L,R=404]
|
||||
# everything else should be redirected to /index.html so it can be routed by it
|
||||
RewriteRule (.*) /index.html [L]
|
||||
BIN
public/favicon.gif
Normal file
|
After Width: | Height: | Size: 642 B |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 1.1 KiB |
BIN
public/favicon.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
1
public/favicon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="512pt" height="512pt" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg"><g fill="#4595e3"><path d=" M 23.71 85.08 C 17.22 49.81 49.44 14.86 85.08 18.12 C 118.83 19.21 145.72 53.33 139.45 86.37 C 155.64 102.30 171.32 118.83 187.87 134.36 C 198.32 111.73 208.84 89.12 219.57 66.62 C 226.05 53.84 243.47 48.74 255.73 56.27 C 263.76 62.10 270.34 69.69 277.25 76.75 C 286.28 86.61 285.72 102.89 276.31 112.31 C 223.38 165.37 170.38 218.37 117.35 271.34 C 107.72 280.99 91.01 281.25 81.11 271.86 C 74.39 264.94 66.82 258.69 61.24 250.77 C 53.72 238.52 58.85 221.07 71.64 214.62 C 94.11 203.87 116.72 193.38 139.33 182.91 C 123.81 166.36 107.30 150.68 91.37 134.49 C 60.20 140.28 27.37 116.78 23.71 85.08 Z" /><path d=" M 205.21 201.23 C 225.32 181.36 260.88 181.11 281.14 200.86 C 299.25 218.75 317.37 236.65 335.10 254.93 C 356.73 278.01 352.01 318.70 326.03 336.56 C 320.07 330.47 313.73 324.65 308.12 318.28 C 323.86 309.39 328.76 286.18 316.63 272.39 C 301.73 256.95 286.30 242.03 271.24 226.75 C 264.49 219.65 256.80 212.00 246.37 211.52 C 224.65 208.64 205.52 233.36 214.49 253.58 C 221.09 266.81 234.22 275.12 243.62 286.24 C 240.43 295.96 238.09 306.13 238.29 316.46 C 225.55 304.29 213.16 291.73 200.89 279.09 C 180.97 257.57 183.10 220.45 205.21 201.23 Z" /><path d=" M 273.90 352.07 C 252.28 328.99 256.98 288.31 282.96 270.46 C 288.93 276.54 295.26 282.36 300.88 288.72 C 285.14 297.62 280.23 320.82 292.38 334.61 C 307.27 350.05 322.70 364.96 337.75 380.25 C 344.51 387.35 352.20 395.00 362.64 395.48 C 384.35 398.37 403.49 373.64 394.51 353.42 C 387.92 340.18 374.78 331.88 365.38 320.76 C 368.56 311.04 370.91 300.86 370.71 290.54 C 383.45 302.70 395.84 315.27 408.11 327.91 C 428.03 349.43 425.90 386.55 403.78 405.77 C 383.68 425.64 348.13 425.89 327.86 406.14 C 309.75 388.25 291.60 370.37 273.90 352.07 Z" /><path d=" M 422.11 403.83 C 431.96 394.07 441.60 384.06 451.66 374.51 C 460.90 383.74 471.89 392.70 474.89 406.11 C 480.16 429.97 484.08 454.13 488.76 478.12 C 490.00 483.41 484.47 488.29 479.35 486.63 C 454.66 481.52 429.55 478.12 405.14 471.84 C 393.17 467.97 385.20 457.75 376.55 449.27 C 386.39 439.49 396.13 429.60 406.06 419.91 C 416.37 433.45 435.74 414.00 422.11 403.83 Z" /></g></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
BIN
public/icons/icon-1024x1024.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
public/icons/icon-114x114.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/icons/icon-120x120.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 1.4 KiB |
BIN
public/icons/icon-150x150.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 1.3 KiB |
BIN
public/icons/icon-160x160.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/icons/icon-167x167.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/icons/icon-16x16.png
Normal file
|
After Width: | Height: | Size: 287 B |
BIN
public/icons/icon-180x180.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 1.7 KiB |
BIN
public/icons/icon-196x196.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
public/icons/icon-228x228.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
public/icons/icon-24x24.png
Normal file
|
After Width: | Height: | Size: 381 B |
BIN
public/icons/icon-256x256.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
public/icons/icon-310x310.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
public/icons/icon-32x32.png
Normal file
|
After Width: | Height: | Size: 437 B |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 3.2 KiB |
BIN
public/icons/icon-40x40.png
Normal file
|
After Width: | Height: | Size: 466 B |
BIN
public/icons/icon-48x48.png
Normal file
|
After Width: | Height: | Size: 551 B |
BIN
public/icons/icon-512x512.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
public/icons/icon-60x60.png
Normal file
|
After Width: | Height: | Size: 638 B |
BIN
public/icons/icon-64x64.png
Normal file
|
After Width: | Height: | Size: 684 B |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 750 B |
BIN
public/icons/icon-76x76.png
Normal file
|
After Width: | Height: | Size: 783 B |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 984 B |
@@ -11,12 +11,74 @@
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
|
||||
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/icons/shlink-128.png">
|
||||
<link rel="apple-touch-icon" sizes="64x64" href="%PUBLIC_URL%/icons/shlink-64.png">
|
||||
<link rel="apple-touch-icon" sizes="32x32" href="%PUBLIC_URL%/icons/shlink-32.png">
|
||||
<link rel="apple-touch-icon" sizes="24x24" href="%PUBLIC_URL%/icons/shlink-24.png">
|
||||
<link rel="apple-touch-icon" sizes="16x16" href="%PUBLIC_URL%/icons/shlink-16.png">
|
||||
<!-- FavIcon itself -->
|
||||
<link rel="icon" type="image/x-icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
<link rel="icon" type="image/svg+xml" href="%PUBLIC_URL%/favicon.svg" sizes="any">
|
||||
<link rel="icon" type="image/png" href="%PUBLIC_URL%/favicon.png">
|
||||
<link rel="icon" type="image/gif" href="%PUBLIC_URL%/favicon.gif">
|
||||
<!-- Apple Touch -->
|
||||
<link rel="apple-touch-icon" sizes="16x16" href="%PUBLIC_URL%/icons/icon-16x16.png">
|
||||
<link rel="apple-touch-icon" sizes="24x24" href="%PUBLIC_URL%/icons/icon-24x24.png">
|
||||
<link rel="apple-touch-icon" sizes="32x32" href="%PUBLIC_URL%/icons/icon-32x32.png">
|
||||
<link rel="apple-touch-icon" sizes="40x40" href="%PUBLIC_URL%/icons/icon-40x40.png">
|
||||
<link rel="apple-touch-icon" sizes="48x48" href="%PUBLIC_URL%/icons/icon-48x48.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="%PUBLIC_URL%/icons/icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="64x64" href="%PUBLIC_URL%/icons/icon-64x64.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="%PUBLIC_URL%/icons/icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="%PUBLIC_URL%/icons/icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="96x96" href="%PUBLIC_URL%/icons/icon-96x96.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="%PUBLIC_URL%/icons/icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="%PUBLIC_URL%/icons/icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="128x128" href="%PUBLIC_URL%/icons/icon-128x128.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="%PUBLIC_URL%/icons/icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="150x150" href="%PUBLIC_URL%/icons/icon-150x150.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="%PUBLIC_URL%/icons/icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="160x160" href="%PUBLIC_URL%/icons/icon-160x160.png">
|
||||
<link rel="apple-touch-icon" sizes="167x167" href="%PUBLIC_URL%/icons/icon-167x167.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/icons/icon-180x180.png">
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="%PUBLIC_URL%/icons/icon-192x192.png">
|
||||
<link rel="apple-touch-icon" sizes="196x196" href="%PUBLIC_URL%/icons/icon-196x196.png">
|
||||
<link rel="apple-touch-icon" sizes="228x228" href="%PUBLIC_URL%/icons/icon-228x228.png">
|
||||
<link rel="apple-touch-icon" sizes="256x256" href="%PUBLIC_URL%/icons/icon-256x256.png">
|
||||
<link rel="apple-touch-icon" sizes="310x310" href="%PUBLIC_URL%/icons/icon-310x310.png">
|
||||
<link rel="apple-touch-icon" sizes="384x384" href="%PUBLIC_URL%/icons/icon-384x384.png">
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="%PUBLIC_URL%/icons/icon-512x512.png">
|
||||
<link rel="apple-touch-icon" sizes="1024x1024" href="%PUBLIC_URL%/icons/icon-1024x1024.png">
|
||||
<!-- Normal -->
|
||||
<link rel="icon" type="image/png" sizes="1024x1024" href="%PUBLIC_URL%/icons/icon-1024x1024.png">
|
||||
<link rel="icon" type="image/png" sizes="512x512" href="%PUBLIC_URL%/icons/icon-512x512.png">
|
||||
<link rel="icon" type="image/png" sizes="384x384" href="%PUBLIC_URL%/icons/icon-384x384.png">
|
||||
<link rel="icon" type="image/png" sizes="310x310" href="%PUBLIC_URL%/icons/icon-310x310.png">
|
||||
<link rel="icon" type="image/png" sizes="256x256" href="%PUBLIC_URL%/icons/icon-256x256.png">
|
||||
<link rel="icon" type="image/png" sizes="228x228" href="%PUBLIC_URL%/icons/icon-228x228.png">
|
||||
<link rel="icon" type="image/png" sizes="196x196" href="%PUBLIC_URL%/icons/icon-196x196.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="%PUBLIC_URL%/icons/icon-192x192.png">
|
||||
<link rel="icon" type="image/png" sizes="180x180" href="%PUBLIC_URL%/icons/icon-180x180.png">
|
||||
<link rel="icon" type="image/png" sizes="167x167" href="%PUBLIC_URL%/icons/icon-167x167.png">
|
||||
<link rel="icon" type="image/png" sizes="160x160" href="%PUBLIC_URL%/icons/icon-160x160.png">
|
||||
<link rel="icon" type="image/png" sizes="152x152" href="%PUBLIC_URL%/icons/icon-152x152.png">
|
||||
<link rel="icon" type="image/png" sizes="150x150" href="%PUBLIC_URL%/icons/icon-150x150.png">
|
||||
<link rel="icon" type="image/png" sizes="144x144" href="%PUBLIC_URL%/icons/icon-144x144.png">
|
||||
<link rel="icon" type="image/png" sizes="128x128" href="%PUBLIC_URL%/icons/icon-128x128.png">
|
||||
<link rel="icon" type="image/png" sizes="120x120" href="%PUBLIC_URL%/icons/icon-120x120.png">
|
||||
<link rel="icon" type="image/png" sizes="114x114" href="%PUBLIC_URL%/icons/icon-114x114.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="%PUBLIC_URL%/icons/icon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="76x76" href="%PUBLIC_URL%/icons/icon-76x76.png">
|
||||
<link rel="icon" type="image/png" sizes="72x72" href="%PUBLIC_URL%/icons/icon-72x72.png">
|
||||
<link rel="icon" type="image/png" sizes="64x64" href="%PUBLIC_URL%/icons/icon-64x64.png">
|
||||
<link rel="icon" type="image/png" sizes="60x60" href="%PUBLIC_URL%/icons/icon-60x60.png">
|
||||
<link rel="icon" type="image/png" sizes="48x48" href="%PUBLIC_URL%/icons/icon-48x48.png">
|
||||
<link rel="icon" type="image/png" sizes="40x40" href="%PUBLIC_URL%/icons/icon-40x40.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/icons/icon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="24x24" href="%PUBLIC_URL%/icons/icon-24x24.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/icons/icon-16x16.png">
|
||||
<!-- MS -->
|
||||
<meta name="msapplication-TileImage" content="%PUBLIC_URL%/icons/icon-144x144.png">
|
||||
<meta name="msapplication-square70x70logo" content="%PUBLIC_URL%/icons/icon-70x70.png">
|
||||
<meta name="msapplication-square144x144logo" content="%PUBLIC_URL%/icons/icon-144x144.png">
|
||||
<meta name="msapplication-square150x150logo" content="%PUBLIC_URL%/icons/icon-150x150.png">
|
||||
<meta name="msapplication-square310x310logo" content="%PUBLIC_URL%/icons/icon-310x310.png">
|
||||
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
|
||||
@@ -6,16 +6,66 @@
|
||||
"theme_color": "#4696e5",
|
||||
"background_color": "#4696e5",
|
||||
"icons": [
|
||||
{
|
||||
"src": "./icons/icon-16x16.png",
|
||||
"type": "image/png",
|
||||
"sizes": "16x16"
|
||||
},
|
||||
{
|
||||
"src": "./icons/icon-24x24.png",
|
||||
"type": "image/png",
|
||||
"sizes": "24x24"
|
||||
},
|
||||
{
|
||||
"src": "./icons/icon-32x32.png",
|
||||
"type": "image/png",
|
||||
"sizes": "32x32"
|
||||
},
|
||||
{
|
||||
"src": "./icons/icon-40x40.png",
|
||||
"type": "image/png",
|
||||
"sizes": "40x40"
|
||||
},
|
||||
{
|
||||
"src": "./icons/icon-48x48.png",
|
||||
"type": "image/png",
|
||||
"sizes": "48x48"
|
||||
},
|
||||
{
|
||||
"src": "./icons/icon-60x60.png",
|
||||
"type": "image/png",
|
||||
"sizes": "60x60"
|
||||
},
|
||||
{
|
||||
"src": "./icons/icon-64x64.png",
|
||||
"type": "image/png",
|
||||
"sizes": "64x64"
|
||||
},
|
||||
{
|
||||
"src": "./icons/icon-72x72.png",
|
||||
"type": "image/png",
|
||||
"sizes": "72x72"
|
||||
},
|
||||
{
|
||||
"src": "./icons/icon-76x76.png",
|
||||
"type": "image/png",
|
||||
"sizes": "76x76"
|
||||
},
|
||||
{
|
||||
"src": "./icons/icon-96x96.png",
|
||||
"type": "image/png",
|
||||
"sizes": "96x96"
|
||||
},
|
||||
{
|
||||
"src": "./icons/icon-114x114.png",
|
||||
"type": "image/png",
|
||||
"sizes": "114x114"
|
||||
},
|
||||
{
|
||||
"src": "./icons/icon-120x120.png",
|
||||
"type": "image/png",
|
||||
"sizes": "120x120"
|
||||
},
|
||||
{
|
||||
"src": "./icons/icon-128x128.png",
|
||||
"type": "image/png",
|
||||
@@ -26,20 +76,70 @@
|
||||
"type": "image/png",
|
||||
"sizes": "144x144"
|
||||
},
|
||||
{
|
||||
"src": "./icons/icon-150x150.png",
|
||||
"type": "image/png",
|
||||
"sizes": "150x150"
|
||||
},
|
||||
{
|
||||
"src": "./icons/icon-152x152.png",
|
||||
"type": "image/png",
|
||||
"sizes": "152x152"
|
||||
},
|
||||
{
|
||||
"src": "./icons/icon-160x160.png",
|
||||
"type": "image/png",
|
||||
"sizes": "160x160"
|
||||
},
|
||||
{
|
||||
"src": "./icons/icon-167x167.png",
|
||||
"type": "image/png",
|
||||
"sizes": "167x167"
|
||||
},
|
||||
{
|
||||
"src": "./icons/icon-180x180.png",
|
||||
"type": "image/png",
|
||||
"sizes": "180x180"
|
||||
},
|
||||
{
|
||||
"src": "./icons/icon-192x192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "./icons/icon-196x196.png",
|
||||
"type": "image/png",
|
||||
"sizes": "196x196"
|
||||
},
|
||||
{
|
||||
"src": "./icons/icon-228x228.png",
|
||||
"type": "image/png",
|
||||
"sizes": "228x228"
|
||||
},
|
||||
{
|
||||
"src": "./icons/icon-256x256.png",
|
||||
"type": "image/png",
|
||||
"sizes": "256x256"
|
||||
},
|
||||
{
|
||||
"src": "./icons/icon-310x310.png",
|
||||
"type": "image/png",
|
||||
"sizes": "310x310"
|
||||
},
|
||||
{
|
||||
"src": "./icons/icon-384x384.png",
|
||||
"type": "image/png",
|
||||
"sizes": "384x384"
|
||||
},
|
||||
{
|
||||
"src": "./icons/icon-512x512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
},
|
||||
{
|
||||
"src": "./icons/icon-1024x1024.png",
|
||||
"type": "image/png",
|
||||
"sizes": "1024x1024"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
117
scripts/build.js
@@ -1,4 +1,4 @@
|
||||
/* eslint no-console: "off" */
|
||||
/* eslint-disable no-console */
|
||||
|
||||
// Do this as the first thing so that any code reading it knows the right env.
|
||||
process.env.BABEL_ENV = 'production';
|
||||
@@ -14,34 +14,48 @@ process.on('unhandledRejection', (err) => {
|
||||
// Ensure environment variables are read.
|
||||
require('../config/env');
|
||||
|
||||
const path = require('path');
|
||||
const chalk = require('chalk');
|
||||
const fs = require('fs-extra');
|
||||
const webpack = require('webpack');
|
||||
const bfj = require('bfj');
|
||||
const AdmZip = require('adm-zip');
|
||||
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
|
||||
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
|
||||
const printHostingInstructions = require('react-dev-utils/printHostingInstructions');
|
||||
const FileSizeReporter = require('react-dev-utils/FileSizeReporter');
|
||||
const printBuildError = require('react-dev-utils/printBuildError');
|
||||
const AdmZip = require('adm-zip');
|
||||
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
|
||||
const paths = require('../config/paths');
|
||||
const config = require('../config/webpack.config.prod');
|
||||
const configFactory = require('../config/webpack.config');
|
||||
|
||||
const { measureFileSizesBeforeBuild, printFileSizesAfterBuild } = FileSizeReporter;
|
||||
const useYarn = fs.existsSync(paths.yarnLockFile);
|
||||
|
||||
// These sizes are pretty large. We'll warn for bundles exceeding them.
|
||||
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; // eslint-disable-line
|
||||
const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; // eslint-disable-line
|
||||
|
||||
const isInteractive = process.stdout.isTTY;
|
||||
|
||||
// Warn and crash if required files are missing
|
||||
if (!checkRequiredFiles([ paths.appHtml, paths.appIndexJs ])) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// First, read the current file sizes in build directory.
|
||||
// This lets us display how much they changed later.
|
||||
measureFileSizesBeforeBuild(paths.appBuild)
|
||||
// Process CLI arguments
|
||||
const argvSliceStart = 2;
|
||||
const argv = process.argv.slice(argvSliceStart);
|
||||
const writeStatsJson = argv.indexOf('--stats') !== -1;
|
||||
const withoutDist = argv.indexOf('--no-dist') !== -1;
|
||||
const { version, hasVersion } = getVersionFromArgs(argv);
|
||||
|
||||
// Generate configuration
|
||||
const config = configFactory('production');
|
||||
|
||||
checkBrowsers(paths.appPath, isInteractive)
|
||||
.then(() =>
|
||||
|
||||
// First, read the current file sizes in build directory.
|
||||
// This lets us display how much they changed later.
|
||||
measureFileSizesBeforeBuild(paths.appBuild))
|
||||
.then((previousFileSizes) => {
|
||||
// Remove all content but keep the directory so that
|
||||
// if you're in it, you don't end up in Trash
|
||||
@@ -70,6 +84,7 @@ measureFileSizesBeforeBuild(paths.appBuild)
|
||||
);
|
||||
} else {
|
||||
console.log(chalk.green('Compiled successfully.\n'));
|
||||
hasVersion && replaceVersionPlaceholder(version);
|
||||
}
|
||||
|
||||
console.log('File sizes after gzip:\n');
|
||||
@@ -81,20 +96,6 @@ measureFileSizesBeforeBuild(paths.appBuild)
|
||||
WARN_AFTER_CHUNK_GZIP_SIZE
|
||||
);
|
||||
console.log();
|
||||
|
||||
const appPackage = require(paths.appPackageJson);
|
||||
|
||||
const { publicUrl } = paths;
|
||||
const { publicPath } = config.output;
|
||||
const buildFolder = path.relative(process.cwd(), paths.appBuild);
|
||||
|
||||
printHostingInstructions(
|
||||
appPackage,
|
||||
publicUrl,
|
||||
publicPath,
|
||||
buildFolder,
|
||||
useYarn
|
||||
);
|
||||
},
|
||||
(err) => {
|
||||
console.log(chalk.red('Failed to compile.\n'));
|
||||
@@ -102,7 +103,14 @@ measureFileSizesBeforeBuild(paths.appBuild)
|
||||
process.exit(1);
|
||||
}
|
||||
)
|
||||
.then(zipDist);
|
||||
.then(() => hasVersion && !withoutDist && zipDist(version))
|
||||
.catch((err) => {
|
||||
if (err && err.message) {
|
||||
console.log(err.message);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Create the production build and print the deployment instructions.
|
||||
function build(previousFileSizes) {
|
||||
@@ -112,12 +120,22 @@ function build(previousFileSizes) {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
compiler.run((err, stats) => {
|
||||
let messages;
|
||||
|
||||
if (err) {
|
||||
return reject(err);
|
||||
if (!err.message) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
messages = formatWebpackMessages({
|
||||
errors: [ err.message ],
|
||||
warnings: [],
|
||||
});
|
||||
} else {
|
||||
messages = formatWebpackMessages(
|
||||
stats.toJson({ all: false, warnings: true, errors: true })
|
||||
);
|
||||
}
|
||||
|
||||
const messages = formatWebpackMessages(stats.toJson({}, true));
|
||||
|
||||
if (messages.errors.length) {
|
||||
// Only keep the first error. Others are often indicative
|
||||
// of the same problem, but confuse the reader with noise.
|
||||
@@ -143,11 +161,20 @@ function build(previousFileSizes) {
|
||||
return reject(new Error(messages.warnings.join('\n\n')));
|
||||
}
|
||||
|
||||
return resolve({
|
||||
const resolveArgs = {
|
||||
stats,
|
||||
previousFileSizes,
|
||||
warnings: messages.warnings,
|
||||
});
|
||||
};
|
||||
|
||||
if (writeStatsJson) {
|
||||
return bfj // eslint-disable-line promise/no-promise-in-callback
|
||||
.write(`${paths.appBuild}/bundle-stats.json`, stats.toJson())
|
||||
.then(() => resolve(resolveArgs))
|
||||
.catch((error) => reject(new Error(error)));
|
||||
}
|
||||
|
||||
return resolve(resolveArgs);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -159,15 +186,7 @@ function copyPublicFolder() {
|
||||
});
|
||||
}
|
||||
|
||||
function zipDist() {
|
||||
const minArgsToContainVersion = 3;
|
||||
|
||||
// If no version was provided, do nothing
|
||||
if (process.argv.length < minArgsToContainVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [ , , version ] = process.argv;
|
||||
function zipDist(version) {
|
||||
const versionFileName = `./dist/shlink-web-client_${version}_dist.zip`;
|
||||
|
||||
console.log(chalk.cyan(`Generating dist file for version ${chalk.bold(version)}...`));
|
||||
@@ -185,4 +204,24 @@ function zipDist() {
|
||||
console.log(chalk.red('An error occurred while generating dist file'));
|
||||
console.log(e);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
function getVersionFromArgs(argv) {
|
||||
const [ version ] = argv;
|
||||
|
||||
return { version, hasVersion: !!version };
|
||||
}
|
||||
|
||||
function replaceVersionPlaceholder(version) {
|
||||
const staticJsFilesPath = './build/static/js';
|
||||
const versionPlaceholder = '%_VERSION_%';
|
||||
|
||||
const isMainFile = (file) => file.startsWith('main.') && file.endsWith('.js');
|
||||
const [ mainJsFile ] = fs.readdirSync(staticJsFilesPath).filter(isMainFile);
|
||||
const filePath = `${staticJsFilesPath}/${mainJsFile}`;
|
||||
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
||||
const replaced = fileContent.replace(versionPlaceholder, version);
|
||||
|
||||
fs.writeFileSync(filePath, replaced, 'utf-8');
|
||||
}
|
||||
|
||||
33
scripts/docker/build
Executable file
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
#PLATFORMS="linux/arm/v7,linux/arm64/v8,linux/amd64"
|
||||
PLATFORMS="linux/amd64"
|
||||
DOCKER_IMAGE="shlinkio/shlink-web-client"
|
||||
BUILDX_VER=v0.4.1
|
||||
export DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
|
||||
mkdir -vp ~/.docker/cli-plugins/ ~/dockercache
|
||||
curl --silent -L "https://github.com/docker/buildx/releases/download/${BUILDX_VER}/buildx-${BUILDX_VER}.linux-amd64" > ~/.docker/cli-plugins/docker-buildx
|
||||
chmod a+x ~/.docker/cli-plugins/docker-buildx
|
||||
|
||||
docker buildx create --use
|
||||
|
||||
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
|
||||
|
||||
if [[ -z $TRAVIS_TAG ]]; then
|
||||
docker buildx build --push \
|
||||
--platform ${PLATFORMS} \
|
||||
-t ${DOCKER_IMAGE}:latest .
|
||||
|
||||
else
|
||||
TAGS="-t ${DOCKER_IMAGE}:${TRAVIS_TAG#?}"
|
||||
# Push stable tag only if this is not an alpha or beta release
|
||||
[[ $TRAVIS_TAG != *"alpha"* && $TRAVIS_TAG != *"beta"* ]] && TAGS="${TAGS} -t ${DOCKER_IMAGE}:stable"
|
||||
|
||||
docker buildx build --push \
|
||||
--build-arg VERSION=${TRAVIS_TAG#?} \
|
||||
--platform ${PLATFORMS} \
|
||||
${TAGS} .
|
||||
fi
|
||||
12
scripts/docker/install-docker
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
|
||||
# install latest docker version
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
|
||||
add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
|
||||
apt-get update
|
||||
apt-get -y -o Dpkg::Options::="--force-confnew" install docker-ce
|
||||
|
||||
# enable multiarch execution
|
||||
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint no-console: "off" */
|
||||
/* eslint-disable no-console */
|
||||
|
||||
// Do this as the first thing so that any code reading it knows the right env.
|
||||
process.env.BABEL_ENV = 'development';
|
||||
@@ -27,8 +27,9 @@ const {
|
||||
prepareUrls,
|
||||
} = require('react-dev-utils/WebpackDevServerUtils');
|
||||
const openBrowser = require('react-dev-utils/openBrowser');
|
||||
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
|
||||
const paths = require('../config/paths');
|
||||
const config = require('../config/webpack.config.dev');
|
||||
const configFactory = require('../config/webpack.config');
|
||||
const createDevServerConfig = require('../config/webpackDevServer.config');
|
||||
|
||||
const useYarn = fs.existsSync(paths.yarnLockFile);
|
||||
@@ -40,8 +41,8 @@ if (!checkRequiredFiles([ paths.appHtml, paths.appIndexJs ])) {
|
||||
}
|
||||
|
||||
// Tools like Cloud9 rely on this.
|
||||
const FALLBACK_PORT = 3000;
|
||||
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || FALLBACK_PORT;
|
||||
const DEFAULT_PORT = 3000;
|
||||
const PORT = parseInt(process.env.PORT) || DEFAULT_PORT;
|
||||
const HOST = process.env.HOST || '0.0.0.0';
|
||||
|
||||
if (process.env.HOST) {
|
||||
@@ -55,33 +56,39 @@ if (process.env.HOST) {
|
||||
console.log(
|
||||
'If this was unintentional, check that you haven\'t mistakenly set it in your shell.'
|
||||
);
|
||||
console.log(`Learn more here: ${chalk.yellow('http://bit.ly/2mwWSwH')}`);
|
||||
console.log(
|
||||
`Learn more here: ${chalk.yellow('http://bit.ly/CRA-advanced-config')}`
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
|
||||
// We attempt to use the default port but if it is busy, we offer the user to
|
||||
// run on a different port. `choosePort()` Promise resolves to the next free port.
|
||||
choosePort(HOST, DEFAULT_PORT)
|
||||
checkBrowsers(paths.appPath, isInteractive)
|
||||
.then(() =>
|
||||
|
||||
// We attempt to use the default port but if it is busy, we offer the user to
|
||||
// run on a different port. `choosePort()` Promise resolves to the next free port.
|
||||
choosePort(HOST, PORT))
|
||||
.then((port) => {
|
||||
if (port === null) {
|
||||
// We have not found a port.
|
||||
return;
|
||||
}
|
||||
|
||||
const config = configFactory('development');
|
||||
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
|
||||
const appName = require(paths.appPackageJson).name;
|
||||
|
||||
const urls = prepareUrls(protocol, HOST, port);
|
||||
|
||||
// Create a webpack compiler that is configured with custom messages.
|
||||
const compiler = createCompiler(webpack, config, appName, urls, useYarn);
|
||||
const compiler = createCompiler({ webpack, config, appName, urls, useYarn });
|
||||
|
||||
// Load proxy config
|
||||
const proxySetting = require(paths.appPackageJson).proxy;
|
||||
|
||||
const proxyConfig = prepareProxy(proxySetting, paths.appPublic);
|
||||
|
||||
// Serve webpack assets generated by the compiler over a web sever.
|
||||
// Serve webpack assets generated by the compiler over a web server.
|
||||
const serverConfig = createDevServerConfig(
|
||||
proxyConfig,
|
||||
urls.lanUrlForConfig
|
||||
@@ -89,19 +96,17 @@ choosePort(HOST, DEFAULT_PORT)
|
||||
const devServer = new WebpackDevServer(compiler, serverConfig);
|
||||
|
||||
// Launch WebpackDevServer.
|
||||
|
||||
devServer.listen(port, HOST, (err) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
|
||||
return;
|
||||
return console.log(err);
|
||||
}
|
||||
if (isInteractive) {
|
||||
clearConsole();
|
||||
}
|
||||
|
||||
console.log(chalk.cyan('Starting the development server...\n'));
|
||||
openBrowser(urls.localUrlForBrowser);
|
||||
|
||||
return openBrowser(urls.localUrlForBrowser);
|
||||
});
|
||||
|
||||
[ 'SIGINT', 'SIGTERM' ].forEach((sig) => {
|
||||
|
||||
BIN
shlink-web-client.gif
Normal file
|
After Width: | Height: | Size: 4.3 MiB |
45
src/App.js
@@ -1,19 +1,40 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import NotFound from './common/NotFound';
|
||||
import './App.scss';
|
||||
|
||||
const App = (MainHeader, Home, MenuLayout, CreateServer) => () => (
|
||||
<div className="container-fluid app-container">
|
||||
<MainHeader />
|
||||
const propTypes = {
|
||||
fetchServers: PropTypes.func,
|
||||
servers: PropTypes.object,
|
||||
};
|
||||
|
||||
<div className="app">
|
||||
<Switch>
|
||||
<Route exact path="/server/create" component={CreateServer} />
|
||||
<Route exact path="/" component={Home} />
|
||||
<Route path="/server/:serverId" component={MenuLayout} />
|
||||
</Switch>
|
||||
const App = (MainHeader, Home, MenuLayout, CreateServer, EditServer, Settings) => ({ fetchServers, servers }) => {
|
||||
// On first load, try to fetch the remote servers if the list is empty
|
||||
useEffect(() => {
|
||||
if (Object.keys(servers).length === 0) {
|
||||
fetchServers();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="container-fluid app-container">
|
||||
<MainHeader />
|
||||
|
||||
<div className="app">
|
||||
<Switch>
|
||||
<Route exact path="/" component={Home} />
|
||||
<Route exact path="/settings" component={Settings} />
|
||||
<Route exact path="/server/create" component={CreateServer} />
|
||||
<Route exact path="/server/:serverId/edit" component={EditServer} />
|
||||
<Route path="/server/:serverId" component={MenuLayout} />
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
App.propTypes = propTypes;
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -1,18 +1,34 @@
|
||||
import listIcon from '@fortawesome/fontawesome-free-solid/faList';
|
||||
import createIcon from '@fortawesome/fontawesome-free-solid/faLink';
|
||||
import tagsIcon from '@fortawesome/fontawesome-free-solid/faTags';
|
||||
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faList as listIcon,
|
||||
faLink as createIcon,
|
||||
faTags as tagsIcon,
|
||||
faPen as editIcon,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import React from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import classNames from 'classnames';
|
||||
import { serverType } from '../servers/prop-types';
|
||||
import './AsideMenu.scss';
|
||||
|
||||
const defaultProps = {
|
||||
className: '',
|
||||
showOnMobile: false,
|
||||
const AsideMenuItem = ({ children, to, className, ...rest }) => (
|
||||
<NavLink
|
||||
className={classNames('aside-menu__item', className)}
|
||||
activeClassName="aside-menu__item--selected"
|
||||
to={to}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</NavLink>
|
||||
);
|
||||
|
||||
AsideMenuItem.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
to: PropTypes.string.isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
const propTypes = {
|
||||
selectedServer: serverType,
|
||||
className: PropTypes.string,
|
||||
@@ -22,43 +38,34 @@ const propTypes = {
|
||||
const AsideMenu = (DeleteServerButton) => {
|
||||
const AsideMenu = ({ selectedServer, className, showOnMobile }) => {
|
||||
const serverId = selectedServer ? selectedServer.id : '';
|
||||
const asideClass = classnames('aside-menu', className, {
|
||||
const asideClass = classNames('aside-menu', className, {
|
||||
'aside-menu--hidden': !showOnMobile,
|
||||
});
|
||||
const shortUrlsIsActive = (match, location) => location.pathname.match('/list-short-urls');
|
||||
const buildPath = (suffix) => `/server/${serverId}${suffix}`;
|
||||
|
||||
return (
|
||||
<aside className={asideClass}>
|
||||
<nav className="nav flex-column aside-menu__nav">
|
||||
<NavLink
|
||||
className="aside-menu__item"
|
||||
activeClassName="aside-menu__item--selected"
|
||||
to={`/server/${serverId}/list-short-urls/1`}
|
||||
isActive={shortUrlsIsActive}
|
||||
>
|
||||
<AsideMenuItem to={buildPath('/list-short-urls/1')} isActive={shortUrlsIsActive}>
|
||||
<FontAwesomeIcon icon={listIcon} />
|
||||
<span className="aside-menu__item-text">List short URLs</span>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
className="aside-menu__item"
|
||||
activeClassName="aside-menu__item--selected"
|
||||
to={`/server/${serverId}/create-short-url`}
|
||||
>
|
||||
</AsideMenuItem>
|
||||
<AsideMenuItem to={buildPath('/create-short-url')}>
|
||||
<FontAwesomeIcon icon={createIcon} flip="horizontal" />
|
||||
<span className="aside-menu__item-text">Create short URL</span>
|
||||
</NavLink>
|
||||
|
||||
<NavLink
|
||||
className="aside-menu__item"
|
||||
activeClassName="aside-menu__item--selected"
|
||||
to={`/server/${serverId}/manage-tags`}
|
||||
>
|
||||
</AsideMenuItem>
|
||||
<AsideMenuItem to={buildPath('/manage-tags')}>
|
||||
<FontAwesomeIcon icon={tagsIcon} />
|
||||
<span className="aside-menu__item-text">Manage tags</span>
|
||||
</NavLink>
|
||||
|
||||
</AsideMenuItem>
|
||||
<AsideMenuItem to={buildPath('/edit')} className="aside-menu__item--push">
|
||||
<FontAwesomeIcon icon={editIcon} />
|
||||
<span className="aside-menu__item-text">Edit this server</span>
|
||||
</AsideMenuItem>
|
||||
<DeleteServerButton
|
||||
className="aside-menu__item aside-menu__item--danger"
|
||||
textClassName="aside-menu__item-text"
|
||||
server={selectedServer}
|
||||
/>
|
||||
</nav>
|
||||
@@ -66,7 +73,6 @@ const AsideMenu = (DeleteServerButton) => {
|
||||
);
|
||||
};
|
||||
|
||||
AsideMenu.defaultProps = defaultProps;
|
||||
AsideMenu.propTypes = propTypes;
|
||||
|
||||
return AsideMenu;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
@import '../utils/base';
|
||||
@import '../utils/mixins/box-shadow';
|
||||
@import '../utils/mixins/vertical-align';
|
||||
|
||||
$asideMenuMobileWidth: 280px;
|
||||
@@ -26,8 +25,7 @@ $asideMenuMobileWidth: 280px;
|
||||
width: $asideMenuMobileWidth !important;
|
||||
transition: left 300ms;
|
||||
top: $headerHeight - 3px;
|
||||
|
||||
@include box-shadow(-10px 0 50px 11px rgba(0, 0, 0, .55));
|
||||
box-shadow: -10px 0 50px 11px rgba(0, 0, 0, .55);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +67,9 @@ $asideMenuMobileWidth: 280px;
|
||||
|
||||
.aside-menu__item--danger {
|
||||
color: $dangerColor;
|
||||
}
|
||||
|
||||
.aside-menu__item--push {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
|
||||
43
src/common/ErrorHandler.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import './ErrorHandler.scss';
|
||||
import { Button } from 'reactstrap';
|
||||
|
||||
// FIXME Replace with typescript: (window, console)
|
||||
const ErrorHandler = ({ location }, { error }) => class ErrorHandler extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError() {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch(e) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
error(e);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className="error-handler">
|
||||
<h1>Oops! This is awkward :S</h1>
|
||||
<p>It seems that something went wrong. Try refreshing the page or just click this button.</p>
|
||||
<br />
|
||||
<Button outline color="primary" onClick={() => location.reload()}>Take me back</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
};
|
||||
|
||||
export default ErrorHandler;
|
||||
9
src/common/ErrorHandler.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
@import '../utils/mixins/vertical-align.scss';
|
||||
|
||||
.error-handler {
|
||||
@include vertical-align();
|
||||
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -1,50 +1,34 @@
|
||||
import chevronIcon from '@fortawesome/fontawesome-free-solid/faChevronRight';
|
||||
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
|
||||
import React, { useEffect } from 'react';
|
||||
import { isEmpty, values } from 'ramda';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ListGroup, ListGroupItem } from 'reactstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
import './Home.scss';
|
||||
import ServersListGroup from '../servers/ServersListGroup';
|
||||
|
||||
export default class Home extends React.Component {
|
||||
static propTypes = {
|
||||
resetSelectedServer: PropTypes.func,
|
||||
servers: PropTypes.object,
|
||||
};
|
||||
const propTypes = {
|
||||
resetSelectedServer: PropTypes.func,
|
||||
servers: PropTypes.object,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.resetSelectedServer();
|
||||
}
|
||||
const Home = ({ resetSelectedServer, servers }) => {
|
||||
const serversList = values(servers);
|
||||
const hasServers = !isEmpty(serversList);
|
||||
|
||||
render() {
|
||||
const servers = values(this.props.servers);
|
||||
const hasServers = !isEmpty(servers);
|
||||
useEffect(() => {
|
||||
resetSelectedServer();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="home">
|
||||
<h1 className="home__title">Welcome to Shlink</h1>
|
||||
<h5 className="home__intro">
|
||||
{hasServers && <span>Please, select a server.</span>}
|
||||
{!hasServers && <span>Please, <Link to="/server/create">add a server</Link>.</span>}
|
||||
</h5>
|
||||
return (
|
||||
<div className="home">
|
||||
<h1 className="home__title">Welcome to Shlink</h1>
|
||||
<ServersListGroup servers={serversList}>
|
||||
{hasServers && <span>Please, select a server.</span>}
|
||||
{!hasServers && <span>Please, <Link to="/server/create">add a server</Link>.</span>}
|
||||
</ServersListGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
{hasServers && (
|
||||
<ListGroup className="home__servers-list">
|
||||
{servers.map(({ name, id }) => (
|
||||
<ListGroupItem
|
||||
key={id}
|
||||
tag={Link}
|
||||
to={`/server/${id}/list-short-urls/1`}
|
||||
className="home__servers-item"
|
||||
>
|
||||
{name}
|
||||
<FontAwesomeIcon icon={chevronIcon} className="home__servers-item-icon" />
|
||||
</ListGroupItem>
|
||||
))}
|
||||
</ListGroup>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Home.propTypes = propTypes;
|
||||
|
||||
export default Home;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
@import '../utils/base';
|
||||
@import '../utils/mixins/vertical-align';
|
||||
|
||||
.home {
|
||||
text-align: center;
|
||||
@@ -17,21 +16,3 @@
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.home__servers-list {
|
||||
margin-top: 1rem;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.home__servers-item.home__servers-item {
|
||||
text-align: left;
|
||||
position: relative;
|
||||
padding: .75rem 2.5rem .75rem 1rem;
|
||||
}
|
||||
|
||||
.home__servers-item-icon {
|
||||
@include vertical-align();
|
||||
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
@@ -1,38 +1,28 @@
|
||||
import plusIcon from '@fortawesome/fontawesome-free-solid/faPlus';
|
||||
import arrowIcon from '@fortawesome/fontawesome-free-solid/faChevronDown';
|
||||
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
|
||||
import React from 'react';
|
||||
import { faPlus as plusIcon, faChevronDown as arrowIcon, faCogs as cogsIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap';
|
||||
import classnames from 'classnames';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useToggle } from '../utils/helpers/hooks';
|
||||
import shlinkLogo from './shlink-logo-white.png';
|
||||
import './MainHeader.scss';
|
||||
|
||||
const MainHeader = (ServersDropdown) => class MainHeader extends React.Component {
|
||||
static propTypes = {
|
||||
location: PropTypes.object,
|
||||
};
|
||||
const propTypes = {
|
||||
location: PropTypes.object,
|
||||
};
|
||||
|
||||
state = { isOpen: false };
|
||||
handleToggle = () => {
|
||||
this.setState(({ isOpen }) => ({
|
||||
isOpen: !isOpen,
|
||||
}));
|
||||
};
|
||||
const MainHeader = (ServersDropdown) => {
|
||||
const MainHeaderComp = ({ location }) => {
|
||||
const [ isOpen, toggleOpen, , close ] = useToggle();
|
||||
const { pathname } = location;
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.location !== prevProps.location) {
|
||||
this.setState({ isOpen: false });
|
||||
}
|
||||
}
|
||||
useEffect(close, [ location ]);
|
||||
|
||||
render() {
|
||||
const { location } = this.props;
|
||||
const createServerPath = '/server/create';
|
||||
const toggleClass = classnames('main-header__toggle-icon', {
|
||||
'main-header__toggle-icon--opened': this.state.isOpen,
|
||||
});
|
||||
const settingsPath = '/settings';
|
||||
const toggleClass = classNames('main-header__toggle-icon', { 'main-header__toggle-icon--opened': isOpen });
|
||||
|
||||
return (
|
||||
<Navbar color="primary" dark fixed="top" className="main-header" expand="md">
|
||||
@@ -40,18 +30,19 @@ const MainHeader = (ServersDropdown) => class MainHeader extends React.Component
|
||||
<img src={shlinkLogo} alt="Shlink" className="main-header__brand-logo" /> Shlink
|
||||
</NavbarBrand>
|
||||
|
||||
<NavbarToggler onClick={this.handleToggle}>
|
||||
<NavbarToggler onClick={toggleOpen}>
|
||||
<FontAwesomeIcon icon={arrowIcon} className={toggleClass} />
|
||||
</NavbarToggler>
|
||||
|
||||
<Collapse navbar isOpen={this.state.isOpen}>
|
||||
<Collapse navbar isOpen={isOpen}>
|
||||
<Nav navbar className="ml-auto">
|
||||
<NavItem>
|
||||
<NavLink
|
||||
tag={Link}
|
||||
to={createServerPath}
|
||||
active={location.pathname === createServerPath}
|
||||
>
|
||||
<NavLink tag={Link} to={settingsPath} active={pathname === settingsPath}>
|
||||
<FontAwesomeIcon icon={cogsIcon} /> Settings
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<NavLink tag={Link} to={createServerPath} active={pathname === createServerPath}>
|
||||
<FontAwesomeIcon icon={plusIcon} /> Add server
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
@@ -60,7 +51,11 @@ const MainHeader = (ServersDropdown) => class MainHeader extends React.Component
|
||||
</Collapse>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
MainHeaderComp.propTypes = propTypes;
|
||||
|
||||
return MainHeaderComp;
|
||||
};
|
||||
|
||||
export default MainHeader;
|
||||
|
||||
@@ -1,100 +1,98 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import Swipeable from 'react-swipeable';
|
||||
import burgerIcon from '@fortawesome/fontawesome-free-solid/faBars';
|
||||
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
|
||||
import classnames from 'classnames';
|
||||
import { Swipeable } from 'react-swipeable';
|
||||
import { faBars as burgerIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import classNames from 'classnames';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import { serverType } from '../servers/prop-types';
|
||||
import { withSelectedServer } from '../servers/helpers/withSelectedServer';
|
||||
import { useToggle } from '../utils/helpers/hooks';
|
||||
import { versionMatch } from '../utils/helpers/version';
|
||||
import NotFound from './NotFound';
|
||||
import './MenuLayout.scss';
|
||||
|
||||
const MenuLayout = (TagsList, ShortUrls, AsideMenu, CreateShortUrl, ShortUrlVisits) =>
|
||||
class MenuLayout extends React.Component {
|
||||
static propTypes = {
|
||||
match: PropTypes.object,
|
||||
selectServer: PropTypes.func,
|
||||
location: PropTypes.object,
|
||||
selectedServer: serverType,
|
||||
const propTypes = {
|
||||
match: PropTypes.object,
|
||||
location: PropTypes.object,
|
||||
selectedServer: serverType,
|
||||
};
|
||||
|
||||
const MenuLayout = (
|
||||
TagsList,
|
||||
ShortUrls,
|
||||
AsideMenu,
|
||||
CreateShortUrl,
|
||||
ShortUrlVisits,
|
||||
TagVisits,
|
||||
ShlinkVersions,
|
||||
ServerError
|
||||
) => {
|
||||
const MenuLayoutComp = ({ match, location, selectedServer }) => {
|
||||
const [ sidebarVisible, toggleSidebar, showSidebar, hideSidebar ] = useToggle();
|
||||
const { params: { serverId } } = match;
|
||||
|
||||
useEffect(() => hideSidebar(), [ location ]);
|
||||
|
||||
if (selectedServer.serverNotReachable) {
|
||||
return <ServerError type="not-reachable" />;
|
||||
}
|
||||
|
||||
const addTagsVisitsRoute = versionMatch(selectedServer.version, { minVersion: '2.2.0' });
|
||||
const burgerClasses = classNames('menu-layout__burger-icon', {
|
||||
'menu-layout__burger-icon--active': sidebarVisible,
|
||||
});
|
||||
const swipeMenuIfNoModalExists = (callback) => (e) => {
|
||||
const swippedOnVisitsTable = e.event.path.some(
|
||||
({ classList }) => classList && classList.contains('visits-table')
|
||||
);
|
||||
|
||||
if (swippedOnVisitsTable || document.querySelector('.modal')) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback();
|
||||
};
|
||||
|
||||
state = { showSideBar: false };
|
||||
return (
|
||||
<React.Fragment>
|
||||
<FontAwesomeIcon icon={burgerIcon} className={burgerClasses} onClick={toggleSidebar} />
|
||||
|
||||
// FIXME Shouldn't use componentWillMount, but this code has to be run before children components are rendered
|
||||
/* eslint react/no-deprecated: "off" */
|
||||
componentWillMount() {
|
||||
const { match, selectServer } = this.props;
|
||||
const { params: { serverId } } = match;
|
||||
|
||||
selectServer(serverId);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { location } = this.props;
|
||||
|
||||
// Hide sidebar when location changes
|
||||
if (location !== prevProps.location) {
|
||||
this.setState({ showSideBar: false });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { selectedServer } = this.props;
|
||||
const burgerClasses = classnames('menu-layout__burger-icon', {
|
||||
'menu-layout__burger-icon--active': this.state.showSideBar,
|
||||
});
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<FontAwesomeIcon
|
||||
icon={burgerIcon}
|
||||
className={burgerClasses}
|
||||
onClick={() => this.setState(({ showSideBar }) => ({ showSideBar: !showSideBar }))}
|
||||
/>
|
||||
|
||||
<Swipeable
|
||||
delta={40}
|
||||
className="menu-layout__swipeable"
|
||||
onSwipedLeft={() => this.setState({ showSideBar: false })}
|
||||
onSwipedRight={() => this.setState({ showSideBar: true })}
|
||||
>
|
||||
<div className="row menu-layout__swipeable-inner">
|
||||
<AsideMenu
|
||||
className="col-lg-2 col-md-3"
|
||||
selectedServer={selectedServer}
|
||||
showOnMobile={this.state.showSideBar}
|
||||
/>
|
||||
<div
|
||||
className="col-lg-10 offset-lg-2 col-md-9 offset-md-3"
|
||||
onClick={() => this.setState({ showSideBar: false })}
|
||||
>
|
||||
<Swipeable
|
||||
delta={40}
|
||||
className="menu-layout__swipeable"
|
||||
onSwipedLeft={swipeMenuIfNoModalExists(hideSidebar)}
|
||||
onSwipedRight={swipeMenuIfNoModalExists(showSidebar)}
|
||||
>
|
||||
<div className="row menu-layout__swipeable-inner">
|
||||
<AsideMenu className="col-lg-2 col-md-3" selectedServer={selectedServer} showOnMobile={sidebarVisible} />
|
||||
<div className="col-lg-10 offset-lg-2 col-md-9 offset-md-3" onClick={() => hideSidebar()}>
|
||||
<div className="menu-layout__container">
|
||||
<Switch>
|
||||
<Route exact path="/server/:serverId/list-short-urls/:page" component={ShortUrls} />
|
||||
<Route exact path="/server/:serverId/create-short-url" component={CreateShortUrl} />
|
||||
<Route exact path="/server/:serverId/short-code/:shortCode/visits" component={ShortUrlVisits} />
|
||||
{addTagsVisitsRoute && <Route exact path="/server/:serverId/tag/:tag/visits" component={TagVisits} />}
|
||||
<Route exact path="/server/:serverId/manage-tags" component={TagsList} />
|
||||
<Route
|
||||
exact
|
||||
path="/server/:serverId/list-short-urls/:page"
|
||||
component={ShortUrls}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/server/:serverId/create-short-url"
|
||||
component={CreateShortUrl}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/server/:serverId/short-code/:shortCode/visits"
|
||||
component={ShortUrlVisits}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/server/:serverId/manage-tags"
|
||||
component={TagsList}
|
||||
render={() => <NotFound to={`/server/${serverId}/list-short-urls/1`}>List short URLs</NotFound>}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
|
||||
<div className="menu-layout__footer text-center text-md-right">
|
||||
<ShlinkVersions />
|
||||
</div>
|
||||
</div>
|
||||
</Swipeable>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
</Swipeable>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
MenuLayoutComp.propTypes = propTypes;
|
||||
|
||||
return withSelectedServer(MenuLayoutComp, ServerError);
|
||||
};
|
||||
|
||||
export default MenuLayout;
|
||||
|
||||
@@ -32,3 +32,26 @@
|
||||
.menu-layout__burger-icon--active {
|
||||
color: white;
|
||||
}
|
||||
|
||||
$footer-height: 2.3rem;
|
||||
$footer-margin: .8rem;
|
||||
|
||||
.menu-layout__container {
|
||||
padding: 20px 0 ($footer-height + $footer-margin);
|
||||
min-height: 100%;
|
||||
margin-bottom: -($footer-height + $footer-margin);
|
||||
|
||||
@media (min-width: $mdMin) {
|
||||
padding: 30px 15px ($footer-height + $footer-margin);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-layout__footer {
|
||||
height: $footer-height;
|
||||
margin-top: $footer-margin;
|
||||
padding: 0;
|
||||
|
||||
@media (min-width: $mdMin) {
|
||||
padding: 0 15px;
|
||||
}
|
||||
}
|
||||
|
||||
13
src/common/NoMenuLayout.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './NoMenuLayout.scss';
|
||||
|
||||
const propTypes = {
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
const NoMenuLayout = ({ children }) => <div className="no-menu-wrapper">{children}</div>;
|
||||
|
||||
NoMenuLayout.propTypes = propTypes;
|
||||
|
||||
export default NoMenuLayout;
|
||||
3
src/common/NoMenuLayout.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
.no-menu-wrapper {
|
||||
padding: 40px 20px;
|
||||
}
|
||||
24
src/common/NotFound.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import * as PropTypes from 'prop-types';
|
||||
|
||||
const propTypes = {
|
||||
to: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
const NotFound = ({ to = '/', children = 'Home' }) => (
|
||||
<div className="home">
|
||||
<h2>Oops! We could not find requested route.</h2>
|
||||
<p>
|
||||
Use your browser's back button to navigate to the page you have previously come from, or just press this
|
||||
button.
|
||||
</p>
|
||||
<br />
|
||||
<Link to={to} className="btn btn-outline-primary btn-lg">{children}</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
NotFound.propTypes = propTypes;
|
||||
|
||||
export default NotFound;
|
||||
@@ -1,23 +1,23 @@
|
||||
import React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const ScrollToTop = (window) => class ScrollToTop extends React.Component {
|
||||
static propTypes = {
|
||||
location: PropTypes.object,
|
||||
children: PropTypes.node,
|
||||
const propTypes = {
|
||||
location: PropTypes.object,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
const ScrollToTop = () => {
|
||||
const ScrollToTopComp = ({ location, children }) => {
|
||||
useEffect(() => {
|
||||
scrollTo(0, 0);
|
||||
}, [ location ]);
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { location } = this.props;
|
||||
ScrollToTopComp.propTypes = propTypes;
|
||||
|
||||
if (location !== prevProps.location) {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.props.children;
|
||||
}
|
||||
return ScrollToTopComp;
|
||||
};
|
||||
|
||||
export default ScrollToTop;
|
||||
|
||||
29
src/common/ShlinkVersions.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { pipe } from 'ramda';
|
||||
import { serverType } from '../servers/prop-types';
|
||||
import { versionToPrintable, versionToSemVer } from '../utils/helpers/version';
|
||||
|
||||
const SHLINK_WEB_CLIENT_VERSION = '%_VERSION_%';
|
||||
|
||||
const propTypes = {
|
||||
selectedServer: serverType,
|
||||
className: PropTypes.string,
|
||||
clientVersion: PropTypes.string,
|
||||
};
|
||||
|
||||
const ShlinkVersions = ({ selectedServer, className, clientVersion = SHLINK_WEB_CLIENT_VERSION }) => {
|
||||
const { printableVersion: serverVersion } = selectedServer;
|
||||
const normalizedClientVersion = pipe(versionToSemVer(), versionToPrintable)(clientVersion);
|
||||
|
||||
return (
|
||||
<small className={classNames('text-muted', className)}>
|
||||
Client: <b>{normalizedClientVersion}</b> - Server: <b>{serverVersion}</b>
|
||||
</small>
|
||||
);
|
||||
};
|
||||
|
||||
ShlinkVersions.propTypes = propTypes;
|
||||
|
||||
export default ShlinkVersions;
|
||||
45
src/common/SimplePaginator.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { Pagination, PaginationItem, PaginationLink } from 'reactstrap';
|
||||
import { isPageDisabled, keyForPage, progressivePagination } from '../utils/helpers/pagination';
|
||||
import './SimplePaginator.scss';
|
||||
|
||||
const propTypes = {
|
||||
pagesCount: PropTypes.number.isRequired,
|
||||
currentPage: PropTypes.number.isRequired,
|
||||
setCurrentPage: PropTypes.func.isRequired,
|
||||
centered: PropTypes.bool,
|
||||
};
|
||||
|
||||
const SimplePaginator = ({ pagesCount, currentPage, setCurrentPage, centered = true }) => {
|
||||
if (pagesCount < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onClick = (page) => () => setCurrentPage(page);
|
||||
|
||||
return (
|
||||
<Pagination listClassName={classNames('flex-wrap mb-0 simple-paginator', { 'justify-content-center': centered })}>
|
||||
<PaginationItem disabled={currentPage <= 1}>
|
||||
<PaginationLink previous tag="span" onClick={onClick(currentPage - 1)} />
|
||||
</PaginationItem>
|
||||
{progressivePagination(currentPage, pagesCount).map((pageNumber, index) => (
|
||||
<PaginationItem
|
||||
key={keyForPage(pageNumber, index)}
|
||||
disabled={isPageDisabled(pageNumber)}
|
||||
active={currentPage === pageNumber}
|
||||
>
|
||||
<PaginationLink tag="span" onClick={onClick(pageNumber)}>{pageNumber}</PaginationLink>
|
||||
</PaginationItem>
|
||||
))}
|
||||
<PaginationItem disabled={currentPage >= pagesCount}>
|
||||
<PaginationLink next tag="span" onClick={onClick(currentPage + 1)} />
|
||||
</PaginationItem>
|
||||
</Pagination>
|
||||
);
|
||||
};
|
||||
|
||||
SimplePaginator.propTypes = propTypes;
|
||||
|
||||
export default SimplePaginator;
|
||||
3
src/common/SimplePaginator.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
.simple-paginator {
|
||||
user-select: none;
|
||||
}
|
||||
@@ -27,12 +27,12 @@
|
||||
|
||||
.react-tagsinput-remove {
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
font-weight: 700;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.react-tagsinput-tag span:before {
|
||||
content: "\2715";
|
||||
content: '\2715';
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,12 @@ import MainHeader from '../MainHeader';
|
||||
import Home from '../Home';
|
||||
import MenuLayout from '../MenuLayout';
|
||||
import AsideMenu from '../AsideMenu';
|
||||
import ErrorHandler from '../ErrorHandler';
|
||||
import ShlinkVersions from '../ShlinkVersions';
|
||||
|
||||
const provideServices = (bottle, connect, withRouter) => {
|
||||
bottle.constant('window', global.window);
|
||||
bottle.constant('console', global.console);
|
||||
|
||||
bottle.serviceFactory('ScrollToTop', ScrollToTop, 'window');
|
||||
bottle.decorator('ScrollToTop', withRouter);
|
||||
@@ -23,12 +26,20 @@ const provideServices = (bottle, connect, withRouter) => {
|
||||
'ShortUrls',
|
||||
'AsideMenu',
|
||||
'CreateShortUrl',
|
||||
'ShortUrlVisits'
|
||||
'ShortUrlVisits',
|
||||
'TagVisits',
|
||||
'ShlinkVersions',
|
||||
'ServerError'
|
||||
);
|
||||
bottle.decorator('MenuLayout', connect([ 'selectedServer', 'shortUrlsListParams' ], [ 'selectServer' ]));
|
||||
bottle.decorator('MenuLayout', withRouter);
|
||||
|
||||
bottle.serviceFactory('AsideMenu', AsideMenu, 'DeleteServerButton');
|
||||
|
||||
bottle.serviceFactory('ShlinkVersions', () => ShlinkVersions);
|
||||
bottle.decorator('ShlinkVersions', connect([ 'selectedServer' ]));
|
||||
|
||||
bottle.serviceFactory('ErrorHandler', ErrorHandler, 'window', 'console');
|
||||
};
|
||||
|
||||
export default provideServices;
|
||||
|
||||
@@ -9,23 +9,27 @@ import provideServersServices from '../servers/services/provideServices';
|
||||
import provideVisitsServices from '../visits/services/provideServices';
|
||||
import provideTagsServices from '../tags/services/provideServices';
|
||||
import provideUtilsServices from '../utils/services/provideServices';
|
||||
import provideMercureServices from '../mercure/services/provideServices';
|
||||
import provideSettingsServices from '../settings/services/provideServices';
|
||||
|
||||
const bottle = new Bottle();
|
||||
const { container } = bottle;
|
||||
|
||||
const lazyService = (container, serviceName) => (...args) => container[serviceName](...args);
|
||||
const mapActionService = (map, actionName) => ({
|
||||
...map,
|
||||
|
||||
// Wrap actual action service in a function so that it is lazily created the first time it is called
|
||||
[actionName]: (...args) => container[actionName](...args),
|
||||
[actionName]: lazyService(container, actionName),
|
||||
});
|
||||
const connect = (propsFromState, actionServiceNames) =>
|
||||
const connect = (propsFromState, actionServiceNames = []) =>
|
||||
reduxConnect(
|
||||
propsFromState ? pick(propsFromState) : null,
|
||||
actionServiceNames.reduce(mapActionService, {})
|
||||
);
|
||||
|
||||
bottle.serviceFactory('App', App, 'MainHeader', 'Home', 'MenuLayout', 'CreateServer');
|
||||
bottle.serviceFactory('App', App, 'MainHeader', 'Home', 'MenuLayout', 'CreateServer', 'EditServer', 'Settings');
|
||||
bottle.decorator('App', connect([ 'servers' ], [ 'fetchServers' ]));
|
||||
|
||||
provideCommonServices(bottle, connect, withRouter);
|
||||
provideShortUrlsServices(bottle, connect);
|
||||
@@ -33,5 +37,7 @@ provideServersServices(bottle, connect, withRouter);
|
||||
provideTagsServices(bottle, connect);
|
||||
provideVisitsServices(bottle, connect);
|
||||
provideUtilsServices(bottle);
|
||||
provideMercureServices(bottle);
|
||||
provideSettingsServices(bottle, connect);
|
||||
|
||||
export default container;
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import ReduxThunk from 'redux-thunk';
|
||||
import { applyMiddleware, compose, createStore } from 'redux';
|
||||
import { save, load } from 'redux-localstorage-simple';
|
||||
import reducers from '../reducers';
|
||||
|
||||
const composeEnhancers = process.env.NODE_ENV !== 'production' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
|
||||
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
|
||||
: compose;
|
||||
|
||||
const store = createStore(reducers, composeEnhancers(
|
||||
applyMiddleware(ReduxThunk)
|
||||
const localStorageConfig = {
|
||||
states: [ 'settings', 'servers' ],
|
||||
namespace: 'shlink',
|
||||
namespaceSeparator: '.',
|
||||
debounce: 300,
|
||||
};
|
||||
|
||||
const store = createStore(reducers, load(localStorageConfig), composeEnhancers(
|
||||
applyMiddleware(save(localStorageConfig), ReduxThunk)
|
||||
));
|
||||
|
||||
export default store;
|
||||
|
||||
17
src/index.js
@@ -7,18 +7,25 @@ import { homepage } from '../package.json';
|
||||
import registerServiceWorker from './registerServiceWorker';
|
||||
import container from './container';
|
||||
import store from './container/store';
|
||||
import '../node_modules/react-datepicker/dist/react-datepicker.css';
|
||||
import { fixLeafletIcons } from './utils/utils';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import './common/react-tagsinput.scss';
|
||||
import './index.scss';
|
||||
|
||||
const { App, ScrollToTop } = container;
|
||||
// This overwrites icons used for leaflet maps, fixing some issues caused by webpack while processing the CSS
|
||||
fixLeafletIcons();
|
||||
|
||||
const { App, ScrollToTop, ErrorHandler } = container;
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<BrowserRouter basename={homepage}>
|
||||
<ScrollToTop>
|
||||
<App />
|
||||
</ScrollToTop>
|
||||
<ErrorHandler>
|
||||
<ScrollToTop>
|
||||
<App />
|
||||
</ScrollToTop>
|
||||
</ErrorHandler>
|
||||
</BrowserRouter>
|
||||
</Provider>,
|
||||
document.getElementById('root')
|
||||
|
||||
@@ -10,10 +10,6 @@ body,
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bg-main {
|
||||
background-color: $mainColor !important;
|
||||
}
|
||||
@@ -28,14 +24,6 @@ body,
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
.shlink-container {
|
||||
padding: 20px 0;
|
||||
|
||||
@media (min-width: $mdMin) {
|
||||
padding: 30px 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.badge-main {
|
||||
color: #fff;
|
||||
background-color: $mainColor;
|
||||
@@ -51,3 +39,29 @@ body,
|
||||
margin: 0 auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination .page-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.indivisible {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.text-ellipsis {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.react-datepicker__day--keyboard-selected {
|
||||
background-color: $mainColor;
|
||||
|
||||
&:hover {
|
||||
background-color: darken($mainColor, 12%);
|
||||
}
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
background-color: $mainColor;
|
||||
}
|
||||
|
||||
28
src/mercure/helpers/index.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useEffect } from 'react';
|
||||
import { EventSourcePolyfill as EventSource } from 'event-source-polyfill';
|
||||
|
||||
export const bindToMercureTopic = (mercureInfo, topic, onMessage, onTokenExpired) => () => {
|
||||
const { mercureHubUrl, token, loading, error } = mercureInfo;
|
||||
|
||||
if (loading || error) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const hubUrl = new URL(mercureHubUrl);
|
||||
|
||||
hubUrl.searchParams.append('topic', topic);
|
||||
const es = new EventSource(hubUrl, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
es.onmessage = ({ data }) => onMessage(JSON.parse(data));
|
||||
es.onerror = ({ status }) => status === 401 && onTokenExpired();
|
||||
|
||||
return () => es.close();
|
||||
};
|
||||
|
||||
export const useMercureTopicBinding = (mercureInfo, topic, onMessage, onTokenExpired) => {
|
||||
useEffect(bindToMercureTopic(mercureInfo, topic, onMessage, onTokenExpired), [ mercureInfo ]);
|
||||
};
|
||||
49
src/mercure/reducers/mercureInfo.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import { handleActions } from 'redux-actions';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/* eslint-disable padding-line-between-statements */
|
||||
export const GET_MERCURE_INFO_START = 'shlink/mercure/GET_MERCURE_INFO_START';
|
||||
export const GET_MERCURE_INFO_ERROR = 'shlink/mercure/GET_MERCURE_INFO_ERROR';
|
||||
export const GET_MERCURE_INFO = 'shlink/mercure/GET_MERCURE_INFO';
|
||||
/* eslint-enable padding-line-between-statements */
|
||||
|
||||
export const MercureInfoType = PropTypes.shape({
|
||||
token: PropTypes.string,
|
||||
mercureHubUrl: PropTypes.string,
|
||||
loading: PropTypes.bool,
|
||||
error: PropTypes.bool,
|
||||
});
|
||||
|
||||
const initialState = {
|
||||
token: undefined,
|
||||
mercureHubUrl: undefined,
|
||||
loading: true,
|
||||
error: false,
|
||||
};
|
||||
|
||||
export default handleActions({
|
||||
[GET_MERCURE_INFO_START]: (state) => ({ ...state, loading: true, error: false }),
|
||||
[GET_MERCURE_INFO_ERROR]: (state) => ({ ...state, loading: false, error: true }),
|
||||
[GET_MERCURE_INFO]: (state, { token, mercureHubUrl }) => ({ token, mercureHubUrl, loading: false, error: false }),
|
||||
}, initialState);
|
||||
|
||||
export const loadMercureInfo = (buildShlinkApiClient) => () => async (dispatch, getState) => {
|
||||
dispatch({ type: GET_MERCURE_INFO_START });
|
||||
|
||||
const { settings } = getState();
|
||||
const { mercureInfo } = buildShlinkApiClient(getState);
|
||||
|
||||
if (!settings.realTimeUpdates.enabled) {
|
||||
dispatch({ type: GET_MERCURE_INFO_ERROR });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await mercureInfo();
|
||||
|
||||
dispatch({ type: GET_MERCURE_INFO, ...result });
|
||||
} catch (e) {
|
||||
dispatch({ type: GET_MERCURE_INFO_ERROR });
|
||||
}
|
||||
};
|
||||
8
src/mercure/services/provideServices.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { loadMercureInfo } from '../reducers/mercureInfo';
|
||||
|
||||
const provideServices = (bottle) => {
|
||||
// Actions
|
||||
bottle.serviceFactory('loadMercureInfo', loadMercureInfo, 'buildShlinkApiClient');
|
||||
};
|
||||
|
||||
export default provideServices;
|
||||
@@ -1,16 +1,21 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import serversReducer from '../servers/reducers/server';
|
||||
import serversReducer from '../servers/reducers/servers';
|
||||
import selectedServerReducer from '../servers/reducers/selectedServer';
|
||||
import shortUrlsListReducer from '../short-urls/reducers/shortUrlsList';
|
||||
import shortUrlsListParamsReducer from '../short-urls/reducers/shortUrlsListParams';
|
||||
import shortUrlCreationReducer from '../short-urls/reducers/shortUrlCreation';
|
||||
import shortUrlDeletionReducer from '../short-urls/reducers/shortUrlDeletion';
|
||||
import shortUrlTagsReducer from '../short-urls/reducers/shortUrlTags';
|
||||
import shortUrlMetaReducer from '../short-urls/reducers/shortUrlMeta';
|
||||
import shortUrlEditionReducer from '../short-urls/reducers/shortUrlEdition';
|
||||
import shortUrlVisitsReducer from '../visits/reducers/shortUrlVisits';
|
||||
import tagVisitsReducer from '../visits/reducers/tagVisits';
|
||||
import shortUrlDetailReducer from '../visits/reducers/shortUrlDetail';
|
||||
import tagsListReducer from '../tags/reducers/tagsList';
|
||||
import tagDeleteReducer from '../tags/reducers/tagDelete';
|
||||
import tagEditReducer from '../tags/reducers/tagEdit';
|
||||
import mercureInfoReducer from '../mercure/reducers/mercureInfo';
|
||||
import settingsReducer from '../settings/reducers/settings';
|
||||
|
||||
export default combineReducers({
|
||||
servers: serversReducer,
|
||||
@@ -20,9 +25,14 @@ export default combineReducers({
|
||||
shortUrlCreationResult: shortUrlCreationReducer,
|
||||
shortUrlDeletion: shortUrlDeletionReducer,
|
||||
shortUrlTags: shortUrlTagsReducer,
|
||||
shortUrlMeta: shortUrlMetaReducer,
|
||||
shortUrlEdition: shortUrlEditionReducer,
|
||||
shortUrlVisits: shortUrlVisitsReducer,
|
||||
tagVisits: tagVisitsReducer,
|
||||
shortUrlDetail: shortUrlDetailReducer,
|
||||
tagsList: tagsListReducer,
|
||||
tagDelete: tagDeleteReducer,
|
||||
tagEdit: tagEditReducer,
|
||||
mercureInfo: mercureInfoReducer,
|
||||
settings: settingsReducer,
|
||||
});
|
||||
|
||||
@@ -43,22 +43,22 @@ export default function register() {
|
||||
|
||||
// Add some additional logging to localhost, pointing developers to the
|
||||
// service worker/PWA documentation.
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
return navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit https://goo.gl/SC7cgQ'
|
||||
'worker. To learn more, visit https://goo.gl/SC7cgQ'
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Is not local host. Just register service worker
|
||||
registerValidSW(swUrl);
|
||||
}
|
||||
|
||||
// Is not local host. Just register service worker
|
||||
return registerValidSW(swUrl);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl) {
|
||||
navigator.serviceWorker
|
||||
return navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then((registration) => {
|
||||
registration.onupdatefound = () => {
|
||||
@@ -99,15 +99,14 @@ function checkValidServiceWorker(swUrl) {
|
||||
response.headers.get('content-type').indexOf('javascript') === -1
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
return navigator.serviceWorker.ready.then((registration) =>
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Service worker found. Proceed as normal.
|
||||
registerValidSW(swUrl);
|
||||
}));
|
||||
}
|
||||
|
||||
// Service worker found. Proceed as normal.
|
||||
return registerValidSW(swUrl);
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
@@ -120,6 +119,6 @@ export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister();
|
||||
});
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,92 +1,57 @@
|
||||
import { assoc, dissoc, pipe } from 'ramda';
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import PropTypes from 'prop-types';
|
||||
import { stateFlagTimeout } from '../utils/utils';
|
||||
import NoMenuLayout from '../common/NoMenuLayout';
|
||||
import { ServerForm } from './helpers/ServerForm';
|
||||
import './CreateServer.scss';
|
||||
|
||||
const SHOW_IMPORT_MSG_TIME = 4000;
|
||||
const propTypes = {
|
||||
createServer: PropTypes.func,
|
||||
history: PropTypes.shape({
|
||||
push: PropTypes.func,
|
||||
}),
|
||||
resetSelectedServer: PropTypes.func,
|
||||
};
|
||||
|
||||
const CreateServer = (ImportServersBtn) => class CreateServer extends React.Component {
|
||||
static propTypes = {
|
||||
createServer: PropTypes.func,
|
||||
history: PropTypes.shape({
|
||||
push: PropTypes.func,
|
||||
}),
|
||||
resetSelectedServer: PropTypes.func,
|
||||
};
|
||||
const CreateServer = (ImportServersBtn, useStateFlagTimeout) => {
|
||||
const CreateServerComp = ({ createServer, history: { push }, resetSelectedServer }) => {
|
||||
const [ serversImported, setServersImported ] = useStateFlagTimeout(false, SHOW_IMPORT_MSG_TIME);
|
||||
const handleSubmit = (serverData) => {
|
||||
const id = uuid();
|
||||
const server = { id, ...serverData };
|
||||
|
||||
state = {
|
||||
name: '',
|
||||
url: '',
|
||||
apiKey: '',
|
||||
serversImported: false,
|
||||
};
|
||||
createServer(server);
|
||||
push(`/server/${id}/list-short-urls/1`);
|
||||
};
|
||||
|
||||
handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const { createServer, history: { push } } = this.props;
|
||||
const server = pipe(
|
||||
assoc('id', uuid()),
|
||||
dissoc('serversImported')
|
||||
)(this.state);
|
||||
|
||||
createServer(server);
|
||||
push(`/server/${server.id}/list-short-urls/1`);
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.resetSelectedServer();
|
||||
}
|
||||
|
||||
render() {
|
||||
const renderInputGroup = (id, placeholder, type = 'text') => (
|
||||
<div className="form-group row">
|
||||
<label htmlFor={id} className="col-lg-1 col-md-2 col-form-label create-server__label">
|
||||
{placeholder}:
|
||||
</label>
|
||||
<div className="col-lg-11 col-md-10">
|
||||
<input
|
||||
type={type}
|
||||
className="form-control"
|
||||
id={id}
|
||||
placeholder={placeholder}
|
||||
value={this.state[id]}
|
||||
required
|
||||
onChange={(e) => this.setState({ [id]: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
useEffect(() => {
|
||||
resetSelectedServer();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="create-server">
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
{renderInputGroup('name', 'Name')}
|
||||
{renderInputGroup('url', 'URL', 'url')}
|
||||
{renderInputGroup('apiKey', 'API key')}
|
||||
<NoMenuLayout>
|
||||
<ServerForm onSubmit={handleSubmit}>
|
||||
<ImportServersBtn onImport={setServersImported} />
|
||||
<button className="btn btn-outline-primary">Create server</button>
|
||||
</ServerForm>
|
||||
|
||||
<div className="text-right">
|
||||
<ImportServersBtn
|
||||
onImport={() => stateFlagTimeout(this.setState.bind(this), 'serversImported', true, SHOW_IMPORT_MSG_TIME)}
|
||||
/>
|
||||
<button className="btn btn-outline-primary">Create server</button>
|
||||
</div>
|
||||
|
||||
{this.state.serversImported && (
|
||||
<div className="row create-server__import-success-msg">
|
||||
<div className="col-md-10 offset-md-1">
|
||||
<div className="p-2 mt-3 bg-main text-white text-center">
|
||||
Servers properly imported. You can now select one from the list :)
|
||||
</div>
|
||||
{serversImported && (
|
||||
<div className="row create-server__import-success-msg">
|
||||
<div className="col-md-10 offset-md-1">
|
||||
<div className="p-2 mt-3 bg-main text-white text-center">
|
||||
Servers properly imported. You can now select one from the list :)
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</NoMenuLayout>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
CreateServerComp.propTypes = propTypes;
|
||||
|
||||
return CreateServerComp;
|
||||
};
|
||||
|
||||
export default CreateServer;
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
@import '../utils/base';
|
||||
|
||||
.create-server {
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.create-server__label {
|
||||
font-weight: bold;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
|
||||
@media (min-width: $mdMin) {
|
||||
|
||||
@@ -1,40 +1,36 @@
|
||||
import deleteIcon from '@fortawesome/fontawesome-free-solid/faMinusCircle';
|
||||
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
|
||||
import React from 'react';
|
||||
import { faMinusCircle as deleteIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useToggle } from '../utils/helpers/hooks';
|
||||
import { serverType } from './prop-types';
|
||||
|
||||
const DeleteServerButton = (DeleteServerModal) => class DeleteServerButton extends React.Component {
|
||||
static propTypes = {
|
||||
server: serverType,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
const propTypes = {
|
||||
server: serverType,
|
||||
className: PropTypes.string,
|
||||
textClassName: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
state = { isModalOpen: false };
|
||||
|
||||
render() {
|
||||
const { server, className } = this.props;
|
||||
const DeleteServerButton = (DeleteServerModal) => {
|
||||
const DeleteServerButtonComp = ({ server, className, children, textClassName }) => {
|
||||
const [ isModalOpen, , showModal, hideModal ] = useToggle();
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<span
|
||||
className={className}
|
||||
key="deleteServerBtn"
|
||||
onClick={() => this.setState({ isModalOpen: true })}
|
||||
>
|
||||
<FontAwesomeIcon icon={deleteIcon} />
|
||||
<span className="aside-menu__item-text">Delete this server</span>
|
||||
<span className={className} onClick={showModal}>
|
||||
{!children && <FontAwesomeIcon icon={deleteIcon} />}
|
||||
<span className={textClassName}>{children || 'Remove this server'}</span>
|
||||
</span>
|
||||
|
||||
<DeleteServerModal
|
||||
isOpen={this.state.isModalOpen}
|
||||
toggle={() => this.setState(({ isModalOpen }) => ({ isModalOpen: !isModalOpen }))}
|
||||
server={server}
|
||||
key="deleteServerModal"
|
||||
/>
|
||||
<DeleteServerModal server={server} isOpen={isModalOpen} toggle={hideModal} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
DeleteServerButtonComp.propTypes = propTypes;
|
||||
|
||||
return DeleteServerButtonComp;
|
||||
};
|
||||
|
||||
export default DeleteServerButton;
|
||||
|
||||
@@ -22,12 +22,14 @@ const DeleteServerModal = ({ server, toggle, isOpen, deleteServer, history }) =>
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} toggle={toggle} centered>
|
||||
<ModalHeader toggle={toggle}><span className="text-danger">Delete server</span></ModalHeader>
|
||||
<ModalHeader toggle={toggle}><span className="text-danger">Remove server</span></ModalHeader>
|
||||
<ModalBody>
|
||||
<p>Are you sure you want to delete server <b>{server ? server.name : ''}</b>?</p>
|
||||
<p>Are you sure you want to remove <b>{server ? server.name : ''}</b>?</p>
|
||||
<p>
|
||||
No data will be deleted, only the access to that server will be removed from this host.
|
||||
You can create it again at any moment.
|
||||
<i>
|
||||
No data will be deleted, only the access to this server will be removed from this host.
|
||||
You can create it again at any moment.
|
||||
</i>
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
|
||||
38
src/servers/EditServer.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from 'reactstrap';
|
||||
import NoMenuLayout from '../common/NoMenuLayout';
|
||||
import { ServerForm } from './helpers/ServerForm';
|
||||
import { withSelectedServer } from './helpers/withSelectedServer';
|
||||
import { serverType } from './prop-types';
|
||||
|
||||
const propTypes = {
|
||||
editServer: PropTypes.func,
|
||||
selectedServer: serverType,
|
||||
history: PropTypes.shape({
|
||||
push: PropTypes.func,
|
||||
goBack: PropTypes.func,
|
||||
}),
|
||||
};
|
||||
|
||||
export const EditServer = (ServerError) => {
|
||||
const EditServerComp = ({ editServer, selectedServer, history: { push, goBack } }) => {
|
||||
const handleSubmit = (serverData) => {
|
||||
editServer(selectedServer.id, serverData);
|
||||
push(`/server/${selectedServer.id}/list-short-urls/1`);
|
||||
};
|
||||
|
||||
return (
|
||||
<NoMenuLayout>
|
||||
<ServerForm initialValues={selectedServer} onSubmit={handleSubmit}>
|
||||
<Button outline className="mr-2" onClick={goBack}>Cancel</Button>
|
||||
<Button outline color="primary">Save</Button>
|
||||
</ServerForm>
|
||||
</NoMenuLayout>
|
||||
);
|
||||
};
|
||||
|
||||
EditServerComp.propTypes = propTypes;
|
||||
|
||||
return withSelectedServer(EditServerComp, ServerError);
|
||||
};
|
||||