Compare commits
653 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8f9e5bcf2 | ||
|
|
0a9a5909d1 | ||
|
|
8f75ea4728 | ||
|
|
91ad46b202 | ||
|
|
752806c0ef | ||
|
|
c39bb67bf3 | ||
|
|
fe6097e0d9 | ||
|
|
6ddf0bb889 | ||
|
|
8dcbad97df | ||
|
|
64b3955bc9 | ||
|
|
0f88a13635 | ||
|
|
e8c7d38608 | ||
|
|
6020657529 | ||
|
|
a8c35f3967 | ||
|
|
62fa8f511a | ||
|
|
18cf202ac0 | ||
|
|
98d9018e13 | ||
|
|
a9e14f6165 | ||
|
|
7005e9f760 | ||
|
|
163bc4e0b3 | ||
|
|
3420886192 | ||
|
|
1704ef6557 | ||
|
|
637b3311d6 | ||
|
|
4b8c34bcbd | ||
|
|
d5a34eedb9 | ||
|
|
c997908a49 | ||
|
|
bd594fa214 | ||
|
|
6bae21a578 | ||
|
|
79db97efdf | ||
|
|
7755affa1e | ||
|
|
ea04069fe5 | ||
|
|
2c3a302c85 | ||
|
|
89d9bfe1f1 | ||
|
|
c9241e2162 | ||
|
|
99f7668901 | ||
|
|
187738ee11 | ||
|
|
68d3724e50 | ||
|
|
8526faa041 | ||
|
|
ac9fbb1f08 | ||
|
|
e82da3a6c1 | ||
|
|
40e36fd875 | ||
|
|
3c86984cf1 | ||
|
|
50d172bbd5 | ||
|
|
da676c6ec1 | ||
|
|
901615d99e | ||
|
|
6ed52f37cc | ||
|
|
15e5a31767 | ||
|
|
8cbab4d4ce | ||
|
|
a9c28e84d0 | ||
|
|
bafabb84b4 | ||
|
|
f5d592a291 | ||
|
|
385bee964d | ||
|
|
18f277182a | ||
|
|
344a9d8334 | ||
|
|
67a76f9d86 | ||
|
|
93d6d1ac6a | ||
|
|
4b3edf053f | ||
|
|
0720ac6a15 | ||
|
|
c0b9cd951e | ||
|
|
a54ebabf53 | ||
|
|
676acb9e5a | ||
|
|
387c468d84 | ||
|
|
64e7fe3422 | ||
|
|
2b1466c51b | ||
|
|
332bcde4b2 | ||
|
|
db783179ce | ||
|
|
c78d687387 | ||
|
|
b717059a39 | ||
|
|
e02bde5eed | ||
|
|
e1765cad41 | ||
|
|
6dab5f836e | ||
|
|
e47f81698c | ||
|
|
2b824839c3 | ||
|
|
26f009a295 | ||
|
|
9b4c997b57 | ||
|
|
39143295b1 | ||
|
|
6eaf2feb40 | ||
|
|
4fe0f1a7af | ||
|
|
7aac377ce8 | ||
|
|
77542c5f06 | ||
|
|
3d365339e4 | ||
|
|
66be400f5f | ||
|
|
9880792c4d | ||
|
|
ca03a473ec | ||
|
|
c364856942 | ||
|
|
4e7a96b67c | ||
|
|
39bfc818a6 | ||
|
|
6d9982f735 | ||
|
|
4f6ba7a388 | ||
|
|
0274fb214c | ||
|
|
15519f142d | ||
|
|
6b258886a4 | ||
|
|
d23a88c997 | ||
|
|
096e17ab5a | ||
|
|
da9e20e96f | ||
|
|
53d5f9f6e0 | ||
|
|
43b4fe75e2 | ||
|
|
925db96b08 | ||
|
|
efdb8e07b0 | ||
|
|
816860dc4f | ||
|
|
314f758d62 | ||
|
|
c799aaa7cb | ||
|
|
2f5bc8db54 | ||
|
|
7c61a27084 | ||
|
|
ad3e83da22 | ||
|
|
8888704b58 | ||
|
|
a3dc5e78fd | ||
|
|
32506860dd | ||
|
|
a89b20584f | ||
|
|
8e42c09721 | ||
|
|
bb5978dfea | ||
|
|
0dc491adfc | ||
|
|
f470a6185a | ||
|
|
e5febc1fef | ||
|
|
354d01e980 | ||
|
|
63e043593a | ||
|
|
770d0c141d | ||
|
|
0a885c8581 | ||
|
|
ec9c704e71 | ||
|
|
02c0176070 | ||
|
|
f847de852b | ||
|
|
86413b5767 | ||
|
|
747c976a14 | ||
|
|
326c28a11d | ||
|
|
263471bcbb | ||
|
|
9478454063 | ||
|
|
a9a9804517 | ||
|
|
0bde44ec2f | ||
|
|
13ad179a1b | ||
|
|
b72acd9e59 | ||
|
|
d0a1ef9571 | ||
|
|
d1f049871f | ||
|
|
070184b309 | ||
|
|
cbb41676e0 | ||
|
|
81130dd514 | ||
|
|
61d3edca17 | ||
|
|
248b56a706 | ||
|
|
90f8d1d6da | ||
|
|
8f08b71475 | ||
|
|
cc6527029c | ||
|
|
8a110567fc | ||
|
|
a451e562fb | ||
|
|
4e2f6b1d26 | ||
|
|
6b8db74a2b | ||
|
|
4c629772cc | ||
|
|
208593d203 | ||
|
|
99019be152 | ||
|
|
fa73e1707d | ||
|
|
d7de2e3d21 | ||
|
|
903ff4fd09 | ||
|
|
feaabacc94 | ||
|
|
a5827f66c9 | ||
|
|
bc0846c190 | ||
|
|
ecdcaadfa9 | ||
|
|
db2478f73d | ||
|
|
5d6a7e692f | ||
|
|
0fbeca1536 | ||
|
|
f5e1869a9b | ||
|
|
10c8414ab0 | ||
|
|
6832e68ada | ||
|
|
24477327aa | ||
|
|
52b11d9b38 | ||
|
|
c7b239f362 | ||
|
|
f1b67ad9d6 | ||
|
|
6279a85d2e | ||
|
|
7a4b5cca04 | ||
|
|
c4eff2478a | ||
|
|
15c918b5d4 | ||
|
|
0a5bfe2939 | ||
|
|
05b9bbdf59 | ||
|
|
b88c89d61b | ||
|
|
31d58f9075 | ||
|
|
743d0fa689 | ||
|
|
3b6d3963f1 | ||
|
|
3a4c2d8f13 | ||
|
|
7d86a40261 | ||
|
|
5527aa7990 | ||
|
|
f48484520a | ||
|
|
5957ddd01c | ||
|
|
994b1584b7 | ||
|
|
9647fbcc74 | ||
|
|
ca6f8d085a | ||
|
|
c2637a7576 | ||
|
|
4fd1cdd7e8 | ||
|
|
ad20ff514a | ||
|
|
5cd50f7138 | ||
|
|
89e6bada56 | ||
|
|
420d703f9d | ||
|
|
090da33f33 | ||
|
|
134e260d7b | ||
|
|
82c8ca55fc | ||
|
|
e92cd3be18 | ||
|
|
93c2d2d860 | ||
|
|
cd79a38755 | ||
|
|
84286b7942 | ||
|
|
a16ca8c897 | ||
|
|
fec1766e26 | ||
|
|
fea07429fe | ||
|
|
fa29f2281d | ||
|
|
002b88ab83 | ||
|
|
54833f9fc6 | ||
|
|
cba9286217 | ||
|
|
9ebbff49fb | ||
|
|
f26e3d6cd1 | ||
|
|
9ee39a83c1 | ||
|
|
c09a4c4374 | ||
|
|
3e44d0ad13 | ||
|
|
b23a09b1b8 | ||
|
|
421ed8f51d | ||
|
|
14a057cf55 | ||
|
|
2e6fcafc68 | ||
|
|
13b43e8902 | ||
|
|
837708fc0c | ||
|
|
2e305d5636 | ||
|
|
f9711de634 | ||
|
|
2c6779bb1c | ||
|
|
ee05392d27 | ||
|
|
1cc84da976 | ||
|
|
5cfdd504c1 | ||
|
|
24debfc75c | ||
|
|
ffe6272299 | ||
|
|
64c3d69641 | ||
|
|
67c19b79e3 | ||
|
|
ea29106101 | ||
|
|
8016f82040 | ||
|
|
7c93e5879c | ||
|
|
dc86f89c4f | ||
|
|
d03be052aa | ||
|
|
fef79f4fc3 | ||
|
|
cc58721ccd | ||
|
|
5f2115da0e | ||
|
|
1dd1e677e4 | ||
|
|
29b89bdc00 | ||
|
|
4493d86fd5 | ||
|
|
6a600df19a | ||
|
|
e8c6e36521 | ||
|
|
3e3610a204 | ||
|
|
3e626d91f0 | ||
|
|
6a0d8d13b0 | ||
|
|
4f762b376f | ||
|
|
bda80c9984 | ||
|
|
7de60e2345 | ||
|
|
77ad2c8958 | ||
|
|
6cca821ba6 | ||
|
|
96da053717 | ||
|
|
3723006f39 | ||
|
|
6cfdc47760 | ||
|
|
00a5600768 | ||
|
|
d8674d09a2 | ||
|
|
a46630f774 | ||
|
|
38fb9d467c | ||
|
|
fee56ca8e0 | ||
|
|
de60419926 | ||
|
|
42a1410cc2 | ||
|
|
ddd4f93f01 | ||
|
|
21de3b31b6 | ||
|
|
aea25119c0 | ||
|
|
aa1100642d | ||
|
|
4121cc0e14 | ||
|
|
3b7b56c789 | ||
|
|
ebc03aee33 | ||
|
|
03c6edf31a | ||
|
|
970d03a5e4 | ||
|
|
ed4c090f21 | ||
|
|
672d582caf | ||
|
|
b6c09683be | ||
|
|
a13fd48cda | ||
|
|
aec1147aa5 | ||
|
|
78da48b08d | ||
|
|
edce191c29 | ||
|
|
8c52a253dc | ||
|
|
589ec0251a | ||
|
|
ffddc275c1 | ||
|
|
4e26bb309c | ||
|
|
7696be5d95 | ||
|
|
ef80c23034 | ||
|
|
1f7be69624 | ||
|
|
6e069dc29d | ||
|
|
e40bbf56c7 | ||
|
|
65beb8d65e | ||
|
|
ebf4f614c4 | ||
|
|
26942e276a | ||
|
|
0bcce67e39 | ||
|
|
3318ba6277 | ||
|
|
c5eb7db432 | ||
|
|
f35345f385 | ||
|
|
a13ac2e0c5 | ||
|
|
27eb95ece8 | ||
|
|
7958bb4cda | ||
|
|
8fc01ebb12 | ||
|
|
d2c10bb9ac | ||
|
|
13eedc9f82 | ||
|
|
6f919bc214 | ||
|
|
496328d511 | ||
|
|
d6af56c51c | ||
|
|
079d59695f | ||
|
|
0867b142da | ||
|
|
ac7b2bce9f | ||
|
|
4664d88aad | ||
|
|
7d6a3da2e2 | ||
|
|
4180a7cfb4 | ||
|
|
a2e11d12c9 | ||
|
|
301c3fbdd2 | ||
|
|
35088188b3 | ||
|
|
95cda71eb5 | ||
|
|
a2e9fc67e4 | ||
|
|
50c541f3fa | ||
|
|
f98a3fe6c5 | ||
|
|
d2c86bf619 | ||
|
|
aeb4f85a48 | ||
|
|
b8f32cc2cb | ||
|
|
d562308b63 | ||
|
|
2d26e7cd07 | ||
|
|
8b011804ca | ||
|
|
93a90dbf13 | ||
|
|
2b6585545c | ||
|
|
047160e4ec | ||
|
|
6a6ee9fc12 | ||
|
|
96577794e9 | ||
|
|
5c3f7b1b82 | ||
|
|
bdf1bd1e8b | ||
|
|
df6d372243 | ||
|
|
b237456420 | ||
|
|
52a79b4f75 | ||
|
|
99382adbf6 | ||
|
|
3f0c950e72 | ||
|
|
2b71ff2dec | ||
|
|
9ccf9d0a83 | ||
|
|
1801691f49 | ||
|
|
2bad0bfcb0 | ||
|
|
ac30b3df9d | ||
|
|
f67ba7ecda | ||
|
|
812b8350d3 | ||
|
|
0c67a23b57 | ||
|
|
976f6be3f0 | ||
|
|
852d2a1d5f | ||
|
|
bf39d4124c | ||
|
|
04eebae3ec | ||
|
|
23a0a89ead | ||
|
|
a7a67621a1 | ||
|
|
2d2a81a3ca | ||
|
|
bebb06a36d | ||
|
|
c4e331ee7e | ||
|
|
13192dc102 | ||
|
|
ab773f5f7b | ||
|
|
164a0fe8d5 | ||
|
|
3032363aea | ||
|
|
3327712253 | ||
|
|
2abd0a871b | ||
|
|
eff7fde8cf | ||
|
|
8b8a60c8ad | ||
|
|
5350f7d933 | ||
|
|
30c9b35354 | ||
|
|
4c46388686 | ||
|
|
3bd9fe4c99 | ||
|
|
837554eb17 | ||
|
|
d8fe54b7b2 | ||
|
|
c49be3391e | ||
|
|
c56adcba76 | ||
|
|
74e20bf7be | ||
|
|
7896fe3ed9 | ||
|
|
0e0cdfe25b | ||
|
|
2c2a10730e | ||
|
|
51f8786905 | ||
|
|
78a5390917 | ||
|
|
d998381bbe | ||
|
|
6326a93d98 | ||
|
|
e4e619ac1e | ||
|
|
439afcb667 | ||
|
|
f4108f241e | ||
|
|
2e3c9114c6 | ||
|
|
746b04646c | ||
|
|
a7577bf83b | ||
|
|
b283e8e66a | ||
|
|
a129e887dd | ||
|
|
9ad915c14a | ||
|
|
9fd5fea50c | ||
|
|
41fa0f2c77 | ||
|
|
ed65d0c922 | ||
|
|
df53369ec1 | ||
|
|
cc385462de | ||
|
|
269ba246ec | ||
|
|
a67b1b9c12 | ||
|
|
8c13d4d7ad | ||
|
|
8562d328ff | ||
|
|
1d8781da90 | ||
|
|
dd64d19037 | ||
|
|
93918bc26c | ||
|
|
bfb82e8a21 | ||
|
|
8609026531 | ||
|
|
0cd598ae14 | ||
|
|
95497350e4 | ||
|
|
9bb1344538 | ||
|
|
ab695d29fe | ||
|
|
fa4b3338af | ||
|
|
c7ed9ea717 | ||
|
|
7ae939623c | ||
|
|
de86a77dfc | ||
|
|
b44a7d3236 | ||
|
|
a3c7e2f807 | ||
|
|
447f8d39dc | ||
|
|
7b961af45f | ||
|
|
bc0ee8138e | ||
|
|
91b2911bb0 | ||
|
|
9848dc54ba | ||
|
|
9a3bd4db4b | ||
|
|
891bde6990 | ||
|
|
d9228b8c4b | ||
|
|
edd58455f6 | ||
|
|
e082167c39 | ||
|
|
5a3daf8ffd | ||
|
|
3719614dee | ||
|
|
e098dc21ce | ||
|
|
f4580f9040 | ||
|
|
6e05186f1a | ||
|
|
9701c55195 | ||
|
|
0d2eebe324 | ||
|
|
e21fe49359 | ||
|
|
bc94082eb6 | ||
|
|
5d769d2297 | ||
|
|
48d0434178 | ||
|
|
f74b84bd12 | ||
|
|
910868cfdb | ||
|
|
5469ec6683 | ||
|
|
764621f762 | ||
|
|
248687684a | ||
|
|
d7b1f89152 | ||
|
|
9a844dae39 | ||
|
|
5193271796 | ||
|
|
ed5bef27e0 | ||
|
|
be25e90009 | ||
|
|
ab98bf5d6c | ||
|
|
ffb1c77697 | ||
|
|
6bddba6762 | ||
|
|
57b0af54df | ||
|
|
0ac50017c4 | ||
|
|
9135ca1e43 | ||
|
|
1dc6256aab | ||
|
|
227be5e3be | ||
|
|
e18e3cf7e4 | ||
|
|
ca09e98d8f | ||
|
|
db174c9655 | ||
|
|
22896470e3 | ||
|
|
56fde5a448 | ||
|
|
6ade9925d7 | ||
|
|
e755ab878d | ||
|
|
fa0d8e86fc | ||
|
|
5458b8297c | ||
|
|
f3c9dde801 | ||
|
|
45aa8bdfae | ||
|
|
09d161f3a6 | ||
|
|
90a65d0a57 | ||
|
|
0bf1328c17 | ||
|
|
3754468cf4 | ||
|
|
6329cb2cec | ||
|
|
f218f48f0f | ||
|
|
a179f8d124 | ||
|
|
f681948a35 | ||
|
|
475f3e75dd | ||
|
|
f337925af9 | ||
|
|
918690e773 | ||
|
|
7623d3e93a | ||
|
|
f2a8a6a03a | ||
|
|
49cc4180a7 | ||
|
|
8ebfc772f3 | ||
|
|
38dccbae09 | ||
|
|
0f68f5a356 | ||
|
|
7999553973 | ||
|
|
3c75ac6156 | ||
|
|
9fcc7b84c3 | ||
|
|
8ac4d93581 | ||
|
|
10a349ade8 | ||
|
|
87a9194189 | ||
|
|
ac371946d8 | ||
|
|
158d47edde | ||
|
|
5ad4e6b1f1 | ||
|
|
ce7c3dfb8a | ||
|
|
208e9261ce | ||
|
|
af8d327ae2 | ||
|
|
0d931ed5b5 | ||
|
|
d8039b0cf2 | ||
|
|
2e27c5957d | ||
|
|
1bfcb5a839 | ||
|
|
6d98119df2 | ||
|
|
a90028d93f | ||
|
|
630ff9d86a | ||
|
|
600b92cbce | ||
|
|
7a609f5a78 | ||
|
|
4c3b9f8fb6 | ||
|
|
b120c47faa | ||
|
|
49a3057765 | ||
|
|
fc5d06be0e | ||
|
|
bb25d30663 | ||
|
|
d20bb15204 | ||
|
|
4261b515e6 | ||
|
|
283db495ad | ||
|
|
535bb34ed4 | ||
|
|
1d4ebf48f2 | ||
|
|
f637018b5d | ||
|
|
4dfbf8ecaf | ||
|
|
c3b17a6816 | ||
|
|
f940a9181a | ||
|
|
95a0151234 | ||
|
|
3bae1565b2 | ||
|
|
66cf29e58f | ||
|
|
9f4920fd2a | ||
|
|
2724eb6518 | ||
|
|
c8742289de | ||
|
|
0e40da73f6 | ||
|
|
93b1576de1 | ||
|
|
cfdbe06d07 | ||
|
|
46a45a8536 | ||
|
|
f86ec64714 | ||
|
|
08c7c8b10f | ||
|
|
8cd896fbfa | ||
|
|
abe7d858a8 | ||
|
|
5e076fc948 | ||
|
|
0f566fe912 | ||
|
|
be8abe9dcb | ||
|
|
8efdf61143 | ||
|
|
c3f885012a | ||
|
|
969eac4c5a | ||
|
|
003b9dcd7f | ||
|
|
0bc20112b2 | ||
|
|
9e137fa6a8 | ||
|
|
1f8701bea6 | ||
|
|
313ffab7c1 | ||
|
|
9f602e416f | ||
|
|
de90e2d801 | ||
|
|
3c12f1133e | ||
|
|
0625c57824 | ||
|
|
a234fdbab7 | ||
|
|
5830ee471b | ||
|
|
c35d97bf69 | ||
|
|
6c3a9370dc | ||
|
|
edb74237bd | ||
|
|
cd8496b62b | ||
|
|
af1821e697 | ||
|
|
c0a14a5618 | ||
|
|
8709545f14 | ||
|
|
e0d8846c44 | ||
|
|
cae1bc9f0a | ||
|
|
eae5fc9be9 | ||
|
|
c2a5aad1e8 | ||
|
|
2a3aeec459 | ||
|
|
4a89d73f73 | ||
|
|
746e8f8a6a | ||
|
|
4f52cc075a | ||
|
|
8cf8c5cabb | ||
|
|
2f44c35ede | ||
|
|
62dee13881 | ||
|
|
3975c74b2b | ||
|
|
0f1c585567 | ||
|
|
1fb3ec4057 | ||
|
|
85cad49b03 | ||
|
|
76203928d2 | ||
|
|
cfbc089207 | ||
|
|
ba693365ec | ||
|
|
2c7fdd3993 | ||
|
|
0828a9fe68 | ||
|
|
b4be9bb741 | ||
|
|
e5fe6b1fd9 | ||
|
|
8d10800cb6 | ||
|
|
67099bba40 | ||
|
|
d94b274439 | ||
|
|
99c2ff15a0 | ||
|
|
0b89a895e7 | ||
|
|
64740ba528 | ||
|
|
f35634a295 | ||
|
|
9807e24e0a | ||
|
|
de43bd46a5 | ||
|
|
b9f64092b8 | ||
|
|
3f1bc02885 | ||
|
|
407d3565cd | ||
|
|
2fcccfb48f | ||
|
|
3262751fea | ||
|
|
80428fc412 | ||
|
|
66626b3427 | ||
|
|
fc3a599f90 | ||
|
|
e355621bbf | ||
|
|
9571cb0cc4 | ||
|
|
66a5f51927 | ||
|
|
98e2d57691 | ||
|
|
a25c7a5ccd | ||
|
|
f93d46d2b1 | ||
|
|
4bb5e96c42 | ||
|
|
4b5b6e3b02 | ||
|
|
a383995522 | ||
|
|
eb9d76655e | ||
|
|
fc20d73520 | ||
|
|
fd64215298 | ||
|
|
9d05ee56a3 | ||
|
|
7ff478b542 | ||
|
|
aee0337d01 | ||
|
|
ddcf34c006 | ||
|
|
44dbb8f285 | ||
|
|
58edf329f5 | ||
|
|
0611c67651 | ||
|
|
3da2d094a0 | ||
|
|
993bcc2719 | ||
|
|
947b2f5072 | ||
|
|
442c5e2d8a | ||
|
|
5f5ff0023b | ||
|
|
1e1a3d891a | ||
|
|
2e27e9f8f6 | ||
|
|
9ff5818a0d | ||
|
|
59d267dff8 | ||
|
|
8153bf7952 | ||
|
|
7200889d8c | ||
|
|
253db8304a | ||
|
|
a17a9ac0a0 | ||
|
|
4f6555b522 | ||
|
|
4167026ca7 | ||
|
|
ef7b8f1ba4 | ||
|
|
ec57b209b6 | ||
|
|
1e64ca04d8 | ||
|
|
1f82dbe8ee | ||
|
|
4f154922c0 | ||
|
|
2b1fe5d3fe | ||
|
|
03402e4342 | ||
|
|
1d85eede43 | ||
|
|
61f4456842 | ||
|
|
c925e04f3c | ||
|
|
00412a70e9 | ||
|
|
0923c50218 | ||
|
|
fe536bc9df | ||
|
|
e8937a285c | ||
|
|
96e704234c | ||
|
|
407528bd94 | ||
|
|
5b107d4e62 | ||
|
|
f71dc004c0 | ||
|
|
ff1f70afa8 | ||
|
|
45969127f5 | ||
|
|
f13c16bb4f | ||
|
|
438c7eabdc | ||
|
|
e12579a2a2 | ||
|
|
a6ede58d8e | ||
|
|
3a82e2a24b | ||
|
|
28d45b89e0 | ||
|
|
01087ca302 | ||
|
|
297365407b | ||
|
|
8e0629062d | ||
|
|
da5ce35c3c | ||
|
|
96d9d323d1 | ||
|
|
dd0112f749 | ||
|
|
d35c5c2719 | ||
|
|
f84e2c316f | ||
|
|
65335162fc | ||
|
|
94157ec4d4 | ||
|
|
87bb682836 | ||
|
|
9f12e2748d | ||
|
|
714a425ae5 | ||
|
|
183a8035dd |
@@ -27,7 +27,7 @@ jobs:
|
||||
# fallback to using the latest cache if no exact match is found
|
||||
- v1-dependencies-
|
||||
|
||||
- node/install-npm
|
||||
- run: sudo npm install -g npm@8.10.0
|
||||
- node/install-packages:
|
||||
app-dir: ~/homebrewery
|
||||
cache-path: node_modules
|
||||
@@ -55,13 +55,19 @@ jobs:
|
||||
at: .
|
||||
|
||||
# run tests!
|
||||
- run:
|
||||
- run:
|
||||
name: Test - API Unit Tests
|
||||
command: npm run test:api-unit
|
||||
- run:
|
||||
name: Test - Basic
|
||||
command: npm run test:basic
|
||||
- run:
|
||||
- run:
|
||||
name: Test - Coverage
|
||||
command: npm run test:coverage
|
||||
- run:
|
||||
name: Test - Mustache Spans
|
||||
command: npm run test:mustache-span
|
||||
- run:
|
||||
- run:
|
||||
name: Test - Routes
|
||||
command: npm run test:route
|
||||
|
||||
@@ -71,4 +77,4 @@ workflows:
|
||||
- build
|
||||
- test:
|
||||
requires:
|
||||
- build
|
||||
- build
|
||||
|
||||
7
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
contact_links:
|
||||
- name: /r/Homebrewery Subreddit
|
||||
url: https://www.reddit.com/r/homebrewery
|
||||
about: The Homebrewery community on Reddit!
|
||||
- name: Discord of Many Things
|
||||
url: https://discord.gg/domt
|
||||
about: "Join the conversation in the #formatting channel on DoMT!"
|
||||
17
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: Feature Request
|
||||
description: Have an idea to improve the Homebrewery? Let us know!
|
||||
labels: ["feature request"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "We'd love to hear your idea! Please be sure to [search the current Issues](https://github.com/naturalcrit/homebrewery/issues) for any duplicate requests."
|
||||
- type: textarea
|
||||
id: user-request
|
||||
attributes:
|
||||
label: "Your idea:"
|
||||
description: The best feature requests provide an explanation of the current issue and then an explanation of how it could be improved. Screenshots/images can be pasted right in as well!
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "Please be sure to search for any close matches to your request in the GitHub Issues tracker before opening a new request, thanks!"
|
||||
55
.github/ISSUE_TEMPLATE/general_issue.yml
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
name: General Issue
|
||||
description: Report an issue unrelated to Saving
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Please include as much information as possible.
|
||||
- type: dropdown
|
||||
id: renderer
|
||||
attributes:
|
||||
label: Renderer
|
||||
description: Which renderer does this issue occur on? If you are unsure, you can check the renderer in the Properties Editor (click the "i" in the Snippet Menu bar above the editor).
|
||||
options:
|
||||
- v3
|
||||
- Legacy
|
||||
- Both
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: browser
|
||||
attributes:
|
||||
label: Browser
|
||||
description: Which browser were you using when the issue occurred?
|
||||
options:
|
||||
- Chrome
|
||||
- Firefox
|
||||
- Edge
|
||||
- Safari
|
||||
- other
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: operating-system
|
||||
attributes:
|
||||
label: Operating System
|
||||
description: Which OS were you using when the issue occurred?
|
||||
options:
|
||||
- Windows
|
||||
- MacOS
|
||||
- Linux
|
||||
- other
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: user-description
|
||||
attributes:
|
||||
label: "What happened?"
|
||||
description: Please include any steps you took leading up to the issue and if you can reproduce it. Let us know what you expected to happen, and what did happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: code
|
||||
attributes:
|
||||
label: Code
|
||||
description: Paste in any relevant code snippet below.
|
||||
render: gfm
|
||||
26
.github/ISSUE_TEMPLATE/save_issue.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Saving Issue
|
||||
description: Report an issue Saving
|
||||
labels: ["Saving"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Woops, sorry there was an issue Saving. Please add any detail you can to this report and check back soon!
|
||||
- type: textarea
|
||||
id: error-code
|
||||
attributes:
|
||||
label: Error Code
|
||||
render: shell
|
||||
- type: textarea
|
||||
id: user-description
|
||||
attributes:
|
||||
label: "Your description of what happened:"
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for the report. Here are some steps that may help in the meantime:
|
||||
1. Refreshing your Google credentials in Homebrewery by signing out, and back in (they expire after one year).
|
||||
2. Waiting a few minutes and trying again - sometimes there is just a momentary blip in the server.
|
||||
3. Check the Issues in Github or the /r/homebrewery subreddit to see if others are experiencing the same issue.
|
||||
1
.gitignore
vendored
@@ -11,3 +11,4 @@ config/docker.*
|
||||
todo.md
|
||||
startDB.bat
|
||||
startMViewer.bat
|
||||
.vscode
|
||||
|
||||
259
changelog.md
@@ -7,6 +7,11 @@ h5 {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.page .taskList {
|
||||
display:block;
|
||||
break-inside:auto;
|
||||
}
|
||||
|
||||
.taskList li input {
|
||||
list-style-type : none;
|
||||
margin-left : -0.52cm;
|
||||
@@ -14,6 +19,11 @@ h5 {
|
||||
filter: brightness(1.1) drop-shadow(1px 2px 1px #222);
|
||||
}
|
||||
|
||||
.taskList ul {
|
||||
margin-bottom: 0px;
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.taskList li input[checked] {
|
||||
filter: sepia(100%) hue-rotate(60deg) saturate(3.5) contrast(4) brightness(1.1) drop-shadow(1px 2px 1px #222);
|
||||
}
|
||||
@@ -30,16 +40,263 @@ pre {
|
||||
margin-top : 0.1cm;
|
||||
}
|
||||
|
||||
.page ul + h5 {
|
||||
margin-top: 0.25cm;
|
||||
}
|
||||
|
||||
.page p + h5 {
|
||||
margin-top: 0.25cm;
|
||||
}
|
||||
|
||||
.page .openSans {
|
||||
font-family: 'Open Sans';
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.page {
|
||||
padding-bottom: 1.5cm;
|
||||
}
|
||||
```
|
||||
|
||||
## changelog
|
||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||
|
||||
### Thursday 27/08/2022 - v3.2.0
|
||||
### Friday 23/01/2023 - v3.6.0
|
||||
{{taskList
|
||||
##### calculuschild
|
||||
|
||||
* [x] Fix Google Drive brews sometimes duplicating
|
||||
|
||||
Fixes issues [#2603](https://github.com/naturalcrit/homebrewery/issues/2603)
|
||||
|
||||
##### Jeddai
|
||||
|
||||
* [x] Add unit tests with full coverage for the Homebrewery API
|
||||
|
||||
* [x] Add message to refresh the browser if the user is missing an update to the Homebrewery
|
||||
|
||||
Fixes issues [#2583](https://github.com/naturalcrit/homebrewery/issues/2583)
|
||||
|
||||
##### G-Ambatte
|
||||
|
||||
* [x] Auto-compile Themes CSS on development server
|
||||
|
||||
##### 5e-Cleric
|
||||
|
||||
* [x] Fix cloned brews inheriting the parent view count
|
||||
}}
|
||||
|
||||
### Friday 23/12/2022 - v3.5.0
|
||||
{{taskList
|
||||
|
||||
##### Jeddai
|
||||
|
||||
* [x] Only brew owners or invited authors can edit a brew
|
||||
|
||||
- Visiting an `/edit` page of a brew that does not list you as an author will result in an error page. Authors can be added to any brew by opening its {{fa,fa-info-circle}} **Properties** menu and typing the author's username (case-sensitive) into the **Invited Authors** bubble.
|
||||
- Warn user if a newer brew version has been saved on another device
|
||||
|
||||
Fixes issues [#1987](https://github.com/naturalcrit/homebrewery/issues/1987)
|
||||
}}
|
||||
|
||||
\page
|
||||
|
||||
### Saturday 10/12/2022 - v3.4.2
|
||||
{{taskList
|
||||
|
||||
##### Jeddai
|
||||
|
||||
* [x] Fix broken tags editor
|
||||
|
||||
* [x] Reduce server load to fix some saving issues
|
||||
|
||||
Fixes issues [#2322](https://github.com/naturalcrit/homebrewery/issues/2322)
|
||||
|
||||
##### G-Ambatte
|
||||
|
||||
* [x] Account page help link for Google Drive errors
|
||||
|
||||
Fixes issues [#2520](https://github.com/naturalcrit/homebrewery/issues/2520)
|
||||
}}
|
||||
|
||||
### Monday 05/12/2022 - v3.4.1
|
||||
{{taskList
|
||||
|
||||
##### G-Ambatte
|
||||
|
||||
* [x] Fix Account page incorrect last login time
|
||||
|
||||
Fixes issues [#2521](https://github.com/naturalcrit/homebrewery/issues/2521)
|
||||
|
||||
##### Gazook
|
||||
|
||||
* [x] Fix crashing on iOS and Safari browsers
|
||||
|
||||
Fixes issues [#2531](https://github.com/naturalcrit/homebrewery/issues/2531)
|
||||
}}
|
||||
|
||||
### Monday 28/11/2022 - v3.4.0
|
||||
{{taskList
|
||||
|
||||
##### G-Ambatte
|
||||
|
||||
* [x] Fix for Chrome v108 handling of page size
|
||||
|
||||
Fixes issues [#2445](https://github.com/naturalcrit/homebrewery/issues/2445), [#2516](https://github.com/naturalcrit/homebrewery/issues/2516)
|
||||
|
||||
* [x] New account page with some user info, at {{openSans **USERNAME {{fa,fa-user}} → ACCOUNT {{fa,fa-user}}**}}
|
||||
|
||||
Fixes issues [#2049](https://github.com/naturalcrit/homebrewery/issues/2049), [#2043](https://github.com/naturalcrit/homebrewery/issues/2043)
|
||||
|
||||
* [x] Fix "Published/Private Brews" buttons on userpage
|
||||
|
||||
Fixes issues [#2449](https://github.com/naturalcrit/homebrewery/issues/2449)
|
||||
|
||||
##### Gazook
|
||||
|
||||
* [x] Make autosave default on for new users
|
||||
|
||||
* [x] Added link to our FAQ at {{openSans **NEED HELP? {{fa,fa-question-circle}} → FAQ {{fa,fa-question-circle}}**}}
|
||||
|
||||
* [x] Fix curly blocks freezing with long property lists
|
||||
|
||||
Fixes issues [#2393](https://github.com/naturalcrit/homebrewery/issues/2393)
|
||||
|
||||
* [x] Items can now be removed from {{openSans **RECENT BREWS** {{fas,fa-history}} }}
|
||||
|
||||
Fixes issues [#1918](https://github.com/naturalcrit/homebrewery/issues/1918)
|
||||
|
||||
* [x] Curly injector syntax `{blue}` highlighting in editor
|
||||
|
||||
Fixes issues [#1670](https://github.com/naturalcrit/homebrewery/issues/1670)
|
||||
|
||||
}}
|
||||
|
||||
### Thursday 28/10/2022 - v3.3.1
|
||||
{{taskList
|
||||
|
||||
##### Calculuschild
|
||||
|
||||
* [x] Fixes to several broken CSS styles from v3.3.0
|
||||
|
||||
Fixes issues [#2468](https://github.com/naturalcrit/homebrewery/issues/2468)
|
||||
|
||||
##### Jeddai
|
||||
|
||||
* [x] Reduce size of thumbnails on social media links
|
||||
|
||||
}}
|
||||
|
||||
### Friday 19/10/2022 - v3.3.0
|
||||
{{taskList
|
||||
|
||||
##### Calculuschild
|
||||
|
||||
* [x] Fix for tables broken by Chrome v106
|
||||
|
||||
|
||||
##### G-Ambatte:
|
||||
|
||||
* [x] Fix Table of Contents broken by Chrome v106
|
||||
|
||||
Fixes issues [#2437](https://github.com/naturalcrit/homebrewery/issues/2437)
|
||||
|
||||
* [x] Show brew thumbnails on user page
|
||||
|
||||
Fixes issues [#2331](https://github.com/naturalcrit/homebrewery/issues/2331)
|
||||
|
||||
* [x] Allow longer URLs for brew thumbnails
|
||||
|
||||
Fixes issues [#2351](https://github.com/naturalcrit/homebrewery/issues/2351)
|
||||
|
||||
* [x] Code no longer unfolds when inserting a snippet
|
||||
|
||||
Fixes issues [#2135](https://github.com/naturalcrit/homebrewery/issues/2135)
|
||||
|
||||
* [x] Fix brew settings being lost on first save
|
||||
|
||||
Fixes issues [#2427](https://github.com/naturalcrit/homebrewery/issues/2427)
|
||||
|
||||
##### Gazook:
|
||||
|
||||
* [x] Several updates to bug reporting and error popups
|
||||
|
||||
Fixes issues [#2376](https://github.com/naturalcrit/homebrewery/issues/2376)
|
||||
|
||||
* [x] Fixes to userpage search bar
|
||||
|
||||
Fixes issues [#1675](https://github.com/naturalcrit/homebrewery/issues/1675), [#2353](https://github.com/naturalcrit/homebrewery/issues/2353)
|
||||
|
||||
* [x] Renderer *(legacy / V3)* now shown next to page #
|
||||
|
||||
Fixes issues [#1928](https://github.com/naturalcrit/homebrewery/issues/1928)
|
||||
|
||||
* [x] Prevent text selection when moving divider bar
|
||||
|
||||
Fixes issues [#1632](https://github.com/naturalcrit/homebrewery/issues/1632)
|
||||
|
||||
* [x] Tweak Monster Stat Block coloring
|
||||
|
||||
Fixes issues [#2123](https://github.com/naturalcrit/homebrewery/issues/2123)
|
||||
|
||||
* [x] Added dropdown button to toggle autosave
|
||||
|
||||
Fixes issues [#1546](https://github.com/naturalcrit/homebrewery/issues/1546)
|
||||
|
||||
}}
|
||||
|
||||
|
||||
### Friday 08/09/2022 - v3.2.2
|
||||
{{taskList
|
||||
|
||||
##### Jeddai:
|
||||
|
||||
* [x] Fix brews not deleting from User page when removed from Google Drive externally.
|
||||
|
||||
Fixes issues: [#2325](https://github.com/naturalcrit/homebrewery/issues/2325)
|
||||
|
||||
##### G-Ambatte:
|
||||
|
||||
* [x] Brew Tags are now searchable on the User page
|
||||
|
||||
Fixes issues [#2317](https://github.com/naturalcrit/homebrewery/issues/2317), [#2319](https://github.com/naturalcrit/homebrewery/issues/2319), [#2334](https://github.com/naturalcrit/homebrewery/issues/2334)
|
||||
|
||||
* [x] Several tweaks to the User page
|
||||
|
||||
Fixes issues: [#1797](https://github.com/naturalcrit/homebrewery/issues/1797), [#2315](https://github.com/naturalcrit/homebrewery/issues/2315), [#2326](https://github.com/naturalcrit/homebrewery/issues/2326), [#2328](https://github.com/naturalcrit/homebrewery/issues/2328)
|
||||
}}
|
||||
|
||||
|
||||
|
||||
\page
|
||||
|
||||
### Wednesday 31/08/2022 - v3.2.1
|
||||
{{taskList
|
||||
|
||||
##### Calculuschild
|
||||
|
||||
* [x] Reference Links should now work inside tables
|
||||
|
||||
Fixes issues: [#2307](https://github.com/naturalcrit/homebrewery/issues/2307)
|
||||
|
||||
##### Jeddai:
|
||||
|
||||
* [x] Fix printing from `/new` not working
|
||||
|
||||
Fixes issues: [#1789](https://github.com/naturalcrit/homebrewery/issues/1789), [#1806](https://github.com/naturalcrit/homebrewery/issues/1806)
|
||||
|
||||
* [x] Fix broken snippet buttons on `/new`
|
||||
|
||||
Fixes issues: [#2311](https://github.com/naturalcrit/homebrewery/issues/2311)
|
||||
|
||||
##### G-Ambatte:
|
||||
|
||||
* [x] Several small tweaks to the User page
|
||||
|
||||
Fixes issues: [#2301](https://github.com/naturalcrit/homebrewery/issues/2301), [#2303](https://github.com/naturalcrit/homebrewery/issues/2303), [#2121](https://github.com/naturalcrit/homebrewery/issues/2121)
|
||||
}}
|
||||
|
||||
### Saturday 27/08/2022 - v3.2.0
|
||||
{{taskList
|
||||
|
||||
##### Calculuschild
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||
require('./brewRenderer.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
@@ -13,6 +14,8 @@ const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx');
|
||||
const NotificationPopup = require('./notificationPopup/notificationPopup.jsx');
|
||||
const Frame = require('react-frame-component').default;
|
||||
|
||||
const Themes = require('themes/themes.json');
|
||||
|
||||
const PAGE_HEIGHT = 1056;
|
||||
const PPR_THRESHOLD = 50;
|
||||
|
||||
@@ -23,6 +26,7 @@ const BrewRenderer = createClass({
|
||||
text : '',
|
||||
style : '',
|
||||
renderer : 'legacy',
|
||||
theme : '5ePHB',
|
||||
errors : []
|
||||
};
|
||||
},
|
||||
@@ -105,7 +109,12 @@ const BrewRenderer = createClass({
|
||||
|
||||
renderPageInfo : function(){
|
||||
return <div className='pageInfo' ref='main'>
|
||||
{this.state.viewablePageNumber + 1} / {this.state.pages.length}
|
||||
<div>
|
||||
{this.props.renderer}
|
||||
</div>
|
||||
<div>
|
||||
{this.state.viewablePageNumber + 1} / {this.state.pages.length}
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
||||
@@ -113,7 +122,7 @@ const BrewRenderer = createClass({
|
||||
if(!this.state.usePPR) return;
|
||||
|
||||
return <div className='ppr_msg'>
|
||||
Partial Page Renderer enabled, because your brew is so large. May effect rendering.
|
||||
Partial Page Renderer is enabled, because your brew is so large. May affect rendering.
|
||||
</div>;
|
||||
},
|
||||
|
||||
@@ -177,6 +186,9 @@ const BrewRenderer = createClass({
|
||||
render : function(){
|
||||
//render in iFrame so broken code doesn't crash the site.
|
||||
//Also render dummy page while iframe is mounting.
|
||||
const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy';
|
||||
const themePath = this.props.theme ?? '5ePHB';
|
||||
const baseThemePath = Themes[rendererPath][themePath].baseTheme;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
@@ -200,7 +212,11 @@ const BrewRenderer = createClass({
|
||||
<RenderWarnings />
|
||||
<NotificationPopup />
|
||||
</div>
|
||||
<link href={`${this.props.renderer == 'legacy' ? '/themes/5ePhbLegacy.style.css' : '/themes/5ePhb.style.css'}`} rel='stylesheet'/>
|
||||
<link href={`/themes/${rendererPath}/Blank/style.css`} rel='stylesheet'/>
|
||||
{baseThemePath &&
|
||||
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} rel='stylesheet'/>
|
||||
}
|
||||
<link href={`/themes/${rendererPath}/${themePath}/style.css`} rel='stylesheet'/>
|
||||
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
||||
{this.state.isMounted
|
||||
&&
|
||||
|
||||
@@ -21,11 +21,17 @@
|
||||
right : 17px;
|
||||
bottom : 0;
|
||||
z-index : 1000;
|
||||
padding : 8px 10px;
|
||||
background-color : #333;
|
||||
font-size : 10px;
|
||||
font-weight : 800;
|
||||
color : white;
|
||||
div {
|
||||
display: inline-block;
|
||||
padding : 8px 10px;
|
||||
&:not(:last-child){
|
||||
border-right: 1px solid #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ppr_msg{
|
||||
position : absolute;
|
||||
|
||||
@@ -19,11 +19,6 @@ const DEFAULT_STYLE_TEXT = dedent`
|
||||
color: black;
|
||||
}`;
|
||||
|
||||
const splice = function(str, index, inject){
|
||||
return str.slice(0, index) + inject + str.slice(index);
|
||||
};
|
||||
|
||||
|
||||
|
||||
const Editor = createClass({
|
||||
displayName : 'Editor',
|
||||
@@ -37,6 +32,7 @@ const Editor = createClass({
|
||||
onTextChange : ()=>{},
|
||||
onStyleChange : ()=>{},
|
||||
onMetaChange : ()=>{},
|
||||
reportError : ()=>{},
|
||||
|
||||
renderer : 'legacy'
|
||||
};
|
||||
@@ -80,19 +76,7 @@ const Editor = createClass({
|
||||
},
|
||||
|
||||
handleInject : function(injectText){
|
||||
let text;
|
||||
if(this.isText()) text = this.props.brew.text;
|
||||
if(this.isStyle()) text = this.props.brew.style ?? DEFAULT_STYLE_TEXT;
|
||||
|
||||
const lines = text.split('\n');
|
||||
const cursorPos = this.refs.codeEditor.getCursorPosition();
|
||||
lines[cursorPos.line] = splice(lines[cursorPos.line], cursorPos.ch, injectText);
|
||||
|
||||
const injectLines = injectText.split('\n');
|
||||
this.refs.codeEditor.setCursorPosition(cursorPos.line + injectLines.length, cursorPos.ch + injectLines[injectLines.length - 1].length);
|
||||
|
||||
if(this.isText()) this.props.onTextChange(lines.join('\n'));
|
||||
if(this.isStyle()) this.props.onStyleChange(lines.join('\n'));
|
||||
this.refs.codeEditor?.injectText(injectText, false);
|
||||
},
|
||||
|
||||
handleViewChange : function(newView){
|
||||
@@ -154,9 +138,17 @@ const Editor = createClass({
|
||||
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
|
||||
}
|
||||
|
||||
// Highlight injectors {style}
|
||||
if(line.includes('{') && line.includes('}')){
|
||||
const regex = /(?:^|[^{\n])({(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\2})/gm;
|
||||
let match;
|
||||
while ((match = regex.exec(line)) != null) {
|
||||
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length }, { className: 'injection' });
|
||||
}
|
||||
}
|
||||
// Highlight inline spans {{content}}
|
||||
if(line.includes('{{') && line.includes('}}')){
|
||||
const regex = /{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])*\s*|}}/g;
|
||||
const regex = /{{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1 *|}}/g;
|
||||
let match;
|
||||
let blockCount = 0;
|
||||
while ((match = regex.exec(line)) != null) {
|
||||
@@ -175,7 +167,7 @@ const Editor = createClass({
|
||||
// Highlight block divs {{\n Content \n}}
|
||||
let endCh = line.length+1;
|
||||
|
||||
const match = line.match(/^ *{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])* *$|^ *}}$/);
|
||||
const match = line.match(/^ *{{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1 *$|^ *}}$/);
|
||||
if(match)
|
||||
endCh = match.index+match[0].length;
|
||||
codeMirror.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' });
|
||||
@@ -300,7 +292,8 @@ const Editor = createClass({
|
||||
rerenderParent={this.rerenderParent} />
|
||||
<MetadataEditor
|
||||
metadata={this.props.brew}
|
||||
onChange={this.props.onMetaChange} />
|
||||
onChange={this.props.onMetaChange}
|
||||
reportError={this.props.reportError}/>
|
||||
</>;
|
||||
}
|
||||
},
|
||||
@@ -327,6 +320,7 @@ const Editor = createClass({
|
||||
onInject={this.handleInject}
|
||||
showEditButtons={this.props.showEditButtons}
|
||||
renderer={this.props.renderer}
|
||||
theme={this.props.brew.theme}
|
||||
undo={this.undo}
|
||||
redo={this.redo}
|
||||
historySize={this.historySize()} />
|
||||
|
||||
@@ -29,6 +29,10 @@
|
||||
font-weight : bold;
|
||||
//font-style: italic;
|
||||
}
|
||||
.injection{
|
||||
color : green;
|
||||
font-weight : bold;
|
||||
}
|
||||
}
|
||||
|
||||
.brewJump{
|
||||
|
||||
@@ -4,13 +4,23 @@ const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
const request = require('superagent');
|
||||
const request = require('../../utils/request-middleware.js');
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
|
||||
|
||||
const Themes = require('themes/themes.json');
|
||||
const validations = require('./validations.js');
|
||||
|
||||
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
|
||||
|
||||
const homebreweryThumbnail = require('../../thumbnail.png');
|
||||
|
||||
const callIfExists = (val, fn, ...args)=>{
|
||||
if(val[fn]) {
|
||||
val[fn](...args);
|
||||
}
|
||||
};
|
||||
|
||||
const MetadataEditor = createClass({
|
||||
displayName : 'MetadataEditor',
|
||||
getDefaultProps : function() {
|
||||
@@ -19,13 +29,16 @@ const MetadataEditor = createClass({
|
||||
editId : null,
|
||||
title : '',
|
||||
description : '',
|
||||
thumbnail : '',
|
||||
tags : [],
|
||||
published : false,
|
||||
authors : [],
|
||||
systems : [],
|
||||
renderer : 'legacy'
|
||||
renderer : 'legacy',
|
||||
theme : '5ePHB'
|
||||
},
|
||||
onChange : ()=>{}
|
||||
onChange : ()=>{},
|
||||
reportError : ()=>{}
|
||||
};
|
||||
},
|
||||
|
||||
@@ -47,11 +60,27 @@ const MetadataEditor = createClass({
|
||||
},
|
||||
|
||||
handleFieldChange : function(name, e){
|
||||
this.props.onChange({
|
||||
...this.props.metadata,
|
||||
[name] : e.target.value
|
||||
});
|
||||
// load validation rules, and check input value against them
|
||||
const inputRules = validations[name] ?? [];
|
||||
const validationErr = inputRules.map((rule)=>rule(e.target.value)).filter(Boolean);
|
||||
|
||||
// if no validation rules, save to props
|
||||
if(validationErr.length === 0){
|
||||
callIfExists(e.target, 'setCustomValidity', '');
|
||||
this.props.onChange({
|
||||
...this.props.metadata,
|
||||
[name] : e.target.value
|
||||
});
|
||||
} else {
|
||||
// if validation issues, display built-in browser error popup with each error.
|
||||
const errMessage = validationErr.map((err)=>{
|
||||
return `- ${err}`;
|
||||
}).join('\n');
|
||||
callIfExists(e.target, 'setCustomValidity', errMessage);
|
||||
callIfExists(e.target, 'reportValidity');
|
||||
}
|
||||
},
|
||||
|
||||
handleSystem : function(system, e){
|
||||
if(e.target.checked){
|
||||
this.props.metadata.systems.push(system);
|
||||
@@ -60,9 +89,12 @@ const MetadataEditor = createClass({
|
||||
}
|
||||
this.props.onChange(this.props.metadata);
|
||||
},
|
||||
|
||||
handleRenderer : function(renderer, e){
|
||||
if(e.target.checked){
|
||||
this.props.metadata.renderer = renderer;
|
||||
if(renderer == 'legacy')
|
||||
this.props.metadata.theme = '5ePHB';
|
||||
}
|
||||
this.props.onChange(this.props.metadata);
|
||||
},
|
||||
@@ -73,6 +105,12 @@ const MetadataEditor = createClass({
|
||||
});
|
||||
},
|
||||
|
||||
handleTheme : function(theme){
|
||||
this.props.metadata.renderer = theme.renderer;
|
||||
this.props.metadata.theme = theme.path;
|
||||
this.props.onChange(this.props.metadata);
|
||||
},
|
||||
|
||||
handleDelete : function(){
|
||||
if(this.props.metadata.authors && this.props.metadata.authors.length <= 1){
|
||||
if(!confirm('Are you sure you want to delete this brew? Because you are the only owner of this brew, the document will be deleted permanently.')) return;
|
||||
@@ -84,8 +122,12 @@ const MetadataEditor = createClass({
|
||||
|
||||
request.delete(`/api/${this.props.metadata.googleId ?? ''}${this.props.metadata.editId}`)
|
||||
.send()
|
||||
.end(function(err, res){
|
||||
window.location.href = '/';
|
||||
.end((err, res)=>{
|
||||
if(err) {
|
||||
this.props.reportError(err);
|
||||
} else {
|
||||
window.location.href = '/';
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -139,6 +181,45 @@ const MetadataEditor = createClass({
|
||||
</div>;
|
||||
},
|
||||
|
||||
renderThemeDropdown : function(){
|
||||
if(!global.enable_themes) return;
|
||||
|
||||
const listThemes = (renderer)=>{
|
||||
return _.map(_.values(Themes[renderer]), (theme)=>{
|
||||
return <div className='item' key={''} onClick={()=>this.handleTheme(theme)} title={''}>
|
||||
{`${theme.renderer} : ${theme.name}`}
|
||||
<img src={`/themes/${theme.renderer}/${theme.path}/dropdownTexture.png`}/>
|
||||
</div>;
|
||||
});
|
||||
};
|
||||
|
||||
const currentTheme = Themes[`${_.upperFirst(this.props.metadata.renderer)}`][this.props.metadata.theme];
|
||||
let dropdown;
|
||||
|
||||
if(this.props.metadata.renderer == 'legacy') {
|
||||
dropdown =
|
||||
<Nav.dropdown className='disabled' trigger='disabled'>
|
||||
<div>
|
||||
{`Themes are not supported in the Legacy Renderer`} <i className='fas fa-caret-down'></i>
|
||||
</div>
|
||||
</Nav.dropdown>;
|
||||
} else {
|
||||
dropdown =
|
||||
<Nav.dropdown trigger='click'>
|
||||
<div>
|
||||
{`${_.upperFirst(currentTheme.renderer)} : ${currentTheme.name}`} <i className='fas fa-caret-down'></i>
|
||||
</div>
|
||||
{/*listThemes('Legacy')*/}
|
||||
{listThemes('V3')}
|
||||
</Nav.dropdown>;
|
||||
}
|
||||
|
||||
return <div className='field themes'>
|
||||
<label>theme</label>
|
||||
{dropdown}
|
||||
</div>;
|
||||
},
|
||||
|
||||
renderRenderOptions : function(){
|
||||
if(!global.enable_v3) return;
|
||||
|
||||
@@ -174,27 +255,33 @@ const MetadataEditor = createClass({
|
||||
|
||||
render : function(){
|
||||
return <div className='metadataEditor'>
|
||||
<h1 className='sectionHead'>Brew</h1>
|
||||
|
||||
<div className='field title'>
|
||||
<label>title</label>
|
||||
<input type='text' className='value'
|
||||
value={this.props.metadata.title}
|
||||
defaultValue={this.props.metadata.title}
|
||||
onChange={(e)=>this.handleFieldChange('title', e)} />
|
||||
</div>
|
||||
<div className='field description'>
|
||||
<label>description</label>
|
||||
<textarea value={this.props.metadata.description} className='value'
|
||||
onChange={(e)=>this.handleFieldChange('description', e)} />
|
||||
</div>
|
||||
<div className='field thumbnail'>
|
||||
<label>thumbnail</label>
|
||||
<input type='text'
|
||||
value={this.props.metadata.thumbnail}
|
||||
placeholder='my.thumbnail.url'
|
||||
className='value'
|
||||
onChange={(e)=>this.handleFieldChange('thumbnail', e)} />
|
||||
<button className='display' onClick={this.toggleThumbnailDisplay}>
|
||||
<i className={`fas fa-caret-${this.state.showThumbnail ? 'right' : 'left'}`} />
|
||||
</button>
|
||||
<div className='field-group'>
|
||||
<div className='field-column'>
|
||||
<div className='field description'>
|
||||
<label>description</label>
|
||||
<textarea defaultValue={this.props.metadata.description} className='value'
|
||||
onChange={(e)=>this.handleFieldChange('description', e)} />
|
||||
</div>
|
||||
<div className='field thumbnail'>
|
||||
<label>thumbnail</label>
|
||||
<input type='text'
|
||||
defaultValue={this.props.metadata.thumbnail}
|
||||
placeholder='https://my.thumbnail.url'
|
||||
className='value'
|
||||
onChange={(e)=>this.handleFieldChange('thumbnail', e)} />
|
||||
<button className='display' onClick={this.toggleThumbnailDisplay}>
|
||||
<i className={`fas fa-caret-${this.state.showThumbnail ? 'right' : 'left'}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{this.renderThumbnail()}
|
||||
</div>
|
||||
|
||||
@@ -203,8 +290,6 @@ const MetadataEditor = createClass({
|
||||
values={this.props.metadata.tags}
|
||||
onChange={(e)=>this.handleFieldChange('tags', e)}/>
|
||||
|
||||
{this.renderAuthors()}
|
||||
|
||||
<div className='field systems'>
|
||||
<label>systems</label>
|
||||
<div className='value'>
|
||||
@@ -212,8 +297,27 @@ const MetadataEditor = createClass({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.renderThemeDropdown()}
|
||||
|
||||
{this.renderRenderOptions()}
|
||||
|
||||
<hr/>
|
||||
|
||||
<h1 className='sectionHead'>Authors</h1>
|
||||
|
||||
{this.renderAuthors()}
|
||||
|
||||
<StringArrayEditor label='invited authors' valuePatterns={[/.+/]}
|
||||
validators={[(v)=>!this.props.metadata.authors?.includes(v)]}
|
||||
placeholder='invite author' unique={true}
|
||||
values={this.props.metadata.invitedAuthors}
|
||||
notes={['Invited authors are case sensitive.', 'After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.']}
|
||||
onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}/>
|
||||
|
||||
<hr/>
|
||||
|
||||
<h1 className='sectionHead'>Privacy</h1>
|
||||
|
||||
<div className='field publish'>
|
||||
<label>publish</label>
|
||||
<div className='value'>
|
||||
|
||||
@@ -7,23 +7,57 @@
|
||||
width : 100%;
|
||||
padding : 25px;
|
||||
background-color : #999;
|
||||
height : calc(100vh - 54px); // 54px is the height of the navbar + snippet bar. probably a better way to dynamic get this.
|
||||
overflow-y : auto;
|
||||
|
||||
.sectionHead {
|
||||
font-weight: 1000;
|
||||
margin: 20px 0;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
& > div {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.field-group {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.field-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 5 0 200px;
|
||||
gap: 10px;
|
||||
|
||||
}
|
||||
.field{
|
||||
display : flex;
|
||||
flex-wrap : wrap;
|
||||
width : 100%;
|
||||
margin-bottom : 10px;
|
||||
min-width : 200px;
|
||||
&>label{
|
||||
display : inline-block;
|
||||
vertical-align : top;
|
||||
width : 80px;
|
||||
font-size : 0.7em;
|
||||
font-size : 11px;
|
||||
font-weight : 800;
|
||||
line-height : 1.8em;
|
||||
text-transform : uppercase;
|
||||
flex : 0 0 auto;
|
||||
}
|
||||
&>.value{
|
||||
flex : 1 1 auto;
|
||||
min-width : 200px;
|
||||
width : 50px;
|
||||
&:invalid {
|
||||
background : #ffb9b9;
|
||||
}
|
||||
}
|
||||
input[type='text'], textarea {
|
||||
border : 1px solid gray;
|
||||
}
|
||||
&.thumbnail{
|
||||
height : 1.4em;
|
||||
@@ -43,22 +77,37 @@
|
||||
background-color: #777;
|
||||
}
|
||||
}
|
||||
.thumbnail-preview{
|
||||
position : relative;
|
||||
width : 80px;
|
||||
height : min-content;
|
||||
border : 2px solid white;
|
||||
margin-left : 5px;
|
||||
max-height : 115px;
|
||||
}
|
||||
|
||||
&.description {
|
||||
flex: 1;
|
||||
textarea.value {
|
||||
resize : none;
|
||||
height : auto;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
font-size : 0.8em;
|
||||
}
|
||||
}
|
||||
|
||||
small {
|
||||
font-size : 0.6em;
|
||||
font-style : italic;
|
||||
}
|
||||
}
|
||||
.description.field textarea.value{
|
||||
resize : none;
|
||||
height : 5em;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
font-size : 0.8em;
|
||||
|
||||
|
||||
.thumbnail-preview {
|
||||
position: relative;
|
||||
justify-self: center;
|
||||
width: 80px;
|
||||
height: min-content;
|
||||
flex: 1 1;
|
||||
max-height: 115px;
|
||||
aspect-ratio: 1 / 1;
|
||||
object-fit: contain;
|
||||
background-color: #AAA;
|
||||
}
|
||||
|
||||
.systems.field .value{
|
||||
label{
|
||||
vertical-align : middle;
|
||||
@@ -94,10 +143,6 @@
|
||||
button.unpublish{
|
||||
.button(@silver);
|
||||
}
|
||||
small{
|
||||
font-size : 0.6em;
|
||||
font-style : italic;
|
||||
}
|
||||
}
|
||||
|
||||
.delete.field .value{
|
||||
@@ -110,8 +155,59 @@
|
||||
line-height : 1.5em;
|
||||
}
|
||||
|
||||
.themes.field{
|
||||
font-size : 13.33px;
|
||||
.navDropdownContainer {
|
||||
background-color : white;
|
||||
width : 100%;
|
||||
position : relative;
|
||||
z-index : 500;
|
||||
&.disabled {
|
||||
font-style :italic;
|
||||
font-style : italic;
|
||||
background-color : darkgray;
|
||||
color : dimgray;
|
||||
}
|
||||
&>div:first-child {
|
||||
border : 2px solid rgb(118,118,118);
|
||||
padding : 6px 3px;
|
||||
background-color : inherit;
|
||||
i {
|
||||
float : right;
|
||||
}
|
||||
&:hover {
|
||||
background-color : @blue;
|
||||
color : white;
|
||||
}
|
||||
}
|
||||
.navDropdown {
|
||||
box-shadow : 0px 5px 10px rgba(0, 0, 0, 0.3);
|
||||
position : absolute;
|
||||
width : 100%;
|
||||
.item {
|
||||
padding : 3px 3px;
|
||||
border-top : 1px solid rgb(118, 118, 118);
|
||||
position : relative;
|
||||
overflow : hidden;
|
||||
background-color : white;
|
||||
&:hover {
|
||||
background-color : @blue;
|
||||
color : white;
|
||||
}
|
||||
img {
|
||||
mask-image : linear-gradient(90deg, transparent, black 20%);
|
||||
-webkit-mask-image : linear-gradient(90deg, transparent, black 20%);
|
||||
position : absolute;
|
||||
left : ~"max(100px, 100% - 300px)";
|
||||
top : 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.field .list {
|
||||
display: flex;
|
||||
flex: 1 0;
|
||||
flex-wrap: wrap;
|
||||
|
||||
> * {
|
||||
|
||||
34
client/homebrew/editor/metadataEditor/validations.js
Normal file
@@ -0,0 +1,34 @@
|
||||
module.exports = {
|
||||
title : [
|
||||
(value)=>{
|
||||
return value?.length > 100 ? 'Max title length of 100 characters' : null;
|
||||
}
|
||||
],
|
||||
description : [
|
||||
(value)=>{
|
||||
return value?.length > 500 ? 'Max description length of 500 characters.' : null;
|
||||
}
|
||||
],
|
||||
thumbnail : [
|
||||
(value)=>{
|
||||
return value?.length > 256 ? 'Max URL length of 256 characters.' : null;
|
||||
},
|
||||
(value)=>{
|
||||
if(value?.length == 0){return null;}
|
||||
try {
|
||||
Boolean(new URL(value));
|
||||
return null;
|
||||
} catch (e) {
|
||||
return 'Must be a valid URL';
|
||||
}
|
||||
}
|
||||
],
|
||||
language : [
|
||||
(value)=>{
|
||||
return new RegExp(/[a-z]{2,3}(-.*)?/).test(value || '') === false ? 'Invalid language code.' : null;
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -4,9 +4,16 @@ const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
//Import all themes
|
||||
|
||||
const SnippetsLegacy = require('./snippetsLegacy/snippets.js');
|
||||
const SnippetsV3 = require('./snippets/snippets.js');
|
||||
const Themes = require('themes/themes.json');
|
||||
|
||||
const ThemeSnippets = {};
|
||||
ThemeSnippets['Legacy_5ePHB'] = require('themes/Legacy/5ePHB/snippets.js');
|
||||
ThemeSnippets['V3_5ePHB'] = require('themes/V3/5ePHB/snippets.js');
|
||||
ThemeSnippets['V3_5eDMG'] = require('themes/V3/5eDMG/snippets.js');
|
||||
ThemeSnippets['V3_Journal'] = require('themes/V3/Journal/snippets.js');
|
||||
ThemeSnippets['V3_Blank'] = require('themes/V3/Blank/snippets.js');
|
||||
|
||||
const execute = function(val, brew){
|
||||
if(_.isFunction(val)) return val(brew);
|
||||
@@ -32,21 +39,63 @@ const Snippetbar = createClass({
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
renderer : this.props.renderer
|
||||
renderer : this.props.renderer,
|
||||
snippets : []
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount : async function() {
|
||||
const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy';
|
||||
const themePath = this.props.theme ?? '5ePHB';
|
||||
let snippets = _.cloneDeep(ThemeSnippets[`${rendererPath}_${themePath}`]);
|
||||
snippets = this.compileSnippets(rendererPath, themePath, snippets);
|
||||
this.setState({
|
||||
snippets : snippets
|
||||
});
|
||||
},
|
||||
|
||||
componentDidUpdate : async function(prevProps) {
|
||||
if(prevProps.renderer != this.props.renderer || prevProps.theme != this.props.theme) {
|
||||
const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy';
|
||||
const themePath = this.props.theme ?? '5ePHB';
|
||||
let snippets = _.cloneDeep(ThemeSnippets[`${rendererPath}_${themePath}`]);
|
||||
snippets = this.compileSnippets(rendererPath, themePath, snippets);
|
||||
this.setState({
|
||||
snippets : snippets
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
mergeCustomizer : function(valueA, valueB, key) {
|
||||
if(key == 'snippets') {
|
||||
const result = _.reverse(_.unionBy(_.reverse(valueB), _.reverse(valueA), 'name')); // Join snippets together, with preference for the current theme over the base theme
|
||||
return _.filter(result, 'gen'); //Only keep snippets with a 'gen' property.
|
||||
}
|
||||
},
|
||||
|
||||
compileSnippets : function(rendererPath, themePath, snippets) {
|
||||
let compiledSnippets = snippets;
|
||||
const baseSnippetsPath = Themes[rendererPath][themePath].baseSnippets;
|
||||
|
||||
const objB = _.keyBy(compiledSnippets, 'groupName');
|
||||
|
||||
if(baseSnippetsPath) {
|
||||
const objA = _.keyBy(_.cloneDeep(ThemeSnippets[`${rendererPath}_${baseSnippetsPath}`]), 'groupName');
|
||||
compiledSnippets = _.values(_.mergeWith(objA, objB, this.mergeCustomizer));
|
||||
compiledSnippets = this.compileSnippets(rendererPath, baseSnippetsPath, _.cloneDeep(compiledSnippets));
|
||||
} else {
|
||||
const objA = _.keyBy(_.cloneDeep(ThemeSnippets[`${rendererPath}_Blank`]), 'groupName');
|
||||
compiledSnippets = _.values(_.mergeWith(objA, objB, this.mergeCustomizer));
|
||||
}
|
||||
return compiledSnippets;
|
||||
},
|
||||
|
||||
handleSnippetClick : function(injectedText){
|
||||
this.props.onInject(injectedText);
|
||||
},
|
||||
|
||||
renderSnippetGroups : function(){
|
||||
let snippets = [];
|
||||
|
||||
if(this.props.renderer === 'V3')
|
||||
snippets = SnippetsV3.filter((snippetGroup)=>snippetGroup.view === this.props.view);
|
||||
else
|
||||
snippets = SnippetsLegacy.filter((snippetGroup)=>snippetGroup.view === this.props.view);
|
||||
const snippets = this.state.snippets.filter((snippetGroup)=>snippetGroup.view === this.props.view);
|
||||
|
||||
return _.map(snippets, (snippetGroup)=>{
|
||||
return <SnippetGroup
|
||||
|
||||
@@ -9,7 +9,9 @@ const StringArrayEditor = createClass({
|
||||
label : '',
|
||||
values : [],
|
||||
valuePatterns : null,
|
||||
validators : [],
|
||||
placeholder : '',
|
||||
notes : [],
|
||||
unique : false,
|
||||
cannotEdit : [],
|
||||
onChange : ()=>{}
|
||||
@@ -83,7 +85,8 @@ const StringArrayEditor = createClass({
|
||||
}
|
||||
const matchesPatterns = !this.props.valuePatterns || this.props.valuePatterns.some((pattern)=>!!(value || '').match(pattern));
|
||||
const uniqueIfSet = !this.props.unique || !values.includes(value);
|
||||
return matchesPatterns && uniqueIfSet;
|
||||
const passesValidators = !this.props.validators || this.props.validators.every((validator)=>validator(value));
|
||||
return matchesPatterns && uniqueIfSet && passesValidators;
|
||||
},
|
||||
|
||||
handleValueInputKeyDown : function(event, index) {
|
||||
@@ -123,17 +126,21 @@ const StringArrayEditor = createClass({
|
||||
</div>
|
||||
);
|
||||
|
||||
return <div className='field values'>
|
||||
return <div className='field'>
|
||||
<label>{this.props.label}</label>
|
||||
<div className='list'>
|
||||
{valueElements}
|
||||
<div className='input-group'>
|
||||
<input type='text' className={`value ${this.valueIsValid(this.state.temporaryValue) ? '' : 'invalid'}`} placeholder={this.props.placeholder}
|
||||
value={this.state.temporaryValue}
|
||||
onKeyDown={(e)=>this.handleValueInputKeyDown(e)}
|
||||
onChange={(e)=>this.setState({ temporaryValue: e.target.value })}/>
|
||||
{this.valueIsValid(this.state.temporaryValue) ? <div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.addValue(this.state.temporaryValue); }}><i className='fa fa-check fa-fw'/></div> : null}
|
||||
<div style={{ flex: '1 0' }}>
|
||||
<div className='list'>
|
||||
{valueElements}
|
||||
<div className='input-group'>
|
||||
<input type='text' className={`value ${this.valueIsValid(this.state.temporaryValue) ? '' : 'invalid'}`} placeholder={this.props.placeholder}
|
||||
value={this.state.temporaryValue}
|
||||
onKeyDown={(e)=>this.handleValueInputKeyDown(e)}
|
||||
onChange={(e)=>this.setState({ temporaryValue: e.target.value })}/>
|
||||
{this.valueIsValid(this.state.temporaryValue) ? <div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.addValue(this.state.temporaryValue); }}><i className='fa fa-check fa-fw'/></div> : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.props.notes ? this.props.notes.map((n)=><p><small>{n}</small></p>) : null}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ const SharePage = require('./pages/sharePage/sharePage.jsx');
|
||||
const NewPage = require('./pages/newPage/newPage.jsx');
|
||||
//const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
||||
const PrintPage = require('./pages/printPage/printPage.jsx');
|
||||
const AccountPage = require('./pages/accountPage/accountPage.jsx');
|
||||
|
||||
const WithRoute = (props)=>{
|
||||
const params = useParams();
|
||||
@@ -54,30 +55,34 @@ const Homebrew = createClass({
|
||||
global.account = this.props.account;
|
||||
global.version = this.props.version;
|
||||
global.enable_v3 = this.props.enable_v3;
|
||||
global.enable_themes = this.props.enable_themes;
|
||||
global.config = this.props.config;
|
||||
|
||||
return {};
|
||||
},
|
||||
|
||||
render : function (){
|
||||
return <Router location={this.props.url}>
|
||||
<div className='homebrew'>
|
||||
<Routes>
|
||||
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={this.props.brew} />} />
|
||||
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={this.props.brew} />} />
|
||||
<Route path='/new' element={<WithRoute el={NewPage}/>} />
|
||||
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
|
||||
<Route path='/print/:id' element={<WithRoute el={PrintPage} brew={this.props.brew} />} />
|
||||
<Route path='/print' element={<WithRoute el={PrintPage} />} />
|
||||
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||
<Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||
<Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||
<Route path='/*' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</Router>;
|
||||
return (
|
||||
<Router location={this.props.url}>
|
||||
<div className='homebrew'>
|
||||
<Routes>
|
||||
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={this.props.brew} />} />
|
||||
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={this.props.brew} />} />
|
||||
<Route path='/new' element={<WithRoute el={NewPage}/>} />
|
||||
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
|
||||
<Route path='/print/:id' element={<WithRoute el={PrintPage} brew={this.props.brew} />} />
|
||||
<Route path='/print' element={<WithRoute el={PrintPage} />} />
|
||||
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||
<Route path='/account' element={<WithRoute el={AccountPage} brew={this.props.brew} uiItems={this.props.brew.uiItems} />} />
|
||||
<Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||
<Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||
<Route path='/*' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -6,10 +6,15 @@
|
||||
height : 100%;
|
||||
background-color : @steel;
|
||||
flex-direction : column;
|
||||
overflow-y : hidden;
|
||||
.content{
|
||||
position : relative;
|
||||
height : calc(~"100% - 29px"); //Navbar height
|
||||
flex : auto;
|
||||
overflow-y : hidden;
|
||||
}
|
||||
&.listPage .content {
|
||||
overflow-y : scroll;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,12 +70,20 @@ const Account = createClass({
|
||||
{global.account.username}
|
||||
</Nav.item>
|
||||
<Nav.item
|
||||
href={`/user/${global.account.username}`}
|
||||
href={`/user/${encodeURI(global.account.username)}`}
|
||||
color='yellow'
|
||||
icon='fas fa-beer'
|
||||
>
|
||||
brews
|
||||
</Nav.item>
|
||||
<Nav.item
|
||||
className='account'
|
||||
color='orange'
|
||||
icon='fas fa-user'
|
||||
href='/account'
|
||||
>
|
||||
account
|
||||
</Nav.item>
|
||||
<Nav.item
|
||||
className='logout'
|
||||
color='red'
|
||||
|
||||
85
client/homebrew/navbar/error-navitem.jsx
Normal file
@@ -0,0 +1,85 @@
|
||||
require('./error-navitem.less');
|
||||
const React = require('react');
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const createClass = require('create-react-class');
|
||||
|
||||
const ErrorNavItem = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
error : '',
|
||||
parent : null
|
||||
};
|
||||
},
|
||||
render : function() {
|
||||
const clearError = ()=>{
|
||||
const state = {
|
||||
error : null
|
||||
};
|
||||
if(this.props.parent.state.isSaving) {
|
||||
state.isSaving = false;
|
||||
}
|
||||
this.props.parent.setState(state);
|
||||
};
|
||||
|
||||
const error = this.props.error;
|
||||
const response = error.response;
|
||||
const status = response.status;
|
||||
const message = response.body?.message;
|
||||
let errMsg = '';
|
||||
try {
|
||||
errMsg += `${error.toString()}\n\n`;
|
||||
errMsg += `\`\`\`\n${error.stack}\n`;
|
||||
errMsg += `${JSON.stringify(response.error, null, ' ')}\n\`\`\``;
|
||||
console.log(errMsg);
|
||||
} catch (e){}
|
||||
|
||||
if(status === 409) {
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={clearError}>
|
||||
{message ?? 'Conflict: please refresh to get latest changes'}
|
||||
</div>
|
||||
</Nav.item>;
|
||||
} else if(status === 412) {
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={clearError}>
|
||||
{message ?? 'Your client is out of date. Please save your changes elsewhere and refresh.'}
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
if(response.req.url.match(/^\/api.*Google.*$/m)){
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={clearError}>
|
||||
Looks like your Google credentials have
|
||||
expired! Visit our log in page to sign out
|
||||
and sign back in with Google,
|
||||
then try saving again!
|
||||
<a target='_blank' rel='noopener noreferrer'
|
||||
href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}>
|
||||
<div className='confirm'>
|
||||
Sign In
|
||||
</div>
|
||||
</a>
|
||||
<div className='deny'>
|
||||
Not Now
|
||||
</div>
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer'>
|
||||
Looks like there was a problem saving. <br />
|
||||
Report the issue <a target='_blank' rel='noopener noreferrer' href={`https://github.com/naturalcrit/homebrewery/issues/new?template=save_issue.yml&error-code=${encodeURIComponent(errMsg)}`}>
|
||||
here
|
||||
</a>.
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ErrorNavItem;
|
||||
77
client/homebrew/navbar/error-navitem.less
Normal file
@@ -0,0 +1,77 @@
|
||||
.navItem {
|
||||
&.error {
|
||||
position : relative;
|
||||
background-color : @red;
|
||||
}
|
||||
|
||||
.errorContainer{
|
||||
animation-name: glideDown;
|
||||
animation-duration: 0.4s;
|
||||
position : absolute;
|
||||
top : 100%;
|
||||
left : 50%;
|
||||
z-index : 1000;
|
||||
width : 140px;
|
||||
padding : 3px;
|
||||
color : white;
|
||||
background-color : #333;
|
||||
border : 3px solid #444;
|
||||
border-radius : 5px;
|
||||
transform : translate(-50% + 3px, 10px);
|
||||
text-align : center;
|
||||
font-size : 10px;
|
||||
font-weight : 800;
|
||||
text-transform : uppercase;
|
||||
a{
|
||||
color : @teal;
|
||||
}
|
||||
&:before {
|
||||
content: "";
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
position: absolute;
|
||||
border-left: 10px solid transparent;
|
||||
border-right: 10px solid transparent;
|
||||
border-top: 10px solid transparent;
|
||||
border-bottom: 10px solid #444;
|
||||
left: 53px;
|
||||
top: -23px;
|
||||
}
|
||||
&:after {
|
||||
content: "";
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
position: absolute;
|
||||
border-left: 10px solid transparent;
|
||||
border-right: 10px solid transparent;
|
||||
border-top: 10px solid transparent;
|
||||
border-bottom: 10px solid #333;
|
||||
left: 53px;
|
||||
top: -19px;
|
||||
}
|
||||
.deny {
|
||||
width : 48%;
|
||||
margin : 1px;
|
||||
padding : 5px;
|
||||
background-color : #333;
|
||||
display : inline-block;
|
||||
border-left : 1px solid #666;
|
||||
.animate(background-color);
|
||||
&:hover{
|
||||
background-color : red;
|
||||
}
|
||||
}
|
||||
.confirm {
|
||||
width : 48%;
|
||||
margin : 1px;
|
||||
padding : 5px;
|
||||
background-color : #333;
|
||||
display : inline-block;
|
||||
color : white;
|
||||
.animate(background-color);
|
||||
&:hover{
|
||||
background-color : teal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,14 +12,20 @@ module.exports = function(props){
|
||||
</Nav.item>
|
||||
<Nav.item color='red' icon='fas fa-fw fa-bug'
|
||||
href={`https://www.reddit.com/r/homebrewery/submit?selftext=true&text=${encodeURIComponent(dedent`
|
||||
**Browser(s)** :
|
||||
**Operating System** :
|
||||
**Legacy or v3 Renderer** :
|
||||
**Issue** : `)}`}
|
||||
- **Browser(s)** :
|
||||
- **Operating System** :
|
||||
- **Legacy or v3 Renderer** :
|
||||
- **Issue** : `)}`}
|
||||
newTab={true}
|
||||
rel='noopener noreferrer'>
|
||||
report issue
|
||||
</Nav.item>
|
||||
<Nav.item color='green' icon='fas fa-question-circle'
|
||||
href='/faq'
|
||||
newTab={true}
|
||||
rel='noopener noreferrer'>
|
||||
FAQ
|
||||
</Nav.item>
|
||||
<Nav.item color='blue' icon='fas fa-fw fa-file-import'
|
||||
href='/migrate'
|
||||
newTab={true}
|
||||
|
||||
@@ -55,6 +55,18 @@
|
||||
text-align : center;
|
||||
text-transform : initial;
|
||||
}
|
||||
.save-menu {
|
||||
.dropdown {
|
||||
z-index: 1000;
|
||||
}
|
||||
.navItem i.fa-power-off {
|
||||
color : red;
|
||||
&.active {
|
||||
color : rgb(0, 182, 52);
|
||||
filter : drop-shadow(0 0 2px rgba(0, 182, 52, 0.765))
|
||||
}
|
||||
}
|
||||
}
|
||||
.patreon.navItem{
|
||||
border-left : 1px solid #666;
|
||||
border-right : 1px solid #666;
|
||||
@@ -78,6 +90,8 @@
|
||||
width : 100%;
|
||||
overflow : hidden auto;
|
||||
max-height : ~"calc(100vh - 28px)";
|
||||
scrollbar-color : #666 #333;
|
||||
scrollbar-width : thin;
|
||||
h4{
|
||||
display : block;
|
||||
box-sizing : border-box;
|
||||
@@ -101,8 +115,36 @@
|
||||
color : white;
|
||||
text-decoration : none;
|
||||
border-top : 1px solid #888;
|
||||
overflow : clip;
|
||||
.clear{
|
||||
display : none;
|
||||
position : absolute;
|
||||
top : 50%;
|
||||
transform : translateY(-50%);
|
||||
right : 0px;
|
||||
width : 20px;
|
||||
height : 100%;
|
||||
background-color : #333;
|
||||
opacity : 70%;
|
||||
border-radius : 3px;
|
||||
&:hover {
|
||||
opacity : 100%;
|
||||
}
|
||||
i {
|
||||
text-align : center;
|
||||
font-size : 10px;
|
||||
margin : 0;
|
||||
height :100%;
|
||||
width :100%;
|
||||
}
|
||||
}
|
||||
&:hover{
|
||||
background-color : @blue;
|
||||
|
||||
.clear{
|
||||
display : grid;
|
||||
place-content : center;
|
||||
}
|
||||
}
|
||||
.title{
|
||||
display : inline-block;
|
||||
|
||||
@@ -119,6 +119,25 @@ const RecentItems = createClass({
|
||||
});
|
||||
},
|
||||
|
||||
removeItem : function(url, evt){
|
||||
evt.preventDefault();
|
||||
|
||||
let edited = JSON.parse(localStorage.getItem(EDIT_KEY) || '[]');
|
||||
let viewed = JSON.parse(localStorage.getItem(VIEW_KEY) || '[]');
|
||||
|
||||
edited = edited.filter((item)=>{ return (item.url !== url);});
|
||||
viewed = viewed.filter((item)=>{ return (item.url !== url);});
|
||||
|
||||
localStorage.setItem(EDIT_KEY, JSON.stringify(edited));
|
||||
localStorage.setItem(VIEW_KEY, JSON.stringify(viewed));
|
||||
|
||||
this.setState({
|
||||
edit : edited,
|
||||
view : viewed
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
renderDropdown : function(){
|
||||
if(!this.state.showDropdown) return null;
|
||||
|
||||
@@ -127,6 +146,7 @@ const RecentItems = createClass({
|
||||
return <a href={brew.url} className='item' key={`${brew.id}-${i}`} target='_blank' rel='noopener noreferrer' title={brew.title || '[ no title ]'}>
|
||||
<span className='title'>{brew.title || '[ no title ]'}</span>
|
||||
<span className='time'>{Moment(brew.ts).fromNow()}</span>
|
||||
<div className='clear' title='Remove from Recents' onClick={(e)=>{this.removeItem(`${brew.url}`, e);}}><i className='fas fa-times'></i></div>
|
||||
</a>;
|
||||
});
|
||||
};
|
||||
|
||||
74
client/homebrew/pages/accountPage/accountPage.jsx
Normal file
@@ -0,0 +1,74 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
const moment = require('moment');
|
||||
|
||||
const UIPage = require('../basePages/uiPage/uiPage.jsx');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const Account = require('../../navbar/account.navitem.jsx');
|
||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
|
||||
const NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx');
|
||||
|
||||
const AccountPage = createClass({
|
||||
displayName : 'AccountPage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {},
|
||||
uiItems : {}
|
||||
};
|
||||
},
|
||||
getInitialState : function() {
|
||||
return {
|
||||
uiItems : this.props.uiItems
|
||||
};
|
||||
},
|
||||
|
||||
renderNavItems : function() {
|
||||
return <Navbar>
|
||||
<Nav.section>
|
||||
<NewBrew />
|
||||
<HelpNavItem />
|
||||
<RecentNavItem />
|
||||
<Account />
|
||||
</Nav.section>
|
||||
</Navbar>;
|
||||
},
|
||||
|
||||
renderUiItems : function() {
|
||||
return <>
|
||||
<div className='dataGroup'>
|
||||
<h1>Account Information <i className='fas fa-user'></i></h1>
|
||||
<p><strong>Username: </strong> {this.props.uiItems.username || 'No user currently logged in'}</p>
|
||||
<p><strong>Last Login: </strong> {moment(this.props.uiItems.issued).format('dddd, MMMM Do YYYY, h:mm:ss a ZZ') || '-'}</p>
|
||||
</div>
|
||||
<div className='dataGroup'>
|
||||
<h3>Homebrewery Information <NaturalCritIcon /></h3>
|
||||
<p><strong>Brews on Homebrewery: </strong> {this.props.uiItems.mongoCount}</p>
|
||||
</div>
|
||||
<div className='dataGroup'>
|
||||
<h3>Google Information <i className='fab fa-google-drive'></i></h3>
|
||||
<p><strong>Linked to Google: </strong> {this.props.uiItems.googleId ? 'YES' : 'NO'}</p>
|
||||
{this.props.uiItems.googleId &&
|
||||
<p>
|
||||
<strong>Brews on Google Drive: </strong> {this.props.uiItems.googleCount ?? <>Unable to retrieve files - <a href='https://github.com/naturalcrit/homebrewery/discussions/1580'>follow these steps to renew your Google credentials.</a></>}
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <UIPage brew={this.props.brew}>
|
||||
{this.renderUiItems()}
|
||||
</UIPage>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = AccountPage;
|
||||
@@ -4,7 +4,7 @@ const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
const moment = require('moment');
|
||||
const request = require('superagent');
|
||||
const request = require('../../../../utils/request-middleware.js');
|
||||
|
||||
const googleDriveIcon = require('../../../../googleDrive.png');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
@@ -18,7 +18,8 @@ const BrewItem = createClass({
|
||||
description : '',
|
||||
authors : [],
|
||||
stubbed : true
|
||||
}
|
||||
},
|
||||
reportError : ()=>{}
|
||||
};
|
||||
},
|
||||
|
||||
@@ -33,8 +34,12 @@ const BrewItem = createClass({
|
||||
|
||||
request.delete(`/api/${this.props.brew.googleId ?? ''}${this.props.brew.editId}`)
|
||||
.send()
|
||||
.end(function(err, res){
|
||||
location.reload();
|
||||
.end((err, res)=>{
|
||||
if(err) {
|
||||
this.props.reportError(err);
|
||||
} else {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -95,15 +100,33 @@ const BrewItem = createClass({
|
||||
|
||||
render : function(){
|
||||
const brew = this.props.brew;
|
||||
if(Array.isArray(brew.tags)) { // temporary fix until dud tags are cleaned
|
||||
brew.tags = brew.tags?.filter((tag)=>tag); //remove tags that are empty strings
|
||||
}
|
||||
const dateFormatString = 'YYYY-MM-DD HH:mm:ss';
|
||||
|
||||
return <div className='brewItem'>
|
||||
{brew.thumbnail &&
|
||||
<div className='thumbnail' style={{ backgroundImage: `url(${brew.thumbnail})` }} >
|
||||
</div>
|
||||
}
|
||||
<div className='text'>
|
||||
<h2>{brew.title}</h2>
|
||||
<p className='description'>{brew.description}</p>
|
||||
</div>
|
||||
<hr />
|
||||
<div className='info'>
|
||||
|
||||
{brew.tags?.length ? <>
|
||||
<div className='brewTags' title={`Tags:\n${brew.tags.join('\n')}`}>
|
||||
<i className='fas fa-tags'/>
|
||||
{brew.tags.map((tag, idx)=>{
|
||||
const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
|
||||
return <span key={idx} className={matches[1]}>{matches[2]}</span>;
|
||||
})}
|
||||
</div>
|
||||
</> : <></>
|
||||
}
|
||||
<span title={`Authors:\n${brew.authors?.join('\n')}`}>
|
||||
<i className='fas fa-user'/> {brew.authors?.join(', ')}
|
||||
</span>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
min-height : 105px;
|
||||
margin-right : 15px;
|
||||
margin-bottom : 15px;
|
||||
padding : 5px 15px 2px 8px;
|
||||
padding : 5px 15px 2px 6px;
|
||||
padding-right : 15px;
|
||||
border : 1px solid #c9ad6a;
|
||||
border-radius : 5px;
|
||||
@@ -19,6 +19,20 @@
|
||||
break-inside : avoid;
|
||||
box-shadow : 0px 4px 5px 0px #333;
|
||||
background-color : #cab2802e;
|
||||
.thumbnail {
|
||||
position: absolute;
|
||||
width: 150px;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: -1;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: right top;
|
||||
mask-image: linear-gradient(80deg, #0000 20%, #050 40%);
|
||||
-webkit-mask-image: linear-gradient(80deg, #0000 20%, #050 40%);
|
||||
opacity: 50%;
|
||||
}
|
||||
.text {
|
||||
min-height : 54px;
|
||||
h4{
|
||||
@@ -29,13 +43,23 @@
|
||||
.info{
|
||||
position: initial;
|
||||
bottom: 2px;
|
||||
font-family : ScalySans;
|
||||
font-family : ScalySansRemake;
|
||||
font-size : 1.2em;
|
||||
&>span{
|
||||
margin-right : 12px;
|
||||
line-height : 1.5em;
|
||||
}
|
||||
}
|
||||
.brewTags span {
|
||||
background-color: #c8ac6e3b;
|
||||
margin: 2px;
|
||||
padding: 2px;
|
||||
border: 1px solid #c8ac6e;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
}
|
||||
&:hover{
|
||||
.links{
|
||||
opacity : 1;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||
require('./listPage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
@@ -6,6 +7,11 @@ const moment = require('moment');
|
||||
|
||||
const BrewItem = require('./brewItem/brewItem.jsx');
|
||||
|
||||
const USERPAGE_KEY_PREFIX = 'HOMEBREWERY-LISTPAGE';
|
||||
|
||||
const DEFAULT_SORT_TYPE = 'alpha';
|
||||
const DEFAULT_SORT_DIR = 'asc';
|
||||
|
||||
const ListPage = createClass({
|
||||
displayName : 'ListPage',
|
||||
getDefaultProps : function() {
|
||||
@@ -17,23 +23,66 @@ const ListPage = createClass({
|
||||
brews : []
|
||||
}
|
||||
],
|
||||
navItems : <></>
|
||||
navItems : <></>,
|
||||
reportError : null
|
||||
};
|
||||
},
|
||||
getInitialState : function() {
|
||||
// HIDE ALL GROUPS UNTIL LOADED
|
||||
const brewCollection = this.props.brewCollection.map((brewGroup)=>{
|
||||
brewGroup.visible = false;
|
||||
return brewGroup;
|
||||
});
|
||||
|
||||
return {
|
||||
sortType : 'alpha',
|
||||
sortDir : 'asc',
|
||||
filterString : this.props.query?.filter || '',
|
||||
query : this.props.query
|
||||
filterString : this.props.query?.filter || '',
|
||||
sortType : this.props.query?.sort || null,
|
||||
sortDir : this.props.query?.dir || null,
|
||||
query : this.props.query,
|
||||
brewCollection : brewCollection
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount : function() {
|
||||
// SAVE TO LOCAL STORAGE WHEN LEAVING PAGE
|
||||
window.onbeforeunload = this.saveToLocalStorage;
|
||||
|
||||
// LOAD FROM LOCAL STORAGE
|
||||
if(typeof window !== 'undefined') {
|
||||
const newSortType = (this.state.sortType ?? (localStorage.getItem(`${USERPAGE_KEY_PREFIX}-SORTTYPE`) || DEFAULT_SORT_TYPE));
|
||||
const newSortDir = (this.state.sortDir ?? (localStorage.getItem(`${USERPAGE_KEY_PREFIX}-SORTDIR`) || DEFAULT_SORT_DIR));
|
||||
this.updateUrl(this.state.filterString, newSortType, newSortDir);
|
||||
|
||||
const brewCollection = this.props.brewCollection.map((brewGroup)=>{
|
||||
brewGroup.visible = (localStorage.getItem(`${USERPAGE_KEY_PREFIX}-VISIBILITY-${brewGroup.class}`) ?? 'true')=='true';
|
||||
return brewGroup;
|
||||
});
|
||||
|
||||
this.setState({
|
||||
brewCollection : brewCollection,
|
||||
sortType : newSortType,
|
||||
sortDir : newSortDir
|
||||
});
|
||||
};
|
||||
},
|
||||
|
||||
componentWillUnmount : function() {
|
||||
window.onbeforeunload = function(){};
|
||||
},
|
||||
|
||||
saveToLocalStorage : function() {
|
||||
this.state.brewCollection.map((brewGroup)=>{
|
||||
localStorage.setItem(`${USERPAGE_KEY_PREFIX}-VISIBILITY-${brewGroup.class}`, `${brewGroup.visible}`);
|
||||
});
|
||||
localStorage.setItem(`${USERPAGE_KEY_PREFIX}-SORTTYPE`, this.state.sortType);
|
||||
localStorage.setItem(`${USERPAGE_KEY_PREFIX}-SORTDIR`, this.state.sortDir);
|
||||
},
|
||||
|
||||
renderBrews : function(brews){
|
||||
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
|
||||
|
||||
return _.map(brews, (brew, idx)=>{
|
||||
return <BrewItem brew={brew} key={idx}/>;
|
||||
return <BrewItem brew={brew} key={idx} reportError={this.props.reportError}/>;
|
||||
});
|
||||
},
|
||||
|
||||
@@ -50,52 +99,61 @@ const ListPage = createClass({
|
||||
},
|
||||
|
||||
handleSortOptionChange : function(event){
|
||||
this.updateUrl(this.state.filterString, event.target.value, this.state.sortDir);
|
||||
this.setState({
|
||||
sortType : event.target.value
|
||||
});
|
||||
},
|
||||
|
||||
handleSortDirChange : function(event){
|
||||
const newDir = this.state.sortDir == 'asc' ? 'desc' : 'asc';
|
||||
|
||||
this.updateUrl(this.state.filterString, this.state.sortType, newDir);
|
||||
this.setState({
|
||||
sortDir : `${(this.state.sortDir == 'asc' ? 'desc' : 'asc')}`
|
||||
sortDir : newDir
|
||||
});
|
||||
},
|
||||
|
||||
renderSortOption : function(sortTitle, sortValue){
|
||||
return <td>
|
||||
<button
|
||||
value={`${sortValue}`}
|
||||
onClick={this.handleSortOptionChange}
|
||||
className={`${(this.state.sortType == sortValue ? 'active' : '')}`}
|
||||
>
|
||||
{`${sortTitle}`}
|
||||
</button>
|
||||
</td>;
|
||||
return <div className={`sort-option ${(this.state.sortType == sortValue ? 'active' : '')}`}>
|
||||
<button
|
||||
value={`${sortValue}`}
|
||||
onClick={this.state.sortType == sortValue ? this.handleSortDirChange : this.handleSortOptionChange}
|
||||
>
|
||||
{`${sortTitle}`}
|
||||
</button>
|
||||
{this.state.sortType == sortValue &&
|
||||
<i className={`sortDir fas ${this.state.sortDir == 'asc' ? 'fa-sort-up' : 'fa-sort-down'}`}></i>
|
||||
}
|
||||
</div>;
|
||||
},
|
||||
|
||||
handleFilterTextChange : function(e){
|
||||
this.setState({
|
||||
filterString : e.target.value,
|
||||
});
|
||||
this.updateUrl(e.target.value);
|
||||
this.updateUrl(e.target.value, this.state.sortType, this.state.sortDir);
|
||||
return;
|
||||
},
|
||||
|
||||
updateUrl : function(filterTerm){
|
||||
updateUrl : function(filterTerm, sortType, sortDir){
|
||||
const url = new URL(window.location.href);
|
||||
const urlParams = new URLSearchParams(url.search);
|
||||
if(urlParams.get('filter') == filterTerm)
|
||||
return;
|
||||
|
||||
urlParams.set('sort', sortType);
|
||||
urlParams.set('dir', sortDir);
|
||||
|
||||
if(!filterTerm)
|
||||
urlParams.delete('filter');
|
||||
else
|
||||
urlParams.set('filter', filterTerm);
|
||||
|
||||
url.search = urlParams;
|
||||
window.history.replaceState(null, null, url);
|
||||
},
|
||||
|
||||
renderFilterOption : function(){
|
||||
return <td>
|
||||
return <div className='filter-option'>
|
||||
<label>
|
||||
<i className='fas fa-search'></i>
|
||||
<input
|
||||
@@ -105,68 +163,69 @@ const ListPage = createClass({
|
||||
value={this.state.filterString}
|
||||
/>
|
||||
</label>
|
||||
</td>;
|
||||
</div>;
|
||||
},
|
||||
|
||||
renderSortOptions : function(){
|
||||
return <div className='sort-container'>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<h6>Sort by :</h6>
|
||||
</td>
|
||||
{this.renderSortOption('Title', 'alpha')}
|
||||
{this.renderSortOption('Created Date', 'created')}
|
||||
{this.renderSortOption('Updated Date', 'updated')}
|
||||
{this.renderSortOption('Views', 'views')}
|
||||
{/* {this.renderSortOption('Latest', 'latest')} */}
|
||||
<td>
|
||||
<h6>Direction :</h6>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
onClick={this.handleSortDirChange}
|
||||
className='sortDir'
|
||||
>
|
||||
{`${(this.state.sortDir == 'asc' ? '\u25B2 ASC' : '\u25BC DESC')}`}
|
||||
</button>
|
||||
</td>
|
||||
{this.renderFilterOption()}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h6>Sort by :</h6>
|
||||
{this.renderSortOption('Title', 'alpha')}
|
||||
{this.renderSortOption('Created Date', 'created')}
|
||||
{this.renderSortOption('Updated Date', 'updated')}
|
||||
{this.renderSortOption('Views', 'views')}
|
||||
{/* {this.renderSortOption('Latest', 'latest')} */}
|
||||
|
||||
{this.renderFilterOption()}
|
||||
|
||||
|
||||
|
||||
</div>;
|
||||
},
|
||||
|
||||
getSortedBrews : function(brews){
|
||||
const testString = _.deburr(this.state.filterString).toLowerCase();
|
||||
brews = _.filter(brews, (brew)=>{
|
||||
return (_.deburr(brew.title).toLowerCase().includes(testString)) ||
|
||||
(_.deburr(brew.description).toLowerCase().includes(testString));
|
||||
});
|
||||
|
||||
brews = _.filter(brews, (brew)=>{
|
||||
const brewStrings = _.deburr([
|
||||
brew.title,
|
||||
brew.description,
|
||||
brew.tags].join('\n')
|
||||
.toLowerCase());
|
||||
|
||||
return brewStrings.includes(testString);
|
||||
});
|
||||
return _.orderBy(brews, (brew)=>{ return this.sortBrewOrder(brew); }, this.state.sortDir);
|
||||
},
|
||||
|
||||
toggleBrewCollectionState : function(brewGroupClass) {
|
||||
this.setState((prevState)=>({
|
||||
brewCollection : prevState.brewCollection.map(
|
||||
(brewGroup)=>brewGroup.class === brewGroupClass ? { ...brewGroup, visible: !brewGroup.visible } : brewGroup
|
||||
)
|
||||
}));
|
||||
},
|
||||
|
||||
renderBrewCollection : function(brewCollection){
|
||||
if(brewCollection == []) return <div className='brewCollection'>
|
||||
<h1>No Brews</h1>
|
||||
</div>;
|
||||
return _.map(brewCollection, (brewGroup, idx)=>{
|
||||
return <div key={idx} className={`brewCollection ${brewGroup.class ?? ''}`}>
|
||||
<h1>{brewGroup.title || 'No Title'}</h1>
|
||||
{this.renderBrews(this.getSortedBrews(brewGroup.brews))}
|
||||
<h1 className={brewGroup.visible ? 'active' : 'inactive'} onClick={()=>{this.toggleBrewCollectionState(brewGroup.class);}}>{brewGroup.title || 'No Title'}</h1>
|
||||
{brewGroup.visible ? this.renderBrews(this.getSortedBrews(brewGroup.brews)) : <></>}
|
||||
</div>;
|
||||
});
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='listPage sitePage'>
|
||||
<link href='/themes/5ePhb.style.css' rel='stylesheet'/>
|
||||
<link href='/themes/V3/5ePHB/style.css' rel='stylesheet'/>
|
||||
{this.props.navItems}
|
||||
{this.renderSortOptions()}
|
||||
|
||||
<div className='content V3'>
|
||||
<div className='phb page'>
|
||||
{this.renderSortOptions()}
|
||||
{this.renderBrewCollection(this.props.brewCollection)}
|
||||
{this.renderBrewCollection(this.state.brewCollection)}
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
}
|
||||
.listPage{
|
||||
.content{
|
||||
overflow-y : overlay;
|
||||
.phb{
|
||||
.noColumns();
|
||||
height : auto;
|
||||
@@ -27,51 +26,101 @@
|
||||
font-size : 1.3em;
|
||||
font-style : italic;
|
||||
}
|
||||
|
||||
.brewCollection {
|
||||
h1:hover{
|
||||
cursor: pointer;
|
||||
}
|
||||
.active::before, .inactive::before {
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-weight: 900;
|
||||
font-size: 0.6cm;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
.active {
|
||||
color: var(--HB_Color_HeaderText);
|
||||
}
|
||||
.active::before {
|
||||
content: '\f107';
|
||||
}
|
||||
.inactive {
|
||||
color: #707070;
|
||||
}
|
||||
.inactive::before {
|
||||
content: '\f105';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.sort-container{
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
position : fixed;
|
||||
top : 35px;
|
||||
left : calc(50vw - 400px);
|
||||
border : 2px solid #58180D;
|
||||
width : 800px;
|
||||
background-color : #EEE5CE;
|
||||
padding : 2px;
|
||||
position : sticky;
|
||||
top : 0;
|
||||
left : 0;
|
||||
width : 100%;
|
||||
height : 30px;
|
||||
background-color : #555;
|
||||
border-top : 1px solid #666;
|
||||
border-bottom : 1px solid #666;
|
||||
color : white;
|
||||
text-align : center;
|
||||
z-index : 15;
|
||||
z-index : 500;
|
||||
display : flex;
|
||||
justify-content : center;
|
||||
align-items : baseline;
|
||||
column-gap : 15px;
|
||||
row-gap : 5px;
|
||||
flex-wrap : wrap;
|
||||
h6{
|
||||
text-transform : uppercase;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
font-size : 11px;
|
||||
font-weight : bold;
|
||||
color : #58180D;
|
||||
}
|
||||
table{
|
||||
margin : 0px;
|
||||
vertical-align : middle;
|
||||
tbody tr{
|
||||
background-color: transparent !important;
|
||||
i{
|
||||
padding-right : 5px
|
||||
}
|
||||
button{
|
||||
background-color : transparent;
|
||||
color : #58180D;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
font-size : 11px;
|
||||
text-transform : uppercase;
|
||||
font-weight : normal;
|
||||
&.active{
|
||||
font-weight : bold;
|
||||
border : 2px solid #58180D;
|
||||
}
|
||||
&.sortDir{
|
||||
width : 75px;
|
||||
}
|
||||
.sort-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
color: #ccc;
|
||||
height: 100%;
|
||||
|
||||
&:hover{
|
||||
background-color : #444;
|
||||
}
|
||||
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
color: #ddd;
|
||||
background-color: #333;
|
||||
|
||||
button {
|
||||
color: white;
|
||||
font-weight: 800;
|
||||
height: 100%;
|
||||
& + .sortDir {
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.filter-option {
|
||||
margin-left: 20px;
|
||||
background-color : transparent !important;
|
||||
font-size : 11px;
|
||||
i{
|
||||
padding-right : 5px;
|
||||
}
|
||||
}
|
||||
button{
|
||||
background-color : transparent;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
text-transform : uppercase;
|
||||
font-weight : normal;
|
||||
font-size : 11px;
|
||||
color : #ccc;
|
||||
padding : 0;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
38
client/homebrew/pages/basePages/uiPage/uiPage.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
require('./uiPage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Navbar = require('../../../navbar/navbar.jsx');
|
||||
const NewBrewItem = require('../../../navbar/newbrew.navitem.jsx');
|
||||
const HelpNavItem = require('../../../navbar/help.navitem.jsx');
|
||||
const RecentNavItem = require('../../../navbar/recent.navitem.jsx').both;
|
||||
const Account = require('../../../navbar/account.navitem.jsx');
|
||||
|
||||
|
||||
const UIPage = createClass({
|
||||
displayName : 'UIPage',
|
||||
|
||||
render : function(){
|
||||
return <div className='uiPage sitePage'>
|
||||
<Navbar>
|
||||
<Nav.section>
|
||||
<Nav.item className='brewTitle'>{this.props.brew.title}</Nav.item>
|
||||
</Nav.section>
|
||||
|
||||
<Nav.section>
|
||||
<NewBrewItem />
|
||||
<HelpNavItem />
|
||||
<RecentNavItem />
|
||||
<Account />
|
||||
</Nav.section>
|
||||
</Navbar>
|
||||
|
||||
<div className='content'>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = UIPage;
|
||||
47
client/homebrew/pages/basePages/uiPage/uiPage.less
Normal file
@@ -0,0 +1,47 @@
|
||||
.uiPage{
|
||||
.content{
|
||||
overflow-y : hidden;
|
||||
width : 90vw;
|
||||
background-color: #f0f0f0;
|
||||
font-family: 'Open Sans';
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 25px;
|
||||
padding: 2% 4%;
|
||||
font-size: 0.8em;
|
||||
line-height: 1.8em;
|
||||
.dataGroup{
|
||||
padding: 6px 20px 15px;
|
||||
border: 2px solid black;
|
||||
border-radius: 5px;
|
||||
margin: 5px 0px;
|
||||
}
|
||||
h1, h2, h3, h4{
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
margin: 0.5em 30% 0.25em 0;
|
||||
border-bottom: 2px solid slategrey;
|
||||
}
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
border-bottom: 2px solid darkslategrey;
|
||||
margin-bottom: 0.5em;
|
||||
margin-right: 0;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.75em;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.5em;
|
||||
svg {
|
||||
width: 19px;
|
||||
}
|
||||
}
|
||||
h4 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ require('./editPage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const request = require('superagent');
|
||||
const request = require('../../utils/request-middleware.js');
|
||||
const { Meta } = require('vitreum/headtags');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
@@ -12,6 +12,7 @@ const Navbar = require('../../navbar/navbar.jsx');
|
||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
const PrintLink = require('../../navbar/print.navitem.jsx');
|
||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||
const Account = require('../../navbar/account.navitem.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
|
||||
@@ -21,6 +22,8 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
|
||||
const Markdown = require('naturalcrit/markdown.js');
|
||||
|
||||
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
||||
|
||||
const googleDriveActive = require('../../googleDrive.png');
|
||||
const googleDriveInactive = require('../../googleDriveMono.png');
|
||||
|
||||
@@ -30,24 +33,7 @@ const EditPage = createClass({
|
||||
displayName : 'EditPage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {
|
||||
text : '',
|
||||
style : '',
|
||||
shareId : null,
|
||||
editId : null,
|
||||
createdAt : null,
|
||||
updatedAt : null,
|
||||
gDrive : false,
|
||||
trashed : false,
|
||||
|
||||
title : '',
|
||||
description : '',
|
||||
tags : '',
|
||||
published : false,
|
||||
authors : [],
|
||||
systems : [],
|
||||
renderer : 'legacy'
|
||||
}
|
||||
brew : DEFAULT_BREW_LOAD
|
||||
};
|
||||
},
|
||||
|
||||
@@ -60,9 +46,12 @@ const EditPage = createClass({
|
||||
alertLoginToTransfer : false,
|
||||
saveGoogle : this.props.brew.googleId ? true : false,
|
||||
confirmGoogleTransfer : false,
|
||||
errors : null,
|
||||
error : null,
|
||||
htmlErrors : Markdown.validate(this.props.brew.text),
|
||||
url : ''
|
||||
url : '',
|
||||
autoSave : true,
|
||||
autoSaveWarning : false,
|
||||
unsavedTime : new Date()
|
||||
};
|
||||
},
|
||||
savedBrew : null,
|
||||
@@ -74,7 +63,14 @@ const EditPage = createClass({
|
||||
|
||||
this.savedBrew = JSON.parse(JSON.stringify(this.props.brew)); //Deep copy
|
||||
|
||||
this.trySave();
|
||||
this.setState({ autoSave: JSON.parse(localStorage.getItem('AUTOSAVE_ON')) ?? true }, ()=>{
|
||||
if(this.state.autoSave){
|
||||
this.trySave();
|
||||
} else {
|
||||
this.setState({ autoSaveWarning: true });
|
||||
}
|
||||
});
|
||||
|
||||
window.onbeforeunload = ()=>{
|
||||
if(this.state.isSaving || this.state.isPending){
|
||||
return 'You have unsaved changes!';
|
||||
@@ -117,14 +113,14 @@ const EditPage = createClass({
|
||||
brew : { ...prevState.brew, text: text },
|
||||
isPending : true,
|
||||
htmlErrors : htmlErrors
|
||||
}), ()=>this.trySave());
|
||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||
},
|
||||
|
||||
handleStyleChange : function(style){
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, style: style },
|
||||
isPending : true
|
||||
}), ()=>this.trySave());
|
||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||
},
|
||||
|
||||
handleMetaChange : function(metadata){
|
||||
@@ -134,7 +130,7 @@ const EditPage = createClass({
|
||||
...metadata
|
||||
},
|
||||
isPending : true,
|
||||
}), ()=>this.trySave());
|
||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||
|
||||
},
|
||||
|
||||
@@ -161,7 +157,10 @@ const EditPage = createClass({
|
||||
this.setState((prevState)=>({
|
||||
confirmGoogleTransfer : !prevState.confirmGoogleTransfer
|
||||
}));
|
||||
this.clearErrors();
|
||||
this.setState({
|
||||
error : null,
|
||||
isSaving : false
|
||||
});
|
||||
},
|
||||
|
||||
closeAlerts : function(event){
|
||||
@@ -177,24 +176,16 @@ const EditPage = createClass({
|
||||
this.setState((prevState)=>({
|
||||
saveGoogle : !prevState.saveGoogle,
|
||||
isSaving : false,
|
||||
errors : null
|
||||
error : null
|
||||
}), ()=>this.save());
|
||||
},
|
||||
|
||||
clearErrors : function(){
|
||||
this.setState({
|
||||
errors : null,
|
||||
isSaving : false
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
save : async function(){
|
||||
if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel();
|
||||
|
||||
this.setState((prevState)=>({
|
||||
isSaving : true,
|
||||
errors : null,
|
||||
error : null,
|
||||
htmlErrors : Markdown.validate(prevState.brew.text)
|
||||
}));
|
||||
|
||||
@@ -209,8 +200,9 @@ const EditPage = createClass({
|
||||
.send(brew)
|
||||
.catch((err)=>{
|
||||
console.log('Error Updating Local Brew');
|
||||
this.setState({ errors: err });
|
||||
this.setState({ error: err });
|
||||
});
|
||||
if(!res) return;
|
||||
|
||||
this.savedBrew = res.body;
|
||||
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
|
||||
@@ -219,10 +211,12 @@ const EditPage = createClass({
|
||||
brew : { ...prevState.brew,
|
||||
googleId : this.savedBrew.googleId ? this.savedBrew.googleId : null,
|
||||
editId : this.savedBrew.editId,
|
||||
shareId : this.savedBrew.shareId
|
||||
shareId : this.savedBrew.shareId,
|
||||
version : this.savedBrew.version
|
||||
},
|
||||
isPending : false,
|
||||
isSaving : false,
|
||||
isPending : false,
|
||||
isSaving : false,
|
||||
unsavedTime : new Date()
|
||||
}));
|
||||
},
|
||||
|
||||
@@ -268,63 +262,15 @@ const EditPage = createClass({
|
||||
},
|
||||
|
||||
renderSaveButton : function(){
|
||||
if(this.state.errors){
|
||||
let errMsg = '';
|
||||
try {
|
||||
errMsg += `${this.state.errors.toString()}\n\n`;
|
||||
errMsg += `\`\`\`\n${this.state.errors.stack}\n`;
|
||||
errMsg += `${JSON.stringify(this.state.errors.response.error, null, ' ')}\n\`\`\``;
|
||||
console.log(errMsg);
|
||||
} catch (e){}
|
||||
if(this.state.autoSaveWarning && this.hasChanges()){
|
||||
this.setAutosaveWarning();
|
||||
const elapsedTime = Math.round((new Date() - this.state.unsavedTime) / 1000 / 60);
|
||||
const text = elapsedTime == 0 ? 'Autosave is OFF.' : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
|
||||
|
||||
// if(this.state.errors.status == '401'){
|
||||
// return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
// Oops!
|
||||
// <div className='errorContainer' onClick={this.clearErrors}>
|
||||
// You must be signed in to a Google account
|
||||
// to save this to<br />Google Drive!<br />
|
||||
// <a target='_blank' rel='noopener noreferrer'
|
||||
// href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
|
||||
// <div className='confirm'>
|
||||
// Sign In
|
||||
// </div>
|
||||
// </a>
|
||||
// <div className='deny'>
|
||||
// Not Now
|
||||
// </div>
|
||||
// </div>
|
||||
// </Nav.item>;
|
||||
// }
|
||||
|
||||
if(this.state.errors.response.req.url.match(/^\/api.*Google.*$/m)){
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={this.clearErrors}>
|
||||
Looks like your Google credentials have
|
||||
expired! Visit our log in page to sign out
|
||||
and sign back in with Google,
|
||||
then try saving again!
|
||||
<a target='_blank' rel='noopener noreferrer'
|
||||
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
|
||||
<div className='confirm'>
|
||||
Sign In
|
||||
</div>
|
||||
</a>
|
||||
<div className='deny'>
|
||||
Not Now
|
||||
</div>
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-circle'>
|
||||
Reminder...
|
||||
<div className='errorContainer'>
|
||||
Looks like there was a problem saving. <br />
|
||||
Report the issue <a target='_blank' rel='noopener noreferrer'
|
||||
href={`https://github.com/naturalcrit/homebrewery/issues/new?body=${encodeURIComponent(errMsg)}`}>
|
||||
here
|
||||
</a>.
|
||||
{text}
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
@@ -335,11 +281,42 @@ const EditPage = createClass({
|
||||
if(this.state.isPending && this.hasChanges()){
|
||||
return <Nav.item className='save' onClick={this.save} color='blue' icon='fas fa-save'>Save Now</Nav.item>;
|
||||
}
|
||||
if(!this.state.isPending && !this.state.isSaving && this.state.autoSave){
|
||||
return <Nav.item className='save saved'>auto-saved.</Nav.item>;
|
||||
}
|
||||
if(!this.state.isPending && !this.state.isSaving){
|
||||
return <Nav.item className='save saved'>saved.</Nav.item>;
|
||||
}
|
||||
},
|
||||
|
||||
handleAutoSave : function(){
|
||||
if(this.warningTimer) clearTimeout(this.warningTimer);
|
||||
this.setState((prevState)=>({
|
||||
autoSave : !prevState.autoSave,
|
||||
autoSaveWarning : prevState.autoSave
|
||||
}), ()=>{
|
||||
localStorage.setItem('AUTOSAVE_ON', JSON.stringify(this.state.autoSave));
|
||||
});
|
||||
},
|
||||
|
||||
setAutosaveWarning : function(){
|
||||
setTimeout(()=>this.setState({ autoSaveWarning: false }), 4000); // 4 seconds to display
|
||||
this.warningTimer = setTimeout(()=>{this.setState({ autoSaveWarning: true });}, 900000); // 15 minutes between warnings
|
||||
this.warningTimer;
|
||||
},
|
||||
|
||||
errorReported : function(error) {
|
||||
this.setState({
|
||||
error
|
||||
});
|
||||
},
|
||||
|
||||
renderAutoSaveButton : function(){
|
||||
return <Nav.item onClick={this.handleAutoSave}>
|
||||
Autosave <i className={this.state.autoSave ? 'fas fa-power-off active' : 'fas fa-power-off'}></i>
|
||||
</Nav.item>;
|
||||
},
|
||||
|
||||
processShareId : function() {
|
||||
return this.state.brew.googleId && !this.state.brew.stubbed ?
|
||||
this.state.brew.googleId + this.state.brew.shareId :
|
||||
@@ -378,7 +355,13 @@ const EditPage = createClass({
|
||||
|
||||
<Nav.section>
|
||||
{this.renderGoogleDriveIcon()}
|
||||
{this.renderSaveButton()}
|
||||
{this.state.error ?
|
||||
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
||||
<Nav.dropdown className='save-menu'>
|
||||
{this.renderSaveButton()}
|
||||
{this.renderAutoSaveButton()}
|
||||
</Nav.dropdown>
|
||||
}
|
||||
<NewBrew />
|
||||
<HelpNavItem/>
|
||||
<Nav.dropdown>
|
||||
@@ -416,9 +399,10 @@ const EditPage = createClass({
|
||||
onTextChange={this.handleTextChange}
|
||||
onStyleChange={this.handleStyleChange}
|
||||
onMetaChange={this.handleMetaChange}
|
||||
reportError={this.errorReported}
|
||||
renderer={this.state.brew.renderer}
|
||||
/>
|
||||
<BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer} errors={this.state.htmlErrors} />
|
||||
<BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer} theme={this.state.brew.theme} errors={this.state.htmlErrors} />
|
||||
</SplitPane>
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
@@ -13,10 +13,6 @@
|
||||
cursor : initial;
|
||||
color : #666;
|
||||
}
|
||||
&.error{
|
||||
position : relative;
|
||||
background-color : @red;
|
||||
}
|
||||
}
|
||||
.googleDriveStorage {
|
||||
position : relative;
|
||||
@@ -26,74 +22,4 @@
|
||||
padding : 0px;
|
||||
margin : -5px;
|
||||
}
|
||||
.errorContainer{
|
||||
animation-name: glideDown;
|
||||
animation-duration: 0.4s;
|
||||
position : absolute;
|
||||
top : 100%;
|
||||
left : 50%;
|
||||
z-index : 100000;
|
||||
width : 140px;
|
||||
padding : 3px;
|
||||
color : white;
|
||||
background-color : #333;
|
||||
border : 3px solid #444;
|
||||
border-radius : 5px;
|
||||
transform : translate(-50% + 3px, 10px);
|
||||
text-align : center;
|
||||
font-size : 10px;
|
||||
font-weight : 800;
|
||||
text-transform : uppercase;
|
||||
a{
|
||||
color : @teal;
|
||||
}
|
||||
&:before {
|
||||
content: "";
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
position: absolute;
|
||||
border-left: 10px solid transparent;
|
||||
border-right: 10px solid transparent;
|
||||
border-top: 10px solid transparent;
|
||||
border-bottom: 10px solid #444;
|
||||
left: 53px;
|
||||
top: -23px;
|
||||
}
|
||||
&:after {
|
||||
content: "";
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
position: absolute;
|
||||
border-left: 10px solid transparent;
|
||||
border-right: 10px solid transparent;
|
||||
border-top: 10px solid transparent;
|
||||
border-bottom: 10px solid #333;
|
||||
left: 53px;
|
||||
top: -19px;
|
||||
}
|
||||
.deny {
|
||||
width : 48%;
|
||||
margin : 1px;
|
||||
padding : 5px;
|
||||
background-color : #333;
|
||||
display : inline-block;
|
||||
border-left : 1px solid #666;
|
||||
.animate(background-color);
|
||||
&:hover{
|
||||
background-color : red;
|
||||
}
|
||||
}
|
||||
.confirm {
|
||||
width : 48%;
|
||||
margin : 1px;
|
||||
padding : 5px;
|
||||
background-color : #333;
|
||||
display : inline-block;
|
||||
color : white;
|
||||
.animate(background-color);
|
||||
&:hover{
|
||||
background-color : teal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
const request = require('superagent');
|
||||
const request = require('../../utils/request-middleware.js');
|
||||
const { Meta } = require('vitreum/headtags');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
@@ -12,37 +12,38 @@ const NewBrewItem = require('../../navbar/newbrew.navitem.jsx');
|
||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||
|
||||
|
||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||
const Editor = require('../../editor/editor.jsx');
|
||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
|
||||
|
||||
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
|
||||
|
||||
const HomePage = createClass({
|
||||
displayName : 'HomePage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {
|
||||
text : '',
|
||||
},
|
||||
ver : '0.0.0'
|
||||
brew : DEFAULT_BREW,
|
||||
ver : '0.0.0'
|
||||
};
|
||||
},
|
||||
getInitialState : function() {
|
||||
return {
|
||||
brew : this.props.brew,
|
||||
welcomeText : this.props.brew.text
|
||||
welcomeText : this.props.brew.text,
|
||||
error : undefined
|
||||
};
|
||||
},
|
||||
handleSave : function(){
|
||||
request.post('/api')
|
||||
.send({
|
||||
text : this.state.brew.text
|
||||
})
|
||||
.send(this.state.brew)
|
||||
.end((err, res)=>{
|
||||
if(err) return;
|
||||
if(err) {
|
||||
this.setState({ error: err });
|
||||
return;
|
||||
}
|
||||
const brew = res.body;
|
||||
window.location = `/edit/${brew.editId}`;
|
||||
});
|
||||
@@ -58,6 +59,10 @@ const HomePage = createClass({
|
||||
renderNavbar : function(){
|
||||
return <Navbar ver={this.props.ver}>
|
||||
<Nav.section>
|
||||
{this.state.error ?
|
||||
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
||||
null
|
||||
}
|
||||
<NewBrewItem />
|
||||
<HelpNavItem />
|
||||
<RecentNavItem />
|
||||
|
||||
@@ -40,4 +40,11 @@
|
||||
right : 350px;
|
||||
}
|
||||
}
|
||||
|
||||
.navItem.save{
|
||||
background-color: @orange;
|
||||
&:hover{
|
||||
background-color: @green;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,14 @@ require('./newPage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const request = require('superagent');
|
||||
const request = require('../../utils/request-middleware.js');
|
||||
|
||||
const Markdown = require('naturalcrit/markdown.js');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
|
||||
@@ -17,6 +18,8 @@ const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||
const Editor = require('../../editor/editor.jsx');
|
||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
|
||||
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
|
||||
|
||||
const BREWKEY = 'homebrewery-new';
|
||||
const STYLEKEY = 'homebrewery-new-style';
|
||||
const METAKEY = 'homebrewery-new-meta';
|
||||
@@ -26,22 +29,7 @@ const NewPage = createClass({
|
||||
displayName : 'NewPage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {
|
||||
text : '',
|
||||
style : undefined,
|
||||
shareId : null,
|
||||
editId : null,
|
||||
createdAt : null,
|
||||
updatedAt : null,
|
||||
gDrive : false,
|
||||
|
||||
title : '',
|
||||
description : '',
|
||||
tags : '',
|
||||
published : false,
|
||||
authors : [],
|
||||
systems : []
|
||||
}
|
||||
brew : DEFAULT_BREW
|
||||
};
|
||||
},
|
||||
|
||||
@@ -49,22 +37,10 @@ const NewPage = createClass({
|
||||
const brew = this.props.brew;
|
||||
|
||||
return {
|
||||
brew : {
|
||||
text : brew.text || '',
|
||||
style : brew.style || undefined,
|
||||
gDrive : false,
|
||||
title : brew.title || '',
|
||||
description : brew.description || '',
|
||||
tags : brew.tags || '',
|
||||
published : false,
|
||||
authors : [],
|
||||
systems : brew.systems || [],
|
||||
renderer : brew.renderer || 'V3'
|
||||
},
|
||||
|
||||
brew : brew,
|
||||
isSaving : false,
|
||||
saveGoogle : (global.account && global.account.googleId ? true : false),
|
||||
errors : null,
|
||||
error : null,
|
||||
htmlErrors : Markdown.validate(brew.text)
|
||||
};
|
||||
},
|
||||
@@ -72,29 +48,29 @@ const NewPage = createClass({
|
||||
componentDidMount : function() {
|
||||
document.addEventListener('keydown', this.handleControlKeys);
|
||||
|
||||
const brew = this.props.brew;
|
||||
const brew = this.state.brew;
|
||||
|
||||
if(typeof window !== 'undefined') { //Load from localStorage if in client browser
|
||||
if(!this.props.brew.shareId && typeof window !== 'undefined') { //Load from localStorage if in client browser
|
||||
const brewStorage = localStorage.getItem(BREWKEY);
|
||||
const styleStorage = localStorage.getItem(STYLEKEY);
|
||||
const metaStorage = JSON.parse(localStorage.getItem(METAKEY));
|
||||
|
||||
if(!brew.text || !brew.style){
|
||||
brew.text = brew.text || (brewStorage ?? '');
|
||||
brew.style = brew.style || (styleStorage ?? undefined);
|
||||
// brew.title = metaStorage?.title || this.state.brew.title;
|
||||
// brew.description = metaStorage?.description || this.state.brew.description;
|
||||
brew.renderer = brew.renderer || metaStorage?.renderer;
|
||||
brew.text = brewStorage ?? brew.text;
|
||||
brew.style = styleStorage ?? brew.style;
|
||||
// brew.title = metaStorage?.title || this.state.brew.title;
|
||||
// brew.description = metaStorage?.description || this.state.brew.description;
|
||||
brew.renderer = metaStorage?.renderer ?? brew.renderer;
|
||||
brew.theme = metaStorage?.theme ?? brew.theme;
|
||||
|
||||
this.setState({
|
||||
brew : brew
|
||||
});
|
||||
}
|
||||
this.setState({
|
||||
brew : brew
|
||||
});
|
||||
}
|
||||
|
||||
localStorage.setItem(BREWKEY, brew.text);
|
||||
localStorage.setItem(STYLEKEY, brew.style);
|
||||
localStorage.setItem(METAKEY, JSON.stringify({'renderer' : brew.renderer}));
|
||||
if(brew.style)
|
||||
localStorage.setItem(STYLEKEY, brew.style);
|
||||
localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme }));
|
||||
},
|
||||
componentWillUnmount : function() {
|
||||
document.removeEventListener('keydown', this.handleControlKeys);
|
||||
@@ -142,18 +118,11 @@ const NewPage = createClass({
|
||||
localStorage.setItem(METAKEY, JSON.stringify({
|
||||
// 'title' : this.state.brew.title,
|
||||
// 'description' : this.state.brew.description,
|
||||
'renderer' : this.state.brew.renderer
|
||||
'renderer' : this.state.brew.renderer,
|
||||
'theme' : this.state.brew.theme
|
||||
}));
|
||||
},
|
||||
|
||||
clearErrors : function(){
|
||||
this.setState({
|
||||
errors : null,
|
||||
isSaving : false
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
save : async function(){
|
||||
this.setState({
|
||||
isSaving : true
|
||||
@@ -176,7 +145,7 @@ const NewPage = createClass({
|
||||
.send(brew)
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
this.setState({ isSaving: false, errors: err });
|
||||
this.setState({ isSaving: false, error: err });
|
||||
});
|
||||
if(!res) return;
|
||||
|
||||
@@ -188,67 +157,6 @@ const NewPage = createClass({
|
||||
},
|
||||
|
||||
renderSaveButton : function(){
|
||||
if(this.state.errors){
|
||||
let errMsg = '';
|
||||
try {
|
||||
errMsg += `${this.state.errors.toString()}\n\n`;
|
||||
errMsg += `\`\`\`\n${this.state.errors.stack}\n`;
|
||||
errMsg += `${JSON.stringify(this.state.errors.response.error, null, ' ')}\n\`\`\``;
|
||||
console.log(errMsg);
|
||||
} catch (e){}
|
||||
|
||||
// if(this.state.errors.status == '401'){
|
||||
// return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
// Oops!
|
||||
// <div className='errorContainer' onClick={this.clearErrors}>
|
||||
// You must be signed in to a Google account
|
||||
// to save this to<br />Google Drive!<br />
|
||||
// <a target='_blank' rel='noopener noreferrer'
|
||||
// href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
|
||||
// <div className='confirm'>
|
||||
// Sign In
|
||||
// </div>
|
||||
// </a>
|
||||
// <div className='deny'>
|
||||
// Not Now
|
||||
// </div>
|
||||
// </div>
|
||||
// </Nav.item>;
|
||||
// }
|
||||
|
||||
if(this.state.errors.response.req.url.match(/^\/api.*Google.*$/m)){
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={this.clearErrors}>
|
||||
Looks like your Google credentials have
|
||||
expired! Visit our log in page to sign out
|
||||
and sign back in with Google,
|
||||
then try saving again!
|
||||
<a target='_blank' rel='noopener noreferrer'
|
||||
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
|
||||
<div className='confirm'>
|
||||
Sign In
|
||||
</div>
|
||||
</a>
|
||||
<div className='deny'>
|
||||
Not Now
|
||||
</div>
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer'>
|
||||
Looks like there was a problem saving. <br />
|
||||
Report the issue <a target='_blank' rel='noopener noreferrer'
|
||||
href={`https://github.com/naturalcrit/homebrewery/issues/new?body=${encodeURIComponent(errMsg)}`}>
|
||||
here
|
||||
</a>.
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
if(this.state.isSaving){
|
||||
return <Nav.item icon='fas fa-spinner fa-spin' className='save'>
|
||||
save...
|
||||
@@ -278,7 +186,10 @@ const NewPage = createClass({
|
||||
</Nav.section>
|
||||
|
||||
<Nav.section>
|
||||
{this.renderSaveButton()}
|
||||
{this.state.error ?
|
||||
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
||||
this.renderSaveButton()
|
||||
}
|
||||
{this.renderLocalPrintButton()}
|
||||
<HelpNavItem />
|
||||
<RecentNavItem />
|
||||
@@ -300,7 +211,7 @@ const NewPage = createClass({
|
||||
onMetaChange={this.handleMetaChange}
|
||||
renderer={this.state.brew.renderer}
|
||||
/>
|
||||
<BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer} errors={this.state.htmlErrors}/>
|
||||
<BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer} theme={this.state.brew.theme} errors={this.state.htmlErrors}/>
|
||||
</SplitPane>
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
@@ -4,79 +4,5 @@
|
||||
&:hover{
|
||||
background-color: @green;
|
||||
}
|
||||
&.error{
|
||||
position : relative;
|
||||
background-color : @red;
|
||||
}
|
||||
}
|
||||
.errorContainer{
|
||||
animation-name: glideDown;
|
||||
animation-duration: 0.4s;
|
||||
position : absolute;
|
||||
top : 100%;
|
||||
left : 50%;
|
||||
z-index : 100000;
|
||||
width : 140px;
|
||||
padding : 3px;
|
||||
color : white;
|
||||
background-color : #333;
|
||||
border : 3px solid #444;
|
||||
border-radius : 5px;
|
||||
transform : translate(-50% + 3px, 10px);
|
||||
text-align : center;
|
||||
font-size : 10px;
|
||||
font-weight : 800;
|
||||
text-transform : uppercase;
|
||||
a{
|
||||
color : @teal;
|
||||
}
|
||||
&:before {
|
||||
content: "";
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
position: absolute;
|
||||
border-left: 10px solid transparent;
|
||||
border-right: 10px solid transparent;
|
||||
border-top: 10px solid transparent;
|
||||
border-bottom: 10px solid #444;
|
||||
left: 53px;
|
||||
top: -23px;
|
||||
}
|
||||
&:after {
|
||||
content: "";
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
position: absolute;
|
||||
border-left: 10px solid transparent;
|
||||
border-right: 10px solid transparent;
|
||||
border-top: 10px solid transparent;
|
||||
border-bottom: 10px solid #333;
|
||||
left: 53px;
|
||||
top: -19px;
|
||||
}
|
||||
.deny {
|
||||
width : 48%;
|
||||
margin : 1px;
|
||||
padding : 5px;
|
||||
background-color : #333;
|
||||
display : inline-block;
|
||||
border-left : 1px solid #666;
|
||||
.animate(background-color);
|
||||
&:hover{
|
||||
background-color : red;
|
||||
}
|
||||
}
|
||||
.confirm {
|
||||
width : 48%;
|
||||
margin : 1px;
|
||||
padding : 5px;
|
||||
background-color : #333;
|
||||
display : inline-block;
|
||||
color : white;
|
||||
.animate(background-color);
|
||||
&:hover{
|
||||
background-color : teal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ const { Meta } = require('vitreum/headtags');
|
||||
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
|
||||
const Markdown = require('naturalcrit/markdown.js');
|
||||
|
||||
const Themes = require('themes/themes.json');
|
||||
|
||||
const BREWKEY = 'homebrewery-new';
|
||||
const STYLEKEY = 'homebrewery-new-style';
|
||||
const METAKEY = 'homebrewery-new-meta';
|
||||
@@ -27,9 +29,10 @@ const PrintPage = createClass({
|
||||
getInitialState : function() {
|
||||
return {
|
||||
brew : {
|
||||
text : this.props.brew.text || '',
|
||||
style : this.props.brew.style || undefined,
|
||||
renderer : this.props.brew.renderer || 'legacy'
|
||||
text : this.props.brew.text || '',
|
||||
style : this.props.brew.style || undefined,
|
||||
renderer : this.props.brew.renderer || 'legacy',
|
||||
theme : this.props.brew.theme || '5ePHB'
|
||||
}
|
||||
};
|
||||
},
|
||||
@@ -45,7 +48,8 @@ const PrintPage = createClass({
|
||||
brew : {
|
||||
text : brewStorage,
|
||||
style : styleStorage,
|
||||
renderer : metaStorage?.renderer || 'legacy'
|
||||
renderer : metaStorage?.renderer || 'legacy',
|
||||
theme : metaStorage?.theme || '5ePHB'
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -82,9 +86,17 @@ const PrintPage = createClass({
|
||||
},
|
||||
|
||||
render : function(){
|
||||
const rendererPath = this.state.brew.renderer == 'V3' ? 'V3' : 'Legacy';
|
||||
const themePath = this.state.brew.theme ?? '5ePHB';
|
||||
const baseThemePath = Themes[rendererPath][themePath].baseTheme;
|
||||
|
||||
return <div>
|
||||
<Meta name='robots' content='noindex, nofollow' />
|
||||
<link href={`${this.state.brew.renderer == 'legacy' ? '/themes/5ePhbLegacy.style.css' : '/themes/5ePhb.style.css'}`} rel='stylesheet'/>
|
||||
<link href={`/themes/${rendererPath}/Blank/style.css`} rel='stylesheet'/>
|
||||
{baseThemePath &&
|
||||
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} rel='stylesheet'/>
|
||||
}
|
||||
<link href={`/themes/${rendererPath}/${themePath}/style.css`} rel='stylesheet'/>
|
||||
{/* Apply CSS from Style tab */}
|
||||
{this.renderStyle()}
|
||||
<div className='pages' ref='pages'>
|
||||
|
||||
@@ -12,21 +12,13 @@ const Account = require('../../navbar/account.navitem.jsx');
|
||||
|
||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
|
||||
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
||||
|
||||
const SharePage = createClass({
|
||||
displayName : 'SharePage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {
|
||||
title : '',
|
||||
text : '',
|
||||
style : '',
|
||||
shareId : null,
|
||||
createdAt : null,
|
||||
updatedAt : null,
|
||||
views : 0,
|
||||
renderer : ''
|
||||
}
|
||||
brew : DEFAULT_BREW_LOAD
|
||||
};
|
||||
},
|
||||
|
||||
@@ -86,7 +78,7 @@ const SharePage = createClass({
|
||||
</Navbar>
|
||||
|
||||
<div className='content'>
|
||||
<BrewRenderer text={this.props.brew.text} style={this.props.brew.style} renderer={this.props.brew.renderer} />
|
||||
<BrewRenderer text={this.props.brew.text} style={this.props.brew.style} renderer={this.props.brew.renderer} theme={this.props.brew.theme} />
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const Account = require('../../navbar/account.navitem.jsx');
|
||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||
|
||||
const UserPage = createClass({
|
||||
displayName : 'UserPage',
|
||||
@@ -19,11 +20,12 @@ const UserPage = createClass({
|
||||
return {
|
||||
username : '',
|
||||
brews : [],
|
||||
query : ''
|
||||
query : '',
|
||||
error : null
|
||||
};
|
||||
},
|
||||
getInitialState : function() {
|
||||
const usernameWithS = this.props.username + (this.props.username.endsWith('s') ? `'` : `'s`);
|
||||
const usernameWithS = this.props.username + (this.props.username.endsWith('s') ? `’` : `’s`);
|
||||
|
||||
const brews = _.groupBy(this.props.brews, (brew)=>{
|
||||
return (brew.published ? 'published' : 'private');
|
||||
@@ -50,10 +52,19 @@ const UserPage = createClass({
|
||||
brewCollection : brewCollection
|
||||
};
|
||||
},
|
||||
errorReported : function(error) {
|
||||
this.setState({
|
||||
error
|
||||
});
|
||||
},
|
||||
|
||||
navItems : function() {
|
||||
return <Navbar>
|
||||
<Nav.section>
|
||||
{this.state.error ?
|
||||
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
||||
null
|
||||
}
|
||||
<NewBrew />
|
||||
<HelpNavItem />
|
||||
<RecentNavItem />
|
||||
@@ -63,7 +74,7 @@ const UserPage = createClass({
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <ListPage brewCollection={this.state.brewCollection} navItems={this.navItems()} query={this.props.query}></ListPage>;
|
||||
return <ListPage brewCollection={this.state.brewCollection} navItems={this.navItems()} query={this.props.query} reportError={this.errorReported}></ListPage>;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
12
client/homebrew/utils/request-middleware.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const request = require('superagent');
|
||||
|
||||
const addHeader = (request)=>request.set('Homebrewery-Version', global.version);
|
||||
|
||||
const requestMiddleware = {
|
||||
get : (path)=>addHeader(request.get(path)),
|
||||
put : (path)=>addHeader(request.put(path)),
|
||||
post : (path)=>addHeader(request.post(path)),
|
||||
delete : (path)=>addHeader(request.delete(path)),
|
||||
};
|
||||
|
||||
module.exports = requestMiddleware;
|
||||
@@ -1,28 +1,31 @@
|
||||
module.exports = async(name, title = '', props = {})=>{
|
||||
const HOMEBREWERY_PUBLIC_URL=props.config.publicUrl;
|
||||
const template = async function(name, title='', props = {}){
|
||||
const ogTags = [];
|
||||
const ogMeta = props.ogMeta ?? {};
|
||||
Object.entries(ogMeta).forEach(([key, value])=>{
|
||||
if(!value) return;
|
||||
const tag = `<meta property="og:${key}" content="${value}">`;
|
||||
ogTags.push(tag);
|
||||
});
|
||||
const ogMetaTags = ogTags.join('\n');
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
|
||||
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
||||
<link href=${`/${name}/bundle.css`} rel='stylesheet' />
|
||||
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon" />
|
||||
<meta property="og:title" content="${props.brew?.title || 'Homebrewery - Untitled Brew'}">
|
||||
<meta property="og:url" content="${HOMEBREWERY_PUBLIC_URL}/${props.brew?.shareId ? `share/${props.brew.shareId}` : ''}">
|
||||
<meta property="og:image" content="${props.brew?.thumbnail || `${HOMEBREWERY_PUBLIC_URL}/thumbnail.png`}">
|
||||
<meta property="og:description" content="${props.brew?.description || 'No description.'}">
|
||||
<meta property="og:site_name" content="The Homebrewery - Make your Homebrew content look legit!">
|
||||
<meta property="og:type" content="article">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<title>${title.length ? `${title} - The Homebrewery`: 'The Homebrewery - NaturalCrit'}</title>
|
||||
</head>
|
||||
<body>
|
||||
<main id="reactRoot">${require(`../build/${name}/ssr.js`)(props)}</main>
|
||||
<script src=${`/${name}/bundle.js`}></script>
|
||||
<script>start_app(${JSON.stringify(props)})</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
|
||||
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
||||
<link href=${`/${name}/bundle.css`} rel='stylesheet' />
|
||||
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon" />
|
||||
${ogMetaTags}
|
||||
<meta name="twitter:card" content="summary">
|
||||
<title>${title.length ? `${title} - The Homebrewery`: 'The Homebrewery - NaturalCrit'}</title>
|
||||
</head>
|
||||
<body>
|
||||
<main id="reactRoot">${require(`../build/${name}/ssr.js`)(props)}</main>
|
||||
<script src=${`/${name}/bundle.js`}></script>
|
||||
<script>start_app(${JSON.stringify(props)})</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
};
|
||||
|
||||
module.exports = template;
|
||||
51708
package-lock.json
generated
58
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "homebrewery",
|
||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||
"version": "3.2.1",
|
||||
"version": "3.6.0",
|
||||
"engines": {
|
||||
"node": "16.11.x"
|
||||
},
|
||||
@@ -14,11 +14,14 @@
|
||||
"quick": "node scripts/quick.js",
|
||||
"build": "node scripts/buildHomebrew.js",
|
||||
"buildall": "node scripts/buildHomebrew.js && node scripts/buildAdmin.js",
|
||||
"builddev": "node scripts/buildHomebrew.js --dev",
|
||||
"lint": "eslint --fix **/*.{js,jsx}",
|
||||
"lint:dry": "eslint **/*.{js,jsx}",
|
||||
"circleci": "npm test && eslint **/*.{js,jsx} --max-warnings=0",
|
||||
"verify": "npm run lint && npm test",
|
||||
"test": "jest",
|
||||
"test:api-unit": "jest server/*.spec.js --verbose",
|
||||
"test:coverage": "jest --coverage --silent",
|
||||
"test:dev": "jest --verbose --watch",
|
||||
"test:basic": "jest tests/markdown/basic.test.js --verbose",
|
||||
"test:mustache-span": "jest tests/markdown/mustache-span.test.js --verbose",
|
||||
@@ -39,7 +42,21 @@
|
||||
"mode_modules",
|
||||
"shared",
|
||||
"server"
|
||||
]
|
||||
],
|
||||
"coverageThreshold" : {
|
||||
"global" : {
|
||||
"statements" : 25,
|
||||
"branches" : 10,
|
||||
"functions" : 22,
|
||||
"lines" : 25
|
||||
},
|
||||
"server/homebrew.api.js" : {
|
||||
"statements" : 65,
|
||||
"branches" : 50,
|
||||
"functions" : 60,
|
||||
"lines" : 70
|
||||
}
|
||||
}
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
@@ -51,44 +68,45 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.18.13",
|
||||
"@babel/plugin-transform-runtime": "^7.18.10",
|
||||
"@babel/preset-env": "^7.18.10",
|
||||
"@babel/core": "^7.20.12",
|
||||
"@babel/plugin-transform-runtime": "^7.19.6",
|
||||
"@babel/preset-env": "^7.19.4",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"body-parser": "^1.20.0",
|
||||
"classnames": "^2.3.1",
|
||||
"body-parser": "^1.20.1",
|
||||
"classnames": "^2.3.2",
|
||||
"codemirror": "^5.65.6",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"create-react-class": "^15.7.0",
|
||||
"dedent-tabs": "^0.10.1",
|
||||
"express": "^4.18.1",
|
||||
"dedent-tabs": "^0.10.2",
|
||||
"express": "^4.18.2",
|
||||
"express-async-handler": "^1.2.0",
|
||||
"express-static-gzip": "2.1.7",
|
||||
"fs-extra": "10.1.0",
|
||||
"googleapis": "107.0.0",
|
||||
"fs-extra": "11.1.0",
|
||||
"googleapis": "110.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jwt-simple": "^0.5.6",
|
||||
"less": "^3.13.1",
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "4.1.0",
|
||||
"marked": "4.2.12",
|
||||
"marked-extended-tables": "^1.0.5",
|
||||
"markedLegacy": "npm:marked@^0.3.19",
|
||||
"moment": "^2.29.4",
|
||||
"mongoose": "^6.5.4",
|
||||
"mongoose": "^6.8.4",
|
||||
"nanoid": "3.3.4",
|
||||
"nconf": "^0.12.0",
|
||||
"react": "^16.14.0",
|
||||
"react-dom": "^16.14.0",
|
||||
"npm": "^8.10.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-frame-component": "4.1.3",
|
||||
"react-router-dom": "6.3.0",
|
||||
"react-router-dom": "6.7.0",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"superagent": "^6.1.0",
|
||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.23.0",
|
||||
"eslint-plugin-react": "^7.31.1",
|
||||
"jest": "^29.0.1",
|
||||
"supertest": "^6.2.4"
|
||||
"eslint": "^8.32.0",
|
||||
"eslint-plugin-react": "^7.32.1",
|
||||
"jest": "^29.2.2",
|
||||
"supertest": "^6.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ const assetTransform = require('vitreum/transforms/asset.js');
|
||||
const babel = require('@babel/core');
|
||||
const less = require('less');
|
||||
|
||||
const babelify = async (code)=>(await babel.transformAsync(code, { presets: ['@babel/preset-env', '@babel/preset-react'], plugins: ['@babel/plugin-transform-runtime'] })).code;
|
||||
const babelify = async (code)=>(await babel.transformAsync(code, { presets: [['@babel/preset-env', { 'exclude': ['proposal-dynamic-import'] }], '@babel/preset-react'], plugins: ['@babel/plugin-transform-runtime'] })).code;
|
||||
|
||||
const transforms = {
|
||||
'.js' : (code, filename, opts)=>babelify(code),
|
||||
@@ -24,29 +24,8 @@ const build = async ({ bundle, render, ssr })=>{
|
||||
await fs.outputFile('./build/homebrew/bundle.css', css);
|
||||
await fs.outputFile('./build/homebrew/bundle.js', bundle);
|
||||
await fs.outputFile('./build/homebrew/ssr.js', ssr);
|
||||
await fs.copy('./themes/fonts', './build/fonts');
|
||||
await fs.copy('./themes/assets', './build/assets');
|
||||
|
||||
await fs.copy('./client/homebrew/favicon.ico', './build/assets/favicon.ico');
|
||||
let src = './themes/5ePhbLegacy.style.less';
|
||||
//Parse brew theme files
|
||||
less.render(fs.readFileSync(src).toString(), {
|
||||
compress : !isDev
|
||||
}, function(e, output) {
|
||||
fs.outputFile('./build/themes/5ePhbLegacy.style.css', output.css);
|
||||
});
|
||||
src = './themes/5ePhb.style.less';
|
||||
less.render(fs.readFileSync(src).toString(), {
|
||||
compress : !isDev
|
||||
}, function(e, output) {
|
||||
fs.outputFile('./build/themes/5ePhb.style.css', output.css);
|
||||
});
|
||||
// await less.render(lessCode, {
|
||||
// compress : !dev,
|
||||
// sourceMap : (dev ? {
|
||||
// sourceMapFileInline: true,
|
||||
// outputSourceFiles: true
|
||||
// } : false),
|
||||
// })
|
||||
|
||||
//compress files in production
|
||||
if(!isDev){
|
||||
@@ -61,20 +40,102 @@ const build = async ({ bundle, render, ssr })=>{
|
||||
};
|
||||
|
||||
fs.emptyDirSync('./build');
|
||||
pack('./client/homebrew/homebrew.jsx', {
|
||||
paths : ['./shared'],
|
||||
libs : Proj.libs,
|
||||
dev : isDev && build,
|
||||
transforms
|
||||
})
|
||||
.then(build)
|
||||
.catch(console.error);
|
||||
|
||||
|
||||
(async ()=>{
|
||||
|
||||
//v==----------------------------- COMPILE THEMES --------------------------------==v//
|
||||
|
||||
// Update list of all Theme files
|
||||
const themes = { Legacy: {}, V3: {} };
|
||||
|
||||
let themeFiles = fs.readdirSync('./themes/Legacy');
|
||||
for (dir of themeFiles) {
|
||||
const themeData = JSON.parse(fs.readFileSync(`./themes/Legacy/${dir}/settings.json`).toString());
|
||||
themeData.path = dir;
|
||||
themes.Legacy[dir] = (themeData);
|
||||
//fs.copy(`./themes/Legacy/${dir}/dropdownTexture.png`, `./build/themes/Legacy/${dir}/dropdownTexture.png`);
|
||||
const src = `./themes/Legacy/${dir}/style.less`;
|
||||
((outputDirectory)=>{
|
||||
less.render(fs.readFileSync(src).toString(), {
|
||||
compress : !isDev
|
||||
}, function(e, output) {
|
||||
fs.outputFile(outputDirectory, output.css);
|
||||
});
|
||||
})(`./build/themes/Legacy/${dir}/style.css`);
|
||||
|
||||
}
|
||||
|
||||
themeFiles = fs.readdirSync('./themes/V3');
|
||||
for (dir of themeFiles) {
|
||||
const themeData = JSON.parse(fs.readFileSync(`./themes/V3/${dir}/settings.json`).toString());
|
||||
themeData.path = dir;
|
||||
themes.V3[dir] = (themeData);
|
||||
fs.copy(`./themes/V3/${dir}/dropdownTexture.png`, `./build/themes/V3/${dir}/dropdownTexture.png`);
|
||||
const src = `./themes/V3/${dir}/style.less`;
|
||||
((outputDirectory)=>{
|
||||
less.render(fs.readFileSync(src).toString(), {
|
||||
compress : !isDev
|
||||
}, function(e, output) {
|
||||
fs.outputFile(outputDirectory, output.css);
|
||||
});
|
||||
})(`./build/themes/V3/${dir}/style.css`);
|
||||
}
|
||||
|
||||
await fs.outputFile('./themes/themes.json', JSON.stringify(themes, null, 2));
|
||||
|
||||
// await less.render(lessCode, {
|
||||
// compress : !dev,
|
||||
// sourceMap : (dev ? {
|
||||
// sourceMapFileInline: true,
|
||||
// outputSourceFiles: true
|
||||
// } : false),
|
||||
// })
|
||||
|
||||
// Move assets
|
||||
await fs.copy('./themes/fonts', './build/fonts');
|
||||
await fs.copy('./themes/assets', './build/assets');
|
||||
|
||||
//v==----------------------------- BUNDLE PACKAGES --------------------------------==v//
|
||||
|
||||
const bundles = await pack('./client/homebrew/homebrew.jsx', {
|
||||
paths : ['./shared', './'],
|
||||
libs : Proj.libs,
|
||||
dev : isDev && build,
|
||||
transforms
|
||||
});
|
||||
build(bundles);
|
||||
|
||||
// Possible method for generating separate bundles for theme snippets: factor-bundle first sending all common files to bundle.js, then again using default settings, keeping only snippet bundles
|
||||
// await fs.outputFile('./build/junk.js', '');
|
||||
// await fs.outputFile('./build/themes/Legacy/5ePHB/snippets.js', '');
|
||||
//
|
||||
// const files = ['./client/homebrew/homebrew.jsx','./themes/Legacy/5ePHB/snippets.js'];
|
||||
//
|
||||
// bundles = await pack(files, {
|
||||
// dedupe: false,
|
||||
// plugin : [['factor-bundle', { outputs: [ './build/junk.js','./build/themes/Legacy/5ePHB/snippets.js'], threshold : function(row, groups) {
|
||||
// console.log(groups);
|
||||
// if (groups.some(group => /.*homebrew.jsx$/.test(group))) {
|
||||
// console.log("found homebrewery")
|
||||
// return true;
|
||||
// }
|
||||
// return this._defaultThreshold(row, groups);
|
||||
// }}]],
|
||||
// paths : ['./shared','./','./build'],
|
||||
// libs : Proj.libs,
|
||||
// dev : isDev && build,
|
||||
// transforms
|
||||
// });
|
||||
// build(bundles);
|
||||
//
|
||||
|
||||
})().catch(console.error);
|
||||
|
||||
//In development set up a watch server and livereload
|
||||
if(isDev){
|
||||
livereload('./build');
|
||||
watchFile('./server.js', {
|
||||
watch : ['./client', './server'] // Watch additional folders if you want
|
||||
watch : ['./client', './server', './themes'] // Watch additional folders if you want
|
||||
});
|
||||
}
|
||||
|
||||
160
server/app.js
@@ -1,4 +1,4 @@
|
||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||
/*eslint max-lines: ["warn", {"max": 400, "skipBlankLines": true, "skipComments": true}]*/
|
||||
// Set working directory to project root
|
||||
process.chdir(`${__dirname}/..`);
|
||||
|
||||
@@ -15,13 +15,15 @@ const serveCompressedStaticAssets = require('./static-assets.mv.js');
|
||||
const sanitizeFilename = require('sanitize-filename');
|
||||
const asyncHandler = require('express-async-handler');
|
||||
|
||||
const { DEFAULT_BREW } = require('./brewDefaults.js');
|
||||
|
||||
const splitTextStyleAndMetadata = (brew)=>{
|
||||
brew.text = brew.text.replaceAll('\r\n', '\n');
|
||||
if(brew.text.startsWith('```metadata')) {
|
||||
const index = brew.text.indexOf('```\n\n');
|
||||
const metadataSection = brew.text.slice(12, index - 1);
|
||||
const metadata = yaml.load(metadataSection);
|
||||
Object.assign(brew, _.pick(metadata, ['title', 'description', 'tags', 'systems', 'renderer']));
|
||||
Object.assign(brew, _.pick(metadata, ['title', 'description', 'tags', 'systems', 'renderer', 'theme']));
|
||||
brew.text = brew.text.slice(index + 5);
|
||||
}
|
||||
if(brew.text.startsWith('```css')) {
|
||||
@@ -76,6 +78,14 @@ const faqText = require('fs').readFileSync('faq.md', 'utf8');
|
||||
|
||||
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
|
||||
|
||||
const defaultMetaTags = {
|
||||
site_name : 'The Homebrewery - Make your Homebrew content look legit!',
|
||||
title : 'The Homebrewery',
|
||||
description : 'A NaturalCrit Tool for creating authentic Homebrews using Markdown.',
|
||||
image : `${config.get('publicUrl')}/thumbnail.png`,
|
||||
type : 'website'
|
||||
};
|
||||
|
||||
//Robots.txt
|
||||
app.get('/robots.txt', (req, res)=>{
|
||||
return res.sendFile(`robots.txt`, { root: process.cwd() });
|
||||
@@ -86,17 +96,29 @@ app.get('/', (req, res, next)=>{
|
||||
req.brew = {
|
||||
text : welcomeText,
|
||||
renderer : 'V3'
|
||||
},
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'Homepage',
|
||||
description : 'Homepage'
|
||||
};
|
||||
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
return next();
|
||||
});
|
||||
|
||||
//Home page v3
|
||||
//Home page Legacy
|
||||
app.get('/legacy', (req, res, next)=>{
|
||||
req.brew = {
|
||||
text : welcomeTextLegacy,
|
||||
renderer : 'legacy'
|
||||
},
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'Homepage (Legacy)',
|
||||
description : 'Homepage'
|
||||
};
|
||||
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
return next();
|
||||
});
|
||||
@@ -106,7 +128,13 @@ app.get('/migrate', (req, res, next)=>{
|
||||
req.brew = {
|
||||
text : migrateText,
|
||||
renderer : 'V3'
|
||||
},
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'v3 Migration Guide',
|
||||
description : 'A brief guide to converting Legacy documents to the v3 renderer.'
|
||||
};
|
||||
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
return next();
|
||||
});
|
||||
@@ -117,7 +145,13 @@ app.get('/changelog', async (req, res, next)=>{
|
||||
title : 'Changelog',
|
||||
text : changelogText,
|
||||
renderer : 'V3'
|
||||
},
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'Changelog',
|
||||
description : 'Development changelog.'
|
||||
};
|
||||
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
return next();
|
||||
});
|
||||
@@ -128,7 +162,13 @@ app.get('/faq', async (req, res, next)=>{
|
||||
title : 'FAQ',
|
||||
text : faqText,
|
||||
renderer : 'V3'
|
||||
},
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'FAQ',
|
||||
description : 'Frequently Asked Questions'
|
||||
};
|
||||
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
return next();
|
||||
});
|
||||
@@ -152,12 +192,19 @@ app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
|
||||
sanitizeBrew(brew, 'share');
|
||||
const prefix = 'HB - ';
|
||||
|
||||
const encodeRFC3986ValueChars = (str)=>{
|
||||
return (
|
||||
encodeURIComponent(str)
|
||||
.replace(/[!'()*]/g, (char)=>{`%${char.charCodeAt(0).toString(16).toUpperCase()}`;})
|
||||
);
|
||||
};
|
||||
|
||||
let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
|
||||
if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
|
||||
res.set({
|
||||
'Cache-Control' : 'no-cache',
|
||||
'Content-Type' : 'text/plain',
|
||||
'Content-Disposition' : `attachment; filename="${fileName}.txt"`
|
||||
'Content-Disposition' : `attachment; filename*=UTF-8''${encodeRFC3986ValueChars(fileName)}.txt`
|
||||
});
|
||||
res.status(200).send(brew.text);
|
||||
});
|
||||
@@ -166,6 +213,12 @@ app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
|
||||
app.get('/user/:username', async (req, res, next)=>{
|
||||
const ownAccount = req.account && (req.account.username == req.params.username);
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : `${req.params.username}'s Collection`,
|
||||
description : 'View my collection of homebrew on the Homebrewery.'
|
||||
// type : could be 'profile'?
|
||||
};
|
||||
|
||||
const fields = [
|
||||
'googleId',
|
||||
'title',
|
||||
@@ -179,6 +232,7 @@ app.get('/user/:username', async (req, res, next)=>{
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'lastViewed',
|
||||
'thumbnail',
|
||||
'tags'
|
||||
];
|
||||
|
||||
@@ -222,6 +276,14 @@ app.get('/user/:username', async (req, res, next)=>{
|
||||
//Edit Page
|
||||
app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
|
||||
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : req.brew.title || 'Untitled Brew',
|
||||
description : req.brew.description || 'No description.',
|
||||
image : req.brew.thumbnail || defaultMetaTags.image,
|
||||
type : 'article'
|
||||
};
|
||||
|
||||
sanitizeBrew(req.brew, 'edit');
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
|
||||
@@ -232,7 +294,14 @@ app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
|
||||
app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
||||
sanitizeBrew(req.brew, 'share');
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
req.brew.views = 0;
|
||||
req.brew.title = `CLONE - ${req.brew.title}`;
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'New',
|
||||
description : 'Start crafting your homebrew on the Homebrewery!'
|
||||
};
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
@@ -240,6 +309,13 @@ app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
||||
app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
|
||||
const { brew } = req;
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : req.brew.title || 'Untitled Brew',
|
||||
description : req.brew.description || 'No description.',
|
||||
image : req.brew.thumbnail || defaultMetaTags.image,
|
||||
type : 'article'
|
||||
};
|
||||
|
||||
if(req.params.id.length > 12 && !brew._id) {
|
||||
const googleId = req.params.id.slice(0, -12);
|
||||
const shareId = req.params.id.slice(-12);
|
||||
@@ -260,6 +336,61 @@ app.get('/print/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
||||
next();
|
||||
});
|
||||
|
||||
//Account Page
|
||||
app.get('/account', asyncHandler(async (req, res, next)=>{
|
||||
const data = {};
|
||||
data.title = 'Account Information Page';
|
||||
|
||||
let auth;
|
||||
let googleCount = [];
|
||||
if(req.account) {
|
||||
if(req.account.googleId) {
|
||||
try {
|
||||
auth = await GoogleActions.authCheck(req.account, res, false);
|
||||
} catch (e) {
|
||||
auth = undefined;
|
||||
console.log('Google auth check failed!');
|
||||
console.log(e);
|
||||
}
|
||||
if(auth.credentials.access_token) {
|
||||
try {
|
||||
googleCount = await GoogleActions.listGoogleBrews(auth);
|
||||
} catch (e) {
|
||||
googleCount = undefined;
|
||||
console.log('List Google files failed!');
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const query = { authors: req.account.username, googleId: { $exists: false } };
|
||||
const mongoCount = await HomebrewModel.countDocuments(query)
|
||||
.catch((err)=>{
|
||||
mongoCount = 0;
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
data.uiItems = {
|
||||
username : req.account.username,
|
||||
issued : req.account.issued,
|
||||
googleId : Boolean(req.account.googleId),
|
||||
authCheck : Boolean(req.account.googleId && auth.credentials.access_token),
|
||||
mongoCount : mongoCount,
|
||||
googleCount : googleCount?.length
|
||||
};
|
||||
}
|
||||
|
||||
req.brew = data;
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : `Account Page`,
|
||||
description : null
|
||||
};
|
||||
|
||||
return next();
|
||||
}));
|
||||
|
||||
|
||||
const nodeEnv = config.get('node_env');
|
||||
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
|
||||
// Local only
|
||||
@@ -274,11 +405,10 @@ if(isLocalEnvironment){
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
//Render the page
|
||||
const templateFn = require('./../client/template.js');
|
||||
app.use(asyncHandler(async (req, res, next)=>{
|
||||
|
||||
// Create configuration object
|
||||
const configuration = {
|
||||
local : isLocalEnvironment,
|
||||
@@ -286,14 +416,16 @@ app.use(asyncHandler(async (req, res, next)=>{
|
||||
environment : nodeEnv
|
||||
};
|
||||
const props = {
|
||||
version : require('./../package.json').version,
|
||||
url : req.originalUrl,
|
||||
brew : req.brew,
|
||||
brews : req.brews,
|
||||
googleBrews : req.googleBrews,
|
||||
account : req.account,
|
||||
enable_v3 : config.get('enable_v3'),
|
||||
config : configuration
|
||||
version : require('./../package.json').version,
|
||||
url : req.originalUrl,
|
||||
brew : req.brew,
|
||||
brews : req.brews,
|
||||
googleBrews : req.googleBrews,
|
||||
account : req.account,
|
||||
enable_v3 : config.get('enable_v3'),
|
||||
enable_themes : config.get('enable_themes'),
|
||||
config : configuration,
|
||||
ogMeta : req.ogMeta
|
||||
};
|
||||
const title = req.brew ? req.brew.title : '';
|
||||
const page = await templateFn('homebrew', title, props)
|
||||
|
||||
36
server/brewDefaults.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
// Default properties for newly-created brews
|
||||
const DEFAULT_BREW = {
|
||||
title : '',
|
||||
text : '',
|
||||
style : undefined,
|
||||
description : '',
|
||||
editId : undefined,
|
||||
shareId : undefined,
|
||||
createdAt : undefined,
|
||||
updatedAt : undefined,
|
||||
renderer : 'V3',
|
||||
theme : '5ePHB',
|
||||
authors : [],
|
||||
tags : [],
|
||||
systems : [],
|
||||
thumbnail : '',
|
||||
published : false,
|
||||
pageCount : 1,
|
||||
gDrive : false,
|
||||
trashed : false
|
||||
|
||||
};
|
||||
// Default values for older brews with missing properties
|
||||
// e.g., missing "renderer" is assumed to be "legacy"
|
||||
const DEFAULT_BREW_LOAD = _.defaults(
|
||||
{
|
||||
renderer : 'legacy',
|
||||
},
|
||||
DEFAULT_BREW);
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_BREW,
|
||||
DEFAULT_BREW_LOAD
|
||||
};
|
||||
@@ -10,7 +10,7 @@ const Mongoose = require('mongoose');
|
||||
const getMongoDBURL = (config)=>{
|
||||
return config.get('mongodb_uri') ||
|
||||
config.get('mongolab_uri') ||
|
||||
'mongodb://localhost/homebrewery';
|
||||
'mongodb://127.0.0.1/homebrewery'; // changed from mongodb://localhost/homebrewery to accommodate versions 16+ of node.
|
||||
};
|
||||
|
||||
const handleConnectionError = (error)=>{
|
||||
|
||||
@@ -5,24 +5,28 @@ const { nanoid } = require('nanoid');
|
||||
const token = require('./token.js');
|
||||
const config = require('./config.js');
|
||||
|
||||
const keys = typeof(config.get('service_account')) == 'string' ?
|
||||
JSON.parse(config.get('service_account')) :
|
||||
config.get('service_account');
|
||||
let serviceAuth;
|
||||
try {
|
||||
serviceAuth = google.auth.fromJSON(keys);
|
||||
serviceAuth.scopes = [
|
||||
'https://www.googleapis.com/auth/drive'
|
||||
];
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
console.log('Please make sure that a Google Service Account is set up properly in your config files.');
|
||||
if(!config.get('service_account')){
|
||||
console.log('No Google Service Account in config files - Google Drive integration will not be available.');
|
||||
} else {
|
||||
const keys = typeof(config.get('service_account')) == 'string' ?
|
||||
JSON.parse(config.get('service_account')) :
|
||||
config.get('service_account');
|
||||
|
||||
try {
|
||||
serviceAuth = google.auth.fromJSON(keys);
|
||||
serviceAuth.scopes = ['https://www.googleapis.com/auth/drive'];
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
console.log('Please make sure the Google Service Account is set up properly in your config files.');
|
||||
}
|
||||
}
|
||||
|
||||
google.options({ auth: serviceAuth || config.get('google_api_key') });
|
||||
|
||||
const GoogleActions = {
|
||||
|
||||
authCheck : (account, res)=>{
|
||||
authCheck : (account, res, updateTokens=true)=>{
|
||||
if(!account || !account.googleId){ // If not signed into Google
|
||||
const err = new Error('Not Signed In');
|
||||
err.status = 401;
|
||||
@@ -40,7 +44,7 @@ const GoogleActions = {
|
||||
refresh_token : account.googleRefreshToken
|
||||
});
|
||||
|
||||
oAuth2Client.on('tokens', (tokens)=>{
|
||||
updateTokens && oAuth2Client.on('tokens', (tokens)=>{
|
||||
if(tokens.refresh_token) {
|
||||
account.googleRefreshToken = tokens.refresh_token;
|
||||
}
|
||||
@@ -125,8 +129,7 @@ const GoogleActions = {
|
||||
description : file.description,
|
||||
views : parseInt(file.properties.views),
|
||||
published : file.properties.published ? file.properties.published == 'true' : false,
|
||||
systems : [],
|
||||
thumbnail : file.properties.thumbnail
|
||||
systems : []
|
||||
};
|
||||
});
|
||||
return brews;
|
||||
@@ -146,8 +149,7 @@ const GoogleActions = {
|
||||
editId : brew.editId || nanoid(12),
|
||||
pageCount : brew.pageCount,
|
||||
renderer : brew.renderer || 'legacy',
|
||||
isStubbed : true,
|
||||
thumbnail : brew.thumbnail
|
||||
isStubbed : true
|
||||
}
|
||||
},
|
||||
media : {
|
||||
@@ -185,8 +187,7 @@ const GoogleActions = {
|
||||
pageCount : brew.pageCount,
|
||||
renderer : brew.renderer || 'legacy',
|
||||
isStubbed : true,
|
||||
version : 1,
|
||||
thumbnail : brew.thumbnail || ''
|
||||
version : 1
|
||||
}
|
||||
};
|
||||
|
||||
@@ -252,7 +253,6 @@ const GoogleActions = {
|
||||
text : file.data,
|
||||
|
||||
description : obj.data.description,
|
||||
tags : obj.data.properties.tags ? obj.data.properties.tags : '',
|
||||
systems : obj.data.properties.systems ? obj.data.properties.systems.split(',') : [],
|
||||
authors : [],
|
||||
published : obj.data.properties.published ? obj.data.properties.published == 'true' : false,
|
||||
@@ -265,7 +265,6 @@ const GoogleActions = {
|
||||
views : parseInt(obj.data.properties.views) || 0, //brews with no view parameter will return undefined
|
||||
version : parseInt(obj.data.properties.version) || 0,
|
||||
renderer : obj.data.properties.renderer ? obj.data.properties.renderer : 'legacy',
|
||||
thumbnail : obj.data.properties.thumbnail || '',
|
||||
|
||||
googleId : id
|
||||
};
|
||||
|
||||
@@ -9,15 +9,19 @@ const yaml = require('js-yaml');
|
||||
const asyncHandler = require('express-async-handler');
|
||||
const { nanoid } = require('nanoid');
|
||||
|
||||
const { DEFAULT_BREW, DEFAULT_BREW_LOAD } = require('./brewDefaults.js');
|
||||
|
||||
// const getTopBrews = (cb) => {
|
||||
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
|
||||
// cb(brews);
|
||||
// });
|
||||
// };
|
||||
|
||||
const getBrew = (accessType)=>{
|
||||
// Create middleware with the accessType passed in as part of the scope
|
||||
return async (req, res, next)=>{
|
||||
const MAX_TITLE_LENGTH = 100;
|
||||
|
||||
const api = {
|
||||
homebrewApi : router,
|
||||
getId : (req)=>{
|
||||
// Set the id and initial potential google id, where the google id is present on the existing brew.
|
||||
let id = req.params.id, googleId = req.body?.googleId;
|
||||
|
||||
@@ -26,296 +30,319 @@ const getBrew = (accessType)=>{
|
||||
googleId = id.slice(0, -12);
|
||||
id = id.slice(-12);
|
||||
}
|
||||
// Try to find the document in the Homebrewery database -- if it doesn't exist, that's fine.
|
||||
let stub = await HomebrewModel.get(accessType === 'edit' ? { editId: id } : { shareId: id })
|
||||
.catch((err)=>{
|
||||
if(googleId) {
|
||||
console.warn(`Unable to find document stub for ${accessType}Id ${id}`);
|
||||
} else {
|
||||
console.warn(err);
|
||||
}
|
||||
});
|
||||
stub = stub?.toObject();
|
||||
return { id, googleId };
|
||||
},
|
||||
getBrew : (accessType, stubOnly = false)=>{
|
||||
// Create middleware with the accessType passed in as part of the scope
|
||||
return async (req, res, next)=>{
|
||||
// Get relevant IDs for the brew
|
||||
const { id, googleId } = api.getId(req);
|
||||
|
||||
// If there is a google id, try to find the google brew
|
||||
if(googleId || stub?.googleId) {
|
||||
let googleError;
|
||||
const googleBrew = await GoogleActions.getGoogleBrew(googleId || stub?.googleId, id, accessType)
|
||||
// Try to find the document in the Homebrewery database -- if it doesn't exist, that's fine.
|
||||
let stub = await HomebrewModel.get(accessType === 'edit' ? { editId: id } : { shareId: id })
|
||||
.catch((err)=>{
|
||||
console.warn(err);
|
||||
googleError = err;
|
||||
if(googleId) {
|
||||
console.warn(`Unable to find document stub for ${accessType}Id ${id}`);
|
||||
} else {
|
||||
console.warn(err);
|
||||
}
|
||||
});
|
||||
// If we can't find the google brew and there is a google id for the brew, throw an error.
|
||||
if(!googleBrew) throw googleError;
|
||||
// Combine the Homebrewery stub with the google brew, or if the stub doesn't exist just use the google brew
|
||||
stub = stub ? _.assign({ ...excludeStubProps(stub), stubbed: true }, excludeGoogleProps(googleBrew)) : googleBrew;
|
||||
}
|
||||
stub = stub?.toObject();
|
||||
|
||||
// If after all of that we still don't have a brew, throw an exception
|
||||
if(!stub) {
|
||||
throw 'Brew not found in Homebrewery database or Google Drive';
|
||||
}
|
||||
// If there is a google id, try to find the google brew
|
||||
if(!stubOnly && (googleId || stub?.googleId)) {
|
||||
let googleError;
|
||||
const googleBrew = await GoogleActions.getGoogleBrew(googleId || stub?.googleId, id, accessType)
|
||||
.catch((err)=>{
|
||||
googleError = err;
|
||||
});
|
||||
// Throw any error caught while attempting to retrieve Google brew.
|
||||
if(googleError) throw googleError;
|
||||
// Combine the Homebrewery stub with the google brew, or if the stub doesn't exist just use the google brew
|
||||
stub = stub ? _.assign({ ...api.excludeStubProps(stub), stubbed: true }, api.excludeGoogleProps(googleBrew)) : googleBrew;
|
||||
}
|
||||
const authorsExist = stub?.authors?.length > 0;
|
||||
const isAuthor = stub?.authors?.includes(req.account?.username);
|
||||
const isInvited = stub?.invitedAuthors?.includes(req.account?.username);
|
||||
if(accessType === 'edit' && (authorsExist && !(isAuthor || isInvited))) {
|
||||
throw `The current logged in user does not have editor access to this brew.
|
||||
|
||||
if(typeof stub.tags === 'string') {
|
||||
stub.tags = [];
|
||||
}
|
||||
req.brew = stub;
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
const mergeBrewText = (brew)=>{
|
||||
let text = brew.text;
|
||||
if(brew.style !== undefined) {
|
||||
text = `\`\`\`css\n` +
|
||||
`${brew.style || ''}\n` +
|
||||
`\`\`\`\n\n` +
|
||||
`${text}`;
|
||||
}
|
||||
const metadata = _.pick(brew, ['title', 'description', 'tags', 'systems', 'renderer']);
|
||||
text = `\`\`\`metadata\n` +
|
||||
`${yaml.dump(metadata)}\n` +
|
||||
`\`\`\`\n\n` +
|
||||
`${text}`;
|
||||
return text;
|
||||
};
|
||||
|
||||
const MAX_TITLE_LENGTH = 100;
|
||||
|
||||
const getGoodBrewTitle = (text)=>{
|
||||
const tokens = Markdown.marked.lexer(text);
|
||||
return (tokens.find((token)=>token.type === 'heading' || token.type === 'paragraph')?.text || 'No Title')
|
||||
.slice(0, MAX_TITLE_LENGTH);
|
||||
};
|
||||
|
||||
const excludePropsFromUpdate = (brew)=>{
|
||||
// Remove undesired properties
|
||||
const modified = _.clone(brew);
|
||||
const propsToExclude = ['_id', 'views', 'lastViewed', 'editId', 'shareId', 'googleId'];
|
||||
for (const prop of propsToExclude) {
|
||||
delete modified[prop];
|
||||
}
|
||||
return modified;
|
||||
};
|
||||
|
||||
const excludeGoogleProps = (brew)=>{
|
||||
const modified = _.clone(brew);
|
||||
const propsToExclude = ['tags', 'systems', 'published', 'authors', 'owner', 'views'];
|
||||
for (const prop of propsToExclude) {
|
||||
delete modified[prop];
|
||||
}
|
||||
return modified;
|
||||
};
|
||||
|
||||
const excludeStubProps = (brew)=>{
|
||||
const propsToExclude = ['text', 'textBin', 'renderer', 'pageCount', 'version'];
|
||||
for (const prop of propsToExclude) {
|
||||
brew[prop] = undefined;
|
||||
}
|
||||
return brew;
|
||||
};
|
||||
|
||||
const beforeNewSave = (account, brew)=>{
|
||||
if(!brew.title) {
|
||||
brew.title = getGoodBrewTitle(brew.text);
|
||||
}
|
||||
|
||||
brew.authors = (account) ? [account.username] : [];
|
||||
brew.text = mergeBrewText(brew);
|
||||
};
|
||||
|
||||
const newGoogleBrew = async (account, brew, res)=>{
|
||||
const oAuth2Client = GoogleActions.authCheck(account, res);
|
||||
|
||||
const newBrew = excludeGoogleProps(brew);
|
||||
|
||||
return await GoogleActions.newGoogleBrew(oAuth2Client, newBrew);
|
||||
};
|
||||
|
||||
const newBrew = async (req, res)=>{
|
||||
const brew = req.body;
|
||||
const { saveToGoogle } = req.query;
|
||||
|
||||
delete brew.editId;
|
||||
delete brew.shareId;
|
||||
delete brew.googleId;
|
||||
|
||||
beforeNewSave(req.account, brew);
|
||||
|
||||
const newHomebrew = new HomebrewModel(brew);
|
||||
newHomebrew.editId = nanoid(12);
|
||||
newHomebrew.shareId = nanoid(12);
|
||||
|
||||
let googleId, saved;
|
||||
if(saveToGoogle) {
|
||||
googleId = await newGoogleBrew(req.account, newHomebrew, res)
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
res.status(err?.status || err?.response?.status || 500).send(err?.message || err);
|
||||
});
|
||||
if(!googleId) return;
|
||||
excludeStubProps(newHomebrew);
|
||||
newHomebrew.googleId = googleId;
|
||||
} else {
|
||||
// Compress brew text to binary before saving
|
||||
newHomebrew.textBin = zlib.deflateRawSync(newHomebrew.text);
|
||||
// Delete the non-binary text field since it's not needed anymore
|
||||
newHomebrew.text = undefined;
|
||||
}
|
||||
|
||||
saved = await newHomebrew.save()
|
||||
.catch((err)=>{
|
||||
console.error(err, err.toString(), err.stack);
|
||||
throw `Error while creating new brew, ${err.toString()}`;
|
||||
});
|
||||
if(!saved) return;
|
||||
saved = saved.toObject();
|
||||
|
||||
res.status(200).send(saved);
|
||||
};
|
||||
|
||||
const updateBrew = async (req, res)=>{
|
||||
// Initialize brew from request and body, destructure query params, set a constant for the google id, and set the initial value for the after-save method
|
||||
let brew = _.assign(req.brew, excludePropsFromUpdate(req.body));
|
||||
const { saveToGoogle, removeFromGoogle } = req.query;
|
||||
const googleId = brew.googleId;
|
||||
let afterSave = async ()=>true;
|
||||
|
||||
brew.text = mergeBrewText(brew);
|
||||
|
||||
if(brew.googleId && removeFromGoogle) {
|
||||
// If the google id exists and we're removing it from google, set afterSave to delete the google brew and mark the brew's google id as undefined
|
||||
afterSave = async ()=>{
|
||||
return await deleteGoogleBrew(req.account, googleId, brew.editId, res)
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
res.status(err?.status || err?.response?.status || 500).send(err.message || err);
|
||||
});
|
||||
};
|
||||
|
||||
brew.googleId = undefined;
|
||||
} else if(!brew.googleId && saveToGoogle) {
|
||||
// If we don't have a google id and the user wants to save to google, create the google brew and set the google id on the brew
|
||||
brew.googleId = await newGoogleBrew(req.account, excludeGoogleProps(brew), res)
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
res.status(err.status || err.response.status).send(err.message || err);
|
||||
});
|
||||
if(!brew.googleId) return;
|
||||
} else if(brew.googleId) {
|
||||
// If the google id exists and no other actions are being performed, update the google brew
|
||||
const updated = await GoogleActions.updateGoogleBrew(excludeGoogleProps(brew))
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
res.status(err?.response?.status || 500).send(err);
|
||||
});
|
||||
if(!updated) return;
|
||||
}
|
||||
|
||||
if(brew.googleId) {
|
||||
// If the google id exists after all those actions, exclude the props that are stored in google and aren't needed for rendering the brew items
|
||||
excludeStubProps(brew);
|
||||
} else {
|
||||
// Compress brew text to binary before saving
|
||||
brew.textBin = zlib.deflateRawSync(brew.text);
|
||||
// Delete the non-binary text field since it's not needed anymore
|
||||
brew.text = undefined;
|
||||
}
|
||||
brew.updatedAt = new Date();
|
||||
|
||||
if(req.account) {
|
||||
brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
|
||||
}
|
||||
|
||||
// Fetch the brew from the database again (if it existed there to begin with), and assign the existing brew to it
|
||||
brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew);
|
||||
|
||||
if(!brew.markModified) {
|
||||
// If it wasn't in the database, create a new db brew
|
||||
brew = new HomebrewModel(brew);
|
||||
}
|
||||
|
||||
brew.markModified('authors');
|
||||
brew.markModified('systems');
|
||||
|
||||
// Save the database brew
|
||||
const saved = await brew.save()
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
res.status(err.status || 500).send(err.message || 'Unable to save brew to Homebrewery database');
|
||||
});
|
||||
if(!saved) return;
|
||||
// Call and wait for afterSave to complete
|
||||
const after = await afterSave();
|
||||
if(!after) return;
|
||||
|
||||
res.status(200).send(saved);
|
||||
};
|
||||
|
||||
const deleteGoogleBrew = async (account, id, editId, res)=>{
|
||||
const auth = await GoogleActions.authCheck(account, res);
|
||||
await GoogleActions.deleteGoogleBrew(auth, id, editId);
|
||||
return true;
|
||||
};
|
||||
|
||||
const deleteBrew = async (req, res)=>{
|
||||
let brew = req.brew;
|
||||
const { googleId, editId } = brew;
|
||||
const account = req.account;
|
||||
const isOwner = account && (brew.authors.length === 0 || brew.authors[0] === account.username);
|
||||
// If the user is the owner and the file is saved to google, mark the google brew for deletion
|
||||
const shouldDeleteGoogleBrew = googleId && isOwner;
|
||||
|
||||
if(brew._id) {
|
||||
brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew);
|
||||
if(account) {
|
||||
// Remove current user as author
|
||||
brew.authors = _.pull(brew.authors, account.username);
|
||||
brew.markModified('authors');
|
||||
}
|
||||
|
||||
if(brew.authors.length === 0) {
|
||||
// Delete brew if there are no authors left
|
||||
await brew.remove()
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
throw { status: 500, message: 'Error while removing' };
|
||||
});
|
||||
} else {
|
||||
if(shouldDeleteGoogleBrew) {
|
||||
// When there are still authors remaining, we delete the google brew but store the full brew in the Homebrewery database
|
||||
brew.googleId = undefined;
|
||||
brew.textBin = zlib.deflateRawSync(brew.text);
|
||||
brew.text = undefined;
|
||||
If you believe you should have access to this brew, ask the file owner to invite you as an author by opening the brew, viewing the Properties tab, and adding your username to the "invited authors" list. You can then try to access this document again.`;
|
||||
}
|
||||
|
||||
// Otherwise, save the brew with updated author list
|
||||
await brew.save()
|
||||
.catch((err)=>{
|
||||
throw { status: 500, message: err };
|
||||
});
|
||||
// If after all of that we still don't have a brew, throw an exception
|
||||
if(!stub && !stubOnly) {
|
||||
throw 'Brew not found in Homebrewery database or Google Drive';
|
||||
}
|
||||
|
||||
// Clean up brew: fill in missing fields with defaults / fix old invalid values
|
||||
if(stub) {
|
||||
stub.tags = stub.tags || undefined; // Clear empty strings
|
||||
stub.renderer = stub.renderer || undefined; // Clear empty strings
|
||||
stub = _.defaults(stub, DEFAULT_BREW_LOAD); // Fill in blank fields
|
||||
}
|
||||
|
||||
req.brew = stub ?? {};
|
||||
next();
|
||||
};
|
||||
},
|
||||
mergeBrewText : (brew)=>{
|
||||
let text = brew.text;
|
||||
if(brew.style !== undefined) {
|
||||
text = `\`\`\`css\n` +
|
||||
`${brew.style || ''}\n` +
|
||||
`\`\`\`\n\n` +
|
||||
`${text}`;
|
||||
}
|
||||
}
|
||||
if(shouldDeleteGoogleBrew) {
|
||||
const deleted = await deleteGoogleBrew(account, googleId, editId, res)
|
||||
const metadata = _.pick(brew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme']);
|
||||
text = `\`\`\`metadata\n` +
|
||||
`${yaml.dump(metadata)}\n` +
|
||||
`\`\`\`\n\n` +
|
||||
`${text}`;
|
||||
return text;
|
||||
},
|
||||
getGoodBrewTitle : (text)=>{
|
||||
const tokens = Markdown.marked.lexer(text);
|
||||
return (tokens.find((token)=>token.type === 'heading' || token.type === 'paragraph')?.text || 'No Title')
|
||||
.slice(0, MAX_TITLE_LENGTH);
|
||||
},
|
||||
excludePropsFromUpdate : (brew)=>{
|
||||
// Remove undesired properties
|
||||
const modified = _.clone(brew);
|
||||
const propsToExclude = ['_id', 'views', 'lastViewed'];
|
||||
for (const prop of propsToExclude) {
|
||||
delete modified[prop];
|
||||
}
|
||||
return modified;
|
||||
},
|
||||
excludeGoogleProps : (brew)=>{
|
||||
const modified = _.clone(brew);
|
||||
const propsToExclude = ['version', 'tags', 'systems', 'published', 'authors', 'owner', 'views', 'thumbnail'];
|
||||
for (const prop of propsToExclude) {
|
||||
delete modified[prop];
|
||||
}
|
||||
return modified;
|
||||
},
|
||||
excludeStubProps : (brew)=>{
|
||||
const propsToExclude = ['text', 'textBin', 'renderer', 'pageCount'];
|
||||
for (const prop of propsToExclude) {
|
||||
brew[prop] = undefined;
|
||||
}
|
||||
return brew;
|
||||
},
|
||||
beforeNewSave : (account, brew)=>{
|
||||
if(!brew.title) {
|
||||
brew.title = api.getGoodBrewTitle(brew.text);
|
||||
}
|
||||
|
||||
brew.authors = (account) ? [account.username] : [];
|
||||
brew.text = api.mergeBrewText(brew);
|
||||
|
||||
_.defaults(brew, DEFAULT_BREW);
|
||||
},
|
||||
newGoogleBrew : async (account, brew, res)=>{
|
||||
const oAuth2Client = GoogleActions.authCheck(account, res);
|
||||
|
||||
const newBrew = api.excludeGoogleProps(brew);
|
||||
|
||||
return await GoogleActions.newGoogleBrew(oAuth2Client, newBrew);
|
||||
},
|
||||
newBrew : async (req, res)=>{
|
||||
const brew = req.body;
|
||||
const { saveToGoogle } = req.query;
|
||||
|
||||
delete brew.editId;
|
||||
delete brew.shareId;
|
||||
delete brew.googleId;
|
||||
|
||||
api.beforeNewSave(req.account, brew);
|
||||
|
||||
const newHomebrew = new HomebrewModel(brew);
|
||||
newHomebrew.editId = nanoid(12);
|
||||
newHomebrew.shareId = nanoid(12);
|
||||
|
||||
let googleId, saved;
|
||||
if(saveToGoogle) {
|
||||
googleId = await api.newGoogleBrew(req.account, newHomebrew, res)
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
res.status(err?.status || err?.response?.status || 500).send(err?.message || err);
|
||||
});
|
||||
if(!googleId) return;
|
||||
api.excludeStubProps(newHomebrew);
|
||||
newHomebrew.googleId = googleId;
|
||||
} else {
|
||||
// Compress brew text to binary before saving
|
||||
newHomebrew.textBin = zlib.deflateRawSync(newHomebrew.text);
|
||||
// Delete the non-binary text field since it's not needed anymore
|
||||
newHomebrew.text = undefined;
|
||||
}
|
||||
|
||||
saved = await newHomebrew.save()
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
res.status(500).send(err);
|
||||
console.error(err, err.toString(), err.stack);
|
||||
throw `Error while creating new brew, ${err.toString()}`;
|
||||
});
|
||||
if(!deleted) return;
|
||||
if(!saved) return;
|
||||
saved = saved.toObject();
|
||||
|
||||
res.status(200).send(saved);
|
||||
},
|
||||
updateBrew : async (req, res)=>{
|
||||
// Initialize brew from request and body, destructure query params, and set the initial value for the after-save method
|
||||
const brewFromClient = api.excludePropsFromUpdate(req.body);
|
||||
const brewFromServer = req.brew;
|
||||
if(brewFromServer.version && brewFromClient.version && brewFromServer.version > brewFromClient.version) {
|
||||
console.log(`Version mismatch on brew ${brewFromClient.editId}`);
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
return res.status(409).send(JSON.stringify({ message: `The brew has been changed on a different device. Please save your changes elsewhere, refresh, and try again.` }));
|
||||
}
|
||||
|
||||
let brew = _.assign(brewFromServer, brewFromClient);
|
||||
const googleId = brew.googleId;
|
||||
const { saveToGoogle, removeFromGoogle } = req.query;
|
||||
let afterSave = async ()=>true;
|
||||
|
||||
brew.text = api.mergeBrewText(brew);
|
||||
|
||||
if(brew.googleId && removeFromGoogle) {
|
||||
// If the google id exists and we're removing it from google, set afterSave to delete the google brew and mark the brew's google id as undefined
|
||||
afterSave = async ()=>{
|
||||
return await api.deleteGoogleBrew(req.account, googleId, brew.editId, res)
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
res.status(err?.status || err?.response?.status || 500).send(err.message || err);
|
||||
});
|
||||
};
|
||||
|
||||
brew.googleId = undefined;
|
||||
} else if(!brew.googleId && saveToGoogle) {
|
||||
// If we don't have a google id and the user wants to save to google, create the google brew and set the google id on the brew
|
||||
brew.googleId = await api.newGoogleBrew(req.account, api.excludeGoogleProps(brew), res)
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
res.status(err.status || err.response.status).send(err.message || err);
|
||||
});
|
||||
if(!brew.googleId) return;
|
||||
} else if(brew.googleId) {
|
||||
// If the google id exists and no other actions are being performed, update the google brew
|
||||
const updated = await GoogleActions.updateGoogleBrew(api.excludeGoogleProps(brew))
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
res.status(err?.response?.status || 500).send(err);
|
||||
});
|
||||
if(!updated) return;
|
||||
}
|
||||
|
||||
if(brew.googleId) {
|
||||
// If the google id exists after all those actions, exclude the props that are stored in google and aren't needed for rendering the brew items
|
||||
api.excludeStubProps(brew);
|
||||
} else {
|
||||
// Compress brew text to binary before saving
|
||||
brew.textBin = zlib.deflateRawSync(brew.text);
|
||||
// Delete the non-binary text field since it's not needed anymore
|
||||
brew.text = undefined;
|
||||
}
|
||||
brew.updatedAt = new Date();
|
||||
brew.version = (brew.version || 1) + 1;
|
||||
|
||||
if(req.account) {
|
||||
brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
|
||||
brew.invitedAuthors = _.uniq(_.filter(brew.invitedAuthors, (a)=>req.account.username !== a));
|
||||
}
|
||||
|
||||
// define a function to catch our save errors
|
||||
const saveError = (err)=>{
|
||||
console.error(err);
|
||||
res.status(err.status || 500).send(err.message || 'Unable to save brew to Homebrewery database');
|
||||
};
|
||||
let saved;
|
||||
if(!brew._id) {
|
||||
// if the brew does not have a stub id, create and save it, then write the new value back to the brew.
|
||||
saved = await new HomebrewModel(brew).save().catch(saveError);
|
||||
} else {
|
||||
// if the brew does have a stub id, update it using the stub id as the key.
|
||||
brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew);
|
||||
saved = await brew.save()
|
||||
.catch(saveError);
|
||||
}
|
||||
if(!saved) return;
|
||||
// Call and wait for afterSave to complete
|
||||
const after = await afterSave();
|
||||
if(!after) return;
|
||||
|
||||
res.status(200).send(saved);
|
||||
},
|
||||
deleteGoogleBrew : async (account, id, editId, res)=>{
|
||||
const auth = await GoogleActions.authCheck(account, res);
|
||||
await GoogleActions.deleteGoogleBrew(auth, id, editId);
|
||||
return true;
|
||||
},
|
||||
deleteBrew : async (req, res, next)=>{
|
||||
// Delete an orphaned stub if its Google brew doesn't exist
|
||||
try {
|
||||
await api.getBrew('edit')(req, res, ()=>{});
|
||||
} catch (err) {
|
||||
const { id, googleId } = api.getId(req);
|
||||
console.warn(`No google brew found for id ${googleId}, the stub with id ${id} will be deleted.`);
|
||||
await HomebrewModel.deleteOne({ editId: id });
|
||||
return next();
|
||||
}
|
||||
|
||||
let brew = req.brew;
|
||||
const { googleId, editId } = brew;
|
||||
const account = req.account;
|
||||
const isOwner = account && (brew.authors.length === 0 || brew.authors[0] === account.username);
|
||||
// If the user is the owner and the file is saved to google, mark the google brew for deletion
|
||||
const shouldDeleteGoogleBrew = googleId && isOwner;
|
||||
|
||||
if(brew._id) {
|
||||
brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew);
|
||||
if(account) {
|
||||
// Remove current user as author
|
||||
brew.authors = _.pull(brew.authors, account.username);
|
||||
}
|
||||
|
||||
if(brew.authors.length === 0) {
|
||||
// Delete brew if there are no authors left
|
||||
await brew.remove()
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
throw { status: 500, message: 'Error while removing' };
|
||||
});
|
||||
} else {
|
||||
if(shouldDeleteGoogleBrew) {
|
||||
// When there are still authors remaining, we delete the google brew but store the full brew in the Homebrewery database
|
||||
brew.googleId = undefined;
|
||||
brew.textBin = zlib.deflateRawSync(brew.text);
|
||||
brew.text = undefined;
|
||||
}
|
||||
|
||||
// Otherwise, save the brew with updated author list
|
||||
await brew.save()
|
||||
.catch((err)=>{
|
||||
throw { status: 500, message: err };
|
||||
});
|
||||
}
|
||||
}
|
||||
if(shouldDeleteGoogleBrew) {
|
||||
const deleted = await api.deleteGoogleBrew(account, googleId, editId, res)
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
res.status(500).send(err);
|
||||
});
|
||||
if(!deleted) return;
|
||||
}
|
||||
|
||||
res.status(204).send();
|
||||
}
|
||||
|
||||
res.status(204).send();
|
||||
};
|
||||
|
||||
router.post('/api', asyncHandler(newBrew));
|
||||
router.put('/api/:id', asyncHandler(getBrew('edit')), asyncHandler(updateBrew));
|
||||
router.put('/api/update/:id', asyncHandler(getBrew('edit')), asyncHandler(updateBrew));
|
||||
router.delete('/api/:id', asyncHandler(getBrew('edit')), asyncHandler(deleteBrew));
|
||||
router.get('/api/remove/:id', asyncHandler(getBrew('edit')), asyncHandler(deleteBrew));
|
||||
router.use('/api', require('./middleware/check-client-version.js'));
|
||||
router.post('/api', asyncHandler(api.newBrew));
|
||||
router.put('/api/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
||||
router.put('/api/update/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
||||
router.delete('/api/:id', asyncHandler(api.deleteBrew));
|
||||
router.get('/api/remove/:id', asyncHandler(api.deleteBrew));
|
||||
|
||||
module.exports = {
|
||||
homebrewApi : router,
|
||||
getBrew
|
||||
};
|
||||
module.exports = api;
|
||||
|
||||
748
server/homebrew.api.spec.js
Normal file
@@ -0,0 +1,748 @@
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
describe('Tests for api', ()=>{
|
||||
let api;
|
||||
let google;
|
||||
let model;
|
||||
let hbBrew;
|
||||
let googleBrew;
|
||||
let res;
|
||||
|
||||
let modelBrew;
|
||||
let saveFunc;
|
||||
let removeFunc;
|
||||
let saved;
|
||||
|
||||
beforeEach(()=>{
|
||||
saved = undefined;
|
||||
saveFunc = jest.fn(async function() {
|
||||
saved = { ...this, _id: '1' };
|
||||
return saved;
|
||||
});
|
||||
removeFunc = jest.fn(async function() {});
|
||||
|
||||
modelBrew = (brew)=>({
|
||||
...brew,
|
||||
save : saveFunc,
|
||||
remove : removeFunc,
|
||||
toObject : function() {
|
||||
delete this.save;
|
||||
delete this.toObject;
|
||||
delete this.remove;
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
google = require('./googleActions.js');
|
||||
model = require('./homebrew.model.js').model;
|
||||
|
||||
jest.mock('./googleActions.js');
|
||||
google.authCheck = jest.fn(()=>'client');
|
||||
google.newGoogleBrew = jest.fn(()=>'id');
|
||||
google.deleteGoogleBrew = jest.fn(()=>true);
|
||||
|
||||
jest.mock('./homebrew.model.js');
|
||||
model.mockImplementation((brew)=>modelBrew(brew));
|
||||
|
||||
res = {
|
||||
status : jest.fn(()=>res),
|
||||
send : jest.fn(()=>{})
|
||||
};
|
||||
|
||||
api = require('./homebrew.api');
|
||||
|
||||
hbBrew = {
|
||||
text : `brew text`,
|
||||
style : 'hello yes i am css',
|
||||
title : 'some title',
|
||||
description : 'this is a description',
|
||||
tags : ['something', 'fun'],
|
||||
systems : ['D&D 5e'],
|
||||
renderer : 'v3',
|
||||
theme : 'phb',
|
||||
published : true,
|
||||
authors : ['1', '2'],
|
||||
owner : '1',
|
||||
thumbnail : '',
|
||||
_id : 'mongoid',
|
||||
editId : 'abcdefg',
|
||||
shareId : 'hijklmnop',
|
||||
views : 1,
|
||||
lastViewed : new Date(),
|
||||
version : 1,
|
||||
pageCount : 1,
|
||||
textBin : ''
|
||||
};
|
||||
googleBrew = {
|
||||
...hbBrew,
|
||||
googleId : '12345'
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(()=>{
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('getId', ()=>{
|
||||
it('should return only id if google id is not present', ()=>{
|
||||
const { id, googleId } = api.getId({
|
||||
params : {
|
||||
id : 'abcdefgh'
|
||||
}
|
||||
});
|
||||
|
||||
expect(id).toEqual('abcdefgh');
|
||||
expect(googleId).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return id and google id from request body', ()=>{
|
||||
const { id, googleId } = api.getId({
|
||||
params : {
|
||||
id : 'abcdefgh'
|
||||
},
|
||||
body : {
|
||||
googleId : '12345'
|
||||
}
|
||||
});
|
||||
|
||||
expect(id).toEqual('abcdefgh');
|
||||
expect(googleId).toEqual('12345');
|
||||
});
|
||||
|
||||
it('should return id and google id from params', ()=>{
|
||||
const { id, googleId } = api.getId({
|
||||
params : {
|
||||
id : '123456789012abcdefghijkl'
|
||||
}
|
||||
});
|
||||
|
||||
expect(id).toEqual('abcdefghijkl');
|
||||
expect(googleId).toEqual('123456789012');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBrew', ()=>{
|
||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||
const notFoundError = 'Brew not found in Homebrewery database or Google Drive';
|
||||
|
||||
it('returns middleware', ()=>{
|
||||
const getFn = api.getBrew('share');
|
||||
expect(getFn).toBeInstanceOf(Function);
|
||||
});
|
||||
|
||||
it('should fetch from mongoose', async ()=>{
|
||||
const testBrew = { title: 'test brew', authors: [] };
|
||||
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||
model.get = jest.fn(()=>toBrewPromise(testBrew));
|
||||
|
||||
const fn = api.getBrew('share', true);
|
||||
const req = { brew: {} };
|
||||
const next = jest.fn();
|
||||
await fn(req, null, next);
|
||||
|
||||
expect(req.brew).toEqual(testBrew);
|
||||
expect(next).toHaveBeenCalled();
|
||||
expect(api.getId).toHaveBeenCalledWith(req);
|
||||
expect(model.get).toHaveBeenCalledWith({ shareId: '1' });
|
||||
});
|
||||
|
||||
it('should handle mongoose error', async ()=>{
|
||||
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||
model.get = jest.fn(()=>new Promise((_, rej)=>rej('Unable to find brew')));
|
||||
|
||||
const fn = api.getBrew('share', false);
|
||||
const req = { brew: {} };
|
||||
const next = jest.fn();
|
||||
let err;
|
||||
try {
|
||||
await fn(req, null, next);
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
expect(err).toEqual(notFoundError);
|
||||
expect(req.brew).toEqual({});
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
expect(api.getId).toHaveBeenCalledWith(req);
|
||||
expect(model.get).toHaveBeenCalledWith({ shareId: '1' });
|
||||
});
|
||||
|
||||
it('changes tags from string to array', async ()=>{
|
||||
const testBrew = { title: 'test brew', authors: [], tags: '' };
|
||||
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||
model.get = jest.fn(()=>toBrewPromise(testBrew));
|
||||
|
||||
const fn = api.getBrew('share', true);
|
||||
const req = { brew: {} };
|
||||
const next = jest.fn();
|
||||
await fn(req, null, next);
|
||||
|
||||
expect(req.brew.tags).toEqual([]);
|
||||
expect(next).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws if invalid author', async ()=>{
|
||||
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||
model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: ['a'] }));
|
||||
|
||||
const fn = api.getBrew('edit', true);
|
||||
const req = { brew: {} };
|
||||
|
||||
let err;
|
||||
try {
|
||||
await fn(req, null, null);
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
expect(err).toEqual(`The current logged in user does not have editor access to this brew.
|
||||
|
||||
If you believe you should have access to this brew, ask the file owner to invite you as an author by opening the brew, viewing the Properties tab, and adding your username to the "invited authors" list. You can then try to access this document again.`);
|
||||
});
|
||||
|
||||
it('does not throw if no authors', async ()=>{
|
||||
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||
model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: [] }));
|
||||
|
||||
const fn = api.getBrew('edit', true);
|
||||
const req = { brew: {} };
|
||||
const next = jest.fn();
|
||||
await fn(req, null, next);
|
||||
|
||||
expect(next).toHaveBeenCalled();
|
||||
expect(req.brew.title).toEqual('test brew');
|
||||
expect(req.brew.authors).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not throw if valid author', async ()=>{
|
||||
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||
model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: ['a'] }));
|
||||
|
||||
const fn = api.getBrew('edit', true);
|
||||
const req = { brew: {}, account: { username: 'a' } };
|
||||
const next = jest.fn();
|
||||
await fn(req, null, next);
|
||||
|
||||
expect(next).toHaveBeenCalled();
|
||||
expect(req.brew.title).toEqual('test brew');
|
||||
expect(req.brew.authors).toEqual(['a']);
|
||||
});
|
||||
|
||||
it('fetches google brew if needed', async()=>{
|
||||
const stubBrew = { title: 'test brew', authors: ['a'] };
|
||||
const googleBrew = { title: 'test google brew', text: 'brew text' };
|
||||
api.getId = jest.fn(()=>({ id: '1', googleId: '2' }));
|
||||
model.get = jest.fn(()=>toBrewPromise(stubBrew));
|
||||
google.getGoogleBrew = jest.fn(()=>new Promise((res)=>res(googleBrew)));
|
||||
|
||||
const fn = api.getBrew('share', false);
|
||||
const req = { brew: {} };
|
||||
const next = jest.fn();
|
||||
await fn(req, null, next);
|
||||
|
||||
expect(req.brew).toEqual({
|
||||
title : 'test google brew',
|
||||
authors : ['a'],
|
||||
text : 'brew text',
|
||||
stubbed : true,
|
||||
description : '',
|
||||
editId : undefined,
|
||||
pageCount : 1,
|
||||
published : false,
|
||||
renderer : 'legacy',
|
||||
shareId : undefined,
|
||||
systems : [],
|
||||
tags : [],
|
||||
theme : '5ePHB',
|
||||
thumbnail : '',
|
||||
textBin : undefined,
|
||||
version : undefined,
|
||||
createdAt : undefined,
|
||||
gDrive : false,
|
||||
style : undefined,
|
||||
trashed : false,
|
||||
updatedAt : undefined
|
||||
});
|
||||
expect(next).toHaveBeenCalled();
|
||||
expect(api.getId).toHaveBeenCalledWith(req);
|
||||
expect(model.get).toHaveBeenCalledWith({ shareId: '1' });
|
||||
expect(google.getGoogleBrew).toHaveBeenCalledWith('2', '1', 'share');
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeBrewText', ()=>{
|
||||
it('should set metadata and no style if it is not present', ()=>{
|
||||
const result = api.mergeBrewText({
|
||||
text : `brew`,
|
||||
title : 'some title',
|
||||
description : 'this is a description',
|
||||
tags : ['something', 'fun'],
|
||||
systems : ['D&D 5e'],
|
||||
renderer : 'v3',
|
||||
theme : 'phb',
|
||||
googleId : '12345'
|
||||
});
|
||||
|
||||
expect(result).toEqual(`\`\`\`metadata
|
||||
title: some title
|
||||
description: this is a description
|
||||
tags:
|
||||
- something
|
||||
- fun
|
||||
systems:
|
||||
- D&D 5e
|
||||
renderer: v3
|
||||
theme: phb
|
||||
|
||||
\`\`\`
|
||||
|
||||
brew`);
|
||||
});
|
||||
|
||||
it('should set metadata and style', ()=>{
|
||||
const result = api.mergeBrewText({
|
||||
text : `brew`,
|
||||
style : 'hello yes i am css',
|
||||
title : 'some title',
|
||||
description : 'this is a description',
|
||||
tags : ['something', 'fun'],
|
||||
systems : ['D&D 5e'],
|
||||
renderer : 'v3',
|
||||
theme : 'phb',
|
||||
googleId : '12345'
|
||||
});
|
||||
|
||||
expect(result).toEqual(`\`\`\`metadata
|
||||
title: some title
|
||||
description: this is a description
|
||||
tags:
|
||||
- something
|
||||
- fun
|
||||
systems:
|
||||
- D&D 5e
|
||||
renderer: v3
|
||||
theme: phb
|
||||
|
||||
\`\`\`
|
||||
|
||||
\`\`\`css
|
||||
hello yes i am css
|
||||
\`\`\`
|
||||
|
||||
brew`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('exclusion methods', ()=>{
|
||||
it('excludePropsFromUpdate removes the correct keys', ()=>{
|
||||
const sent = Object.assign({}, googleBrew);
|
||||
const result = api.excludePropsFromUpdate(sent);
|
||||
|
||||
expect(sent).toEqual(googleBrew);
|
||||
expect(result._id).toBeUndefined();
|
||||
expect(result.views).toBeUndefined();
|
||||
expect(result.lastViewed).toBeUndefined();
|
||||
});
|
||||
|
||||
it('excludeGoogleProps removes the correct keys', ()=>{
|
||||
const sent = Object.assign({}, googleBrew);
|
||||
const result = api.excludeGoogleProps(sent);
|
||||
|
||||
expect(sent).toEqual(googleBrew);
|
||||
expect(result.tags).toBeUndefined();
|
||||
expect(result.systems).toBeUndefined();
|
||||
expect(result.published).toBeUndefined();
|
||||
expect(result.authors).toBeUndefined();
|
||||
expect(result.owner).toBeUndefined();
|
||||
expect(result.views).toBeUndefined();
|
||||
expect(result.thumbnail).toBeUndefined();
|
||||
expect(result.version).toBeUndefined();
|
||||
});
|
||||
|
||||
it('excludeStubProps removes the correct keys from the original object', ()=>{
|
||||
const sent = Object.assign({}, googleBrew);
|
||||
const result = api.excludeStubProps(sent);
|
||||
|
||||
expect(sent).not.toEqual(googleBrew);
|
||||
expect(result.text).toBeUndefined();
|
||||
expect(result.textBin).toBeUndefined();
|
||||
expect(result.renderer).toBeUndefined();
|
||||
expect(result.pageCount).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('beforeNewSave', ()=>{
|
||||
it('sets the title if none', ()=>{
|
||||
const brew = {
|
||||
...hbBrew,
|
||||
title : undefined
|
||||
};
|
||||
api.beforeNewSave({}, brew);
|
||||
|
||||
expect(brew.title).toEqual('brew text');
|
||||
});
|
||||
|
||||
it('does not override the title if present', ()=>{
|
||||
const brew = {
|
||||
...hbBrew,
|
||||
title : 'test'
|
||||
};
|
||||
api.beforeNewSave({}, brew);
|
||||
|
||||
expect(brew.title).toEqual('test');
|
||||
});
|
||||
|
||||
it('does not set authors if account missing username', ()=>{
|
||||
api.beforeNewSave({}, hbBrew);
|
||||
|
||||
expect(hbBrew.authors).toEqual([]);
|
||||
});
|
||||
|
||||
it('sets authors if account has username', ()=>{
|
||||
api.beforeNewSave({ username: 'hi' }, hbBrew);
|
||||
|
||||
expect(hbBrew.authors).toEqual(['hi']);
|
||||
});
|
||||
|
||||
it('merges brew text', ()=>{
|
||||
api.mergeBrewText = jest.fn(()=>'merged');
|
||||
api.beforeNewSave({}, hbBrew);
|
||||
|
||||
expect(api.mergeBrewText).toHaveBeenCalled();
|
||||
expect(hbBrew.text).toEqual('merged');
|
||||
});
|
||||
});
|
||||
|
||||
describe('newGoogleBrew', ()=>{
|
||||
it('should call the correct methods', ()=>{
|
||||
api.excludeGoogleProps = jest.fn(()=>'newBrew');
|
||||
|
||||
const acct = { username: 'test' };
|
||||
const brew = { title: 'test title' };
|
||||
api.newGoogleBrew(acct, brew, res);
|
||||
|
||||
expect(google.authCheck).toHaveBeenCalledWith(acct, res);
|
||||
expect(api.excludeGoogleProps).toHaveBeenCalledWith(brew);
|
||||
expect(google.newGoogleBrew).toHaveBeenCalledWith('client', 'newBrew');
|
||||
});
|
||||
});
|
||||
|
||||
describe('newBrew', ()=>{
|
||||
it('should set up a default brew via Homebrew model', async ()=>{
|
||||
await api.newBrew({ body: { text: 'asdf' }, query: {}, account: { username: 'test user' } }, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.send).toHaveBeenCalledWith({
|
||||
_id : '1',
|
||||
authors : ['test user'],
|
||||
createdAt : undefined,
|
||||
description : '',
|
||||
editId : expect.any(String),
|
||||
gDrive : false,
|
||||
pageCount : 1,
|
||||
published : false,
|
||||
renderer : 'V3',
|
||||
shareId : expect.any(String),
|
||||
style : undefined,
|
||||
systems : [],
|
||||
tags : [],
|
||||
text : undefined,
|
||||
textBin : expect.objectContaining({}),
|
||||
theme : '5ePHB',
|
||||
thumbnail : '',
|
||||
title : 'asdf',
|
||||
trashed : false,
|
||||
updatedAt : undefined
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove edit/share/google ids', async ()=>{
|
||||
await api.newBrew({ body: { editId: '1234', shareId: '1234', googleId: '1234', text: 'asdf', title: '' }, query: {} }, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.send).toHaveBeenCalled();
|
||||
const sent = res.send.mock.calls[0][0];
|
||||
expect(sent.editId).not.toEqual('1234');
|
||||
expect(sent.shareId).not.toEqual('1234');
|
||||
expect(sent.googleId).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle mongo error', async ()=>{
|
||||
saveFunc = jest.fn(async function() {
|
||||
throw 'err';
|
||||
});
|
||||
model.mockImplementation((brew)=>modelBrew(brew));
|
||||
|
||||
let err;
|
||||
try {
|
||||
await api.newBrew({ body: { editId: '1234', shareId: '1234', googleId: '1234', text: 'asdf', title: '' }, query: {} }, res);
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
expect(res.send).not.toHaveBeenCalled();
|
||||
expect(err).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should save to google if requested', async()=>{
|
||||
await api.newBrew({ body: { text: 'asdf', title: '' }, query: { saveToGoogle: true }, account: { username: 'test user' } }, res);
|
||||
|
||||
expect(google.newGoogleBrew).toHaveBeenCalled();
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.send).toHaveBeenCalledWith({
|
||||
_id : '1',
|
||||
authors : ['test user'],
|
||||
createdAt : undefined,
|
||||
description : '',
|
||||
editId : expect.any(String),
|
||||
gDrive : false,
|
||||
pageCount : undefined,
|
||||
published : false,
|
||||
renderer : undefined,
|
||||
shareId : expect.any(String),
|
||||
googleId : expect.any(String),
|
||||
style : undefined,
|
||||
systems : [],
|
||||
tags : [],
|
||||
text : undefined,
|
||||
textBin : undefined,
|
||||
theme : '5ePHB',
|
||||
thumbnail : '',
|
||||
title : 'asdf',
|
||||
trashed : false,
|
||||
updatedAt : undefined
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle google error', async()=>{
|
||||
google.newGoogleBrew = jest.fn(()=>{
|
||||
throw 'err';
|
||||
});
|
||||
await api.newBrew({ body: { text: 'asdf', title: '' }, query: { saveToGoogle: true }, account: { username: 'test user' } }, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(500);
|
||||
expect(res.send).toHaveBeenCalledWith('err');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteGoogleBrew', ()=>{
|
||||
it('should check auth and delete brew', async ()=>{
|
||||
const result = await api.deleteGoogleBrew({ username: 'test user' }, 'id', 'editId', res);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(google.authCheck).toHaveBeenCalledWith({ username: 'test user' }, expect.objectContaining({}));
|
||||
expect(google.deleteGoogleBrew).toHaveBeenCalledWith('client', 'id', 'editId');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteBrew', ()=>{
|
||||
it('should handle case where fetching the brew returns an error', async ()=>{
|
||||
api.getBrew = jest.fn(()=>async ()=>{ throw 'err'; });
|
||||
api.getId = jest.fn(()=>({ id: '1', googleId: '2' }));
|
||||
model.deleteOne = jest.fn(async ()=>{});
|
||||
const next = jest.fn(()=>{});
|
||||
|
||||
await api.deleteBrew(null, null, next);
|
||||
|
||||
expect(next).toHaveBeenCalled();
|
||||
expect(model.deleteOne).toHaveBeenCalledWith({ editId: '1' });
|
||||
});
|
||||
|
||||
it('should delete if no authors', async ()=>{
|
||||
const brew = {
|
||||
...hbBrew,
|
||||
authors : []
|
||||
};
|
||||
api.getBrew = jest.fn(()=>async (req)=>{
|
||||
req.brew = brew;
|
||||
});
|
||||
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||
const req = {};
|
||||
|
||||
await api.deleteBrew(req, res);
|
||||
|
||||
expect(api.getBrew).toHaveBeenCalled();
|
||||
expect(model.findOne).toHaveBeenCalled();
|
||||
expect(removeFunc).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw on delete error', async ()=>{
|
||||
const brew = {
|
||||
...hbBrew,
|
||||
authors : []
|
||||
};
|
||||
api.getBrew = jest.fn(()=>async (req)=>{
|
||||
req.brew = brew;
|
||||
});
|
||||
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||
removeFunc = jest.fn(async ()=>{ throw 'err'; });
|
||||
const req = {};
|
||||
|
||||
let err;
|
||||
try {
|
||||
await api.deleteBrew(req, res);
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
expect(err).not.toBeUndefined();
|
||||
expect(api.getBrew).toHaveBeenCalled();
|
||||
expect(model.findOne).toHaveBeenCalled();
|
||||
expect(removeFunc).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should delete when one author', async ()=>{
|
||||
const brew = {
|
||||
...hbBrew,
|
||||
authors : ['test']
|
||||
};
|
||||
api.getBrew = jest.fn(()=>async (req)=>{
|
||||
req.brew = brew;
|
||||
});
|
||||
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||
const req = { account: { username: 'test' } };
|
||||
|
||||
await api.deleteBrew(req, res);
|
||||
|
||||
expect(api.getBrew).toHaveBeenCalled();
|
||||
expect(model.findOne).toHaveBeenCalled();
|
||||
expect(removeFunc).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should remove one author when multiple present', async ()=>{
|
||||
const brew = {
|
||||
...hbBrew,
|
||||
authors : ['test', 'test2']
|
||||
};
|
||||
api.getBrew = jest.fn(()=>async (req)=>{
|
||||
req.brew = brew;
|
||||
});
|
||||
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||
const req = { account: { username: 'test' } };
|
||||
|
||||
await api.deleteBrew(req, res);
|
||||
|
||||
expect(api.getBrew).toHaveBeenCalled();
|
||||
expect(model.findOne).toHaveBeenCalled();
|
||||
expect(removeFunc).not.toHaveBeenCalled();
|
||||
expect(saveFunc).toHaveBeenCalled();
|
||||
expect(saved.authors).toEqual(['test2']);
|
||||
});
|
||||
|
||||
it('should handle save error', async ()=>{
|
||||
const brew = {
|
||||
...hbBrew,
|
||||
authors : ['test', 'test2']
|
||||
};
|
||||
api.getBrew = jest.fn(()=>async (req)=>{
|
||||
req.brew = brew;
|
||||
});
|
||||
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||
saveFunc = jest.fn(async ()=>{ throw 'err'; });
|
||||
const req = { account: { username: 'test' } };
|
||||
|
||||
let err;
|
||||
try {
|
||||
await api.deleteBrew(req, res);
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
expect(err).not.toBeUndefined();
|
||||
expect(api.getBrew).toHaveBeenCalled();
|
||||
expect(model.findOne).toHaveBeenCalled();
|
||||
expect(removeFunc).not.toHaveBeenCalled();
|
||||
expect(saveFunc).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should delete google brew', async ()=>{
|
||||
const brew = {
|
||||
...googleBrew,
|
||||
authors : ['test']
|
||||
};
|
||||
api.getBrew = jest.fn(()=>async (req)=>{
|
||||
req.brew = brew;
|
||||
});
|
||||
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||
api.deleteGoogleBrew = jest.fn(async ()=>true);
|
||||
const req = { account: { username: 'test' } };
|
||||
|
||||
await api.deleteBrew(req, res);
|
||||
|
||||
expect(api.getBrew).toHaveBeenCalled();
|
||||
expect(model.findOne).toHaveBeenCalled();
|
||||
expect(removeFunc).toHaveBeenCalled();
|
||||
expect(api.deleteGoogleBrew).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle google brew delete error', async ()=>{
|
||||
const brew = {
|
||||
...googleBrew,
|
||||
authors : ['test']
|
||||
};
|
||||
api.getBrew = jest.fn(()=>async (req)=>{
|
||||
req.brew = brew;
|
||||
});
|
||||
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||
api.deleteGoogleBrew = jest.fn(async ()=>{
|
||||
throw 'err';
|
||||
});
|
||||
const req = { account: { username: 'test' } };
|
||||
|
||||
await api.deleteBrew(req, res);
|
||||
|
||||
expect(api.getBrew).toHaveBeenCalled();
|
||||
expect(model.findOne).toHaveBeenCalled();
|
||||
expect(removeFunc).toHaveBeenCalled();
|
||||
expect(api.deleteGoogleBrew).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should delete google brew and retain stub when multiple authors and owner request deletion', async ()=>{
|
||||
const brew = {
|
||||
...googleBrew,
|
||||
authors : ['test', 'test2']
|
||||
};
|
||||
api.getBrew = jest.fn(()=>async (req)=>{
|
||||
req.brew = brew;
|
||||
});
|
||||
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||
api.deleteGoogleBrew = jest.fn(async ()=>true);
|
||||
const req = { account: { username: 'test' } };
|
||||
|
||||
await api.deleteBrew(req, res);
|
||||
|
||||
expect(api.getBrew).toHaveBeenCalled();
|
||||
expect(model.findOne).toHaveBeenCalled();
|
||||
expect(removeFunc).not.toHaveBeenCalled();
|
||||
expect(api.deleteGoogleBrew).toHaveBeenCalled();
|
||||
expect(saveFunc).toHaveBeenCalled();
|
||||
expect(saved.authors).toEqual(['test2']);
|
||||
expect(saved.googleId).toEqual(undefined);
|
||||
expect(saved.text).toEqual(undefined);
|
||||
expect(saved.textBin).not.toEqual(undefined);
|
||||
});
|
||||
|
||||
it('should retain google brew and update stub when multiple authors and extra author requests deletion', async ()=>{
|
||||
const brew = {
|
||||
...googleBrew,
|
||||
authors : ['test', 'test2']
|
||||
};
|
||||
api.getBrew = jest.fn(()=>async (req)=>{
|
||||
req.brew = brew;
|
||||
});
|
||||
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||
api.deleteGoogleBrew = jest.fn(async ()=>true);
|
||||
const req = { account: { username: 'test2' } };
|
||||
|
||||
await api.deleteBrew(req, res);
|
||||
|
||||
expect(api.getBrew).toHaveBeenCalled();
|
||||
expect(model.findOne).toHaveBeenCalled();
|
||||
expect(removeFunc).not.toHaveBeenCalled();
|
||||
expect(api.deleteGoogleBrew).not.toHaveBeenCalled();
|
||||
expect(saveFunc).toHaveBeenCalled();
|
||||
expect(saved.authors).toEqual(['test']);
|
||||
expect(saved.googleId).toEqual(brew.googleId);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -12,13 +12,14 @@ const HomebrewSchema = mongoose.Schema({
|
||||
textBin : { type: Buffer },
|
||||
pageCount : { type: Number, default: 1 },
|
||||
|
||||
description : { type: String, default: '' },
|
||||
tags : [String],
|
||||
systems : [String],
|
||||
renderer : { type: String, default: '' },
|
||||
authors : [String],
|
||||
published : { type: Boolean, default: false },
|
||||
thumbnail : { type: String, default: '' },
|
||||
description : { type: String, default: '' },
|
||||
tags : [String],
|
||||
systems : [String],
|
||||
renderer : { type: String, default: '' },
|
||||
authors : [String],
|
||||
invitedAuthors : [String],
|
||||
published : { type: Boolean, default: false },
|
||||
thumbnail : { type: String, default: '' },
|
||||
|
||||
createdAt : { type: Date, default: Date.now },
|
||||
updatedAt : { type: Date, default: Date.now },
|
||||
@@ -46,8 +47,6 @@ HomebrewSchema.statics.get = function(query, fields=null){
|
||||
unzipped = zlib.inflateRawSync(brews[0].textBin);
|
||||
brews[0].text = unzipped.toString();
|
||||
}
|
||||
if(!brews[0].renderer)
|
||||
brews[0].renderer = 'legacy';
|
||||
return resolve(brews[0]);
|
||||
});
|
||||
});
|
||||
|
||||
12
server/middleware/check-client-version.js
Normal file
@@ -0,0 +1,12 @@
|
||||
module.exports = (req, res, next)=>{
|
||||
const userVersion = req.get('Homebrewery-Version');
|
||||
const version = require('../../package.json').version;
|
||||
|
||||
if(userVersion != version) {
|
||||
return res.status(412).send({
|
||||
message : `Client version ${userVersion} is out of date. Please save your changes elsewhere and refresh to pick up client version ${version}.`
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
@@ -229,6 +229,15 @@ const CodeEditor = createClass({
|
||||
this.codeMirror.replaceSelection('\n\\page\n\n', 'end');
|
||||
},
|
||||
|
||||
injectText : function(injectText, overwrite=true) {
|
||||
const cm = this.codeMirror;
|
||||
if(!overwrite) {
|
||||
cm.setCursor(cm.getCursor('from'));
|
||||
}
|
||||
cm.replaceSelection(injectText, 'end');
|
||||
cm.focus();
|
||||
},
|
||||
|
||||
makeUnderline : function() {
|
||||
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 3) === '<u>' && selection.slice(-4) === '</u>';
|
||||
this.codeMirror.replaceSelection(t ? selection.slice(3, -4) : `<u>${selection}</u>`, 'around');
|
||||
@@ -355,12 +364,20 @@ const CodeEditor = createClass({
|
||||
let text = '';
|
||||
let currentLine = from.line;
|
||||
const maxLength = 50;
|
||||
|
||||
let foldPreviewText = '';
|
||||
while (currentLine <= to.line && text.length <= maxLength) {
|
||||
text += this.codeMirror.getLine(currentLine);
|
||||
if(currentLine < to.line)
|
||||
text += ' ';
|
||||
currentLine += 1;
|
||||
const currentText = this.codeMirror.getLine(currentLine);
|
||||
currentLine++;
|
||||
if(currentText[0] == '#'){
|
||||
foldPreviewText = currentText;
|
||||
break;
|
||||
}
|
||||
if(!foldPreviewText && currentText != '\n') {
|
||||
foldPreviewText = currentText;
|
||||
}
|
||||
}
|
||||
text = foldPreviewText || `Lines ${from.line+1}-${to.line+1}`;
|
||||
|
||||
text = text.trim();
|
||||
if(text.length > maxLength)
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
font-family: inherit;
|
||||
text-shadow: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.sourceMoveFlash .CodeMirror-line{
|
||||
animation-name: sourceMoveAnimation;
|
||||
|
||||
@@ -32,7 +32,7 @@ const mustacheSpans = {
|
||||
start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token
|
||||
const inlineRegex = /{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])*\s*|}}/g;
|
||||
const inlineRegex = /{{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1 *|}}/g;
|
||||
const match = completeSpan.exec(src);
|
||||
if(match) {
|
||||
//Find closing delimiter
|
||||
@@ -82,7 +82,7 @@ const mustacheDivs = {
|
||||
start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
const completeBlock = /^ *{{[^\n}]* *\n.*\n *}}/s; // Regex for the complete token
|
||||
const blockRegex = /^ *{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])* *$|^ *}}$/gm;
|
||||
const blockRegex = /^ *{{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1 *$|^ *}}$/gm;
|
||||
const match = completeBlock.exec(src);
|
||||
if(match) {
|
||||
//Find closing delimiter
|
||||
@@ -130,7 +130,7 @@ const mustacheInjectInline = {
|
||||
level : 'inline',
|
||||
start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
const inlineRegex = /^ *{((?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])*)}/g;
|
||||
const inlineRegex = /^ *{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1}/g;
|
||||
const match = inlineRegex.exec(src);
|
||||
if(match) {
|
||||
const lastToken = tokens[tokens.length - 1];
|
||||
@@ -165,7 +165,7 @@ const mustacheInjectBlock = {
|
||||
level : 'block',
|
||||
start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
const inlineRegex = /^ *{((?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])*)}/ym;
|
||||
const inlineRegex = /^ *{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1}/ym;
|
||||
const match = inlineRegex.exec(src);
|
||||
if(match) {
|
||||
const lastToken = tokens[tokens.length - 1];
|
||||
|
||||
@@ -73,18 +73,35 @@ const Nav = {
|
||||
|
||||
dropdown : createClass({
|
||||
displayName : 'Nav.dropdown',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
trigger : 'hover'
|
||||
};
|
||||
},
|
||||
getInitialState : function() {
|
||||
return {
|
||||
showDropdown : false
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount : function() {
|
||||
if(this.props.trigger == 'click')
|
||||
document.addEventListener('click', this.handleClickOutside);
|
||||
},
|
||||
componentWillUnmount : function() {
|
||||
if(this.props.trigger == 'click')
|
||||
document.removeEventListener('click', this.handleClickOutside);
|
||||
},
|
||||
handleClickOutside : function(e){
|
||||
// Close dropdown when clicked outside
|
||||
if(this.refs.dropdown && !this.refs.dropdown.contains(e.target)) {
|
||||
this.handleDropdown(false);
|
||||
}
|
||||
},
|
||||
handleDropdown : function(show){
|
||||
this.setState({
|
||||
showDropdown : show
|
||||
});
|
||||
},
|
||||
|
||||
renderDropdown : function(dropdownChildren){
|
||||
if(!this.state.showDropdown) return null;
|
||||
|
||||
@@ -94,7 +111,6 @@ const Nav = {
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
render : function () {
|
||||
const dropdownChildren = React.Children.map(this.props.children, (child, i)=>{
|
||||
// Ignore the first child
|
||||
@@ -102,10 +118,12 @@ const Nav = {
|
||||
return child;
|
||||
});
|
||||
return (
|
||||
<div className='navDropdownContainer'
|
||||
onMouseEnter={()=>this.handleDropdown(true)}
|
||||
onMouseLeave={()=>this.handleDropdown(false)}>
|
||||
{this.props.children[0]}
|
||||
<div className={`navDropdownContainer ${this.props.className}`}
|
||||
ref='dropdown'
|
||||
onMouseEnter={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(true);} : undefined}
|
||||
onClick= {this.props.trigger == 'click' ? ()=>{this.handleDropdown(true);} : undefined}
|
||||
onMouseLeave={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(false);} : undefined}>
|
||||
{this.props.children[0] || this.props.children /*children is not an array when only one child*/}
|
||||
{this.renderDropdown(dropdownChildren)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -69,7 +69,8 @@ const SplitPane = createClass({
|
||||
this.setState({ isDragging: false });
|
||||
},
|
||||
|
||||
handleDown : function(){
|
||||
handleDown : function(e){
|
||||
e.preventDefault();
|
||||
this.setState({ isDragging: true });
|
||||
//this.unFocus()
|
||||
},
|
||||
|
||||
@@ -1 +1,27 @@
|
||||
html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{border:0;font-size:100%;font:inherit;vertical-align:baseline;margin:0;padding:0}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:none}table{border-collapse:collapse;border-spacing:0}
|
||||
:where(html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video){
|
||||
border:0;font-size:100%;font:inherit;vertical-align:baseline;margin:0;padding:0
|
||||
}
|
||||
|
||||
:where(article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section){
|
||||
display:block
|
||||
}
|
||||
|
||||
:where(body){
|
||||
line-height:1
|
||||
}
|
||||
|
||||
:where(ol,ul){
|
||||
list-style:none
|
||||
}
|
||||
|
||||
:where(blockquote,q){
|
||||
quotes:none
|
||||
}
|
||||
|
||||
:where(blockquote:before,blockquote:after,q:before,q:after){
|
||||
content:none
|
||||
}
|
||||
|
||||
:where(table){
|
||||
border-collapse:collapse;border-spacing:0
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ describe('Tests for static pages', ()=>{
|
||||
return app.get('/').expect(200);
|
||||
});
|
||||
|
||||
it('Home page v3 works', ()=>{
|
||||
return app.get('/v3_preview').expect(200);
|
||||
it('Home page legacy works', ()=>{
|
||||
return app.get('/legacy').expect(200);
|
||||
});
|
||||
|
||||
it('Changelog page works', ()=>{
|
||||
|
||||
5
themes/Legacy/5ePHB/settings.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name" : "5e PHB",
|
||||
"renderer" : "legacy",
|
||||
"baseTheme" : false
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
const MagicGen = require('./magic.gen.js');
|
||||
const ClassTableGen = require('./classtable.gen.js');
|
||||
const MonsterBlockGen = require('./monsterblock.gen.js');
|
||||
const ClassFeatureGen = require('./classfeature.gen.js');
|
||||
const CoverPageGen = require('./coverpage.gen.js');
|
||||
const TableOfContentsGen = require('./tableOfContents.gen.js');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
const MagicGen = require('./snippets/magic.gen.js');
|
||||
const ClassTableGen = require('./snippets/classtable.gen.js');
|
||||
const MonsterBlockGen = require('./snippets/monsterblock.gen.js');
|
||||
const ClassFeatureGen = require('./snippets/classfeature.gen.js');
|
||||
const CoverPageGen = require('./snippets/coverpage.gen.js');
|
||||
const TableOfContentsGen = require('./snippets/tableOfContents.gen.js');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
module.exports = [
|
||||
|
||||
@@ -58,6 +58,7 @@ body {
|
||||
text-rendering : optimizeLegibility;
|
||||
page-break-before : always;
|
||||
page-break-after : always;
|
||||
contain : size;
|
||||
//*****************************
|
||||
// * BASE
|
||||
// *****************************/
|
||||
BIN
themes/V3/5eDMG/dropdownTexture.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
6
themes/V3/5eDMG/settings.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name" : "5e DMG",
|
||||
"renderer" : "V3",
|
||||
"baseTheme" : "5ePHB",
|
||||
"baseSnippets" : "5ePHB"
|
||||
}
|
||||
4
themes/V3/5eDMG/snippets.js
Normal file
@@ -0,0 +1,4 @@
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
module.exports = [
|
||||
];
|
||||
19
themes/V3/5eDMG/style.less
Normal file
@@ -0,0 +1,19 @@
|
||||
:root {
|
||||
//Colors
|
||||
--HB_Color_Accent : #EBCEC3; // Salmon
|
||||
--HB_Color_Footnotes : #5C5C5C; // Dark gray
|
||||
}
|
||||
|
||||
.page {
|
||||
background-image : url(/assets/DMG_background.png);
|
||||
background-size : cover;
|
||||
|
||||
&:after {
|
||||
background-image : url(/assets/DMG_footerAccent.png);
|
||||
height: 58px;
|
||||
}
|
||||
|
||||
.footnote {
|
||||
bottom : 40px;
|
||||
}
|
||||
}
|
||||
BIN
themes/V3/5ePHB/dropdownTexture.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
6
themes/V3/5ePHB/settings.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name" : "5e PHB",
|
||||
"renderer" : "V3",
|
||||
"baseTheme" : false,
|
||||
"baseSnippets" : false
|
||||
}
|
||||
264
themes/V3/5ePHB/snippets.js
Normal file
@@ -0,0 +1,264 @@
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
const MagicGen = require('./snippets/magic.gen.js');
|
||||
const ClassTableGen = require('./snippets/classtable.gen.js');
|
||||
const MonsterBlockGen = require('./snippets/monsterblock.gen.js');
|
||||
const ClassFeatureGen = require('./snippets/classfeature.gen.js');
|
||||
const CoverPageGen = require('./snippets/coverpage.gen.js');
|
||||
const TableOfContentsGen = require('./snippets/tableOfContents.gen.js');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
|
||||
|
||||
module.exports = [
|
||||
|
||||
{
|
||||
groupName : 'Text Editor',
|
||||
icon : 'fas fa-pencil-alt',
|
||||
view : 'text',
|
||||
snippets : [
|
||||
{
|
||||
name : 'Page Number',
|
||||
icon : 'fas fa-bookmark',
|
||||
gen : '{{pageNumber 1}}\n{{footnote PART 1 | SECTION NAME}}\n\n'
|
||||
},
|
||||
{
|
||||
name : 'Auto-incrementing Page Number',
|
||||
icon : 'fas fa-sort-numeric-down',
|
||||
gen : '{{pageNumber,auto}}\n{{footnote PART 1 | SECTION NAME}}\n\n'
|
||||
},
|
||||
{
|
||||
name : 'Table of Contents',
|
||||
icon : 'fas fa-book',
|
||||
gen : TableOfContentsGen
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
groupName : 'Style Editor',
|
||||
icon : 'fas fa-pencil-alt',
|
||||
view : 'style',
|
||||
snippets : [
|
||||
{
|
||||
name : 'Remove Drop Cap',
|
||||
icon : 'fas fa-remove-format',
|
||||
gen : dedent`/* Removes Drop Caps */
|
||||
.page h1+p:first-letter {
|
||||
all: unset;
|
||||
}\n\n
|
||||
/* Removes Small-Caps in first line */
|
||||
.page h1+p:first-line {
|
||||
all: unset;
|
||||
}`
|
||||
},
|
||||
{
|
||||
name : 'Tweak Drop Cap',
|
||||
icon : 'fas fa-sliders-h',
|
||||
gen : dedent`/* Drop Cap settings */
|
||||
.page h1 + p::first-letter {
|
||||
font-family: SolberaImitationRemake;
|
||||
font-size: 3.5cm;
|
||||
background-image: linear-gradient(-45deg, #322814, #998250, #322814);
|
||||
line-height: 1em;
|
||||
}\n\n`
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
/*********************** IMAGES *******************/
|
||||
{
|
||||
groupName : 'Images',
|
||||
icon : 'fas fa-images',
|
||||
view : 'text',
|
||||
snippets : [
|
||||
{
|
||||
name : 'Image',
|
||||
icon : 'fas fa-image',
|
||||
gen : dedent`
|
||||
 {width:325px,mix-blend-mode:multiply}
|
||||
|
||||
{{artist,position:relative,top:-230px,left:10px,margin-bottom:-30px
|
||||
##### Cat Warrior
|
||||
[Kyoung Hwan Kim](https://www.artstation.com/tahra)
|
||||
}}`
|
||||
},
|
||||
{
|
||||
name : 'Background Image',
|
||||
icon : 'fas fa-tree',
|
||||
gen : dedent`
|
||||
 {position:absolute,top:50px,right:30px,width:280px}
|
||||
|
||||
{{artist,top:80px,right:30px
|
||||
##### Homebrew Mug
|
||||
[naturalcrit](https://homebrew.naturalcrit.com)
|
||||
}}`
|
||||
},
|
||||
{
|
||||
name : 'Watermark',
|
||||
icon : 'fas fa-id-card',
|
||||
gen : dedent`
|
||||
{{watermark Homebrewery}}\n`
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
/************************* PHB ********************/
|
||||
|
||||
{
|
||||
groupName : 'PHB',
|
||||
icon : 'fas fa-book',
|
||||
view : 'text',
|
||||
snippets : [
|
||||
{
|
||||
name : 'Spell',
|
||||
icon : 'fas fa-magic',
|
||||
gen : MagicGen.spell,
|
||||
},
|
||||
{
|
||||
name : 'Spell List',
|
||||
icon : 'fas fa-scroll',
|
||||
gen : MagicGen.spellList,
|
||||
},
|
||||
{
|
||||
name : 'Class Feature',
|
||||
icon : 'fas fa-mask',
|
||||
gen : ClassFeatureGen,
|
||||
},
|
||||
{
|
||||
name : 'Note',
|
||||
icon : 'fas fa-sticky-note',
|
||||
gen : function(){
|
||||
return dedent`
|
||||
{{note
|
||||
##### Time to Drop Knowledge
|
||||
Use notes to point out some interesting information.
|
||||
|
||||
**Tables and lists** both work within a note.
|
||||
}}
|
||||
\n`;
|
||||
},
|
||||
},
|
||||
{
|
||||
name : 'Descriptive Text Box',
|
||||
icon : 'fas fa-comment-alt',
|
||||
gen : function(){
|
||||
return dedent`
|
||||
{{descriptive
|
||||
##### Time to Drop Knowledge
|
||||
Use descriptive boxes to highlight text that should be read aloud.
|
||||
|
||||
**Tables and lists** both work within a descriptive box.
|
||||
}}
|
||||
\n`;
|
||||
},
|
||||
},
|
||||
{
|
||||
name : 'Monster Stat Block (unframed)',
|
||||
icon : 'fas fa-paw',
|
||||
gen : MonsterBlockGen.monster('monster', 2),
|
||||
},
|
||||
{
|
||||
name : 'Monster Stat Block',
|
||||
icon : 'fas fa-spider',
|
||||
gen : MonsterBlockGen.monster('monster,frame', 2),
|
||||
},
|
||||
{
|
||||
name : 'Wide Monster Stat Block',
|
||||
icon : 'fas fa-dragon',
|
||||
gen : MonsterBlockGen.monster('monster,frame,wide', 4),
|
||||
},
|
||||
{
|
||||
name : 'Cover Page',
|
||||
icon : 'fas fa-file-word',
|
||||
gen : CoverPageGen,
|
||||
},
|
||||
{
|
||||
name : 'Magic Item',
|
||||
icon : 'fas fa-hat-wizard',
|
||||
gen : MagicGen.item,
|
||||
},
|
||||
{
|
||||
name : 'Artist Credit',
|
||||
icon : 'fas fa-signature',
|
||||
gen : function(){
|
||||
return dedent`
|
||||
{{artist,top:90px,right:30px
|
||||
##### Starry Night
|
||||
[Van Gogh](https://www.vangoghmuseum.nl/en)
|
||||
}}
|
||||
\n`;
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
|
||||
/********************* TABLES *********************/
|
||||
|
||||
{
|
||||
groupName : 'Tables',
|
||||
icon : 'fas fa-table',
|
||||
view : 'text',
|
||||
snippets : [
|
||||
{
|
||||
name : 'Class Table',
|
||||
icon : 'fas fa-table',
|
||||
gen : ClassTableGen.full('classTable,frame,decoration,wide'),
|
||||
},
|
||||
{
|
||||
name : 'Class Table (unframed)',
|
||||
icon : 'fas fa-border-none',
|
||||
gen : ClassTableGen.full('classTable,wide'),
|
||||
},
|
||||
{
|
||||
name : '1/2 Class Table',
|
||||
icon : 'fas fa-list-alt',
|
||||
gen : ClassTableGen.half('classTable,decoration,frame'),
|
||||
},
|
||||
{
|
||||
name : '1/2 Class Table (unframed)',
|
||||
icon : 'fas fa-border-none',
|
||||
gen : ClassTableGen.half('classTable'),
|
||||
},
|
||||
{
|
||||
name : '1/3 Class Table',
|
||||
icon : 'fas fa-border-all',
|
||||
gen : ClassTableGen.third('classTable,frame'),
|
||||
},
|
||||
{
|
||||
name : '1/3 Class Table (unframed)',
|
||||
icon : 'fas fa-border-none',
|
||||
gen : ClassTableGen.third('classTable'),
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
/**************** PAGE *************/
|
||||
|
||||
{
|
||||
groupName : 'Print',
|
||||
icon : 'fas fa-print',
|
||||
view : 'style',
|
||||
snippets : [
|
||||
{
|
||||
name : 'Ink Friendly',
|
||||
icon : 'fas fa-tint',
|
||||
gen : dedent`
|
||||
/* Ink Friendly */
|
||||
*:is(.page,.monster,.note,.descriptive) {
|
||||
background : white !important;
|
||||
filter : drop-shadow(0px 0px 3px #888) !important;
|
||||
}
|
||||
|
||||
.page img {
|
||||
visibility : hidden;
|
||||
}\n\n`
|
||||
},
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -17,15 +17,15 @@ module.exports = function(classname){
|
||||
|
||||
return dedent`
|
||||
## Class Features
|
||||
As a ${classname}, you gain the following class features
|
||||
#### Hit Points
|
||||
|
||||
As a ${classname}, you gain the following class features
|
||||
|
||||
#### Hit Points
|
||||
**Hit Dice:** :: 1d${hitDie} per ${classname} level
|
||||
**Hit Points at 1st Level:** :: ${hitDie} + your Constitution modifier
|
||||
**Hit Points at Higher Levels:** :: 1d${hitDie} (or ${hitDie/2 + 1}) + your Constitution modifier per ${classname} level after 1st
|
||||
|
||||
#### Proficiencies
|
||||
|
||||
**Armor:** :: ${_.sampleSize(['Light armor', 'Medium armor', 'Heavy armor', 'Shields'], _.random(0, 3)).join(', ') || 'None'}
|
||||
**Weapons:** :: ${_.sampleSize(['Squeegee', 'Rubber Chicken', 'Simple weapons', 'Martial weapons'], _.random(0, 2)).join(', ') || 'None'}
|
||||
**Tools:** :: ${_.sampleSize(['Artisan\'s tools', 'one musical instrument', 'Thieves\' tools'], _.random(0, 2)).join(', ') || 'None'}
|
||||
@@ -34,7 +34,6 @@ module.exports = function(classname){
|
||||
**Skills:** :: Choose two from ${_.sampleSize(skillList, _.random(4, 6)).join(', ')}
|
||||
|
||||
#### Spellcasting Ability
|
||||
|
||||
{{text-align:center
|
||||
**Spell save DC**:: = ${_.sample([6, 8, 10])} + your proficiency bonus + your ${spellSkill} modifier
|
||||
|
||||
@@ -46,6 +45,5 @@ module.exports = function(classname){
|
||||
- *(a)* a martial weapon and a shield or *(b)* two martial weapons
|
||||
- *(a)* five javelins or *(b)* any simple melee weapon
|
||||
- ${_.sample(['10 lint fluffs', '1 button', 'a cherished lost sock'])}
|
||||
|
||||
`;
|
||||
};
|
||||
@@ -1,15 +1,19 @@
|
||||
@import (less) './themes/fonts/5e/fonts.less';
|
||||
@import (less) './themes/assets/assets.less';
|
||||
|
||||
//Colors
|
||||
@background : #EEE5CE; // Light parchment
|
||||
@noteGreen : #e0e5c1; // Pastel green
|
||||
@headerUnderline : #c9ad6a; // Gold
|
||||
@horizontalRule : #9c2b1b; // Maroon
|
||||
@headerText : #58180D; // Dark maroon
|
||||
@monsterStatBackground : #EEDBAB; // Light orange parchment
|
||||
@captionText : #766649; // Brown
|
||||
@watercolorStain : #BBAD82; // Light brown
|
||||
:root {
|
||||
//Colors
|
||||
--HB_Color_Background : #EEE5CE; // Light parchment
|
||||
--HB_Color_Accent : #E0E5C1; // Pastel green
|
||||
--HB_Color_HeaderUnderline : #C0AD6A; // Gold
|
||||
--HB_Color_HorizontalRule : #9C2B1B; // Maroon
|
||||
--HB_Color_HeaderText : #58180D; // Dark Maroon
|
||||
--HB_Color_MonsterStatBackground : #F2E5B5; // Light orange parchment
|
||||
--HB_Color_CaptionText : #766649; // Brown
|
||||
--HB_Color_WatercolorStain : #BBAD82; // Light brown
|
||||
--HB_Color_Footnotes : #C9AD6A; // Gold
|
||||
}
|
||||
|
||||
@page { margin: 0; }
|
||||
body {
|
||||
counter-reset : phb-page-numbers;
|
||||
@@ -65,7 +69,7 @@ body {
|
||||
overflow : hidden;
|
||||
height : 279.4mm;
|
||||
width : 215.9mm;
|
||||
background-color : @background;
|
||||
background-color : var(--HB_Color_Background);
|
||||
background-image : @backgroundImage;
|
||||
padding : 1.4cm 1.9cm 1.7cm;
|
||||
font-family : BookInsanityRemake;
|
||||
@@ -73,9 +77,12 @@ body {
|
||||
text-rendering : optimizeLegibility;
|
||||
page-break-before : always;
|
||||
page-break-after : always;
|
||||
}
|
||||
//*****************************
|
||||
// * BASE
|
||||
// *****************************/
|
||||
|
||||
.page{
|
||||
p{
|
||||
overflow-wrap : break-word; //TODO: MAKE ALL MARGINS TOP-ONLY. USE * + * STYLE SELECTORS
|
||||
display : block;
|
||||
@@ -131,12 +138,13 @@ body {
|
||||
h1,h2,h3,h4{
|
||||
font-family : MrEavesRemake;
|
||||
font-weight : 800;
|
||||
color : @headerText;
|
||||
color : var(--HB_Color_HeaderText);
|
||||
}
|
||||
h1{
|
||||
margin-bottom : 0.18cm; //Margin-bottom only because this is WIDE
|
||||
column-span : all;
|
||||
font-size : 0.89cm;
|
||||
line-height : 1em;
|
||||
-webkit-column-span : all;
|
||||
-moz-column-span : all;
|
||||
&+p::first-letter{
|
||||
@@ -150,9 +158,9 @@ body {
|
||||
padding-bottom : 2px;
|
||||
margin-bottom : -20px;
|
||||
background-image : linear-gradient(-45deg, #322814, #998250, #322814);
|
||||
background-clip : text;
|
||||
-webkit-background-clip : text;
|
||||
color : rgba(0, 0, 0, 0);
|
||||
background-clip : text;
|
||||
-webkit-background-clip : text;
|
||||
color : rgba(0, 0, 0, 0);
|
||||
}
|
||||
&+p::first-line{
|
||||
font-variant : small-caps;
|
||||
@@ -168,14 +176,26 @@ body {
|
||||
//margin-top : -0.1cm; //Font is misaligned. Shift up slightly
|
||||
//margin-bottom : 0.1cm;
|
||||
font-size : 0.575cm;
|
||||
border-bottom : 2px solid @headerUnderline;
|
||||
border-bottom : 2px solid var(--HB_Color_HeaderUnderline);;
|
||||
line-height : 0.995em; //Font is misaligned. Shift up slightly
|
||||
& + * {
|
||||
margin-top: 0.17cm;
|
||||
}
|
||||
}
|
||||
* + h3 {
|
||||
margin-top : 0.155cm; //(0.325 - 0.17)
|
||||
}
|
||||
h4{
|
||||
//margin-top : -0.02cm; //Font is misaligned. Shift up slightly
|
||||
//margin-bottom : 0.02cm;
|
||||
font-size : 0.458cm;
|
||||
line-height : 0.971em; //Font is misaligned. Shift up slightly
|
||||
& + * {
|
||||
margin-top: 0.09cm;
|
||||
}
|
||||
}
|
||||
* + h4 {
|
||||
margin-top : 0.235cm; //(0.325 - 0.09)
|
||||
}
|
||||
h5{
|
||||
//margin-top : -0.02cm; //Font is misaligned. Shift up slightly
|
||||
@@ -194,6 +214,7 @@ body {
|
||||
table{
|
||||
.useSansSerif();
|
||||
width : 100%;
|
||||
line-height : 16px;
|
||||
& + * {
|
||||
margin-top : 0.325cm;
|
||||
}
|
||||
@@ -202,16 +223,20 @@ body {
|
||||
font-weight : 800;
|
||||
th{
|
||||
vertical-align : bottom;
|
||||
padding : 0.14em 0.4em;
|
||||
//padding : 0.14em 0.4em;
|
||||
padding : 0px 1.5px; // Both of these are temporary, just to force
|
||||
//line-height : 16px; // PDF to render at same height until Chrome 108
|
||||
}
|
||||
}
|
||||
tbody{
|
||||
tr{
|
||||
td{
|
||||
padding : 0.14em 0.4em;
|
||||
//padding : 0.14em 0.4em;
|
||||
padding : 0px 1.5px; // Both of these are temporary, just to force
|
||||
//line-height : 16px; // PDF to render at same height until Chrome 108
|
||||
}
|
||||
&:nth-child(odd){
|
||||
background-color : @noteGreen;
|
||||
background-color : var(--HB_Color_Accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -221,7 +246,7 @@ body {
|
||||
// *****************************/
|
||||
.note{
|
||||
.useSansSerif();
|
||||
background-color : @noteGreen;
|
||||
background-color : var(--HB_Color_Accent);
|
||||
border-style : solid;
|
||||
border-width : 1px;
|
||||
border-image : @noteBorderImage 12 stretch;
|
||||
@@ -287,7 +312,7 @@ body {
|
||||
text-align : center;
|
||||
font-family : WalterTurncoat;
|
||||
font-size : 0.27cm;
|
||||
color : @captionText;
|
||||
color : var(--HB_Color_CaptionText);
|
||||
p, p + p {
|
||||
margin : unset;
|
||||
text-indent : unset;
|
||||
@@ -340,7 +365,7 @@ body {
|
||||
mask-size : contain;
|
||||
mask-repeat : no-repeat;
|
||||
background-size : cover;
|
||||
background-color : @watercolorStain; /*default color*/
|
||||
background-color : var(--HB_Color_WatercolorStain); /*default color*/
|
||||
--wc : @watercolor1; /*default image*/
|
||||
z-index : -2;
|
||||
}
|
||||
@@ -366,7 +391,7 @@ body {
|
||||
&.frame {
|
||||
border-style : solid;
|
||||
border-width : 7px 6px;
|
||||
background-color : @monsterStatBackground;
|
||||
background-color : var(--HB_Color_MonsterStatBackground);
|
||||
background-image : @monsterBlockBackground;
|
||||
border-image : @monsterBorderImage 14 round;
|
||||
border-image-outset : 0px 2px;
|
||||
@@ -397,7 +422,7 @@ body {
|
||||
font-family : ScalySansRemake;
|
||||
font-weight : 800;
|
||||
font-variant : small-caps;
|
||||
border-bottom : 2px solid @headerText;
|
||||
border-bottom : 2px solid var(--HB_Color_HeaderText);
|
||||
// margin-top : 0.05cm; //Font is misaligned. Shift up slightly
|
||||
padding-bottom : 0.05cm;
|
||||
}
|
||||
@@ -414,7 +439,7 @@ body {
|
||||
|
||||
//Attribute Lists - All text between HRs is red
|
||||
hr ~ :is(dl,p) {
|
||||
color : @headerText;
|
||||
color : var(--HB_Color_HeaderText);
|
||||
}
|
||||
hr:last-of-type {
|
||||
& ~ :is(dl,p) {
|
||||
@@ -429,7 +454,7 @@ body {
|
||||
hr + table:first-of-type{
|
||||
margin : 0;
|
||||
column-span : none;
|
||||
color : @headerText;
|
||||
color : var(--HB_Color_HeaderText);
|
||||
background-color : transparent;
|
||||
border-style : none;
|
||||
border-image : none;
|
||||
@@ -484,7 +509,7 @@ body {
|
||||
bottom : 22px;
|
||||
width : 50px;
|
||||
font-size : 0.9em;
|
||||
color : #c9ad6a;
|
||||
color : var(--HB_Color_Footnotes);
|
||||
text-align : center;
|
||||
text-indent : 0;
|
||||
&.auto::after {
|
||||
@@ -498,7 +523,7 @@ body {
|
||||
z-index : 150;
|
||||
width : 200px;
|
||||
font-size : 0.8em;
|
||||
color : #c9ad6a;
|
||||
color : var(--HB_Color_Footnotes);
|
||||
text-align : right;
|
||||
}
|
||||
//************************************
|
||||
@@ -620,6 +645,9 @@ body {
|
||||
&.wide:first-child {
|
||||
margin-top: 0.12cm;
|
||||
}
|
||||
& + * {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
&.decoration {
|
||||
position:relative;
|
||||
@@ -652,90 +680,78 @@ body {
|
||||
//*****************************
|
||||
// * TABLE OF CONTENTS
|
||||
// *****************************/
|
||||
.page .toc{
|
||||
.page {
|
||||
&:has(.toc):after {
|
||||
display: none;
|
||||
}
|
||||
.toc {
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
h1 {
|
||||
text-align : center;
|
||||
margin-bottom : 0.3cm;
|
||||
}
|
||||
a{
|
||||
display : inline;
|
||||
color : inherit;
|
||||
text-decoration : none;
|
||||
&:hover{
|
||||
text-decoration : underline;
|
||||
h1 {
|
||||
text-align : center;
|
||||
margin-bottom : 0.3cm;
|
||||
}
|
||||
}
|
||||
h4 {
|
||||
margin-top : 0.2cm;
|
||||
line-height : 0.4cm;
|
||||
& + ul li {
|
||||
line-height: 1.2em;
|
||||
a{
|
||||
display : inline;
|
||||
color : inherit;
|
||||
text-decoration : none;
|
||||
&:hover{
|
||||
text-decoration : underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
ul{
|
||||
padding-left : 0;
|
||||
list-style-type : none;
|
||||
li + li h3 {
|
||||
margin-top : 0.26cm;
|
||||
line-height : 1em
|
||||
h4 {
|
||||
margin-top : 0.2cm;
|
||||
line-height : 0.4cm;
|
||||
& + ul li {
|
||||
line-height: 1.2em;
|
||||
}
|
||||
}
|
||||
h3 span:first-child::after {
|
||||
border : none;
|
||||
}
|
||||
span {
|
||||
display : table-cell;
|
||||
&:first-child {
|
||||
position : relative;
|
||||
overflow : hidden;
|
||||
&::after {
|
||||
ul{
|
||||
padding-left : 0;
|
||||
list-style-type : none;
|
||||
margin-top : 0;
|
||||
a {
|
||||
width : 100%;
|
||||
display : flex;
|
||||
flex-flow : row nowrap;
|
||||
justify-content : space-between;
|
||||
}
|
||||
li + li h3 {
|
||||
margin-top : 0.26cm;
|
||||
line-height : 1em
|
||||
}
|
||||
h3 span:first-child::after {
|
||||
border : none;
|
||||
}
|
||||
span {
|
||||
display : contents;
|
||||
&:first-child::after {
|
||||
content : "";
|
||||
position : absolute;
|
||||
bottom : 0.08cm;
|
||||
margin-left : 0.06cm; /* Spacing before dot leaders */
|
||||
width : 100%;
|
||||
flex : 1;
|
||||
margin-left : 0.08cm; /* Spacing before dot leaders */
|
||||
margin-right : 0.16cm;
|
||||
border-bottom : 0.05cm dotted #000;
|
||||
margin-bottom : 0.08cm;
|
||||
}
|
||||
&:last-child {
|
||||
display : inline-block;
|
||||
align-self : flex-end;
|
||||
font-family : "BookInsanityRemake";
|
||||
font-size : 0.34cm;
|
||||
font-weight : normal;
|
||||
color : #000;
|
||||
}
|
||||
}
|
||||
&:last-child {
|
||||
font-family : BookInsanityRemake;
|
||||
font-size : 0.34cm;
|
||||
font-weight : normal;
|
||||
color : black;
|
||||
text-align : right;
|
||||
vertical-align : bottom; /* Keep page number bottom-aligned */
|
||||
width : 1%;
|
||||
padding-left : 0.06cm; /* Spacing after dot leaders */
|
||||
/*white-space : nowrap; /* Uncomment if needed */
|
||||
ul { /*List indent*/
|
||||
margin-left : 1em;
|
||||
}
|
||||
}
|
||||
ul { /*List indent*/
|
||||
margin-left : 1em;
|
||||
&.wide{
|
||||
.useColumns(0.96, @fillMode: balance);
|
||||
}
|
||||
}
|
||||
&.wide{
|
||||
.useColumns(0.96, @fillMode: balance);
|
||||
}
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * MUSTACHE DIVS/SPANS
|
||||
// *****************************/
|
||||
.page {
|
||||
.block {
|
||||
break-inside : avoid;
|
||||
display : inline-block;
|
||||
.page :where(&) {
|
||||
width : 100%;
|
||||
}
|
||||
//-webkit-transform : translateZ(0); //Prevents shadows from breaking across columns
|
||||
}
|
||||
.inline-block {
|
||||
display : inline-block;
|
||||
text-indent : initial;
|
||||
}
|
||||
}
|
||||
|
||||
//*****************************
|
||||
@@ -768,26 +784,13 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * BLANK LINE
|
||||
// *****************************/
|
||||
.page {
|
||||
.blank {
|
||||
height : 1em;
|
||||
margin-top : 0;
|
||||
}
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * WIDE
|
||||
// *****************************/
|
||||
.page .wide{
|
||||
column-span : all;
|
||||
-webkit-column-span : all;
|
||||
-moz-column-span : all;
|
||||
display : block;
|
||||
margin-bottom : 0.34cm;
|
||||
&+* {
|
||||
margin-top : 0;
|
||||
}
|
||||
margin-bottom : 0.325cm;
|
||||
}
|
||||
|
||||
.page h1 + *{
|
||||
margin-top : 0;
|
||||
}
|
||||
BIN
themes/V3/Blank/dropdownTexture.png
Normal file
|
After Width: | Height: | Size: 236 B |
6
themes/V3/Blank/settings.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name" : "Blank",
|
||||
"renderer" : "V3",
|
||||
"baseTheme" : false,
|
||||
"baseSnippets" : false
|
||||
}
|
||||
@@ -1,13 +1,8 @@
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
const MagicGen = require('./magic.gen.js');
|
||||
const ClassTableGen = require('./classtable.gen.js');
|
||||
const MonsterBlockGen = require('./monsterblock.gen.js');
|
||||
const ClassFeatureGen = require('./classfeature.gen.js');
|
||||
const CoverPageGen = require('./coverpage.gen.js');
|
||||
const TableOfContentsGen = require('./tableOfContents.gen.js');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
const watercolorGen = require('./watercolor.gen.js');
|
||||
const WatercolorGen = require('./snippets/watercolor.gen.js');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
|
||||
|
||||
module.exports = [
|
||||
@@ -59,26 +54,11 @@ module.exports = [
|
||||
`&size=100x100) {width:100px;mix-blend-mode:multiply}`;
|
||||
}
|
||||
},
|
||||
{
|
||||
name : 'Page Number',
|
||||
icon : 'fas fa-bookmark',
|
||||
gen : '{{pageNumber 1}}\n{{footnote PART 1 | SECTION NAME}}\n\n'
|
||||
},
|
||||
{
|
||||
name : 'Auto-incrementing Page Number',
|
||||
icon : 'fas fa-sort-numeric-down',
|
||||
gen : '{{pageNumber,auto}}\n{{footnote PART 1 | SECTION NAME}}\n\n'
|
||||
},
|
||||
{
|
||||
name : 'Link to page',
|
||||
icon : 'fas fa-link',
|
||||
gen : '[Click here](#p3) to go to page 3\n'
|
||||
},
|
||||
{
|
||||
name : 'Table of Contents',
|
||||
icon : 'fas fa-book',
|
||||
gen : TableOfContentsGen
|
||||
},
|
||||
{
|
||||
name : 'Add Comment',
|
||||
icon : 'fas fa-code',
|
||||
@@ -91,29 +71,6 @@ module.exports = [
|
||||
icon : 'fas fa-pencil-alt',
|
||||
view : 'style',
|
||||
snippets : [
|
||||
{
|
||||
name : 'Remove Drop Cap',
|
||||
icon : 'fas fa-remove-format',
|
||||
gen : dedent`/* Removes Drop Caps */
|
||||
.page h1+p:first-letter {
|
||||
all: unset;
|
||||
}\n\n
|
||||
/* Removes Small-Caps in first line */
|
||||
.page h1+p:first-line {
|
||||
all: unset;
|
||||
}`
|
||||
},
|
||||
{
|
||||
name : 'Tweak Drop Cap',
|
||||
icon : 'fas fa-sliders-h',
|
||||
gen : dedent`/* Drop Cap settings */
|
||||
.page h1 + p::first-letter {
|
||||
font-family: SolberaImitationRemake;
|
||||
font-size: 3.5cm;
|
||||
background-image: linear-gradient(-45deg, #322814, #998250, #322814);
|
||||
line-height: 1em;
|
||||
}\n\n`
|
||||
},
|
||||
{
|
||||
name : 'Add Comment',
|
||||
icon : 'fas fa-code',
|
||||
@@ -132,28 +89,18 @@ module.exports = [
|
||||
name : 'Image',
|
||||
icon : 'fas fa-image',
|
||||
gen : dedent`
|
||||
 {width:325px,mix-blend-mode:multiply}
|
||||
|
||||
{{artist,position:relative,top:-230px,left:10px,margin-bottom:-30px
|
||||
##### Cat Warrior
|
||||
[Kyoung Hwan Kim](https://www.artstation.com/tahra)
|
||||
}}`
|
||||
 {width:325px,mix-blend-mode:multiply}`
|
||||
},
|
||||
{
|
||||
name : 'Background Image',
|
||||
icon : 'fas fa-tree',
|
||||
gen : dedent`
|
||||
 {position:absolute,top:50px,right:30px,width:280px}
|
||||
|
||||
{{artist,top:80px,right:30px
|
||||
##### Homebrew Mug
|
||||
[naturalcrit](https://homebrew.naturalcrit.com)
|
||||
}}`
|
||||
 {position:absolute,top:50px,right:30px,width:280px}`
|
||||
},
|
||||
{
|
||||
name : 'Watercolor Splatter',
|
||||
icon : 'fas fa-fill-drip',
|
||||
gen : watercolorGen,
|
||||
gen : WatercolorGen,
|
||||
},
|
||||
{
|
||||
name : 'Watermark',
|
||||
@@ -164,99 +111,6 @@ module.exports = [
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
/************************* PHB ********************/
|
||||
|
||||
{
|
||||
groupName : 'PHB',
|
||||
icon : 'fas fa-book',
|
||||
view : 'text',
|
||||
snippets : [
|
||||
{
|
||||
name : 'Spell',
|
||||
icon : 'fas fa-magic',
|
||||
gen : MagicGen.spell,
|
||||
},
|
||||
{
|
||||
name : 'Spell List',
|
||||
icon : 'fas fa-scroll',
|
||||
gen : MagicGen.spellList,
|
||||
},
|
||||
{
|
||||
name : 'Class Feature',
|
||||
icon : 'fas fa-mask',
|
||||
gen : ClassFeatureGen,
|
||||
},
|
||||
{
|
||||
name : 'Note',
|
||||
icon : 'fas fa-sticky-note',
|
||||
gen : function(){
|
||||
return dedent`
|
||||
{{note
|
||||
##### Time to Drop Knowledge
|
||||
Use notes to point out some interesting information.
|
||||
|
||||
**Tables and lists** both work within a note.
|
||||
}}
|
||||
\n`;
|
||||
},
|
||||
},
|
||||
{
|
||||
name : 'Descriptive Text Box',
|
||||
icon : 'fas fa-comment-alt',
|
||||
gen : function(){
|
||||
return dedent`
|
||||
{{descriptive
|
||||
##### Time to Drop Knowledge
|
||||
Use descriptive boxes to highlight text that should be read aloud.
|
||||
|
||||
**Tables and lists** both work within a descriptive box.
|
||||
}}
|
||||
\n`;
|
||||
},
|
||||
},
|
||||
{
|
||||
name : 'Monster Stat Block (unframed)',
|
||||
icon : 'fas fa-paw',
|
||||
gen : MonsterBlockGen.monster('monster', 2),
|
||||
},
|
||||
{
|
||||
name : 'Monster Stat Block',
|
||||
icon : 'fas fa-spider',
|
||||
gen : MonsterBlockGen.monster('monster,frame', 2),
|
||||
},
|
||||
{
|
||||
name : 'Wide Monster Stat Block',
|
||||
icon : 'fas fa-dragon',
|
||||
gen : MonsterBlockGen.monster('monster,frame,wide', 4),
|
||||
},
|
||||
{
|
||||
name : 'Cover Page',
|
||||
icon : 'fas fa-file-word',
|
||||
gen : CoverPageGen,
|
||||
},
|
||||
{
|
||||
name : 'Magic Item',
|
||||
icon : 'fas fa-hat-wizard',
|
||||
gen : MagicGen.item,
|
||||
},
|
||||
{
|
||||
name : 'Artist Credit',
|
||||
icon : 'fas fa-signature',
|
||||
gen : function(){
|
||||
return dedent`
|
||||
{{artist,top:90px,right:30px
|
||||
##### Starry Night
|
||||
[Van Gogh](https://www.vangoghmuseum.nl/en)
|
||||
}}
|
||||
\n`;
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
|
||||
/********************* TABLES *********************/
|
||||
|
||||
{
|
||||
@@ -323,43 +177,10 @@ module.exports = [
|
||||
}}
|
||||
\n`;
|
||||
}
|
||||
},
|
||||
{
|
||||
name : 'Class Table',
|
||||
icon : 'fas fa-table',
|
||||
gen : ClassTableGen.full('classTable,frame,decoration,wide'),
|
||||
},
|
||||
{
|
||||
name : 'Class Table (unframed)',
|
||||
icon : 'fas fa-border-none',
|
||||
gen : ClassTableGen.full('classTable,wide'),
|
||||
},
|
||||
{
|
||||
name : '1/2 Class Table',
|
||||
icon : 'fas fa-list-alt',
|
||||
gen : ClassTableGen.half('classTable,decoration,frame'),
|
||||
},
|
||||
{
|
||||
name : '1/2 Class Table (unframed)',
|
||||
icon : 'fas fa-border-none',
|
||||
gen : ClassTableGen.half('classTable'),
|
||||
},
|
||||
{
|
||||
name : '1/3 Class Table',
|
||||
icon : 'fas fa-border-all',
|
||||
gen : ClassTableGen.third('classTable,frame'),
|
||||
},
|
||||
{
|
||||
name : '1/3 Class Table (unframed)',
|
||||
icon : 'fas fa-border-none',
|
||||
gen : ClassTableGen.third('classTable'),
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
/**************** PAGE *************/
|
||||
|
||||
{
|
||||
@@ -392,7 +213,7 @@ module.exports = [
|
||||
icon : 'fas fa-tint',
|
||||
gen : dedent`
|
||||
/* Ink Friendly */
|
||||
*:is(.page,.monster,.note,.descriptive) {
|
||||
*:is(.page) {
|
||||
background : white !important;
|
||||
filter : drop-shadow(0px 0px 3px #888) !important;
|
||||
}
|
||||
@@ -402,6 +223,5 @@ module.exports = [
|
||||
}\n\n`
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
}
|
||||
];
|
||||
5
themes/V3/Blank/snippets/watercolor.gen.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = ()=>{
|
||||
return `{{watercolor${_.random(1, 12)},top:20px,left:30px,width:300px,background-color:#BBAD82,opacity:80%}}\n\n`;
|
||||
};
|
||||
276
themes/V3/Blank/style.less
Normal file
@@ -0,0 +1,276 @@
|
||||
@import (less) './themes/fonts/5e/fonts.less';
|
||||
@import (less) './themes/assets/assets.less';
|
||||
|
||||
:root {
|
||||
//Colors
|
||||
--HB_Color_Background : #FFFFFF; // White
|
||||
--HB_Color_WatercolorStain : #000000; // Black
|
||||
}
|
||||
|
||||
@page { margin: 0; }
|
||||
body {
|
||||
counter-reset : phb-page-numbers;
|
||||
}
|
||||
*{
|
||||
-webkit-print-color-adjust : exact;
|
||||
}
|
||||
|
||||
.useColumns(@multiplier : 1, @fillMode: balance){
|
||||
column-fill : @fillMode;
|
||||
column-count : 2;
|
||||
}
|
||||
.columnWrapper{
|
||||
max-height : 100%;
|
||||
column-span : all;
|
||||
columns : inherit;
|
||||
column-gap : inherit;
|
||||
}
|
||||
.page{
|
||||
.useColumns();
|
||||
height : 279.4mm;
|
||||
width : 215.9mm;
|
||||
padding : 1.4cm 1.9cm 1.7cm;
|
||||
counter-increment : phb-page-numbers;
|
||||
background-color : var(--HB_Color_Background);
|
||||
position : relative;
|
||||
z-index : 15;
|
||||
box-sizing : border-box;
|
||||
overflow : hidden;
|
||||
text-rendering : optimizeLegibility;
|
||||
page-break-before : always;
|
||||
page-break-after : always;
|
||||
contain : size;
|
||||
}
|
||||
//*****************************
|
||||
// * BASE
|
||||
// *****************************/
|
||||
.page{
|
||||
p{
|
||||
overflow-wrap : break-word;
|
||||
display : block;
|
||||
}
|
||||
strong{
|
||||
font-weight : bold;
|
||||
}
|
||||
em{
|
||||
font-style : italic;
|
||||
}
|
||||
sup{
|
||||
vertical-align : super;
|
||||
font-size : smaller;
|
||||
line-height : 0;
|
||||
}
|
||||
sub{
|
||||
vertical-align : sub;
|
||||
font-size : smaller;
|
||||
line-height : 0;
|
||||
}
|
||||
ul {
|
||||
list-style-position : outside; //Needed for multiline list items
|
||||
list-style-type : disc;
|
||||
padding-left : 1.4em;
|
||||
}
|
||||
ol {
|
||||
list-style-position : outside;
|
||||
list-style-type : decimal;
|
||||
padding-left : 1.4em;
|
||||
}
|
||||
img{
|
||||
z-index : -1;
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * HEADERS
|
||||
// *****************************/
|
||||
h1,h2,h3,h4,h5,h6{
|
||||
font-weight : bold;
|
||||
line-height : 1.2em;
|
||||
}
|
||||
h1{
|
||||
font-size : 2em;
|
||||
}
|
||||
h2{
|
||||
font-size : 1.5em;
|
||||
}
|
||||
h3{
|
||||
font-size : 1.17em;
|
||||
}
|
||||
h4{
|
||||
font-size : 1em;
|
||||
}
|
||||
h5{
|
||||
font-size : 0.83em;
|
||||
}
|
||||
//*****************************
|
||||
// * TABLE
|
||||
// *****************************/
|
||||
table{
|
||||
width : 100%;
|
||||
thead{
|
||||
display : table-row-group;
|
||||
font-weight : bold;
|
||||
}
|
||||
}
|
||||
div:not(.columnWrapper) > table + table { // Side-by-side tables should not
|
||||
margin-top : 0; // have vertical spacing.
|
||||
}
|
||||
|
||||
/* Watermark */
|
||||
.watermark {
|
||||
display : grid !important;
|
||||
place-items : center;
|
||||
justify-content : center;
|
||||
position : absolute;
|
||||
margin : 0;
|
||||
top : 0;
|
||||
left : 0;
|
||||
width : 100%;
|
||||
height : 100%;
|
||||
font-size : 120px;
|
||||
text-transform : uppercase;
|
||||
color : black;
|
||||
mix-blend-mode : overlay;
|
||||
opacity : 30%;
|
||||
transform : rotate(-45deg);
|
||||
z-index : 500;
|
||||
p {
|
||||
margin-bottom : none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Watercolor */
|
||||
[class*="watercolor"] {
|
||||
position : absolute;
|
||||
width : 2000px; /* dimensions need to be real big so the user can set */
|
||||
height : 2000px; /* height or width and the image will maintain aspect ratio */
|
||||
-webkit-mask-image : var(--wc);
|
||||
-webkit-mask-size : contain;
|
||||
-webkit-mask-repeat : no-repeat;
|
||||
mask-image : var(--wc);
|
||||
mask-size : contain;
|
||||
mask-repeat : no-repeat;
|
||||
background-size : cover;
|
||||
background-color : var(--HB_Color_WatercolorStain); /*default color*/
|
||||
--wc : @watercolor1; /*default image*/
|
||||
z-index : -2;
|
||||
}
|
||||
|
||||
.watercolor1 { --wc : @watercolor1; }
|
||||
.watercolor2 { --wc : @watercolor2; }
|
||||
.watercolor3 { --wc : @watercolor3; }
|
||||
.watercolor4 { --wc : @watercolor4; }
|
||||
.watercolor5 { --wc : @watercolor5; }
|
||||
.watercolor6 { --wc : @watercolor6; }
|
||||
.watercolor7 { --wc : @watercolor7; }
|
||||
.watercolor8 { --wc : @watercolor8; }
|
||||
.watercolor9 { --wc : @watercolor9; }
|
||||
.watercolor10 { --wc : @watercolor10; }
|
||||
.watercolor11 { --wc : @watercolor11; }
|
||||
.watercolor12 { --wc : @watercolor12; }
|
||||
|
||||
//************************************
|
||||
// * CODE BLOCKS
|
||||
// ************************************/
|
||||
code{
|
||||
font-family : "Courier New", Courier, monospace;
|
||||
white-space : pre-wrap;
|
||||
overflow-wrap : break-word;
|
||||
}
|
||||
|
||||
pre code{
|
||||
width : 100%;
|
||||
display : inline-block;
|
||||
}
|
||||
//*****************************
|
||||
// * EXTRAS
|
||||
// *****************************/
|
||||
.columnSplit {
|
||||
visibility : hidden;
|
||||
-webkit-column-break-after : always;
|
||||
break-after : always;
|
||||
-moz-column-break-after : always;
|
||||
margin-top : 0;
|
||||
& + * {
|
||||
margin-top : 0;
|
||||
}
|
||||
}
|
||||
//Avoid breaking up
|
||||
blockquote,table{
|
||||
z-index : 15;
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
}
|
||||
// Nested lists
|
||||
ul ul,ol ol,ul ol,ol ul{
|
||||
margin-bottom : 0px;
|
||||
margin-left : 1.5em;
|
||||
}
|
||||
li{
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
}
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * MUSTACHE DIVS/SPANS
|
||||
// *****************************/
|
||||
.page {
|
||||
.block {
|
||||
break-inside : avoid;
|
||||
display : inline-block;
|
||||
width : 100%;
|
||||
}
|
||||
.inline-block {
|
||||
display : inline-block;
|
||||
text-indent : initial;
|
||||
}
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * DEFINITION LISTS
|
||||
// *****************************/
|
||||
.page {
|
||||
dl {
|
||||
padding-left : 1em;
|
||||
white-space : pre-line;
|
||||
}
|
||||
dt {
|
||||
display : inline;
|
||||
margin-right : 0.5ch;
|
||||
margin-left : -1em;
|
||||
}
|
||||
dd {
|
||||
display : inline;
|
||||
margin-left : 0;
|
||||
text-indent : 0;
|
||||
}
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * BLANK LINE
|
||||
// *****************************/
|
||||
.page {
|
||||
.blank {
|
||||
height : 1em;
|
||||
margin-top : 0;
|
||||
& + * {
|
||||
margin-top : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * WIDE
|
||||
// *****************************/
|
||||
.page {
|
||||
.wide{
|
||||
column-span : all;
|
||||
display : block;
|
||||
margin-bottom : 1em;
|
||||
&+* {
|
||||
margin-top : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
themes/V3/Journal/dropdownTexture.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
6
themes/V3/Journal/settings.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name" : "Journal",
|
||||
"renderer" : "V3",
|
||||
"baseTheme" : false,
|
||||
"baseSnippets" : "5ePHB"
|
||||
}
|
||||
4
themes/V3/Journal/snippets.js
Normal file
@@ -0,0 +1,4 @@
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
module.exports = [
|
||||
];
|
||||
559
themes/V3/Journal/style.less
Normal file
@@ -0,0 +1,559 @@
|
||||
@import (less) './themes/fonts/Journal/fonts.less';
|
||||
@import (less) './themes/assets/assets.less';
|
||||
|
||||
:root {
|
||||
//Colors
|
||||
--HB_Color_Background : unset; // Light parchment
|
||||
--HB_Color_Text : #412121; // Dark Maroon Brown
|
||||
--HB_Color_Text2 : #261C13CC; //Dark Brown
|
||||
--HB_Color_HeaderText : #58180D; // Dark Maroon
|
||||
--HB_Color_CaptionText : #766649; // Brown
|
||||
--HB_Color_WatercolorStain : #BBAD82; // Light brown
|
||||
}
|
||||
|
||||
.useSansSerif(){
|
||||
font-family : PermanentMarker;
|
||||
font-size : 0.3cm;
|
||||
line-height : 1.2em;
|
||||
color : var(--HB_Color_Text2);
|
||||
p,dl,ul,ol {
|
||||
line-height : 1.2em;
|
||||
}
|
||||
ul, ol {
|
||||
padding-left : 1em;
|
||||
}
|
||||
em{
|
||||
font-style : italic;
|
||||
}
|
||||
strong{
|
||||
font-weight : 800;
|
||||
font-size : 1.1em;
|
||||
}
|
||||
h5 + * {
|
||||
margin-top : 0.1cm;
|
||||
}
|
||||
}
|
||||
.useColumns(@multiplier : 1, @fillMode: balance){
|
||||
column-gap : 0.5cm;
|
||||
}
|
||||
|
||||
.page{
|
||||
background-size : 200% 100%;
|
||||
background-repeat : no-repeat;
|
||||
filter : drop-shadow(1px 4px 14px black);
|
||||
background-image : url(/assets/Journal/Background1.webp);
|
||||
padding : 2.1cm 1.9cm 1.7cm 3.8cm;
|
||||
&:nth-of-type(2n + 1) {
|
||||
background-position : left;
|
||||
}
|
||||
&:nth-of-type(2n) {
|
||||
background-position : right;
|
||||
padding : 2.1cm 3.9cm 1.7cm 1.8cm;
|
||||
}
|
||||
&:nth-of-type(2) {
|
||||
background-image : url(/assets/Journal/Background2.webp); //Only first page should show ribbon
|
||||
}
|
||||
|
||||
& .columnWrapper {
|
||||
//transform: rotate(-0.5deg); // Breaks absolute positioning of images/footers. Wait for Chrome fix
|
||||
}
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * BASE
|
||||
// *****************************/
|
||||
.page{
|
||||
color : var(--HB_Color_Text);
|
||||
font-family : ReenieBeanie;
|
||||
font-size : 0.53cm;
|
||||
line-height : 0.8em;
|
||||
p + * {
|
||||
margin-top : 0.325cm;
|
||||
}
|
||||
p + p{
|
||||
margin-top : 0;
|
||||
}
|
||||
ul{
|
||||
margin-bottom : 0.8em;
|
||||
}
|
||||
ol{
|
||||
margin-bottom : 0.8em;
|
||||
}
|
||||
em{
|
||||
text-decoration : underline;
|
||||
font-style : unset;
|
||||
}
|
||||
del{
|
||||
text-decoration-style: double;
|
||||
}
|
||||
|
||||
//Indents after p or lists
|
||||
p+p, ul+p, ol+p{
|
||||
text-indent : 1em;
|
||||
}
|
||||
//*****************************
|
||||
// * HEADERS
|
||||
// *****************************/
|
||||
h1,h2,h3,h4,h5{
|
||||
font-family : FrederickaTheGreat;
|
||||
font-weight : unset;
|
||||
color : var(--HB_Color_HeaderText);
|
||||
}
|
||||
h1{
|
||||
margin-bottom : 0.18cm; //Margin-bottom only because this is WIDE
|
||||
font-size : 0.89cm;
|
||||
line-height : 1em;
|
||||
font-variant : small-caps;
|
||||
&+p::first-letter{
|
||||
float : left;
|
||||
font-family : FrederickaTheGreat;
|
||||
line-height : 1em;
|
||||
font-size : 1.9em;
|
||||
padding-left : 40px; //Allow background color to extend into margins
|
||||
margin-top : -0.3cm;
|
||||
margin-bottom : -20px;
|
||||
margin-left : -40px;
|
||||
margin-right : 0.1em;
|
||||
padding-top : 0.3em;
|
||||
padding-bottom : 2px;
|
||||
}
|
||||
&+p::first-line{
|
||||
font-variant : small-caps;
|
||||
}
|
||||
}
|
||||
h2{
|
||||
font-size : 0.62cm;
|
||||
line-height : 0.988em; //Font is misaligned. Shift up slightly
|
||||
}
|
||||
h3{
|
||||
font-size : 0.575cm;
|
||||
line-height : 0.995em; //Font is misaligned. Shift up slightly
|
||||
margin-left : -0.9em;
|
||||
}
|
||||
h4{
|
||||
font-size : 0.55cm;
|
||||
line-height : 0.971em; //Font is misaligned. Shift up slightly
|
||||
color : var(--HB_Color_Text);
|
||||
padding-bottom : 5px;
|
||||
transform:rotate(0deg);
|
||||
&:nth-of-type(2n) {
|
||||
transform:rotate(1deg);
|
||||
}
|
||||
&:nth-of-type(3n) {
|
||||
transform:rotate(-1.5deg);
|
||||
}
|
||||
}
|
||||
h5{
|
||||
font-family : PermanentMarker;
|
||||
font-size : 0.4cm;
|
||||
color : var(--HB_Color_Text2);
|
||||
font-weight : bold;
|
||||
line-height : 0.951em; //Font is misaligned. Shift up slightly
|
||||
& + * {
|
||||
margin-top : 0.2cm;
|
||||
}
|
||||
}
|
||||
//*****************************
|
||||
// * TABLE
|
||||
// *****************************/
|
||||
table{
|
||||
.useSansSerif();
|
||||
& + * {
|
||||
margin-top : 0.325cm;
|
||||
}
|
||||
thead{
|
||||
th{
|
||||
vertical-align : bottom;
|
||||
padding : 0.14em 0;
|
||||
}
|
||||
}
|
||||
tbody{
|
||||
tr{
|
||||
td{
|
||||
padding : 0.14em 0;
|
||||
}
|
||||
&:nth-child(odd){
|
||||
background-image : linear-gradient(to left, #41212100, #41212122, #41212100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//*****************************
|
||||
// * NOTE
|
||||
// *****************************/
|
||||
.note{
|
||||
.useSansSerif();
|
||||
border-style : solid;
|
||||
border-width : 1px;
|
||||
border-image-source : url(/assets/Journal/Border1.png);
|
||||
border-image-slice : 18 18 18 18;
|
||||
border-image-width : 6px 6px 6px 6px;
|
||||
border-image-outset : 5px 5px 5px 5px;
|
||||
border-image-repeat : stretch stretch;
|
||||
background-image : url(/assets/Journal/HashMarks.png),
|
||||
linear-gradient(to bottom right, #ff000000, #a36a4e14, #41212100);
|
||||
background-size : 120% 120%;
|
||||
background-repeat : no-repeat;
|
||||
background-position : center;
|
||||
padding : 0.2cm;
|
||||
:where(&) {
|
||||
margin-top : 9px; //Prevent top border getting cut off on colbreak
|
||||
}
|
||||
& + * {
|
||||
margin-top : 0.45cm;
|
||||
}
|
||||
h5 {
|
||||
font-size : 0.375cm;
|
||||
}
|
||||
p{
|
||||
padding-bottom : 0px;
|
||||
}
|
||||
:last-child {
|
||||
margin-bottom : 0;
|
||||
}
|
||||
}
|
||||
//************************************
|
||||
// * DESCRIPTIVE TEXT BOX
|
||||
// ************************************/
|
||||
* + .descriptive {
|
||||
margin-top : 0.6cm;
|
||||
}
|
||||
.descriptive{
|
||||
.useSansSerif();
|
||||
border-style : solid;
|
||||
border-width : 1px;
|
||||
border-image-source : url('/assets/Journal/Border2.png');
|
||||
border-image-slice : 48 48 48 48;
|
||||
border-image-width : 20px;
|
||||
border-image-outset : 16px 20px 16px 20px;
|
||||
border-image-repeat : stretch stretch;
|
||||
background-image : url(/assets/Journal/HashMarks.png),
|
||||
linear-gradient(to bottom right, #ff000000, #41212114, #41212100);
|
||||
background-size : 120% 120%;
|
||||
background-repeat : no-repeat;
|
||||
background-position : center;
|
||||
padding : 0.2cm;
|
||||
:where(&) {
|
||||
margin-top : 4px; //Prevent top border getting cut off on colbreak
|
||||
}
|
||||
& + * {
|
||||
margin-top : 0.45cm;
|
||||
}
|
||||
h5 {
|
||||
font-size : 0.375cm;
|
||||
}
|
||||
p{
|
||||
padding-bottom : 0px;
|
||||
}
|
||||
:last-child {
|
||||
margin-bottom : 0;
|
||||
}
|
||||
}
|
||||
//*****************************
|
||||
// * Images Snippets
|
||||
// *****************************/
|
||||
|
||||
/* Arist Credit */
|
||||
.artist {
|
||||
position : absolute;
|
||||
width : auto;
|
||||
text-align : center;
|
||||
font-family : WalterTurncoat;
|
||||
font-size : 0.27cm;
|
||||
color : var(--HB_Color_CaptionText);
|
||||
p, p + p {
|
||||
margin : unset;
|
||||
text-indent : unset;
|
||||
line-height : 1em;
|
||||
}
|
||||
h5 {
|
||||
font-size : 1.3em;
|
||||
font-family : WalterTurncoat;
|
||||
}
|
||||
a{
|
||||
color : inherit;
|
||||
text-decoration : unset;
|
||||
&:hover {
|
||||
text-decoration : underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * MONSTER STAT BLOCK
|
||||
// *****************************/
|
||||
.monster {
|
||||
.useSansSerif();
|
||||
&.frame {
|
||||
border-style : solid;
|
||||
border-width : 7px 6px;
|
||||
border-image-source : url('/assets/Journal/Border3.png');
|
||||
border-image-slice : 63 74 63 74;
|
||||
border-image-width : 15px 20px 15px 20px;
|
||||
border-image-outset : 12px 12px 12px 12px;
|
||||
border-image-repeat : stretch round;
|
||||
background-image : url('/assets/Journal/HashMarks.png'),
|
||||
linear-gradient(to bottom right, #ff000000, #a36a4e14, #41212100);
|
||||
background-blend-mode : screen multiply;
|
||||
background-size : 100%;
|
||||
padding : 0.2cm;
|
||||
}
|
||||
|
||||
color: var(--HB_Color_Text);
|
||||
position : relative;
|
||||
padding : 0px;
|
||||
margin-bottom : 0.325cm;
|
||||
|
||||
//Headers
|
||||
h2{
|
||||
font-size : 0.62cm;
|
||||
line-height : 1em;
|
||||
margin : 0;
|
||||
&+p {
|
||||
margin-bottom : 0; //Monster size and type subtext
|
||||
}
|
||||
}
|
||||
h3{
|
||||
margin-left : 0;
|
||||
font-variant : small-caps;
|
||||
padding-bottom : 0.05cm;
|
||||
}
|
||||
hr{
|
||||
visibility : visible;
|
||||
height : 6px;
|
||||
margin : 0.12cm 0cm;
|
||||
background-image : url('/assets/Journal/HorizontalRule.png');
|
||||
background-size : 100% 100%;
|
||||
}
|
||||
hr:last-of-type + * {
|
||||
margin-top : 0.325cm; // Space after last HR
|
||||
}
|
||||
|
||||
// Monster Ability table
|
||||
hr + table:first-of-type{
|
||||
margin : 0;
|
||||
column-span : none;
|
||||
background-image : none;
|
||||
border-style : none;
|
||||
border-image : none;
|
||||
color : inherit;
|
||||
tr {
|
||||
background-image : none;
|
||||
}
|
||||
td,th {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
:last-child {
|
||||
margin-bottom : 0;
|
||||
}
|
||||
|
||||
strong, em {
|
||||
font-style : normal;
|
||||
text-decoration : none;
|
||||
}
|
||||
}
|
||||
|
||||
//Full Width
|
||||
.monster.wide{
|
||||
.useColumns(0.96, @fillMode: balance);
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * FOOTER
|
||||
// *****************************/
|
||||
&:nth-child(odd){
|
||||
.pageNumber{
|
||||
left : 3cm;
|
||||
}
|
||||
.footnote{
|
||||
left : 4.5cm;
|
||||
text-align : left;
|
||||
}
|
||||
}
|
||||
.pageNumber{
|
||||
font-family : FrederickaTheGreat;
|
||||
position : absolute;
|
||||
right : 3cm;
|
||||
bottom : 1.25cm;
|
||||
width : 50px;
|
||||
font-size : 0.9em;
|
||||
color : var(--HB_Color_HeaderText);
|
||||
text-align : center;
|
||||
text-indent : 0;
|
||||
&.auto::after {
|
||||
content : counter(phb-page-numbers);
|
||||
}
|
||||
}
|
||||
.footnote{
|
||||
position : absolute;
|
||||
right : 4.5cm;
|
||||
bottom : 1.25cm;
|
||||
z-index : 150;
|
||||
width : 200px;
|
||||
font-size : 0.8em;
|
||||
color : var(--HB_Color_HeaderText);
|
||||
text-align : right;
|
||||
}
|
||||
//************************************
|
||||
// * CODE BLOCKS
|
||||
// ************************************/
|
||||
code{
|
||||
font-size : 0.3cm;
|
||||
padding : 0px 4px;
|
||||
color : var(--HB_Color_Text);
|
||||
vertical-align : middle;
|
||||
background-color : #faf7ea;
|
||||
border-radius : 4px;
|
||||
}
|
||||
|
||||
pre code{
|
||||
border-style : solid;
|
||||
border-width : 1px;
|
||||
border-image : @codeBorderImage 26 stretch;
|
||||
border-image-width : 10px;
|
||||
border-image-outset : 2px;
|
||||
border-radius : 12px;
|
||||
margin-bottom : 2px;
|
||||
padding : 0.15cm;
|
||||
.page :where(&) {
|
||||
margin-top : 2px; //Prevent top border getting cut off on colbreak
|
||||
}
|
||||
& + * {
|
||||
margin-top : 0.325cm;
|
||||
}
|
||||
}
|
||||
//*****************************
|
||||
// * EXTRAS
|
||||
// *****************************/
|
||||
hr{
|
||||
visibility : hidden;
|
||||
border : none;
|
||||
margin : 0px;
|
||||
}
|
||||
//Text indent right after table
|
||||
table+p{
|
||||
text-indent : 1em;
|
||||
}
|
||||
a, a:visited, a:hover {
|
||||
color: var(--HB_Color_Text);
|
||||
transition:all 1s ease;
|
||||
}
|
||||
a:hover {
|
||||
color:red;
|
||||
}
|
||||
}
|
||||
//*****************************
|
||||
// * SPELL LIST
|
||||
// *****************************/
|
||||
|
||||
.page .spellList{
|
||||
.useSansSerif();
|
||||
font-family : PermanentMarker;
|
||||
column-count : 2;
|
||||
ul+h5{
|
||||
margin-top : 15px;
|
||||
}
|
||||
ul{
|
||||
margin-bottom : 0.5em;
|
||||
padding-left : 1em;
|
||||
text-indent : -1em;
|
||||
list-style-type : none;
|
||||
-webkit-column-break-inside : auto;
|
||||
page-break-inside : auto;
|
||||
break-inside : auto;
|
||||
}
|
||||
&.wide{
|
||||
column-count : 4;
|
||||
}
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * CLASS TABLE
|
||||
// *****************************/
|
||||
.page .classTable{
|
||||
th[colspan]:not([rowspan]) {
|
||||
white-space : nowrap;
|
||||
}
|
||||
h5 + table{
|
||||
margin-top : 0.2cm;
|
||||
}
|
||||
}
|
||||
//*****************************
|
||||
// * TABLE OF CONTENTS
|
||||
// *****************************/
|
||||
.page .toc{
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
h1 {
|
||||
text-align : center;
|
||||
margin-bottom : 0.3cm;
|
||||
}
|
||||
a{
|
||||
display : inline;
|
||||
color : inherit;
|
||||
text-decoration : none;
|
||||
&:hover{
|
||||
text-decoration : underline;
|
||||
}
|
||||
}
|
||||
h4 {
|
||||
margin-top : 0.2cm;
|
||||
line-height : 0.4cm;
|
||||
& + ul li {
|
||||
line-height: 1.2em;
|
||||
}
|
||||
}
|
||||
ul{
|
||||
padding-left : 0;
|
||||
list-style-type : none;
|
||||
li + li h3 {
|
||||
margin-top : 0.26cm;
|
||||
line-height : 1em
|
||||
}
|
||||
h3 span:first-child::after {
|
||||
border : none;
|
||||
}
|
||||
span {
|
||||
display : table-cell;
|
||||
&:first-child {
|
||||
position : relative;
|
||||
overflow : hidden;
|
||||
&::after {
|
||||
content : "";
|
||||
position : absolute;
|
||||
bottom : 0.08cm;
|
||||
margin-left : 0.06cm; /* Spacing before dot leaders */
|
||||
width : 100%;
|
||||
border-bottom : 0.05cm dotted #000;
|
||||
}
|
||||
}
|
||||
&:last-child {
|
||||
font-family : ReenieBeanie;
|
||||
font-size : 0.34cm;
|
||||
font-weight : normal;
|
||||
color : black;
|
||||
text-align : right;
|
||||
vertical-align : bottom; /* Keep page number bottom-aligned */
|
||||
width : 1%;
|
||||
padding-left : 0.06cm; /* Spacing after dot leaders */
|
||||
/*white-space : nowrap; /* Uncomment if needed */
|
||||
}
|
||||
}
|
||||
ul { /*List indent*/
|
||||
margin-left : 1em;
|
||||
}
|
||||
}
|
||||
&.wide{
|
||||
.useColumns(0.96, @fillMode: balance);
|
||||
}
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * WIDE
|
||||
// *****************************/
|
||||
.page .wide {
|
||||
margin-bottom : 0.45cm;
|
||||
}
|
||||
BIN
themes/assets/DMG_background.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
themes/assets/DMG_footerAccent.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
themes/assets/Journal/Background1.webp
Normal file
|
After Width: | Height: | Size: 151 KiB |
BIN
themes/assets/Journal/Background2.webp
Normal file
|
After Width: | Height: | Size: 143 KiB |
BIN
themes/assets/Journal/Border1.png
Normal file
|
After Width: | Height: | Size: 14 KiB |